From 1ade02b7ad1c20de775d10c0f0a0c48e0ca25038 Mon Sep 17 00:00:00 2001 From: johannesd3 Date: Sat, 22 May 2021 19:05:13 +0200 Subject: [PATCH 001/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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/561] 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 72b2c01b3ab8cf0c48cd89d450367148ff8b00c4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 26 Oct 2021 20:10:39 +0200 Subject: [PATCH 023/561] Update crates --- Cargo.lock | 312 ++++++++++++++++++++++++++--------------------------- 1 file changed, 151 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1651f794..07f1e23d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "async-trait" @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -164,15 +164,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" dependencies = [ "jobserver", ] @@ -228,13 +228,13 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" dependencies = [ "glob", "libc", - "libloading 0.7.0", + "libloading 0.7.1", ] [[package]] @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" dependencies = [ "bytes", "memchr", @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreaudio-rs" @@ -311,9 +311,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", "futures-sink", @@ -464,15 +464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -481,15 +481,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-macro" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ "autocfg", "proc-macro-hack", @@ -500,21 +500,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-channel", @@ -729,18 +729,18 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" dependencies = [ "base64", "bitflags", "bytes", "headers-core", "http", + "httpdate", "mime", "sha-1", - "time", ] [[package]] @@ -799,9 +799,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -839,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" dependencies = [ "bytes", "futures-channel", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" +checksum = "c9a83ec4af652890ac713ffd8dc859e650420a5ef47f7b9be29b6664ab50fbc8" dependencies = [ "if-addrs-sys", "libc", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -943,15 +943,15 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jack" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e720259b4a3e1f33cba335ca524a99a5f2411d405b05f6405fadd69269e2db" +checksum = "39722b9795ae57c6967da99b1ab009fe72897fcbc59be59508c7c520327d9e34" dependencies = [ "bitflags", "jack-sys", @@ -1001,9 +1001,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -1033,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.99" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" [[package]] name = "libloading" @@ -1049,9 +1049,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" dependencies = [ "cfg-if 1.0.0", "winapi", @@ -1065,9 +1065,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libmdns" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98477a6781ae1d6a1c2aeabfd2e23353a75fe8eb7c2545f6ed282ac8f3e2fc53" +checksum = "fac185a4d02e873c6d1ead59d674651f8ae5ec23ffe1637bee8de80665562a6a" dependencies = [ "byteorder", "futures-util", @@ -1083,9 +1083,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.24.0" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b4154b9bc606019cb15125f96e08e1e9c4f53d55315f1ef69ae229e30d1765" +checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a" dependencies = [ "bitflags", "libc", @@ -1097,9 +1097,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.24.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165af13c42b9c325582b1a75eaa4a0f176c9094bb3a13877826e9be24881231" +checksum = "d6a22538257c4d522bea6089d6478507f5d2589ea32150e20740aaaaaba44590" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1108,9 +1108,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83346d68605e656afdefa9a8a2f1968fa05ab9369b55f2e26f7bf2a11b7e8444" +checksum = "0b8b0fcb9665401cc7c156c337c8edc7eb4e797b9d3ae1667e1e9e17b29e0c7c" dependencies = [ "libpulse-sys", "pkg-config", @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.1" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ebed2cc92c38cac12307892ce6fb17e2e950bfda1ed17b3e1d47fd5184c8f2b" +checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d" dependencies = [ "libc", "num-derive", @@ -1307,9 +1307,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -1350,15 +1350,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1367,9 +1358,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", @@ -1476,15 +1467,14 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.20.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", - "memoffset", ] [[package]] @@ -1586,7 +1576,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "proc-macro-crate 1.0.0", + "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", @@ -1638,9 +1628,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -1649,9 +1639,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if 1.0.0", "instant", @@ -1703,9 +1693,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "portaudio-rs" @@ -1730,9 +1720,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "pretty-hex" @@ -1742,9 +1732,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "priority-queue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1340009a04e81f656a4e45e295f0b1191c81de424bf940c865e33577a8e223" +checksum = "cf40e51ccefb72d42720609e1d3c518de8b5800d723a09358d4a6d6245e1f8ca" dependencies = [ "autocfg", "indexmap", @@ -1761,9 +1751,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ "thiserror", "toml", @@ -1807,33 +1797,33 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b581350bde2d774a19c6f30346796806b8f42b5fd3458c5f9a8623337fb27897" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020f86b07722c5c4291f7c723eac4676b3892d47d9a7708dc2779696407f039b" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" [[package]] name = "protobuf-codegen" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8ac7c5128619b0df145d9bace18e8ed057f18aebda1aa837a5525d4422f68c" +checksum = "3df8c98c08bd4d6653c2dbae00bd68c1d1d82a360265a5b0bbc73d48c63cb853" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d0daa1b61d6e7a128cdca8c8604b3c5ee22c424c15c8d3a92fafffeda18aaf" +checksum = "394a73e2a819405364df8d30042c0f1174737a763e0170497ec9d36f8a2ea8f7" dependencies = [ "protobuf", "protobuf-codegen", @@ -1841,9 +1831,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -2019,18 +2009,18 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -2039,9 +2029,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -2050,9 +2040,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2106,21 +2096,21 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi", @@ -2164,9 +2154,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -2175,9 +2165,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", @@ -2225,18 +2215,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -2255,9 +2245,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -2270,9 +2260,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" dependencies = [ "autocfg", "bytes", @@ -2289,9 +2279,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" dependencies = [ "proc-macro2", "quote", @@ -2311,9 +2301,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" dependencies = [ "bytes", "futures-core", @@ -2340,9 +2330,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2351,9 +2341,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -2366,15 +2356,15 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -2393,9 +2383,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -2476,9 +2466,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2486,9 +2476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -2501,9 +2491,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2511,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -2524,15 +2514,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "web-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", From 52bd212e4357a755fd8b680be47f1ab68c822945 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 26 Oct 2021 22:06:52 -0500 Subject: [PATCH 024/561] Add disable credential cache flag As mentioned in https://github.com/librespot-org/librespot/discussions/870, this allows someone who would otherwise like to take advantage of audio file and volume caching to disable credential caching. --- core/src/cache.rs | 29 +++++++++++++++++++---------- src/main.rs | 17 +++++++++++++---- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/core/src/cache.rs b/core/src/cache.rs index 612b7c39..20270e3e 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -238,29 +238,38 @@ pub struct RemoveFileError(()); impl Cache { pub fn new>( - system_location: Option

, - audio_location: Option

, + credentials: Option

, + volume: Option

, + audio: Option

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

, - volume: Option

, - audio: Option

, + credentials_path: Option

, + volume_path: Option

, + audio_path: Option

, size_limit: Option, ) -> io::Result { let mut size_limiter = None; - if let Some(location) = &credentials { + if let Some(location) = &credentials_path { fs::create_dir_all(location)?; } - let credentials_location = credentials + let credentials_location = credentials_path .as_ref() .map(|p| p.as_ref().join("credentials.json")); - if let Some(location) = &volume { + if let Some(location) = &volume_path { fs::create_dir_all(location)?; } - let volume_location = volume.as_ref().map(|p| p.as_ref().join("volume")); + let volume_location = volume_path.as_ref().map(|p| p.as_ref().join("volume")); - if let Some(location) = &audio { + if let Some(location) = &audio_path { fs::create_dir_all(location)?; if let Some(limit) = size_limit { @@ -269,7 +269,7 @@ impl Cache { } } - let audio_location = audio.map(|p| p.as_ref().to_owned()); + let audio_location = audio_path.map(|p| p.as_ref().to_owned()); let cache = Cache { credentials_location, From 24e4d2b636e40c4adc039ca200d2f4611b502619 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 28 Oct 2021 09:10:10 -0500 Subject: [PATCH 029/561] Prevent librespot from becoming a zombie Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. --- CHANGELOG.md | 3 +++ src/main.rs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678880eb..a8da8d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [cache] Add `disable-credential-cache` flag (breaking). +### Fixed +- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. + ## [0.3.1] - 2021-10-24 ### Changed diff --git a/src/main.rs b/src/main.rs index ae3258a1..51519013 100644 --- a/src/main.rs +++ b/src/main.rs @@ -647,6 +647,11 @@ fn get_setup(args: &[String]) -> Setup { ) }; + if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + error!("Credentials are required if discovery is disabled."); + exit(1); + } + let session_config = { let device_id = device_id(&name); @@ -923,7 +928,8 @@ async fn main() { player_event_channel = Some(event_channel); }, Err(e) => { - warn!("Connection failed: {}", e); + error!("Connection failed: {}", e); + exit(1); } }, _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { From 0e9fdbe6b443c9d55dd9b9148ffc62a8e6d20c5b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 30 Oct 2021 14:22:24 -0500 Subject: [PATCH 030/561] Refactor main.rs * Don't panic when parsing options. Instead list valid values and exit. * Get rid of needless .expect in playback/src/audio_backend/mod.rs. * Enforce reasonable ranges for option values (breaking). * Don't evaluate options that would otherwise have no effect. * Add pub const MIXERS to mixer/mod.rs very similar to the audio_backend's implementation. (non-breaking though) * Use different option descriptions and error messages based on what backends are enabled at build time. * Add a -q, --quiet option that changed the logging level to warn. * Add a short name for every flag and option. * Note removed options. * Other misc cleanups. --- CHANGELOG.md | 13 + core/src/config.rs | 12 + playback/src/audio_backend/mod.rs | 7 +- playback/src/config.rs | 4 +- playback/src/mixer/mod.rs | 18 +- src/main.rs | 1247 +++++++++++++++++++++-------- 6 files changed, 969 insertions(+), 332 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8da8d80..c480e03f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- [main] Enforce reasonable ranges for option values (breaking). +- [main] Don't evaluate options that would otherwise have no effect. + ### Added - [cache] Add `disable-credential-cache` flag (breaking). +- [main] Use different option descriptions and error messages based on what backends are enabled at build time. +- [main] Add a `-q`, `--quiet` option that changes the logging level to warn. +- [main] Add a short name for every flag and option. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. +- [main] Don't panic when parsing options. Instead list valid values and exit. + +### Removed +- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed. ## [0.3.1] - 2021-10-24 diff --git a/core/src/config.rs b/core/src/config.rs index 0e3eaf4a..b8c448c2 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -125,3 +125,15 @@ pub struct ConnectConfig { pub has_volume_ctrl: bool, pub autoplay: bool, } + +impl Default for ConnectConfig { + fn default() -> ConnectConfig { + ConnectConfig { + name: "Librespot".to_string(), + device_type: DeviceType::default(), + initial_volume: Some(50), + has_volume_ctrl: true, + autoplay: false, + } + } +} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index b89232b7..4d3b0171 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -146,11 +146,6 @@ pub fn find(name: Option) -> Option { .find(|backend| name == backend.0) .map(|backend| backend.1) } else { - Some( - BACKENDS - .first() - .expect("No backends were enabled at build time") - .1, - ) + BACKENDS.first().map(|backend| backend.1) } } diff --git a/playback/src/config.rs b/playback/src/config.rs index c442faee..b8313bf4 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -76,7 +76,7 @@ impl AudioFormat { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationType { Album, Track, @@ -101,7 +101,7 @@ impl Default for NormalisationType { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationMethod { Basic, Dynamic, diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 5397598f..a3c7a5a1 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -53,11 +53,19 @@ fn mk_sink(config: MixerConfig) -> Box { Box::new(M::open(config)) } +pub const MIXERS: &[(&str, MixerFn)] = &[ + (SoftMixer::NAME, mk_sink::), // default goes first + #[cfg(feature = "alsa-backend")] + (AlsaMixer::NAME, mk_sink::), +]; + pub fn find(name: Option<&str>) -> Option { - match name { - None | Some(SoftMixer::NAME) => Some(mk_sink::), - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => Some(mk_sink::), - _ => None, + if let Some(name) = name { + MIXERS + .iter() + .find(|mixer| name == mixer.0) + .map(|mixer| mixer.1) + } else { + MIXERS.first().map(|mixer| mixer.1) } } diff --git a/src/main.rs b/src/main.rs index 51519013..990de629 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,15 +19,15 @@ use librespot::playback::config::{ use librespot::playback::dither; #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; -use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; -use librespot::playback::player::{db_to_ratio, Player}; +use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; use std::io::{stderr, Write}; +use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; use std::process::exit; @@ -44,7 +44,7 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } -fn setup_logging(verbose: bool) { +fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { Ok(config) => { @@ -53,21 +53,29 @@ fn setup_logging(verbose: bool) { if verbose { warn!("`--verbose` flag overidden by `RUST_LOG` environment variable"); + } else if quiet { + warn!("`--quiet` flag overidden by `RUST_LOG` environment variable"); } } Err(_) => { if verbose { builder.parse_filters("libmdns=info,librespot=trace"); + } else if quiet { + builder.parse_filters("libmdns=warn,librespot=warn"); } else { builder.parse_filters("libmdns=info,librespot=info"); } builder.init(); + + if verbose && quiet { + warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode."); + } } } } fn list_backends() { - println!("Available backends : "); + println!("Available backends: "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { println!("- {} (default)", name); @@ -194,11 +202,18 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; + const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; + const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; + const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; + const AP_PORT: &str = "ap-port"; const AUTOPLAY: &str = "autoplay"; const BACKEND: &str = "backend"; - const BITRATE: &str = "b"; - const CACHE: &str = "c"; + const BITRATE: &str = "bitrate"; + const CACHE: &str = "cache"; const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; @@ -210,7 +225,7 @@ fn get_setup(args: &[String]) -> Setup { const EMIT_SINK_EVENTS: &str = "emit-sink-events"; const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation"; const FORMAT: &str = "format"; - const HELP: &str = "h"; + const HELP: &str = "help"; const INITIAL_VOLUME: &str = "initial-volume"; const MIXER_TYPE: &str = "mixer"; const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device"; @@ -228,6 +243,7 @@ fn get_setup(args: &[String]) -> Setup { const PASSTHROUGH: &str = "passthrough"; const PASSWORD: &str = "password"; const PROXY: &str = "proxy"; + const QUIET: &str = "quiet"; const SYSTEM_CACHE: &str = "system-cache"; const USERNAME: &str = "username"; const VERBOSE: &str = "verbose"; @@ -236,196 +252,331 @@ fn get_setup(args: &[String]) -> Setup { const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; + // Mostly arbitrary. + const AUTOPLAY_SHORT: &str = "A"; + const AP_PORT_SHORT: &str = "a"; + const BACKEND_SHORT: &str = "B"; + const BITRATE_SHORT: &str = "b"; + const SYSTEM_CACHE_SHORT: &str = "C"; + const CACHE_SHORT: &str = "c"; + const DITHER_SHORT: &str = "D"; + const DEVICE_SHORT: &str = "d"; + const VOLUME_CTRL_SHORT: &str = "E"; + const VOLUME_RANGE_SHORT: &str = "e"; + const DEVICE_TYPE_SHORT: &str = "F"; + const FORMAT_SHORT: &str = "f"; + const DISABLE_AUDIO_CACHE_SHORT: &str = "G"; + const DISABLE_GAPLESS_SHORT: &str = "g"; + const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; + const HELP_SHORT: &str = "h"; + const CACHE_SIZE_LIMIT_SHORT: &str = "M"; + const MIXER_TYPE_SHORT: &str = "m"; + const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; + const NAME_SHORT: &str = "n"; + const DISABLE_DISCOVERY_SHORT: &str = "O"; + const ONEVENT_SHORT: &str = "o"; + const PASSTHROUGH_SHORT: &str = "P"; + const PASSWORD_SHORT: &str = "p"; + const EMIT_SINK_EVENTS_SHORT: &str = "Q"; + const QUIET_SHORT: &str = "q"; + const INITIAL_VOLUME_SHORT: &str = "R"; + const ALSA_MIXER_DEVICE_SHORT: &str = "S"; + const ALSA_MIXER_INDEX_SHORT: &str = "s"; + const ALSA_MIXER_CONTROL_SHORT: &str = "T"; + const NORMALISATION_ATTACK_SHORT: &str = "U"; + const USERNAME_SHORT: &str = "u"; + const VERSION_SHORT: &str = "V"; + const VERBOSE_SHORT: &str = "v"; + const NORMALISATION_GAIN_TYPE_SHORT: &str = "W"; + const NORMALISATION_KNEE_SHORT: &str = "w"; + const NORMALISATION_METHOD_SHORT: &str = "X"; + const PROXY_SHORT: &str = "x"; + const NORMALISATION_PREGAIN_SHORT: &str = "Y"; + const NORMALISATION_RELEASE_SHORT: &str = "y"; + const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; + const ZEROCONF_PORT_SHORT: &str = "z"; + + // Options that have different desc's + // depending on what backends were enabled at build time. + #[cfg(feature = "alsa-backend")] + const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol."; + #[cfg(not(feature = "alsa-backend"))] + const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] + const DEVICE_DESC: &str = "Audio device to use. Use ? to list options if using alsa, portaudio or rodio. Defaults to the backend's default."; + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + const DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_CONTROL_DESC: &str = + "Alsa mixer control, e.g. PCM, Master or similar. Defaults to PCM."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_CONTROL_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_DEVICE_DESC: &str = "Alsa mixer device, e.g hw:0 or similar from `aplay -l`. Defaults to `--device` if specified, default otherwise."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_INDEX_DESC: &str = "Alsa index of the cards mixer. Defaults to 0."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_INDEX_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Default for softvol: 50. For the alsa mixer: the current volume."; + #[cfg(not(feature = "alsa-backend"))] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Defaults to 50."; + #[cfg(feature = "alsa-backend")] + const VOLUME_RANGE_DESC: &str = "Range of the volume control (dB) from 0.0 to 100.0. Default for softvol: 60.0. For the alsa mixer: what the control supports."; + #[cfg(not(feature = "alsa-backend"))] + const VOLUME_RANGE_DESC: &str = + "Range of the volume control (dB) from 0.0 to 100.0. Defaults to 60.0."; + let mut opts = getopts::Options::new(); opts.optflag( + HELP_SHORT, HELP, - "help", "Print this help menu.", - ).optopt( - CACHE, - "cache", - "Path to a directory where files will be cached.", - "PATH", - ).optopt( - "", - SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value.", - "PATH", - ).optopt( - "", - CACHE_SIZE_LIMIT, - "Limits the size of the cache for audio files.", - "SIZE" - ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") - .optflag("", DISABLE_CREDENTIAL_CACHE, "Disable caching of credentials.") - .optopt("n", NAME, "Device name.", "NAME") - .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") + ) + .optflag( + VERSION_SHORT, + VERSION, + "Display librespot version string.", + ) + .optflag( + VERBOSE_SHORT, + VERBOSE, + "Enable verbose log output.", + ) + .optflag( + QUIET_SHORT, + QUIET, + "Only log warning and error messages.", + ) + .optflag( + DISABLE_AUDIO_CACHE_SHORT, + DISABLE_AUDIO_CACHE, + "Disable caching of the audio data.", + ) + .optflag( + DISABLE_CREDENTIAL_CACHE_SHORT, + DISABLE_CREDENTIAL_CACHE, + "Disable caching of credentials.", + ) + .optflag( + DISABLE_DISCOVERY_SHORT, + DISABLE_DISCOVERY, + "Disable zeroconf discovery mode.", + ) + .optflag( + DISABLE_GAPLESS_SHORT, + DISABLE_GAPLESS, + "Disable gapless playback.", + ) + .optflag( + EMIT_SINK_EVENTS_SHORT, + EMIT_SINK_EVENTS, + "Run PROGRAM set by `--onevent` before the sink is opened and after it is closed.", + ) + .optflag( + AUTOPLAY_SHORT, + AUTOPLAY, + "Automatically play similar songs when your music ends.", + ) + .optflag( + PASSTHROUGH_SHORT, + PASSTHROUGH, + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + ) + .optflag( + ENABLE_VOLUME_NORMALISATION_SHORT, + ENABLE_VOLUME_NORMALISATION, + "Play all tracks at approximately the same apparent volume.", + ) .optopt( + NAME_SHORT, + NAME, + "Device name. Defaults to Librespot.", + "NAME", + ) + .optopt( + BITRATE_SHORT, BITRATE, - "bitrate", "Bitrate (kbps) {96|160|320}. Defaults to 160.", "BITRATE", ) .optopt( - "", - ONEVENT, - "Run PROGRAM when a playback event occurs.", - "PROGRAM", - ) - .optflag("", EMIT_SINK_EVENTS, "Run PROGRAM set by --onevent before sink is opened and after it is closed.") - .optflag("v", VERBOSE, "Enable verbose output.") - .optflag("V", VERSION, "Display librespot version string.") - .optopt("u", USERNAME, "Username used to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password used to sign in with.", "PASSWORD") - .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") - .optopt("", AP_PORT, "Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", DISABLE_DISCOVERY, "Disable zeroconf discovery mode.") - .optopt( - "", - BACKEND, - "Audio backend to use. Use '?' to list options.", - "NAME", - ) - .optopt( - "", - DEVICE, - "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default.", - "NAME", - ) - .optopt( - "", + FORMAT_SHORT, FORMAT, "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "FORMAT", ) .optopt( - "", + DITHER_SHORT, DITHER, - "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", + "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to tpdf for formats S16, S24, S24_3 and none for other formats.", "DITHER", ) - .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}. Defaults to softvol", "MIXER") .optopt( - "", - "mixer-name", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_CONTROL, - "Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'.", - "NAME", - ) - .optopt( - "", - "mixer-card", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_DEVICE, - "Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise.", - "DEVICE", - ) - .optopt( - "", - "mixer-index", // deprecated - "", - "", - ) - .optopt( - "", - ALSA_MIXER_INDEX, - "Alsa index of the cards mixer. Defaults to 0.", - "NUMBER", - ) - .optopt( - "", - INITIAL_VOLUME, - "Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.", - "VOLUME", - ) - .optopt( - "", - ZEROCONF_PORT, - "The port the internal server advertises over zeroconf.", - "PORT", - ) - .optflag( - "", - ENABLE_VOLUME_NORMALISATION, - "Play all tracks at approximately the same apparent volume.", - ) - .optopt( - "", - NORMALISATION_METHOD, - "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", - "METHOD", - ) - .optopt( - "", - NORMALISATION_GAIN_TYPE, - "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", + DEVICE_TYPE_SHORT, + DEVICE_TYPE, + "Displayed device type. Defaults to speaker.", "TYPE", ) .optopt( - "", - NORMALISATION_PREGAIN, - "Pregain (dB) applied by volume normalisation. Defaults to 0.", - "PREGAIN", + CACHE_SHORT, + CACHE, + "Path to a directory where files will be cached.", + "PATH", ) .optopt( - "", - NORMALISATION_THRESHOLD, - "Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0.", - "THRESHOLD", + SYSTEM_CACHE_SHORT, + SYSTEM_CACHE, + "Path to a directory where system files (credentials, volume) will be cached. May be different from the `--cache` option value.", + "PATH", ) .optopt( - "", - NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5.", - "TIME", + CACHE_SIZE_LIMIT_SHORT, + CACHE_SIZE_LIMIT, + "Limits the size of the cache for audio files. It's possible to use suffixes like K, M or G, e.g. 16G for example.", + "SIZE" ) .optopt( - "", - NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100.", - "TIME", + BACKEND_SHORT, + BACKEND, + "Audio backend to use. Use ? to list options.", + "NAME", ) .optopt( - "", - NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter. Defaults to 1.0.", - "KNEE", + USERNAME_SHORT, + USERNAME, + "Username used to sign in with.", + "USERNAME", ) .optopt( - "", + PASSWORD_SHORT, + PASSWORD, + "Password used to sign in with.", + "PASSWORD", + ) + .optopt( + ONEVENT_SHORT, + ONEVENT, + "Run PROGRAM when a playback event occurs.", + "PROGRAM", + ) + .optopt( + ALSA_MIXER_CONTROL_SHORT, + ALSA_MIXER_CONTROL, + ALSA_MIXER_CONTROL_DESC, + "NAME", + ) + .optopt( + ALSA_MIXER_DEVICE_SHORT, + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_DESC, + "DEVICE", + ) + .optopt( + ALSA_MIXER_INDEX_SHORT, + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_DESC, + "NUMBER", + ) + .optopt( + MIXER_TYPE_SHORT, + MIXER_TYPE, + MIXER_TYPE_DESC, + "MIXER", + ) + .optopt( + DEVICE_SHORT, + DEVICE, + DEVICE_DESC, + "NAME", + ) + .optopt( + INITIAL_VOLUME_SHORT, + INITIAL_VOLUME, + INITIAL_VOLUME_DESC, + "VOLUME", + ) + .optopt( + VOLUME_CTRL_SHORT, VOLUME_CTRL, "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", "VOLUME_CTRL" ) .optopt( - "", + VOLUME_RANGE_SHORT, VOLUME_RANGE, - "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", + VOLUME_RANGE_DESC, "RANGE", ) - .optflag( - "", - AUTOPLAY, - "Automatically play similar songs when your music ends.", + .optopt( + NORMALISATION_METHOD_SHORT, + NORMALISATION_METHOD, + "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", + "METHOD", ) - .optflag( - "", - DISABLE_GAPLESS, - "Disable gapless playback.", + .optopt( + NORMALISATION_GAIN_TYPE_SHORT, + NORMALISATION_GAIN_TYPE, + "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", + "TYPE", ) - .optflag( - "", - PASSTHROUGH, - "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + .optopt( + NORMALISATION_PREGAIN_SHORT, + NORMALISATION_PREGAIN, + "Pregain (dB) applied by volume normalisation from -10.0 to 10.0. Defaults to 0.0.", + "PREGAIN", + ) + .optopt( + NORMALISATION_THRESHOLD_SHORT, + NORMALISATION_THRESHOLD, + "Threshold (dBFS) at which point the dynamic limiter engages to prevent clipping from 0.0 to -10.0. Defaults to -2.0.", + "THRESHOLD", + ) + .optopt( + NORMALISATION_ATTACK_SHORT, + NORMALISATION_ATTACK, + "Attack time (ms) in which the dynamic limiter reduces gain from 1 to 500. Defaults to 5.", + "TIME", + ) + .optopt( + NORMALISATION_RELEASE_SHORT, + NORMALISATION_RELEASE, + "Release or decay time (ms) in which the dynamic limiter restores gain from 1 to 1000. Defaults to 100.", + "TIME", + ) + .optopt( + NORMALISATION_KNEE_SHORT, + NORMALISATION_KNEE, + "Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", + "KNEE", + ) + .optopt( + ZEROCONF_PORT_SHORT, + ZEROCONF_PORT, + "The port the internal server advertises over zeroconf 1 - 65535. Ports <= 1024 may require root privileges.", + "PORT", + ) + .optopt( + PROXY_SHORT, + PROXY, + "HTTP proxy to use when connecting.", + "URL", + ) + .optopt( + AP_PORT_SHORT, + AP_PORT, + "Connect to an AP with a specified port 1 - 65535. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", + "PORT", ); let matches = match opts.parse(&args[1..]) { @@ -450,110 +601,216 @@ fn get_setup(args: &[String]) -> Setup { exit(0); } - let verbose = matches.opt_present(VERBOSE); - setup_logging(verbose); + setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); info!("{}", get_version_string()); + #[cfg(not(feature = "alsa-backend"))] + for a in &[ + MIXER_TYPE, + ALSA_MIXER_DEVICE, + ALSA_MIXER_INDEX, + ALSA_MIXER_CONTROL, + ] { + if matches.opt_present(a) { + warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); + break; + } + } + let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); } - let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + BACKEND, + BACKEND_SHORT, + matches.opt_str(BACKEND).unwrap_or_default() + ); + list_backends(); + exit(1); + }); let format = matches .opt_str(FORMAT) .as_deref() - .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) + .map(|format| { + AudioFormat::from_str(format).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + println!( + "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", + FORMAT, FORMAT_SHORT + ); + println!("Default: {:?}", AudioFormat::default()); + exit(1); + }) + }) .unwrap_or_default(); + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] let device = matches.opt_str(DEVICE); + + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] if device == Some("?".into()) { backend(device, format); exit(0); } + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + let device: Option = None; + + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + if matches.opt_present(DEVICE) { + warn!( + "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", + DEVICE, DEVICE_SHORT, + ); + } + + #[cfg(feature = "alsa-backend")] let mixer_type = matches.opt_str(MIXER_TYPE); - let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); + #[cfg(not(feature = "alsa-backend"))] + let mixer_type: Option = None; + + let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + MIXER_TYPE, + MIXER_TYPE_SHORT, + matches.opt_str(MIXER_TYPE).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: alsa, softvol", + MIXER_TYPE, MIXER_TYPE_SHORT + ); + println!("Default: softvol"); + exit(1); + }); let mixer_config = { - let mixer_device = match matches.opt_str("mixer-card") { - Some(card) => { - warn!("--mixer-card is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-device instead."); - card - } - None => matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - MixerConfig::default().device - } - }), - }; + let mixer_default_config = MixerConfig::default(); - let index = match matches.opt_str("mixer-index") { - Some(index) => { - warn!("--mixer-index is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-index instead."); - index - .parse::() - .expect("Mixer index is not a valid number") + #[cfg(feature = "alsa-backend")] + let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + if let Some(ref device_name) = device { + device_name.to_string() + } else { + mixer_default_config.device.clone() } - None => matches - .opt_str(ALSA_MIXER_INDEX) - .map(|index| { - index - .parse::() - .expect("Alsa mixer index is not a valid number") + }); + + #[cfg(not(feature = "alsa-backend"))] + let device = mixer_default_config.device; + + #[cfg(feature = "alsa-backend")] + let index = matches + .opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index.parse::().unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + ); + println!("Default: {}", mixer_default_config.index); + exit(1); }) - .unwrap_or(0), - }; + }) + .unwrap_or_else(|| mixer_default_config.index); - let control = match matches.opt_str("mixer-name") { - Some(name) => { - warn!("--mixer-name is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-control instead."); - name - } - None => matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or_else(|| MixerConfig::default().control), - }; + #[cfg(not(feature = "alsa-backend"))] + let index = mixer_default_config.index; - let mut volume_range = matches + #[cfg(feature = "alsa-backend")] + let control = matches + .opt_str(ALSA_MIXER_CONTROL) + .unwrap_or(mixer_default_config.control); + + #[cfg(not(feature = "alsa-backend"))] + let control = mixer_default_config.control; + + let volume_range = matches .opt_str(VOLUME_RANGE) - .map(|range| range.parse::().unwrap()) + .map(|range| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_RANGE, VOLUME_RANGE_SHORT, range + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + VALID_VOLUME_RANGE.start(), + VALID_VOLUME_RANGE.end() + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: softvol - {}, alsa - what the control supports", + VolumeCtrl::DEFAULT_DB_RANGE + ); + #[cfg(not(feature = "alsa-backend"))] + println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + }; + + let range = range.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_VOLUME_RANGE).contains(&range) { + on_error(); + exit(1); + } + + range + }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control + Some(AlsaMixer::NAME) => 0.0, // let alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - if volume_range < 0.0 { - // User might have specified range as minimum dB volume. - volume_range = -volume_range; - warn!( - "Please enter positive volume ranges only, assuming {:.2} dB", - volume_range - ); - } + let volume_ctrl = matches .opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { - VolumeCtrl::from_str_with_range(volume_ctrl, volume_range) - .expect("Invalid volume control type") + VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + ); + println!( + "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", + VOLUME_CTRL, VOLUME_CTRL + ); + println!("Default: log"); + exit(1); + }) }) - .unwrap_or_else(|| { - let mut volume_ctrl = VolumeCtrl::default(); - volume_ctrl.set_db_range(volume_range); - volume_ctrl - }); + .unwrap_or_else(|| VolumeCtrl::Log(volume_range)); MixerConfig { - device: mixer_device, + device, control, index, volume_ctrl, @@ -588,7 +845,10 @@ fn get_setup(args: &[String]) -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - eprintln!("Invalid argument passed as cache size limit: {}", e); + error!( + "Invalid `--{}` / `-{}`: {}", + CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + ); exit(1); }) }) @@ -596,6 +856,13 @@ fn get_setup(args: &[String]) -> Setup { None }; + if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + warn!( + "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", + CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT + ); + } + match Cache::new(cred_dir, volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { @@ -605,31 +872,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let initial_volume = matches - .opt_str(INITIAL_VOLUME) - .map(|initial_volume| { - let volume = initial_volume.parse::().unwrap(); - if volume > 100 { - error!("Initial volume must be in the range 0-100."); - // the cast will saturate, not necessary to take further action - } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 - }) - .or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), - }); - - let zeroconf_port = matches - .opt_str(ZEROCONF_PORT) - .map(|port| port.parse::().unwrap()) - .unwrap_or(0); - - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| "Librespot".to_string()); - let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); @@ -647,13 +889,131 @@ fn get_setup(args: &[String]) -> Setup { ) }; - if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + + if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } + if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + warn!( + "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", + DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + } + + let zeroconf_port = if enable_discovery { + matches + .opt_str(ZEROCONF_PORT) + .map(|port| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port + ); + println!( + "Valid `--{}` / `-{}` values: 1 - 65535", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }) + .unwrap_or(0) + } else { + 0 + }; + + let connect_config = { + let connect_default_config = ConnectConfig::default(); + + let name = matches + .opt_str(NAME) + .unwrap_or_else(|| connect_default_config.name.clone()); + + let initial_volume = matches + .opt_str(INITIAL_VOLUME) + .map(|initial_volume| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume + ); + println!( + "Valid `--{}` / `-{}` values: 0 - 100", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: {}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); + #[cfg(not(feature = "alsa-backend"))] + println!( + "Default: {}", + connect_default_config.initial_volume.unwrap_or_default() + ); + }; + + let volume = initial_volume.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if volume > 100 { + on_error(); + exit(1); + } + + (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 + }) + .or_else(|| match mixer_type.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => None, + _ => cache.as_ref().and_then(Cache::volume), + }); + + let device_type = matches + .opt_str(DEVICE_TYPE) + .as_deref() + .map(|device_type| { + DeviceType::from_str(device_type).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", + DEVICE_TYPE, DEVICE_TYPE_SHORT + ); + println!("Default: speaker"); + exit(1); + }) + }) + .unwrap_or_default(); + + let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); + let autoplay = matches.opt_present(AUTOPLAY); + + ConnectConfig { + name, + device_type, + initial_volume, + has_volume_ctrl, + autoplay, + } + }; + let session_config = { - let device_id = device_id(&name); + let device_id = device_id(&connect_config.name); SessionConfig { user_agent: version::VERSION_STRING.to_string(), @@ -663,78 +1023,329 @@ fn get_setup(args: &[String]) -> Setup { match Url::parse(&s) { Ok(url) => { if url.host().is_none() || url.port_or_known_default().is_none() { - panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + exit(1); } if url.scheme() != "http" { - panic!("Only unsecure http:// proxies are supported"); + error!("Only unsecure http:// proxies are supported"); + exit(1); } + url }, - Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err) + Err(e) => { + error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + exit(1); + } } }, ), ap_port: matches .opt_str(AP_PORT) - .map(|port| port.parse::().expect("Invalid port")), + .map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }), } }; let player_config = { + let player_default_config = PlayerConfig::default(); + let bitrate = matches .opt_str(BITRATE) .as_deref() - .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) - .unwrap_or_default(); + .map(|bitrate| { + Bitrate::from_str(bitrate).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + BITRATE, BITRATE_SHORT, bitrate + ); + println!( + "Valid `--{}` / `-{}` values: 96, 160, 320", + BITRATE, BITRATE_SHORT + ); + println!("Default: 160"); + exit(1); + }) + }) + .unwrap_or(player_default_config.bitrate); let gapless = !matches.opt_present(DISABLE_GAPLESS); let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); - let normalisation_method = matches - .opt_str(NORMALISATION_METHOD) - .as_deref() - .map(|method| { - NormalisationMethod::from_str(method).expect("Invalid normalisation method") - }) - .unwrap_or_default(); - let normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) - .as_deref() - .map(|gain_type| { - NormalisationType::from_str(gain_type).expect("Invalid normalisation type") - }) - .unwrap_or_default(); - let normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) - .unwrap_or(PlayerConfig::default().normalisation_pregain); - let normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - db_to_ratio( - threshold - .parse::() - .expect("Invalid threshold float value"), - ) - }) - .unwrap_or(PlayerConfig::default().normalisation_threshold); - let normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) - .map(|attack| { - Duration::from_millis(attack.parse::().expect("Invalid attack value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_attack); - let normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) - .map(|release| { - Duration::from_millis(release.parse::().expect("Invalid release value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_release); - let normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) - .map(|knee| knee.parse::().expect("Invalid knee float value")) - .unwrap_or(PlayerConfig::default().normalisation_knee); + + let normalisation_method; + let normalisation_type; + let normalisation_pregain; + let normalisation_threshold; + let normalisation_attack; + let normalisation_release; + let normalisation_knee; + + if !normalisation { + for a in &[ + NORMALISATION_METHOD, + NORMALISATION_GAIN_TYPE, + NORMALISATION_PREGAIN, + NORMALISATION_THRESHOLD, + NORMALISATION_ATTACK, + NORMALISATION_RELEASE, + NORMALISATION_KNEE, + ] { + if matches.opt_present(a) { + warn!( + "Without the `--{}` / `-{}` flag normalisation options have no effect.", + ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, + ); + break; + } + } + + normalisation_method = player_default_config.normalisation_method; + normalisation_type = player_default_config.normalisation_type; + normalisation_pregain = player_default_config.normalisation_pregain; + normalisation_threshold = player_default_config.normalisation_threshold; + normalisation_attack = player_default_config.normalisation_attack; + normalisation_release = player_default_config.normalisation_release; + normalisation_knee = player_default_config.normalisation_knee; + } else { + normalisation_method = matches + .opt_str(NORMALISATION_METHOD) + .as_deref() + .map(|method| { + warn!( + "`--{}` / `-{}` will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + + let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + println!( + "Valid `--{}` / `-{}` values: basic, dynamic", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); + }); + + if matches!(method, NormalisationMethod::Basic) { + warn!( + "`--{}` / `-{}` {:?} will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + } + + method + }) + .unwrap_or(player_default_config.normalisation_method); + + normalisation_type = matches + .opt_str(NORMALISATION_GAIN_TYPE) + .as_deref() + .map(|gain_type| { + NormalisationType::from_str(gain_type).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type + ); + println!( + "Valid `--{}` / `-{}` values: track, album, auto", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, + ); + println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); + }) + }) + .unwrap_or(player_default_config.normalisation_type); + + normalisation_pregain = matches + .opt_str(NORMALISATION_PREGAIN) + .map(|pregain| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_PREGAIN, + NORMALISATION_PREGAIN_SHORT, + VALID_NORMALISATION_PREGAIN_RANGE.start(), + VALID_NORMALISATION_PREGAIN_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_pregain); + }; + + let pregain = pregain.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { + on_error(); + exit(1); + } + + pregain + }) + .unwrap_or(player_default_config.normalisation_pregain); + + normalisation_threshold = matches + .opt_str(NORMALISATION_THRESHOLD) + .map(|threshold| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + VALID_NORMALISATION_THRESHOLD_RANGE.start(), + VALID_NORMALISATION_THRESHOLD_RANGE.end() + ); + println!( + "Default: {}", + ratio_to_db(player_default_config.normalisation_threshold) + ); + }; + + let threshold = threshold.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { + on_error(); + exit(1); + } + + db_to_ratio(threshold) + }) + .unwrap_or(player_default_config.normalisation_threshold); + + normalisation_attack = matches + .opt_str(NORMALISATION_ATTACK) + .map(|attack| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + VALID_NORMALISATION_ATTACK_RANGE.start(), + VALID_NORMALISATION_ATTACK_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_attack.as_millis() + ); + }; + + let attack = attack.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { + on_error(); + exit(1); + } + + Duration::from_millis(attack) + }) + .unwrap_or(player_default_config.normalisation_attack); + + normalisation_release = matches + .opt_str(NORMALISATION_RELEASE) + .map(|release| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + VALID_NORMALISATION_RELEASE_RANGE.start(), + VALID_NORMALISATION_RELEASE_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_release.as_millis() + ); + }; + + let release = release.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { + on_error(); + exit(1); + } + + Duration::from_millis(release) + }) + .unwrap_or(player_default_config.normalisation_release); + + normalisation_knee = matches + .opt_str(NORMALISATION_KNEE) + .map(|knee| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_KNEE, + NORMALISATION_KNEE_SHORT, + VALID_NORMALISATION_KNEE_RANGE.start(), + VALID_NORMALISATION_KNEE_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_knee); + }; + + let knee = knee.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { + on_error(); + exit(1); + } + + knee + }) + .unwrap_or(player_default_config.normalisation_knee); + } let ditherer_name = matches.opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { @@ -742,15 +1353,32 @@ fn get_setup(args: &[String]) -> Setup { Some("none") => None, // explicitly set on command line Some(_) => { - if format == AudioFormat::F64 || format == AudioFormat::F32 { - unimplemented!("Dithering is not available on format {:?}", format); + if matches!(format, AudioFormat::F64 | AudioFormat::F32) { + error!("Dithering is not available with format: {:?}.", format); + exit(1); } - Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) + + Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + DITHER, + DITHER_SHORT, + matches.opt_str(DITHER).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", + DITHER, DITHER_SHORT + ); + println!( + "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + ); + exit(1); + })) } // nothing set on command line => use default None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - PlayerConfig::default().ditherer + player_default_config.ditherer } _ => None, }, @@ -774,25 +1402,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let connect_config = { - let device_type = matches - .opt_str(DEVICE_TYPE) - .as_deref() - .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) - .unwrap_or_default(); - let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); - - ConnectConfig { - name, - device_type, - initial_volume, - has_volume_ctrl, - autoplay, - } - }; - - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); let player_event_program = matches.opt_str(ONEVENT); let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); From 3016d6fbdb867eeb9cff7aa19fe61fa29ea13b72 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 17 Nov 2021 21:15:35 -0600 Subject: [PATCH 031/561] Guard against tracks_len being zero to prevent 'index out of bounds: the len is 0 but the index is 0' https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 --- connect/src/spirc.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d644e2b0..344f63b7 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1140,6 +1140,14 @@ impl SpircTask { fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { let tracks_len = self.state.get_track().len(); + // Guard against tracks_len being zero to prevent + // 'index out of bounds: the len is 0 but the index is 0' + // https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 + if tracks_len == 0 { + warn!("No playable track found in state: {:?}", self.state); + return None; + } + let mut new_playlist_index = index as usize; if new_playlist_index >= tracks_len { From c006a2364452a83584afc7b50e9714c4c71d24c7 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 19 Nov 2021 16:45:13 -0600 Subject: [PATCH 032/561] Improve `--device ?` functionality for the alsa backend This makes `--device ?` only show compatible devices (ones that support 2 ch 44.1 Interleaved) and it shows what `librespot` format(s) they support. This should be more useful to users as the info maps directly to `librespot`'s `--device` and `--format` options. --- CHANGELOG.md | 1 + playback/src/audio_backend/alsa.rs | 86 +++++++++++++++++++++--------- 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c480e03f..fb800c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. +- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9dd3ea0c..e572f953 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -80,6 +80,23 @@ impl From for SinkError { } } +impl From for Format { + fn from(f: AudioFormat) -> Format { + use AudioFormat::*; + match f { + F64 => Format::float64(), + F32 => Format::float(), + S32 => Format::s32(), + S24 => Format::s24(), + S16 => Format::s16(), + #[cfg(target_endian = "little")] + S24_3 => Format::S243LE, + #[cfg(target_endian = "big")] + S24_3 => Format::S243BE, + } + } +} + pub struct AlsaSink { pcm: Option, format: AudioFormat, @@ -87,20 +104,50 @@ pub struct AlsaSink { period_buffer: Vec, } -fn list_outputs() -> SinkResult<()> { - println!("Listing available Alsa outputs:"); - for t in &["pcm", "ctl", "hwdep"] { - println!("{} devices:", t); +fn list_compatible_devices() -> SinkResult<()> { + println!("\n\n\tCompatible alsa device(s):\n"); + println!("\t------------------------------------------------------\n"); - let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?; + let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?; - for a in i { - if let Some(Direction::Playback) = a.direction { - // mimic aplay -L - let name = a.name.ok_or(AlsaError::Parsing)?; - let desc = a.desc.ok_or(AlsaError::Parsing)?; + for a in i { + if let Some(Direction::Playback) = a.direction { + let name = a.name.ok_or(AlsaError::Parsing)?; + let desc = a.desc.ok_or(AlsaError::Parsing)?; - println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + // Only show devices that support + // 2 ch 44.1 Interleaved. + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + println!("\tDevice:\n\n\t\t{}\n", name); + println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t")); + + let mut supported_formats = vec![]; + + for f in &[ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, + ] { + if hwp.test_format(Format::from(*f)).is_ok() { + supported_formats.push(format!("{:?}", f)); + } + } + + println!( + "\tSupported Format(s):\n\n\t\t{}\n", + supported_formats.join(" ") + ); + println!("\t------------------------------------------------------\n"); + } + }; } } } @@ -114,19 +161,6 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - let alsa_format = match format { - AudioFormat::F64 => Format::float64(), - AudioFormat::F32 => Format::float(), - AudioFormat::S32 => Format::s32(), - AudioFormat::S24 => Format::s24(), - AudioFormat::S16 => Format::s16(), - - #[cfg(target_endian = "little")] - AudioFormat::S24_3 => Format::S243LE, - #[cfg(target_endian = "big")] - AudioFormat::S24_3 => Format::S243BE, - }; - let bytes_per_period = { let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; @@ -136,6 +170,8 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; + let alsa_format = Format::from(format); + hwp.set_format(alsa_format) .map_err(|e| AlsaError::UnsupportedFormat { device: dev_name.to_string(), @@ -194,7 +230,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { let name = match device.as_deref() { - Some("?") => match list_outputs() { + Some("?") => match list_compatible_devices() { Ok(_) => { exit(0); } From bbd575ed23cf9e27a1b43007875568fba8458694 Mon Sep 17 00:00:00 2001 From: Tom Vincent Date: Fri, 26 Nov 2021 18:49:50 +0000 Subject: [PATCH 033/561] Harden systemd service, update restart policy (#888) --- CHANGELOG.md | 1 + contrib/librespot.service | 8 ++++---- contrib/librespot.user.service | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb800c00..7ffd99cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. +- [contrib] Hardened security of the systemd service units ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/contrib/librespot.service b/contrib/librespot.service index 76037c8c..2c92a149 100644 --- a/contrib/librespot.service +++ b/contrib/librespot.service @@ -2,12 +2,12 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options -Requires=network-online.target -After=network-online.target +Wants=network.target sound.target +After=network.target sound.target [Service] -User=nobody -Group=audio +DynamicUser=yes +SupplementaryGroups=audio Restart=always RestartSec=10 ExecStart=/usr/bin/librespot --name "%p@%H" diff --git a/contrib/librespot.user.service b/contrib/librespot.user.service index a676dde0..36f7f8c9 100644 --- a/contrib/librespot.user.service +++ b/contrib/librespot.user.service @@ -2,6 +2,8 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options +Wants=network.target sound.target +After=network.target sound.target [Service] Restart=always From 56585cabb6e4527ef3cd9f621a239d58550d42c7 Mon Sep 17 00:00:00 2001 From: Nick Botticelli Date: Tue, 21 Sep 2021 01:18:58 -0700 Subject: [PATCH 034/561] 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 035/561] 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 036/561] 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 037/561] 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 038/561] 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 039/561] Update tokio and fix build --- Cargo.lock | 24 ++-- playback/Cargo.toml | 2 +- playback/src/decoder/symphonia_decoder.rs | 136 ++++++++++++++++++++++ 3 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 playback/src/decoder/symphonia_decoder.rs diff --git a/Cargo.lock b/Cargo.lock index 7eddf8df..57e50c03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1808,18 +1808,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7509cc106041c40a4518d2af7a61530e1eed0e6285296a3d8c5472806ccc4a4" +checksum = "576bc800220cc65dac09e99e97b08b358cfab6e17078de8dc5fee223bd2d0c08" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c950132583b500556b1efd71d45b319029f2b71518d979fcc208e16b42426f" +checksum = "6e8fe8163d14ce7f0cdac2e040116f22eac817edabff0be91e8aff7e9accf389" dependencies = [ "proc-macro2", "quote", @@ -2498,9 +2498,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.6.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd3076b5c8cc18138b8f8814895c11eb4de37114a5d127bafdc5e55798ceef37" +checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" dependencies = [ "autocfg", "bytes", @@ -2517,9 +2517,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", @@ -2539,9 +2539,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite", @@ -2567,9 +2567,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" dependencies = [ "bytes", "futures-core", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 0bed793c..96b3649a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -23,7 +23,7 @@ futures-util = { version = "0.3", default_features = false, features = ["alloc"] log = "0.4" byteorder = "1.4" shell-words = "1.0.0" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } zerocopy = { version = "0.3" } # Backends diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs new file mode 100644 index 00000000..309c495d --- /dev/null +++ b/playback/src/decoder/symphonia_decoder.rs @@ -0,0 +1,136 @@ +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; + +use crate::audio::AudioFile; + +use symphonia::core::audio::{AudioBufferRef, Channels}; +use symphonia::core::codecs::Decoder; +use symphonia::core::errors::Error as SymphoniaError; +use symphonia::core::formats::{FormatReader, SeekMode, SeekTo}; +use symphonia::core::io::{MediaSource, MediaSourceStream}; +use symphonia::core::units::TimeStamp; +use symphonia::default::{codecs::VorbisDecoder, formats::OggReader}; + +use std::io::{Read, Seek, SeekFrom}; + +impl MediaSource for FileWithConstSize +where + R: Read + Seek + Send, +{ + fn is_seekable(&self) -> bool { + true + } + + fn byte_len(&self) -> Option { + Some(self.len()) + } +} + +pub struct FileWithConstSize { + stream: T, + len: u64, +} + +impl FileWithConstSize { + pub fn len(&self) -> u64 { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } +} + +impl FileWithConstSize +where + T: Seek, +{ + pub fn new(mut stream: T) -> Self { + stream.seek(SeekFrom::End(0)).unwrap(); + let len = stream.stream_position().unwrap(); + stream.seek(SeekFrom::Start(0)).unwrap(); + Self { stream, len } + } +} + +impl Read for FileWithConstSize +where + T: Read, +{ + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.stream.read(buf) + } +} + +impl Seek for FileWithConstSize +where + T: Seek, +{ + fn seek(&mut self, pos: SeekFrom) -> std::io::Result { + self.stream.seek(pos) + } +} + +pub struct SymphoniaDecoder { + track_id: u32, + decoder: Box, + format: Box, + position: TimeStamp, +} + +impl SymphoniaDecoder { + pub fn new(input: R) -> DecoderResult + where + R: Read + Seek, + { + let mss_opts = Default::default(); + let mss = MediaSourceStream::new(Box::new(FileWithConstSize::new(input)), mss_opts); + + let format_opts = Default::default(); + let format = OggReader::try_new(mss, &format_opts).map_err(|e| DecoderError::SymphoniaDecoder(e.to_string()))?; + + let track = format.default_track().unwrap(); + let decoder_opts = Default::default(); + let decoder = VorbisDecoder::try_new(&track.codec_params, &decoder_opts)?; + + Ok(Self { + track_id: track.id, + decoder: Box::new(decoder), + format: Box::new(format), + position: 0, + }) + } +} + +impl AudioDecoder for SymphoniaDecoder { + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + let seeked_to = self.format.seek( + SeekMode::Accurate, + SeekTo::Time { + time: absgp, // TODO : move to Duration + track_id: Some(self.track_id), + }, + )?; + self.position = seeked_to.actual_ts; + // TODO : Ok(self.position) + Ok(()) + } + + fn next_packet(&mut self) -> DecoderResult> { + let packet = match self.format.next_packet() { + Ok(packet) => packet, + Err(e) => { + log::error!("format error: {}", err); + return Err(DecoderError::SymphoniaDecoder(e.to_string())), + } + }; + match self.decoder.decode(&packet) { + Ok(audio_buf) => { + self.position += packet.frames() as TimeStamp; + Ok(Some(packet)) + } + // TODO: Handle non-fatal decoding errors and retry. + Err(e) => + return Err(DecoderError::SymphoniaDecoder(e.to_string())), + } + } +} From e66cc5508cee0413829aa347c7a31bd0293eb856 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Wed, 1 Dec 2021 14:29:58 -0600 Subject: [PATCH 040/561] parse environment variables (#886) Make librespot able to parse environment variables for options and flags. To avoid name collisions environment variables must be prepended with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. Verbose logging mode (`-v`, `--verbose`) logs all parsed environment variables and command line arguments (credentials are redacted). --- CHANGELOG.md | 2 + src/main.rs | 206 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 134 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ffd99cf..c5757aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. - [contrib] Hardened security of the systemd service units +- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). ### Added - [cache] Add `disable-credential-cache` flag (breaking). - [main] Use different option descriptions and error messages based on what backends are enabled at build time. - [main] Add a `-q`, `--quiet` option that changes the logging level to warn. - [main] Add a short name for every flag and option. +- [main] Add the ability to parse environment variables. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. diff --git a/src/main.rs b/src/main.rs index 990de629..2dec56ae 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; -use log::{error, info, warn}; +use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::sync::mpsc::UnboundedReceiver; @@ -44,6 +44,23 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } +fn arg_to_var(arg: &str) -> String { + // To avoid name collisions environment variables must be prepended + // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. + format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) +} + +fn env_var_present(arg: &str) -> bool { + env::var(arg_to_var(arg)).is_ok() +} + +fn env_var_opt_str(option: &str) -> Option { + match env::var(arg_to_var(option)) { + Ok(value) => Some(value), + Err(_) => None, + } +} + fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -591,20 +608,84 @@ fn get_setup(args: &[String]) -> Setup { } }; - if matches.opt_present(HELP) { + let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + + let opt_str = |opt| { + if matches.opt_present(opt) { + matches.opt_str(opt) + } else { + env_var_opt_str(opt) + } + }; + + if opt_present(HELP) { println!("{}", usage(&args[0], &opts)); exit(0); } - if matches.opt_present(VERSION) { + if opt_present(VERSION) { println!("{}", get_version_string()); exit(0); } - setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); + setup_logging(opt_present(QUIET), opt_present(VERBOSE)); info!("{}", get_version_string()); + let librespot_env_vars: Vec = env::vars_os() + .filter_map(|(k, v)| { + let mut env_var = None; + if let Some(key) = k.to_str() { + if key.starts_with("LIBRESPOT_") { + if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + // Don't log creds. + env_var = Some(format!("\t\t{}=XXXXXXXX", key)); + } else if let Some(value) = v.to_str() { + env_var = Some(format!("\t\t{}={}", key, value)); + } + } + } + + env_var + }) + .collect(); + + if !librespot_env_vars.is_empty() { + trace!("Environment variable(s):"); + + for kv in librespot_env_vars { + trace!("{}", kv); + } + } + + let cmd_args = &args[1..]; + + let cmd_args_len = cmd_args.len(); + + if cmd_args_len > 0 { + trace!("Command line argument(s):"); + + for (index, key) in cmd_args.iter().enumerate() { + if key.starts_with('-') || key.starts_with("--") { + if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { + // Don't log creds. + trace!("\t\t{} XXXXXXXX", key); + } else { + let mut value = "".to_string(); + let next = index + 1; + if next < cmd_args_len { + let next_key = cmd_args[next].clone(); + if !next_key.starts_with('-') && !next_key.starts_with("--") { + value = next_key; + } + } + + trace!("\t\t{} {}", key, value); + } + } + } + } + #[cfg(not(feature = "alsa-backend"))] for a in &[ MIXER_TYPE, @@ -612,13 +693,13 @@ fn get_setup(args: &[String]) -> Setup { ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); break; } } - let backend_name = matches.opt_str(BACKEND); + let backend_name = opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -629,14 +710,13 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", BACKEND, BACKEND_SHORT, - matches.opt_str(BACKEND).unwrap_or_default() + opt_str(BACKEND).unwrap_or_default() ); list_backends(); exit(1); }); - let format = matches - .opt_str(FORMAT) + let format = opt_str(FORMAT) .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { @@ -656,7 +736,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - let device = matches.opt_str(DEVICE); + let device = opt_str(DEVICE); #[cfg(any( feature = "alsa-backend", @@ -680,7 +760,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" )))] - if matches.opt_present(DEVICE) { + if opt_present(DEVICE) { warn!( "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", DEVICE, DEVICE_SHORT, @@ -688,7 +768,7 @@ fn get_setup(args: &[String]) -> Setup { } #[cfg(feature = "alsa-backend")] - let mixer_type = matches.opt_str(MIXER_TYPE); + let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] let mixer_type: Option = None; @@ -697,7 +777,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", MIXER_TYPE, MIXER_TYPE_SHORT, - matches.opt_str(MIXER_TYPE).unwrap_or_default() + opt_str(MIXER_TYPE).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: alsa, softvol", @@ -711,7 +791,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer_default_config = MixerConfig::default(); #[cfg(feature = "alsa-backend")] - let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + let device = opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { if let Some(ref device_name) = device { device_name.to_string() } else { @@ -723,8 +803,7 @@ fn get_setup(args: &[String]) -> Setup { let device = mixer_default_config.device; #[cfg(feature = "alsa-backend")] - let index = matches - .opt_str(ALSA_MIXER_INDEX) + let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { error!( @@ -741,15 +820,12 @@ fn get_setup(args: &[String]) -> Setup { let index = mixer_default_config.index; #[cfg(feature = "alsa-backend")] - let control = matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or(mixer_default_config.control); + let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; - let volume_range = matches - .opt_str(VOLUME_RANGE) + let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { error!( @@ -790,8 +866,7 @@ fn get_setup(args: &[String]) -> Setup { _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - let volume_ctrl = matches - .opt_str(VOLUME_CTRL) + let volume_ctrl = opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { @@ -818,29 +893,26 @@ fn get_setup(args: &[String]) -> Setup { }; let cache = { - let volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) + let volume_dir = opt_str(SYSTEM_CACHE) + .or_else(|| opt_str(CACHE)) .map(|p| p.into()); - let cred_dir = if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + let cred_dir = if opt_present(DISABLE_CREDENTIAL_CACHE) { None } else { volume_dir.clone() }; - let audio_dir = if matches.opt_present(DISABLE_AUDIO_CACHE) { + let audio_dir = if opt_present(DISABLE_AUDIO_CACHE) { None } else { - matches - .opt_str(CACHE) + opt_str(CACHE) .as_ref() .map(|p| AsRef::::as_ref(p).join("files")) }; let limit = if audio_dir.is_some() { - matches - .opt_str(CACHE_SIZE_LIMIT) + opt_str(CACHE_SIZE_LIMIT) .as_deref() .map(parse_file_size) .map(|e| { @@ -856,7 +928,7 @@ fn get_setup(args: &[String]) -> Setup { None }; - if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + if audio_dir.is_none() && opt_present(CACHE_SIZE_LIMIT) { warn!( "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT @@ -882,21 +954,21 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str(USERNAME), - matches.opt_str(PASSWORD), + opt_str(USERNAME), + opt_str(PASSWORD), cached_credentials, password, ) }; - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + let enable_discovery = !opt_present(DISABLE_DISCOVERY); if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } - if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + if !enable_discovery && opt_present(ZEROCONF_PORT) { warn!( "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT @@ -904,8 +976,7 @@ fn get_setup(args: &[String]) -> Setup { } let zeroconf_port = if enable_discovery { - matches - .opt_str(ZEROCONF_PORT) + opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { error!( @@ -938,12 +1009,9 @@ fn get_setup(args: &[String]) -> Setup { let connect_config = { let connect_default_config = ConnectConfig::default(); - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| connect_default_config.name.clone()); + let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); - let initial_volume = matches - .opt_str(INITIAL_VOLUME) + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( @@ -984,8 +1052,7 @@ fn get_setup(args: &[String]) -> Setup { _ => cache.as_ref().and_then(Cache::volume), }); - let device_type = matches - .opt_str(DEVICE_TYPE) + let device_type = opt_str(DEVICE_TYPE) .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { @@ -1001,7 +1068,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or_default(); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); + let autoplay = opt_present(AUTOPLAY); ConnectConfig { name, @@ -1018,7 +1085,7 @@ fn get_setup(args: &[String]) -> Setup { SessionConfig { user_agent: version::VERSION_STRING.to_string(), device_id, - proxy: matches.opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( |s| { match Url::parse(&s) { Ok(url) => { @@ -1041,8 +1108,7 @@ fn get_setup(args: &[String]) -> Setup { } }, ), - ap_port: matches - .opt_str(AP_PORT) + ap_port: opt_str(AP_PORT) .map(|port| { let on_error = || { error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); @@ -1067,8 +1133,7 @@ fn get_setup(args: &[String]) -> Setup { let player_config = { let player_default_config = PlayerConfig::default(); - let bitrate = matches - .opt_str(BITRATE) + let bitrate = opt_str(BITRATE) .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { @@ -1086,9 +1151,9 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.bitrate); - let gapless = !matches.opt_present(DISABLE_GAPLESS); + let gapless = !opt_present(DISABLE_GAPLESS); - let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); + let normalisation = opt_present(ENABLE_VOLUME_NORMALISATION); let normalisation_method; let normalisation_type; @@ -1108,7 +1173,7 @@ fn get_setup(args: &[String]) -> Setup { NORMALISATION_RELEASE, NORMALISATION_KNEE, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!( "Without the `--{}` / `-{}` flag normalisation options have no effect.", ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, @@ -1125,8 +1190,7 @@ fn get_setup(args: &[String]) -> Setup { normalisation_release = player_default_config.normalisation_release; normalisation_knee = player_default_config.normalisation_knee; } else { - normalisation_method = matches - .opt_str(NORMALISATION_METHOD) + normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() .map(|method| { warn!( @@ -1158,8 +1222,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_method); - normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) + normalisation_type = opt_str(NORMALISATION_GAIN_TYPE) .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { @@ -1177,8 +1240,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) + normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { error!( @@ -1209,8 +1271,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_pregain); - normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) + normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { error!( @@ -1244,8 +1305,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_threshold); - normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) + normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { error!( @@ -1279,8 +1339,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_attack); - normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) + normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { error!( @@ -1314,8 +1373,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_release); - normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) + normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { error!( @@ -1347,7 +1405,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or(player_default_config.normalisation_knee); } - let ditherer_name = matches.opt_str(DITHER); + let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { // explicitly disabled on command line Some("none") => None, @@ -1363,7 +1421,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", DITHER, DITHER_SHORT, - matches.opt_str(DITHER).unwrap_or_default() + opt_str(DITHER).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", @@ -1384,7 +1442,7 @@ fn get_setup(args: &[String]) -> Setup { }, }; - let passthrough = matches.opt_present(PASSTHROUGH); + let passthrough = opt_present(PASSTHROUGH); PlayerConfig { bitrate, @@ -1402,8 +1460,8 @@ fn get_setup(args: &[String]) -> Setup { } }; - let player_event_program = matches.opt_str(ONEVENT); - let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); + let player_event_program = opt_str(ONEVENT); + let emit_sink_events = opt_present(EMIT_SINK_EVENTS); Setup { format, From 4370258716e3e3303b9242cda4ec894c80c0c31e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 3 Dec 2021 11:47:51 -0600 Subject: [PATCH 041/561] Address clippy lint warnings for rust 1.57 --- connect/src/context.rs | 2 ++ core/src/connection/codec.rs | 3 +-- playback/src/audio_backend/jackaudio.rs | 9 +++------ playback/src/audio_backend/mod.rs | 2 +- playback/src/audio_backend/rodio.rs | 1 + playback/src/mixer/alsamixer.rs | 1 + 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/connect/src/context.rs b/connect/src/context.rs index 63a2aebb..154d9507 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -46,6 +46,7 @@ pub struct TrackContext { // pub metadata: MetadataContext, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ArtistContext { @@ -54,6 +55,7 @@ pub struct ArtistContext { image_uri: String, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct MetadataContext { album_title: String, diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index 299220f6..86533aaf 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -87,8 +87,7 @@ impl Decoder for ApCodec { let mut payload = buf.split_to(size + MAC_SIZE); - self.decode_cipher - .decrypt(&mut payload.get_mut(..size).unwrap()); + self.decode_cipher.decrypt(payload.get_mut(..size).unwrap()); let mac = payload.split_off(size); self.decode_cipher.check_mac(mac.as_ref())?; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 5ba7b7ff..15acf99d 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -24,15 +24,12 @@ pub struct JackData { impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers - let mut out_r = self.port_r.as_mut_slice(ps); - let mut out_l = self.port_l.as_mut_slice(ps); - let buf_r: &mut [f32] = &mut out_r; - let buf_l: &mut [f32] = &mut out_l; + let buf_r: &mut [f32] = self.port_r.as_mut_slice(ps); + let buf_l: &mut [f32] = self.port_l.as_mut_slice(ps); // get queue iterator let mut queue_iter = self.rec.try_iter(); - let buf_size = buf_r.len(); - for i in 0..buf_size { + for i in 0..buf_r.len() { buf_r[i] = queue_iter.next().unwrap_or(0.0); buf_l[i] = queue_iter.next().unwrap_or(0.0); } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 4d3b0171..dc21fb3d 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -104,7 +104,7 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] +#[cfg(feature = "rodio-backend")] use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 200c9fc4..ab356d67 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -227,5 +227,6 @@ impl Sink for RodioSink { } impl RodioSink { + #[allow(dead_code)] pub const NAME: &'static str = "rodio"; } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 81d0436f..55398cb7 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -10,6 +10,7 @@ use alsa::{Ctl, Round}; use std::ffi::CString; #[derive(Clone)] +#[allow(dead_code)] pub struct AlsaMixer { config: MixerConfig, min: i64, From 0e2686863aa0746f2e329f7c2220fb779a83d8d1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 7 Dec 2021 23:22:24 +0100 Subject: [PATCH 042/561] 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 043/561] 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 044/561] 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 045/561] 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 046/561] 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 047/561] 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 048/561] 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 049/561] 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 050/561] 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 051/561] 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 052/561] 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 053/561] 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 054/561] Get connection ID --- connect/src/spirc.rs | 29 +++++++++++++++++++++++++++++ core/src/mercury/mod.rs | 1 + core/src/session.rs | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e0b817ec..b3878a42 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -61,6 +61,7 @@ struct SpircTask { play_status: SpircPlayStatus, subscription: BoxedStream, + connection_id_update: BoxedStream, user_attributes_update: BoxedStream, user_attributes_mutation: BoxedStream, sender: MercurySender, @@ -254,6 +255,21 @@ impl Spirc { }), ); + let connection_id_update = Box::pin( + session + .mercury() + .listen_for("hm://pusher/v1/connections/") + .map(UnboundedReceiverStream::new) + .flatten_stream() + .map(|response| -> String { + response + .uri + .strip_prefix("hm://pusher/v1/connections/") + .unwrap_or("") + .to_owned() + }), + ); + let user_attributes_update = Box::pin( session .mercury() @@ -306,6 +322,7 @@ impl Spirc { play_status: SpircPlayStatus::Stopped, subscription, + connection_id_update, user_attributes_update, user_attributes_mutation, sender, @@ -390,6 +407,13 @@ impl SpircTask { break; } }, + connection_id_update = self.connection_id_update.next() => match connection_id_update { + Some(connection_id) => self.handle_connection_id_update(connection_id), + None => { + error!("connection ID update selected, but none received"); + break; + } + }, cmd = async { commands.unwrap().recv().await }, if commands.is_some() => if let Some(cmd) = cmd { self.handle_command(cmd); }, @@ -619,6 +643,11 @@ impl SpircTask { } } + fn handle_connection_id_update(&mut self, connection_id: String) { + trace!("Received connection ID update: {:?}", connection_id); + self.session.set_connection_id(connection_id); + } + fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { trace!("Received attributes update: {:#?}", update); let attributes: UserAttributes = update diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 841bd3d1..ad2d5013 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -264,6 +264,7 @@ impl MercuryManager { if !found { debug!("unknown subscription uri={}", response.uri); + trace!("response pushed over Mercury: {:?}", response); } }) } else if let Some(cb) = pending.callback { diff --git a/core/src/session.rs b/core/src/session.rs index ed9609d7..426480f6 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -52,6 +52,7 @@ pub struct UserData { #[derive(Debug, Clone, Default)] struct SessionData { + connection_id: String, time_delta: i64, invalid: bool, user_data: UserData, @@ -319,6 +320,14 @@ impl Session { &self.config().device_id } + pub fn connection_id(&self) -> String { + self.0.data.read().unwrap().connection_id.clone() + } + + pub fn set_connection_id(&self, connection_id: String) { + self.0.data.write().unwrap().connection_id = connection_id; + } + pub fn username(&self) -> String { self.0 .data From 8c480b7e39e911ddbfc4d564932d09dbdd7b6af6 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 5 Dec 2021 13:00:33 -0600 Subject: [PATCH 055/561] Fix Command line arguments incorrectly echoed in TRACE Fix up for #886 Closes: #898 And... * Don't silently ignore non-Unicode while parsing env vars. * Iterating over `std::env::args` will panic! on invalid unicode. Let's not do that. `getopts` will catch missing args and exit if those args are required after our error message about the arg not being valid unicode. * Gaurd against empty strings. There are a few places while parsing options strings that we don't immediately evaluate their validity let's at least makes sure that they are not empty if present. * `args` is only used in `get_setup` it doesn't need to be in main. * Nicer help header. * Get rid of `use std::io::{stderr, Write};` and just use `rpassword::prompt_password_stderr`. * Get rid of `get_credentials` it was clunky, ugly and only used once. There is no need for it to be a separate function. * Handle an empty password prompt and password prompt parsing errors. * + Other random misc clean ups. --- src/main.rs | 415 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 240 insertions(+), 175 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2dec56ae..789654ff 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; -use std::io::{stderr, Write}; use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; @@ -40,27 +39,16 @@ fn device_id(name: &str) -> String { } fn usage(program: &str, opts: &getopts::Options) -> String { - let brief = format!("Usage: {} [options]", program); + let repo_home = env!("CARGO_PKG_REPOSITORY"); + let desc = env!("CARGO_PKG_DESCRIPTION"); + let version = get_version_string(); + let brief = format!( + "{}\n\n{}\n\n{}\n\nUsage: {} []", + version, desc, repo_home, program + ); opts.usage(&brief) } -fn arg_to_var(arg: &str) -> String { - // To avoid name collisions environment variables must be prepended - // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. - format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) -} - -fn env_var_present(arg: &str) -> bool { - env::var(arg_to_var(arg)).is_ok() -} - -fn env_var_opt_str(option: &str) -> Option { - match env::var(arg_to_var(option)) { - Ok(value) => Some(value), - Err(_) => None, - } -} - fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -102,29 +90,6 @@ fn list_backends() { } } -pub fn get_credentials Option>( - username: Option, - password: Option, - cached_credentials: Option, - prompt: F, -) -> Option { - if let Some(username) = username { - if let Some(password) = password { - return Some(Credentials::with_password(username, password)); - } - - match cached_credentials { - Some(credentials) if username == credentials.username => Some(credentials), - _ => { - let password = prompt(&username)?; - Some(Credentials::with_password(username, password)) - } - } - } else { - cached_credentials - } -} - #[derive(Debug, Error)] pub enum ParseFileSizeError { #[error("empty argument")] @@ -218,9 +183,10 @@ struct Setup { emit_sink_events: bool, } -fn get_setup(args: &[String]) -> Setup { - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; +fn get_setup() -> Setup { + const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; @@ -596,25 +562,72 @@ fn get_setup(args: &[String]) -> Setup { "PORT", ); + let args: Vec<_> = std::env::args_os() + .filter_map(|s| match s.into_string() { + Ok(valid) => Some(valid), + Err(s) => { + eprintln!( + "Command line argument was not valid Unicode and will not be evaluated: {:?}", + s + ); + None + } + }) + .collect(); + let matches = match opts.parse(&args[1..]) { Ok(m) => m, - Err(f) => { - eprintln!( - "Error parsing command line options: {}\n{}", - f, - usage(&args[0], &opts) - ); + Err(e) => { + eprintln!("Error parsing command line options: {}", e); + println!("\n{}", usage(&args[0], &opts)); exit(1); } }; - let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + let stripped_env_key = |k: &str| { + k.trim_start_matches("LIBRESPOT_") + .replace("_", "-") + .to_lowercase() + }; + + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { + let mut env_var = None; + if let Ok(key) = k.into_string() { + if key.starts_with("LIBRESPOT_") { + let stripped_key = stripped_env_key(&key); + // Only match against long option/flag names. + // Something like LIBRESPOT_V for example is + // not valid because there are both -v and -V flags + // but env vars are assumed to be all uppercase. + let len = stripped_key.chars().count(); + if len > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => { + env_var = Some((key, value)); + }, + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + } + } + } + } + } + + env_var + }) + .collect(); + + let opt_present = + |opt| matches.opt_present(opt) || env_vars.iter().any(|(k, _)| stripped_env_key(k) == opt); let opt_str = |opt| { if matches.opt_present(opt) { matches.opt_str(opt) } else { - env_var_opt_str(opt) + env_vars + .iter() + .find(|(k, _)| stripped_env_key(k) == opt) + .map(|(_, v)| v.to_string()) } }; @@ -632,55 +645,43 @@ fn get_setup(args: &[String]) -> Setup { info!("{}", get_version_string()); - let librespot_env_vars: Vec = env::vars_os() - .filter_map(|(k, v)| { - let mut env_var = None; - if let Some(key) = k.to_str() { - if key.starts_with("LIBRESPOT_") { - if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { - // Don't log creds. - env_var = Some(format!("\t\t{}=XXXXXXXX", key)); - } else if let Some(value) = v.to_str() { - env_var = Some(format!("\t\t{}={}", key, value)); - } - } - } - - env_var - }) - .collect(); - - if !librespot_env_vars.is_empty() { + if !env_vars.is_empty() { trace!("Environment variable(s):"); - for kv in librespot_env_vars { - trace!("{}", kv); + for (k, v) in &env_vars { + if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + trace!("\t\t{}=\"XXXXXXXX\"", k); + } else if v.is_empty() { + trace!("\t\t{}=", k); + } else { + trace!("\t\t{}=\"{}\"", k, v); + } } } - let cmd_args = &args[1..]; + let args_len = args.len(); - let cmd_args_len = cmd_args.len(); - - if cmd_args_len > 0 { + if args_len > 1 { trace!("Command line argument(s):"); - for (index, key) in cmd_args.iter().enumerate() { - if key.starts_with('-') || key.starts_with("--") { - if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { - // Don't log creds. - trace!("\t\t{} XXXXXXXX", key); - } else { - let mut value = "".to_string(); - let next = index + 1; - if next < cmd_args_len { - let next_key = cmd_args[next].clone(); - if !next_key.starts_with('-') && !next_key.starts_with("--") { - value = next_key; - } - } + for (index, key) in args.iter().enumerate() { + let opt = key.trim_start_matches('-'); - trace!("\t\t{} {}", key, value); + if index > 0 + && &args[index - 1] != key + && matches.opt_defined(opt) + && matches.opt_present(opt) + { + if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { + // Don't log creds. + trace!("\t\t{} \"XXXXXXXX\"", key); + } else { + let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string()); + if value.is_empty() { + trace!("\t\t{}", key); + } else { + trace!("\t\t{} \"{}\"", key, value); + } } } } @@ -707,7 +708,7 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BACKEND, BACKEND_SHORT, opt_str(BACKEND).unwrap_or_default() @@ -720,7 +721,10 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + error!( + "Invalid `--{}` / `-{}`: \"{}\"", + FORMAT, FORMAT_SHORT, format + ); println!( "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", FORMAT, FORMAT_SHORT @@ -743,9 +747,17 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - if device == Some("?".into()) { - backend(device, format); - exit(0); + if let Some(ref value) = device { + if value == "?" { + backend(device, format); + exit(0); + } else if value.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + DEVICE, DEVICE_SHORT + ); + exit(1); + } } #[cfg(not(any( @@ -774,7 +786,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", MIXER_TYPE, MIXER_TYPE_SHORT, opt_str(MIXER_TYPE).unwrap_or_default() @@ -807,7 +819,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|index| { index.parse::().unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index ); println!("Default: {}", mixer_default_config.index); @@ -822,6 +834,15 @@ fn get_setup(args: &[String]) -> Setup { #[cfg(feature = "alsa-backend")] let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); + #[cfg(feature = "alsa-backend")] + if control.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT + ); + exit(1); + } + #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; @@ -829,7 +850,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|range| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_RANGE, VOLUME_RANGE_SHORT, range ); println!( @@ -871,7 +892,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl ); println!( @@ -918,7 +939,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|e| { e.unwrap_or_else(|e| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e ); exit(1); @@ -945,20 +966,57 @@ fn get_setup(args: &[String]) -> Setup { }; let credentials = { - let cached_credentials = cache.as_ref().and_then(Cache::credentials); + let cached_creds = cache.as_ref().and_then(Cache::credentials); - let password = |username: &String| -> Option { - write!(stderr(), "Password for {}: ", username).ok()?; - stderr().flush().ok()?; - rpassword::read_password().ok() - }; + if let Some(username) = opt_str(USERNAME) { + if username.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + USERNAME, USERNAME_SHORT + ); + exit(1); + } + if let Some(password) = opt_str(PASSWORD) { + if password.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + PASSWORD, PASSWORD_SHORT + ); + exit(1); + } + Some(Credentials::with_password(username, password)) + } else { + match cached_creds { + Some(creds) if username == creds.username => Some(creds), + _ => { + let prompt = &format!("Password for {}: ", username); - get_credentials( - opt_str(USERNAME), - opt_str(PASSWORD), - cached_credentials, - password, - ) + match rpassword::prompt_password_stderr(prompt) { + Ok(password) => { + if !password.is_empty() { + Some(Credentials::with_password(username, password)) + } else { + trace!("Password was empty."); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + Err(e) => { + warn!("Cannot parse password: {}", e); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + } + } + } + } else { + cached_creds + } }; let enable_discovery = !opt_present(DISABLE_DISCOVERY); @@ -980,12 +1038,14 @@ fn get_setup(args: &[String]) -> Setup { .map(|port| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port ); println!( - "Valid `--{}` / `-{}` values: 1 - 65535", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT + "Valid `--{}` / `-{}` values: 1 - {}", + ZEROCONF_PORT, + ZEROCONF_PORT_SHORT, + u16::MAX ); }; @@ -1011,16 +1071,27 @@ fn get_setup(args: &[String]) -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); + if name.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + NAME, NAME_SHORT + ); + exit(1); + } + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume ); println!( - "Valid `--{}` / `-{}` values: 0 - 100", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT + "Valid `--{}` / `-{}` values: {} - {}", + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() ); #[cfg(feature = "alsa-backend")] println!( @@ -1039,7 +1110,7 @@ fn get_setup(args: &[String]) -> Setup { exit(1); }); - if volume > 100 { + if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { on_error(); exit(1); } @@ -1056,7 +1127,7 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", DEVICE_TYPE, DEVICE_TYPE_SHORT @@ -1079,55 +1150,50 @@ fn get_setup(args: &[String]) -> Setup { } }; - let session_config = { - let device_id = device_id(&connect_config.name); - - SessionConfig { - user_agent: version::VERSION_STRING.to_string(), - device_id, - proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( - |s| { - match Url::parse(&s) { - Ok(url) => { - if url.host().is_none() || url.port_or_known_default().is_none() { - error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); - exit(1); - } - - if url.scheme() != "http" { - error!("Only unsecure http:// proxies are supported"); - exit(1); - } - - url - }, - Err(e) => { - error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + let session_config = SessionConfig { + user_agent: version::VERSION_STRING.to_string(), + device_id: device_id(&connect_config.name), + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + |s| { + match Url::parse(&s) { + Ok(url) => { + if url.host().is_none() || url.port_or_known_default().is_none() { + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); exit(1); } - } - }, - ), - ap_port: opt_str(AP_PORT) - .map(|port| { - let on_error = || { - error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); + if url.scheme() != "http" { + error!("Only unsecure http:// proxies are supported"); + exit(1); + } - if port == 0 { - on_error(); + url + }, + Err(e) => { + error!("Invalid proxy URL: \"{}\", only URLs in the format \"http://host:port\" are allowed", e); exit(1); } + } + }, + ), + ap_port: opt_str(AP_PORT).map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + }; - port - }), - } + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }), }; let player_config = { @@ -1138,7 +1204,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BITRATE, BITRATE_SHORT, bitrate ); println!( @@ -1200,7 +1266,7 @@ fn get_setup(args: &[String]) -> Setup { let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method ); println!( @@ -1227,7 +1293,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type ); println!( @@ -1244,7 +1310,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|pregain| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain ); println!( @@ -1275,7 +1341,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|threshold| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold ); println!( @@ -1309,7 +1375,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|attack| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack ); println!( @@ -1343,7 +1409,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|release| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release ); println!( @@ -1377,7 +1443,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|knee| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee ); println!( @@ -1418,7 +1484,7 @@ fn get_setup(args: &[String]) -> Setup { Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", DITHER, DITHER_SHORT, opt_str(DITHER).unwrap_or_default() @@ -1488,8 +1554,7 @@ async fn main() { env::set_var(RUST_BACKTRACE, "full") } - let args: Vec = std::env::args().collect(); - let setup = get_setup(&args); + let setup = get_setup(); let mut last_credentials = None; let mut spirc: Option = None; From 79c4040a53f50f14f27c6d9e0fec9ad00101f638 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 21:32:34 +0100 Subject: [PATCH 056/561] Skip track on decoding error --- playback/src/player.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a7ff916d..a56130f3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -737,7 +737,14 @@ impl PlayerTrackLoader { } }; - assert!(audio.duration >= 0); + if audio.duration < 0 { + error!( + "Track duration for <{}> cannot be {}", + spotify_id.to_uri(), + audio.duration + ); + return None; + } let duration_ms = audio.duration as u32; // (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it @@ -945,7 +952,7 @@ impl Future for PlayerInternal { } Poll::Ready(Err(_)) => { warn!("Unable to load <{:?}>\nSkipping to next track", track_id); - assert!(self.state.is_loading()); + debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1045,8 +1052,11 @@ impl Future for PlayerInternal { } } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } @@ -1058,8 +1068,15 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!( + "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", + e, + track_id + ); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } else { From 8f23c3498fd9eeaf7d74ab2cb57548f0e811de57 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 12 Dec 2021 20:01:05 +0100 Subject: [PATCH 057/561] Clean up warnings --- playback/src/player.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a56130f3..d8dbb190 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -950,8 +950,11 @@ impl Future for PlayerInternal { exit(1); } } - Poll::Ready(Err(_)) => { - warn!("Unable to load <{:?}>\nSkipping to next track", track_id); + Poll::Ready(Err(e)) => { + warn!( + "Skipping to next track, unable to load track <{:?}>: {:?}", + track_id, e + ); debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, @@ -1052,7 +1055,7 @@ impl Future for PlayerInternal { } } Err(e) => { - warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + warn!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1068,11 +1071,7 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - warn!( - "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", - e, - track_id - ); + warn!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, From d29337c62df4e12e8d2120ab973b50ec6ab75a38 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 12 Dec 2021 17:28:41 -0600 Subject: [PATCH 058/561] Dry up error messages. --- src/main.rs | 406 +++++++++++++++++++++++++--------------------------- 1 file changed, 199 insertions(+), 207 deletions(-) diff --git a/src/main.rs b/src/main.rs index 789654ff..7e13a323 100644 --- a/src/main.rs +++ b/src/main.rs @@ -590,30 +590,23 @@ fn get_setup() -> Setup { .to_lowercase() }; - let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { - let mut env_var = None; - if let Ok(key) = k.into_string() { - if key.starts_with("LIBRESPOT_") { - let stripped_key = stripped_env_key(&key); - // Only match against long option/flag names. - // Something like LIBRESPOT_V for example is - // not valid because there are both -v and -V flags - // but env vars are assumed to be all uppercase. - let len = stripped_key.chars().count(); - if len > 1 && matches.opt_defined(&stripped_key) { - match v.into_string() { - Ok(value) => { - env_var = Some((key, value)); - }, - Err(s) => { - eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); - } + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| match k.into_string() { + Ok(key) if key.starts_with("LIBRESPOT_") => { + let stripped_key = stripped_env_key(&key); + // We only care about long option/flag names. + if stripped_key.chars().count() > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => Some((key, value)), + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + None } } + } else { + None } - } - - env_var + }, + _ => None }) .collect(); @@ -706,13 +699,33 @@ fn get_setup() -> Setup { exit(0); } + let invalid_error_msg = + |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { + error!("Invalid `--{}` / `-{}`: \"{}\"", long, short, invalid); + + if !valid_values.is_empty() { + println!("Valid `--{}` / `-{}` values: {}", long, short, valid_values); + } + + if !default_value.is_empty() { + println!("Default: {}", default_value); + } + }; + + let empty_string_error_msg = |long: &str, short: &str| { + error!("`--{}` / `-{}` can not be an empty string", long, short); + exit(1); + }; + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( BACKEND, BACKEND_SHORT, - opt_str(BACKEND).unwrap_or_default() + &opt_str(BACKEND).unwrap_or_default(), + "", + "", ); + list_backends(); exit(1); }); @@ -721,15 +734,15 @@ fn get_setup() -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - FORMAT, FORMAT_SHORT, format + let default_value = &format!("{:?}", AudioFormat::default()); + invalid_error_msg( + FORMAT, + FORMAT_SHORT, + format, + "F64, F32, S32, S24, S24_3, S16", + default_value, ); - println!( - "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", - FORMAT, FORMAT_SHORT - ); - println!("Default: {:?}", AudioFormat::default()); + exit(1); }) }) @@ -752,11 +765,7 @@ fn get_setup() -> Setup { backend(device, format); exit(0); } else if value.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - DEVICE, DEVICE_SHORT - ); - exit(1); + empty_string_error_msg(DEVICE, DEVICE_SHORT); } } @@ -785,17 +794,14 @@ fn get_setup() -> Setup { let mixer_type: Option = None; let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( MIXER_TYPE, MIXER_TYPE_SHORT, - opt_str(MIXER_TYPE).unwrap_or_default() + &opt_str(MIXER_TYPE).unwrap_or_default(), + "alsa, softvol", + "softvol", ); - println!( - "Valid `--{}` / `-{}` values: alsa, softvol", - MIXER_TYPE, MIXER_TYPE_SHORT - ); - println!("Default: softvol"); + exit(1); }); @@ -818,11 +824,14 @@ fn get_setup() -> Setup { let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + invalid_error_msg( + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_SHORT, + &index, + "", + &mixer_default_config.index.to_string(), ); - println!("Default: {}", mixer_default_config.index); + exit(1); }) }) @@ -836,11 +845,7 @@ fn get_setup() -> Setup { #[cfg(feature = "alsa-backend")] if control.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT - ); - exit(1); + empty_string_error_msg(ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT); } #[cfg(not(feature = "alsa-backend"))] @@ -849,24 +854,28 @@ fn get_setup() -> Setup { let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_RANGE, VOLUME_RANGE_SHORT, range - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - VOLUME_RANGE, - VOLUME_RANGE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_VOLUME_RANGE.start(), VALID_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: softvol - {}, alsa - what the control supports", + let default_value = &format!( + "softvol - {}, alsa - what the control supports", VolumeCtrl::DEFAULT_DB_RANGE ); + #[cfg(not(feature = "alsa-backend"))] - println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + let default_value = &VolumeCtrl::DEFAULT_DB_RANGE.to_string(); + + invalid_error_msg( + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + &range, + valid_values, + default_value, + ); }; let range = range.parse::().unwrap_or_else(|_| { @@ -891,15 +900,14 @@ fn get_setup() -> Setup { .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + invalid_error_msg( + VOLUME_CTRL, + VOLUME_CTRL_SHORT, + volume_ctrl, + "cubic, fixed, linear, log", + "log", ); - println!( - "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", - VOLUME_CTRL, VOLUME_CTRL - ); - println!("Default: log"); + exit(1); }) }) @@ -938,10 +946,14 @@ fn get_setup() -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + invalid_error_msg( + CACHE_SIZE_LIMIT, + CACHE_SIZE_LIMIT_SHORT, + &e.to_string(), + "", + "", ); + exit(1); }) }) @@ -970,19 +982,11 @@ fn get_setup() -> Setup { if let Some(username) = opt_str(USERNAME) { if username.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - USERNAME, USERNAME_SHORT - ); - exit(1); + empty_string_error_msg(USERNAME, USERNAME_SHORT); } if let Some(password) = opt_str(PASSWORD) { if password.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - PASSWORD, PASSWORD_SHORT - ); - exit(1); + empty_string_error_msg(PASSWORD, PASSWORD_SHORT); } Some(Credentials::with_password(username, password)) } else { @@ -990,7 +994,6 @@ fn get_setup() -> Setup { Some(creds) if username == creds.username => Some(creds), _ => { let prompt = &format!("Password for {}: ", username); - match rpassword::prompt_password_stderr(prompt) { Ok(password) => { if !password.is_empty() { @@ -1015,6 +1018,9 @@ fn get_setup() -> Setup { } } } else { + if cached_creds.is_some() { + trace!("Using cached credentials."); + } cached_creds } }; @@ -1037,16 +1043,8 @@ fn get_setup() -> Setup { opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port - ); - println!( - "Valid `--{}` / `-{}` values: 1 - {}", - ZEROCONF_PORT, - ZEROCONF_PORT_SHORT, - u16::MAX - ); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1072,36 +1070,37 @@ fn get_setup() -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); if name.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - NAME, NAME_SHORT - ); + empty_string_error_msg(NAME, NAME_SHORT); exit(1); } let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, + let valid_values = &format!( + "{} - {}", VALID_INITIAL_VOLUME_RANGE.start(), VALID_INITIAL_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: {}, or the current value when the alsa mixer is used.", + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", connect_default_config.initial_volume.unwrap_or_default() ); + #[cfg(not(feature = "alsa-backend"))] - println!( - "Default: {}", - connect_default_config.initial_volume.unwrap_or_default() + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); + + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, ); }; @@ -1127,12 +1126,18 @@ fn get_setup() -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); - println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ - gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", - DEVICE_TYPE, DEVICE_TYPE_SHORT + invalid_error_msg( + DEVICE_TYPE, + DEVICE_TYPE_SHORT, + device_type, + "computer, tablet, smartphone, \ + speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, \ + automobile, smartwatch, chromebook, \ + carthing, homething", + "speaker", ); - println!("Default: speaker"); + exit(1); }) }) @@ -1178,8 +1183,8 @@ fn get_setup() -> Setup { ), ap_port: opt_str(AP_PORT).map(|port| { let on_error = || { - error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1203,15 +1208,7 @@ fn get_setup() -> Setup { .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - BITRATE, BITRATE_SHORT, bitrate - ); - println!( - "Valid `--{}` / `-{}` values: 96, 160, 320", - BITRATE, BITRATE_SHORT - ); - println!("Default: 160"); + invalid_error_msg(BITRATE, BITRATE_SHORT, bitrate, "96, 160, 320", "160"); exit(1); }) }) @@ -1265,15 +1262,14 @@ fn get_setup() -> Setup { ); let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + invalid_error_msg( + NORMALISATION_METHOD, + NORMALISATION_METHOD_SHORT, + method, + "basic, dynamic", + &format!("{:?}", player_default_config.normalisation_method), ); - println!( - "Valid `--{}` / `-{}` values: basic, dynamic", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT - ); - println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); }); @@ -1292,15 +1288,14 @@ fn get_setup() -> Setup { .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type + invalid_error_msg( + NORMALISATION_GAIN_TYPE, + NORMALISATION_GAIN_TYPE_SHORT, + gain_type, + "track, album, auto", + &format!("{:?}", player_default_config.normalisation_type), ); - println!( - "Valid `--{}` / `-{}` values: track, album, auto", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, - ); - println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); }) }) @@ -1309,18 +1304,19 @@ fn get_setup() -> Setup { normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_PREGAIN, - NORMALISATION_PREGAIN_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_PREGAIN_RANGE.start(), VALID_NORMALISATION_PREGAIN_RANGE.end() ); - println!("Default: {}", player_default_config.normalisation_pregain); + + invalid_error_msg( + NORMALISATION_PREGAIN, + NORMALISATION_PREGAIN_SHORT, + &pregain, + valid_values, + &player_default_config.normalisation_pregain.to_string(), + ); }; let pregain = pregain.parse::().unwrap_or_else(|_| { @@ -1340,20 +1336,18 @@ fn get_setup() -> Setup { normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_THRESHOLD, - NORMALISATION_THRESHOLD_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), VALID_NORMALISATION_THRESHOLD_RANGE.end() ); - println!( - "Default: {}", - ratio_to_db(player_default_config.normalisation_threshold) + + invalid_error_msg( + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + &threshold, + valid_values, + &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); }; @@ -1374,20 +1368,21 @@ fn get_setup() -> Setup { normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_ATTACK, - NORMALISATION_ATTACK_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), VALID_NORMALISATION_ATTACK_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_attack.as_millis() + + invalid_error_msg( + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + &attack, + valid_values, + &player_default_config + .normalisation_attack + .as_millis() + .to_string(), ); }; @@ -1408,20 +1403,21 @@ fn get_setup() -> Setup { normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_RELEASE, - NORMALISATION_RELEASE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), VALID_NORMALISATION_RELEASE_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_release.as_millis() + + invalid_error_msg( + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + &release, + valid_values, + &player_default_config + .normalisation_release + .as_millis() + .to_string(), ); }; @@ -1442,18 +1438,19 @@ fn get_setup() -> Setup { normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_KNEE, - NORMALISATION_KNEE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_KNEE_RANGE.start(), VALID_NORMALISATION_KNEE_RANGE.end() ); - println!("Default: {}", player_default_config.normalisation_knee); + + invalid_error_msg( + NORMALISATION_KNEE, + NORMALISATION_KNEE_SHORT, + &knee, + valid_values, + &player_default_config.normalisation_knee.to_string(), + ); }; let knee = knee.parse::().unwrap_or_else(|_| { @@ -1483,19 +1480,14 @@ fn get_setup() -> Setup { } Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( DITHER, DITHER_SHORT, - opt_str(DITHER).unwrap_or_default() - ); - println!( - "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", - DITHER, DITHER_SHORT - ); - println!( - "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp", + "tpdf for formats S16, S24, S24_3 and none for other formats", ); + exit(1); })) } From 368bee10885d5d0e528e45328676e59857cd6896 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 13 Dec 2021 16:40:26 -0600 Subject: [PATCH 059/561] condense some option parsings --- src/main.rs | 229 ++++++++++++++++++---------------------------------- 1 file changed, 78 insertions(+), 151 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7e13a323..2ce526e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -852,8 +852,9 @@ fn get_setup() -> Setup { let control = mixer_default_config.control; let volume_range = opt_str(VOLUME_RANGE) - .map(|range| { - let on_error = || { + .map(|range| match range.parse::() { + Ok(value) if (VALID_VOLUME_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_VOLUME_RANGE.start(), @@ -876,19 +877,9 @@ fn get_setup() -> Setup { valid_values, default_value, ); - }; - let range = range.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_VOLUME_RANGE).contains(&range) { - on_error(); exit(1); } - - range }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] @@ -1041,23 +1032,14 @@ fn get_setup() -> Setup { let zeroconf_port = if enable_discovery { opt_str(ZEROCONF_PORT) - .map(|port| { - let on_error = || { + .map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if port == 0 { - on_error(); exit(1); } - - port }) .unwrap_or(0) } else { @@ -1076,44 +1058,39 @@ fn get_setup() -> Setup { let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { - let on_error = || { - let valid_values = &format!( - "{} - {}", - VALID_INITIAL_VOLUME_RANGE.start(), - VALID_INITIAL_VOLUME_RANGE.end() - ); + let volume = match initial_volume.parse::() { + Ok(value) if (VALID_INITIAL_VOLUME_RANGE).contains(&value) => value, + _ => { + let valid_values = &format!( + "{} - {}", + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() + ); - #[cfg(feature = "alsa-backend")] - let default_value = &format!( - "{}, or the current value when the alsa mixer is used.", - connect_default_config.initial_volume.unwrap_or_default() - ); + #[cfg(feature = "alsa-backend")] + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); - #[cfg(not(feature = "alsa-backend"))] - let default_value = &connect_default_config - .initial_volume - .unwrap_or_default() - .to_string(); + #[cfg(not(feature = "alsa-backend"))] + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); - invalid_error_msg( - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, - &initial_volume, - valid_values, - default_value, - ); + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, + ); + + exit(1); + } }; - let volume = initial_volume.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { - on_error(); - exit(1); - } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) .or_else(|| match mixer_type.as_deref() { @@ -1135,7 +1112,7 @@ fn get_setup() -> Setup { gameconsole, castaudio, castvideo, \ automobile, smartwatch, chromebook, \ carthing, homething", - "speaker", + DeviceType::default().into(), ); exit(1); @@ -1181,23 +1158,14 @@ fn get_setup() -> Setup { } }, ), - ap_port: opt_str(AP_PORT).map(|port| { - let on_error = || { + ap_port: opt_str(AP_PORT).map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if port == 0 { - on_error(); exit(1); } - - port }), }; @@ -1302,8 +1270,9 @@ fn get_setup() -> Setup { .unwrap_or(player_default_config.normalisation_type); normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| { - let on_error = || { + .map(|pregain| match pregain.parse::() { + Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_PREGAIN_RANGE.start(), @@ -1317,25 +1286,18 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_pregain.to_string(), ); - }; - let pregain = pregain.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { - on_error(); exit(1); } - - pregain }) .unwrap_or(player_default_config.normalisation_pregain); normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - let on_error = || { + .map(|threshold| match threshold.parse::() { + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { + db_to_ratio(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), @@ -1349,25 +1311,18 @@ fn get_setup() -> Setup { valid_values, &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); - }; - let threshold = threshold.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { - on_error(); exit(1); } - - db_to_ratio(threshold) }) .unwrap_or(player_default_config.normalisation_threshold); normalisation_attack = opt_str(NORMALISATION_ATTACK) - .map(|attack| { - let on_error = || { + .map(|attack| match attack.parse::() { + Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), @@ -1384,25 +1339,18 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - let attack = attack.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { - on_error(); exit(1); } - - Duration::from_millis(attack) }) .unwrap_or(player_default_config.normalisation_attack); normalisation_release = opt_str(NORMALISATION_RELEASE) - .map(|release| { - let on_error = || { + .map(|release| match release.parse::() { + Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), @@ -1419,25 +1367,16 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - let release = release.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { - on_error(); exit(1); } - - Duration::from_millis(release) }) .unwrap_or(player_default_config.normalisation_release); normalisation_knee = opt_str(NORMALISATION_KNEE) - .map(|knee| { - let on_error = || { + .map(|knee| match knee.parse::() { + Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_KNEE_RANGE.start(), @@ -1451,47 +1390,35 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_knee.to_string(), ); - }; - let knee = knee.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { - on_error(); exit(1); } - - knee }) .unwrap_or(player_default_config.normalisation_knee); } let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { - // explicitly disabled on command line - Some("none") => None, - // explicitly set on command line - Some(_) => { - if matches!(format, AudioFormat::F64 | AudioFormat::F32) { - error!("Dithering is not available with format: {:?}.", format); - exit(1); - } + Some(value) => match value { + "none" => None, + _ => match format { + AudioFormat::F64 | AudioFormat::F32 => { + error!("Dithering is not available with format: {:?}.", format); + exit(1); + } + _ => Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + invalid_error_msg( + DITHER, + DITHER_SHORT, + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp for formats S16, S24, S24_3, S32, none for formats F32, F64", + "tpdf for formats S16, S24, S24_3 and none for formats S32, F32, F64", + ); - Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - invalid_error_msg( - DITHER, - DITHER_SHORT, - &opt_str(DITHER).unwrap_or_default(), - "none, gpdf, tpdf, tpdf_hp", - "tpdf for formats S16, S24, S24_3 and none for other formats", - ); - - exit(1); - })) - } - // nothing set on command line => use default + exit(1); + })), + }, + }, None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { player_default_config.ditherer From 67836b5b02f2990376849fc04d2f9cd0316f1e0b Mon Sep 17 00:00:00 2001 From: Mateusz Mojsiejuk Date: Tue, 14 Dec 2021 21:23:13 +0100 Subject: [PATCH 060/561] Added arm64 target to docker run examples. Also removed feature quotes as they're not nessesary and don't match the non-quoted examples of targets in the WIKI --- contrib/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 74b83d31..aa29183c 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -9,8 +9,10 @@ # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend + # $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh FROM debian:stretch From d5efb8a620554ced4fda731c72046c4b1af53111 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 14 Dec 2021 16:49:09 -0600 Subject: [PATCH 061/561] Dynamic failable buffer sizing alsa-backend Dynamically set the alsa buffer and period based on the device's reported min/max buffer and period sizes. In the event of failure use the device's defaults. This should have no effect on devices that allow for reasonable buffer and period sizes but would allow us to be more forgiving with less reasonable devices or configurations. Closes: https://github.com/librespot-org/librespot/issues/895 --- playback/src/audio_backend/alsa.rs | 187 +++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 13 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index e572f953..4f82a097 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -4,16 +4,18 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, HwParams, PCM}; +use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; use std::process::exit; -use std::time::Duration; use thiserror::Error; -// 0.5 sec buffer. -const PERIOD_TIME: Duration = Duration::from_millis(100); -const BUFFER_TIME: Duration = Duration::from_millis(500); +const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames; +const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames; +const ZERO_FRAMES: Frames = 0; + +const MAX_PERIOD_DIVISOR: Frames = 4; +const MIN_PERIOD_DIVISOR: Frames = 10; #[derive(Debug, Error)] enum AlsaError { @@ -195,28 +197,187 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + // Clone the hwp while it's in + // a good working state so that + // in the event of an error setting + // the buffer and period sizes + // we can use the good working clone + // instead of the hwp that's in an + // error state. + let hwp_clone = hwp.clone(); - hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + // At a sampling rate of 44100: + // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms). + // The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms). + // Actual values may vary. + // + // Larger buffer and period sizes are preferred as extremely small values + // will cause high CPU useage. + // + // If no buffer or period size is in those ranges or an error happens + // trying to set the buffer or period size use the device's defaults + // which may not be ideal but are *hopefully* serviceable. - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + let buffer_size = { + let max = match hwp.get_buffer_size_max() { + Err(e) => { + trace!("Error getting the device's max Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; - let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + let min = match hwp.get_buffer_size_min() { + Err(e) => { + trace!("Error getting the device's min Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let buffer_size = if min < max { + match (MIN_BUFFER..=MAX_BUFFER) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Buffer: {:?}", size); + + match hwp.set_buffer_size_near(size) { + Err(e) => { + trace!("Error setting the device's Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Buffer size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); + ZERO_FRAMES + }; + + if buffer_size == ZERO_FRAMES { + trace!( + "Desired Buffer Frame range: {:?} - {:?}", + MIN_BUFFER, + MAX_BUFFER + ); + + trace!( + "Actual Buffer Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } + + buffer_size + }; + + let period_size = { + if buffer_size == ZERO_FRAMES { + ZERO_FRAMES + } else { + let max = match hwp.get_period_size_max() { + Err(e) => { + trace!("Error getting the device's max Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let min = match hwp.get_period_size_min() { + Err(e) => { + trace!("Error getting the device's min Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let max_period = buffer_size / MAX_PERIOD_DIVISOR; + let min_period = buffer_size / MIN_PERIOD_DIVISOR; + + let period_size = if min < max && min_period < max_period { + match (min_period..=max_period) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Period: {:?}", size); + + match hwp.set_period_size_near(size, ValueOr::Nearest) { + Err(e) => { + trace!("Error setting the device's Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Period size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); + trace!("or the desired min Period size was greater than or equal to the desired max Period size."); + ZERO_FRAMES + }; + + if period_size == ZERO_FRAMES { + trace!("Buffer size: {:?}", buffer_size); + + trace!( + "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", + min_period, + MIN_PERIOD_DIVISOR, + max_period, + MAX_PERIOD_DIVISOR, + ); + + trace!( + "Actual Period Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } + + period_size + } + }; + + if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES { + trace!( + "Failed to set Buffer and/or Period size, falling back to the device's defaults." + ); + + trace!("You may experience higher than normal CPU usage and/or audio issues."); + + pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; + } else { + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + } + + let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; // Don't assume we got what we wanted. Ask to make sure. let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; + let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + swp.set_start_threshold(frames_per_buffer - frames_per_period) .map_err(AlsaError::SwParams)?; pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - trace!("Frames per Buffer: {:?}", frames_per_buffer); - trace!("Frames per Period: {:?}", frames_per_period); + trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); + trace!("Actual Frames per Period: {:?}", frames_per_period); // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize From 2f7b9863d9c2862cf16890a755268bef3f3e50bb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 16 Dec 2021 22:42:37 +0100 Subject: [PATCH 062/561] 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 063/561] 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 064/561] 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 065/561] 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 066/561] 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 067/561] 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 068/561] 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 305f80bdfd1305028bb24cc250f1bb8e3502e10a Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 16 Dec 2021 23:13:50 -0600 Subject: [PATCH 069/561] Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index As mentioned in https://github.com/librespot-org/librespot/issues/898#issuecomment-986528998 --- CHANGELOG.md | 1 + src/main.rs | 165 ++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 133 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5757aaf..1d603a93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. - [main] Don't panic when parsing options. Instead list valid values and exit. +- [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/src/main.rs b/src/main.rs index 2ce526e1..84ad2a76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -805,41 +805,136 @@ fn get_setup() -> Setup { exit(1); }); + let is_alsa_mixer = match mixer_type.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => true, + _ => false, + }; + + #[cfg(feature = "alsa-backend")] + if !is_alsa_mixer { + for a in &[ALSA_MIXER_DEVICE, ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL] { + if opt_present(a) { + warn!("Alsa specific mixer options have no effect if not using the alsa mixer."); + break; + } + } + } + let mixer_config = { let mixer_default_config = MixerConfig::default(); #[cfg(feature = "alsa-backend")] - let device = opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - mixer_default_config.device.clone() - } - }); + let index = if !is_alsa_mixer { + mixer_default_config.index + } else { + opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index.parse::().unwrap_or_else(|_| { + invalid_error_msg( + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_SHORT, + &index, + "", + &mixer_default_config.index.to_string(), + ); - #[cfg(not(feature = "alsa-backend"))] - let device = mixer_default_config.device; - - #[cfg(feature = "alsa-backend")] - let index = opt_str(ALSA_MIXER_INDEX) - .map(|index| { - index.parse::().unwrap_or_else(|_| { - invalid_error_msg( - ALSA_MIXER_INDEX, - ALSA_MIXER_INDEX_SHORT, - &index, - "", - &mixer_default_config.index.to_string(), - ); - - exit(1); + exit(1); + }) }) - }) - .unwrap_or_else(|| mixer_default_config.index); + .unwrap_or_else(|| match device { + // Look for the dev index portion of --device. + // Specifically when --device is :CARD=,DEV= + // or :,. + + // If --device does not contain a ',' it does not contain a dev index. + // In the case that the dev index is omitted it is assumed to be 0 (mixer_default_config.index). + // Malformed --device values will also fallback to mixer_default_config.index. + Some(ref device_name) if device_name.contains(',') => { + // Turn :CARD=,DEV= or :, + // into DEV= or . + let dev = &device_name[device_name.find(',').unwrap_or_default()..] + .trim_start_matches(','); + + // Turn DEV= into (noop if it's already ) + // and then parse . + // Malformed --device values will fail the parse and fallback to mixer_default_config.index. + dev[dev.find('=').unwrap_or_default()..] + .trim_start_matches('=') + .parse::() + .unwrap_or(mixer_default_config.index) + } + _ => mixer_default_config.index, + }) + }; #[cfg(not(feature = "alsa-backend"))] let index = mixer_default_config.index; + #[cfg(feature = "alsa-backend")] + let device = if !is_alsa_mixer { + mixer_default_config.device + } else { + match opt_str(ALSA_MIXER_DEVICE) { + Some(mixer_device) => { + if mixer_device.is_empty() { + empty_string_error_msg(ALSA_MIXER_DEVICE, ALSA_MIXER_DEVICE_SHORT); + } + + mixer_device + } + None => match device { + Some(ref device_name) => { + // Look for the card name or card index portion of --device. + // Specifically when --device is :CARD=,DEV= + // or card index when --device is :,. + // --device values like `pulse`, `default`, `jack` may be valid but there is no way to + // infer automatically what the mixer should be so they fail auto fallback + // so --alsa-mixer-device must be manually specified in those situations. + let start_index = device_name.find(':').unwrap_or_default(); + + let end_index = match device_name.find(',') { + Some(index) if index > start_index => index, + _ => device_name.len(), + }; + + let card = &device_name[start_index..end_index]; + + if card.starts_with(':') { + // mixers are assumed to be hw:CARD= or hw:. + "hw".to_owned() + card + } else { + error!( + "Could not find an alsa mixer for \"{}\", it must be specified with `--{}` / `-{}`", + &device.unwrap_or_default(), + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_SHORT + ); + + exit(1); + } + } + None => { + error!( + "`--{}` / `-{}` or `--{}` / `-{}` \ + must be specified when `--{}` / `-{}` is set to \"alsa\"", + DEVICE, + DEVICE_SHORT, + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_SHORT, + MIXER_TYPE, + MIXER_TYPE_SHORT + ); + + exit(1); + } + }, + } + }; + + #[cfg(not(feature = "alsa-backend"))] + let device = mixer_default_config.device; + #[cfg(feature = "alsa-backend")] let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); @@ -881,10 +976,12 @@ fn get_setup() -> Setup { exit(1); } }) - .unwrap_or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => 0.0, // let alsa query the control - _ => VolumeCtrl::DEFAULT_DB_RANGE, + .unwrap_or_else(|| { + if is_alsa_mixer { + 0.0 + } else { + VolumeCtrl::DEFAULT_DB_RANGE + } }); let volume_ctrl = opt_str(VOLUME_CTRL) @@ -1093,10 +1190,12 @@ fn get_setup() -> Setup { (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), + .or_else(|| { + if is_alsa_mixer { + None + } else { + cache.as_ref().and_then(Cache::volume) + } }); let device_type = opt_str(DEVICE_TYPE) From 62461be1fcfcaa93bb52f32cc2f88b7fdcd6ecd7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 26 Dec 2021 21:18:42 +0100 Subject: [PATCH 070/561] 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 071/561] 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 072/561] 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 073/561] 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 074/561] 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 1f43e9e389a1f1e40f1f4ebb728661d2d09c1c65 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 23 Dec 2021 20:56:16 -0600 Subject: [PATCH 075/561] Remove that last couple unwraps from main Also: * Don't just hang if Spirc shuts down too often. * Replace the while loop with Vec retain. * Be more explicit with the rate limit. --- src/main.rs | 82 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index 84ad2a76..6eb9d84d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1568,6 +1568,9 @@ fn get_setup() -> Setup { #[tokio::main(flavor = "current_thread")] async fn main() { const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; + const RECONNECT_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(600); + const RECONNECT_RATE_LIMIT: usize = 5; + if env::var(RUST_BACKTRACE).is_err() { env::set_var(RUST_BACKTRACE, "full") } @@ -1585,14 +1588,18 @@ async fn main() { if setup.enable_discovery { 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() - .unwrap(), - ); + discovery = match librespot::discovery::Discovery::builder(device_id) + .name(setup.connect_config.name.clone()) + .device_type(setup.connect_config.device_type) + .port(setup.zeroconf_port) + .launch() + { + Ok(d) => Some(d), + Err(e) => { + error!("Discovery Error: {}", e); + exit(1); + } + } } if let Some(credentials) = setup.credentials { @@ -1609,7 +1616,12 @@ async fn main() { loop { tokio::select! { - credentials = async { discovery.as_mut().unwrap().next().await }, if discovery.is_some() => { + credentials = async { + match discovery.as_mut() { + Some(d) => d.next().await, + _ => None + } + }, if discovery.is_some() => { match credentials { Some(credentials) => { last_credentials = Some(credentials.clone()); @@ -1630,8 +1642,8 @@ async fn main() { ).fuse()); }, None => { - warn!("Discovery stopped!"); - discovery = None; + error!("Discovery stopped unexpectedly"); + exit(1); } } }, @@ -1682,20 +1694,22 @@ async fn main() { exit(1); } }, - _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { + _ = async { + if let Some(task) = spirc_task.as_mut() { + task.await; + } + }, if spirc_task.is_some() => { spirc_task = None; warn!("Spirc shut down unexpectedly"); - while !auto_connect_times.is_empty() - && ((Instant::now() - auto_connect_times[0]).as_secs() > 600) - { - let _ = auto_connect_times.remove(0); - } - if let Some(credentials) = last_credentials.clone() { - if auto_connect_times.len() >= 5 { - warn!("Spirc shut down too often. Not reconnecting automatically."); - } else { + let mut reconnect_exceeds_rate_limit = || { + auto_connect_times.retain(|&t| t.elapsed() < RECONNECT_RATE_LIMIT_WINDOW); + auto_connect_times.len() > RECONNECT_RATE_LIMIT + }; + + match last_credentials.clone() { + Some(credentials) if !reconnect_exceeds_rate_limit() => { auto_connect_times.push(Instant::now()); connecting = Box::pin(Session::connect( @@ -1703,19 +1717,25 @@ async fn main() { credentials, setup.cache.clone(), ).fuse()); - } + }, + _ => { + error!("Spirc shut down too often. Not reconnecting automatically."); + exit(1); + }, } }, - event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { + event = async { + match player_event_channel.as_mut() { + Some(p) => p.recv().await, + _ => None + } + }, if player_event_channel.is_some() => match event { Some(event) => { if let Some(program) = &setup.player_event_program { if let Some(child) = run_program_on_events(event, program) { - if child.is_ok() { - - let mut child = child.unwrap(); - + if let Ok(mut child) = child { tokio::spawn(async move { - match child.wait().await { + match child.wait().await { Ok(e) if e.success() => (), Ok(e) => { if let Some(code) = e.code() { @@ -1741,7 +1761,8 @@ async fn main() { }, _ = tokio::signal::ctrl_c() => { break; - } + }, + else => break, } } @@ -1754,7 +1775,8 @@ async fn main() { if let Some(mut spirc_task) = spirc_task { tokio::select! { _ = tokio::signal::ctrl_c() => (), - _ = spirc_task.as_mut() => () + _ = spirc_task.as_mut() => (), + else => (), } } } From b622e3811e468ca217e9ea41389f41183be7abab Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 27 Dec 2021 00:45:27 +0100 Subject: [PATCH 076/561] 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 077/561] 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 078/561] 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 079/561] 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 080/561] 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 081/561] 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 082/561] 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 083/561] 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 084/561] 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 085/561] 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 086/561] 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 087/561] 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 088/561] 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 089/561] 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 090/561] 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 f09be4850ed1c79dc77becdb103b97bbe6a26d48 Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Wed, 29 Dec 2021 16:26:24 +0100 Subject: [PATCH 091/561] Sink: pass ownership of the packet on write() Prevent a copy if the implementation needs to keep the data around. --- CHANGELOG.md | 1 + playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 16 ++++++++-------- playback/src/audio_backend/portaudio.rs | 8 ++++---- playback/src/audio_backend/rodio.rs | 2 +- playback/src/audio_backend/sdl.rs | 2 +- playback/src/player.rs | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d603a93..a2be0992 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. - [contrib] Hardened security of the systemd service units - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). +- [playback] `Sink`: `write()` now receives ownership of the packet (breaking). ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 15acf99d..b4d24949 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -66,7 +66,7 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() .map_err(|e| SinkError::OnWrite(e.to_string()))?; diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index dc21fb3d..6c903d22 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -28,7 +28,7 @@ pub trait Sink { fn stop(&mut self) -> SinkResult<()> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()>; + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -44,34 +44,34 @@ 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) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::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); + let samples_f32: &[f32] = &converter.f64_to_f32(&samples); self.write_bytes(samples_f32.as_bytes()) } AudioFormat::S32 => { - let samples_s32: &[i32] = &converter.f64_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] = &converter.f64_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] = &converter.f64_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] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(&samples); self.write_bytes(samples_s16.as_bytes()) } }, - AudioPacket::OggData(samples) => self.write_bytes(samples), + AudioPacket::OggData(samples) => self.write_bytes(&samples), } } }; diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 7a0b179f..12a5404d 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -140,7 +140,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -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) } }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index ab356d67..9f4ad059 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -189,7 +189,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() .map_err(|e| RodioError::Samples(e.to_string()))?; diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 6272fa32..1c9794a2 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -82,7 +82,7 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit diff --git a/playback/src/player.rs b/playback/src/player.rs index d8dbb190..d3bc2b39 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1384,7 +1384,7 @@ impl PlayerInternal { } } - if let Err(e) = self.sink.write(&packet, &mut self.converter) { + if let Err(e) = self.sink.write(packet, &mut self.converter) { error!("{}", e); exit(1); } From 2d699e288a43d4eb54dd451e6d66d8e6b0cc3c93 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 1 Jan 2022 20:23:21 +0100 Subject: [PATCH 092/561] 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 8dfa00d66f64180e504067f81824397638eed6a8 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 1 Jan 2022 17:19:12 -0600 Subject: [PATCH 093/561] Clean up list_compatible_devices Fix a typo and be a little more forgiving. --- playback/src/audio_backend/alsa.rs | 79 +++++++++++++++++------------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 4f82a097..16aa420d 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -62,8 +62,8 @@ enum AlsaError { #[error(" PCM, {0}")] Pcm(alsa::Error), - #[error(" Could Not Parse Ouput Name(s) and/or Description(s)")] - Parsing, + #[error(" Could Not Parse Output Name(s) and/or Description(s), {0}")] + Parsing(alsa::Error), #[error("")] NotConnected, @@ -107,49 +107,58 @@ pub struct AlsaSink { } fn list_compatible_devices() -> SinkResult<()> { + let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?; + println!("\n\n\tCompatible alsa device(s):\n"); println!("\t------------------------------------------------------\n"); - let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?; - for a in i { if let Some(Direction::Playback) = a.direction { - let name = a.name.ok_or(AlsaError::Parsing)?; - let desc = a.desc.ok_or(AlsaError::Parsing)?; + if let Some(name) = a.name { + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + // Only show devices that support + // 2 ch 44.1 Interleaved. - if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { - if let Ok(hwp) = HwParams::any(&pcm) { - // Only show devices that support - // 2 ch 44.1 Interleaved. - if hwp.set_access(Access::RWInterleaved).is_ok() - && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() - && hwp.set_channels(NUM_CHANNELS as u32).is_ok() - { - println!("\tDevice:\n\n\t\t{}\n", name); - println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t")); + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + let mut supported_formats = vec![]; - let mut supported_formats = vec![]; + for f in &[ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, + ] { + if hwp.test_format(Format::from(*f)).is_ok() { + supported_formats.push(format!("{:?}", f)); + } + } - for f in &[ - AudioFormat::S16, - AudioFormat::S24, - AudioFormat::S24_3, - AudioFormat::S32, - AudioFormat::F32, - AudioFormat::F64, - ] { - if hwp.test_format(Format::from(*f)).is_ok() { - supported_formats.push(format!("{:?}", f)); + if !supported_formats.is_empty() { + println!("\tDevice:\n\n\t\t{}\n", name); + + println!( + "\tDescription:\n\n\t\t{}\n", + a.desc.unwrap_or_default().replace("\n", "\n\t\t") + ); + + println!( + "\tSupported Format(s):\n\n\t\t{}\n", + supported_formats.join(" ") + ); + + println!( + "\t------------------------------------------------------\n" + ); } } - - println!( - "\tSupported Format(s):\n\n\t\t{}\n", - supported_formats.join(" ") - ); - println!("\t------------------------------------------------------\n"); - } - }; + }; + } } } } From 7921f239276099ac1175233fc45252e14030ea52 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 3 Jan 2022 00:13:28 +0100 Subject: [PATCH 094/561] 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 095/561] 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 096/561] 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 097/561] 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 098/561] 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 099/561] 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 100/561] 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 101/561] 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 102/561] 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 103/561] 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 104/561] 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 105/561] 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 106/561] 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 cfde70f6f90fc1351ba82662f7e9a1568812aa44 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 5 Jan 2022 16:54:25 -0600 Subject: [PATCH 107/561] Fix clippy lint warning --- 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 5d44f910f3fe00e0ab5e3427da008594b78902df Mon Sep 17 00:00:00 2001 From: Philip Deljanov Date: Wed, 5 Jan 2022 00:03:54 -0500 Subject: [PATCH 108/561] 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 109/561] 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 110/561] 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 111/561] 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 112/561] 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 113/561] 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 114/561] 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 115/561] 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 116/561] 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 117/561] 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 118/561] 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 119/561] 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 120/561] 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 121/561] 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 122/561] 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 123/561] 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 124/561] 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 125/561] 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 126/561] 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 127/561] 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 5e8e2ba8c59dbc3684244ca5180e930550e4cafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20F=C3=B6rberg?= Date: Sat, 8 Jan 2022 15:48:33 +0100 Subject: [PATCH 128/561] examples/playlist_tracks: Use normal URI parser --- examples/playlist_tracks.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 75c656bb..0bf17ee7 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -1,4 +1,5 @@ use std::env; +use std::process; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; @@ -18,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\"" + ); + process::exit(1); + }); let session = Session::connect(session_config, credentials, None) .await From 0b7ccc803c90b6fde8278a54aa053bc70938ad0e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 10 Jan 2022 21:19:47 +0100 Subject: [PATCH 129/561] 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 130/561] 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 131/561] 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 132/561] 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 133/561] 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 134/561] 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 135/561] 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 1e54913523e7897f728984e65dcfe301cf739670 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 08:20:29 +0100 Subject: [PATCH 136/561] Fix `--device` argument to various backends (#938) Fix `--device` argument to various backends --- CHANGELOG.md | 1 + playback/src/audio_backend/pipe.rs | 21 ++++++++++++----- playback/src/audio_backend/rodio.rs | 21 +++++++---------- playback/src/audio_backend/subprocess.rs | 26 +++++++++++++------- src/main.rs | 30 ------------------------ 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2be0992..0bd19240 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [contrib] Hardened security of the systemd service units - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). +- [playback] `pipe`: create file if it doesn't already exist ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index fd804a0e..682f8124 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -4,19 +4,27 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; +use std::process::exit; pub struct StdoutSink { output: Option>, - path: Option, + file: Option, format: AudioFormat, } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> Self { + fn open(file: Option, format: AudioFormat) -> Self { + if let Some("?") = file.as_deref() { + info!("Usage:"); + println!(" Output to stdout: --backend pipe"); + println!(" Output to file: --backend pipe --device {{filename}}"); + exit(0); + } + info!("Using pipe sink with format: {:?}", format); Self { output: None, - path, + file, format, } } @@ -25,11 +33,12 @@ impl Open for StdoutSink { impl Sink for StdoutSink { fn start(&mut self) -> SinkResult<()> { if self.output.is_none() { - let output: Box = match self.path.as_deref() { - Some(path) => { + let output: Box = match self.file.as_deref() { + Some(file) => { let open_op = OpenOptions::new() .write(true) - .open(path) + .create(true) + .open(file) .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; Box::new(open_op) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 9f4ad059..bbc5de1a 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -135,21 +135,18 @@ fn create_sink( host: &cpal::Host, device: Option, ) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { - let rodio_device = match device { - Some(ask) if &ask == "?" => { - let exit_code = match list_outputs(host) { - Ok(()) => 0, - Err(e) => { - error!("{}", e); - 1 - } - }; - exit(exit_code) - } + let rodio_device = match device.as_deref() { + Some("?") => match list_outputs(host) { + Ok(()) => exit(0), + Err(e) => { + error!("{}", e); + exit(1); + } + }, Some(device_name) => { host.output_devices()? .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails - .ok_or(RodioError::DeviceNotAvailable(device_name))? + .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? } None => host .default_output_device() diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index c501cf83..63fc5d88 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -5,7 +5,7 @@ use crate::decoder::AudioPacket; use shell_words::split; use std::io::Write; -use std::process::{Child, Command, Stdio}; +use std::process::{exit, Child, Command, Stdio}; pub struct SubprocessSink { shell_command: String, @@ -15,16 +15,24 @@ pub struct SubprocessSink { impl Open for SubprocessSink { fn open(shell_command: Option, format: AudioFormat) -> Self { + let shell_command = match shell_command.as_deref() { + Some("?") => { + info!("Usage: --backend subprocess --device {{shell_command}}"); + exit(0); + } + Some(cmd) => cmd.to_owned(), + None => { + error!("subprocess sink requires specifying a shell command"); + exit(1); + } + }; + info!("Using subprocess sink with format: {:?}", format); - if let Some(shell_command) = shell_command { - SubprocessSink { - shell_command, - child: None, - format, - } - } else { - panic!("subprocess sink requires specifying a shell command"); + Self { + shell_command, + child: None, + format, } } } diff --git a/src/main.rs b/src/main.rs index 6eb9d84d..9bef5184 100644 --- a/src/main.rs +++ b/src/main.rs @@ -748,18 +748,7 @@ fn get_setup() -> Setup { }) .unwrap_or_default(); - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] let device = opt_str(DEVICE); - - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] if let Some(ref value) = device { if value == "?" { backend(device, format); @@ -769,25 +758,6 @@ fn get_setup() -> Setup { } } - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - let device: Option = None; - - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - if opt_present(DEVICE) { - warn!( - "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", - DEVICE, DEVICE_SHORT, - ); - } - #[cfg(feature = "alsa-backend")] let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] From 7fe13be564f1382114758f87743d189904885eff Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:24:43 +0100 Subject: [PATCH 137/561] 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 138/561] 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 72af0d2014c7cd5f9f41ba6521e6b17f604695ce Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:31:29 +0100 Subject: [PATCH 139/561] New dynamic limiter for very wide dynamic ranges (#935) New dynamic limiter for very wide dynamic ranges --- CHANGELOG.md | 1 + playback/src/config.rs | 29 +++--- playback/src/player.rs | 213 ++++++++++++++++++----------------------- src/main.rs | 88 ++++++++--------- 4 files changed, 150 insertions(+), 181 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd19240..c0a91a29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). - [playback] `pipe`: create file if it doesn't already exist +- [playback] More robust dynamic limiter for very wide dynamic range (breaking) ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/config.rs b/playback/src/config.rs index b8313bf4..4070a26a 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,10 +1,7 @@ -use super::player::db_to_ratio; -use crate::convert::i24; -pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use std::{mem, str::FromStr, time::Duration}; -use std::mem; -use std::str::FromStr; -use std::time::Duration; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use crate::{convert::i24, player::duration_to_coefficient}; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -133,11 +130,11 @@ pub struct PlayerConfig { 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, + pub normalisation_pregain_db: f32, + pub normalisation_threshold_dbfs: f64, + pub normalisation_attack_cf: f64, + pub normalisation_release_cf: f64, + pub normalisation_knee_db: f64, // pass function pointers so they can be lazily instantiated *after* spawning a thread // (thereby circumventing Send bounds that they might not satisfy) @@ -152,11 +149,11 @@ impl Default for PlayerConfig { normalisation: false, normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), - normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-2.0), - normalisation_attack: Duration::from_millis(5), - normalisation_release: Duration::from_millis(100), - normalisation_knee: 1.0, + normalisation_pregain_db: 0.0, + normalisation_threshold_dbfs: -2.0, + normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)), + normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)), + normalisation_knee_db: 5.0, passthrough: false, ditherer: Some(mk_ditherer::), } diff --git a/playback/src/player.rs b/playback/src/player.rs index d3bc2b39..f863b0e9 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -61,12 +61,8 @@ struct PlayerInternal { 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, + normalisation_integrator: f64, + normalisation_peak: f64, auto_normalise_as_album: bool, } @@ -208,6 +204,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 { ratio.log10() * DB_VOLTAGE_RATIO } +pub fn duration_to_coefficient(duration: Duration) -> f64 { + f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64)) +} + +pub fn coefficient_to_duration(coefficient: f64) -> Duration { + Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64) +} + #[derive(Clone, Copy, Debug)] pub struct NormalisationData { track_gain_db: f32, @@ -241,17 +245,18 @@ impl NormalisationData { return 1.0; } - let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { - [data.album_gain_db, data.album_peak] + let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { + (data.album_gain_db as f64, data.album_peak as f64) } else { - [data.track_gain_db, data.track_peak] + (data.track_gain_db as f64, data.track_peak as f64) }; - let normalisation_power = gain_db as f64 + config.normalisation_pregain; + let normalisation_power = gain_db + config.normalisation_pregain_db as f64; 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_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 config.normalisation_method == NormalisationMethod::Basic { @@ -295,18 +300,25 @@ impl Player { debug!("Normalisation Type: {:?}", config.normalisation_type); debug!( "Normalisation Pregain: {:.1} dB", - config.normalisation_pregain + config.normalisation_pregain_db ); debug!( "Normalisation Threshold: {:.1} dBFS", - ratio_to_db(config.normalisation_threshold) + config.normalisation_threshold_dbfs ); 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); + // as_millis() has rounding errors (truncates) + debug!( + "Normalisation Attack: {:.0} ms", + coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000. + ); + debug!( + "Normalisation Release: {:.0} ms", + coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000. + ); + debug!("Normalisation Knee: {} dB", config.normalisation_knee_db); } } @@ -329,12 +341,8 @@ impl Player { event_senders: [event_sender].to_vec(), converter, - limiter_active: false, - limiter_attack_counter: 0, - limiter_release_counter: 0, - limiter_peak_sample: 0.0, - limiter_factor: 1.0, - limiter_strength: 0.0, + normalisation_peak: 0.0, + normalisation_integrator: 0.0, auto_normalise_as_album: false, }; @@ -1275,110 +1283,82 @@ impl PlayerInternal { Some(mut packet) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { + // 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; + for sample in data.iter_mut() { - let mut actual_normalisation_factor = normalisation_factor; + *sample *= normalisation_factor; // for both the basic and dynamic limiter + + // 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 { - if self.limiter_active { - // "S"-shaped curve with a configurable knee during attack and release: - // - > 1.0 yields soft knees at start and end, steeper in between - // - 1.0 yields a linear function from 0-100% - // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between - // - 0.0 yields a step response to 50%, causing distortion - // - Rates < 0.0 invert the limiter and are invalid - let mut shaped_limiter_strength = self.limiter_strength; - if shaped_limiter_strength > 0.0 - && shaped_limiter_strength < 1.0 - { - shaped_limiter_strength = 1.0 - / (1.0 - + f64::powf( - shaped_limiter_strength - / (1.0 - shaped_limiter_strength), - -self.config.normalisation_knee, - )); - } - actual_normalisation_factor = - (1.0 - shaped_limiter_strength) * normalisation_factor - + shaped_limiter_strength * self.limiter_factor; + // steps 1 + 2: half-wave rectification and conversion into dB + let abs_sample_db = ratio_to_db(sample.abs()); + + // 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; + } + + // 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) + } else { + threshold_db as f64 }; - // 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; + // step 4: subtractor + let limiter_input = abs_sample_db - limited_sample; - // 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); - 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)) - 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); - - if abs_sample > self.limiter_peak_sample { - self.limiter_peak_sample = abs_sample; - self.limiter_factor = - self.config.normalisation_threshold - / self.limiter_peak_sample; - } - } else if self.limiter_active { - if self.limiter_attack_counter > 0 { - // Release may start within the attack period, before - // 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)) - as u32; - self.limiter_attack_counter = 0; - } - - self.limiter_release_counter = - self.limiter_release_counter.saturating_add(1); - - if self.limiter_release_counter - > (samples_per_second * normalisation_release) as u32 - { - self.reset_limiter(); - } else { - self.limiter_strength = ((samples_per_second - * normalisation_release) - - limiter_release_counter) - / (samples_per_second * normalisation_release); - } + // Spare the CPU unless the limiter is active or we are riding a peak. + if !(limiter_input > 0.0 + || self.normalisation_integrator > 0.0 + || self.normalisation_peak > 0.0) + { + continue; } + + // 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); } - *sample *= actual_normalisation_factor; } } + // Apply volume attenuation last. TODO: make this so we can chain + // the normaliser and mixer as a processing pipeline. if let Some(ref editor) = self.audio_filter { editor.modify_stream(data) } @@ -1411,15 +1391,6 @@ impl PlayerInternal { } } - fn reset_limiter(&mut self) { - self.limiter_active = false; - self.limiter_release_counter = 0; - self.limiter_attack_counter = 0; - self.limiter_peak_sample = 0.0; - self.limiter_factor = 1.0; - self.limiter_strength = 0.0; - } - fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 9bef5184..6c432808 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ 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}; +use librespot::playback::player::{coefficient_to_duration, duration_to_coefficient, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; @@ -186,8 +186,8 @@ struct Setup { fn get_setup() -> Setup { const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; - const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.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; @@ -540,7 +540,7 @@ fn get_setup() -> Setup { .optopt( NORMALISATION_KNEE_SHORT, NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", + "Knee width (dB) of the dynamic limiter from 0.0 to 10.0. Defaults to 5.0.", "KNEE", ) .optopt( @@ -1257,11 +1257,11 @@ fn get_setup() -> Setup { let normalisation_method; let normalisation_type; - let normalisation_pregain; - let normalisation_threshold; - let normalisation_attack; - let normalisation_release; - let normalisation_knee; + let normalisation_pregain_db; + let normalisation_threshold_dbfs; + let normalisation_attack_cf; + let normalisation_release_cf; + let normalisation_knee_db; if !normalisation { for a in &[ @@ -1284,11 +1284,11 @@ fn get_setup() -> Setup { normalisation_method = player_default_config.normalisation_method; normalisation_type = player_default_config.normalisation_type; - normalisation_pregain = player_default_config.normalisation_pregain; - normalisation_threshold = player_default_config.normalisation_threshold; - normalisation_attack = player_default_config.normalisation_attack; - normalisation_release = player_default_config.normalisation_release; - normalisation_knee = player_default_config.normalisation_knee; + normalisation_pregain_db = player_default_config.normalisation_pregain_db; + normalisation_threshold_dbfs = player_default_config.normalisation_threshold_dbfs; + normalisation_attack_cf = player_default_config.normalisation_attack_cf; + normalisation_release_cf = player_default_config.normalisation_release_cf; + normalisation_knee_db = player_default_config.normalisation_knee_db; } else { normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() @@ -1338,8 +1338,8 @@ fn get_setup() -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| match pregain.parse::() { + normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN) + .map(|pregain| match pregain.parse::() { Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, _ => { let valid_values = &format!( @@ -1353,19 +1353,17 @@ fn get_setup() -> Setup { NORMALISATION_PREGAIN_SHORT, &pregain, valid_values, - &player_default_config.normalisation_pregain.to_string(), + &player_default_config.normalisation_pregain_db.to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_pregain); + .unwrap_or(player_default_config.normalisation_pregain_db); - normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) + normalisation_threshold_dbfs = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| match threshold.parse::() { - Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { - db_to_ratio(value) - } + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => value, _ => { let valid_values = &format!( "{} - {}", @@ -1378,18 +1376,20 @@ fn get_setup() -> Setup { NORMALISATION_THRESHOLD_SHORT, &threshold, valid_values, - &ratio_to_db(player_default_config.normalisation_threshold).to_string(), + &player_default_config + .normalisation_threshold_dbfs + .to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_threshold); + .unwrap_or(player_default_config.normalisation_threshold_dbfs); - normalisation_attack = opt_str(NORMALISATION_ATTACK) + normalisation_attack_cf = opt_str(NORMALISATION_ATTACK) .map(|attack| match attack.parse::() { Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1403,8 +1403,7 @@ fn get_setup() -> Setup { NORMALISATION_ATTACK_SHORT, &attack, valid_values, - &player_default_config - .normalisation_attack + &coefficient_to_duration(player_default_config.normalisation_attack_cf) .as_millis() .to_string(), ); @@ -1412,12 +1411,12 @@ fn get_setup() -> Setup { exit(1); } }) - .unwrap_or(player_default_config.normalisation_attack); + .unwrap_or(player_default_config.normalisation_attack_cf); - normalisation_release = opt_str(NORMALISATION_RELEASE) + normalisation_release_cf = opt_str(NORMALISATION_RELEASE) .map(|release| match release.parse::() { Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1431,18 +1430,19 @@ fn get_setup() -> Setup { NORMALISATION_RELEASE_SHORT, &release, valid_values, - &player_default_config - .normalisation_release - .as_millis() - .to_string(), + &coefficient_to_duration( + player_default_config.normalisation_release_cf, + ) + .as_millis() + .to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_release); + .unwrap_or(player_default_config.normalisation_release_cf); - normalisation_knee = opt_str(NORMALISATION_KNEE) + normalisation_knee_db = opt_str(NORMALISATION_KNEE) .map(|knee| match knee.parse::() { Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, _ => { @@ -1457,13 +1457,13 @@ fn get_setup() -> Setup { NORMALISATION_KNEE_SHORT, &knee, valid_values, - &player_default_config.normalisation_knee.to_string(), + &player_default_config.normalisation_knee_db.to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_knee); + .unwrap_or(player_default_config.normalisation_knee_db); } let ditherer_name = opt_str(DITHER); @@ -1505,11 +1505,11 @@ fn get_setup() -> Setup { normalisation, normalisation_type, normalisation_method, - normalisation_pregain, - normalisation_threshold, - normalisation_attack, - normalisation_release, - normalisation_knee, + normalisation_pregain_db, + normalisation_threshold_dbfs, + normalisation_attack_cf, + normalisation_release_cf, + normalisation_knee_db, ditherer, } }; From 8811b89b2d243c6b5ffba6246bf65eec1e3972b3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:45:31 +0100 Subject: [PATCH 140/561] 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 141/561] 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 142/561] 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 143/561] 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 144/561] 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 c6e97a7f8ab4b7d97b82427071b2f1555557e0cc Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 17 Jan 2022 15:57:30 -0600 Subject: [PATCH 145/561] Save some more CPU cycles in the limiter (#939) Optimise limiter CPU usage --- playback/src/config.rs | 2 +- playback/src/player.rs | 112 ++++++++++++++++++++++------------------- src/main.rs | 4 +- 3 files changed, 62 insertions(+), 56 deletions(-) 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/player.rs b/playback/src/player.rs index f863b0e9..b5603491 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -214,10 +214,10 @@ pub fn coefficient_to_duration(coefficient: f64) -> Duration { #[derive(Clone, Copy, Debug)] pub struct NormalisationData { - track_gain_db: f32, - track_peak: f32, - album_gain_db: f32, - album_peak: f32, + track_gain_db: f64, + track_peak: f64, + album_gain_db: f64, + album_peak: f64, } impl NormalisationData { @@ -225,10 +225,10 @@ impl NormalisationData { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; - 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, @@ -246,17 +246,17 @@ 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 normalisation_power = gain_db + config.normalisation_pregain_db; let mut normalisation_factor = db_to_ratio(normalisation_power); 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; + db_to_ratio(config.normalisation_threshold_dbfs) / gain_peak; let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { @@ -279,7 +279,7 @@ impl NormalisationData { normalisation_factor * 100.0 ); - normalisation_factor as f64 + normalisation_factor } } @@ -1305,54 +1305,60 @@ impl PlayerInternal { // 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()); + // 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-2: half-wave rectification and conversion into dB + let abs_sample_db = ratio_to_db(sample.abs()); - // 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; - } + // step 3-4: gain computer with soft knee and subtractor + let bias_db = abs_sample_db - 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 { + abs_sample_db + - (abs_sample_db + - (bias_db + knee_db / 2.0).powi(2) + / (2.0 * knee_db)) + } else { + abs_sample_db - threshold_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 + self.normalisation_integrator = f64::max( + limiter_db, + release_cf * self.normalisation_integrator + + (1.0 - release_cf) * limiter_db, + ); + 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); } - - // 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); } } } diff --git a/src/main.rs b/src/main.rs index 6c432808..f43500fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,7 +187,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; @@ -1339,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!( From f2625965b3e5d5dd5a66822d3b2829aa08ffcfa3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 22 Jan 2022 21:13:11 +0100 Subject: [PATCH 146/561] 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 147/561] 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 148/561] 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 149/561] 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 150/561] 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 ceebb374f0db8848a8d4135c5a06b466f2d963e1 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Sun, 23 Jan 2022 12:02:04 -0600 Subject: [PATCH 151/561] Remove unsafe code (#940) Remove unsafe code --- CHANGELOG.md | 1 + core/src/cache.rs | 17 ++- core/src/spotify_id.rs | 34 +++-- metadata/src/lib.rs | 262 +++++++++++++++++++++++------------- playback/src/player.rs | 7 +- src/player_event_handler.rs | 124 +++++++++++++---- 6 files changed, 295 insertions(+), 150 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a91a29..74809104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ 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) ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/core/src/cache.rs b/core/src/cache.rs index da2ad022..f8b0ca2c 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -351,12 +351,17 @@ impl Cache { } 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!("Invalid FileId: {}", e.utf8_error()); + None + } + } } pub fn file(&self, file: FileId) -> Option { diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index e6e2bae0..10298a42 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -2,6 +2,7 @@ use std::convert::TryInto; use std::fmt; +use std::string::FromUtf8Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SpotifyAudioType { @@ -136,7 +137,7 @@ impl SpotifyId { /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. - pub fn to_base16(&self) -> String { + pub fn to_base16(&self) -> Result { to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) } @@ -144,7 +145,7 @@ impl SpotifyId { /// character long `String`. /// /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn to_base62(&self) -> String { + pub fn to_base62(&self) -> Result { let mut dst = [0u8; 22]; let mut i = 0; let n = self.id; @@ -182,10 +183,7 @@ 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()) } /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in @@ -202,7 +200,7 @@ 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 { + pub fn to_uri(&self) -> Result { // 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(); @@ -210,9 +208,10 @@ impl SpotifyId { dst.push_str("spotify:"); dst.push_str(audio_type); dst.push(':'); - dst.push_str(&self.to_base62()); + let base62 = self.to_base62()?; + dst.push_str(&base62); - dst + Ok(dst) } } @@ -220,7 +219,7 @@ impl SpotifyId { pub struct FileId(pub [u8; 20]); impl FileId { - pub fn to_base16(&self) -> String { + pub fn to_base16(&self) -> Result { to_base16(&self.0, &mut [0u8; 40]) } } @@ -233,12 +232,12 @@ 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()) } } #[inline] -fn to_base16(src: &[u8], buf: &mut [u8]) -> String { +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]; @@ -246,10 +245,7 @@ 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()) } #[cfg(test)] @@ -366,7 +362,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_base62(), c.base62); + assert_eq!(id.to_base62().unwrap(), c.base62); } } @@ -389,7 +385,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_base16(), c.base16); + assert_eq!(id.to_base16().unwrap(), c.base16); } } @@ -415,7 +411,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_uri(), c.uri); + assert_eq!(id.to_uri().unwrap(), c.uri); } } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 2ed9273e..435633ad 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -7,12 +7,12 @@ extern crate log; extern crate async_trait; pub mod cover; - use std::collections::HashMap; +use std::string::FromUtf8Error; use librespot_core::mercury::MercuryError; use librespot_core::session::Session; -use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId}; +use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId, SpotifyIdError}; use librespot_protocol as protocol; use protobuf::Message; @@ -83,33 +83,48 @@ trait AudioFiles { #[async_trait] impl AudioFiles for Track { async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { - let item = Self::get(session, id).await?; - Ok(AudioItem { - id, - uri: format!("spotify:track:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: Some(item.alternatives), - }) + match id.to_base62() { + Err(e) => { + warn!("Invalid Track SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let item = Self::get(session, id).await?; + Ok(AudioItem { + id, + uri: format!("spotify:track:{}", uri), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: Some(item.alternatives), + }) + } + } } } #[async_trait] impl AudioFiles for Episode { async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { - 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, - }) + match id.to_base62() { + Err(e) => { + warn!("Invalid Episode SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let item = Self::get(session, id).await?; + Ok(AudioItem { + id, + uri: format!("spotify:episode:{}", uri), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: None, + }) + } + } } } @@ -117,16 +132,38 @@ impl AudioFiles for Episode { pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; - fn request_url(id: SpotifyId) -> String; - fn parse(msg: &Self::Message, session: &Session) -> Self; + fn request_url(id: SpotifyId) -> Result; + fn parse(msg: &Self::Message, session: &Session) -> Result; 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)) + match Self::request_url(id) { + Err(e) => { + warn!("Invalid SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let response = session.mercury().get(uri).await?; + match response.payload.first() { + None => { + warn!("Empty payload"); + Err(MercuryError) + } + Some(data) => match Self::Message::parse_from_bytes(data) { + Err(e) => { + warn!("Error parsing message from bytes: {}", e); + Err(MercuryError) + } + Ok(msg) => match Self::parse(&msg, session) { + Err(e) => { + warn!("Error parsing message: {:?}", e); + Err(MercuryError) + } + Ok(parsed_msg) => Ok(parsed_msg), + }, + }, + } + } + } } } @@ -192,101 +229,125 @@ pub struct Artist { impl Metadata for Track { type Message = protocol::metadata::Track; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/track/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/track/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { 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::>(); + .filter_map(|artist| { + if artist.has_gid() { + SpotifyId::from_raw(artist.get_gid()).ok() + } else { + None + } + }) + .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)) + .filter_map(|file| { + if file.has_file_id() { + let mut dst = [0u8; 20]; + dst.clone_from_slice(file.get_file_id()); + Some((file.get_format(), FileId(dst))) + } else { + None + } }) .collect(); - Track { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Track { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), duration: msg.get_duration(), - album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), + album: SpotifyId::from_raw(msg.get_album().get_gid())?, artists, files, alternatives: msg .get_alternative() .iter() - .map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap()) + .filter_map(|alt| SpotifyId::from_raw(alt.get_gid()).ok()) .collect(), available: parse_restrictions(msg.get_restriction(), &country, "premium"), - } + }) } } impl Metadata for Album { type Message = protocol::metadata::Album; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/album/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/album/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let artists = msg .get_artist() .iter() - .filter(|artist| artist.has_gid()) - .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) - .collect::>(); + .filter_map(|artist| { + if artist.has_gid() { + SpotifyId::from_raw(artist.get_gid()).ok() + } else { + None + } + }) + .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::>(); + .filter_map(|track| { + if track.has_gid() { + SpotifyId::from_raw(track.get_gid()).ok() + } else { + None + } + }) + .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) + .filter_map(|image| { + if image.has_file_id() { + let mut dst = [0u8; 20]; + dst.clone_from_slice(image.get_file_id()); + Some(FileId(dst)) + } else { + None + } }) - .collect::>(); + .collect(); - Album { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Album { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), artists, tracks, covers, - } + }) } } impl Metadata for Playlist { type Message = protocol::playlist4changes::SelectedListContent; - fn request_url(id: SpotifyId) -> String { - format!("hm://playlist/v2/playlist/{}", id.to_base62()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base62()?; + Ok(format!("hm://playlist/v2/playlist/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let tracks = msg .get_contents() .get_items() @@ -306,23 +367,24 @@ impl Metadata for Playlist { ); } - Playlist { + Ok(Playlist { revision: msg.get_revision().to_vec(), name: msg.get_attributes().get_name().to_owned(), tracks, user: msg.get_owner_username().to_string(), - } + }) } } impl Metadata for Artist { type Message = protocol::metadata::Artist; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/artist/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/artist/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { let country = session.country(); let top_tracks: Vec = match msg @@ -333,17 +395,22 @@ impl Metadata for Artist { Some(tracks) => tracks .get_track() .iter() - .filter(|track| track.has_gid()) - .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) - .collect::>(), + .filter_map(|track| { + if track.has_gid() { + SpotifyId::from_raw(track.get_gid()).ok() + } else { + None + } + }) + .collect(), None => Vec::new(), }; - Artist { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Artist { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), top_tracks, - } + }) } } @@ -351,11 +418,12 @@ impl Metadata for Artist { impl Metadata for Episode { type Message = protocol::metadata::Episode; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/episode/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/episode/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { let country = session.country(); let files = msg @@ -379,9 +447,9 @@ impl Metadata for Episode { dst.clone_from_slice(image.get_file_id()); FileId(dst) }) - .collect::>(); + .collect(); - Episode { + Ok(Episode { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), external_url: msg.get_external_url().to_owned(), @@ -392,24 +460,30 @@ impl Metadata for Episode { files, available: parse_restrictions(msg.get_restriction(), &country, "premium"), explicit: msg.get_explicit().to_owned(), - } + }) } } impl Metadata for Show { type Message = protocol::metadata::Show; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/show/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/show/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let episodes = msg .get_episode() .iter() - .filter(|episode| episode.has_gid()) - .map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap()) - .collect::>(); + .filter_map(|episode| { + if episode.has_gid() { + SpotifyId::from_raw(episode.get_gid()).ok() + } else { + None + } + }) + .collect(); let covers = msg .get_covers() @@ -421,15 +495,15 @@ impl Metadata for Show { dst.clone_from_slice(image.get_file_id()); FileId(dst) }) - .collect::>(); + .collect(); - Show { + Ok(Show { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), publisher: msg.get_publisher().to_owned(), episodes, covers, - } + }) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index b5603491..b2bdea0c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -740,7 +740,10 @@ impl PlayerTrackLoader { let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { - warn!("<{}> is not available", spotify_id.to_uri()); + warn!( + "<{}> is not available", + spotify_id.to_uri().unwrap_or_default() + ); return None; } }; @@ -748,7 +751,7 @@ impl PlayerTrackLoader { 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; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 4c75128c..785290ed 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -5,6 +5,7 @@ use tokio::process::{Child as AsyncChild, Command as AsyncCommand}; use std::collections::HashMap; use std::io; +use std::io::{Error, ErrorKind}; use std::process::{Command, ExitStatus}; pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { @@ -13,45 +14,110 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option { - 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Changed: Invalid old track id: {}", + e.utf8_error() + ), + ))) + } + Ok(old_id) => match new_track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Changed: Invalid new track id: {}", + e.utf8_error() + ), + ))) + } + 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Started: Invalid track id: {}", e.utf8_error()), + ))) + } + 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Stopped: Invalid track id: {}", e.utf8_error()), + ))) + } + 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Playing: Invalid track id: {}", e.utf8_error()), + ))) + } + 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Paused: Invalid track id: {}", e.utf8_error()), + ))) + } + 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(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Preloading: Invalid track id: {}", + e.utf8_error() + ), + ))) + } + 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()); From 8498ad807829b0b273c8bbd1c01b04fa10146d72 Mon Sep 17 00:00:00 2001 From: SuisChan Date: Mon, 24 Jan 2022 12:52:15 +0100 Subject: [PATCH 152/561] 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 153/561] 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 154/561] 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 155/561] 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 156/561] 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 157/561] 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 158/561] 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 159/561] 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 160/561] 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 161/561] 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 03e71f6e0a94265c3a3106e34a87264cc9e825fd Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Thu, 27 Jan 2022 00:40:59 -0600 Subject: [PATCH 162/561] simplify get_factor (#942) Simplify `get_factor` --- CHANGELOG.md | 1 + playback/src/player.rs | 58 ++++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74809104..c1a8fd73 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/playback/src/player.rs b/playback/src/player.rs index b2bdea0c..48129177 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -31,6 +31,7 @@ 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 const PCM_AT_0DBFS: f64 = 1.0; pub struct Player { commands: Option>, @@ -251,26 +252,57 @@ impl NormalisationData { (data.track_gain_db, data.track_peak) }; - let normalisation_power = gain_db + config.normalisation_pregain_db; - 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) / 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!( From c8971dce63f24a2e19da07fd5ce52e09bfdcf40e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 27 Jan 2022 18:39:28 +0100 Subject: [PATCH 163/561] Fix Alsa softvol linear mapping (#950) Use `--volume-range` overrides --- CHANGELOG.md | 1 + playback/src/mixer/alsamixer.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a8fd73..24d1a17a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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 ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. 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), From 3c749a8f0e66df865701bbfa36df37e8ef258b9e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 27 Jan 2022 17:00:08 -0600 Subject: [PATCH 164/561] Remove basic normalisation deprecation warning --- src/main.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index f43500fe..c70f39da 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1293,12 +1293,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, @@ -1308,16 +1303,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); From edb98d5c1dce233f30ac4781bf74a939701291e9 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 31 Jan 2022 18:20:10 -0600 Subject: [PATCH 165/561] Prevent shuffle crash fixes https://github.com/librespot-org/librespot/issues/959 --- connect/src/spirc.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 344f63b7..b574ff52 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -668,15 +668,15 @@ impl SpircTask { self.state.set_shuffle(frame.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); From d54f3982a0b265e11270d2743d847853727dc67f Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 1 Feb 2022 17:48:13 -0600 Subject: [PATCH 166/561] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d1a17a..6db058bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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. From e64f09fd777d1015f924459419788e8f8765a20a Mon Sep 17 00:00:00 2001 From: Philip Deljanov Date: Tue, 1 Feb 2022 19:02:14 -0500 Subject: [PATCH 167/561] 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 6f6d330bce17061ea5960a593f5c0579a0a822f6 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Mon, 7 Feb 2022 23:37:29 +0100 Subject: [PATCH 168/561] Only log runtime argument if it starts with a dash "-" When there's a value that corresponds to an argument name used in the same command line, the logging of the arguments gets confused and logs the matching argument, but without the leading dash: ``` % target/debug/librespot -n c --verbose -c /path/to/my/cache [2022-02-07T22:32:25Z INFO librespot] librespot 0.3.1 55ced49 (Built on 2022-02-07, Build ID: qaEB8kEW, Profile: debug) [2022-02-07T22:32:25Z TRACE librespot] Command line argument(s): [2022-02-07T22:32:25Z TRACE librespot] -n "c" [2022-02-07T22:32:25Z TRACE librespot] c "/path/to/my/cache" [2022-02-07T22:32:25Z TRACE librespot] --verbose [2022-02-07T22:32:25Z TRACE librespot] -c "/path/to/my/cache" ``` Here we're using the literal `c` as the device name, and the `-c` argument. Thus the `c /path/to/my/cache` is logged in addition to `-c /path...`. After checking whether the key has a leading dash, this issue is gone: ``` % target/debug/librespot -n c --verbose -c /path/to/my/cache [2022-02-07T22:32:41Z INFO librespot] librespot 0.3.1 55ced49 (Built on 2022-02-07, Build ID: qaEB8kEW, Profile: debug) [2022-02-07T22:32:41Z TRACE librespot] Command line argument(s): [2022-02-07T22:32:41Z TRACE librespot] -n "c" [2022-02-07T22:32:41Z TRACE librespot] --verbose [2022-02-07T22:32:41Z TRACE librespot] -c "/path/to/my/cache" ``` --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index c70f39da..e9969f50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -661,6 +661,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) 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 169/561] 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 616809b64c1d517cd6cf120ab1bc39f2cd7b0a2a Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Sun, 13 Feb 2022 15:50:32 -0600 Subject: [PATCH 170/561] Quantum-realm level normalisation optimization (#965) This saves up to 1-2% CPU useage on a PI 4 depending on how much normalisation is actually being done. * We don't need to test against EPSILON. The factor will never be over 1.0 in basic normalisation mode. * Don't check the normalisation mode EVERY sample. * Do as little math as possible by simplfiying all equations as much as possible (while still retaining the textbook equations in comments). * Misc cleanup --- playback/src/convert.rs | 40 +++++------ playback/src/dither.rs | 6 +- playback/src/player.rs | 147 +++++++++++++++++++++------------------- 3 files changed, 96 insertions(+), 97 deletions(-) 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/player.rs b/playback/src/player.rs index 48129177..74ba1fc4 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -760,7 +760,16 @@ impl PlayerTrackLoader { position_ms: u32, ) -> Option { let audio = match AudioItem::get_audio_item(&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; @@ -769,17 +778,6 @@ impl PlayerTrackLoader { info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); - let 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; - } - }; - if audio.duration < 0 { error!( "Track duration for <{}> cannot be {}", @@ -809,26 +807,24 @@ 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 => { - warn!("<{}> 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); let play_from_beginning = position_ms == 0; - // 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( @@ -1321,25 +1317,30 @@ 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. - // 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 - { // 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. @@ -1348,22 +1349,26 @@ impl PlayerInternal { // 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-2: half-wave rectification and conversion into dB - let abs_sample_db = ratio_to_db(sample.abs()); - - // step 3-4: gain computer with soft knee and subtractor - let bias_db = abs_sample_db - threshold_db; + // 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; if knee_boundary_db < -knee_db { 0.0 } else if knee_boundary_db.abs() <= knee_db { - abs_sample_db - - (abs_sample_db - - (bias_db + knee_db / 2.0).powi(2) - / (2.0 * 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 { - abs_sample_db - threshold_db + // Textbook: + // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db. + bias_db } } else { 0.0 @@ -1377,14 +1382,24 @@ impl PlayerInternal { || self.normalisation_peak > 0.0 { // 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 - + (1.0 - release_cf) * limiter_db, + - 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 - + (1.0 - attack_cf) * self.normalisation_integrator; + - 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, @@ -1897,15 +1912,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( @@ -2079,10 +2087,7 @@ impl Seek for Subfile { }; let newpos = self.stream.seek(pos)?; - if newpos > self.offset { - Ok(newpos - self.offset) - } else { - Ok(0) - } + + Ok(newpos.saturating_sub(self.offset)) } } From 47f1362453d7b4b99933578c044709251da5918b Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 14 Feb 2022 05:15:19 -0600 Subject: [PATCH 171/561] 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 172/561] 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 173/561] 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 e0e23c9167db6f56a6645c32ea73bb627e5f9d13 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 7 Mar 2022 20:12:18 -0600 Subject: [PATCH 174/561] Use the librespot name arg for the app name in the PulseAudio backend This sets the name displayed by PulseAudio to Librespot - Instance Name if a name is given otherwise Librespot (the default name). This also sets the correct "role" as per the docs: https://www.freedesktop.org/wiki/Software/PulseAudio/Documentation/Developer/Clients/ApplicationProperties/ PA_PROP_MEDIA_ROLE "This is a property of the actual streamed data, not so much the application" Roles are used for policies, things like automatically muting a music player when a call comes in and whatnot. For bonus points this also sets PULSE_PROP_application.icon_name to audio-x-generic so that we get a nice icon in the PulseAudio settings by our name instead of a missing icon placeholder. --- 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 e9969f50..8d81834d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1124,6 +1124,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 dc9f822c802f353b19085ddce13bc90df4204399 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 19 Mar 2022 21:15:46 -0500 Subject: [PATCH 175/561] 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 176/561] 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 177/561] 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 178/561] 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 179/561] 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 180/561] 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 10c9a0f8eab743a2d59268eed0a2eac53a270ab8 Mon Sep 17 00:00:00 2001 From: Gnarflord Date: Thu, 19 May 2022 18:37:21 +0200 Subject: [PATCH 181/561] Use libmdns 0.7 to avoid packet 47 error --- discovery/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 9b4d415e..4dccdc1e 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -15,7 +15,7 @@ form_urlencoded = "1.0" futures-core = "0.3" hmac = "0.11" hyper = { version = "0.14", features = ["server", "http1", "tcp"] } -libmdns = "0.6" +libmdns = "0.7" log = "0.4" rand = "0.8" serde_json = "1.0.25" From 7efc62b9cad876bb438af8ab75458db6d2db17dc Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Thu, 19 May 2022 15:23:14 -0500 Subject: [PATCH 182/561] Remove the volume sample iteration (#986) Move volume calculations out of their own separate samples iteration and into the normalisation iteration --- examples/play.rs | 3 +- playback/src/mixer/mod.rs | 16 ++- playback/src/mixer/softmixer.rs | 23 ++-- playback/src/player.rs | 205 ++++++++++++++++---------------- src/main.rs | 4 +- 5 files changed, 126 insertions(+), 125 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index d6c7196d..6156cb7b 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -6,6 +6,7 @@ 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::mixer::NoOpVolume; use librespot::playback::player::Player; #[tokio::main] @@ -30,7 +31,7 @@ async fn main() { .await .unwrap(); - let (mut player, _) = Player::new(player_config, session, None, move || { + let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || { backend(None, audio_format) }); diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index a3c7a5a1..0a8b8d6c 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -3,6 +3,8 @@ use crate::config::VolumeCtrl; pub mod mappings; use self::mappings::MappedCtrl; +pub struct NoOpVolume; + pub trait Mixer: Send { fn open(config: MixerConfig) -> Self where @@ -11,13 +13,19 @@ pub trait Mixer: Send { fn set_volume(&self, volume: u16); fn volume(&self) -> u16; - fn get_audio_filter(&self) -> Option> { - None + fn get_soft_volume(&self) -> Box { + Box::new(NoOpVolume) } } -pub trait AudioFilter { - fn modify_stream(&self, data: &mut [f64]); +pub trait VolumeGetter { + fn attenuation_factor(&self) -> f64; +} + +impl VolumeGetter for NoOpVolume { + fn attenuation_factor(&self) -> f64 { + 1.0 + } } pub mod softmixer; diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index cefc2de5..93da5fec 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,7 +1,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -use super::AudioFilter; +use super::VolumeGetter; use super::{MappedCtrl, VolumeCtrl}; use super::{Mixer, MixerConfig}; @@ -35,10 +35,8 @@ impl Mixer for SoftMixer { .store(mapped_volume.to_bits(), Ordering::Relaxed) } - fn get_audio_filter(&self) -> Option> { - Some(Box::new(SoftVolumeApplier { - volume: self.volume.clone(), - })) + fn get_soft_volume(&self) -> Box { + Box::new(SoftVolume(self.volume.clone())) } } @@ -46,17 +44,10 @@ impl SoftMixer { pub const NAME: &'static str = "softvol"; } -struct SoftVolumeApplier { - volume: Arc, -} +struct SoftVolume(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 { - for x in data.iter_mut() { - *x *= volume; - } - } +impl VolumeGetter for SoftVolume { + fn attenuation_factor(&self) -> f64 { + f64::from_bits(self.0.load(Ordering::Relaxed)) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 74ba1fc4..a6935010 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -25,7 +25,7 @@ use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}; use crate::metadata::{AudioItem, FileFormat}; -use crate::mixer::AudioFilter; +use crate::mixer::VolumeGetter; use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; @@ -58,7 +58,7 @@ struct PlayerInternal { sink: Box, sink_status: SinkStatus, sink_event_callback: Option, - audio_filter: Option>, + volume_getter: Box, event_senders: Vec>, converter: Converter, @@ -319,7 +319,7 @@ impl Player { pub fn new( config: PlayerConfig, session: Session, - audio_filter: Option>, + volume_getter: Box, sink_builder: F, ) -> (Player, PlayerEventChannel) where @@ -369,7 +369,7 @@ impl Player { sink: sink_builder(), sink_status: SinkStatus::Closed, sink_event_callback: None, - audio_filter, + volume_getter, event_senders: [event_sender].to_vec(), converter, @@ -1314,109 +1314,110 @@ impl PlayerInternal { Some(mut packet) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { + // Get the volume for the packet. + // In the case of hardware volume control this will + // always be 1.0 (no change). + let volume = self.volume_getter.attenuation_factor(); + // 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 { - 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; - - // 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. 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; - - 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 { - 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 - { - // 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); - } - } + // No matter the case we apply volume attenuation last if there is any. + if !self.config.normalisation && volume < 1.0 { + for sample in data.iter_mut() { + *sample *= volume; } - } + } else if self.config.normalisation_method == NormalisationMethod::Basic + && (normalisation_factor < 1.0 || volume < 1.0) + { + for sample in data.iter_mut() { + *sample *= normalisation_factor * volume; + } + } 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; - // Apply volume attenuation last. TODO: make this so we can chain - // the normaliser and mixer as a processing pipeline. - if let Some(ref editor) = self.audio_filter { - editor.modify_stream(data) + 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. + + // 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; + + 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 { + 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 + { + // 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); + } + + *sample *= volume; + } } } diff --git a/src/main.rs b/src/main.rs index 8d81834d..59ab0ce6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1648,12 +1648,12 @@ async fn main() { let player_config = setup.player_config.clone(); let connect_config = setup.connect_config.clone(); - let audio_filter = mixer.get_audio_filter(); + let soft_volume = mixer.get_soft_volume(); 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 || { + Player::new(player_config, session.clone(), soft_volume, move || { (backend)(device, format) }); From c4af90f5febaa99b80e1ae9fbed8f638150728f5 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Tue, 3 May 2022 22:20:14 +0200 Subject: [PATCH 183/561] Avoid crashing when Avahi is not available When librespot is built with Avahi turned on, it will crash if Avahi is later not available at runtime. This change avoids it crashing hard when Avahi is not available; librespot will merely warn of the issue. This affects some distribution packages too, where the maintainer might prefer leaving Avahi support enabled, but many setups don't (or can't) run Avahi. Co-authored-by: Nick Steel --- CHANGELOG.md | 1 + discovery/src/lib.rs | 3 +-- src/main.rs | 17 +++++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d93b636c..a1d92e1b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [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. +- [main] Fix crash when built with Avahi support but Avahi is locally unavailable. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index b1249a0d..b2e65586 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -111,8 +111,7 @@ impl Builder { None, port, &["VERSION=1.0", "CPath=/"], - ) - .unwrap(); + ).map_err(|e| Error::DnsSdError(io::Error::new(io::ErrorKind::Unsupported, e)))?; } else { let responder = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?; diff --git a/src/main.rs b/src/main.rs index 59ab0ce6..7ec21a5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1581,19 +1581,15 @@ async fn main() { if setup.enable_discovery { let device_id = setup.session_config.device_id.clone(); - - discovery = match librespot::discovery::Discovery::builder(device_id) + match librespot::discovery::Discovery::builder(device_id) .name(setup.connect_config.name.clone()) .device_type(setup.connect_config.device_type) .port(setup.zeroconf_port) .launch() { - Ok(d) => Some(d), - Err(e) => { - error!("Discovery Error: {}", e); - exit(1); - } - } + Ok(d) => discovery = Some(d), + Err(err) => warn!("Could not initialise discovery: {}.", err), + }; } if let Some(credentials) = setup.credentials { @@ -1606,6 +1602,11 @@ async fn main() { ) .fuse(), ); + } else if discovery.is_none() { + error!( + "Discovery is unavailable and no credentials provided. Authentication is not possible." + ); + exit(1); } loop { From 4fd7ac24ce637a9668bcd69718aa899d7ca00480 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Thu, 5 May 2022 00:19:29 +0200 Subject: [PATCH 184/561] Bump MSRV to 1.53 See: https://github.com/librespot-org/librespot/pull/997 --- .github/workflows/test.yml | 2 +- CHANGELOG.md | 1 + COMPILING.md | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e447ff9..ab92d984 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.48 # MSRV (Minimum supported rust version) + - 1.53 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] diff --git a/CHANGELOG.md b/CHANGELOG.md index d93b636c..77672a68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). - [playback] `pipe`: create file if it doesn't already exist - [playback] More robust dynamic limiter for very wide dynamic range (breaking) +- [build] The MSRV is now 1.53. ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/COMPILING.md b/COMPILING.md index 39ae20cc..a76d9a36 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, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) 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: From 9de1f38e926de827cf2f05b4d0e42600a568c384 Mon Sep 17 00:00:00 2001 From: "Bernhard M. Wiedemann" Date: Mon, 2 May 2022 13:18:53 +0200 Subject: [PATCH 185/561] Allow to override build_id with SOURCE_DATE_EPOCH in order to make builds reproducible. See https://reproducible-builds.org/ for why this is good and https://reproducible-builds.org/specs/source-date-epoch/ for the definition of this variable. This PR was done while working on reproducible builds for openSUSE. --- core/build.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/core/build.rs b/core/build.rs index 8e61c912..dd16cc0f 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,5 +1,6 @@ use rand::distributions::Alphanumeric; use rand::Rng; +use std::env; use vergen::{generate_cargo_keys, ConstantsFlags}; fn main() { @@ -7,11 +8,17 @@ fn main() { flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE); generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); - let build_id: String = rand::thread_rng() - .sample_iter(Alphanumeric) - .take(8) - .map(char::from) - .collect(); + let build_id: String; + match env::var("SOURCE_DATE_EPOCH") { + Ok(val) => build_id = val, + Err(_) => { + build_id = rand::thread_rng() + .sample_iter(Alphanumeric) + .take(8) + .map(char::from) + .collect() + } + } println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); } From 6c2491b9a3d170dd3cb8a74a6b3cd82a6d9e13b1 Mon Sep 17 00:00:00 2001 From: Louis Seubert Date: Fri, 20 May 2022 12:53:44 +0200 Subject: [PATCH 186/561] adding callback for reusable credentials (#983) This allows more control over how the credentials are saved to the cache --- CHANGELOG.md | 2 ++ core/src/session.rs | 11 +++++++---- core/tests/connect.rs | 1 + examples/get_token.rs | 2 +- examples/play.rs | 2 +- examples/playlist_tracks.rs | 2 +- src/main.rs | 5 ++++- 7 files changed, 17 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b205977..1e37fae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). - [playback] `pipe`: create file if it doesn't already exist - [playback] More robust dynamic limiter for very wide dynamic range (breaking) +- [core] `Session`: `connect()` now returns the long-term credentials. +- [core] `Session`: `connect()` now accespt a flag if the credentails should be stored via the cache. - [build] The MSRV is now 1.53. ### Added diff --git a/core/src/session.rs b/core/src/session.rs index 6c4abc54..6c8bf93f 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -66,7 +66,8 @@ impl Session { config: SessionConfig, credentials: Credentials, cache: Option, - ) -> Result { + store_credentials: bool, + ) -> Result<(Session, Credentials), SessionError> { let ap = apresolve(config.proxy.as_ref(), config.ap_port).await; info!("Connecting to AP \"{}\"", ap); @@ -76,18 +77,20 @@ impl Session { 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); + if store_credentials { + cache.save_credentials(&reusable_credentials); + } } let session = Session::create( conn, config, cache, - reusable_credentials.username, + reusable_credentials.username.clone(), tokio::runtime::Handle::current(), ); - Ok(session) + Ok((session, reusable_credentials)) } fn create( diff --git a/core/tests/connect.rs b/core/tests/connect.rs index 8b95e437..e6aa7c66 100644 --- a/core/tests/connect.rs +++ b/core/tests/connect.rs @@ -13,6 +13,7 @@ async fn test_connection() { SessionConfig::default(), Credentials::with_password("test", "test"), None, + false, ) .await; diff --git a/examples/get_token.rs b/examples/get_token.rs index 636155e0..4d9e1f1c 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -20,7 +20,7 @@ async fn main() { println!("Connecting.."); let credentials = Credentials::with_password(&args[1], &args[2]); - let session = Session::connect(session_config, credentials, None) + let (session, _) = Session::connect(session_config, credentials, None, false) .await .unwrap(); diff --git a/examples/play.rs b/examples/play.rs index 6156cb7b..a91b6851 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -27,7 +27,7 @@ async fn main() { let backend = audio_backend::find(None).unwrap(); println!("Connecting .."); - let session = Session::connect(session_config, credentials, None) + let (session, _) = Session::connect(session_config, credentials, None, false) .await .unwrap(); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 0bf17ee7..8dbe1d5f 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -27,7 +27,7 @@ async fn main() { process::exit(1); }); - let session = Session::connect(session_config, credentials, None) + let (session, _) = Session::connect(session_config, credentials, None, false) .await .unwrap(); diff --git a/src/main.rs b/src/main.rs index 7ec21a5a..55df381d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1599,6 +1599,7 @@ async fn main() { setup.session_config.clone(), credentials, setup.cache.clone(), + true, ) .fuse(), ); @@ -1634,6 +1635,7 @@ async fn main() { setup.session_config.clone(), credentials, setup.cache.clone(), + true, ).fuse()); }, None => { @@ -1643,7 +1645,7 @@ async fn main() { } }, session = &mut connecting, if !connecting.is_terminated() => match session { - Ok(session) => { + Ok((session,_)) => { let mixer_config = setup.mixer_config.clone(); let mixer = (setup.mixer)(mixer_config); let player_config = setup.player_config.clone(); @@ -1711,6 +1713,7 @@ async fn main() { setup.session_config.clone(), credentials, setup.cache.clone(), + true ).fuse()); }, _ => { From 3d298768b3d1933443c115db1d2bdf836418c0fb Mon Sep 17 00:00:00 2001 From: Sean McNamara Date: Sat, 21 May 2022 14:55:55 -0400 Subject: [PATCH 187/561] Backport #964 GStreamer backend cleanup (#979) --- CHANGELOG.md | 3 +- Cargo.lock | 342 ++++++++++++++++++++---- playback/Cargo.toml | 10 +- playback/src/audio_backend/gstreamer.rs | 238 ++++++++++------- 4 files changed, 445 insertions(+), 148 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e37fae3..13e7594d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] More robust dynamic limiter for very wide dynamic range (breaking) - [core] `Session`: `connect()` now returns the long-term credentials. - [core] `Session`: `connect()` now accespt a flag if the credentails should be stored via the cache. -- [build] The MSRV is now 1.53. +- [chore] The MSRV is now 1.53. +- [playback] `gstreamer`: create own context, set correct states and use sync handler ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/Cargo.lock b/Cargo.lock index 07f1e23d..10738efd 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" @@ -82,6 +97,12 @@ version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +[[package]] +name = "array-init" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6945cc5422176fc5e602e590c2878d2c2acd9a4fe20a4baa7c28022521698ec6" + [[package]] name = "async-trait" version = "0.1.51" @@ -110,6 +131,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +[[package]] +name = "backtrace" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +dependencies = [ + "addr2line", + "cc", + "cfg-if 1.0.0", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -192,6 +228,15 @@ dependencies = [ "nom", ] +[[package]] +name = "cfg-expr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +dependencies = [ + "smallvec", +] + [[package]] name = "cfg-if" version = "0.1.10" @@ -421,6 +466,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" @@ -561,6 +612,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" @@ -573,13 +630,32 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "glib-macros", - "glib-sys", - "gobject-sys", + "glib-macros 0.10.1", + "glib-sys 0.10.1", + "gobject-sys 0.10.0", "libc", "once_cell", ] +[[package]] +name = "glib" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "glib-macros 0.14.1", + "glib-sys 0.14.0", + "gobject-sys 0.14.0", + "libc", + "once_cell", + "smallvec", +] + [[package]] name = "glib-macros" version = "0.10.1" @@ -588,7 +664,7 @@ checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" dependencies = [ "anyhow", "heck", - "itertools", + "itertools 0.9.0", "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", @@ -596,6 +672,21 @@ dependencies = [ "syn", ] +[[package]] +name = "glib-macros" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" +dependencies = [ + "anyhow", + "heck", + "proc-macro-crate 1.1.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "glib-sys" version = "0.10.1" @@ -603,7 +694,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" dependencies = [ "libc", - "system-deps", + "system-deps 1.3.2", +] + +[[package]] +name = "glib-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" +dependencies = [ + "libc", + "system-deps 3.2.0", ] [[package]] @@ -618,28 +719,38 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" dependencies = [ - "glib-sys", + "glib-sys 0.10.1", "libc", - "system-deps", + "system-deps 1.3.2", +] + +[[package]] +name = "gobject-sys" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" +dependencies = [ + "glib-sys 0.14.0", + "libc", + "system-deps 3.2.0", ] [[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", "futures-channel", "futures-core", "futures-util", - "glib", - "glib-sys", - "gobject-sys", + "glib 0.14.8", "gstreamer-sys", "libc", "muldiv", + "num-integer", "num-rational", "once_cell", "paste", @@ -649,76 +760,102 @@ 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", + "glib 0.14.8", "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", + "glib-sys 0.14.0", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps", + "system-deps 3.2.0", +] + +[[package]] +name = "gstreamer-audio" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "420b6bcb1759231f01172751da094e7afa5cd9edf40bee7475f5bc86df433c57" +dependencies = [ + "array-init", + "bitflags", + "cfg-if 1.0.0", + "glib 0.14.8", + "gstreamer", + "gstreamer-audio-sys", + "gstreamer-base", + "libc", + "once_cell", +] + +[[package]] +name = "gstreamer-audio-sys" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d066ddfd05f63836f35ac4a5830d5bb2f7f3d6c33c870e9b15c667d20f65d7f6" +dependencies = [ + "glib-sys 0.14.0", + "gobject-sys 0.14.0", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps 3.2.0", ] [[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", - "glib", - "glib-sys", - "gobject-sys", + "cfg-if 1.0.0", + "glib 0.14.8", "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", + "glib-sys 0.14.0", + "gobject-sys 0.14.0", "gstreamer-sys", "libc", - "system-deps", + "system-deps 3.2.0", ] [[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", + "glib-sys 0.14.0", + "gobject-sys 0.14.0", "libc", - "system-deps", + "system-deps 3.2.0", ] [[package]] @@ -941,6 +1078,15 @@ dependencies = [ "either", ] +[[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" @@ -1273,9 +1419,10 @@ dependencies = [ "cpal", "futures-executor", "futures-util", - "glib", + "glib 0.10.3", "gstreamer", "gstreamer-app", + "gstreamer-audio", "jack", "lewton", "libpulse-binding", @@ -1285,6 +1432,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", + "parking_lot", "portaudio-rs", "rand", "rand_distr", @@ -1356,6 +1504,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" @@ -1380,9 +1538,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" @@ -1531,9 +1689,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.3.2" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07" +checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" dependencies = [ "autocfg", "num-integer", @@ -1582,6 +1740,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" @@ -1643,11 +1810,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", ] @@ -1679,6 +1849,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-lite" version = "0.2.7" @@ -1942,6 +2122,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" @@ -1998,7 +2184,7 @@ checksum = "41a29aa21f175b5a41a6e26da572d5e5d1ee5660d35f9f9d0913e8a802098f74" dependencies = [ "cfg-if 0.1.10", "libc", - "version-compare", + "version-compare 0.0.10", ] [[package]] @@ -2102,9 +2288,9 @@ 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" @@ -2134,6 +2320,12 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" +[[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.18.0" @@ -2146,6 +2338,18 @@ dependencies = [ "syn", ] +[[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" @@ -2183,11 +2387,29 @@ checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" dependencies = [ "heck", "pkg-config", - "strum", - "strum_macros", + "strum 0.18.0", + "strum_macros 0.18.0", "thiserror", "toml", - "version-compare", + "version-compare 0.0.10", +] + +[[package]] +name = "system-deps" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" +dependencies = [ + "anyhow", + "cfg-expr", + "heck", + "itertools 0.10.3", + "pkg-config", + "strum 0.21.0", + "strum_macros 0.21.1", + "thiserror", + "toml", + "version-compare 0.0.11", ] [[package]] @@ -2233,6 +2455,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" @@ -2271,6 +2504,7 @@ dependencies = [ "mio", "num_cpus", "once_cell", + "parking_lot", "pin-project-lite", "signal-hook-registry", "tokio-macros", @@ -2377,9 +2611,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -2431,6 +2665,12 @@ version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" +[[package]] +name = "version-compare" +version = "0.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" + [[package]] name = "version_check" version = "0.9.3" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 4e8d19c6..7126e3da 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -21,9 +21,10 @@ version = "0.3.1" futures-executor = "0.3" futures-util = { version = "0.3", default_features = false, features = ["alloc"] } log = "0.4" +parking_lot = { version = "0.11", features = ["deadlock_detection"] } byteorder = "1.4" shell-words = "1.0.0" -tokio = { version = "1", features = ["sync"] } +tokio = { version = "1", features = ["sync", "parking_lot"] } zerocopy = { version = "0.3" } thiserror = { version = "1" } @@ -34,8 +35,9 @@ libpulse-binding = { version = "2", optional = true, default-features = f 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 } +gstreamer = { version = "0.17", optional = true } +gstreamer-app = { version = "0.17", optional = true } +gstreamer-audio = { version = "0.17", optional = true } glib = { version = "0.10", optional = true } # Rodio dependencies @@ -58,4 +60,4 @@ jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal"] rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio", "glib"] diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 8b957577..0a98846e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,23 +1,28 @@ -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 gstreamer::{ + event::{FlushStart, FlushStop}, + prelude::*, + State, +}; use gstreamer as gst; use gstreamer_app as gst_app; +use gstreamer_audio as gst_audio; -use gst::prelude::*; -use zerocopy::AsBytes; +use parking_lot::Mutex; +use std::sync::Arc; -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 { - tx: SyncSender>, + appsrc: gst_app::AppSrc, + bufferpool: gst::BufferPool, pipeline: gst::Pipeline, format: AudioFormat, + async_error: Arc>>, } impl Open for GstreamerSink { @@ -25,117 +30,166 @@ 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 = 2048 * sample_size; + let gst_bytes = NUM_CHANNELS as usize * 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 - ); - // 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.get_bus().expect("couldn't get bus from pipeline"); - let mainloop = glib::MainLoop::new(None, false); - let appsrce: gst::Element = pipeline - .get_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!"); + 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 appsrc_caps = appsrc.get_caps().expect("couldn't get appsrc caps"); - let mut conf = bufferpool.get_config(); - conf.set_params(Some(&appsrc_caps), 4096 * sample_size as u32, 0, 0); + + let mut conf = bufferpool.config(); + conf.set_params(Some(&gst_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); + let async_error = Arc::new(Mutex::new(None)); + let async_error_clone = async_error.clone(); + + 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() + )); + } + _ => (), } - }); - 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::Error(err) => { - println!( - "Error from {:?}: {} ({:?})", - err.get_src().map(|s| s.get_path_string()), - err.get_error(), - err.get_debug() - ); - watch_mainloop.quit(); - } - _ => (), - }; - - glib::Continue(true) - }) - .expect("failed to add bus watch"); - thread_mainloop.run(); + gst::BusSyncReply::Drop }); 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, + appsrc, + 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) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + self.pipeline + .set_state(State::Playing) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + Ok(()) + } + + 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::OnWrite(e.to_string()))?; + self.bufferpool + .set_active(false) + .map_err(|e| SinkError::OnWrite(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 - self.tx - .send(data.to_vec()) - .expect("tx send failed in write function"); + 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) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + + let mutbuf = buffer.make_mut(); + mutbuf.set_size(data.len()); + mutbuf + .copy_from_slice(0, data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + + self.appsrc + .push_buffer(buffer) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + Ok(()) } } From cf25c2aa36e19ffe05927654c7b45a8fb9e01521 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 21:06:29 +0200 Subject: [PATCH 188/561] Fix CI on ARM --- .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 ab92d984..db6388eb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -196,4 +196,4 @@ jobs: - name: Install cross run: cargo install cross || true - name: Build - run: cross build --locked --target ${{ matrix.target }} --no-default-features + run: cross build --target ${{ matrix.target }} --no-default-features From 2a3e248bba3a490ca27d7e1741e3f3a86827690a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 21:36:56 +0200 Subject: [PATCH 189/561] Fix clippy lints --- core/build.rs | 19 +++++++--------- discovery/Cargo.toml | 1 - discovery/src/lib.rs | 40 ++++++++++++++------------------- playback/src/mixer/alsamixer.rs | 2 +- playback/src/mixer/mappings.rs | 4 ++-- playback/src/mixer/softmixer.rs | 2 +- src/main.rs | 2 +- 7 files changed, 30 insertions(+), 40 deletions(-) diff --git a/core/build.rs b/core/build.rs index dd16cc0f..e4aa1187 100644 --- a/core/build.rs +++ b/core/build.rs @@ -8,17 +8,14 @@ fn main() { flags.toggle(ConstantsFlags::REBUILD_ON_HEAD_CHANGE); generate_cargo_keys(ConstantsFlags::all()).expect("Unable to generate the cargo keys!"); - let build_id: String; - match env::var("SOURCE_DATE_EPOCH") { - Ok(val) => build_id = val, - Err(_) => { - build_id = rand::thread_rng() - .sample_iter(Alphanumeric) - .take(8) - .map(char::from) - .collect() - } - } + let build_id = match env::var("SOURCE_DATE_EPOCH") { + Ok(val) => val, + Err(_) => rand::thread_rng() + .sample_iter(Alphanumeric) + .take(8) + .map(char::from) + .collect(), + }; println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 4dccdc1e..aebaf4df 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -10,7 +10,6 @@ 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" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index b2e65586..ca403b16 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -16,7 +16,6 @@ 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; @@ -100,29 +99,24 @@ 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=/"], + ) + .map_err(|e| Error::DnsSdError(io::Error::new(io::ErrorKind::Unsupported, e)))?; - 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=/"], - ).map_err(|e| Error::DnsSdError(io::Error::new(io::ErrorKind::Unsupported, e)))?; - - } 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/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index c04e6ee8..f03af958 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.to_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..548d0648 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 to_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 to_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 93da5fec..db72659d 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.to_unmapped(mapped_volume) } fn set_volume(&self, volume: u16) { diff --git a/src/main.rs b/src/main.rs index 55df381d..9a1427bc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -586,7 +586,7 @@ fn get_setup() -> Setup { let stripped_env_key = |k: &str| { k.trim_start_matches("LIBRESPOT_") - .replace("_", "-") + .replace('_', "-") .to_lowercase() }; From b08502c801f5dabc03f1b3bc72873b286f4baab1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 21:51:37 +0200 Subject: [PATCH 190/561] Update crates --- Cargo.lock | 775 +++++++++++++++++++++++++------------------- playback/Cargo.toml | 2 +- 2 files changed, 435 insertions(+), 342 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10738efd..e3da3392 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,9 +93,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.44" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "array-init" @@ -105,9 +105,9 @@ checksum = "6945cc5422176fc5e602e590c2878d2c2acd9a4fe20a4baa7c28022521698ec6" [[package]] name = "async-trait" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -127,15 +127,15 @@ dependencies = [ [[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.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" dependencies = [ "addr2line", "cc", @@ -154,9 +154,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", @@ -187,10 +187,19 @@ dependencies = [ ] [[package]] -name = "bumpalo" -version = "3.8.0" +name = "block-buffer" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "byteorder" @@ -206,9 +215,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.71" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" dependencies = [ "jobserver", ] @@ -221,9 +230,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", ] @@ -258,7 +267,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time", + "time 0.1.43", "winapi", ] @@ -273,13 +282,13 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.2.2" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" +checksum = "bf6b561dcf059c85bbe388e0a7b0a1469acb3934cc0cfa148613a830629e3049" dependencies = [ "glob", "libc", - "libloading 0.7.1", + "libloading 0.7.3", ] [[package]] @@ -295,9 +304,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ "bytes", "memchr", @@ -321,9 +330,9 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.8" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e3347be6a09b46aba228d6608386739fb70beff4f61e07422da87b0bb31fa" +checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" dependencies = [ "bindgen", ] @@ -344,10 +353,10 @@ dependencies = [ "libc", "mach", "ndk 0.3.0", - "ndk-glue 0.3.0", + "ndk-glue", "nix", "oboe", - "parking_lot", + "parking_lot 0.11.2", "stdweb", "thiserror", "web-sys", @@ -356,13 +365,23 @@ 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", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-mac" version = "0.11.1" @@ -417,17 +436,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" @@ -437,6 +445,16 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", +] + [[package]] name = "dns-sd" version = "0.1.3" @@ -466,6 +484,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "fixedbitset" version = "0.2.0" @@ -490,9 +517,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -505,9 +532,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -515,15 +542,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -532,18 +559,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "autocfg", - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -551,23 +576,22 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.17" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ - "autocfg", "futures-channel", "futures-core", "futures-io", @@ -577,16 +601,14 @@ dependencies = [ "memchr", "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] [[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", @@ -603,13 +625,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.3" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] @@ -680,7 +702,7 @@ checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" dependencies = [ "anyhow", "heck", - "proc-macro-crate 1.1.0", + "proc-macro-crate 1.1.3", "proc-macro-error", "proc-macro2", "quote", @@ -866,9 +888,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", @@ -877,7 +899,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha-1 0.10.0", ] [[package]] @@ -920,7 +942,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" dependencies = [ "crypto-mac", - "digest", + "digest 0.9.0", ] [[package]] @@ -936,9 +958,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.5" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", @@ -947,9 +969,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes", "http", @@ -958,15 +980,15 @@ dependencies = [ [[package]] name = "httparse" -version = "1.5.1" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] name = "httpdate" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "humantime" @@ -976,9 +998,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.14" +version = "0.14.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" +checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" dependencies = [ "bytes", "futures-channel", @@ -1031,30 +1053,19 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9a83ec4af652890ac713ffd8dc859e650420a5ef47f7b9be29b6664ab50fbc8" +checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" dependencies = [ - "if-addrs-sys", "libc", "winapi", ] -[[package]] -name = "if-addrs-sys" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de74b9dd780476e837e5eb5ab7c88b49ed304126e412030a0adba99c8efe79ea" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "indexmap" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" dependencies = [ "autocfg", "hashbrown", @@ -1089,31 +1100,33 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jack" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39722b9795ae57c6967da99b1ab009fe72897fcbc59be59508c7c520327d9e34" +checksum = "d79b205ea723e478eb31a91dcdda100912c69cc32992eb7ba26ec0bbae7bebe4" dependencies = [ "bitflags", "jack-sys", "lazy_static", "libc", + "log", ] [[package]] name = "jack-sys" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" +checksum = "7b91f2d2d10bc2bab38f4dfa4bc77123a988828af39dd3f30dd9db14d44f2cc1" dependencies = [ "lazy_static", "libc", "libloading 0.6.7", + "pkg-config", ] [[package]] @@ -1147,9 +1160,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.55" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ "wasm-bindgen", ] @@ -1179,9 +1192,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.105" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libloading" @@ -1195,9 +1208,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.1" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" +checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" dependencies = [ "cfg-if 1.0.0", "winapi", @@ -1205,15 +1218,15 @@ 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" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac185a4d02e873c6d1ead59d674651f8ae5ec23ffe1637bee8de80665562a6a" +checksum = "d6fb7fd715150e59e9a74049d2b50e862c3959c139b95eea132a66ddae20c3d9" dependencies = [ "byteorder", "futures-util", @@ -1229,9 +1242,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", @@ -1243,9 +1256,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", @@ -1254,9 +1267,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.1" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8b0fcb9665401cc7c156c337c8edc7eb4e797b9d3ae1667e1e9e17b29e0c7c" +checksum = "7c73f96f9ca34809692c4760cfe421225860aa000de50edab68a16221fd27cc1" dependencies = [ "libpulse-sys", "pkg-config", @@ -1264,9 +1277,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.2" +version = "1.19.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d" +checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30" dependencies = [ "libc", "num-derive", @@ -1294,7 +1307,7 @@ dependencies = [ "librespot-protocol", "log", "rpassword", - "sha-1", + "sha-1 0.9.8", "thiserror", "tokio", "url", @@ -1362,7 +1375,7 @@ dependencies = [ "rand", "serde", "serde_json", - "sha-1", + "sha-1 0.9.8", "shannon", "thiserror", "tokio", @@ -1379,7 +1392,6 @@ version = "0.3.1" dependencies = [ "aes-ctr", "base64", - "cfg-if 1.0.0", "dns-sd", "form_urlencoded", "futures", @@ -1392,7 +1404,7 @@ dependencies = [ "log", "rand", "serde_json", - "sha-1", + "sha-1 0.9.8", "simple_logger", "thiserror", "tokio", @@ -1432,7 +1444,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot", + "parking_lot 0.11.2", "portaudio-rs", "rand", "rand_distr", @@ -1455,18 +1467,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", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", ] @@ -1494,9 +1507,9 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "mime" @@ -1505,35 +1518,30 @@ 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", ] [[package]] name = "mio" -version = "0.7.14" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "miow", - "ntapi", - "winapi", -] - -[[package]] -name = "miow" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" -dependencies = [ - "winapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] @@ -1558,24 +1566,30 @@ 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", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "ndk-glue" version = "0.3.0" @@ -1587,21 +1601,7 @@ dependencies = [ "log", "ndk 0.3.0", "ndk-macro", - "ndk-sys", -] - -[[package]] -name = "ndk-glue" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk 0.4.0", - "ndk-macro", - "ndk-sys", + "ndk-sys 0.2.2", ] [[package]] @@ -1619,9 +1619,18 @@ dependencies = [ [[package]] name = "ndk-sys" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" +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" @@ -1637,28 +1646,19 @@ 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", -] - -[[package]] -name = "ntapi" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f6bb902e437b6d86e03cce10a7e2af662292c5dfef23b65899ea3ac9354ad44" -dependencies = [ - "winapi", + "minimal-lexical", ] [[package]] name = "num-bigint" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" dependencies = [ "autocfg", "num-integer", @@ -1679,9 +1679,9 @@ dependencies = [ [[package]] name = "num-integer" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -1700,9 +1700,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", "libm", @@ -1710,9 +1710,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", @@ -1720,44 +1720,52 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ - "derivative", "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.4" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.0", + "proc-macro-crate 1.1.3", "proc-macro2", "quote", "syn", ] [[package]] -name = "object" -version = "0.27.1" +name = "num_threads" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.28.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "memchr", ] [[package]] name = "oboe" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" +checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ "jni", - "ndk 0.4.0", - "ndk-glue 0.4.0", + "ndk 0.6.0", + "ndk-context", "num-derive", "num-traits", "oboe-sys", @@ -1765,9 +1773,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", ] @@ -1783,9 +1791,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca" [[package]] name = "opaque-debug" @@ -1801,7 +1809,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.3", ] [[package]] @@ -1822,10 +1840,23 @@ dependencies = [ ] [[package]] -name = "paste" -version = "1.0.5" +name = "parking_lot_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf547ad0c65e31259204bd90935776d1c693cec2f4ff7abb7a1bbbd40dfe58" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if 1.0.0", + "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" @@ -1861,9 +1892,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1873,9 +1904,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.22" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "portaudio-rs" @@ -1900,9 +1931,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" @@ -1912,9 +1943,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "priority-queue" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf40e51ccefb72d42720609e1d3c518de8b5800d723a09358d4a6d6245e1f8ca" +checksum = "00ba480ac08d3cfc40dea10fd466fd2c14dee3ea6fc7873bc4079eda2727caf0" dependencies = [ "autocfg", "indexmap", @@ -1931,9 +1962,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.0" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ "thiserror", "toml", @@ -1963,47 +1994,35 @@ 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.31" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b581350bde2d774a19c6f30346796806b8f42b5fd3458c5f9a8623337fb27897" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[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", @@ -2011,23 +2030,22 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.10" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" 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]] @@ -2051,37 +2069,28 @@ 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", ] -[[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.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -2090,9 +2099,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -2145,9 +2154,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "same-file" @@ -2189,24 +2198,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.4" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -2215,9 +2224,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.68" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "itoa", "ryu", @@ -2230,13 +2239,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.3", +] + [[package]] name = "shannon" version = "0.2.0" @@ -2248,15 +2268,15 @@ 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" -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" @@ -2269,22 +2289,22 @@ dependencies = [ [[package]] name = "simple_logger" -version = "1.13.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7de33c687404ec3045d4a0d437580455257c0436f858d702f244e7d652f9f07" +checksum = "45b60258a35dc3cb8a16890b8fd6723349bfa458d7960e25e633f1b1c19d7b5e" dependencies = [ "atty", - "chrono", "colored", "log", + "time 0.3.9", "winapi", ] [[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" @@ -2294,9 +2314,9 @@ checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "socket2" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ "libc", "winapi", @@ -2358,13 +2378,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.80" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2414,13 +2434,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", @@ -2428,27 +2448,27 @@ 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", ] [[package]] name = "thiserror" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.30" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" dependencies = [ "proc-macro2", "quote", @@ -2477,10 +2497,28 @@ dependencies = [ ] [[package]] -name = "tinyvec" -version = "1.5.0" +name = "time" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] @@ -2493,29 +2531,29 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.12.0" +version = "1.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" +checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" dependencies = [ - "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", - "parking_lot", + "parking_lot 0.12.0", "pin-project-lite", "signal-hook-registry", + "socket2", "tokio-macros", "winapi", ] [[package]] name = "tokio-macros" -version = "1.5.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -2524,9 +2562,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" dependencies = [ "futures-core", "pin-project-lite", @@ -2535,9 +2573,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.8" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ "bytes", "futures-core", @@ -2549,9 +2587,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", ] @@ -2564,9 +2602,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2575,9 +2613,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", ] @@ -2590,15 +2628,21 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[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" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" @@ -2623,9 +2667,9 @@ checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" [[package]] name = "url" @@ -2673,9 +2717,9 @@ checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" [[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" @@ -2705,10 +2749,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] -name = "wasm-bindgen" -version = "0.2.78" +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" +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 1.0.0", "wasm-bindgen-macro", @@ -2716,9 +2766,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.78" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" dependencies = [ "bumpalo", "lazy_static", @@ -2731,9 +2781,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.78" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2741,9 +2791,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.78" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", @@ -2754,15 +2804,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.78" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" dependencies = [ "js-sys", "wasm-bindgen", @@ -2799,6 +2849,49 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +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.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + [[package]] name = "zerocopy" version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 7126e3da..516bd8af 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -42,7 +42,7 @@ glib = { version = "0.10", optional = true } # Rodio dependencies rodio = { version = "0.14", optional = true, default-features = false } -cpal = { version = "0.13", optional = true } +cpal = { version = "<0.13.5", optional = true } # Decoder lewton = "0.10" From 310b3cca81cf976288691d7c2b4493090119ec68 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 22:14:18 +0200 Subject: [PATCH 191/561] Prepare for 0.4.0 release --- CHANGELOG.md | 58 +++++++++++++++++++++++++--------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e7594d..c0489ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,46 +5,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. -## [Unreleased] +## [0.4.0] - 2022-05-21 ### Changed -- [main] Enforce reasonable ranges for option values (breaking). -- [main] Don't evaluate options that would otherwise have no effect. -- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. -- [contrib] Hardened security of the systemd service units -- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). -- [playback] `Sink`: `write()` now receives ownership of the packet (breaking). -- [playback] `pipe`: create file if it doesn't already exist +- [chore] The MSRV is now 1.53 +- [contrib] Hardened security of the `systemd` service units +- [core] `Session`: `connect()` now returns the long-term credentials +- [core] `Session`: `connect()` now accepts a flag if the credentails should be stored via the cache +- [main] Different option descriptions and error messages based on what backends are enabled at build time - [playback] More robust dynamic limiter for very wide dynamic range (breaking) -- [core] `Session`: `connect()` now returns the long-term credentials. -- [core] `Session`: `connect()` now accespt a flag if the credentails should be stored via the cache. -- [chore] The MSRV is now 1.53. +- [playback] `alsa`: improve `--device ?` output for the Alsa backend - [playback] `gstreamer`: create own context, set correct states and use sync handler +- [playback] `pipe`: create file if it doesn't already exist +- [playback] `Sink`: `write()` now receives ownership of the packet (breaking) ### Added -- [cache] Add `disable-credential-cache` flag (breaking). -- [main] Use different option descriptions and error messages based on what backends are enabled at build time. -- [main] Add a `-q`, `--quiet` option that changes the logging level to warn. -- [main] Add a short name for every flag and option. -- [main] Add the ability to parse environment variables. -- [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) +- [main] Enforce reasonable ranges for option values (breaking) +- [main] Add the ability to parse environment variables +- [main] Log now emits warning when trying to use options that would otherwise have no effect +- [main] Verbose logging now logs all parsed environment variables and command line arguments (credentials are redacted) +- [main] Add a `-q`, `--quiet` option that changes the logging level to WARN +- [main] Add `disable-credential-cache` flag (breaking) +- [main] Add a short name for every flag and option +- [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. -- [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`. +- [connect] Don't panic when activating shuffle without previous interaction - [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. -- [main] Fix crash when built with Avahi support but Avahi is locally unavailable. +- [main] Fix crash when built with Avahi support but Avahi is locally unavailable +- [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`. +- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor +- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls ### Removed -- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. -- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed. -- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed. +- [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been removed ## [0.3.1] - 2021-10-24 @@ -119,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.1..HEAD +[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 [0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 [0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 From dff19377fa5e3ca7e89b8bfb0bae278143e38251 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 22:31:15 +0200 Subject: [PATCH 192/561] Update version numbers to 0.4.0 --- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8429ba2e..cc4c029e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.3.1" +version = "0.4.0" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-connect] path = "connect" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-core] path = "core" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-metadata] path = "metadata" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-playback] path = "playback" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.3.1" +version = "0.4.0" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 77855e62..e60c1f40 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,7 +8,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.3.1" +version = "0.4.0" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 4daf89f4..c69df940 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-playback] path = "../playback" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-discovery] path = "../discovery" -version = "0.3.1" +version = "0.4.0" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 2494a19a..bc3eba86 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.1" +version = "0.4.0" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index aebaf4df..beb094fb 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -27,7 +27,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.3.1" +version = "0.4.0" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 8eb7be8c..11992fa2 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.1" +version = "0.4.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 516bd8af..42294107 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.3.1" +version = "0.4.0" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-core] path = "../core" -version = "0.3.1" +version = "0.4.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.3.1" +version = "0.4.0" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 38f76371..005c2073 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.3.1" +version = "0.4.0" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From 8ad5a78bf6b85bd11879ef2781111b9980948131 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 22:51:30 +0200 Subject: [PATCH 193/561] Add repository URL --- audio/Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/audio/Cargo.toml b/audio/Cargo.toml index e60c1f40..1c89c81b 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -2,8 +2,9 @@ name = "librespot-audio" version = "0.4.0" authors = ["Paul Lietar "] -description="The audio fetching and processing logic for librespot" -license="MIT" +description = "The audio fetching logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" edition = "2018" [dependencies.librespot-core] From d596b16e5a1114f276d0e9325242f4891962f8a5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 22:52:06 +0200 Subject: [PATCH 194/561] Expand publishing documentation --- PUBLISHING.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/PUBLISHING.md b/PUBLISHING.md index ceab506c..e38cd6d8 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -2,7 +2,17 @@ ## How To -The bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script. +Read through this paragraph in its entirety before running anything. + +The Bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows from the project root: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script. + +Make sure that you are are starting from a clean working directory for both `dev` and `master`, completely up to date with remote and all local changes either committed and pushed or stashed. + +You will want to perform a dry run first: `./publish --dry-run 0.1.0`. Please make note of any errors or warnings. In particular, you may need to explicitly inform Git which remote you want to track for the `master` branch like so: `git --track origin/master` (or whatever you have called the `librespot-org` remote `master` branch). + +Depending on your system the script may fail to publish the main `librespot` crate after having published all the `librespot-xyz` sub-crates. If so then make sure the working directory is committed and pushed (watch `Cargo.toml`) and then run `cargo publish` manually after `publish.sh` finished. + +To publish the crates your GitHub account needs to be authorized on `crates.io` by `librespot-org`. ## What the script does From 5568c70cd1f1f704c1c65b92f3c5bba886233877 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 21 May 2022 22:52:39 +0200 Subject: [PATCH 195/561] Prepare for new developments --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0489ade..a50f6c45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. 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] + +### Changed + +### Added + +### Fixed + +### Removed + ## [0.4.0] - 2022-05-21 ### Changed @@ -117,6 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.0..HEAD [0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 [0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 [0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 From c03d3ad1026e52291661a1912b2a122a26e59f7f Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 23 May 2022 08:00:35 -0500 Subject: [PATCH 196/561] Bump deps This bumps deps that don't need major code refactoring or MSRV bumps. --- Cargo.lock | 92 +++++++++++++++++++++++++------------------- Cargo.toml | 4 +- core/Cargo.toml | 8 ++-- core/src/config.rs | 2 +- discovery/Cargo.toml | 2 +- playback/Cargo.toml | 4 +- src/main.rs | 2 +- 7 files changed, 64 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3da3392..488fb209 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "colored" -version = "1.9.3" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" dependencies = [ "atty", "lazy_static", @@ -473,9 +473,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "env_logger" -version = "0.8.4" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" dependencies = [ "atty", "humantime", @@ -495,9 +495,9 @@ dependencies = [ [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" [[package]] name = "fnv" @@ -1063,9 +1063,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f647032dfaa1f8b6dc29bd3edb7bbef4861b8b8007ebb118d6db284fd59f6ee" +checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" dependencies = [ "autocfg", "hashbrown", @@ -1290,7 +1290,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.3.1" +version = "0.4.0" dependencies = [ "base64", "env_logger", @@ -1315,7 +1315,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.3.1" +version = "0.4.0" dependencies = [ "aes-ctr", "byteorder", @@ -1329,7 +1329,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.3.1" +version = "0.4.0" dependencies = [ "form_urlencoded", "futures-util", @@ -1348,7 +1348,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.3.1" +version = "0.4.0" dependencies = [ "aes", "base64", @@ -1388,7 +1388,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.3.1" +version = "0.4.0" dependencies = [ "aes-ctr", "base64", @@ -1412,7 +1412,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.3.1" +version = "0.4.0" dependencies = [ "async-trait", "byteorder", @@ -1424,7 +1424,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.3.1" +version = "0.4.0" dependencies = [ "alsa", "byteorder", @@ -1444,7 +1444,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot 0.11.2", + "parking_lot 0.12.0", "portaudio-rs", "rand", "rand_distr", @@ -1458,7 +1458,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.3.1" +version = "0.4.0" dependencies = [ "glob", "protobuf", @@ -1791,9 +1791,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" [[package]] name = "opaque-debug" @@ -1828,14 +1828,11 @@ 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", ] @@ -1845,10 +1842,13 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ + "backtrace", "cfg-if 1.0.0", "libc", + "petgraph", "redox_syscall", "smallvec", + "thread-id", "windows-sys", ] @@ -1882,9 +1882,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "51b305cc4569dd4e8765bab46261f67ef5d4d11a4b6e745100ee5dad8948b46c" dependencies = [ "fixedbitset", "indexmap", @@ -1943,9 +1943,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "priority-queue" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00ba480ac08d3cfc40dea10fd466fd2c14dee3ea6fc7873bc4079eda2727caf0" +checksum = "de9cde7493f5f5d2d163b174be9f9a72d756b79b0f6ed85654128d238c347c1e" dependencies = [ "autocfg", "indexmap", @@ -2123,11 +2123,13 @@ dependencies = [ [[package]] name = "rpassword" -version = "5.0.1" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc936cf8a7ea60c58f030fd36a612a48f440610214dc54bc36431f9ea0c3efb" +checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" dependencies = [ "libc", + "serde", + "serde_json", "winapi", ] @@ -2289,9 +2291,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "1.16.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45b60258a35dc3cb8a16890b8fd6723349bfa458d7960e25e633f1b1c19d7b5e" +checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" dependencies = [ "atty", "colored", @@ -2573,16 +2575,16 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.10" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", "futures-sink", - "log", "pin-project-lite", "tokio", + "tracing", ] [[package]] @@ -2608,9 +2610,21 @@ checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.26" @@ -2685,9 +2699,9 @@ dependencies = [ [[package]] name = "uuid" -version = "0.8.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" dependencies = [ "getrandom", ] @@ -2894,9 +2908,9 @@ checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" [[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", @@ -2904,9 +2918,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 cc4c029e..0f966ba6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,13 +50,13 @@ version = "0.4.0" [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" hex = "0.4" hyper = "0.14" log = "0.4" -rpassword = "5.0" +rpassword = "6.0" thiserror = "1.0" tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "signal", "sync", "process"] } url = "2.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index bc3eba86..8ebcbf6a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -31,7 +31,7 @@ num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" pbkdf2 = { version = "0.8", default-features = false, features = ["hmac"] } -priority-queue = "1.1" +priority-queue = "1.2" protobuf = "2.14.0" rand = "0.8" serde = { version = "1.0", features = ["derive"] } @@ -41,14 +41,14 @@ shannon = "0.2.0" thiserror = "1.0.7" tokio = { version = "1.0", features = ["io-util", "net", "rt", "sync"] } tokio-stream = "0.1.1" -tokio-util = { version = "0.6", features = ["codec"] } +tokio-util = { version = "0.7", features = ["codec"] } url = "2.1" -uuid = { version = "0.8", default-features = false, features = ["v4"] } +uuid = { version = "1.0", default-features = false, features = ["v4"] } [build-dependencies] rand = "0.8" vergen = "3.0.4" [dev-dependencies] -env_logger = "0.8" +env_logger = "0.9" tokio = {version = "1.0", features = ["macros"] } diff --git a/core/src/config.rs b/core/src/config.rs index b8c448c2..8ead5654 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -12,7 +12,7 @@ pub struct SessionConfig { impl Default for SessionConfig { fn default() -> SessionConfig { - let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string(); + let device_id = uuid::Uuid::new_v4().as_hyphenated().to_string(); SessionConfig { user_agent: crate::version::VERSION_STRING.to_string(), device_id, diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index beb094fb..4810c6ea 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -32,7 +32,7 @@ version = "0.4.0" [dev-dependencies] futures = "0.3" hex = "0.4" -simple_logger = "1.11" +simple_logger = "2.1" tokio = { version = "1.0", features = ["macros", "rt"] } [features] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 42294107..63e7f27f 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -21,11 +21,11 @@ version = "0.4.0" futures-executor = "0.3" futures-util = { version = "0.3", default_features = false, features = ["alloc"] } log = "0.4" -parking_lot = { version = "0.11", features = ["deadlock_detection"] } +parking_lot = { version = "0.12", features = ["deadlock_detection"] } byteorder = "1.4" shell-words = "1.0.0" tokio = { version = "1", features = ["sync", "parking_lot"] } -zerocopy = { version = "0.3" } +zerocopy = { version = "0.6" } thiserror = { version = "1" } # Backends diff --git a/src/main.rs b/src/main.rs index 9a1427bc..5e4b0bb5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1053,7 +1053,7 @@ fn get_setup() -> Setup { Some(creds) if username == creds.username => Some(creds), _ => { let prompt = &format!("Password for {}: ", username); - match rpassword::prompt_password_stderr(prompt) { + match rpassword::prompt_password(prompt) { Ok(password) => { if !password.is_empty() { Some(Credentials::with_password(username, password)) From 0b7508a2bfe9e7fb52f313b48b7e202ada23100b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 23 May 2022 13:14:43 -0500 Subject: [PATCH 197/561] Update deps round 2 --- Cargo.lock | 461 ++++++++++------------------- playback/Cargo.toml | 18 +- playback/src/audio_backend/alsa.rs | 5 +- playback/src/audio_backend/sdl.rs | 9 +- 4 files changed, 177 insertions(+), 316 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 488fb209..39b34a77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,9 +71,9 @@ dependencies = [ [[package]] name = "alsa" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c4da790adcb2ce5e758c064b4f3ec17a30349f9961d3e5e6c9688b052a9e18" +checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" dependencies = [ "alsa-sys", "bitflags", @@ -139,7 +139,7 @@ checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" dependencies = [ "addr2line", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", "miniz_oxide", "object", @@ -239,19 +239,13 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.8.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b412e83326147c2bb881f8b40edfbf9905b9b8abaebd0e47ca190ba62fda8f0e" +checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" dependencies = [ "smallvec", ] -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -339,20 +333,20 @@ dependencies = [ [[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", "core-foundation-sys", "coreaudio-rs", - "jack", + "jack 0.8.3", "jni", "js-sys", "lazy_static", "libc", "mach", - "ndk 0.3.0", + "ndk", "ndk-glue", "nix", "oboe", @@ -403,9 +397,9 @@ 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", "darling_macro", @@ -413,9 +407,9 @@ dependencies = [ [[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", @@ -427,9 +421,9 @@ dependencies = [ [[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", "quote", @@ -465,12 +459,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 = "env_logger" version = "0.9.0" @@ -629,7 +617,7 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi 0.10.2+wasi-snapshot-preview1", ] @@ -642,67 +630,33 @@ checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" [[package]] name = "glib" -version = "0.10.3" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c685013b7515e668f1b57a165b009d4d28cb139a8a989bbd699c10dad29d0c5" +checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", - "futures-util", - "glib-macros 0.10.1", - "glib-sys 0.10.1", - "gobject-sys 0.10.0", - "libc", - "once_cell", -] - -[[package]] -name = "glib" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c515f1e62bf151ef6635f528d05b02c11506de986e43b34a5c920ef0b3796a4" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "glib-macros 0.14.1", - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-macros", + "glib-sys", + "gobject-sys", "libc", "once_cell", "smallvec", + "thiserror", ] [[package]] name = "glib-macros" -version = "0.10.1" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41486a26d1366a8032b160b59065a59fb528530a46a49f627e7048fb8c064039" +checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" dependencies = [ "anyhow", "heck", - "itertools 0.9.0", - "proc-macro-crate 0.1.5", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-macros" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aad66361f66796bfc73f530c51ef123970eb895ffba991a234fcf7bea89e518" -dependencies = [ - "anyhow", - "heck", - "proc-macro-crate 1.1.3", + "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", @@ -711,22 +665,12 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.10.1" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7e9b997a66e9a23d073f2b1abb4dbfc3925e0b8952f67efd8d9b6e168e4cdc1" +checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" dependencies = [ "libc", - "system-deps 1.3.2", -] - -[[package]] -name = "glib-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c1d60554a212445e2a858e42a0e48cece1bd57b311a19a9468f70376cf554ae" -dependencies = [ - "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -737,44 +681,34 @@ checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] name = "gobject-sys" -version = "0.10.0" +version = "0.15.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "952133b60c318a62bf82ee75b93acc7e84028a093e06b9e27981c2b6fe68218c" +checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" dependencies = [ - "glib-sys 0.10.1", + "glib-sys", "libc", - "system-deps 1.3.2", -] - -[[package]] -name = "gobject-sys" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa92cae29759dae34ab5921d73fff5ad54b3d794ab842c117e36cafc7994c3f5" -dependencies = [ - "glib-sys 0.14.0", - "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "gstreamer" -version = "0.17.4" +version = "0.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6a255f142048ba2c4a4dce39106db1965abe355d23f4b5335edea43a553faa4" +checksum = "d66363bacf5e4f6eb281564adc2902e44c52ae5c45082423e7439e9012b75456" dependencies = [ "bitflags", - "cfg-if 1.0.0", + "cfg-if", "futures-channel", "futures-core", "futures-util", - "glib 0.14.8", + "glib", "gstreamer-sys", "libc", "muldiv", "num-integer", "num-rational", "once_cell", + "option-operations", "paste", "pretty-hex", "thiserror", @@ -782,14 +716,14 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.17.2" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73b8d33b1bbe9f22d0cf56661a1d2a2c9a0e099ea10e5f1f347be5038f5c043" +checksum = "664adf6abc6546c1ad54492a067dcbc605032c9c789ce8f6f78cb9ddeef4b684" dependencies = [ "bitflags", "futures-core", "futures-sink", - "glib 0.14.8", + "glib", "gstreamer", "gstreamer-app-sys", "gstreamer-base", @@ -799,27 +733,27 @@ 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 0.14.0", + "glib-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "gstreamer-audio" -version = "0.17.2" +version = "0.18.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420b6bcb1759231f01172751da094e7afa5cd9edf40bee7475f5bc86df433c57" +checksum = "9ceb43e669be4c33c38b273fd4ca0511c0a7748987835233c529fc3c805c807e" dependencies = [ "array-init", "bitflags", - "cfg-if 1.0.0", - "glib 0.14.8", + "cfg-if", + "glib", "gstreamer", "gstreamer-audio-sys", "gstreamer-base", @@ -829,27 +763,27 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.17.0" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d066ddfd05f63836f35ac4a5830d5bb2f7f3d6c33c870e9b15c667d20f65d7f6" +checksum = "a34258fb53c558c0f41dad194037cbeaabf49d347570df11b8bd1c4897cf7d7c" dependencies = [ - "glib-sys 0.14.0", - "gobject-sys 0.14.0", + "glib-sys", + "gobject-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] name = "gstreamer-base" -version = "0.17.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0c1d8c62eb5d08fb80173609f2eea71d385393363146e4e78107facbd67715" +checksum = "224f35f36582407caf58ded74854526beeecc23d0cf64b8d1c3e00584ed6863f" dependencies = [ "bitflags", - "cfg-if 1.0.0", - "glib 0.14.8", + "cfg-if", + "glib", "gstreamer", "gstreamer-base-sys", "libc", @@ -857,27 +791,27 @@ 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 0.14.0", - "gobject-sys 0.14.0", + "glib-sys", + "gobject-sys", "gstreamer-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[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 0.14.0", - "gobject-sys 0.14.0", + "glib-sys", + "gobject-sys", "libc", - "system-deps 3.2.0", + "system-deps", ] [[package]] @@ -913,12 +847,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" @@ -1077,25 +1008,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", -] - -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", + "cfg-if", ] [[package]] @@ -1106,12 +1019,25 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] name = "jack" -version = "0.7.3" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d79b205ea723e478eb31a91dcdda100912c69cc32992eb7ba26ec0bbae7bebe4" +checksum = "b3902a02287c3dcad784edd1ecc5f487774b63a5fe77dd9842ea9a993d0a4a23" dependencies = [ "bitflags", - "jack-sys", + "jack-sys 0.2.2", + "lazy_static", + "libc", + "log", +] + +[[package]] +name = "jack" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce722655a29b13bb98ec7e8ba9dc65d670b9b37c7b1c09775c7f7516811c5a36" +dependencies = [ + "bitflags", + "jack-sys 0.4.0", "lazy_static", "libc", "log", @@ -1119,13 +1045,25 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.3" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b91f2d2d10bc2bab38f4dfa4bc77123a988828af39dd3f30dd9db14d44f2cc1" +checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" dependencies = [ "lazy_static", "libc", "libloading 0.6.7", +] + +[[package]] +name = "jack-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d70559ff166d148ccb750ddd77702af760718f3a752c731add168c22c16a9f" +dependencies = [ + "bitflags", + "lazy_static", + "libc", + "libloading 0.7.3", "pkg-config", ] @@ -1202,7 +1140,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", ] @@ -1212,7 +1150,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "winapi", ] @@ -1431,11 +1369,11 @@ dependencies = [ "cpal", "futures-executor", "futures-util", - "glib 0.10.3", + "glib", "gstreamer", "gstreamer-app", "gstreamer-audio", - "jack", + "jack 0.10.0", "lewton", "libpulse-binding", "libpulse-simple-binding", @@ -1481,7 +1419,7 @@ version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -1511,6 +1449,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[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" @@ -1559,18 +1506,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" @@ -1579,7 +1514,7 @@ checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" dependencies = [ "bitflags", "jni-sys", - "ndk-sys 0.3.0", + "ndk-sys", "num_enum", "thiserror", ] @@ -1592,37 +1527,32 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-glue" -version = "0.3.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5caf0c24d51ac1c905c27d4eda4fa0635bbe0de596b8f79235e0b17a4d29385" +checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" dependencies = [ "lazy_static", "libc", "log", - "ndk 0.3.0", + "ndk", + "ndk-context", "ndk-macro", - "ndk-sys 0.2.2", + "ndk-sys", ] [[package]] name = "ndk-macro" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" +checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ "darling", - "proc-macro-crate 0.1.5", + "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" @@ -1634,14 +1564,15 @@ dependencies = [ [[package]] name = "nix" -version = "0.20.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" dependencies = [ "bitflags", "cc", - "cfg-if 1.0.0", + "cfg-if", "libc", + "memoffset", ] [[package]] @@ -1733,7 +1664,7 @@ version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ - "proc-macro-crate 1.1.3", + "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -1764,7 +1695,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" dependencies = [ "jni", - "ndk 0.6.0", + "ndk", "ndk-context", "num-derive", "num-traits", @@ -1801,6 +1732,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[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" @@ -1828,7 +1768,7 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "instant", "libc", "redox_syscall", @@ -1843,7 +1783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ "backtrace", - "cfg-if 1.0.0", + "cfg-if", "libc", "petgraph", "redox_syscall", @@ -1937,9 +1877,9 @@ checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "pretty-hex" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" +checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "priority-queue" @@ -1951,15 +1891,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "proc-macro-crate" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" -dependencies = [ - "toml", -] - [[package]] name = "proc-macro-crate" version = "1.1.3" @@ -2114,9 +2045,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", ] @@ -2177,9 +2108,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "sdl2" -version = "0.34.5" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deecbc3fa9460acff5a1e563e05cb5f31bba0aa0c214bb49a43db8159176d54b" +checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" dependencies = [ "bitflags", "lazy_static", @@ -2189,13 +2120,13 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.34.5" +version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a29aa21f175b5a41a6e26da572d5e5d1ee5660d35f9f9d0913e8a802098f74" +checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", "libc", - "version-compare 0.0.10", + "version-compare", ] [[package]] @@ -2242,7 +2173,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", @@ -2254,7 +2185,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.3", ] @@ -2332,45 +2263,9 @@ checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" [[package]] name = "strsim" -version = "0.9.3" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" - -[[package]] -name = "strum" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bd81eb48f4c437cadc685403cad539345bf703d78e63707418431cecd4522b" - -[[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.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[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", -] +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "subtle" @@ -2403,35 +2298,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "1.3.2" +version = "6.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3ecc17269a19353b3558b313bba738b25d82993e30d62a18406a24aba4649b" +checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" dependencies = [ - "heck", - "pkg-config", - "strum 0.18.0", - "strum_macros 0.18.0", - "thiserror", - "toml", - "version-compare 0.0.10", -] - -[[package]] -name = "system-deps" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "480c269f870722b3b08d2f13053ce0c2ab722839f472863c3e2d61ff3a1c2fa6" -dependencies = [ - "anyhow", "cfg-expr", "heck", - "itertools 0.10.3", "pkg-config", - "strum 0.21.0", - "strum_macros 0.21.1", - "thiserror", "toml", - "version-compare 0.0.11", + "version-compare", ] [[package]] @@ -2440,7 +2315,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", @@ -2608,7 +2483,7 @@ version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2667,12 +2542,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - [[package]] name = "unicode-width" version = "0.1.9" @@ -2719,15 +2588,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.0.10" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d63556a25bae6ea31b52e640d7c41d1ab27faba4ccb600013837a3d0b3994ca1" - -[[package]] -name = "version-compare" -version = "0.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c18c859eead79d8b95d09e4678566e8d70105c4e7b251f707a03df32442661b" +checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" [[package]] name = "version_check" @@ -2774,7 +2637,7 @@ version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "wasm-bindgen-macro", ] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 63e7f27f..723b3e95 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -29,20 +29,20 @@ zerocopy = { version = "0.6" } thiserror = { version = "1" } # 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.17", optional = true } -gstreamer-app = { version = "0.17", optional = true } -gstreamer-audio = { version = "0.17", optional = true } -glib = { version = "0.10", optional = true } +jack = { version = "0.10", optional = true } +sdl2 = { version = "0.35", optional = true } +gstreamer = { version = "0.18", optional = true } +gstreamer-app = { version = "0.18", optional = true } +gstreamer-audio = { version = "0.18", optional = true } +glib = { version = "0.15", optional = true } # Rodio dependencies -rodio = { version = "0.14", optional = true, default-features = false } -cpal = { version = "<0.13.5", optional = true } +rodio = { version = "0.15", optional = true, default-features = false } +cpal = { version = "0.13", optional = true } # Decoder lewton = "0.10" 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/sdl.rs b/playback/src/audio_backend/sdl.rs index 1c9794a2..9ee78e94 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -99,19 +99,20 @@ impl Sink for SdlSink { 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) } - }; + } + .map_err(SinkError::OnWrite)?; Ok(()) } } From a433f2b9118b289a8489171a641bfdda620d6d19 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 23 May 2022 13:40:00 -0500 Subject: [PATCH 198/561] update MSRV --- .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 db6388eb..e1243ce7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.53 # MSRV (Minimum supported rust version) + - 1.56 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] From 79b92bbe7a8a4ddd0737018263df19b23eb799ad Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 23 May 2022 13:42:38 -0500 Subject: [PATCH 199/561] Update changelog and such --- CHANGELOG.md | 1 + COMPILING.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a50f6c45..b1b4491f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- [chore] The MSRV is now 1.56 ### Added diff --git a/COMPILING.md b/COMPILING.md index a76d9a36..2b0a1350 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, 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.56, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) 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: From 15aea747fdb6fe97f01dbb02656adbf3fdd6dbd1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 23 May 2022 21:42:13 +0200 Subject: [PATCH 200/561] Fix updating lockfile --- publish.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/publish.sh b/publish.sh index fb4a475a..cf51212f 100755 --- a/publish.sh +++ b/publish.sh @@ -27,14 +27,17 @@ function updateVersion { do if [ "$CRATE" = "librespot" ] then - CRATE='' + CRATE_DIR='' + else + CRATE_DIR=$CRATE fi - crate_path="$WORKINGDIR/$CRATE/Cargo.toml" + crate_path="$WORKINGDIR/$CRATE_DIR/Cargo.toml" crate_path=${crate_path//\/\///} sed -i '' "s/^version.*/version = \"$1\"/g" "$crate_path" echo "Path is $crate_path" if [ "$CRATE" = "librespot" ] then + echo "Updating lockfile" if [ "$DRY_RUN" = 'true' ] ; then cargo update --dry-run git add . && git commit --dry-run -a -m "Update Cargo.lock" From 88e64bd8843d812cb60f2d07de3f4d218f5c3b8c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 23 May 2022 21:43:16 +0200 Subject: [PATCH 201/561] Update Cargo.lock --- Cargo.lock | 16 ++++++++-------- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39b34a77..44417d29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.4.0" +version = "0.4.1" dependencies = [ "base64", "env_logger", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aes-ctr", "byteorder", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.4.0" +version = "0.4.1" dependencies = [ "form_urlencoded", "futures-util", @@ -1286,7 +1286,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aes", "base64", @@ -1326,7 +1326,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.4.0" +version = "0.4.1" dependencies = [ "aes-ctr", "base64", @@ -1350,7 +1350,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.4.0" +version = "0.4.1" dependencies = [ "async-trait", "byteorder", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.4.0" +version = "0.4.1" dependencies = [ "alsa", "byteorder", @@ -1396,7 +1396,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.4.0" +version = "0.4.1" dependencies = [ "glob", "protobuf", diff --git a/Cargo.toml b/Cargo.toml index 0f966ba6..b958a619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.4.0" +version = "0.4.1" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-connect] path = "connect" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-core] path = "core" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-discovery] path = "discovery" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-metadata] path = "metadata" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-playback] path = "playback" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-protocol] path = "protocol" -version = "0.4.0" +version = "0.4.1" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 1c89c81b..a4d54a07 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" @@ -9,7 +9,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.4.0" +version = "0.4.1" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index c69df940..a6393754 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-playback] path = "../playback" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-discovery] path = "../discovery" -version = "0.4.0" +version = "0.4.1" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 8ebcbf6a..23ff91e8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.0" +version = "0.4.1" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 4810c6ea..1f57f61c 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -27,7 +27,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.4.0" +version = "0.4.1" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 11992fa2..bbc9805c 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.0" +version = "0.4.1" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 723b3e95..cc4f0470 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.4.0" +version = "0.4.1" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-core] path = "../core" -version = "0.4.0" +version = "0.4.1" [dependencies.librespot-metadata] path = "../metadata" -version = "0.4.0" +version = "0.4.1" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 005c2073..52a83342 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.4.0" +version = "0.4.1" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From fc1e74574a2c1537a7a25292e9354b341c0a91cb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 23 May 2022 22:21:56 +0200 Subject: [PATCH 202/561] Update changelog --- CHANGELOG.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1b4491f..4e975209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed -- [chore] The MSRV is now 1.56 ### Added @@ -16,6 +15,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +## [0.4.1] - 2022-05-23 + +### Changed +- [chore] The MSRV is now 1.56 + +### Fixed +- [playback] Fixed dependency issues when installing from crate + ## [0.4.0] - 2022-05-21 ### Changed @@ -128,7 +135,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.0..HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD +[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1 [0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 [0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 [0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 From e4deb5ddcdf8ec6683c1441ae0fb956bce8eae5a Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 28 May 2022 09:08:50 -0500 Subject: [PATCH 203/561] Fix fixed volume with hardware mixer. fixes https://github.com/librespot-org/librespot/issues/1008 --- CHANGELOG.md | 1 + playback/src/mixer/alsamixer.rs | 32 +++++++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e975209..24885534 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added ### Fixed +- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` ### Removed diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index f03af958..a930d168 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -104,23 +104,33 @@ impl Mixer for AlsaMixer { let min_db = min_millibel.to_db() as f64; let max_db = max_millibel.to_db() as f64; - let mut db_range = f64::abs(max_db - min_db); + let reported_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 = if config.volume_ctrl.range_ok() { let db_range_override = config.volume_ctrl.db_range(); + if db_range_override.is_normal() { + db_range_override + } else { + reported_db_range + } + } else { + config.volume_ctrl.set_db_range(reported_db_range); + reported_db_range + }; + + if reported_db_range == db_range { + debug!("Alsa dB volume range was reported as {}", reported_db_range); + if reported_db_range > 100.0 { + debug!("Alsa mixer reported dB range > 100, which is suspect"); + debug!("Please manually set `--volume-range` if this is incorrect"); + } + } else { debug!( - "Alsa dB volume range was detected as {} but overridden as {}", - db_range, db_range_override + "Alsa dB volume range was reported as {} but overridden to {}", + reported_db_range, db_range ); - db_range = db_range_override; } // For hardware controls with a small range (24 dB or less), From 5a10a963ff2fcddc8ddd1fa8ff8a11793415953b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 18 Jun 2022 09:08:04 -0500 Subject: [PATCH 204/561] Minor alsa backend tweaks Silence a clippy warning and we don't need `std::cmp::min` when `usize` already has `min`. --- playback/src/audio_backend/alsa.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 20e73618..e8d9ee05 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -6,7 +6,6 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, ValueOr}; -use std::cmp::min; use std::process::exit; use thiserror::Error; @@ -141,7 +140,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!( @@ -467,7 +466,7 @@ impl SinkAsBytes for AlsaSink { loop { let data_left = data_len - start_index; let space_left = capacity - self.period_buffer.len(); - let data_to_buffer = min(data_left, space_left); + let data_to_buffer = data_left.min(space_left); let end_index = start_index + data_to_buffer; self.period_buffer From 0d4367fca23971dc8d790eae52e8f41044278bf4 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 20 Jun 2022 15:55:06 -0500 Subject: [PATCH 205/561] Improve the subprocess backend Better error handling. Move the checking of the shell command to start so a proper error can be thrown if it's None. Use write instead of write_all for finer grained error handling and the ability to attempt a restart on write errors. Use try_wait to skip flushing and killing the process if it's already dead. Stop the player on shutdown to *mostly* prevent write errors from spamming the logs during shutdown. Previously Ctrl+c always resulted in a write error. --- connect/src/spirc.rs | 1 + playback/src/audio_backend/subprocess.rs | 197 ++++++++++++++++++----- 2 files changed, 158 insertions(+), 40 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index b574ff52..698ca46a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -478,6 +478,7 @@ impl SpircTask { } SpircCommand::Shutdown => { CommandSender::new(self, MessageType::kMessageTypeGoodbye).send(); + self.player.stop(); self.shutdown = true; if let Some(rx) = self.commands.as_mut() { rx.close() diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 63fc5d88..6ce545da 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -4,30 +4,75 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; -use std::io::Write; +use std::io::{ErrorKind, Write}; use std::process::{exit, Child, Command, Stdio}; +use thiserror::Error; + +#[derive(Debug, Error)] +enum SubprocessError { + #[error(" {0}")] + OnWrite(std::io::Error), + + #[error(" Command {command} Can Not be Executed, {e}")] + SpawnFailure { command: String, e: std::io::Error }, + + #[error(" Failed to Parse Command args for {command}, {e}")] + InvalidArgs { + command: String, + e: shell_words::ParseError, + }, + + #[error(" Failed to Flush the Subprocess, {0}")] + FlushFailure(std::io::Error), + + #[error(" Failed to Kill the Subprocess, {0}")] + KillFailure(std::io::Error), + + #[error(" Failed to Wait for the Subprocess to Exit, {0}")] + WaitFailure(std::io::Error), + + #[error(" The Subprocess is no longer able to accept Bytes")] + WriteZero, + + #[error(" Missing Required Shell Command")] + MissingCommand, + + #[error(" The Subprocess is None")] + NoChild, + + #[error(" The Subprocess's stdin is None")] + NoStdin, +} + +impl From for SinkError { + fn from(e: SubprocessError) -> SinkError { + use SubprocessError::*; + let es = e.to_string(); + match e { + FlushFailure(_) | KillFailure(_) | WaitFailure(_) | OnWrite(_) | WriteZero => { + SinkError::OnWrite(es) + } + SpawnFailure { .. } => SinkError::ConnectionRefused(es), + MissingCommand | InvalidArgs { .. } => SinkError::InvalidParams(es), + NoChild | NoStdin => SinkError::NotConnected(es), + } + } +} pub struct SubprocessSink { - shell_command: String, + shell_command: Option, child: Option, format: AudioFormat, } impl Open for SubprocessSink { fn open(shell_command: Option, format: AudioFormat) -> Self { - let shell_command = match shell_command.as_deref() { - Some("?") => { - info!("Usage: --backend subprocess --device {{shell_command}}"); - exit(0); - } - Some(cmd) => cmd.to_owned(), - None => { - error!("subprocess sink requires specifying a shell command"); - exit(1); - } - }; + if let Some("?") = shell_command.as_deref() { + println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n"); + exit(0); + } - info!("Using subprocess sink with format: {:?}", format); + info!("Using SubprocessSink with format: {:?}", format); Self { shell_command, @@ -39,26 +84,53 @@ impl Open for SubprocessSink { impl Sink for SubprocessSink { fn start(&mut self) -> SinkResult<()> { - let args = split(&self.shell_command).unwrap(); - let child = Command::new(&args[0]) - .args(&args[1..]) - .stdin(Stdio::piped()) - .spawn() - .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; - self.child = Some(child); + self.child.get_or_insert({ + match self.shell_command.as_deref() { + Some(command) => { + let args = split(command).map_err(|e| SubprocessError::InvalidArgs { + command: command.to_string(), + e, + })?; + + Command::new(&args[0]) + .args(&args[1..]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| SubprocessError::SpawnFailure { + command: command.to_string(), + e, + })? + } + None => return Err(SubprocessError::MissingCommand.into()), + } + }); + Ok(()) } fn stop(&mut self) -> SinkResult<()> { - if let Some(child) = &mut self.child.take() { - child - .kill() - .map_err(|e| SinkError::OnWrite(e.to_string()))?; - child - .wait() - .map_err(|e| SinkError::OnWrite(e.to_string()))?; + let child = &mut self.child.take().ok_or(SubprocessError::NoChild)?; + + match child.try_wait() { + // The process has already exited + // nothing to do. + Ok(Some(_)) => Ok(()), + Ok(_) => { + // The process Must DIE!!! + child + .stdin + .take() + .ok_or(SubprocessError::NoStdin)? + .flush() + .map_err(SubprocessError::FlushFailure)?; + + child.kill().map_err(SubprocessError::KillFailure)?; + child.wait().map_err(SubprocessError::WaitFailure)?; + + Ok(()) + } + Err(e) => Err(SubprocessError::WaitFailure(e).into()), } - Ok(()) } sink_as_bytes!(); @@ -66,22 +138,67 @@ impl Sink for SubprocessSink { impl SinkAsBytes for SubprocessSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - if let Some(child) = &mut self.child { - let child_stdin = child + // We get one attempted restart per write. + // We don't want to get stuck in a restart loop. + let mut restarted = false; + let mut start_index = 0; + let data_len = data.len(); + let mut end_index = data_len; + + loop { + match self + .child + .as_ref() + .ok_or(SubprocessError::NoChild)? .stdin - .as_mut() - .ok_or_else(|| SinkError::NotConnected("Child is None".to_string()))?; - child_stdin - .write_all(data) - .map_err(|e| SinkError::OnWrite(e.to_string()))?; - child_stdin - .flush() - .map_err(|e| SinkError::OnWrite(e.to_string()))?; + .as_ref() + .ok_or(SubprocessError::NoStdin)? + .write(&data[start_index..end_index]) + { + Ok(0) => { + // Potentially fatal. + // As per the docs a return value of 0 + // means we shouldn't try to write to the + // process anymore so let's try a restart + // if we haven't already. + self.try_restart(SubprocessError::WriteZero, &mut restarted)?; + + continue; + } + Ok(bytes_written) => { + // What we want, a successful write. + start_index = data_len.min(start_index + bytes_written); + end_index = data_len.min(start_index + bytes_written); + + if end_index == data_len { + break Ok(()); + } + } + // Non-fatal, retry the write. + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => { + // Very possibly fatal, + // but let's try a restart anyway if we haven't already. + self.try_restart(SubprocessError::OnWrite(e), &mut restarted)?; + + continue; + } + } } - Ok(()) } } impl SubprocessSink { pub const NAME: &'static str = "subprocess"; + + fn try_restart(&mut self, e: SubprocessError, restarted: &mut bool) -> SinkResult<()> { + // If the restart fails throw the original error back. + if !*restarted && self.stop().is_ok() && self.start().is_ok() { + *restarted = true; + + Ok(()) + } else { + Err(e.into()) + } + } } From 0db17973a152657990c3fd5d695bf531edcd2b0f Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 25 Jun 2022 00:17:36 -0500 Subject: [PATCH 206/561] Fix --opt=value line argument logging Fixes https://github.com/librespot-org/librespot/issues/1011 --- src/main.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 5e4b0bb5..0c542f6f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -658,7 +658,15 @@ fn get_setup() -> Setup { trace!("Command line argument(s):"); for (index, key) in args.iter().enumerate() { - let opt = key.trim_start_matches('-'); + let opt = { + let key = key.trim_start_matches('-'); + + if let Some((s, _)) = key.split_once('=') { + s + } else { + key + } + }; if index > 0 && key.starts_with('-') @@ -668,13 +676,13 @@ fn get_setup() -> Setup { { if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { // Don't log creds. - trace!("\t\t{} \"XXXXXXXX\"", key); + trace!("\t\t{} \"XXXXXXXX\"", opt); } else { let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string()); if value.is_empty() { - trace!("\t\t{}", key); + trace!("\t\t{}", opt); } else { - trace!("\t\t{} \"{}\"", key, value); + trace!("\t\t{} \"{}\"", opt, value); } } } From 2532687cc62501c238b61a0c19666b8308262abc Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 30 Jun 2022 21:57:23 -0500 Subject: [PATCH 207/561] Improve pipe backend * Implement stop * Better error handling --- playback/src/audio_backend/pipe.rs | 84 ++++++++++++++++++++---------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 682f8124..e0e8a77c 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -2,9 +2,38 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; + use std::fs::OpenOptions; use std::io::{self, Write}; use std::process::exit; +use thiserror::Error; + +#[derive(Debug, Error)] +enum StdoutError { + #[error(" {0}")] + OnWrite(std::io::Error), + + #[error(" File Path {file} Can Not be Opened and/or Created, {e}")] + OpenFailure { file: String, e: std::io::Error }, + + #[error(" Failed to Flush the Output Stream, {0}")] + FlushFailure(std::io::Error), + + #[error(" The Output Stream is None")] + NoOutput, +} + +impl From for SinkError { + fn from(e: StdoutError) -> SinkError { + use StdoutError::*; + let es = e.to_string(); + match e { + FlushFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + OpenFailure { .. } => SinkError::ConnectionRefused(es), + NoOutput => SinkError::NotConnected(es), + } + } +} pub struct StdoutSink { output: Option>, @@ -15,13 +44,12 @@ pub struct StdoutSink { impl Open for StdoutSink { fn open(file: Option, format: AudioFormat) -> Self { if let Some("?") = file.as_deref() { - info!("Usage:"); - println!(" Output to stdout: --backend pipe"); - println!(" Output to file: --backend pipe --device {{filename}}"); + println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n"); exit(0); } - info!("Using pipe sink with format: {:?}", format); + info!("Using StdoutSink (pipe) with format: {:?}", format); + Self { output: None, file, @@ -32,21 +60,31 @@ impl Open for StdoutSink { impl Sink for StdoutSink { fn start(&mut self) -> SinkResult<()> { - if self.output.is_none() { - let output: Box = match self.file.as_deref() { - Some(file) => { - let open_op = OpenOptions::new() + self.output.get_or_insert({ + match self.file.as_deref() { + Some(file) => Box::new( + OpenOptions::new() .write(true) .create(true) .open(file) - .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; - Box::new(open_op) - } + .map_err(|e| StdoutError::OpenFailure { + file: file.to_string(), + e, + })?, + ), None => Box::new(io::stdout()), - }; + } + }); - self.output = Some(output); - } + Ok(()) + } + + fn stop(&mut self) -> SinkResult<()> { + self.output + .take() + .ok_or(StdoutError::NoOutput)? + .flush() + .map_err(StdoutError::FlushFailure)?; Ok(()) } @@ -56,19 +94,11 @@ impl Sink for StdoutSink { impl SinkAsBytes for StdoutSink { fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - match self.output.as_deref_mut() { - Some(output) => { - output - .write_all(data) - .map_err(|e| SinkError::OnWrite(e.to_string()))?; - output - .flush() - .map_err(|e| SinkError::OnWrite(e.to_string()))?; - } - None => { - return Err(SinkError::NotConnected("Output is None".to_string())); - } - } + self.output + .as_deref_mut() + .ok_or(StdoutError::NoOutput)? + .write_all(data) + .map_err(StdoutError::OnWrite)?; Ok(()) } From 582fdebadd4c1c5196911ae84773508c197ed8d6 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 20 Jul 2022 20:27:45 -0500 Subject: [PATCH 208/561] Update Changelog This should update the changelog for my last 3 PR's. --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24885534..e4ba4628 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Changed +- [playback] `subprocess`: Better error handling +- [playback] `pipe`: Better error handling ### Added +- [playback] `pipe`: Implement stop ### Fixed - [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` +- [main] fix `--opt=value` line argument logging ### Removed From e37fe22195cfd7fbe31938a59ba37db94a609fbd Mon Sep 17 00:00:00 2001 From: "Art M. Gallagher" Date: Thu, 28 Jul 2022 11:25:41 +0100 Subject: [PATCH 209/561] 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 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 210/561] 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 211/561] 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 7bd9186e948a31e913690a73000cfeb33e3096db Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 28 Jul 2022 18:51:49 +0200 Subject: [PATCH 212/561] Blacklist ap-gew4 access point (#1026) --- CHANGELOG.md | 3 ++- core/src/apresolve.rs | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e4ba4628..da1cfccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `pipe`: Better error handling ### Added +- [core] `apresolve`: Blacklist ap-gew4 access point that causes channel errors - [playback] `pipe`: Implement stop ### Fixed @@ -116,7 +117,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [connect] Fix step size on volume up/down events - [connect] Fix looping back to the first track after the last track of an album or playlist -- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream +- [playback] 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 diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 759577d4..f6b89aad 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -8,6 +8,7 @@ use url::Url; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; const AP_FALLBACK: &str = "ap.spotify.com:443"; +const AP_BLACKLIST: [&str; 1] = ["ap-gew4.spotify.com"]; #[derive(Clone, Debug, Deserialize)] struct ApResolveData { @@ -42,8 +43,24 @@ async fn try_apresolve( let body = hyper::body::to_bytes(response.into_body()).await?; let data: ApResolveData = serde_json::from_slice(body.as_ref())?; + // filter APs that are known to cause channel errors + let aps: Vec = data + .ap_list + .into_iter() + .filter_map(|ap| { + let host = ap.parse::().ok()?.host()?.to_owned(); + if !AP_BLACKLIST.iter().any(|&blacklisted| host == blacklisted) { + Some(ap) + } else { + warn!("Ignoring blacklisted access point {}", ap); + None + } + }) + .collect(); + let ap = if ap_port.is_some() || proxy.is_some() { - data.ap_list.into_iter().find_map(|ap| { + // filter on ports if specified on the command line... + aps.into_iter().find_map(|ap| { if ap.parse::().ok()?.port()? == port { Some(ap) } else { @@ -51,9 +68,10 @@ async fn try_apresolve( } }) } else { - data.ap_list.into_iter().next() + // ...or pick the first on the list + aps.into_iter().next() } - .ok_or("empty AP List")?; + .ok_or("Unable to resolve any viable access points.")?; Ok(ap) } From 4ec38ca1937f90a2c683d17e0eb8c22ffea6fc79 Mon Sep 17 00:00:00 2001 From: "Art M. Gallagher" Date: Thu, 28 Jul 2022 11:25:41 +0100 Subject: [PATCH 213/561] 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 214/561] 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 215/561] 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 { From 87ea69b4573f1ec9205dbab29d997916e7218fc0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 29 Jul 2022 21:30:27 +0200 Subject: [PATCH 216/561] Blacklist ap-gue1.spotify.com access point --- CHANGELOG.md | 2 +- core/src/apresolve.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index da1cfccb..e6b5cfc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `pipe`: Better error handling ### Added -- [core] `apresolve`: Blacklist ap-gew4 access point that causes channel errors +- [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors - [playback] `pipe`: Implement stop ### Fixed diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index f6b89aad..dc3a5cce 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -8,7 +8,7 @@ use url::Url; const APRESOLVE_ENDPOINT: &str = "http://apresolve.spotify.com:80"; const AP_FALLBACK: &str = "ap.spotify.com:443"; -const AP_BLACKLIST: [&str; 1] = ["ap-gew4.spotify.com"]; +const AP_BLACKLIST: [&str; 2] = ["ap-gew4.spotify.com", "ap-gue1.spotify.com"]; #[derive(Clone, Debug, Deserialize)] struct ApResolveData { From 38bebc242a297cba99617e20716c5be795716f2b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 29 Jul 2022 21:34:01 +0200 Subject: [PATCH 217/561] Prepare for 0.4.2 release --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e6b5cfc0..d84fd31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,21 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. -## [Unreleased] +## [0.4.2] - 2022-07-29 ### Changed -- [playback] `subprocess`: Better error handling - [playback] `pipe`: Better error handling +- [playback] `subprocess`: Better error handling ### Added - [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors - [playback] `pipe`: Implement stop ### Fixed -- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` - [main] fix `--opt=value` line argument logging +- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` -### Removed +## Removed ## [0.4.1] - 2022-05-23 @@ -141,7 +141,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD +[0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1..v0.4.2 [0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1 [0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 [0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 From 91c06efda3e03d5190c0e75e1abebc84416390a6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 29 Jul 2022 22:01:22 +0200 Subject: [PATCH 218/561] Fix publish script on Linux --- publish.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/publish.sh b/publish.sh index cf51212f..c39f1c96 100755 --- a/publish.sh +++ b/publish.sh @@ -8,6 +8,17 @@ cd $WORKINGDIR crates=( "protocol" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) +OS=`uname` +function replace_in_file() { + if [ "$OS" == 'darwin' ]; then + # for MacOS + sed -i '' -e "$1" "$2" + else + # for Linux and Windows + sed -i'' -e "$1" "$2" + fi +} + function switchBranch { if [ "$SKIP_MERGE" = 'false' ] ; then # You are expected to have committed/stashed your changes before running this. @@ -33,7 +44,7 @@ function updateVersion { fi crate_path="$WORKINGDIR/$CRATE_DIR/Cargo.toml" crate_path=${crate_path//\/\///} - sed -i '' "s/^version.*/version = \"$1\"/g" "$crate_path" + $(replace_in_file "s/^version.*/version = \"$1\"/g" "$crate_path") echo "Path is $crate_path" if [ "$CRATE" = "librespot" ] then From 6537c441c3082aa2a7bbfe20e2afc316962dfc6a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 29 Jul 2022 22:05:10 +0200 Subject: [PATCH 219/561] Update Cargo.lock --- Cargo.lock | 283 +++++++++++++++++++++---------------------- Cargo.toml | 16 +-- audio/Cargo.toml | 4 +- connect/Cargo.toml | 10 +- core/Cargo.toml | 4 +- discovery/Cargo.toml | 4 +- metadata/Cargo.toml | 6 +- playback/Cargo.toml | 8 +- protocol/Cargo.toml | 2 +- 9 files changed, 165 insertions(+), 172 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44417d29..82a555dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -93,21 +93,21 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.57" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" [[package]] name = "array-init" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6945cc5422176fc5e602e590c2878d2c2acd9a4fe20a4baa7c28022521698ec6" +checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596" [[package]] name = "async-trait" -version = "0.1.53" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ "proc-macro2", "quote", @@ -133,9 +133,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.65" +version = "0.3.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" dependencies = [ "addr2line", "cc", @@ -197,9 +197,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.9.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" @@ -209,9 +209,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" [[package]] name = "cc" @@ -261,7 +261,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time 0.1.44", "winapi", ] @@ -276,9 +276,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.2" +version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf6b561dcf059c85bbe388e0a7b0a1469acb3934cc0cfa148613a830629e3049" +checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" dependencies = [ "glob", "libc", @@ -368,9 +368,9 @@ dependencies = [ [[package]] name = "crypto-common" -version = "0.1.3" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", "typenum", @@ -474,18 +474,18 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] [[package]] name = "fixedbitset" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "fnv" @@ -613,26 +613,26 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] name = "gimli" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" [[package]] name = "glib" -version = "0.15.11" +version = "0.15.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab" +checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" dependencies = [ "bitflags", "futures-channel", @@ -816,9 +816,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "headers" @@ -889,9 +889,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", @@ -929,9 +929,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.18" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b26ae0a80afebe130861d90abf98e3814a4f28a4c6ffeb5ab8ebb2be311e0ef2" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -994,9 +994,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.7.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown", @@ -1098,9 +1098,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -1228,7 +1228,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.4.1" +version = "0.4.2" dependencies = [ "base64", "env_logger", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.4.1" +version = "0.4.2" dependencies = [ "aes-ctr", "byteorder", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.4.1" +version = "0.4.2" dependencies = [ "form_urlencoded", "futures-util", @@ -1286,7 +1286,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.4.1" +version = "0.4.2" dependencies = [ "aes", "base64", @@ -1326,7 +1326,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.4.1" +version = "0.4.2" dependencies = [ "aes-ctr", "base64", @@ -1350,7 +1350,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.4.1" +version = "0.4.2" dependencies = [ "async-trait", "byteorder", @@ -1362,7 +1362,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.4.1" +version = "0.4.2" dependencies = [ "alsa", "byteorder", @@ -1382,7 +1382,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "portaudio-rs", "rand", "rand_distr", @@ -1396,7 +1396,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.4.1" +version = "0.4.2" dependencies = [ "glob", "protobuf", @@ -1472,18 +1472,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.1" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b29bd4bc3f33391105ebee3589c19197c4271e3e5a9ec9bfe8127eeff8f082" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", @@ -1620,9 +1620,9 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d41702bd167c2df5520b384281bc111a4b5efcf7fbc4c9c222c815b07e0a6a6a" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", "num-integer", @@ -1681,9 +1681,9 @@ dependencies = [ [[package]] name = "object" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ "memchr", ] @@ -1722,9 +1722,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "opaque-debug" @@ -1734,9 +1734,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "option-operations" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95d6113415f41b268f1195907427519769e40ee6f28cbb053795098a2c16f447" +checksum = "42b01597916c91a493b1e8a2fde64fec1764be3259abc1f06efc99c274f150a2" dependencies = [ "paste", ] @@ -1754,9 +1754,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core 0.9.3", @@ -1822,9 +1822,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b305cc4569dd4e8765bab46261f67ef5d4d11a4b6e745100ee5dad8948b46c" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", "indexmap", @@ -1883,9 +1883,9 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "priority-queue" -version = "1.2.2" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9cde7493f5f5d2d163b174be9f9a72d756b79b0f6ed85654128d238c347c1e" +checksum = "815082d99af3acc75a3e67efd2a07f72e67b4e81b4344eb8ca34c6ebf3dfa9c5" dependencies = [ "autocfg", "indexmap", @@ -1927,9 +1927,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" dependencies = [ "unicode-ident", ] @@ -1961,9 +1961,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -2010,18 +2010,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] [[package]] name = "regex" -version = "1.5.6" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" dependencies = [ "aho-corasick", "memchr", @@ -2030,9 +2030,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.26" +version = "0.6.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" [[package]] name = "remove_dir_all" @@ -2131,24 +2131,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" [[package]] name = "serde" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", @@ -2157,9 +2157,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ "itoa", "ryu", @@ -2222,28 +2222,31 @@ dependencies = [ [[package]] name = "simple_logger" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75a9723083573ace81ad0cdfc50b858aa3c366c48636edb4109d73122a0c0ea" +checksum = "166fea527c36d9b8a0a88c0c6d4c5077c699d9ffb5cf890b231a3f08c35f3d40" dependencies = [ "atty", "colored", "log", - "time 0.3.9", + "time 0.3.11", "winapi", ] [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" @@ -2275,9 +2278,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.95" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", @@ -2365,19 +2368,20 @@ dependencies = [ [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] [[package]] name = "time" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ "itoa", "libc", @@ -2408,17 +2412,18 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.18.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg", "bytes", "libc", "memchr", "mio", "num_cpus", "once_cell", - "parking_lot 0.12.0", + "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", "socket2", @@ -2428,9 +2433,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" dependencies = [ "proc-macro2", "quote", @@ -2439,9 +2444,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" dependencies = [ "futures-core", "pin-project-lite", @@ -2450,9 +2455,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ "bytes", "futures-core", @@ -2473,40 +2478,28 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if", "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -2529,15 +2522,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" dependencies = [ "tinyvec", ] @@ -2568,9 +2561,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.0.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cfcd319456c4d6ea10087ed423473267e1a071f3bc0aa89f80d60997843c6f0" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom", ] @@ -2621,9 +2614,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" @@ -2633,9 +2626,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2643,13 +2636,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -2658,9 +2651,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2668,9 +2661,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -2681,15 +2674,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index b958a619..c7e9ef37 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.4.1" +version = "0.4.2" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-connect] path = "connect" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-core] path = "core" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-discovery] path = "discovery" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-metadata] path = "metadata" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-playback] path = "playback" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-protocol] path = "protocol" -version = "0.4.1" +version = "0.4.2" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index a4d54a07..d6dc8d7e 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" @@ -9,7 +9,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.4.1" +version = "0.4.2" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index a6393754..313e6440 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-playback] path = "../playback" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-discovery] path = "../discovery" -version = "0.4.1" +version = "0.4.2" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 23ff91e8..6f8b89eb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.1" +version = "0.4.2" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 1f57f61c..b1d07e48 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -27,7 +27,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.4.1" +version = "0.4.2" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index bbc9805c..69a4d6a1 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.1" +version = "0.4.2" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index cc4f0470..aaaf3293 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.4.1" +version = "0.4.2" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-core] path = "../core" -version = "0.4.1" +version = "0.4.2" [dependencies.librespot-metadata] path = "../metadata" -version = "0.4.1" +version = "0.4.2" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 52a83342..7a5da342 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.4.1" +version = "0.4.2" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From c74dc250d98550f9037485674a05d02d22b77cab Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 29 Jul 2022 23:17:10 +0200 Subject: [PATCH 220/561] Update MSRV to 1.57 --- .github/workflows/test.yml | 2 +- COMPILING.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e1243ce7..4a93a3d6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -64,7 +64,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - 1.56 # MSRV (Minimum supported rust version) + - 1.57 # MSRV (Minimum supported rust version) - stable - beta experimental: [false] diff --git a/COMPILING.md b/COMPILING.md index 2b0a1350..9163afa3 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.56, 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.57, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) 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: From 786f8832d1aac028801d8f2ff6ec7636033db07d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 30 Jul 2022 22:28:12 +0200 Subject: [PATCH 221/561] Update version numbers to 0.5.0-dev --- CHANGELOG.md | 13 +++++++++++-- Cargo.lock | 16 ++++++++-------- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 8 ++++---- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 10 files changed, 45 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d84fd31d..d168f84b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. 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. +## [0.5.0-dev] - YYYY-MM-DD + +### Changed + +### Added + +### Fixed + +### Removed + ## [0.4.2] - 2022-07-29 ### Changed @@ -19,8 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] fix `--opt=value` line argument logging - [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` -## Removed - ## [0.4.1] - 2022-05-23 ### Changed @@ -141,6 +149,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 +[0.5.0-dev]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD [0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1..v0.4.2 [0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1 [0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 diff --git a/Cargo.lock b/Cargo.lock index 299ee713..0c15d140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1332,7 +1332,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "env_logger", "futures-util", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "aes", "byteorder", @@ -1374,7 +1374,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "form_urlencoded", "futures-util", @@ -1393,7 +1393,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "aes", "base64", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "aes", "base64", @@ -1468,7 +1468,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "async-trait", "byteorder", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "alsa", "byteorder", @@ -1516,7 +1516,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.4.2" +version = "0.5.0-dev" dependencies = [ "glob", "protobuf", diff --git a/Cargo.toml b/Cargo.toml index 2c3f2769..ffa3d6f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-connect] path = "connect" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-core] path = "core" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-discovery] path = "discovery" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-metadata] path = "metadata" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-playback] path = "playback" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-protocol] path = "protocol" -version = "0.4.2" +version = "0.5.0-dev" [dependencies] env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] } diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 18467d36..43d15b87 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" @@ -9,7 +9,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.4.2" +version = "0.5.0-dev" [dependencies] aes = "0.8" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index fd2473fe..1a41b3bb 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -21,12 +21,12 @@ tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-playback] path = "../playback" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.2" +version = "0.5.0-dev" diff --git a/core/Cargo.toml b/core/Cargo.toml index 5a7dd88d..cc319c99 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.2" +version = "0.5.0-dev" [dependencies] aes = "0.8" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index a83c8e8e..c8b0e08f 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -28,7 +28,7 @@ tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } [dependencies.librespot-core] path = "../core" -version = "0.4.2" +version = "0.5.0-dev" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 7b0a72ff..f57159e3 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -18,8 +18,8 @@ uuid = { version = "1", default-features = false } [dependencies.librespot-core] path = "../core" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-protocol] path = "../protocol" -version = "0.4.2" +version = "0.5.0-dev" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8e9f7019..89237108 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-core] path = "../core" -version = "0.4.2" +version = "0.5.0-dev" [dependencies.librespot-metadata] path = "../metadata" -version = "0.4.2" +version = "0.5.0-dev" [dependencies] byteorder = "1" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index da6544a7..00b97bb5 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.4.2" +version = "0.5.0-dev" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From 5344258ba97bd70dc1086f57f84b6b025efac5d8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 30 Jul 2022 22:39:05 +0200 Subject: [PATCH 222/561] Relax some non-fatal events to WARN level Fixes #1029 --- connect/src/spirc.rs | 4 ++-- core/src/session.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 1b50c068..6adb340c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -431,7 +431,7 @@ impl SpircTask { Some(result) => match result { Ok((username, frame)) => { if username != self.session.username() { - error!("could not dispatch remote update: frame was intended for {}", username); + warn!("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); } @@ -475,7 +475,7 @@ impl SpircTask { }, 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); + warn!("could not dispatch command: {}", e); } }, event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event { diff --git a/core/src/session.rs b/core/src/session.rs index 11f2cf8a..29c4cc5f 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -301,7 +301,7 @@ impl Session { } Ok(Event::Eof) => break, Ok(_) => (), - Err(e) => error!( + Err(e) => warn!( "Error parsing XML at position {}: {:?}", reader.buffer_position(), e From e9f3b6d2901be90bc373af0829485e7dcc7ab86b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 31 Jul 2022 00:04:08 +0200 Subject: [PATCH 223/561] Catch-up changelog --- CHANGELOG.md | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d168f84b..7ad245b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,103 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.5.0-dev] - YYYY-MM-DD +This version will be a major departure from the architecture up until now. It +focuses on implementing the "new Spotify API". This means moving large parts +of the Spotify protocol from Mercury to HTTP. A lot of this was reverse +engineered before by @devgianlu of librespot-java. It was long overdue that we +started implementing it too, not in the least because new features like the +hopefully upcoming Spotify HiFi depend on it. + +Splitting up the work on the new Spotify API, v0.5.0 brings HTTP-based file +downloads and metadata access. Implementing the "dealer" (replacing the current +Mercury-based SPIRC message bus with WebSockets, also required for social plays) +is separate large effort, to be targeted for v0.6.0. + +While at it, we are taking the liberty to do some major refactoring to make +librespot more robust. Consequently not only the Spotify API changed but large +parts of the librespot API too. For downstream maintainers, we realise that it +can be a lot to move from the current codebase to this one, but believe us it +will be well worth it. + +All these changes are likely to introduce new bugs as well as some regressions. +We appreciate all your testing and contributions to the repository: +https://github.com/librespot-org/librespot + ### Changed +- [all] Assertions were changed into `Result` or removed (breaking) +- [all] Purge use of `unwrap`, `expect` and return `Result` +- [all] `chrono` replaced with `time` (breaking) +- [all] `time` updated (CVE-2020-26235) +- [all] Improve lock contention and performance (breaking) +- [audio] Files are now downloaded over the HTTPS CDN (breaking) +- [audio] Improve file opening and seeking performance +- [chore] MSRV is now 1.61 +- [connect] `DeviceType` moved out of `connect` into `core` (breaking) +- [core] Message listeners are registered before authenticating. As a result + there now is a separate `Session::new` and subsequent `session.connect`. + (breaking) +- [core] `ConnectConfig` moved out of `core` into `connect` (breaking) +- [core] `client_id` for `get_token` moved to `SessionConfig` (breaking) +- [core] Mercury code has been refactored for better legibility (breaking) +- [core] Cache resolved access points during runtime (breaking) +- [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported. +- [core] Report actual platform data on login +- [playback] The audio decoder has been switched from `lewton` to `Symphonia`. + This improves the Vorbis sound quality, adds support for MP3 as well as for + FLAC in the future. (breaking) +- [playback] Improve reporting of actual playback cursor +- [playback] The passthrough decoder is now feature-gated (breaking) +- [playback] `rodio`: call play and pause +- [protocol] protobufs have been updated +- [metadata] Most metadata is now retrieved with the `spclient` (breaking) +- [metadata] Playlists are moved to the `playlist4_external` protobuf (breaking) + ### Added +- [all] Check that array indexes are within bounds (panic safety) +- [all] Wrap errors in librespot `Error` type (breaking) +- [core] Send metrics with metadata queries: client ID, country & product +- [core] Verify Spotify server certificates (prevents man-in-the-middle attacks) +- [core] User attributes are stored in `Session` upon login, accessible with a + getter and setter, and automatically updated as changes are pushed by the + Spotify infrastructure (breaking) +- [core] HTTPS is now supported, including for proxies (breaking) +- [core] Resolve `spclient` and `dealer` access points (breaking) +- [core] Get and cache tokens through new token provider (breaking) +- [core] `spclient` is the API for HTTP-based calls to the Spotify servers. + It supports a lot of functionality, including audio previews and image + downloads even if librespot doesn't use that for playback itself. +- [core] Support downloading of lyrics +- [playback] Explicit tracks are skipped if the controlling Connect client has + disabled such content. Applications that use librespot as a library without + Connect should use the 'filter-explicit-content' user attribute in the session. +- [metadata] All metadata fields in the protobufs are now exposed (breaking) + ### Fixed ### Removed +- [main] `autoplay` is no longer a command-line option. Instead, librespot now + follows the setting in the Connect client that controls it. Applications that + use librespot as a library without Connect should now instead use the + 'autoplay' user attribute in the session. + ## [0.4.2] - 2022-07-29 +Besides a couple of small fixes, this point release is mainly to blacklist the +ap-gew4 and ap-gue1 access points that caused librespot to fail to playback +anything. + +Development will now shift to the new HTTP-based API, targeted for a future +v0.5.0 release. The new-api branch will therefore be promoted to dev. This is a +major departure from the old API and although it brings many exciting new +things, it is also likely to introduce new bugs and some regressions. + +Long story short, this v0.4.2 release is the most stable that librespot has yet +to offer. But, unless anything big comes up, it is also intended as the last +release to be based on the old API. Happy listening. + ### Changed - [playback] `pipe`: Better error handling - [playback] `subprocess`: Better error handling @@ -31,6 +118,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.1] - 2022-05-23 +This release fixes dependency issues when installing from crates. + ### Changed - [chore] The MSRV is now 1.56 @@ -39,6 +128,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.0] - 2022-05-21 +Note: This version was yanked, because a corrupt package was uploaded and failed +to install. + +This is a polishing release, adding a few little extras and improving on many +thers. We had to break a couple of API's to do so, and therefore bumped the +minor version number. v0.4.x may be the last in series before we migrate from +the current channel-based Spotify backend to a more HTTP-based backend. +Targeting that major effort for a v0.5 release sometime, we intend to maintain +v0.4.x as a stable branch until then. + ### Changed - [chore] The MSRV is now 1.53 - [contrib] Hardened security of the `systemd` service units From c0fc35fd1845df10608975319e4d476b4be730c3 Mon Sep 17 00:00:00 2001 From: eladyn Date: Thu, 30 Jun 2022 00:22:27 +0200 Subject: [PATCH 224/561] Include more metadata in track struct Based on changes from @capnfabs. --- metadata/src/artist.rs | 4 ++-- metadata/src/track.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index ac07d90e..1543d24d 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -21,10 +21,10 @@ pub struct Artist { } #[derive(Debug, Clone)] -pub struct Artists(pub Vec); +pub struct Artists(pub Vec); impl Deref for Artists { - type Target = Vec; + type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 4ab9b2b4..c4ba7e85 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -18,7 +18,7 @@ use crate::{ restriction::Restrictions, sale_period::SalePeriods, util::try_from_repeated_message, - Metadata, RequestResult, + Album, Metadata, RequestResult, }; use librespot_core::{date::Date, Error, Session, SpotifyId}; @@ -28,7 +28,7 @@ use librespot_protocol as protocol; pub struct Track { pub id: SpotifyId, pub name: String, - pub album: SpotifyId, + pub album: Album, pub artists: Artists, pub number: i32, pub disc_number: i32, From e7348db17c8dead6dad16efb45fa38b6468af91f Mon Sep 17 00:00:00 2001 From: Daniel M Date: Mon, 1 Aug 2022 00:44:43 +0200 Subject: [PATCH 225/561] Default for vec / hashmap wrappers in metadata - Derive `Default` trait for the vec / hashmap wrapper types in `librespot-metadata` - The wrapped types (`Vec` / `Hashmap`) implement Default, so the wrapper types should as well --- metadata/src/album.rs | 4 ++-- metadata/src/artist.rs | 6 +++--- metadata/src/audio/file.rs | 2 +- metadata/src/availability.rs | 2 +- metadata/src/content_rating.rs | 2 +- metadata/src/copyright.rs | 2 +- metadata/src/episode.rs | 2 +- metadata/src/external_id.rs | 2 +- metadata/src/image.rs | 4 ++-- metadata/src/playlist/attribute.rs | 6 +++--- metadata/src/playlist/item.rs | 4 ++-- metadata/src/playlist/list.rs | 4 ++-- metadata/src/playlist/operation.rs | 2 +- metadata/src/playlist/permission.rs | 2 +- metadata/src/restriction.rs | 2 +- metadata/src/sale_period.rs | 2 +- metadata/src/track.rs | 2 +- metadata/src/video.rs | 2 +- 18 files changed, 26 insertions(+), 26 deletions(-) diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 8a372245..42b5d1e3 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -41,7 +41,7 @@ pub struct Album { pub availability: Availabilities, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Albums(pub Vec); impl Deref for Albums { @@ -58,7 +58,7 @@ pub struct Disc { pub tracks: Tracks, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Discs(pub Vec); impl Deref for Discs { diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index ac07d90e..c6e2925e 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -20,7 +20,7 @@ pub struct Artist { pub top_tracks: CountryTopTracks, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Artists(pub Vec); impl Deref for Artists { @@ -37,7 +37,7 @@ pub struct ArtistWithRole { pub role: ArtistRole, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ArtistsWithRole(pub Vec); impl Deref for ArtistsWithRole { @@ -53,7 +53,7 @@ pub struct TopTracks { pub tracks: Tracks, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct CountryTopTracks(pub Vec); impl Deref for CountryTopTracks { diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 237b8e31..49f910d0 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -6,7 +6,7 @@ use librespot_protocol as protocol; use protocol::metadata::AudioFile as AudioFileMessage; pub use protocol::metadata::AudioFile_Format as AudioFileFormat; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct AudioFiles(pub HashMap); impl Deref for AudioFiles { diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 09016a5b..aca7667f 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -21,7 +21,7 @@ pub struct Availability { pub start: Date, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Availabilities(pub Vec); impl Deref for Availabilities { diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index 343f0e26..ee2e4638 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -11,7 +11,7 @@ pub struct ContentRating { pub tags: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ContentRatings(pub Vec); impl Deref for ContentRatings { diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index b7f0e838..f4fb0202 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -12,7 +12,7 @@ pub struct Copyright { pub text: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Copyrights(pub Vec); impl Deref for Copyrights { diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index c5b65f80..d91e721e 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -52,7 +52,7 @@ pub struct Episode { pub is_audiobook_chapter: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Episodes(pub Vec); impl Deref for Episodes { diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index b310200a..1573570b 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -11,7 +11,7 @@ pub struct ExternalId { pub id: String, // this can be anything from a URL to a ISRC, EAN or UPC } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct ExternalIds(pub Vec); impl Deref for ExternalIds { diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 495158d6..7540ff54 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -22,7 +22,7 @@ pub struct Image { pub height: i32, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Images(pub Vec); impl Deref for Images { @@ -38,7 +38,7 @@ pub struct PictureSize { pub url: String, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PictureSizes(pub Vec); impl Deref for PictureSizes { diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index d271bc54..24695196 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -34,7 +34,7 @@ pub struct PlaylistAttributes { pub picture_sizes: PictureSizes, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistAttributeKinds(pub Vec); impl Deref for PlaylistAttributeKinds { @@ -46,7 +46,7 @@ impl Deref for PlaylistAttributeKinds { from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistFormatAttribute(pub HashMap); impl Deref for PlaylistFormatAttribute { @@ -66,7 +66,7 @@ pub struct PlaylistItemAttributes { pub item_id: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistItemAttributeKinds(pub Vec); impl Deref for PlaylistItemAttributeKinds { diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 20f94a0b..0c413892 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -24,7 +24,7 @@ pub struct PlaylistItem { pub attributes: PlaylistItemAttributes, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistItems(pub Vec); impl Deref for PlaylistItems { @@ -53,7 +53,7 @@ pub struct PlaylistMetaItem { pub capabilities: Capabilities, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistMetaItems(pub Vec); impl Deref for PlaylistMetaItems { diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index a8ef677b..db6c14c0 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -26,7 +26,7 @@ use librespot_core::{ use librespot_protocol as protocol; use protocol::playlist4_external::GeoblockBlockingType as Geoblock; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Geoblocks(Vec); impl Deref for Geoblocks { @@ -55,7 +55,7 @@ pub struct Playlist { pub geoblocks: Geoblocks, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Playlists(pub Vec); impl Deref for Playlists { diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index fe33d0dc..5897ce5f 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -29,7 +29,7 @@ pub struct PlaylistOperation { pub update_list_attributes: PlaylistUpdateAttributes, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PlaylistOperations(pub Vec); impl Deref for PlaylistOperations { diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 2923a636..8d8c6592 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -16,7 +16,7 @@ pub struct Capabilities { pub can_cancel_membership: bool, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct PermissionLevels(pub Vec); impl Deref for PermissionLevels { diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 279da342..be3ed2b7 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -17,7 +17,7 @@ pub struct Restriction { pub countries_forbidden: Option>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Restrictions(pub Vec); impl Deref for Restrictions { diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index b02d2368..5b5752bc 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -18,7 +18,7 @@ pub struct SalePeriod { pub end: Date, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct SalePeriods(pub Vec); impl Deref for SalePeriods { diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 4ab9b2b4..cefe7519 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -53,7 +53,7 @@ pub struct Track { pub artists_with_role: ArtistsWithRole, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct Tracks(pub Vec); impl Deref for Tracks { diff --git a/metadata/src/video.rs b/metadata/src/video.rs index 5e883339..eab9e057 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -7,7 +7,7 @@ use librespot_core::FileId; use librespot_protocol as protocol; use protocol::metadata::VideoFile as VideoFileMessage; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] pub struct VideoFiles(pub Vec); impl Deref for VideoFiles { From 65b9289c4de047a85e6495f2211e26f2e6d04fff Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Mon, 1 Aug 2022 11:14:14 +0200 Subject: [PATCH 226/561] Fix #1031 - Remove `new-api` branch from test matrix --- .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 caa2722a..45ceba00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: test on: push: - branches: [dev, master, new-api] + branches: [dev, master] paths: [ "**.rs", From d09ee4206c9192e026158ed72cd8707e6eea60f1 Mon Sep 17 00:00:00 2001 From: sqozz Date: Mon, 1 Aug 2022 13:10:39 +0200 Subject: [PATCH 227/561] Add minimum rust version --- COMPILING.md | 2 -- Cargo.toml | 1 + audio/Cargo.toml | 1 + connect/Cargo.toml | 1 + core/Cargo.toml | 1 + discovery/Cargo.toml | 1 + metadata/Cargo.toml | 1 + playback/Cargo.toml | 1 + protocol/Cargo.toml | 1 + 9 files changed, 8 insertions(+), 2 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 527be464..d5b94b0e 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -7,8 +7,6 @@ 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.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: ```bash diff --git a/Cargo.toml b/Cargo.toml index ffa3d6f1..e02c6990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 43d15b87..e6649b20 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-audio" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1a41b3bb..2caa9f33 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-connect" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" diff --git a/core/Cargo.toml b/core/Cargo.toml index cc319c99..d64590c2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-core" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index c8b0e08f..ebbfb809 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-discovery" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index f57159e3..0d86cd29 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-metadata" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 89237108..39e87261 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-playback" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 00b97bb5..aa5f4496 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "librespot-protocol" version = "0.5.0-dev" +rust-version = "1.61" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From 355c3b06ebb336ce59f05c3afd8c628bfd02fbcb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 1 Aug 2022 22:18:44 +0200 Subject: [PATCH 228/561] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 28 +++++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 ++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..db1159ae --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,28 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To reproduce** +Steps to reproduce the behavior: +1. Launch `librespot` with '...' +2. Connect with '...' +3. In the client click on '...' +4. See error + +**Log** +A full log so we may trace your problem (launch `librespot` with `--verbose`). Format the log as code. + +**Host (what you are running `librespot` on):** +- OS: [e.g. Linux] +- Platform: [e.g. RPi 3B+] + +**Additional context** +Add any other context about the problem here. If your issue is related to sound playback, at a minimum specify the type and make of your output device. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. From 2bce489159b530a6717fcbf1813bc2903d120a85 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 1 Aug 2022 22:46:05 +0200 Subject: [PATCH 229/561] Create SECURITY.md --- SECURITY.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..6a1c6b2e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security Policy + +## Supported Versions + +We will support the latest release and main development branch with security updates. + +## Reporting a Vulnerability + +If you believe to have found a vulnerability in `librespot` itself or as a result from +one of its dependencies, please report it by contacting one or more of the active +maintainers directly, allowing no less than three calendar days to receive a response. + +If you believe that the vulnerability is public knowledge or already being exploited +in the wild, regardless of having received a response to your direct messages or not, +please create an issue report to warn other users about continued use and instruct +them on any known workarounds. + +On your report you may expect feedback on whether we believe that the vulnerability +is indeed applicable and if so, when and how it may be fixed. You may expect to +be asked for assistance with review and testing. From 8ffaf7cb8d2187c90b452efdb8c5e7850345293a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 1 Aug 2022 23:14:21 +0200 Subject: [PATCH 230/561] Document crate and changelog updates --- PUBLISHING.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/PUBLISHING.md b/PUBLISHING.md index e38cd6d8..9d32caad 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -8,19 +8,24 @@ The Bash script in the root of the project, named `publish.sh` can be used to pu Make sure that you are are starting from a clean working directory for both `dev` and `master`, completely up to date with remote and all local changes either committed and pushed or stashed. +Note that the script will update the crates and lockfile, so in case you did not do so before, you really should to make sure none of the dependencies introduce some SemVer breaking change. Then commit so you again have a clean working directory. + +Also don't forget to update `CHANGELOG.md` with the version number, release date, and at the bottom the comparison links. + You will want to perform a dry run first: `./publish --dry-run 0.1.0`. Please make note of any errors or warnings. In particular, you may need to explicitly inform Git which remote you want to track for the `master` branch like so: `git --track origin/master` (or whatever you have called the `librespot-org` remote `master` branch). Depending on your system the script may fail to publish the main `librespot` crate after having published all the `librespot-xyz` sub-crates. If so then make sure the working directory is committed and pushed (watch `Cargo.toml`) and then run `cargo publish` manually after `publish.sh` finished. -To publish the crates your GitHub account needs to be authorized on `crates.io` by `librespot-org`. +To publish the crates your GitHub account needs to be authorized on `crates.io` by `librespot-org`. First time you should run `cargo login` and follow the on-screen instructions. ## What the script does This is briefly how the script works: - Change to branch master, pull latest version, merge development branch. - - CD to working dir. + - Change to working directory. - Change version number in all files. + - Update crates and lockfile. - Commit and tag changes. - Publish crates in given order. - Push version commit and tags to master. @@ -35,4 +40,4 @@ The `protocol` package needs to be published with `cargo publish --no-verify` du Publishing can be done using the command `cargo publish` in each of the directories of the respective crate. -The script is meant to cover the standard publishing process. There are various improvements that could be made, such as adding options such as the user being able to add a change log, though this is not the main focus, as the script is intended to be run by a CI. Feel free to improve and extend functionality, keeping in mind that it should always be possible for the script to be run in a non-interactive fashion. +The script is meant to cover the standard publishing process. There are various improvements that could be made, such as adding options such as the user being able to add a changelog, though this is not the main focus, as the script is intended to be run by a CI. Feel free to improve and extend functionality, keeping in mind that it should always be possible for the script to be run in a non-interactive fashion. From e5092c84fd7ecea74c0b1ac4fbbef9d6df35ee7b Mon Sep 17 00:00:00 2001 From: dnlmlr <34707428+dnlmlr@users.noreply.github.com> Date: Tue, 2 Aug 2022 11:43:48 +0200 Subject: [PATCH 231/561] Implement additional metadata for Artist (#1036) - Added `*-current()` functions to `Artist` to get the list of current versions / releases of each album - This is useful since the `AlbumGroups` can contain multiple versions / releases of the same album --- metadata/src/artist.rs | 198 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 196 insertions(+), 2 deletions(-) diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 4144f0c3..d6d0b4e2 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -4,20 +4,51 @@ use std::{ ops::Deref, }; -use crate::{request::RequestResult, track::Tracks, util::try_from_repeated_message, Metadata}; +use crate::{ + album::Albums, + availability::Availabilities, + external_id::ExternalIds, + image::Images, + request::RequestResult, + restriction::Restrictions, + sale_period::SalePeriods, + track::Tracks, + util::{from_repeated_message, try_from_repeated_message}, + Metadata, +}; use librespot_core::{Error, Session, SpotifyId}; use librespot_protocol as protocol; -use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage; pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; + +use protocol::metadata::ActivityPeriod as ActivityPeriodMessage; +use protocol::metadata::AlbumGroup as AlbumGroupMessage; +use protocol::metadata::ArtistWithRole as ArtistWithRoleMessage; +use protocol::metadata::Biography as BiographyMessage; use protocol::metadata::TopTracks as TopTracksMessage; #[derive(Debug, Clone)] pub struct Artist { pub id: SpotifyId, pub name: String, + pub popularity: i32, pub top_tracks: CountryTopTracks, + pub albums: AlbumGroups, + pub singles: AlbumGroups, + pub compilations: AlbumGroups, + pub appears_on_albums: AlbumGroups, + pub genre: Vec, + pub external_ids: ExternalIds, + pub portraits: Images, + pub biographies: Biographies, + pub activity_periods: ActivityPeriods, + pub restrictions: Restrictions, + pub related: Artists, + pub is_portrait_album_cover: bool, + pub portrait_group: Images, + pub sales_periods: SalePeriods, + pub availabilities: Availabilities, } #[derive(Debug, Clone, Default)] @@ -63,6 +94,69 @@ impl Deref for CountryTopTracks { } } +#[derive(Debug, Clone, Default)] +pub struct AlbumGroup(pub Albums); + +impl Deref for AlbumGroup { + type Target = Albums; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// `AlbumGroups` contains collections of album variants (different releases of the same album). +/// Ignoring the wrapping types it is structured roughly like this: +/// ```text +/// AlbumGroups [ +/// [Album1], [Album2-relelease, Album2-older-release], [Album3] +/// ] +/// ``` +/// In most cases only the current variant of each album is needed. A list of every album in it's +/// current release variant can be obtained by using [`AlbumGroups::current_releases`] +#[derive(Debug, Clone, Default)] +pub struct AlbumGroups(pub Vec); + +impl Deref for AlbumGroups { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct Biography { + pub text: String, + pub portraits: Images, + pub portrait_group: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct Biographies(pub Vec); + +impl Deref for Biographies { + type Target = Vec; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone)] +pub struct ActivityPeriod { + pub start_year: i32, + pub end_year: i32, + pub decade: i32, +} + +#[derive(Debug, Clone, Default)] +pub struct ActivityPeriods(pub Vec); + +impl Deref for ActivityPeriods { + 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) { @@ -77,6 +171,37 @@ impl CountryTopTracks { } } +impl Artist { + /// Get the full list of albums, not containing duplicate variants of the same albums. + /// + /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] + pub fn albums_current(&self) -> Albums { + self.albums.current_releases() + } + + /// Get the full list of singles, not containing duplicate variants of the same singles. + /// + /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] + pub fn singles_current(&self) -> Albums { + self.singles.current_releases() + } + + /// Get the full list of compilations, not containing duplicate variants of the same + /// compilations. + /// + /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] + pub fn compilations_current(&self) -> Albums { + self.compilations.current_releases() + } + + /// Get the full list of albums, not containing duplicate variants of the same albums. + /// + /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] + pub fn appears_on_albums_current(&self) -> Albums { + self.appears_on_albums.current_releases() + } +} + #[async_trait] impl Metadata for Artist { type Message = protocol::metadata::Artist; @@ -96,7 +221,23 @@ impl TryFrom<&::Message> for Artist { Ok(Self { id: artist.try_into()?, name: artist.get_name().to_owned(), + popularity: artist.get_popularity(), top_tracks: artist.get_top_track().try_into()?, + albums: artist.get_album_group().try_into()?, + singles: artist.get_single_group().try_into()?, + compilations: artist.get_compilation_group().try_into()?, + appears_on_albums: artist.get_appears_on_group().try_into()?, + genre: artist.get_genre().to_vec(), + external_ids: artist.get_external_id().into(), + portraits: artist.get_portrait().into(), + biographies: artist.get_biography().into(), + activity_periods: artist.get_activity_period().into(), + restrictions: artist.get_restriction().into(), + related: artist.get_related().try_into()?, + is_portrait_album_cover: artist.get_is_portrait_album_cover(), + portrait_group: artist.get_portrait_group().get_image().into(), + sales_periods: artist.get_sale_period().try_into()?, + availabilities: artist.get_availability().try_into()?, }) } } @@ -127,3 +268,56 @@ impl TryFrom<&TopTracksMessage> for TopTracks { } try_from_repeated_message!(TopTracksMessage, CountryTopTracks); + +impl TryFrom<&AlbumGroupMessage> for AlbumGroup { + type Error = librespot_core::Error; + fn try_from(album_groups: &AlbumGroupMessage) -> Result { + Ok(Self(album_groups.get_album().try_into()?)) + } +} + +impl AlbumGroups { + /// Get the contained albums. This will only use the latest release / variant of an album if + /// multiple variants are available. This should be used if multiple variants of the same album + /// are not explicitely desired. + pub fn current_releases(&self) -> Albums { + let albums = self + .iter() + .filter_map(|agrp| agrp.first()) + .cloned() + .collect(); + Albums(albums) + } +} + +try_from_repeated_message!(AlbumGroupMessage, AlbumGroups); + +impl From<&BiographyMessage> for Biography { + fn from(biography: &BiographyMessage) -> Self { + let portrait_group = biography + .get_portrait_group() + .iter() + .map(|it| it.get_image().into()) + .collect(); + + Self { + text: biography.get_text().to_owned(), + portraits: biography.get_portrait().into(), + portrait_group, + } + } +} + +from_repeated_message!(BiographyMessage, Biographies); + +impl From<&ActivityPeriodMessage> for ActivityPeriod { + fn from(activity_periode: &ActivityPeriodMessage) -> Self { + Self { + start_year: activity_periode.get_start_year(), + end_year: activity_periode.get_end_year(), + decade: activity_periode.get_decade(), + } + } +} + +from_repeated_message!(ActivityPeriodMessage, ActivityPeriods); From a7fa0e2299d1567d65e768e3ca617d61a14c02d2 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 2 Aug 2022 12:45:37 +0200 Subject: [PATCH 232/561] Implement `DerefMut` for wrapper types in metadata - Change the `Deref` implementations for metadata wrapper types to use a new `impl_deref_wrapped` macro - The macro implements `Deref` and `DerefMut` --- metadata/src/album.rs | 30 ++++++++-------- metadata/src/artist.rs | 53 +++++------------------------ metadata/src/audio/file.rs | 15 ++++---- metadata/src/availability.rs | 11 ++---- metadata/src/content_rating.rs | 14 ++++---- metadata/src/copyright.rs | 14 ++++---- metadata/src/episode.rs | 11 ++---- metadata/src/external_id.rs | 14 ++++---- metadata/src/image.rs | 25 +++----------- metadata/src/playlist/attribute.rs | 28 +++++---------- metadata/src/playlist/item.rs | 18 +++------- metadata/src/playlist/list.rs | 25 +++----------- metadata/src/playlist/operation.rs | 11 ++---- metadata/src/playlist/permission.rs | 14 ++++---- metadata/src/restriction.rs | 20 ++++------- metadata/src/sale_period.rs | 14 ++++---- metadata/src/track.rs | 11 ++---- metadata/src/util.rs | 19 +++++++++++ metadata/src/video.rs | 14 ++++---- 19 files changed, 127 insertions(+), 234 deletions(-) diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 42b5d1e3..a7b7a1cf 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -1,13 +1,21 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; 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, + artist::Artists, + availability::Availabilities, + copyright::Copyrights, + external_id::ExternalIds, + image::Images, + request::RequestResult, + restriction::Restrictions, + sale_period::SalePeriods, + track::Tracks, + util::{impl_deref_wrapped, try_from_repeated_message}, + Metadata, }; use librespot_core::{date::Date, Error, Session, SpotifyId}; @@ -44,12 +52,7 @@ pub struct Album { #[derive(Debug, Clone, Default)] pub struct Albums(pub Vec); -impl Deref for Albums { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Albums, Vec); #[derive(Debug, Clone)] pub struct Disc { @@ -61,12 +64,7 @@ pub struct Disc { #[derive(Debug, Clone, Default)] pub struct Discs(pub Vec); -impl Deref for Discs { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Discs, Vec); impl Album { pub fn tracks(&self) -> Tracks { diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index d6d0b4e2..cd668c2a 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -1,7 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use crate::{ @@ -13,7 +13,7 @@ use crate::{ restriction::Restrictions, sale_period::SalePeriods, track::Tracks, - util::{from_repeated_message, try_from_repeated_message}, + util::{from_repeated_message, impl_deref_wrapped, try_from_repeated_message}, Metadata, }; @@ -54,12 +54,7 @@ pub struct Artist { #[derive(Debug, Clone, Default)] pub struct Artists(pub Vec); -impl Deref for Artists { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Artists, Vec); #[derive(Debug, Clone)] pub struct ArtistWithRole { @@ -71,12 +66,7 @@ pub struct ArtistWithRole { #[derive(Debug, Clone, Default)] pub struct ArtistsWithRole(pub Vec); -impl Deref for ArtistsWithRole { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(ArtistsWithRole, Vec); #[derive(Debug, Clone)] pub struct TopTracks { @@ -87,22 +77,12 @@ pub struct TopTracks { #[derive(Debug, Clone, Default)] pub struct CountryTopTracks(pub Vec); -impl Deref for CountryTopTracks { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(CountryTopTracks, Vec); #[derive(Debug, Clone, Default)] pub struct AlbumGroup(pub Albums); -impl Deref for AlbumGroup { - type Target = Albums; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(AlbumGroup, Albums); /// `AlbumGroups` contains collections of album variants (different releases of the same album). /// Ignoring the wrapping types it is structured roughly like this: @@ -116,12 +96,7 @@ impl Deref for AlbumGroup { #[derive(Debug, Clone, Default)] pub struct AlbumGroups(pub Vec); -impl Deref for AlbumGroups { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(AlbumGroups, Vec); #[derive(Debug, Clone)] pub struct Biography { @@ -133,12 +108,7 @@ pub struct Biography { #[derive(Debug, Clone, Default)] pub struct Biographies(pub Vec); -impl Deref for Biographies { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Biographies, Vec); #[derive(Debug, Clone)] pub struct ActivityPeriod { @@ -150,12 +120,7 @@ pub struct ActivityPeriod { #[derive(Debug, Clone, Default)] pub struct ActivityPeriods(pub Vec); -impl Deref for ActivityPeriods { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(ActivityPeriods, Vec); impl CountryTopTracks { pub fn for_country(&self, country: &str) -> Tracks { diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 49f910d0..08017187 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -1,4 +1,8 @@ -use std::{collections::HashMap, fmt::Debug, ops::Deref}; +use std::{ + collections::HashMap, + fmt::Debug, + ops::{Deref, DerefMut}, +}; use librespot_core::FileId; @@ -6,15 +10,12 @@ use librespot_protocol as protocol; use protocol::metadata::AudioFile as AudioFileMessage; pub use protocol::metadata::AudioFile_Format as AudioFileFormat; +use crate::util::impl_deref_wrapped; + #[derive(Debug, Clone, Default)] pub struct AudioFiles(pub HashMap); -impl Deref for AudioFiles { - type Target = HashMap; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(AudioFiles, HashMap); impl AudioFiles { pub fn is_ogg_vorbis(format: AudioFileFormat) -> bool { diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index aca7667f..62fada0d 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,12 +1,12 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use thiserror::Error; -use crate::util::try_from_repeated_message; +use crate::util::{impl_deref_wrapped, try_from_repeated_message}; use librespot_core::date::Date; @@ -24,12 +24,7 @@ pub struct Availability { #[derive(Debug, Clone, Default)] pub struct Availabilities(pub Vec); -impl Deref for Availabilities { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Availabilities, Vec); #[derive(Debug, Copy, Clone, Error)] pub enum UnavailabilityReason { diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index ee2e4638..eeb7b717 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -1,6 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; -use crate::util::from_repeated_message; +use crate::util::{from_repeated_message, impl_deref_wrapped}; use librespot_protocol as protocol; use protocol::metadata::ContentRating as ContentRatingMessage; @@ -14,12 +17,7 @@ pub struct ContentRating { #[derive(Debug, Clone, Default)] pub struct ContentRatings(pub Vec); -impl Deref for ContentRatings { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(ContentRatings, Vec); impl From<&ContentRatingMessage> for ContentRating { fn from(content_rating: &ContentRatingMessage) -> Self { diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index f4fb0202..689aa87c 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -1,6 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; -use crate::util::from_repeated_message; +use crate::util::{from_repeated_message, impl_deref_wrapped}; use librespot_protocol as protocol; use protocol::metadata::Copyright as CopyrightMessage; @@ -15,12 +18,7 @@ pub struct Copyright { #[derive(Debug, Clone, Default)] pub struct Copyrights(pub Vec); -impl Deref for Copyrights { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Copyrights, Vec); impl From<&CopyrightMessage> for Copyright { fn from(copyright: &CopyrightMessage) -> Self { diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index d91e721e..e7305b9f 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -1,7 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use crate::{ @@ -14,7 +14,7 @@ use crate::{ image::Images, request::RequestResult, restriction::Restrictions, - util::try_from_repeated_message, + util::{impl_deref_wrapped, try_from_repeated_message}, video::VideoFiles, Metadata, }; @@ -55,12 +55,7 @@ pub struct Episode { #[derive(Debug, Clone, Default)] pub struct Episodes(pub Vec); -impl Deref for Episodes { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Episodes, Vec); #[async_trait] impl InnerAudioItem for Episode { diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index 1573570b..f5dff80e 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -1,6 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; -use crate::util::from_repeated_message; +use crate::util::{from_repeated_message, impl_deref_wrapped}; use librespot_protocol as protocol; use protocol::metadata::ExternalId as ExternalIdMessage; @@ -14,12 +17,7 @@ pub struct ExternalId { #[derive(Debug, Clone, Default)] pub struct ExternalIds(pub Vec); -impl Deref for ExternalIds { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(ExternalIds, Vec); impl From<&ExternalIdMessage> for ExternalId { fn from(external_id: &ExternalIdMessage) -> Self { diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 7540ff54..488fe8b8 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -1,10 +1,10 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, try_from_repeated_message}; +use crate::util::{from_repeated_message, impl_deref_wrapped, try_from_repeated_message}; use librespot_core::{FileId, SpotifyId}; @@ -25,12 +25,7 @@ pub struct Image { #[derive(Debug, Clone, Default)] pub struct Images(pub Vec); -impl Deref for Images { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Images, Vec); #[derive(Debug, Clone)] pub struct PictureSize { @@ -41,12 +36,7 @@ pub struct PictureSize { #[derive(Debug, Clone, Default)] pub struct PictureSizes(pub Vec); -impl Deref for PictureSizes { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PictureSizes, Vec); #[derive(Debug, Clone)] pub struct TranscodedPicture { @@ -57,12 +47,7 @@ pub struct TranscodedPicture { #[derive(Debug, Clone)] pub struct TranscodedPictures(pub Vec); -impl Deref for TranscodedPictures { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(TranscodedPictures, Vec); impl From<&ImageMessage> for Image { fn from(image: &ImageMessage) -> Self { diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index 24695196..43154e90 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -2,10 +2,13 @@ use std::{ collections::HashMap, convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; -use crate::{image::PictureSizes, util::from_repeated_enum}; +use crate::{ + image::PictureSizes, + util::{from_repeated_enum, impl_deref_wrapped}, +}; use librespot_core::date::Date; @@ -37,24 +40,14 @@ pub struct PlaylistAttributes { #[derive(Debug, Clone, Default)] pub struct PlaylistAttributeKinds(pub Vec); -impl Deref for PlaylistAttributeKinds { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistAttributeKinds, Vec); from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds); #[derive(Debug, Clone, Default)] pub struct PlaylistFormatAttribute(pub HashMap); -impl Deref for PlaylistFormatAttribute { - type Target = HashMap; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistFormatAttribute, HashMap); #[derive(Debug, Clone)] pub struct PlaylistItemAttributes { @@ -69,12 +62,7 @@ pub struct PlaylistItemAttributes { #[derive(Debug, Clone, Default)] pub struct PlaylistItemAttributeKinds(pub Vec); -impl Deref for PlaylistItemAttributeKinds { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistItemAttributeKinds, Vec); from_repeated_enum!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds); diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 0c413892..819783e0 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -1,10 +1,10 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; -use crate::util::try_from_repeated_message; +use crate::util::{impl_deref_wrapped, try_from_repeated_message}; use super::{ attribute::{PlaylistAttributes, PlaylistItemAttributes}, @@ -27,12 +27,7 @@ pub struct PlaylistItem { #[derive(Debug, Clone, Default)] pub struct PlaylistItems(pub Vec); -impl Deref for PlaylistItems { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistItems, Vec); #[derive(Debug, Clone)] pub struct PlaylistItemList { @@ -56,12 +51,7 @@ pub struct PlaylistMetaItem { #[derive(Debug, Clone, Default)] pub struct PlaylistMetaItems(pub Vec); -impl Deref for PlaylistMetaItems { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistMetaItems, Vec); impl TryFrom<&PlaylistItemMessage> for PlaylistItem { type Error = librespot_core::Error; diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index db6c14c0..909e5d7a 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -1,14 +1,14 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use protobuf::Message; use crate::{ request::{MercuryRequest, RequestResult}, - util::{from_repeated_enum, try_from_repeated_message}, + util::{from_repeated_enum, impl_deref_wrapped, try_from_repeated_message}, Metadata, }; @@ -29,12 +29,7 @@ use protocol::playlist4_external::GeoblockBlockingType as Geoblock; #[derive(Debug, Clone, Default)] pub struct Geoblocks(Vec); -impl Deref for Geoblocks { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Geoblocks, Vec); #[derive(Debug, Clone)] pub struct Playlist { @@ -58,22 +53,12 @@ pub struct Playlist { #[derive(Debug, Clone, Default)] pub struct Playlists(pub Vec); -impl Deref for Playlists { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Playlists, Vec); #[derive(Debug, Clone)] pub struct RootPlaylist(pub SelectedListContent); -impl Deref for RootPlaylist { - type Target = SelectedListContent; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(RootPlaylist, SelectedListContent); #[derive(Debug, Clone)] pub struct SelectedListContent { diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index 5897ce5f..44f5ed74 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -1,7 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use crate::{ @@ -9,7 +9,7 @@ use crate::{ attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes}, item::PlaylistItems, }, - util::try_from_repeated_message, + util::{impl_deref_wrapped, try_from_repeated_message}, }; use librespot_protocol as protocol; @@ -32,12 +32,7 @@ pub struct PlaylistOperation { #[derive(Debug, Clone, Default)] pub struct PlaylistOperations(pub Vec); -impl Deref for PlaylistOperations { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PlaylistOperations, Vec); #[derive(Debug, Clone)] pub struct PlaylistOperationAdd { diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 8d8c6592..41479d94 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -1,6 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; -use crate::util::from_repeated_enum; +use crate::util::{from_repeated_enum, impl_deref_wrapped}; use librespot_protocol as protocol; use protocol::playlist_permission::Capabilities as CapabilitiesMessage; @@ -19,12 +22,7 @@ pub struct Capabilities { #[derive(Debug, Clone, Default)] pub struct PermissionLevels(pub Vec); -impl Deref for PermissionLevels { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(PermissionLevels, Vec); impl From<&CapabilitiesMessage> for Capabilities { fn from(playlist: &CapabilitiesMessage) -> Self { diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index be3ed2b7..614daf64 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -1,5 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; +use crate::util::impl_deref_wrapped; use crate::util::{from_repeated_enum, from_repeated_message}; use protocol::metadata::Restriction as RestrictionMessage; @@ -20,22 +24,12 @@ pub struct Restriction { #[derive(Debug, Clone, Default)] pub struct Restrictions(pub Vec); -impl Deref for Restrictions { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Restrictions, Vec); #[derive(Debug, Clone)] pub struct RestrictionCatalogues(pub Vec); -impl Deref for RestrictionCatalogues { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(RestrictionCatalogues, Vec); impl Restriction { fn parse_country_codes(country_codes: &str) -> Vec { diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 5b5752bc..4981bb97 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,10 +1,13 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; -use crate::{restriction::Restrictions, util::try_from_repeated_message}; +use crate::{ + restriction::Restrictions, + util::{impl_deref_wrapped, try_from_repeated_message}, +}; use librespot_core::date::Date; @@ -21,12 +24,7 @@ pub struct SalePeriod { #[derive(Debug, Clone, Default)] pub struct SalePeriods(pub Vec); -impl Deref for SalePeriods { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(SalePeriods, Vec); impl TryFrom<&SalePeriodMessage> for SalePeriod { type Error = librespot_core::Error; diff --git a/metadata/src/track.rs b/metadata/src/track.rs index d2ead8ac..ab365924 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -1,7 +1,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt::Debug, - ops::Deref, + ops::{Deref, DerefMut}, }; use uuid::Uuid; @@ -17,7 +17,7 @@ use crate::{ external_id::ExternalIds, restriction::Restrictions, sale_period::SalePeriods, - util::try_from_repeated_message, + util::{impl_deref_wrapped, try_from_repeated_message}, Album, Metadata, RequestResult, }; @@ -56,12 +56,7 @@ pub struct Track { #[derive(Debug, Clone, Default)] pub struct Tracks(pub Vec); -impl Deref for Tracks { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(Tracks, Vec); #[async_trait] impl InnerAudioItem for Track { diff --git a/metadata/src/util.rs b/metadata/src/util.rs index 59142847..2f3d8340 100644 --- a/metadata/src/util.rs +++ b/metadata/src/util.rs @@ -37,3 +37,22 @@ macro_rules! try_from_repeated_message { } pub(crate) use try_from_repeated_message; + +macro_rules! impl_deref_wrapped { + ($wrapper:ty, $inner:ty) => { + impl Deref for $wrapper { + type Target = $inner; + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for $wrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + }; +} + +pub(crate) use impl_deref_wrapped; diff --git a/metadata/src/video.rs b/metadata/src/video.rs index eab9e057..18d7073f 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -1,6 +1,9 @@ -use std::{fmt::Debug, ops::Deref}; +use std::{ + fmt::Debug, + ops::{Deref, DerefMut}, +}; -use crate::util::from_repeated_message; +use crate::util::{from_repeated_message, impl_deref_wrapped}; use librespot_core::FileId; @@ -10,11 +13,6 @@ use protocol::metadata::VideoFile as VideoFileMessage; #[derive(Debug, Clone, Default)] pub struct VideoFiles(pub Vec); -impl Deref for VideoFiles { - type Target = Vec; - fn deref(&self) -> &Self::Target { - &self.0 - } -} +impl_deref_wrapped!(VideoFiles, Vec); from_repeated_message!(VideoFileMessage, VideoFiles); From 0c69126065dfc6e7d28d0f5575cd166e71ddb926 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Tue, 2 Aug 2022 14:21:07 +0200 Subject: [PATCH 233/561] Rename `from_repeated_*` functions to `impl_*` - `from_repeated_message` -> `impl_from_repeated` - `from_repeated_enum` -> `impl_from_repeated_copy` since the enum references were just simply deref copied - `try_from_repeated_message` -> `impl_try_from_repeated` - Simplified the implementation of `from_repeated_enum` --- metadata/src/album.rs | 6 +++--- metadata/src/artist.rs | 14 +++++++------- metadata/src/availability.rs | 4 ++-- metadata/src/content_rating.rs | 4 ++-- metadata/src/copyright.rs | 4 ++-- metadata/src/episode.rs | 4 ++-- metadata/src/external_id.rs | 4 ++-- metadata/src/image.rs | 8 ++++---- metadata/src/playlist/attribute.rs | 6 +++--- metadata/src/playlist/item.rs | 6 +++--- metadata/src/playlist/list.rs | 6 +++--- metadata/src/playlist/operation.rs | 4 ++-- metadata/src/playlist/permission.rs | 4 ++-- metadata/src/restriction.rs | 6 +++--- metadata/src/sale_period.rs | 4 ++-- metadata/src/track.rs | 4 ++-- metadata/src/util.rs | 14 +++++++------- metadata/src/video.rs | 4 ++-- 18 files changed, 53 insertions(+), 53 deletions(-) diff --git a/metadata/src/album.rs b/metadata/src/album.rs index a7b7a1cf..85bee013 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -14,7 +14,7 @@ use crate::{ restriction::Restrictions, sale_period::SalePeriods, track::Tracks, - util::{impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_try_from_repeated}, Metadata, }; @@ -119,7 +119,7 @@ impl TryFrom<&::Message> for Album { } } -try_from_repeated_message!(::Message, Albums); +impl_try_from_repeated!(::Message, Albums); impl TryFrom<&DiscMessage> for Disc { type Error = librespot_core::Error; @@ -132,4 +132,4 @@ impl TryFrom<&DiscMessage> for Disc { } } -try_from_repeated_message!(DiscMessage, Discs); +impl_try_from_repeated!(DiscMessage, Discs); diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index cd668c2a..7ddf07a1 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -13,7 +13,7 @@ use crate::{ restriction::Restrictions, sale_period::SalePeriods, track::Tracks, - util::{from_repeated_message, impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}, Metadata, }; @@ -207,7 +207,7 @@ impl TryFrom<&::Message> for Artist { } } -try_from_repeated_message!(::Message, Artists); +impl_try_from_repeated!(::Message, Artists); impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { type Error = librespot_core::Error; @@ -220,7 +220,7 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { } } -try_from_repeated_message!(ArtistWithRoleMessage, ArtistsWithRole); +impl_try_from_repeated!(ArtistWithRoleMessage, ArtistsWithRole); impl TryFrom<&TopTracksMessage> for TopTracks { type Error = librespot_core::Error; @@ -232,7 +232,7 @@ impl TryFrom<&TopTracksMessage> for TopTracks { } } -try_from_repeated_message!(TopTracksMessage, CountryTopTracks); +impl_try_from_repeated!(TopTracksMessage, CountryTopTracks); impl TryFrom<&AlbumGroupMessage> for AlbumGroup { type Error = librespot_core::Error; @@ -255,7 +255,7 @@ impl AlbumGroups { } } -try_from_repeated_message!(AlbumGroupMessage, AlbumGroups); +impl_try_from_repeated!(AlbumGroupMessage, AlbumGroups); impl From<&BiographyMessage> for Biography { fn from(biography: &BiographyMessage) -> Self { @@ -273,7 +273,7 @@ impl From<&BiographyMessage> for Biography { } } -from_repeated_message!(BiographyMessage, Biographies); +impl_from_repeated!(BiographyMessage, Biographies); impl From<&ActivityPeriodMessage> for ActivityPeriod { fn from(activity_periode: &ActivityPeriodMessage) -> Self { @@ -285,4 +285,4 @@ impl From<&ActivityPeriodMessage> for ActivityPeriod { } } -from_repeated_message!(ActivityPeriodMessage, ActivityPeriods); +impl_from_repeated!(ActivityPeriodMessage, ActivityPeriods); diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 62fada0d..20727f8c 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -6,7 +6,7 @@ use std::{ use thiserror::Error; -use crate::util::{impl_deref_wrapped, try_from_repeated_message}; +use crate::util::{impl_deref_wrapped, impl_try_from_repeated}; use librespot_core::date::Date; @@ -48,4 +48,4 @@ impl TryFrom<&AvailabilityMessage> for Availability { } } -try_from_repeated_message!(AvailabilityMessage, Availabilities); +impl_try_from_repeated!(AvailabilityMessage, Availabilities); diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index eeb7b717..42d0ad5e 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -3,7 +3,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, impl_deref_wrapped}; +use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_protocol as protocol; use protocol::metadata::ContentRating as ContentRatingMessage; @@ -28,4 +28,4 @@ impl From<&ContentRatingMessage> for ContentRating { } } -from_repeated_message!(ContentRatingMessage, ContentRatings); +impl_from_repeated!(ContentRatingMessage, ContentRatings); diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index 689aa87c..360fd994 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -3,7 +3,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, impl_deref_wrapped}; +use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_protocol as protocol; use protocol::metadata::Copyright as CopyrightMessage; @@ -29,4 +29,4 @@ impl From<&CopyrightMessage> for Copyright { } } -from_repeated_message!(CopyrightMessage, Copyrights); +impl_from_repeated!(CopyrightMessage, Copyrights); diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index e7305b9f..e3a5c7a3 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -14,7 +14,7 @@ use crate::{ image::Images, request::RequestResult, restriction::Restrictions, - util::{impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_try_from_repeated}, video::VideoFiles, Metadata, }; @@ -125,4 +125,4 @@ impl TryFrom<&::Message> for Episode { } } -try_from_repeated_message!(::Message, Episodes); +impl_try_from_repeated!(::Message, Episodes); diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index f5dff80e..ce8d4fd8 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -3,7 +3,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, impl_deref_wrapped}; +use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_protocol as protocol; use protocol::metadata::ExternalId as ExternalIdMessage; @@ -28,4 +28,4 @@ impl From<&ExternalIdMessage> for ExternalId { } } -from_repeated_message!(ExternalIdMessage, ExternalIds); +impl_from_repeated!(ExternalIdMessage, ExternalIds); diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 488fe8b8..dd716623 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -4,7 +4,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, impl_deref_wrapped, try_from_repeated_message}; +use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}; use librespot_core::{FileId, SpotifyId}; @@ -60,7 +60,7 @@ impl From<&ImageMessage> for Image { } } -from_repeated_message!(ImageMessage, Images); +impl_from_repeated!(ImageMessage, Images); impl From<&PictureSizeMessage> for PictureSize { fn from(size: &PictureSizeMessage) -> Self { @@ -71,7 +71,7 @@ impl From<&PictureSizeMessage> for PictureSize { } } -from_repeated_message!(PictureSizeMessage, PictureSizes); +impl_from_repeated!(PictureSizeMessage, PictureSizes); impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { type Error = librespot_core::Error; @@ -83,4 +83,4 @@ impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { } } -try_from_repeated_message!(TranscodedPictureMessage, TranscodedPictures); +impl_try_from_repeated!(TranscodedPictureMessage, TranscodedPictures); diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index 43154e90..b5849a66 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, impl_deref_wrapped}, + util::{impl_deref_wrapped, impl_from_repeated_copy}, }; use librespot_core::date::Date; @@ -42,7 +42,7 @@ pub struct PlaylistAttributeKinds(pub Vec); impl_deref_wrapped!(PlaylistAttributeKinds, Vec); -from_repeated_enum!(PlaylistAttributeKind, PlaylistAttributeKinds); +impl_from_repeated_copy!(PlaylistAttributeKind, PlaylistAttributeKinds); #[derive(Debug, Clone, Default)] pub struct PlaylistFormatAttribute(pub HashMap); @@ -64,7 +64,7 @@ pub struct PlaylistItemAttributeKinds(pub Vec); impl_deref_wrapped!(PlaylistItemAttributeKinds, Vec); -from_repeated_enum!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds); +impl_from_repeated_copy!(PlaylistItemAttributeKind, PlaylistItemAttributeKinds); #[derive(Debug, Clone)] pub struct PlaylistPartialAttributes { diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 819783e0..f684f771 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -4,7 +4,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{impl_deref_wrapped, try_from_repeated_message}; +use crate::util::{impl_deref_wrapped, impl_try_from_repeated}; use super::{ attribute::{PlaylistAttributes, PlaylistItemAttributes}, @@ -63,7 +63,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem { } } -try_from_repeated_message!(PlaylistItemMessage, PlaylistItems); +impl_try_from_repeated!(PlaylistItemMessage, PlaylistItems); impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { type Error = librespot_core::Error; @@ -92,4 +92,4 @@ impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { } } -try_from_repeated_message!(PlaylistMetaItemMessage, PlaylistMetaItems); +impl_try_from_repeated!(PlaylistMetaItemMessage, PlaylistMetaItems); diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 909e5d7a..223f8d30 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -8,7 +8,7 @@ use protobuf::Message; use crate::{ request::{MercuryRequest, RequestResult}, - util::{from_repeated_enum, impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated}, Metadata, }; @@ -210,5 +210,5 @@ impl TryFrom<&::Message> for SelectedListContent { } } -from_repeated_enum!(Geoblock, Geoblocks); -try_from_repeated_message!(Vec, Playlists); +impl_from_repeated_copy!(Geoblock, Geoblocks); +impl_try_from_repeated!(Vec, Playlists); diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index 44f5ed74..06264aca 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -9,7 +9,7 @@ use crate::{ attribute::{PlaylistUpdateAttributes, PlaylistUpdateItemAttributes}, item::PlaylistItems, }, - util::{impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_try_from_repeated}, }; use librespot_protocol as protocol; @@ -71,7 +71,7 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { } } -try_from_repeated_message!(PlaylistOperationMessage, PlaylistOperations); +impl_try_from_repeated!(PlaylistOperationMessage, PlaylistOperations); impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { type Error = librespot_core::Error; diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 41479d94..47ddbdee 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -3,7 +3,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_enum, impl_deref_wrapped}; +use crate::util::{impl_deref_wrapped, impl_from_repeated_copy}; use librespot_protocol as protocol; use protocol::playlist_permission::Capabilities as CapabilitiesMessage; @@ -37,4 +37,4 @@ impl From<&CapabilitiesMessage> for Capabilities { } } -from_repeated_enum!(PermissionLevel, PermissionLevels); +impl_from_repeated_copy!(PermissionLevel, PermissionLevels); diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 614daf64..51035a6c 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -4,7 +4,7 @@ use std::{ }; use crate::util::impl_deref_wrapped; -use crate::util::{from_repeated_enum, from_repeated_message}; +use crate::util::{impl_from_repeated, impl_from_repeated_copy}; use protocol::metadata::Restriction as RestrictionMessage; @@ -68,8 +68,8 @@ impl From<&RestrictionMessage> for Restriction { } } -from_repeated_message!(RestrictionMessage, Restrictions); -from_repeated_enum!(RestrictionCatalogue, RestrictionCatalogues); +impl_from_repeated!(RestrictionMessage, Restrictions); +impl_from_repeated_copy!(RestrictionCatalogue, RestrictionCatalogues); struct StrChunks<'s>(&'s str, usize); diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 4981bb97..911e1d04 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -6,7 +6,7 @@ use std::{ use crate::{ restriction::Restrictions, - util::{impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_try_from_repeated}, }; use librespot_core::date::Date; @@ -37,4 +37,4 @@ impl TryFrom<&SalePeriodMessage> for SalePeriod { } } -try_from_repeated_message!(SalePeriodMessage, SalePeriods); +impl_try_from_repeated!(SalePeriodMessage, SalePeriods); diff --git a/metadata/src/track.rs b/metadata/src/track.rs index ab365924..7d416f14 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -17,7 +17,7 @@ use crate::{ external_id::ExternalIds, restriction::Restrictions, sale_period::SalePeriods, - util::{impl_deref_wrapped, try_from_repeated_message}, + util::{impl_deref_wrapped, impl_try_from_repeated}, Album, Metadata, RequestResult, }; @@ -141,4 +141,4 @@ impl TryFrom<&::Message> for Track { } } -try_from_repeated_message!(::Message, Tracks); +impl_try_from_repeated!(::Message, Tracks); diff --git a/metadata/src/util.rs b/metadata/src/util.rs index 2f3d8340..f6866340 100644 --- a/metadata/src/util.rs +++ b/metadata/src/util.rs @@ -1,4 +1,4 @@ -macro_rules! from_repeated_message { +macro_rules! impl_from_repeated { ($src:ty, $dst:ty) => { impl From<&[$src]> for $dst { fn from(src: &[$src]) -> Self { @@ -9,22 +9,22 @@ macro_rules! from_repeated_message { }; } -pub(crate) use from_repeated_message; +pub(crate) use impl_from_repeated; -macro_rules! from_repeated_enum { +macro_rules! impl_from_repeated_copy { ($src:ty, $dst:ty) => { impl From<&[$src]> for $dst { fn from(src: &[$src]) -> Self { - let result = src.iter().map(|x| <$src>::from(*x)).collect(); + let result = src.iter().copied().collect(); Self(result) } } }; } -pub(crate) use from_repeated_enum; +pub(crate) use impl_from_repeated_copy; -macro_rules! try_from_repeated_message { +macro_rules! impl_try_from_repeated { ($src:ty, $dst:ty) => { impl TryFrom<&[$src]> for $dst { type Error = librespot_core::Error; @@ -36,7 +36,7 @@ macro_rules! try_from_repeated_message { }; } -pub(crate) use try_from_repeated_message; +pub(crate) use impl_try_from_repeated; macro_rules! impl_deref_wrapped { ($wrapper:ty, $inner:ty) => { diff --git a/metadata/src/video.rs b/metadata/src/video.rs index 18d7073f..df634f23 100644 --- a/metadata/src/video.rs +++ b/metadata/src/video.rs @@ -3,7 +3,7 @@ use std::{ ops::{Deref, DerefMut}, }; -use crate::util::{from_repeated_message, impl_deref_wrapped}; +use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_core::FileId; @@ -15,4 +15,4 @@ pub struct VideoFiles(pub Vec); impl_deref_wrapped!(VideoFiles, Vec); -from_repeated_message!(VideoFileMessage, VideoFiles); +impl_from_repeated!(VideoFileMessage, VideoFiles); From 2a79af1f0ab50931906133dd5a24d88f4f8b9015 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 2 Aug 2022 21:42:38 +0200 Subject: [PATCH 234/561] Migrate to Rust 2021 --- Cargo.toml | 2 +- audio/Cargo.toml | 2 +- audio/src/range_set.rs | 2 +- connect/Cargo.toml | 2 +- connect/src/spirc.rs | 4 ++-- core/Cargo.toml | 2 +- core/src/config.rs | 2 +- core/src/file_id.rs | 4 ++-- core/src/lib.rs | 1 - core/src/spotify_id.rs | 8 ++++---- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- metadata/src/restriction.rs | 4 ++-- playback/Cargo.toml | 4 +++- playback/src/audio_backend/mod.rs | 2 +- playback/src/player.rs | 2 +- protocol/Cargo.toml | 2 +- protocol/src/lib.rs | 1 - rustfmt.toml | 2 +- 19 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e02c6990..7a423d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ description = "An open source client library for Spotify, with support for Spoti keywords = ["spotify"] repository = "https://github.com/librespot-org/librespot" readme = "README.md" -edition = "2018" +edition = "2021" [workspace] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index e6649b20..eaa84313 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies.librespot-core] path = "../core" diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index 9c4b0b87..ee9136f2 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -60,7 +60,7 @@ impl RangeSet { self.ranges[index] } - pub fn iter(&self) -> Iter { + pub fn iter(&self) -> Iter<'_, Range> { self.ranges.iter() } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 2caa9f33..108a3804 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies] form_urlencoded = "1.0" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 6adb340c..b8c60a85 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1443,7 +1443,7 @@ struct CommandSender<'a> { } impl<'a> CommandSender<'a> { - fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender { + fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender<'_> { let mut frame = protocol::spirc::Frame::new(); frame.set_version(1); frame.set_protocol_version(::std::convert::Into::into("2.0.0")); @@ -1455,7 +1455,7 @@ impl<'a> CommandSender<'a> { CommandSender { spirc, frame } } - fn recipient(mut self, recipient: &'a str) -> CommandSender { + fn recipient(mut self, recipient: &'a str) -> CommandSender<'_> { self.frame.mut_recipient().push(recipient.to_owned()); self } diff --git a/core/Cargo.toml b/core/Cargo.toml index d64590c2..c2988bb8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,7 +7,7 @@ build = "build.rs" description = "The core functionality provided by librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies.librespot-protocol] path = "../protocol" diff --git a/core/src/config.rs b/core/src/config.rs index 46f11fe8..7c5f75a1 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -109,7 +109,7 @@ impl From for &str { } impl fmt::Display for DeviceType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let str: &str = self.into(); f.write_str(str) } diff --git a/core/src/file_id.rs b/core/src/file_id.rs index 5422c428..1e84b489 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -21,13 +21,13 @@ impl FileId { } impl fmt::Debug for FileId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + 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 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_base16().unwrap_or_default()) } } diff --git a/core/src/lib.rs b/core/src/lib.rs index a0f180ca..d476d917 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,6 +1,5 @@ #[macro_use] extern crate log; -extern crate num_derive; use librespot_protocol as protocol; diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 3db010e9..227fa526 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -274,7 +274,7 @@ impl SpotifyId { } impl fmt::Debug for SpotifyId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SpotifyId") .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) .finish() @@ -282,7 +282,7 @@ impl fmt::Debug for SpotifyId { } impl fmt::Display for SpotifyId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) } } @@ -345,7 +345,7 @@ impl Deref for NamedSpotifyId { } impl fmt::Debug for NamedSpotifyId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("NamedSpotifyId") .field( &self @@ -358,7 +358,7 @@ impl fmt::Debug for NamedSpotifyId { } impl fmt::Display for NamedSpotifyId { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str( &self .inner_id diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index ebbfb809..c2274fbe 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies] aes = "0.8" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 0d86cd29..490b5227 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -6,7 +6,7 @@ authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies] async-trait = "0.1" diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 51035a6c..565010ff 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -74,11 +74,11 @@ impl_from_repeated_copy!(RestrictionCatalogue, RestrictionCatalogues); struct StrChunks<'s>(&'s str, usize); trait StrChunksExt { - fn chunks(&self, size: usize) -> StrChunks; + fn chunks(&self, size: usize) -> StrChunks<'_>; } impl StrChunksExt for str { - fn chunks(&self, size: usize) -> StrChunks { + fn chunks(&self, size: usize) -> StrChunks<'_> { StrChunks(self, size) } } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 39e87261..a8d43b68 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -6,14 +6,16 @@ authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies.librespot-audio] path = "../audio" version = "0.5.0-dev" + [dependencies.librespot-core] path = "../core" version = "0.5.0-dev" + [dependencies.librespot-metadata] path = "../metadata" version = "0.5.0-dev" diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 959bf17d..05822395 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -126,7 +126,7 @@ pub const BACKENDS: &[(&str, SinkBuilder)] = &[ #[cfg(feature = "alsa-backend")] (AlsaSink::NAME, mk_sink::), #[cfg(feature = "portaudio-backend")] - (PortAudioSink::NAME, mk_sink::), + (PortAudioSink::NAME, mk_sink::>), #[cfg(feature = "pulseaudio-backend")] (PulseAudioSink::NAME, mk_sink::), #[cfg(feature = "jackaudio-backend")] diff --git a/playback/src/player.rs b/playback/src/player.rs index 1ebf42d7..30d6d2a2 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2132,7 +2132,7 @@ impl Drop for PlayerInternal { } impl fmt::Debug for PlayerCommand { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { PlayerCommand::Load { track_id, diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index aa5f4496..36d00c2b 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -7,7 +7,7 @@ build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" license = "MIT" repository = "https://github.com/librespot-org/librespot" -edition = "2018" +edition = "2021" [dependencies] protobuf = "2" diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 94180d54..224043e7 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,4 +1,3 @@ -extern crate protobuf; // This file is parsed by build.rs // Each included module will be compiled from the matching .proto definition. diff --git a/rustfmt.toml b/rustfmt.toml index 32a9786f..3a26366d 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -edition = "2018" +edition = "2021" From cdf84925ad0bf3b872a13922a0e9ee9523e17f26 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 2 Aug 2022 23:06:02 +0200 Subject: [PATCH 235/561] Add `client-token` header to `spclient` requests - Also fix an overflow panic when a token cannot be parsed. - Getting tokens always requires the keymaster client ID; passing the actual client ID yields a HashCash challenge. --- core/src/config.rs | 2 +- core/src/spclient.rs | 10 ++++++++-- core/src/token.rs | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index 46f11fe8..3aaefdf0 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -2,7 +2,7 @@ use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; -const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +pub const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; #[derive(Clone, Debug)] pub struct SessionConfig { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c4f13656..2f98eb39 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -9,7 +9,7 @@ use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ client::ResponseFuture, - header::{ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE}, + header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE}, Body, HeaderMap, Method, Request, }; use protobuf::Message; @@ -19,6 +19,7 @@ use thiserror::Error; use crate::{ apresolve::SocketAddress, cdn_url::CdnUrl, + config::KEYMASTER_CLIENT_ID, error::ErrorKind, protocol::{ canvaz::EntityCanvazRequest, @@ -40,6 +41,9 @@ component! { pub type SpClientResult = Result; +#[allow(clippy::declare_interior_mutable_const)] +const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); + #[derive(Debug, Error)] pub enum SpClientError { #[error("missing attribute {0}")] @@ -116,7 +120,7 @@ impl SpClient { 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_id(KEYMASTER_CLIENT_ID.to_string()); client_data.set_client_version(version::SEMVER.to_string()); let connectivity_data = client_data.mut_connectivity_sdk_data(); @@ -287,6 +291,7 @@ impl SpClient { .token_provider() .get_token("playlist-read") .await?; + let client_token = self.client_token().await?; let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { @@ -296,6 +301,7 @@ impl SpClient { AUTHORIZATION, HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, ); + headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); last_response = self.session().http_client().request_body(request).await; diff --git a/core/src/token.rs b/core/src/token.rs index 02f94b60..af253ddc 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; use serde::Deserialize; use thiserror::Error; -use crate::Error; +use crate::{config::KEYMASTER_CLIENT_ID, Error}; component! { TokenProvider : TokenProviderInner { @@ -65,7 +65,7 @@ impl TokenProvider { // scopes must be comma-separated pub async fn get_token(&self, scopes: &str) -> Result { - let client_id = self.session().client_id(); + let client_id = KEYMASTER_CLIENT_ID; if client_id.is_empty() { return Err(Error::invalid_argument("Client ID cannot be empty")); } @@ -115,7 +115,7 @@ impl Token { } pub fn is_expired(&self) -> bool { - self.timestamp + (self.expires_in - Self::EXPIRY_THRESHOLD) < Instant::now() + self.timestamp + (self.expires_in.saturating_sub(Self::EXPIRY_THRESHOLD)) < Instant::now() } pub fn in_scope(&self, scope: &str) -> bool { From 176a47f10fa3f93f5d987a075d7546c5c42c07c7 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Wed, 3 Aug 2022 16:13:13 +0200 Subject: [PATCH 236/561] Return iterators instead of collected vecs - Change the return type of metadata convenience iter functions to actual iterators instead of allocated collections - The iterator item type is set to be a reference --- examples/playlist_tracks.rs | 2 +- metadata/src/album.rs | 9 ++------- metadata/src/artist.rs | 17 ++++++----------- metadata/src/playlist/list.rs | 9 ++------- 4 files changed, 11 insertions(+), 26 deletions(-) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 7404e755..c2cf419a 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -36,7 +36,7 @@ async fn main() { let plist = Playlist::get(&session, plist_uri).await.unwrap(); println!("{:?}", plist); for track_id in plist.tracks() { - let plist_track = Track::get(&session, track_id).await.unwrap(); + let plist_track = Track::get(&session, *track_id).await.unwrap(); println!("track: {} ", plist_track.name); } } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 85bee013..c2c439c2 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -67,13 +67,8 @@ pub struct Discs(pub Vec); impl_deref_wrapped!(Discs, Vec); impl Album { - pub fn tracks(&self) -> Tracks { - let result = self - .discs - .iter() - .flat_map(|disc| disc.tracks.deref().clone()) - .collect(); - Tracks(result) + pub fn tracks(&self) -> impl Iterator { + self.discs.iter().flat_map(|disc| disc.tracks.iter()) } } diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 7ddf07a1..db297b59 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -140,14 +140,14 @@ impl Artist { /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn albums_current(&self) -> Albums { + pub fn albums_current(&self) -> impl Iterator { self.albums.current_releases() } /// Get the full list of singles, not containing duplicate variants of the same singles. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn singles_current(&self) -> Albums { + pub fn singles_current(&self) -> impl Iterator { self.singles.current_releases() } @@ -155,14 +155,14 @@ impl Artist { /// compilations. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn compilations_current(&self) -> Albums { + pub fn compilations_current(&self) -> impl Iterator { self.compilations.current_releases() } /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn appears_on_albums_current(&self) -> Albums { + pub fn appears_on_albums_current(&self) -> impl Iterator { self.appears_on_albums.current_releases() } } @@ -245,13 +245,8 @@ impl AlbumGroups { /// Get the contained albums. This will only use the latest release / variant of an album if /// multiple variants are available. This should be used if multiple variants of the same album /// are not explicitely desired. - pub fn current_releases(&self) -> Albums { - let albums = self - .iter() - .filter_map(|agrp| agrp.first()) - .cloned() - .collect(); - Albums(albums) + pub fn current_releases(&self) -> impl Iterator { + self.iter().filter_map(|agrp| agrp.first()) } } diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 223f8d30..300c0c09 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -105,13 +105,8 @@ impl Playlist { Self::parse(&msg, playlist_id) } - pub fn tracks(&self) -> Vec { - let tracks = self - .contents - .items - .iter() - .map(|item| item.id) - .collect::>(); + pub fn tracks(&self) -> impl ExactSizeIterator { + let tracks = self.contents.items.iter().map(|item| &item.id); let length = tracks.len(); let expected_length = self.length as usize; From 131310b920bda545a5cb28789e96d1f83524212f Mon Sep 17 00:00:00 2001 From: dnlmlr <34707428+dnlmlr@users.noreply.github.com> Date: Wed, 3 Aug 2022 20:01:03 +0200 Subject: [PATCH 237/561] Fix panic in `ApResolver::resolve` (#1038) - Fixed resolve function panicking when resolving endpoint type with no AP in the list - Fixed fallback APs not being applied when only some of the AP types were missing - Switch container type from `Vec` to `VecDeque` for the `AccessPoints` - Remove the note about fallback AP being used even if the port is not matching the configured `ap_port` --- core/src/apresolve.rs | 97 +++++++++++++++++++++++++++---------------- src/main.rs | 2 +- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 72b089dd..c02ec1c8 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,3 +1,5 @@ +use std::collections::VecDeque; + use hyper::{Body, Method, Request}; use serde::Deserialize; @@ -7,23 +9,23 @@ pub type SocketAddress = (String, u16); #[derive(Default)] pub struct AccessPoints { - accesspoint: Vec, - dealer: Vec, - spclient: Vec, + accesspoint: VecDeque, + dealer: VecDeque, + spclient: VecDeque, } -#[derive(Deserialize)] +#[derive(Deserialize, Default)] pub 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 { +impl ApResolveData { + // 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. + fn fallback() -> Self { Self { accesspoint: vec![String::from("ap.spotify.com:443")], dealer: vec![String::from("dealer.spotify.com:443")], @@ -32,6 +34,12 @@ impl Default for ApResolveData { } } +impl AccessPoints { + fn is_any_empty(&self) -> bool { + self.accesspoint.is_empty() || self.dealer.is_empty() || self.spclient.is_empty() + } +} + component! { ApResolver : ApResolverInner { data: AccessPoints = AccessPoints::default(), @@ -49,27 +57,34 @@ impl ApResolver { } } - fn process_data(&self, data: Vec) -> Vec { + fn process_ap_strings(&self, data: Vec) -> VecDeque { + let filter_port = self.port_config(); data.into_iter() .filter_map(|ap| { let mut split = ap.rsplitn(2, ':'); let port = split.next()?; - let host = split.next()?.to_owned(); let port: u16 = port.parse().ok()?; - if let Some(p) = self.port_config() { - if p != port { - return None; - } + let host = split.next()?.to_owned(); + match filter_port { + Some(filter_port) if filter_port != port => None, + _ => Some((host, port)), } - Some((host, port)) }) .collect() } + fn parse_resolve_to_access_points(&self, resolve: ApResolveData) -> AccessPoints { + AccessPoints { + accesspoint: self.process_ap_strings(resolve.accesspoint), + dealer: self.process_ap_strings(resolve.dealer), + spclient: self.process_ap_strings(resolve.spclient), + } + } + 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") + .uri("https://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") .body(Body::empty())?; let body = self.session().http_client().request_body(req).await?; @@ -82,30 +97,33 @@ impl ApResolver { 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() - } + let (data, error) = match result { + Ok(data) => (data, None), + Err(e) => (ApResolveData::default(), Some(e)), }; - inner.data.accesspoint = self.process_data(data.accesspoint); - inner.data.dealer = self.process_data(data.dealer); - inner.data.spclient = self.process_data(data.spclient); + inner.data = self.parse_resolve_to_access_points(data); + + if inner.data.is_any_empty() { + warn!("Failed to resolve all access points, using fallbacks"); + if let Some(error) = error { + warn!("Resolve access points error: {}", error); + } + + let fallback = self.parse_resolve_to_access_points(ApResolveData::fallback()); + inner.data.accesspoint.extend(fallback.accesspoint); + inner.data.dealer.extend(fallback.dealer); + inner.data.spclient.extend(fallback.spclient); + } }) } - fn is_empty(&self) -> bool { - self.lock(|inner| { - inner.data.accesspoint.is_empty() - || inner.data.dealer.is_empty() - || inner.data.spclient.is_empty() - }) + fn is_any_empty(&self) -> bool { + self.lock(|inner| inner.data.is_any_empty()) } pub async fn resolve(&self, endpoint: &str) -> Result { - if self.is_empty() { + if self.is_any_empty() { self.apresolve().await; } @@ -114,9 +132,9 @@ impl ApResolver { // 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), + "accesspoint" => inner.data.accesspoint.pop_front(), + "dealer" => inner.data.dealer.pop_front(), + "spclient" => inner.data.spclient.pop_front(), _ => { return Err(Error::unimplemented(format!( "No implementation to resolve access point {}", @@ -125,6 +143,13 @@ impl ApResolver { } }; + let access_point = access_point.ok_or_else(|| { + Error::unavailable(format!( + "No access point available for endpoint {}", + endpoint + )) + })?; + Ok(access_point) }) } diff --git a/src/main.rs b/src/main.rs index 0aaa613b..e1af4c10 100644 --- a/src/main.rs +++ b/src/main.rs @@ -561,7 +561,7 @@ fn get_setup() -> Setup { .optopt( AP_PORT_SHORT, AP_PORT, - "Connect to an AP with a specified port 1 - 65535. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", + "Connect to an AP with a specified port 1 - 65535. Available ports are usually 80, 443 and 4070.", "PORT", ); From 70eb3f9d729e9eef072316697e124566eb5e7f7e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 3 Aug 2022 21:27:07 +0200 Subject: [PATCH 238/561] Add more HTTP endpoints and migrate `playlist` --- core/src/spclient.rs | 83 ++++++++++++++++++++++++++++++++++- core/src/spotify_id.rs | 2 +- metadata/src/playlist/list.rs | 56 +---------------------- 3 files changed, 85 insertions(+), 56 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 2f98eb39..479c86e0 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -386,7 +386,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/v2/track/{}", track_id.to_base62()?); self.request_as_json(&Method::GET, &endpoint, None, None) .await @@ -407,6 +407,87 @@ impl SpClient { .await } + pub async fn get_playlist(&self, playlist_id: SpotifyId) -> SpClientResult { + let endpoint = format!("/playlist/v2/playlist/{}", playlist_id); + + self.request(&Method::GET, &endpoint, None, None).await + } + + pub async fn get_user_profile( + &self, + username: String, + playlist_limit: Option, + artist_limit: Option, + ) -> SpClientResult { + let mut endpoint = format!("/user-profile-view/v3/profile/{}", username); + + if playlist_limit.is_some() || artist_limit.is_some() { + let _ = write!(endpoint, "?"); + + if let Some(limit) = playlist_limit { + let _ = write!(endpoint, "playlist_limit={}", limit); + if artist_limit.is_some() { + let _ = write!(endpoint, "&"); + } + } + + if let Some(limit) = artist_limit { + let _ = write!(endpoint, "artist_limit={}", limit); + } + } + + self.request_as_json(&Method::GET, &endpoint, None, None) + .await + } + + pub async fn get_user_followers(&self, username: String) -> SpClientResult { + let endpoint = format!("/user-profile-view/v3/profile/{}/followers", username); + + self.request_as_json(&Method::GET, &endpoint, None, None) + .await + } + + pub async fn get_user_following(&self, username: String) -> SpClientResult { + let endpoint = format!("/user-profile-view/v3/profile/{}/following", username); + + self.request_as_json(&Method::GET, &endpoint, None, None) + .await + } + + pub async fn get_radio_for_track(&self, track_id: SpotifyId) -> SpClientResult { + let endpoint = format!( + "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", + track_id.to_uri()? + ); + + self.request_as_json(&Method::GET, &endpoint, None, None) + .await + } + + pub async fn get_apollo_station( + &self, + context: SpotifyId, + count: u32, + previous_tracks: Vec, + autoplay: bool, + ) -> SpClientResult { + let previous_track_str = previous_tracks + .iter() + .map(|track| track.to_uri()) + .collect::, _>>()? + .join(","); + let endpoint = format!( + "/radio-apollo/v3/stations/{}?count={}&prev_tracks={}&autoplay={}", + context.to_uri()?, + count, + previous_track_str, + autoplay, + ); + + 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"; diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 227fa526..66e3ac36 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -163,7 +163,7 @@ impl SpotifyId { /// 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:playlist:{id}`. /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn from_uri(src: &str) -> SpotifyIdResult { diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 300c0c09..414e1fda 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -4,10 +4,8 @@ use std::{ ops::{Deref, DerefMut}, }; -use protobuf::Message; - use crate::{ - request::{MercuryRequest, RequestResult}, + request::RequestResult, util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated}, Metadata, }; @@ -55,11 +53,6 @@ pub struct Playlists(pub Vec); impl_deref_wrapped!(Playlists, Vec); -#[derive(Debug, Clone)] -pub struct RootPlaylist(pub SelectedListContent); - -impl_deref_wrapped!(RootPlaylist, SelectedListContent); - #[derive(Debug, Clone)] pub struct SelectedListContent { pub revision: Vec, @@ -80,31 +73,6 @@ pub struct SelectedListContent { } 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) -> impl ExactSizeIterator { let tracks = self.contents.items.iter().map(|item| &item.id); @@ -125,15 +93,12 @@ impl Playlist { } } -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 + session.spclient().get_playlist(playlist_id).await } fn parse(msg: &Self::Message, id: SpotifyId) -> Result { @@ -161,23 +126,6 @@ impl Metadata for Playlist { } } -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 = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { From 80f0d3c59b2e2473b767eabb3dbc13e77c8cda16 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 3 Aug 2022 22:26:52 +0200 Subject: [PATCH 239/561] Pass by reference --- core/src/cdn_url.rs | 2 +- core/src/spclient.rs | 63 ++++++++++++++--------------- core/src/spotify_id.rs | 4 +- metadata/src/album.rs | 4 +- metadata/src/artist.rs | 4 +- metadata/src/episode.rs | 6 +-- metadata/src/lib.rs | 6 +-- metadata/src/playlist/annotation.rs | 8 ++-- metadata/src/playlist/list.rs | 6 +-- metadata/src/show.rs | 4 +- metadata/src/track.rs | 6 +-- 11 files changed, 55 insertions(+), 58 deletions(-) diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 9df43ea9..e7016143 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -67,7 +67,7 @@ impl CdnUrl { pub async fn resolve_audio(&self, session: &Session) -> Result { let file_id = self.file_id; - let response = session.spclient().get_audio_storage(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)?; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 479c86e0..e65e0bff 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -235,7 +235,7 @@ impl SpClient { HeaderValue::from_static("application/x-protobuf"), ); - self.request(method, endpoint, Some(headers), Some(body)) + self.request(method, endpoint, Some(headers), Some(&body)) .await } @@ -244,7 +244,7 @@ impl SpClient { method: &Method, endpoint: &str, headers: Option, - body: Option, + body: Option<&str>, ) -> SpClientResult { let mut headers = headers.unwrap_or_else(HeaderMap::new); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); @@ -257,7 +257,7 @@ impl SpClient { method: &Method, endpoint: &str, headers: Option, - body: Option, + body: Option<&str>, ) -> SpClientResult { let mut tries: usize = 0; let mut last_response; @@ -283,7 +283,7 @@ impl SpClient { let mut request = Request::builder() .method(method) .uri(url) - .body(Body::from(body.clone()))?; + .body(Body::from(body.to_owned()))?; // Reconnection logic: keep getting (cached) tokens because they might have expired. let token = self @@ -348,44 +348,44 @@ impl SpClient { pub async fn put_connect_state( &self, - connection_id: String, - state: PutStateRequest, + connection_id: &str, + state: &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.request_with_protobuf(&Method::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 { + pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult { let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); self.request(&Method::GET, &endpoint, None, None).await } - pub async fn get_track_metadata(&self, track_id: SpotifyId) -> SpClientResult { + 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 { + 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 { + 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 { + 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 { + pub async fn get_show_metadata(&self, show_id: &SpotifyId) -> SpClientResult { self.get_metadata("show", show_id).await } - pub async fn get_lyrics(&self, track_id: SpotifyId) -> SpClientResult { + pub async fn get_lyrics(&self, track_id: &SpotifyId) -> SpClientResult { let endpoint = format!("/color-lyrics/v2/track/{}", track_id.to_base62()?); self.request_as_json(&Method::GET, &endpoint, None, None) @@ -394,8 +394,8 @@ impl SpClient { pub async fn get_lyrics_for_image( &self, - track_id: SpotifyId, - image_id: FileId, + track_id: &SpotifyId, + image_id: &FileId, ) -> SpClientResult { let endpoint = format!( "/color-lyrics/v2/track/{}/image/spotify:image:{}", @@ -407,7 +407,7 @@ impl SpClient { .await } - pub async fn get_playlist(&self, playlist_id: SpotifyId) -> SpClientResult { + pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientResult { let endpoint = format!("/playlist/v2/playlist/{}", playlist_id); self.request(&Method::GET, &endpoint, None, None).await @@ -415,7 +415,7 @@ impl SpClient { pub async fn get_user_profile( &self, - username: String, + username: &str, playlist_limit: Option, artist_limit: Option, ) -> SpClientResult { @@ -440,14 +440,14 @@ impl SpClient { .await } - pub async fn get_user_followers(&self, username: String) -> SpClientResult { + pub async fn get_user_followers(&self, username: &str) -> SpClientResult { let endpoint = format!("/user-profile-view/v3/profile/{}/followers", username); self.request_as_json(&Method::GET, &endpoint, None, None) .await } - pub async fn get_user_following(&self, username: String) -> SpClientResult { + pub async fn get_user_following(&self, username: &str) -> SpClientResult { let endpoint = format!("/user-profile-view/v3/profile/{}/following", username); self.request_as_json(&Method::GET, &endpoint, None, None) @@ -466,9 +466,9 @@ impl SpClient { pub async fn get_apollo_station( &self, - context: SpotifyId, + context_uri: &str, count: u32, - previous_tracks: Vec, + previous_tracks: Vec<&SpotifyId>, autoplay: bool, ) -> SpClientResult { let previous_track_str = previous_tracks @@ -478,10 +478,7 @@ impl SpClient { .join(","); let endpoint = format!( "/radio-apollo/v3/stations/{}?count={}&prev_tracks={}&autoplay={}", - context.to_uri()?, - count, - previous_track_str, - autoplay, + context_uri, count, previous_track_str, autoplay, ); self.request_as_json(&Method::GET, &endpoint, None, None) @@ -501,7 +498,7 @@ impl SpClient { .await } - pub async fn get_audio_storage(&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()? @@ -530,7 +527,7 @@ impl SpClient { Ok(stream) } - pub async fn request_url(&self, url: String) -> SpClientResult { + pub async fn request_url(&self, url: &str) -> SpClientResult { let request = Request::builder() .method(&Method::GET) .uri(url) @@ -554,11 +551,11 @@ impl SpClient { }; let _ = write!(url, "{}cid={}", separator, self.session().client_id()); - self.request_url(url).await + self.request_url(&url).await } // The first 128 kB of a track, unencrypted - pub async fn get_head_file(&self, file_id: FileId) -> SpClientResult { + pub async fn get_head_file(&self, file_id: &FileId) -> SpClientResult { let attribute = "head-files-url"; let template = self .session() @@ -567,10 +564,10 @@ impl SpClient { let url = template.replace("{file_id}", &file_id.to_base16()?); - self.request_url(url).await + self.request_url(&url).await } - pub async fn get_image(&self, image_id: FileId) -> SpClientResult { + pub async fn get_image(&self, image_id: &FileId) -> SpClientResult { let attribute = "image-url"; let template = self .session() @@ -578,6 +575,6 @@ impl SpClient { .ok_or_else(|| SpClientError::Attribute(attribute.to_string()))?; let url = template.replace("{file_id}", &image_id.to_base16()?); - self.request_url(url).await + self.request_url(&url).await } } diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 66e3ac36..f1c1f1b8 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -329,10 +329,10 @@ impl NamedSpotifyId { Ok(dst) } - pub fn from_spotify_id(id: SpotifyId, username: String) -> Self { + pub fn from_spotify_id(id: SpotifyId, username: &str) -> Self { Self { inner_id: id, - username, + username: username.to_owned(), } } } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index c2c439c2..45db39ea 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -76,11 +76,11 @@ impl Album { impl Metadata for Album { type Message = protocol::metadata::Album; - async fn request(session: &Session, album_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, album_id: &SpotifyId) -> RequestResult { 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) } } diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index db297b59..1a7e1862 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -171,11 +171,11 @@ impl Artist { impl Metadata for Artist { type Message = protocol::metadata::Artist; - async fn request(session: &Session, artist_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, artist_id: &SpotifyId) -> RequestResult { 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) } } diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index e3a5c7a3..e65a1045 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -60,7 +60,7 @@ impl_deref_wrapped!(Episodes, Vec); #[async_trait] impl InnerAudioItem for Episode { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { - let episode = Self::get(session, id).await?; + let episode = Self::get(session, &id).await?; let availability = Self::available_for_user( &session.user_data(), &episode.availability, @@ -84,11 +84,11 @@ impl InnerAudioItem for Episode { impl Metadata for Episode { type Message = protocol::metadata::Episode; - async fn request(session: &Session, episode_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, episode_id: &SpotifyId) -> RequestResult { 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) } } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 577af387..ef8443db 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -42,15 +42,15 @@ pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; // Request a protobuf - async fn request(session: &Session, id: SpotifyId) -> RequestResult; + 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 fd8863cf..9cb7f144 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -27,12 +27,12 @@ pub struct PlaylistAnnotation { impl Metadata for PlaylistAnnotation { type Message = protocol::playlist_annotate3::PlaylistAnnotation; - async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult { + 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 { + 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? @@ -47,7 +47,7 @@ impl PlaylistAnnotation { async fn request_for_user( session: &Session, username: &str, - playlist_id: SpotifyId, + playlist_id: &SpotifyId, ) -> RequestResult { let uri = format!( "hm://playlist-annotate/v1/annotation/user/{}/playlist/{}", @@ -61,7 +61,7 @@ impl PlaylistAnnotation { async fn get_for_user( session: &Session, username: &str, - playlist_id: SpotifyId, + playlist_id: &SpotifyId, ) -> Result { let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 414e1fda..c813a14d 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -97,14 +97,14 @@ impl Playlist { impl Metadata for Playlist { type Message = protocol::playlist4_external::SelectedListContent; - async fn request(session: &Session, playlist_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult { session.spclient().get_playlist(playlist_id).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); + let id = NamedSpotifyId::from_spotify_id(*id, &playlist.owner_username); Ok(Self { id, diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 19e910d8..0d3acef8 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -39,11 +39,11 @@ pub struct Show { impl Metadata for Show { type Message = protocol::metadata::Show; - async fn request(session: &Session, show_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, show_id: &SpotifyId) -> RequestResult { 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) } } diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 7d416f14..f4855d8a 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -61,7 +61,7 @@ impl_deref_wrapped!(Tracks, Vec); #[async_trait] impl InnerAudioItem for Track { async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult { - let track = Self::get(session, id).await?; + let track = Self::get(session, &id).await?; let alternatives = { if track.alternatives.is_empty() { None @@ -98,11 +98,11 @@ impl InnerAudioItem for Track { impl Metadata for Track { type Message = protocol::metadata::Track; - async fn request(session: &Session, track_id: SpotifyId) -> RequestResult { + async fn request(session: &Session, track_id: &SpotifyId) -> RequestResult { 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) } } From a60d63637daa0976ccc90928751b37191e6a0b60 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 3 Aug 2022 22:50:21 +0200 Subject: [PATCH 240/561] Fix example --- examples/playlist_tracks.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index c2cf419a..ddf456ac 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -33,10 +33,10 @@ async fn main() { exit(1); } - let plist = Playlist::get(&session, plist_uri).await.unwrap(); + let plist = Playlist::get(&session, &plist_uri).await.unwrap(); println!("{:?}", plist); for track_id in plist.tracks() { - let plist_track = Track::get(&session, *track_id).await.unwrap(); + let plist_track = Track::get(&session, track_id).await.unwrap(); println!("track: {} ", plist_track.name); } } From d88a20929ff66be58bca3634a511d658582960d2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 3 Aug 2022 23:21:38 +0200 Subject: [PATCH 241/561] Playlist ID should be Base62 encoded --- core/src/spclient.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index e65e0bff..0b2f08a8 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -408,7 +408,7 @@ impl SpClient { } pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientResult { - let endpoint = format!("/playlist/v2/playlist/{}", playlist_id); + let endpoint = format!("/playlist/v2/playlist/{:?}", playlist_id.to_base62()); self.request(&Method::GET, &endpoint, None, None).await } @@ -454,7 +454,7 @@ impl SpClient { .await } - pub async fn get_radio_for_track(&self, track_id: SpotifyId) -> SpClientResult { + pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult { let endpoint = format!( "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", track_id.to_uri()? From f55bdbb962eeb1fff010919fbb47b7304a5906c5 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 4 Aug 2022 00:09:30 +0200 Subject: [PATCH 242/561] Fix `SpClient::get_playlist` endpoint generation --- core/src/spclient.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 0b2f08a8..1c8ea916 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -408,7 +408,7 @@ impl SpClient { } pub async fn get_playlist(&self, playlist_id: &SpotifyId) -> SpClientResult { - let endpoint = format!("/playlist/v2/playlist/{:?}", playlist_id.to_base62()); + let endpoint = format!("/playlist/v2/playlist/{}", playlist_id.to_base62()?); self.request(&Method::GET, &endpoint, None, None).await } From b588d9fd0701a80ad318d79dcb14f77458545174 Mon Sep 17 00:00:00 2001 From: Daniel M Date: Thu, 4 Aug 2022 18:37:32 +0200 Subject: [PATCH 243/561] Keep using the same hyper client - Keep using the same hyper client instead of building a new one for each request - This allows the client to reuse connections and improves the performance of multiple requests by almost 2x. - The playlist_tracks example takes 38 secs before and 20 secs after the change to enumerate a 180 track playlist - To avoid carrying the hyper Client generics through the whole project, `ProxyConnector` is always used as the Connector, but disabled when not using a proxy. - The client creation is done lazily to keep the `HttpClient::new` without a `Result` return type --- core/src/http_client.rs | 55 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index ed7eac6d..903894ad 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -2,7 +2,7 @@ use std::env::consts::OS; use bytes::Bytes; use futures_util::{future::IntoStream, FutureExt}; -use http::header::HeaderValue; +use http::{header::HeaderValue, Uri}; use hyper::{ client::{HttpConnector, ResponseFuture}, header::USER_AGENT, @@ -10,6 +10,7 @@ use hyper::{ }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use once_cell::sync::OnceCell; use thiserror::Error; use url::Url; @@ -70,15 +71,17 @@ impl From for Error { } } +type HyperClient = Client>, Body>; + #[derive(Clone)] pub struct HttpClient { user_agent: HeaderValue, - proxy: Option, - https_connector: HttpsConnector, + proxy_url: Option, + hyper_client: OnceCell, } impl HttpClient { - pub fn new(proxy: Option<&Url>) -> Self { + pub fn new(proxy_url: Option<&Url>) -> Self { let spotify_version = match OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), _ => SPOTIFY_VERSION.to_string(), @@ -102,6 +105,14 @@ impl HttpClient { HeaderValue::from_static(FALLBACK_USER_AGENT) }); + Self { + user_agent, + proxy_url: proxy_url.cloned(), + hyper_client: OnceCell::new(), + } + } + + fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process let https_connector = HttpsConnectorBuilder::new() .with_native_roots() @@ -110,11 +121,23 @@ impl HttpClient { .enable_http2() .build(); - Self { - user_agent, - proxy: proxy.cloned(), - https_connector, - } + // When not using a proxy a dummy proxy is configured that will not intercept any traffic. + // This prevents needing to carry the Client Connector generics through the whole project + let proxy = match &proxy_url { + Some(proxy_url) => Proxy::new(Intercept::All, proxy_url.to_string().parse()?), + None => Proxy::new(Intercept::None, Uri::from_static("0.0.0.0")), + }; + let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?; + + let client = Client::builder() + .http2_adaptive_window(true) + .build(proxy_connector); + Ok(client) + } + + fn hyper_client(&self) -> Result<&HyperClient, Error> { + self.hyper_client + .get_or_try_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref())) } pub async fn request(&self, req: Request) -> Result, Error> { @@ -146,19 +169,7 @@ impl HttpClient { 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(self.https_connector.clone(), proxy)?; - - Client::builder().build(proxy_connector).request(req) - } else { - Client::builder() - .http2_adaptive_window(true) - .build(self.https_connector.clone()) - .request(req) - }; - + let request = self.hyper_client()?.request(req); Ok(request) } } From 411e95a7f8edc8b8e27b89fc815f8250d109b097 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 5 Aug 2022 12:09:41 -0500 Subject: [PATCH 244/561] Update raspotify description --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 793ade7e..acd78d63 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ This is a non exhaustive list of projects that either use or have modified libre - [librespot-golang](https://github.com/librespot-org/librespot-golang) - A golang port of librespot. - [plugin.audio.spotify](https://github.com/marcelveldt/plugin.audio.spotify) - A Kodi plugin for Spotify. -- [raspotify](https://github.com/dtcooper/raspotify) - Spotify Connect client for the Raspberry Pi that Just Worksâ„¢ +- [raspotify](https://github.com/dtcooper/raspotify) - A Spotify Connect client that mostly Just Worksâ„¢ - [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon. - [rpi-audio-receiver](https://github.com/nicokaiser/rpi-audio-receiver) - easy Raspbian install scripts for Spotifyd, Bluetooth, Shairport and other audio receivers - [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No playback From a08efbc2f66c2a3a67dcae9e53982aec7d6bd1f7 Mon Sep 17 00:00:00 2001 From: Felix Storm Date: Fri, 12 Aug 2022 17:42:03 +0200 Subject: [PATCH 245/561] fix contrib/docker-build-pi-armv6hf.sh --- contrib/docker-build-pi-armv6hf.sh | 45 +++++++----------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/contrib/docker-build-pi-armv6hf.sh b/contrib/docker-build-pi-armv6hf.sh index 9cc52a98..676b84c5 100755 --- a/contrib/docker-build-pi-armv6hf.sh +++ b/contrib/docker-build-pi-armv6hf.sh @@ -1,42 +1,17 @@ #!/usr/bin/env bash -# Snipped and tucked from https://github.com/plietar/librespot/pull/202/commits/21549641d39399cbaec0bc92b36c9951d1b87b90 -# and further inputs from https://github.com/kingosticks/librespot/commit/c55dd20bd6c7e44dd75ff33185cf50b2d3bd79c3 +# largerly inspired by https://github.com/Spotifyd/spotifyd/blob/993336f7/.github/workflows/cd.yml#L109 set -eux -# Get alsa lib and headers -ALSA_VER="1.0.25-4" -DEPS=( \ - "http://mirrordirector.raspbian.org/raspbian/pool/main/a/alsa-lib/libasound2_${ALSA_VER}_armhf.deb" \ - "http://mirrordirector.raspbian.org/raspbian/pool/main/a/alsa-lib/libasound2-dev_${ALSA_VER}_armhf.deb" \ + +# See https://github.com/raspberrypi/tools/commit/5caa7046 +# Since this commit is not (yet) contained in what is downloaded in Dockerfile, we use the target of the symlink directly +PI1_TOOLS_DIR="/pi-tools/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf" + +PI1_LIB_DIRS=( + "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/lib" + "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/usr/lib" ) +export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[@]/#/-L}" -# Collect Paths -SYSROOT="/pi-tools/arm-bcm2708/arm-bcm2708hardfp-linux-gnueabi/arm-bcm2708hardfp-linux-gnueabi/sysroot" -GCC="/pi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin" -GCC_SYSROOT="$GCC/gcc-sysroot" - - -export PATH=/pi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin/:$PATH - -# Link the compiler -export TARGET_CC="$GCC/arm-linux-gnueabihf-gcc" - -# Create wrapper around gcc to point to rpi sysroot -echo -e '#!/bin/bash' "\n$TARGET_CC --sysroot $SYSROOT \"\$@\"" > $GCC_SYSROOT -chmod +x $GCC_SYSROOT - -# Add extra target dependencies to our rpi sysroot -for path in "${DEPS[@]}"; do - curl -OL $path - dpkg -x $(basename $path) $SYSROOT -done - -# i don't why this is neccessary -# ln -s ld-linux.so.3 $SYSROOT/lib/ld-linux-armhf.so.3 - -# point cargo to use gcc wrapper as linker -echo -e '[target.arm-unknown-linux-gnueabihf]\nlinker = "gcc-sysroot"' > /.cargo/config - -# Build cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" From 9d80521e09bf2daaf7f34d2d4c3c6f9b9dbd84b6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 22 Aug 2022 22:38:19 +0200 Subject: [PATCH 246/561] Fix warning and clippy lints --- core/src/spotify_id.rs | 4 ++-- playback/src/config.rs | 4 ++-- playback/src/decoder/symphonia_decoder.rs | 2 +- playback/src/player.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index f1c1f1b8..3e913ce1 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -87,10 +87,10 @@ impl SpotifyId { /// Returns whether this `SpotifyId` is for a playable audio item, if known. pub fn is_playable(&self) -> bool { - return matches!( + matches!( self.item_type, SpotifyItemType::Episode | SpotifyItemType::Track - ); + ) } /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. diff --git a/playback/src/config.rs b/playback/src/config.rs index f1276adb..cdb455ce 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -73,7 +73,7 @@ impl AudioFormat { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NormalisationType { Album, Track, @@ -98,7 +98,7 @@ impl Default for NormalisationType { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum NormalisationMethod { Basic, Dynamic, diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 08c7b37c..b0d47acd 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -144,7 +144,7 @@ impl SymphoniaDecoder { (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), + None => ts as f64 * PAGES_PER_MS, }; seeked_to_ms as u32 } diff --git a/playback/src/player.rs b/playback/src/player.rs index 30d6d2a2..a7a51762 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -61,7 +61,7 @@ pub struct Player { play_request_id_generator: SeqGenerator, } -#[derive(PartialEq, Debug, Clone, Copy)] +#[derive(PartialEq, Eq, Debug, Clone, Copy)] pub enum SinkStatus { Running, Closed, From dbf71c0dffe94ae0ebf1b981f8bd8051dc5982f4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 25 Aug 2022 20:49:40 +0200 Subject: [PATCH 247/561] Move mostly harmless messages to debug level --- connect/src/spirc.rs | 2 +- core/src/session.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index b8c60a85..9105ffdf 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -475,7 +475,7 @@ impl SpircTask { }, cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd { if let Err(e) = self.handle_command(cmd) { - warn!("could not dispatch command: {}", e); + debug!("could not dispatch command: {}", e); } }, event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event { diff --git a/core/src/session.rs b/core/src/session.rs index 29c4cc5f..9c95282f 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -478,7 +478,7 @@ where }; if let Err(e) = session.dispatch(cmd, data) { - error!("could not dispatch command: {}", e); + debug!("could not dispatch command: {}", e); } } } From 42a665fb0d4117d5a176001b78209ab7863b5fee Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 25 Aug 2022 21:01:39 +0200 Subject: [PATCH 248/561] Revert most of cdf84925ad0bf3b872a13922a0e9ee9523e17f26 --- core/src/config.rs | 2 +- core/src/spclient.rs | 3 +-- core/src/token.rs | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index a873670e..7c5f75a1 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -2,7 +2,7 @@ use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; -pub const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; #[derive(Clone, Debug)] pub struct SessionConfig { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 1c8ea916..2d567f81 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -19,7 +19,6 @@ use thiserror::Error; use crate::{ apresolve::SocketAddress, cdn_url::CdnUrl, - config::KEYMASTER_CLIENT_ID, error::ErrorKind, protocol::{ canvaz::EntityCanvazRequest, @@ -120,7 +119,7 @@ impl SpClient { message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); let client_data = message.mut_client_data(); - client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string()); + 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(); diff --git a/core/src/token.rs b/core/src/token.rs index af253ddc..bea37074 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -13,7 +13,7 @@ use std::time::{Duration, Instant}; use serde::Deserialize; use thiserror::Error; -use crate::{config::KEYMASTER_CLIENT_ID, Error}; +use crate::Error; component! { TokenProvider : TokenProviderInner { @@ -65,7 +65,7 @@ impl TokenProvider { // scopes must be comma-separated pub async fn get_token(&self, scopes: &str) -> Result { - let client_id = KEYMASTER_CLIENT_ID; + let client_id = self.session().client_id(); if client_id.is_empty() { return Err(Error::invalid_argument("Client ID cannot be empty")); } From 7b19d4c1dd6f5a28b397afc83813c15c312d18c5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Aug 2022 01:51:00 +0200 Subject: [PATCH 249/561] Solve hash cash challenges (experimental) --- Cargo.lock | 2 + core/Cargo.toml | 3 +- core/src/config.rs | 2 +- core/src/http_client.rs | 11 ++- core/src/spclient.rs | 150 ++++++++++++++++++++++++++++++++-------- core/src/version.rs | 7 ++ 6 files changed, 138 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0c15d140..cd1a2a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1404,6 +1404,7 @@ dependencies = [ "form_urlencoded", "futures-core", "futures-util", + "hex", "hmac", "http", "httparse", @@ -3065,6 +3066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" dependencies = [ "getrandom", + "rand", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index c2988bb8..b68c6434 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,7 @@ 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"] } +hex = "0.4" hmac = "0.12" httparse = "1.7" http = "0.2" @@ -53,7 +54,7 @@ tokio-stream = "0.1" tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.7", features = ["codec"] } url = "2" -uuid = { version = "1", default-features = false, features = ["v4"] } +uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } [build-dependencies] rand = "0.8" diff --git a/core/src/config.rs b/core/src/config.rs index 7c5f75a1..27a77cec 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -2,7 +2,7 @@ use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; -const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; #[derive(Clone, Debug)] pub struct SessionConfig { diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 903894ad..f283ccf5 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -15,7 +15,7 @@ use thiserror::Error; use url::Url; use crate::{ - version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING}, + version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING}, Error, }; @@ -82,11 +82,6 @@ pub struct HttpClient { impl HttpClient { pub fn new(proxy_url: 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.2.1", @@ -97,7 +92,9 @@ impl HttpClient { let user_agent_str = &format!( "Spotify/{} {} ({})", - spotify_version, spotify_platform, VERSION_STRING + spotify_version(), + spotify_platform, + VERSION_STRING ); let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 2d567f81..a910d0ae 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -4,30 +4,37 @@ use std::{ time::{Duration, Instant}, }; +use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ client::ResponseFuture, - header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE}, + header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, Body, HeaderMap, Method, Request, }; -use protobuf::Message; +use protobuf::{Message, ProtobufEnum}; use rand::Rng; +use sha1::{Digest, Sha1}; use thiserror::Error; use crate::{ apresolve::SocketAddress, cdn_url::CdnUrl, + config::KEYMASTER_CLIENT_ID, error::ErrorKind, protocol::{ canvaz::EntityCanvazRequest, - clienttoken_http::{ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse}, + clienttoken_http::{ + ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType, + ClientTokenResponse, ClientTokenResponseType, + }, connect::PutStateRequest, extended_metadata::BatchedEntityRequest, }, token::Token, - version, Error, FileId, SpotifyId, + version::spotify_version, + Error, FileId, SpotifyId, }; component! { @@ -99,6 +106,42 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } + fn solve_hash_cash(ctx: &[u8], prefix: &[u8], length: i32, dst: &mut [u8]) { + let md = Sha1::digest(ctx); + + let mut counter: i64 = 0; + let target: i64 = BigEndian::read_i64(&md[12..20]); + + let suffix = loop { + let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); + + let mut hasher = Sha1::new(); + hasher.update(prefix); + hasher.update(&suffix); + let md = hasher.finalize(); + + if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) { + break suffix; + } + + counter += 1; + }; + + dst.copy_from_slice(&suffix); + } + + async fn client_token_request(&self, message: &dyn Message) -> Result { + let body = message.write_to_bytes()?; + + let request = Request::builder() + .method(&Method::POST) + .uri("https://clienttoken.spotify.com/v1/clienttoken") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .body(Body::from(body))?; + + self.session().http_client().request_body(request).await + } + pub async fn client_token(&self) -> Result { let client_token = self.lock(|inner| { if let Some(token) = &inner.client_token { @@ -119,8 +162,8 @@ impl SpClient { 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()); + client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string()); + client_data.set_client_version(spotify_version()); let connectivity_data = client_data.mut_connectivity_sdk_data(); connectivity_data.set_device_id(self.session().device_id().to_string()); @@ -169,40 +212,92 @@ impl SpClient { _ => { 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_system_release("5.15.0-46-generic".to_string()); + linux_data.set_system_version( + "#49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022".to_string(), + ); linux_data.set_hardware(std::env::consts::ARCH.to_string()); } } - let body = message.write_to_bytes()?; + let mut response = self.client_token_request(&message).await?; - 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 token_response = loop { + let message = ClientTokenResponse::parse_from_bytes(&response)?; + match ClientTokenResponseType::from_i32(message.response_type.value()) { + // depending on the platform, you're either given a token immediately + // or are presented a hash cash challenge to solve first + Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, + Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { + trace!("received a hash cash challenge"); - let response = self.session().http_client().request_body(request).await?; - let message = ClientTokenResponse::parse_from_bytes(&response)?; + let challenges = message.get_challenges().clone(); + let state = challenges.get_state(); + if let Some(challenge) = challenges.challenges.first() { + let hash_cash_challenge = challenge.get_evaluate_hashcash_parameters(); - let client_token = self.lock(|inner| { - let access_token = message.get_granted_token().get_token().to_owned(); + let ctx = vec![]; + let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { + Error::failed_precondition(format!( + "unable to decode Hashcash challenge: {}", + e + )) + })?; + let length = hash_cash_challenge.length; + let mut suffix = vec![0; 0x10]; + Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); + + // the suffix must be in uppercase + let suffix = hex::encode(suffix).to_uppercase(); + + let mut answer_message = ClientTokenRequest::new(); + answer_message.set_request_type( + ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, + ); + + let challenge_answers = answer_message.mut_challenge_answers(); + + let mut challenge_answer = ChallengeAnswer::new(); + challenge_answer.mut_hash_cash().suffix = suffix.to_string(); + challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; + + challenge_answers.state = state.to_string(); + challenge_answers.answers.push(challenge_answer); + + response = self.client_token_request(&answer_message).await?; + + // we should have been granted a token now + continue; + } else { + return Err(Error::failed_precondition("no challenges found")); + } + } + + Some(unknown) => { + return Err(Error::unimplemented(format!( + "unknown client token response type: {:?}", + unknown + ))) + } + None => return Err(Error::failed_precondition("no client token response type")), + } + }; + + let granted_token = token_response.get_granted_token(); + let access_token = granted_token.get_token().to_owned(); + + self.lock(|inner| { let client_token = Token { access_token: access_token.clone(), expires_in: Duration::from_secs( - message - .get_granted_token() + granted_token .get_refresh_after_seconds() .try_into() .unwrap_or(7200), ), token_type: "client-token".to_string(), - scopes: message - .get_granted_token() + scopes: granted_token .get_domains() .iter() .map(|d| d.domain.clone()) @@ -210,13 +305,12 @@ impl SpClient { timestamp: Instant::now(), }; - trace!("Got client token: {:?}", client_token); - inner.client_token = Some(client_token); - access_token }); - Ok(client_token) + trace!("Got client token: {:?}", client_token); + + Ok(access_token) } pub async fn request_with_protobuf( diff --git a/core/src/version.rs b/core/src/version.rs index aadc1356..f2369c91 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -24,3 +24,10 @@ 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)"; + +pub fn spotify_version() -> String { + match std::env::consts::OS { + "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), + _ => SPOTIFY_VERSION.to_string(), + } +} From 65e48864a541d703b1580b54cdaa2382170da8ee Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Aug 2022 02:15:13 +0200 Subject: [PATCH 250/561] Fix tracing of client token --- core/src/spclient.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index a910d0ae..5e57ae2b 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -162,9 +162,12 @@ impl SpClient { message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); let client_data = message.mut_client_data(); - client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string()); client_data.set_client_version(spotify_version()); + // using 9a8d2f0ce77a4e248bb71fefcb557637 on Android + // instead of the keymaster ID presents a hash cash challenge + client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string()); + let connectivity_data = client_data.mut_connectivity_sdk_data(); connectivity_data.set_device_id(self.session().device_id().to_string()); @@ -308,7 +311,7 @@ impl SpClient { inner.client_token = Some(client_token); }); - trace!("Got client token: {:?}", client_token); + trace!("Got client token: {:?}", granted_token); Ok(access_token) } From 111c7781d2a958b129a1bb152786cf690dec09fb Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Aug 2022 21:14:43 +0200 Subject: [PATCH 251/561] Use actual OS and kernel versions --- Cargo.lock | 24 ++++++++++++++++++++++++ core/Cargo.toml | 1 + core/src/http_client.rs | 21 ++++++++++++++------- core/src/spclient.rs | 24 +++++++++++++++--------- 4 files changed, 54 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cd1a2a0f..55d319d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1430,6 +1430,7 @@ dependencies = [ "serde_json", "sha1", "shannon", + "sysinfo", "thiserror", "time", "tokio", @@ -1718,6 +1719,15 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi", +] + [[package]] name = "num" version = "0.4.0" @@ -2746,6 +2756,20 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "sysinfo" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71eb43e528fdc239f08717ec2a378fdb017dddbc3412de15fff527554591a66c" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "winapi", +] + [[package]] name = "system-deps" version = "6.0.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index b68c6434..476f5216 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -47,6 +47,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = "0.10" shannon = "0.2" +sysinfo = { version = "0.25", default-features = false } thiserror = "1.0" time = "0.3" tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index f283ccf5..4e454f97 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -11,6 +11,7 @@ use hyper::{ use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use once_cell::sync::OnceCell; +use sysinfo::{System, SystemExt}; use thiserror::Error; use url::Url; @@ -82,18 +83,24 @@ pub struct HttpClient { impl HttpClient { pub fn new(proxy_url: Option<&Url>) -> Self { - let spotify_platform = match OS { - "android" => "Android/31", - "ios" => "iOS/15.2.1", - "macos" => "OSX/0", - "windows" => "Win32/0", - _ => "Linux/0", + let zero_str = String::from("0"); + let os_version = System::new() + .os_version() + .unwrap_or_else(|| zero_str.clone()); + + let (spotify_platform, os_version) = match OS { + "android" => ("Android", os_version), + "ios" => ("iOS", os_version), + "macos" => ("OSX", zero_str), + "windows" => ("Win32", zero_str), + _ => ("Linux", zero_str), }; let user_agent_str = &format!( - "Spotify/{} {} ({})", + "Spotify/{} {}/{} ({})", spotify_version(), spotify_platform, + os_version, VERSION_STRING ); diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 5e57ae2b..6aa331e8 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -16,6 +16,7 @@ use hyper::{ use protobuf::{Message, ProtobufEnum}; use rand::Rng; use sha1::{Digest, Sha1}; +use sysinfo::{System, SystemExt}; use thiserror::Error; use crate::{ @@ -173,8 +174,15 @@ impl SpClient { let platform_data = connectivity_data.mut_platform_specific_data(); + let sys = System::new(); + let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); + let kernel_version = sys.kernel_version().unwrap_or_else(|| String::from("0")); + match std::env::consts::OS { "windows" => { + let os_version = os_version.parse::().unwrap_or(10.) as i32; + let kernel_version = kernel_version.parse::().unwrap_or(21370); + let (pe, image_file) = match std::env::consts::ARCH { "arm" => (448, 452), "aarch64" => (43620, 452), @@ -183,8 +191,8 @@ impl SpClient { }; let windows_data = platform_data.mut_desktop_windows(); - windows_data.set_os_version(10); - windows_data.set_os_build(21370); + windows_data.set_os_version(os_version); + windows_data.set_os_build(kernel_version); windows_data.set_platform_id(2); windows_data.set_unknown_value_6(9); windows_data.set_image_file_machine(image_file); @@ -196,11 +204,11 @@ impl SpClient { 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()); + ios_data.set_system_version(os_version); } "android" => { let android_data = platform_data.mut_android(); - android_data.set_android_version("12.0.0_r26".to_string()); + android_data.set_android_version(os_version); android_data.set_api_version(31); android_data.set_device_name("Pixel".to_owned()); android_data.set_model_str("GF5KQ".to_owned()); @@ -208,17 +216,15 @@ impl SpClient { } "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_system_version(os_version); 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.15.0-46-generic".to_string()); - linux_data.set_system_version( - "#49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022".to_string(), - ); + linux_data.set_system_release(kernel_version); + linux_data.set_system_version(os_version); linux_data.set_hardware(std::env::consts::ARCH.to_string()); } } From 10650712a77c196c1d0fc2c837c82411fd16410c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 26 Aug 2022 22:29:00 +0200 Subject: [PATCH 252/561] Add mobile client IDs and improve hash cash logic --- core/src/config.rs | 11 ++++++++++- core/src/spclient.rs | 34 ++++++++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index 27a77cec..7d45cdd7 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -3,6 +3,8 @@ use std::{fmt, path::PathBuf, str::FromStr}; use url::Url; pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +pub(crate) const ANDROID_CLIENT_ID: &str = "9a8d2f0ce77a4e248bb71fefcb557637"; +pub(crate) const IOS_CLIENT_ID: &str = "58bd3c95768941ea9eb4350aaa033eb3"; #[derive(Clone, Debug)] pub struct SessionConfig { @@ -16,8 +18,15 @@ pub struct SessionConfig { impl Default for SessionConfig { fn default() -> SessionConfig { let device_id = uuid::Uuid::new_v4().as_hyphenated().to_string(); + let client_id = match std::env::consts::OS { + "android" => ANDROID_CLIENT_ID, + "ios" => IOS_CLIENT_ID, + _ => KEYMASTER_CLIENT_ID, + } + .to_owned(); + SessionConfig { - client_id: KEYMASTER_CLIENT_ID.to_owned(), + client_id, device_id, proxy: None, ap_port: None, diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 6aa331e8..ebcf9057 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,5 +1,6 @@ use std::{ convert::TryInto, + env::consts::OS, fmt::Write, time::{Duration, Instant}, }; @@ -22,7 +23,7 @@ use thiserror::Error; use crate::{ apresolve::SocketAddress, cdn_url::CdnUrl, - config::KEYMASTER_CLIENT_ID, + config::SessionConfig, error::ErrorKind, protocol::{ canvaz::EntityCanvazRequest, @@ -165,9 +166,17 @@ impl SpClient { let client_data = message.mut_client_data(); client_data.set_client_version(spotify_version()); - // using 9a8d2f0ce77a4e248bb71fefcb557637 on Android - // instead of the keymaster ID presents a hash cash challenge - client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string()); + // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, + // so it seems a good idea to mimick the real clients. `self.session().client_id()` returns the + // ID of the client that last connected, but requesting a client token with this ID only works + // on macOS and Windows. On Android and iOS we can send a platform-specific client ID and are + // then presented with a hash cash challenge. On Linux, we have to pass the old keymaster ID. + // We delegate most of this logic to `SessionConfig`. + let client_id = match OS { + "macos" | "windows" => self.session().client_id(), + _ => SessionConfig::default().client_id, + }; + client_data.set_client_id(client_id); let connectivity_data = client_data.mut_connectivity_sdk_data(); connectivity_data.set_device_id(self.session().device_id().to_string()); @@ -178,7 +187,7 @@ impl SpClient { let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); let kernel_version = sys.kernel_version().unwrap_or_else(|| String::from("0")); - match std::env::consts::OS { + match OS { "windows" => { let os_version = os_version.parse::().unwrap_or(10.) as i32; let kernel_version = kernel_version.parse::().unwrap_or(21370); @@ -238,7 +247,7 @@ impl SpClient { // or are presented a hash cash challenge to solve first Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { - trace!("received a hash cash challenge"); + trace!("received a hash cash challenge, solving..."); let challenges = message.get_challenges().clone(); let state = challenges.get_state(); @@ -248,7 +257,7 @@ impl SpClient { let ctx = vec![]; let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { Error::failed_precondition(format!( - "unable to decode Hashcash challenge: {}", + "unable to decode hash cash challenge: {}", e )) })?; @@ -274,7 +283,16 @@ impl SpClient { challenge_answers.state = state.to_string(); challenge_answers.answers.push(challenge_answer); - response = self.client_token_request(&answer_message).await?; + trace!("answering hash cash challenge"); + match self.client_token_request(&answer_message).await { + Ok(token) => response = token, + Err(e) => { + return Err(Error::failed_precondition(format!( + "unable to solve this challenge: {}", + e + ))) + } + } // we should have been granted a token now continue; From 49e885d15817efeadba8255a63e0fcf78e4702a7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 28 Aug 2022 23:52:22 +0200 Subject: [PATCH 253/561] Make hash cash challenges a bit more robust --- core/src/spclient.rs | 119 ++++++++++++++++++++++++++++++------------- 1 file changed, 84 insertions(+), 35 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index ebcf9057..433fa64f 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -108,13 +108,29 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } - fn solve_hash_cash(ctx: &[u8], prefix: &[u8], length: i32, dst: &mut [u8]) { + fn solve_hash_cash( + ctx: &[u8], + prefix: &[u8], + length: i32, + dst: &mut [u8], + ) -> Result<(), Error> { + // after a certain number of seconds, the challenge expires + const TIMEOUT: u64 = 5; // seconds + let now = Instant::now(); + let md = Sha1::digest(ctx); let mut counter: i64 = 0; let target: i64 = BigEndian::read_i64(&md[12..20]); let suffix = loop { + if now.elapsed().as_secs() >= TIMEOUT { + return Err(Error::deadline_exceeded(format!( + "{} seconds expired", + TIMEOUT + ))); + } + let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); let mut hasher = Sha1::new(); @@ -130,6 +146,8 @@ impl SpClient { }; dst.copy_from_slice(&suffix); + + Ok(()) } async fn client_token_request(&self, message: &dyn Message) -> Result { @@ -160,10 +178,10 @@ impl SpClient { trace!("Client token unavailable or expired, requesting new token."); - let mut message = ClientTokenRequest::new(); - message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); + let mut request = ClientTokenRequest::new(); + request.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); - let client_data = message.mut_client_data(); + let client_data = request.mut_client_data(); client_data.set_client_version(spotify_version()); // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, @@ -238,16 +256,21 @@ impl SpClient { } } - let mut response = self.client_token_request(&message).await?; + let mut response = self.client_token_request(&request).await?; + let mut count = 0; + const MAX_TRIES: u8 = 3; let token_response = loop { + count += 1; + let message = ClientTokenResponse::parse_from_bytes(&response)?; + match ClientTokenResponseType::from_i32(message.response_type.value()) { // depending on the platform, you're either given a token immediately // or are presented a hash cash challenge to solve first Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { - trace!("received a hash cash challenge, solving..."); + trace!("Received a hash cash challenge, solving..."); let challenges = message.get_challenges().clone(); let state = challenges.get_state(); @@ -257,57 +280,78 @@ impl SpClient { let ctx = vec![]; let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { Error::failed_precondition(format!( - "unable to decode hash cash challenge: {}", + "Unable to decode hash cash challenge: {}", e )) })?; let length = hash_cash_challenge.length; let mut suffix = vec![0; 0x10]; - Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); + let answer = Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); - // the suffix must be in uppercase - let suffix = hex::encode(suffix).to_uppercase(); + match answer { + Ok(_) => { + // the suffix must be in uppercase + let suffix = hex::encode(suffix).to_uppercase(); - let mut answer_message = ClientTokenRequest::new(); - answer_message.set_request_type( - ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, - ); + let mut answer_message = ClientTokenRequest::new(); + answer_message.set_request_type( + ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, + ); - let challenge_answers = answer_message.mut_challenge_answers(); + let challenge_answers = answer_message.mut_challenge_answers(); - let mut challenge_answer = ChallengeAnswer::new(); - challenge_answer.mut_hash_cash().suffix = suffix.to_string(); - challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; + let mut challenge_answer = ChallengeAnswer::new(); + challenge_answer.mut_hash_cash().suffix = suffix.to_string(); + challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; - challenge_answers.state = state.to_string(); - challenge_answers.answers.push(challenge_answer); + challenge_answers.state = state.to_string(); + challenge_answers.answers.push(challenge_answer); - trace!("answering hash cash challenge"); - match self.client_token_request(&answer_message).await { - Ok(token) => response = token, - Err(e) => { - return Err(Error::failed_precondition(format!( - "unable to solve this challenge: {}", - e - ))) + trace!("Answering hash cash challenge"); + match self.client_token_request(&answer_message).await { + Ok(token) => { + response = token; + continue; + } + Err(e) => { + trace!( + "Answer not accepted {}/{}: {}", + count, + MAX_TRIES, + e + ); + } + } } + Err(e) => trace!( + "Unable to solve hash cash challenge {}/{}: {}", + count, + MAX_TRIES, + e + ), } - // we should have been granted a token now - continue; + if count < MAX_TRIES { + response = self.client_token_request(&request).await?; + } else { + return Err(Error::failed_precondition(format!( + "Unable to solve any of {} hash cash challenges", + MAX_TRIES + ))); + } } else { - return Err(Error::failed_precondition("no challenges found")); + return Err(Error::failed_precondition("No challenges found")); } } Some(unknown) => { return Err(Error::unimplemented(format!( - "unknown client token response type: {:?}", + "Unknown client token response type: {:?}", unknown ))) } - None => return Err(Error::failed_precondition("no client token response type")), + None => return Err(Error::failed_precondition("No client token response type")), } }; @@ -411,7 +455,6 @@ impl SpClient { .token_provider() .get_token("playlist-read") .await?; - let client_token = self.client_token().await?; let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { @@ -421,7 +464,13 @@ impl SpClient { AUTHORIZATION, HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, ); - headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); + + if let Ok(client_token) = self.client_token().await { + headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); + } else { + // currently these endpoints seem to work fine without it + warn!("Unable to get client token. Trying to continue without..."); + } last_response = self.session().http_client().request_body(request).await; From 6c2127bfcdcd4cd05568d31e30448b6419433726 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 29 Aug 2022 23:09:51 +0200 Subject: [PATCH 254/561] Implement rate limiting --- Cargo.lock | 35 ++++++++++++++++++ core/Cargo.toml | 2 ++ core/src/http_client.rs | 78 +++++++++++++++++++++++++++++++++++------ core/src/spclient.rs | 14 ++------ 4 files changed, 107 insertions(+), 22 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 55d319d1..43270907 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -585,6 +585,12 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.21" @@ -726,6 +732,21 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87" +dependencies = [ + "futures", + "futures-timer", + "no-std-compat", + "nonzero_ext", + "parking_lot 0.12.1", + "rand", + "smallvec", +] + [[package]] name = "gstreamer" version = "0.18.8" @@ -1404,6 +1425,7 @@ dependencies = [ "form_urlencoded", "futures-core", "futures-util", + "governor", "hex", "hmac", "http", @@ -1413,6 +1435,7 @@ dependencies = [ "hyper-rustls 0.23.0", "librespot-protocol", "log", + "nonzero_ext", "num", "num-bigint", "num-derive", @@ -1709,6 +1732,12 @@ dependencies = [ "memoffset", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "nom" version = "7.1.1" @@ -1719,6 +1748,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "ntapi" version = "0.3.7" diff --git a/core/Cargo.toml b/core/Cargo.toml index 476f5216..d0bcda02 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,6 +22,7 @@ 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"] } +governor = { version = "0.4", default-features = false, features = ["std", "jitter"] } hex = "0.4" hmac = "0.12" httparse = "1.7" @@ -30,6 +31,7 @@ hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } hyper-rustls = { version = "0.23", features = ["http2"] } log = "0.4" +nonzero_ext = "0.3" num = "0.4" num-bigint = { version = "0.4", features = ["rand"] } num-derive = "0.3" diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 4e454f97..ef9af1ac 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,7 +1,13 @@ -use std::env::consts::OS; +use std::{env::consts::OS, time::Duration}; use bytes::Bytes; use futures_util::{future::IntoStream, FutureExt}; +use governor::{ + clock::MonotonicClock, + middleware::NoOpMiddleware, + state::{InMemoryState, NotKeyed}, + Jitter, Quota, RateLimiter, +}; use http::{header::HeaderValue, Uri}; use hyper::{ client::{HttpConnector, ResponseFuture}, @@ -10,6 +16,7 @@ use hyper::{ }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use nonzero_ext::nonzero; use once_cell::sync::OnceCell; use sysinfo::{System, SystemExt}; use thiserror::Error; @@ -20,6 +27,12 @@ use crate::{ Error, }; +// The 30 seconds interval is documented by Spotify, but the calls per interval +// is a guesstimate and probably subject to licensing (purchasing extra calls) +// and may change at any time. +pub const RATE_LIMIT_INTERVAL: u64 = 30; // seconds +pub const RATE_LIMIT_CALLS_PER_INTERVAL: u32 = 300; + #[derive(Debug, Error)] pub enum HttpClientError { #[error("Response status code: {0}")] @@ -74,11 +87,11 @@ impl From for Error { type HyperClient = Client>, Body>; -#[derive(Clone)] pub struct HttpClient { user_agent: HeaderValue, proxy_url: Option, hyper_client: OnceCell, + rate_limiter: RateLimiter, } impl HttpClient { @@ -109,10 +122,18 @@ impl HttpClient { HeaderValue::from_static(FALLBACK_USER_AGENT) }); + let replenish_interval_ns = Duration::from_secs(RATE_LIMIT_INTERVAL).as_nanos() + / RATE_LIMIT_CALLS_PER_INTERVAL as u128; + let quota = Quota::with_period(Duration::from_nanos(replenish_interval_ns as u64)) + .expect("replenish interval should be valid") + .allow_burst(nonzero![RATE_LIMIT_CALLS_PER_INTERVAL]); + let rate_limiter = RateLimiter::direct(quota); + Self { user_agent, proxy_url: proxy_url.cloned(), hyper_client: OnceCell::new(), + rate_limiter, } } @@ -147,17 +168,54 @@ impl HttpClient { 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; + // `Request` does not implement `Clone` because its `Body` may be a single-shot stream. + // As correct as that may be technically, we now need all this boilerplate to clone it + // ourselves, as any `Request` is moved in the loop. + let (parts, body) = req.into_parts(); + let body_as_bytes = hyper::body::to_bytes(body) + .await + .unwrap_or_else(|_| Bytes::new()); - if let Ok(response) = &response { - let code = response.status(); - if code != StatusCode::OK { - return Err(HttpClientError::StatusCode(code).into()); + loop { + let mut req = Request::new(Body::from(body_as_bytes.clone())); + *req.method_mut() = parts.method.clone(); + *req.uri_mut() = parts.uri.clone(); + *req.version_mut() = parts.version; + *req.headers_mut() = parts.headers.clone(); + + // For rate limiting we cannot *just* depend on Spotify sending us HTTP/429 + // Retry-After headers. For example, when there is a service interruption + // and HTTP/500 is returned, we don't want to DoS the Spotify infrastructure. + self.rate_limiter + .until_ready_with_jitter(Jitter::up_to(Duration::from_secs(5))) + .await; + + let request = self.request_fut(req)?; + let response = request.await; + + if let Ok(response) = &response { + let code = response.status(); + + if code == StatusCode::TOO_MANY_REQUESTS { + if let Some(retry_after) = response.headers().get("Retry-After") { + if let Ok(retry_after_str) = retry_after.to_str() { + if let Ok(retry_after_secs) = retry_after_str.parse::() { + warn!("Rate limiting, retrying in {} seconds...", retry_after_secs); + let duration = Duration::from_secs(retry_after_secs); + tokio::time::sleep(duration).await; + continue; + } + } + } + } + + if code != StatusCode::OK { + return Err(HttpClientError::StatusCode(code).into()); + } } - } - Ok(response?) + return Ok(response?); + } } pub async fn request_body(&self, req: Request) -> Result { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 433fa64f..4af0472f 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -15,7 +15,6 @@ use hyper::{ Body, HeaderMap, Method, Request, }; use protobuf::{Message, ProtobufEnum}; -use rand::Rng; use sha1::{Digest, Sha1}; use sysinfo::{System, SystemExt}; use thiserror::Error; @@ -176,7 +175,7 @@ impl SpClient { return Ok(client_token.access_token); } - trace!("Client token unavailable or expired, requesting new token."); + debug!("Client token unavailable or expired, requesting new token."); let mut request = ClientTokenRequest::new(); request.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); @@ -270,7 +269,7 @@ impl SpClient { // or are presented a hash cash challenge to solve first Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { - trace!("Received a hash cash challenge, solving..."); + debug!("Received a hash cash challenge, solving..."); let challenges = message.get_challenges().clone(); let state = challenges.get_state(); @@ -500,16 +499,7 @@ impl SpClient { } } - // 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 From 16dbade51665667ecf768f21099afc648e0b0176 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 29 Aug 2022 23:51:29 +0200 Subject: [PATCH 255/561] Try another access point if so instructed --- core/src/connection/mod.rs | 4 ++-- core/src/session.rs | 32 +++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 0b59de88..a0ea8b79 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -23,8 +23,8 @@ fn login_error_message(code: &ErrorCode) -> &'static str { pub use ErrorCode::*; match code { ProtocolError => "Protocol error", - TryAnotherAP => "Try another AP", - BadConnectionId => "Bad connection id", + TryAnotherAP => "Try another access point", + BadConnectionId => "Bad connection ID", TravelRestriction => "Travel restriction", PremiumAccountRequired => "Premium account required", BadCredentials => "Bad credentials", diff --git a/core/src/session.rs b/core/src/session.rs index 9c95282f..d719748c 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -32,6 +32,7 @@ use crate::{ http_client::HttpClient, mercury::MercuryManager, packet::PacketType, + protocol::keyexchange::ErrorCode, spclient::SpClient, token::TokenProvider, Error, @@ -131,12 +132,33 @@ impl Session { credentials: Credentials, store_credentials: bool, ) -> 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, self.config().proxy.as_ref()).await?; + let (reusable_credentials, transport) = loop { + let ap = self.apresolver().resolve("accesspoint").await?; + info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); + let mut transport = + connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?; + + match connection::authenticate( + &mut transport, + credentials.clone(), + &self.config().device_id, + ) + .await + { + Ok(creds) => break (creds, transport), + Err(e) => { + if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) = + e.error.downcast_ref::() + { + warn!("Instructed to try another access point..."); + continue; + } else { + return Err(e); + } + } + } + }; - let reusable_credentials = - connection::authenticate(&mut transport, credentials, &self.config().device_id).await?; info!("Authenticated as \"{}\" !", reusable_credentials.username); self.set_username(&reusable_credentials.username); if let Some(cache) = self.cache() { From 56b5f08a32d0a1e68561f64b4bf9dc89e5af5d5c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 31 Aug 2022 21:08:11 +0200 Subject: [PATCH 256/561] Sanitize rate limiting timeout --- core/src/http_client.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index ef9af1ac..d63910b6 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -30,7 +30,8 @@ use crate::{ // The 30 seconds interval is documented by Spotify, but the calls per interval // is a guesstimate and probably subject to licensing (purchasing extra calls) // and may change at any time. -pub const RATE_LIMIT_INTERVAL: u64 = 30; // seconds +pub const RATE_LIMIT_INTERVAL: Duration = Duration::from_secs(30); +pub const RATE_LIMIT_MAX_WAIT: Duration = Duration::from_secs(10); pub const RATE_LIMIT_CALLS_PER_INTERVAL: u32 = 300; #[derive(Debug, Error)] @@ -122,8 +123,8 @@ impl HttpClient { HeaderValue::from_static(FALLBACK_USER_AGENT) }); - let replenish_interval_ns = Duration::from_secs(RATE_LIMIT_INTERVAL).as_nanos() - / RATE_LIMIT_CALLS_PER_INTERVAL as u128; + let replenish_interval_ns = + RATE_LIMIT_INTERVAL.as_nanos() / RATE_LIMIT_CALLS_PER_INTERVAL as u128; let quota = Quota::with_period(Duration::from_nanos(replenish_interval_ns as u64)) .expect("replenish interval should be valid") .allow_burst(nonzero![RATE_LIMIT_CALLS_PER_INTERVAL]); @@ -200,10 +201,17 @@ impl HttpClient { if let Some(retry_after) = response.headers().get("Retry-After") { if let Ok(retry_after_str) = retry_after.to_str() { if let Ok(retry_after_secs) = retry_after_str.parse::() { - warn!("Rate limiting, retrying in {} seconds...", retry_after_secs); let duration = Duration::from_secs(retry_after_secs); - tokio::time::sleep(duration).await; - continue; + if duration <= RATE_LIMIT_MAX_WAIT { + warn!( + "Rate limiting, retrying in {} seconds...", + retry_after_secs + ); + tokio::time::sleep(duration).await; + continue; + } else { + debug!("Not going to wait {} seconds", retry_after_secs); + } } } } From 5451d149720f7a5888a1d4c774e7fdad16bb2976 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 1 Sep 2022 22:35:03 +0200 Subject: [PATCH 257/561] Rate limit audio file streaming too --- audio/src/fetch/receive.rs | 14 +++- core/Cargo.toml | 2 +- core/src/date.rs | 10 ++- core/src/error.rs | 6 ++ core/src/http_client.rs | 130 ++++++++++++++++++++++++++----------- core/src/lib.rs | 2 +- 6 files changed, 123 insertions(+), 41 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index af12810c..2c58fbf8 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -11,7 +11,7 @@ use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use librespot_core::{session::Session, Error}; +use librespot_core::{http_client::HttpClient, session::Session, Error}; use crate::range_set::{Range, RangeSet}; @@ -64,6 +64,18 @@ async fn receive_data( let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { + if code == StatusCode::TOO_MANY_REQUESTS { + if let Some(duration) = HttpClient::get_retry_after(response.headers()) { + warn!( + "Rate limiting, retrying in {} seconds...", + duration.as_secs() + ); + // sleeping here means we hold onto this streamer "slot" + // (we don't decrease the number of open requests) + tokio::time::sleep(duration).await; + } + } + break Err(AudioFileError::StatusCode(code).into()); } diff --git a/core/Cargo.toml b/core/Cargo.toml index d0bcda02..5974ef2a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -51,7 +51,7 @@ sha1 = "0.10" shannon = "0.2" sysinfo = { version = "0.25", default-features = false } thiserror = "1.0" -time = "0.3" +time = { version = "0.3", features = ["formatting", "parsing"] } 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/date.rs b/core/src/date.rs index c9aadce8..a3c1b8d7 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -4,7 +4,10 @@ use std::{ ops::Deref, }; -use time::{error::ComponentRange, Date as _Date, OffsetDateTime, PrimitiveDateTime, Time}; +use time::{ + error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime, + PrimitiveDateTime, Time, +}; use crate::Error; @@ -48,6 +51,11 @@ impl Date { pub fn now_utc() -> Self { Self(OffsetDateTime::now_utc()) } + + pub fn from_iso8601(input: &str) -> Result { + let date_time = OffsetDateTime::parse(input, &Iso8601::DEFAULT)?; + Ok(Self(date_time)) + } } impl TryFrom<&DateMessage> for Date { diff --git a/core/src/error.rs b/core/src/error.rs index d032bd2a..5614a5d3 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -342,6 +342,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: time::error::Parse) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + impl From for Error { fn from(err: quick_xml::Error) -> Self { Self::new(ErrorKind::FailedPrecondition, err) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index d63910b6..63f1d7a8 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,28 +1,31 @@ -use std::{env::consts::OS, time::Duration}; +use std::{ + collections::HashMap, + env::consts::OS, + time::{Duration, Instant}, +}; use bytes::Bytes; use futures_util::{future::IntoStream, FutureExt}; use governor::{ - clock::MonotonicClock, - middleware::NoOpMiddleware, - state::{InMemoryState, NotKeyed}, - Jitter, Quota, RateLimiter, + clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, Quota, RateLimiter, }; use http::{header::HeaderValue, Uri}; use hyper::{ client::{HttpConnector, ResponseFuture}, header::USER_AGENT, - Body, Client, Request, Response, StatusCode, + Body, Client, HeaderMap, Request, Response, StatusCode, }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use nonzero_ext::nonzero; use once_cell::sync::OnceCell; +use parking_lot::Mutex; use sysinfo::{System, SystemExt}; use thiserror::Error; use url::Url; use crate::{ + date::Date, version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING}, Error, }; @@ -92,7 +95,11 @@ pub struct HttpClient { user_agent: HeaderValue, proxy_url: Option, hyper_client: OnceCell, - rate_limiter: RateLimiter, + + // while the DashMap variant is more performant, our level of concurrency + // is pretty low so we can save pulling in that extra dependency + rate_limiter: + RateLimiter>, MonotonicClock, NoOpMiddleware>, } impl HttpClient { @@ -128,7 +135,7 @@ impl HttpClient { let quota = Quota::with_period(Duration::from_nanos(replenish_interval_ns as u64)) .expect("replenish interval should be valid") .allow_burst(nonzero![RATE_LIMIT_CALLS_PER_INTERVAL]); - let rate_limiter = RateLimiter::direct(quota); + let rate_limiter = RateLimiter::keyed(quota); Self { user_agent, @@ -178,19 +185,13 @@ impl HttpClient { .unwrap_or_else(|_| Bytes::new()); loop { - let mut req = Request::new(Body::from(body_as_bytes.clone())); - *req.method_mut() = parts.method.clone(); - *req.uri_mut() = parts.uri.clone(); - *req.version_mut() = parts.version; + let mut req = Request::builder() + .method(parts.method.clone()) + .uri(parts.uri.clone()) + .version(parts.version) + .body(Body::from(body_as_bytes.clone()))?; *req.headers_mut() = parts.headers.clone(); - // For rate limiting we cannot *just* depend on Spotify sending us HTTP/429 - // Retry-After headers. For example, when there is a service interruption - // and HTTP/500 is returned, we don't want to DoS the Spotify infrastructure. - self.rate_limiter - .until_ready_with_jitter(Jitter::up_to(Duration::from_secs(5))) - .await; - let request = self.request_fut(req)?; let response = request.await; @@ -198,22 +199,13 @@ impl HttpClient { let code = response.status(); if code == StatusCode::TOO_MANY_REQUESTS { - if let Some(retry_after) = response.headers().get("Retry-After") { - if let Ok(retry_after_str) = retry_after.to_str() { - if let Ok(retry_after_secs) = retry_after_str.parse::() { - let duration = Duration::from_secs(retry_after_secs); - if duration <= RATE_LIMIT_MAX_WAIT { - warn!( - "Rate limiting, retrying in {} seconds...", - retry_after_secs - ); - tokio::time::sleep(duration).await; - continue; - } else { - debug!("Not going to wait {} seconds", retry_after_secs); - } - } - } + if let Some(duration) = Self::get_retry_after(response.headers()) { + warn!( + "Rate limited by service, retrying in {} seconds...", + duration.as_secs() + ); + tokio::time::sleep(duration).await; + continue; } } @@ -239,7 +231,71 @@ impl HttpClient { let headers_mut = req.headers_mut(); headers_mut.insert(USER_AGENT, self.user_agent.clone()); - let request = self.hyper_client()?.request(req); - Ok(request) + // For rate limiting we cannot *just* depend on Spotify sending us HTTP/429 + // Retry-After headers. For example, when there is a service interruption + // and HTTP/500 is returned, we don't want to DoS the Spotify infrastructure. + let domain = match req.uri().host() { + Some(host) => { + // strip the prefix from *.domain.tld (assume rate limit is per domain, not subdomain) + let mut parts = host + .split('.') + .map(|s| s.to_string()) + .collect::>(); + let n = parts.len().saturating_sub(2); + parts.drain(n..).collect() + } + None => String::from(""), + }; + self.rate_limiter.check_key(&domain).map_err(|e| { + Error::resource_exhausted(format!( + "rate limited for at least another {} seconds", + e.wait_time_from(Instant::now()).as_secs() + )) + })?; + + Ok(self.hyper_client()?.request(req)) + } + + pub fn get_retry_after(headers: &HeaderMap) -> Option { + let now = Date::now_utc().as_timestamp_ms(); + + let mut retry_after_ms = None; + if let Some(header_val) = headers.get("X-RateLimit-Next") { + // *.akamaized.net (Akamai) + if let Ok(date_str) = header_val.to_str() { + if let Ok(target) = Date::from_iso8601(date_str) { + retry_after_ms = Some(target.as_timestamp_ms().saturating_sub(now)) + } + } + } else if let Some(header_val) = headers.get("Fastly-RateLimit-Reset") { + // *.scdn.co (Fastly) + if let Ok(timestamp) = header_val.to_str() { + if let Ok(target) = timestamp.parse::() { + retry_after_ms = Some(target.saturating_sub(now)) + } + } + } else if let Some(header_val) = headers.get("Retry-After") { + // Generic RFC compliant (including *.spotify.com) + if let Ok(retry_after) = header_val.to_str() { + if let Ok(duration) = retry_after.parse::() { + retry_after_ms = Some(duration * 1000) + } + } + } + + if let Some(retry_after) = retry_after_ms { + let duration = Duration::from_millis(retry_after as u64); + if duration <= RATE_LIMIT_MAX_WAIT { + return Some(duration); + } else { + debug!( + "Waiting {} seconds would exceed {} second limit", + duration.as_secs(), + RATE_LIMIT_MAX_WAIT.as_secs() + ); + } + } + + None } } diff --git a/core/src/lib.rs b/core/src/lib.rs index d476d917..f0ee345c 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -21,7 +21,7 @@ mod dealer; pub mod diffie_hellman; pub mod error; pub mod file_id; -mod http_client; +pub mod http_client; pub mod mercury; pub mod packet; mod proxytunnel; From 762f6d1a6fe6c4d7af3016e68d83416bb0b9f2f5 Mon Sep 17 00:00:00 2001 From: eladyn <59307989+eladyn@users.noreply.github.com> Date: Sat, 3 Sep 2022 09:59:53 +0200 Subject: [PATCH 258/561] strongly type `ActivityPeriod` (#1055) switch fallibly to unsigned integers --- core/src/error.rs | 13 +++++++++++- metadata/src/artist.rs | 45 ++++++++++++++++++++++++++++++------------ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/core/src/error.rs b/core/src/error.rs index 5614a5d3..700aed84 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -1,4 +1,9 @@ -use std::{error, fmt, num::ParseIntError, str::Utf8Error, string::FromUtf8Error}; +use std::{ + error, fmt, + num::{ParseIntError, TryFromIntError}, + str::Utf8Error, + string::FromUtf8Error, +}; use base64::DecodeError; use http::{ @@ -419,6 +424,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: TryFromIntError) -> Self { + Self::new(ErrorKind::FailedPrecondition, err) + } +} + impl From for Error { fn from(err: ProtobufError) -> Self { Self::new(ErrorKind::FailedPrecondition, err) diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 1a7e1862..2f50488b 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -111,10 +111,12 @@ pub struct Biographies(pub Vec); impl_deref_wrapped!(Biographies, Vec); #[derive(Debug, Clone)] -pub struct ActivityPeriod { - pub start_year: i32, - pub end_year: i32, - pub decade: i32, +pub enum ActivityPeriod { + Timespan { + start_year: u16, + end_year: Option, + }, + Decade(u16), } #[derive(Debug, Clone, Default)] @@ -196,7 +198,7 @@ impl TryFrom<&::Message> for Artist { external_ids: artist.get_external_id().into(), portraits: artist.get_portrait().into(), biographies: artist.get_biography().into(), - activity_periods: artist.get_activity_period().into(), + activity_periods: artist.get_activity_period().try_into()?, restrictions: artist.get_restriction().into(), related: artist.get_related().try_into()?, is_portrait_album_cover: artist.get_is_portrait_album_cover(), @@ -270,14 +272,31 @@ impl From<&BiographyMessage> for Biography { impl_from_repeated!(BiographyMessage, Biographies); -impl From<&ActivityPeriodMessage> for ActivityPeriod { - fn from(activity_periode: &ActivityPeriodMessage) -> Self { - Self { - start_year: activity_periode.get_start_year(), - end_year: activity_periode.get_end_year(), - decade: activity_periode.get_decade(), - } +impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod { + type Error = librespot_core::Error; + + fn try_from(period: &ActivityPeriodMessage) -> Result { + let activity_period = match ( + period.has_decade(), + period.has_start_year(), + period.has_end_year(), + ) { + // (decade, start_year, end_year) + (true, false, false) => Self::Decade(period.get_decade().try_into()?), + (false, true, closed_period) => Self::Timespan { + start_year: period.get_start_year().try_into()?, + end_year: closed_period + .then(|| period.get_end_year().try_into()) + .transpose()?, + }, + _ => { + return Err(librespot_core::Error::failed_precondition( + "ActivityPeriod is expected to be either a decade or timespan", + )) + } + }; + Ok(activity_period) } } -impl_from_repeated!(ActivityPeriodMessage, ActivityPeriods); +impl_try_from_repeated!(ActivityPeriodMessage, ActivityPeriods); From 8545f361c47b764739ebc4925c4bd33416baa91b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 23 Aug 2022 15:23:37 -0500 Subject: [PATCH 259/561] Major Events Overhaul Special thanks to @eladyn for all of their help and suggestions. * Add all player events to `player_event_handler.rs` * Move event handler code to `player_event_handler.rs` * Add session events * Clean up and de-noise events and event firing * Added metadata support via a TrackChanged event * Add `event_handler_example.py` * Handle invalid track start positions by just starting the track from the beginning * Add repeat support to `spirc.rs` * Add `disconnect`, `set_position_ms` and `set_volume` to `spirc.rs` * Set `PlayStatus` to the correct value when Player is loading to avoid blanking out the controls when `self.play_status` is `LoadingPlay` or `LoadingPause` in `spirc.rs` * Handle attempts to play local files better by basically ignoring attempts to load them in `handle_remote_update` in `spirc.rs` * Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers. * Get rid of (probably harmless) `.unwrap()` in `main.rs` * Ensure that events are emited in a logical order and at logical times * Handle invalid and disappearing devices better * Ignore SpircCommands unless we're active with the exception of ShutDown --- CHANGELOG.md | 11 + connect/src/spirc.rs | 301 +++++++++++------ contrib/event_handler_example.py | 77 +++++ core/src/authentication.rs | 2 +- core/src/session.rs | 41 +++ examples/play.rs | 2 +- metadata/src/album.rs | 2 +- metadata/src/audio/item.rs | 314 +++++++++++++----- metadata/src/audio/mod.rs | 2 +- metadata/src/episode.rs | 32 +- metadata/src/error.rs | 4 + metadata/src/image.rs | 7 + metadata/src/track.rs | 43 +-- playback/src/audio_backend/alsa.rs | 44 ++- playback/src/player.rs | 501 ++++++++++++++++++----------- src/main.rs | 72 +---- src/player_event_handler.rs | 423 ++++++++++++++++-------- 17 files changed, 1239 insertions(+), 639 deletions(-) create mode 100644 contrib/event_handler_example.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad245b1..a1412875 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,9 +79,20 @@ https://github.com/librespot-org/librespot disabled such content. Applications that use librespot as a library without Connect should use the 'filter-explicit-content' user attribute in the session. - [metadata] All metadata fields in the protobufs are now exposed (breaking) +- [connect] Add session events +- [playback] Add metadata support via a `TrackChanged` event +- [main] Add all player events to `player_event_handler.rs` +- [contrib] Add `event_handler_example.py` +- [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs` +- [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers ### Fixed +- [connect] Set `PlayStatus` to the correct value when Player is loading to avoid blanking out the controls when `self.play_status` is `LoadingPlay` or `LoadingPause` in `spirc.rs` +- [connect] Handle attempts to play local files better by basically ignoring attempts to load them in `handle_remote_update` in `spirc.rs` +- [playback] Handle invalid track start positions by just starting the track from the beginning +- [playback, connect] Clean up and de-noise events and event firing +- [playback] Handle disappearing and invalid devices better ### Removed - [main] `autoplay` is no longer a command-line option. Instead, librespot now diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 9105ffdf..616a44e5 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -44,6 +44,8 @@ use crate::{ pub enum SpircError { #[error("response payload empty")] NoData, + #[error("playback of local files is not supported")] + UnsupportedLocalPlayBack, #[error("message addressed at another ident: {0}")] Ident(String), #[error("message pushed for another URI")] @@ -52,10 +54,10 @@ pub enum SpircError { impl From for Error { fn from(err: SpircError) -> Self { + use SpircError::*; match err { - SpircError::NoData => Error::unavailable(err), - SpircError::Ident(_) => Error::aborted(err), - SpircError::InvalidUri(_) => Error::aborted(err), + NoData | UnsupportedLocalPlayBack => Error::unavailable(err), + Ident(_) | InvalidUri(_) => Error::aborted(err), } } } @@ -113,6 +115,7 @@ struct SpircTask { static SPIRC_COUNTER: AtomicUsize = AtomicUsize::new(0); +#[derive(Debug)] pub enum SpircCommand { Play, PlayPause, @@ -122,7 +125,11 @@ pub enum SpircCommand { VolumeUp, VolumeDown, Shutdown, - Shuffle, + Shuffle(bool), + Repeat(bool), + Disconnect, + SetPosition(u32), + SetVolume(u16), } const CONTEXT_TRACKS_HISTORY: usize = 10; @@ -243,10 +250,8 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { msg.set_typ(protocol::spirc::CapabilityType::kSupportedTypes); { let repeated = msg.mut_stringValue(); - repeated.push(::std::convert::Into::into("audio/local")); repeated.push(::std::convert::Into::into("audio/track")); repeated.push(::std::convert::Into::into("audio/episode")); - repeated.push(::std::convert::Into::into("local")); repeated.push(::std::convert::Into::into("track")) }; msg @@ -416,8 +421,20 @@ impl Spirc { pub fn shutdown(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Shutdown)?) } - pub fn shuffle(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Shuffle)?) + pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?) + } + pub fn repeat(&self, repeat: bool) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Repeat(repeat))?) + } + pub fn set_volume(&self, volume: u16) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::SetVolume(volume))?) + } + pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?) + } + pub fn disconnect(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Disconnect)?) } } @@ -552,76 +569,71 @@ impl SpircTask { } fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { - let active = self.device.get_is_active(); - match cmd { - SpircCommand::Play => { - if active { + if matches!(cmd, SpircCommand::Shutdown) { + trace!("Received SpircCommand::Shutdown"); + CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?; + self.handle_disconnect(); + self.shutdown = true; + if let Some(rx) = self.commands.as_mut() { + rx.close() + } + Ok(()) + } else if self.device.get_is_active() { + trace!("Received SpircCommand::{:?}", cmd); + match cmd { + SpircCommand::Play => { self.handle_play(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypePlay).send() } - } - SpircCommand::PlayPause => { - if active { + SpircCommand::PlayPause => { self.handle_play_pause(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypePlayPause).send() } - } - SpircCommand::Pause => { - if active { + SpircCommand::Pause => { self.handle_pause(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypePause).send() } - } - SpircCommand::Prev => { - if active { + SpircCommand::Prev => { self.handle_prev(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypePrev).send() } - } - SpircCommand::Next => { - if active { + SpircCommand::Next => { self.handle_next(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypeNext).send() } - } - SpircCommand::VolumeUp => { - if active { + SpircCommand::VolumeUp => { self.handle_volume_up(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeUp).send() } - } - SpircCommand::VolumeDown => { - if active { + SpircCommand::VolumeDown => { self.handle_volume_down(); self.notify(None) - } else { - CommandSender::new(self, MessageType::kMessageTypeVolumeDown).send() } - } - SpircCommand::Shutdown => { - CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?; - self.player.stop(); - self.shutdown = true; - if let Some(rx) = self.commands.as_mut() { - rx.close() + SpircCommand::Disconnect => { + self.handle_disconnect(); + self.notify(None) } - Ok(()) - } - SpircCommand::Shuffle => { - CommandSender::new(self, MessageType::kMessageTypeShuffle).send() + SpircCommand::Shuffle(shuffle) => { + self.state.set_shuffle(shuffle); + self.notify(None) + } + SpircCommand::Repeat(repeat) => { + self.state.set_repeat(repeat); + self.notify(None) + } + SpircCommand::SetPosition(position) => { + self.handle_seek(position); + self.notify(None) + } + SpircCommand::SetVolume(volume) => { + self.set_volume(volume); + self.notify(None) + } + _ => Ok(()), } + } else { + warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); + Ok(()) } } @@ -635,11 +647,28 @@ impl SpircTask { match event { PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), PlayerEvent::Loading { .. } => { - trace!("==> kPlayStatusLoading"); - self.state.set_status(PlayStatus::kPlayStatusLoading); + match self.play_status { + SpircPlayStatus::LoadingPlay { position_ms } => { + self.update_state_position(position_ms); + self.state.set_status(PlayStatus::kPlayStatusPlay); + trace!("==> kPlayStatusPlay"); + } + SpircPlayStatus::LoadingPause { position_ms } => { + self.update_state_position(position_ms); + self.state.set_status(PlayStatus::kPlayStatusPause); + trace!("==> kPlayStatusPause"); + } + _ => { + self.state.set_status(PlayStatus::kPlayStatusLoading); + self.update_state_position(0); + trace!("==> kPlayStatusLoading"); + } + } self.notify(None) } - PlayerEvent::Playing { position_ms, .. } => { + PlayerEvent::Playing { position_ms, .. } + | PlayerEvent::PositionCorrection { position_ms, .. } + | PlayerEvent::Seeked { position_ms, .. } => { trace!("==> kPlayStatusPlay"); let new_nominal_start_time = self.now_ms() - position_ms as i64; match self.play_status { @@ -674,17 +703,14 @@ impl SpircTask { } => { trace!("==> kPlayStatusPause"); match self.play_status { - SpircPlayStatus::Paused { - ref mut position_ms, - .. - } => { - if *position_ms != new_position_ms { - *position_ms = new_position_ms; - self.update_state_position(new_position_ms); - self.notify(None) - } else { - Ok(()) - } + SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => { + self.state.set_status(PlayStatus::kPlayStatusPause); + self.update_state_position(new_position_ms); + self.play_status = SpircPlayStatus::Paused { + position_ms: new_position_ms, + preloading_of_next_track_triggered: false, + }; + self.notify(None) } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { @@ -762,7 +788,13 @@ impl SpircTask { ); if key == "filter-explicit-content" && new_value == "1" { - self.player.skip_explicit_content(); + self.player + .emit_filter_explicit_content_changed_event(matches!(new_value, "1")); + } + + if key == "autoplay" && old_value != new_value { + self.player + .emit_auto_play_changed_event(matches!(new_value, "1")); } } else { trace!( @@ -785,13 +817,31 @@ impl SpircTask { return Err(SpircError::Ident(ident.to_string()).into()); } + let old_client_id = self.session.client_id(); + 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 entry.get_field_type() { + "client-id" => self.session.set_client_id(entry.get_metadata()), + "brand_display_name" => self.session.set_client_brand_name(entry.get_metadata()), + "model_display_name" => self.session.set_client_model_name(entry.get_metadata()), + _ => (), } } + self.session + .set_client_name(update.get_device_state().get_name()); + + let new_client_id = self.session.client_id(); + + if self.device.get_is_active() && new_client_id != old_client_id { + self.player.emit_session_client_changed_event( + new_client_id, + self.session.client_name(), + self.session.client_brand_name(), + self.session.client_model_name(), + ); + } + match update.get_typ() { MessageType::kMessageTypeHello => self.notify(Some(ident)), @@ -800,6 +850,40 @@ impl SpircTask { let now = self.now_ms(); self.device.set_is_active(true); self.device.set_became_active_at(now); + self.player.emit_session_connected_event( + self.session.connection_id(), + self.session.username(), + ); + self.player.emit_session_client_changed_event( + self.session.client_id(), + self.session.client_name(), + self.session.client_brand_name(), + self.session.client_model_name(), + ); + + self.player + .emit_volume_changed_event(self.device.get_volume() as u16); + + self.player + .emit_auto_play_changed_event(self.session.autoplay()); + + self.player.emit_filter_explicit_content_changed_event( + self.session.filter_explicit_content(), + ); + + self.player + .emit_shuffle_changed_event(self.state.get_shuffle()); + + self.player + .emit_repeat_changed_event(self.state.get_repeat()); + } + + let context_uri = update.get_state().get_context_uri().to_owned(); + + // completely ignore local playback. + if context_uri.starts_with("spotify:local-files") { + self.notify(None)?; + return Err(SpircError::UnsupportedLocalPlayBack.into()); } self.update_tracks(&update); @@ -852,12 +936,17 @@ impl SpircTask { } MessageType::kMessageTypeRepeat => { - self.state.set_repeat(update.get_state().get_repeat()); + let repeat = update.get_state().get_repeat(); + self.state.set_repeat(repeat); + + self.player.emit_repeat_changed_event(repeat); + self.notify(None) } MessageType::kMessageTypeShuffle => { - self.state.set_shuffle(update.get_state().get_shuffle()); + let shuffle = update.get_state().get_shuffle(); + self.state.set_shuffle(shuffle); if self.state.get_shuffle() { let current_index = self.state.get_playing_track_index(); let tracks = self.state.mut_track(); @@ -873,6 +962,9 @@ impl SpircTask { let context = self.state.get_context_uri(); debug!("{:?}", context); } + + self.player.emit_shuffle_changed_event(shuffle); + self.notify(None) } @@ -882,6 +974,14 @@ impl SpircTask { } MessageType::kMessageTypeReplace => { + let context_uri = update.get_state().get_context_uri().to_owned(); + + // completely ignore local playback. + if context_uri.starts_with("spotify:local-files") { + self.notify(None)?; + return Err(SpircError::UnsupportedLocalPlayBack.into()); + } + self.update_tracks(&update); if let SpircPlayStatus::Playing { @@ -915,16 +1015,23 @@ impl SpircTask { && self.device.get_became_active_at() <= update.get_device_state().get_became_active_at() { - self.device.set_is_active(false); - self.handle_stop(); + self.handle_disconnect(); } - Ok(()) + self.notify(None) } _ => Ok(()), } } + fn handle_disconnect(&mut self) { + self.device.set_is_active(false); + self.handle_stop(); + + self.player + .emit_session_disconnected_event(self.session.connection_id(), self.session.username()); + } + fn handle_stop(&mut self) { self.player.stop(); } @@ -1100,17 +1207,7 @@ impl SpircTask { } if new_index >= tracks_len { - 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" { + if self.session.autoplay() { // Extend the playlist debug!("Extending playlist <{}>", context_uri); self.update_tracks_from_context(); @@ -1282,12 +1379,10 @@ impl SpircTask { || context_uri.starts_with("spotify:dailymix:") { self.context_fut = self.resolve_station(&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); - } + } else if self.session.autoplay() { + info!("Fetching autoplay context uri"); + // Get autoplay_station_uri for regular playlists + self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); } self.player @@ -1422,12 +1517,18 @@ impl SpircTask { } fn set_volume(&mut self, volume: u16) { - self.device.set_volume(volume as u32); - self.mixer.set_volume(volume); - if let Some(cache) = self.session.cache() { - cache.save_volume(volume) + let old_volume = self.device.get_volume(); + let new_volume = volume as u32; + if old_volume != new_volume { + self.device.set_volume(new_volume); + self.mixer.set_volume(volume); + if let Some(cache) = self.session.cache() { + cache.save_volume(volume) + } + if self.device.get_is_active() { + self.player.emit_volume_changed_event(volume); + } } - self.player.emit_volume_set_event(volume); } } diff --git a/contrib/event_handler_example.py b/contrib/event_handler_example.py new file mode 100644 index 00000000..f419e7d6 --- /dev/null +++ b/contrib/event_handler_example.py @@ -0,0 +1,77 @@ +#!/usr/bin/python3 +import os +import json +from datetime import datetime + +player_event = os.getenv('PLAYER_EVENT') + +json_dict = { + 'event_time': str(datetime.now()), + 'event': player_event, +} + +if player_event in ('session_connected', 'session_disconnected'): + json_dict['user_name'] = os.environ['USER_NAME'] + json_dict['connection_id'] = os.environ['CONNECTION_ID'] + +elif player_event == 'session_client_changed': + json_dict['client_id'] = os.environ['CLIENT_ID'] + json_dict['client_name'] = os.environ['CLIENT_NAME'] + json_dict['client_brand_name'] = os.environ['CLIENT_BRAND_NAME'] + json_dict['client_model_name'] = os.environ['CLIENT_MODEL_NAME'] + +elif player_event == 'shuffle_changed': + json_dict['shuffle'] = os.environ['SHUFFLE'] + +elif player_event == 'repeat_changed': + json_dict['repeat'] = os.environ['REPEAT'] + +elif player_event == 'auto_play_changed': + json_dict['auto_play'] = os.environ['AUTO_PLAY'] + +elif player_event == 'filter_explicit_content_changed': + json_dict['filter'] = os.environ['FILTER'] + +elif player_event == 'volume_changed': + json_dict['volume'] = os.environ['VOLUME'] + +elif player_event in ('seeked', 'position_correction', 'playing', 'paused'): + json_dict['track_id'] = os.environ['TRACK_ID'] + json_dict['position_ms'] = os.environ['POSITION_MS'] + +elif player_event in ('unavailable', 'end_of_track', 'preload_next', 'preloading', 'loading', 'stopped'): + json_dict['track_id'] = os.environ['TRACK_ID'] + +elif player_event == 'track_changed': + common_metadata_fields = {} + item_type = os.environ['ITEM_TYPE'] + common_metadata_fields['item_type'] = item_type + common_metadata_fields['track_id'] = os.environ['TRACK_ID'] + common_metadata_fields['uri'] = os.environ['URI'] + common_metadata_fields['name'] = os.environ['NAME'] + common_metadata_fields['duration_ms'] = os.environ['DURATION_MS'] + common_metadata_fields['is_explicit'] = os.environ['IS_EXPLICIT'] + common_metadata_fields['language'] = os.environ['LANGUAGE'].split('\n') + common_metadata_fields['covers'] = os.environ['COVERS'].split('\n') + json_dict['common_metadata_fields'] = common_metadata_fields + + + if item_type == 'Track': + track_metadata_fields = {} + track_metadata_fields['number'] = os.environ['NUMBER'] + track_metadata_fields['disc_number'] = os.environ['DISC_NUMBER'] + track_metadata_fields['popularity'] = os.environ['POPULARITY'] + track_metadata_fields['album'] = os.environ['ALBUM'] + track_metadata_fields['artists'] = os.environ['ARTISTS'].split('\n') + track_metadata_fields['album_artists'] = os.environ['ALBUM_ARTISTS'].split('\n') + json_dict['track_metadata_fields'] = track_metadata_fields + + elif item_type == 'Episode': + episode_metadata_fields = {} + episode_metadata_fields['show_name'] = os.environ['SHOW_NAME'] + publish_time = datetime.utcfromtimestamp(int(os.environ['PUBLISH_TIME'])).strftime('%Y-%m-%d') + episode_metadata_fields['publish_time'] = publish_time + episode_metadata_fields['description'] = os.environ['DESCRIPTION'] + json_dict['episode_metadata_fields'] = episode_metadata_fields + +print(json.dumps(json_dict, indent = 4)) diff --git a/core/src/authentication.rs b/core/src/authentication.rs index dad514b0..b2bcad94 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -26,7 +26,7 @@ impl From for Error { } /// The credentials are used to log into the Spotify API. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct Credentials { pub username: String, diff --git a/core/src/session.rs b/core/src/session.rs index d719748c..82c9e4a8 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -73,6 +73,9 @@ pub struct UserData { #[derive(Debug, Clone, Default)] struct SessionData { client_id: String, + client_name: String, + client_brand_name: String, + client_model_name: String, connection_id: String, time_delta: i64, invalid: bool, @@ -383,6 +386,30 @@ impl Session { self.0.data.write().client_id = client_id.to_owned(); } + pub fn client_name(&self) -> String { + self.0.data.read().client_name.clone() + } + + pub fn set_client_name(&self, client_name: &str) { + self.0.data.write().client_name = client_name.to_owned(); + } + + pub fn client_brand_name(&self) -> String { + self.0.data.read().client_brand_name.clone() + } + + pub fn set_client_brand_name(&self, client_brand_name: &str) { + self.0.data.write().client_brand_name = client_brand_name.to_owned(); + } + + pub fn client_model_name(&self) -> String { + self.0.data.read().client_model_name.clone() + } + + pub fn set_client_model_name(&self, client_model_name: &str) { + self.0.data.write().client_model_name = client_model_name.to_owned(); + } + pub fn connection_id(&self) -> String { self.0.data.read().connection_id.clone() } @@ -403,6 +430,20 @@ impl Session { self.0.data.read().user_data.country.clone() } + pub fn filter_explicit_content(&self) -> bool { + match self.get_user_attribute("filter-explicit-content") { + Some(value) => matches!(&*value, "1"), + None => false, + } + } + + pub fn autoplay(&self) -> bool { + match self.get_user_attribute("autoplay") { + Some(value) => matches!(&*value, "1"), + None => false, + } + } + 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/examples/play.rs b/examples/play.rs index dc22103b..1209bf95 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -36,7 +36,7 @@ async fn main() { exit(1); } - let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || { + let mut player = Player::new(player_config, session, Box::new(NoOpVolume), move || { backend(None, audio_format) }); diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 45db39ea..11f13331 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -97,7 +97,7 @@ impl TryFrom<&::Message> for Album { date: album.get_date().try_into()?, popularity: album.get_popularity(), genres: album.get_genre().to_vec(), - covers: album.get_cover().into(), + covers: album.get_cover_group().into(), external_ids: album.get_external_id().into(), discs: album.get_disc().try_into()?, reviews: album.get_review().to_vec(), diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index c3df12e0..2a672075 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -1,11 +1,14 @@ use std::fmt::Debug; use crate::{ + artist::ArtistsWithRole, availability::{AudioItemAvailability, Availabilities, UnavailabilityReason}, episode::Episode, error::MetadataError, + image::{ImageSize, Images}, restriction::Restrictions, track::{Track, Tracks}, + Metadata, }; use super::file::AudioFiles; @@ -16,98 +19,259 @@ use librespot_core::{ pub type AudioItemResult = Result; -// A wrapper with fields the player needs +#[derive(Debug, Clone)] +pub struct CoverImage { + pub url: String, + pub size: ImageSize, + pub width: i32, + pub height: i32, +} + #[derive(Debug, Clone)] pub struct AudioItem { - pub id: SpotifyId, - pub spotify_uri: String, + pub track_id: SpotifyId, + pub uri: String, pub files: AudioFiles, pub name: String, - pub duration: i32, + pub covers: Vec, + pub language: Vec, + pub duration_ms: u32, + pub is_explicit: bool, pub availability: AudioItemAvailability, pub alternatives: Option, - pub is_explicit: bool, + pub unique_fields: UniqueFields, +} + +#[derive(Debug, Clone)] +pub enum UniqueFields { + Track { + artists: ArtistsWithRole, + album: String, + album_artists: Vec, + popularity: u8, + number: u32, + disc_number: u32, + }, + Episode { + description: String, + publish_time: Date, + show_name: String, + }, } impl AudioItem { pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult { + let image_url = session + .get_user_attribute("image-url") + .unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}")); + match id.item_type { - SpotifyItemType::Track => Track::get_audio_item(session, id).await, - SpotifyItemType::Episode => Episode::get_audio_item(session, id).await, + SpotifyItemType::Track => { + let track = Track::get(session, &id).await?; + + if track.duration <= 0 { + return Err(Error::unavailable(MetadataError::InvalidDuration( + track.duration, + ))); + } + + if track.is_explicit && session.filter_explicit_content() { + return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); + } + + let track_id = track.id; + let uri = track_id.to_uri()?; + let album = track.album.name; + + let album_artists = track + .album + .artists + .0 + .into_iter() + .map(|a| a.name) + .collect::>(); + + let covers = get_covers(track.album.covers, image_url); + + let alternatives = if track.alternatives.is_empty() { + None + } else { + Some(track.alternatives) + }; + + let availability = if Date::now_utc() < track.earliest_live_timestamp { + Err(UnavailabilityReason::Embargo) + } else { + available_for_user( + &session.user_data(), + &track.availability, + &track.restrictions, + ) + }; + + let popularity = track.popularity.max(0).min(100) as u8; + let number = track.number.max(0) as u32; + let disc_number = track.disc_number.max(0) as u32; + + let unique_fields = UniqueFields::Track { + artists: track.artists_with_role, + album, + album_artists, + popularity, + number, + disc_number, + }; + + Ok(Self { + track_id, + uri, + files: track.files, + name: track.name, + covers, + language: track.language_of_performance, + duration_ms: track.duration as u32, + is_explicit: track.is_explicit, + availability, + alternatives, + unique_fields, + }) + } + SpotifyItemType::Episode => { + let episode = Episode::get(session, &id).await?; + + if episode.duration <= 0 { + return Err(Error::unavailable(MetadataError::InvalidDuration( + episode.duration, + ))); + } + + if episode.is_explicit && session.filter_explicit_content() { + return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); + } + + let track_id = episode.id; + let uri = track_id.to_uri()?; + + let covers = get_covers(episode.covers, image_url); + + let availability = available_for_user( + &session.user_data(), + &episode.availability, + &episode.restrictions, + ); + + let unique_fields = UniqueFields::Episode { + description: episode.description, + publish_time: episode.publish_time, + show_name: episode.show_name, + }; + + Ok(Self { + track_id, + uri, + files: episode.audio, + name: episode.name, + covers, + language: vec![episode.language], + duration_ms: episode.duration as u32, + is_explicit: episode.is_explicit, + availability, + alternatives: None, + unique_fields, + }) + } _ => Err(Error::unavailable(MetadataError::NonPlayable)), } } } -#[async_trait] -pub trait InnerAudioItem { - async fn get_audio_item(session: &Session, id: SpotifyId) -> AudioItemResult; +fn get_covers(covers: Images, image_url: String) -> Vec { + let mut covers = covers; - 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", - }; + covers.sort_by(|a, b| b.width.cmp(&a.width)); - for premium_restriction in restrictions.iter().filter(|restriction| { - restriction - .catalogue_strs - .iter() - .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) { - return Ok(()); - } else { - return Err(UnavailabilityReason::NotWhitelisted); - } + covers + .iter() + .filter_map(|cover| { + let cover_id = cover.id.to_string(); + + if !cover_id.is_empty() { + let cover_image = CoverImage { + url: image_url.replace("{file_id}", &cover_id), + size: cover.size, + width: cover.width, + height: cover.height, + }; + + Some(cover_image) + } else { + None } - - 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| Date::now_utc() >= availability.start)) - { - return Err(UnavailabilityReason::Embargo); - } - - Ok(()) - } - - fn available_for_user( - user_data: &UserData, - availability: &Availabilities, - restrictions: &Restrictions, - ) -> AudioItemAvailability { - Self::available(availability)?; - Self::allowed_for_user(user_data, restrictions)?; - Ok(()) - } + }) + .collect() +} + +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(|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) { + 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| Date::now_utc() >= availability.start)) + { + return Err(UnavailabilityReason::Embargo); + } + + Ok(()) +} + +fn available_for_user( + user_data: &UserData, + availability: &Availabilities, + restrictions: &Restrictions, +) -> AudioItemAvailability { + available(availability)?; + allowed_for_user(user_data, restrictions)?; + Ok(()) } diff --git a/metadata/src/audio/mod.rs b/metadata/src/audio/mod.rs index 7e31f190..af9ccdc9 100644 --- a/metadata/src/audio/mod.rs +++ b/metadata/src/audio/mod.rs @@ -2,4 +2,4 @@ pub mod file; pub mod item; pub use file::{AudioFileFormat, AudioFiles}; -pub use item::AudioItem; +pub use item::{AudioItem, UniqueFields}; diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index e65a1045..fe795a25 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -5,10 +5,7 @@ use std::{ }; use crate::{ - audio::{ - file::AudioFiles, - item::{AudioItem, AudioItemResult, InnerAudioItem}, - }, + audio::file::AudioFiles, availability::Availabilities, content_rating::ContentRatings, image::Images, @@ -36,7 +33,7 @@ pub struct Episode { pub covers: Images, pub language: String, pub is_explicit: bool, - pub show: SpotifyId, + pub show_name: String, pub videos: VideoFiles, pub video_previews: VideoFiles, pub audio_previews: AudioFiles, @@ -57,29 +54,6 @@ pub struct Episodes(pub Vec); impl_deref_wrapped!(Episodes, Vec); -#[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_for_user( - &session.user_data(), - &episode.availability, - &episode.restrictions, - ); - - Ok(AudioItem { - id, - spotify_uri: id.to_uri()?, - files: episode.audio, - name: episode.name, - duration: episode.duration, - availability, - alternatives: None, - is_explicit: episode.is_explicit, - }) - } -} - #[async_trait] impl Metadata for Episode { type Message = protocol::metadata::Episode; @@ -107,7 +81,7 @@ impl TryFrom<&::Message> for Episode { 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()?, + show_name: episode.get_show().get_name().to_owned(), videos: episode.get_video().into(), video_previews: episode.get_video_preview().into(), audio_previews: episode.get_audio_preview().into(), diff --git a/metadata/src/error.rs b/metadata/src/error.rs index 31c600b0..26f5ce0b 100644 --- a/metadata/src/error.rs +++ b/metadata/src/error.rs @@ -7,4 +7,8 @@ pub enum MetadataError { Empty, #[error("audio item is non-playable when it should be")] NonPlayable, + #[error("audio item duration can not be: {0}")] + InvalidDuration(i32), + #[error("track is marked as explicit, which client setting forbids")] + ExplicitContentFiltered, } diff --git a/metadata/src/image.rs b/metadata/src/image.rs index dd716623..0bbe5010 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -10,6 +10,7 @@ use librespot_core::{FileId, SpotifyId}; use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; +use protocol::metadata::ImageGroup; pub use protocol::metadata::Image_Size as ImageSize; use protocol::playlist4_external::PictureSize as PictureSizeMessage; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; @@ -25,6 +26,12 @@ pub struct Image { #[derive(Debug, Clone, Default)] pub struct Images(pub Vec); +impl From<&ImageGroup> for Images { + fn from(image_group: &ImageGroup) -> Self { + Self(image_group.image.iter().map(|i| i.into()).collect()) + } +} + impl_deref_wrapped!(Images, Vec); #[derive(Debug, Clone)] diff --git a/metadata/src/track.rs b/metadata/src/track.rs index f4855d8a..03fab92c 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -8,11 +8,8 @@ use uuid::Uuid; use crate::{ artist::{Artists, ArtistsWithRole}, - audio::{ - file::AudioFiles, - item::{AudioItem, AudioItemResult, InnerAudioItem}, - }, - availability::{Availabilities, UnavailabilityReason}, + audio::file::AudioFiles, + availability::Availabilities, content_rating::ContentRatings, external_id::ExternalIds, restriction::Restrictions, @@ -58,42 +55,6 @@ pub struct Tracks(pub Vec); impl_deref_wrapped!(Tracks, Vec); -#[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 Date::now_utc() < track.earliest_live_timestamp { - Err(UnavailabilityReason::Embargo) - } else { - Self::available_for_user( - &session.user_data(), - &track.availability, - &track.restrictions, - ) - }; - - Ok(AudioItem { - id, - spotify_uri: id.to_uri()?, - files: track.files, - name: track.name, - duration: track.duration, - availability, - alternatives, - is_explicit: track.is_explicit, - }) - } -} - #[async_trait] impl Metadata for Track { type Message = protocol::metadata::Track; diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index e8d9ee05..49cc579e 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -442,14 +442,16 @@ impl Sink for AlsaSink { } fn stop(&mut self) -> SinkResult<()> { - // Zero fill the remainder of the period buffer and - // write any leftover data before draining the actual PCM buffer. - self.period_buffer.resize(self.period_buffer.capacity(), 0); - self.write_buf()?; + if self.pcm.is_some() { + // Zero fill the remainder of the period buffer and + // write any leftover data before draining the actual PCM buffer. + self.period_buffer.resize(self.period_buffer.capacity(), 0); + self.write_buf()?; - let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; + let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; - pcm.drain().map_err(AlsaError::DrainFailure)?; + pcm.drain().map_err(AlsaError::DrainFailure)?; + } Ok(()) } @@ -489,17 +491,29 @@ impl AlsaSink { pub const NAME: &'static str = "alsa"; fn write_buf(&mut self) -> SinkResult<()> { - let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + if self.pcm.is_some() { + let write_result = { + let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; - if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) { - // Capture and log the original error as a warning, and then try to recover. - // If recovery fails then forward that error back to player. - warn!( - "Error writing from AlsaSink buffer to PCM, trying to recover, {}", - e - ); + match pcm.io_bytes().writei(&self.period_buffer) { + Ok(_) => Ok(()), + Err(e) => { + // 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, {}", + e + ); - pcm.try_recover(e, false).map_err(AlsaError::OnWrite)? + pcm.try_recover(e, false).map_err(AlsaError::OnWrite) + } + } + }; + + if let Err(e) = write_result { + self.pcm = None; + return Err(e.into()); + } } self.period_buffer.clear(); diff --git a/playback/src/player.rs b/playback/src/player.rs index a7a51762..cd08197c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -111,9 +111,26 @@ enum PlayerCommand { Seek(u32), AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), - EmitVolumeSetEvent(u16), + EmitVolumeChangedEvent(u16), SetAutoNormaliseAsAlbum(bool), - SkipExplicitContent(), + EmitSessionDisconnectedEvent { + connection_id: String, + user_name: String, + }, + EmitSessionConnectedEvent { + connection_id: String, + user_name: String, + }, + EmitSessionClientChangedEvent { + client_id: String, + client_name: String, + client_brand_name: String, + client_model_name: String, + }, + EmitFilterExplicitContentChangedEvent(bool), + EmitShuffleChangedEvent(bool), + EmitRepeatChangedEvent(bool), + EmitAutoPlayChangedEvent(bool), } #[derive(Debug, Clone)] @@ -123,19 +140,6 @@ pub enum PlayerEvent { play_request_id: u64, track_id: SpotifyId, }, - // The player started working on playback of a track while it was in a stopped state. - // This is always immediately followed up by a "Loading" or "Playing" event. - Started { - play_request_id: u64, - track_id: SpotifyId, - position_ms: u32, - }, - // Same as started but in the case that the player already had a track loaded. - // The player was either playing the loaded track or it was paused. - Changed { - old_track_id: SpotifyId, - new_track_id: SpotifyId, - }, // The player is delayed by loading a track. Loading { play_request_id: u64, @@ -157,14 +161,12 @@ pub enum PlayerEvent { play_request_id: u64, track_id: SpotifyId, position_ms: u32, - duration_ms: u32, }, // The player entered a paused state. Paused { play_request_id: u64, track_id: SpotifyId, position_ms: u32, - duration_ms: u32, }, // The player thinks it's a good idea to issue a preload command for the next track now. // This event is intended for use within spirc. @@ -173,8 +175,7 @@ pub enum PlayerEvent { track_id: SpotifyId, }, // The player reached the end of a track. - // This event is intended for use within spirc. Spirc will respond by issuing another command - // which will trigger another event (e.g. Changed or Stopped) + // This event is intended for use within spirc. Spirc will respond by issuing another command. EndOfTrack { play_request_id: u64, track_id: SpotifyId, @@ -185,9 +186,48 @@ pub enum PlayerEvent { track_id: SpotifyId, }, // The mixer volume was set to a new level. - VolumeSet { + VolumeChanged { volume: u16, }, + PositionCorrection { + play_request_id: u64, + track_id: SpotifyId, + position_ms: u32, + }, + Seeked { + play_request_id: u64, + track_id: SpotifyId, + position_ms: u32, + }, + TrackChanged { + audio_item: Box, + }, + SessionConnected { + connection_id: String, + user_name: String, + }, + SessionDisconnected { + connection_id: String, + user_name: String, + }, + SessionClientChanged { + client_id: String, + client_name: String, + client_brand_name: String, + client_model_name: String, + }, + ShuffleChanged { + shuffle: bool, + }, + RepeatChanged { + repeat: bool, + }, + AutoPlayChanged { + auto_play: bool, + }, + FilterExplicitContentChanged { + filter: bool, + }, } impl PlayerEvent { @@ -200,9 +240,6 @@ impl PlayerEvent { | Unavailable { play_request_id, .. } - | Started { - play_request_id, .. - } | Playing { play_request_id, .. } @@ -217,8 +254,14 @@ impl PlayerEvent { } | Stopped { play_request_id, .. + } + | PositionCorrection { + play_request_id, .. + } + | Seeked { + play_request_id, .. } => Some(*play_request_id), - Changed { .. } | Preloading { .. } | VolumeSet { .. } => None, + _ => None, } } } @@ -370,12 +413,11 @@ impl Player { session: Session, volume_getter: Box, sink_builder: F, - ) -> (Player, PlayerEventChannel) + ) -> Self where F: FnOnce() -> Box + Send + 'static, { 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); @@ -421,7 +463,7 @@ impl Player { sink_status: SinkStatus::Closed, sink_event_callback: None, volume_getter, - event_senders: [event_sender].to_vec(), + event_senders: vec![], converter, normalisation_peak: 0.0, @@ -440,14 +482,11 @@ impl Player { debug!("PlayerInternal thread finished."); }); - ( - Player { - commands: Some(cmd_tx), - thread_handle: Some(handle), - play_request_id_generator: SeqGenerator::new(0), - }, - event_receiver, - ) + Self { + commands: Some(cmd_tx), + thread_handle: Some(handle), + play_request_id_generator: SeqGenerator::new(0), + } } fn command(&self, cmd: PlayerCommand) { @@ -512,16 +551,57 @@ impl Player { self.command(PlayerCommand::SetSinkEventCallback(callback)); } - pub fn emit_volume_set_event(&self, volume: u16) { - self.command(PlayerCommand::EmitVolumeSetEvent(volume)); + pub fn emit_volume_changed_event(&self, volume: u16) { + self.command(PlayerCommand::EmitVolumeChangedEvent(volume)); } 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()); + pub fn emit_filter_explicit_content_changed_event(&self, filter: bool) { + self.command(PlayerCommand::EmitFilterExplicitContentChangedEvent(filter)); + } + + pub fn emit_session_connected_event(&self, connection_id: String, user_name: String) { + self.command(PlayerCommand::EmitSessionConnectedEvent { + connection_id, + user_name, + }); + } + + pub fn emit_session_disconnected_event(&self, connection_id: String, user_name: String) { + self.command(PlayerCommand::EmitSessionDisconnectedEvent { + connection_id, + user_name, + }); + } + + pub fn emit_session_client_changed_event( + &self, + client_id: String, + client_name: String, + client_brand_name: String, + client_model_name: String, + ) { + self.command(PlayerCommand::EmitSessionClientChangedEvent { + client_id, + client_name, + client_brand_name, + client_model_name, + }); + } + + pub fn emit_shuffle_changed_event(&self, shuffle: bool) { + self.command(PlayerCommand::EmitShuffleChangedEvent(shuffle)); + } + + pub fn emit_repeat_changed_event(&self, repeat: bool) { + self.command(PlayerCommand::EmitRepeatChangedEvent(repeat)); + } + + pub fn emit_auto_play_changed_event(&self, auto_play: bool) { + self.command(PlayerCommand::EmitAutoPlayChangedEvent(auto_play)); } } @@ -541,6 +621,7 @@ struct PlayerLoadedTrackData { decoder: Decoder, normalisation_data: NormalisationData, stream_loader_controller: StreamLoaderController, + audio_item: AudioItem, bytes_per_second: usize, duration_ms: u32, stream_position_ms: u32, @@ -573,6 +654,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, + audio_item: AudioItem, normalisation_data: NormalisationData, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, @@ -587,6 +669,7 @@ enum PlayerState { play_request_id: u64, decoder: Decoder, normalisation_data: NormalisationData, + audio_item: AudioItem, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, @@ -660,6 +743,7 @@ impl PlayerState { stream_loader_controller, stream_position_ms, is_explicit, + audio_item, .. } => { *self = EndOfTrack { @@ -669,6 +753,7 @@ impl PlayerState { decoder, normalisation_data, stream_loader_controller, + audio_item, bytes_per_second, duration_ms, stream_position_ms, @@ -694,6 +779,7 @@ impl PlayerState { track_id, play_request_id, decoder, + audio_item, normalisation_data, normalisation_factor, stream_loader_controller, @@ -707,13 +793,15 @@ impl PlayerState { track_id, play_request_id, decoder, + audio_item, normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, stream_position_ms, - reported_nominal_start_time: None, + reported_nominal_start_time: Instant::now() + .checked_sub(Duration::from_millis(stream_position_ms as u64)), suggested_to_preload_next_track, is_explicit, }; @@ -736,20 +824,22 @@ impl PlayerState { track_id, play_request_id, decoder, + audio_item, normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, bytes_per_second, stream_position_ms, - reported_nominal_start_time: _, suggested_to_preload_next_track, is_explicit, + .. } => { *self = Paused { track_id, play_request_id, decoder, + audio_item, normalisation_data, normalisation_factor, stream_loader_controller, @@ -777,13 +867,13 @@ struct PlayerTrackLoader { } impl PlayerTrackLoader { - async fn find_available_alternative(&self, audio: AudioItem) -> Option { - if let Err(e) = audio.availability { + async fn find_available_alternative(&self, audio_item: AudioItem) -> Option { + if let Err(e) = audio_item.availability { error!("Track is unavailable: {}", e); None - } else if !audio.files.is_empty() { - Some(audio) - } else if let Some(alternatives) = &audio.alternatives { + } else if !audio_item.files.is_empty() { + Some(audio_item) + } else if let Some(alternatives) = &audio_item.alternatives { let alternatives: FuturesUnordered<_> = alternatives .iter() .map(|alt_id| AudioItem::get_file(&self.session, *alt_id)) @@ -822,7 +912,7 @@ impl PlayerTrackLoader { spotify_id: SpotifyId, position_ms: u32, ) -> Option { - let audio = match AudioItem::get_file(&self.session, spotify_id).await { + let audio_item = match AudioItem::get_file(&self.session, spotify_id).await { Ok(audio) => match self.find_available_alternative(audio).await { Some(audio) => audio, None => { @@ -841,31 +931,9 @@ impl PlayerTrackLoader { info!( "Loading <{}> with Spotify URI <{}>", - audio.name, audio.spotify_uri + audio_item.name, audio_item.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; - } - } - } - - if audio.duration < 0 { - error!( - "Track duration for <{}> cannot be {}", - 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 let formats = match self.config.bitrate { Bitrate::Bitrate96 => [ @@ -900,13 +968,16 @@ impl PlayerTrackLoader { let (format, file_id) = match formats .iter() - .find_map(|format| match audio.files.get(format) { + .find_map(|format| match audio_item.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); + warn!( + "<{}> is not available in any supported format", + audio_item.name + ); return None; } }; @@ -1020,6 +1091,17 @@ impl PlayerTrackLoader { } }; + let duration_ms = audio_item.duration_ms; + // Don't try to seek past the track's duration. + // If the position is invalid just start from + // the beginning of the track. + let position_ms = if position_ms > duration_ms { + warn!("Invalid start position of {}ms exceeds track's duration of {}ms, starting track from the beginning", position_ms, duration_ms); + 0 + } else { + position_ms + }; + // 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 @@ -1038,12 +1120,15 @@ impl PlayerTrackLoader { // 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); + let is_explicit = audio_item.is_explicit; + + info!("<{}> ({} ms) loaded", audio_item.name, duration_ms); return Some(PlayerLoadedTrackData { decoder, normalisation_data, stream_loader_controller, + audio_item, bytes_per_second, duration_ms, stream_position_ms, @@ -1164,7 +1249,6 @@ impl Future for PlayerInternal { normalisation_factor, ref mut stream_position_ms, ref mut reported_nominal_start_time, - duration_ms, .. } = self.state { @@ -1226,11 +1310,10 @@ impl Future for PlayerInternal { if notify_about_position { *reported_nominal_start_time = now.checked_sub(new_stream_position); - self.send_event(PlayerEvent::Playing { - track_id, + self.send_event(PlayerEvent::PositionCorrection { play_request_id, + track_id, position_ms: new_stream_position_ms as u32, - duration_ms, }); } } @@ -1315,7 +1398,7 @@ impl PlayerInternal { Ok(()) => self.sink_status = SinkStatus::Running, Err(e) => { error!("{}", e); - exit(1); + self.handle_pause(); } } } @@ -1396,7 +1479,6 @@ impl PlayerInternal { track_id, play_request_id, stream_position_ms, - duration_ms, .. } = self.state { @@ -1405,7 +1487,6 @@ impl PlayerInternal { track_id, play_request_id, position_ms: stream_position_ms, - duration_ms, }); self.ensure_sink_running(); } else { @@ -1414,25 +1495,24 @@ impl PlayerInternal { } fn handle_pause(&mut self) { - if let PlayerState::Playing { - track_id, - play_request_id, - stream_position_ms, - duration_ms, - .. - } = self.state - { - self.state.playing_to_paused(); - - self.ensure_sink_stopped(false); - self.send_event(PlayerEvent::Paused { + match self.state { + PlayerState::Paused { .. } => self.ensure_sink_stopped(false), + PlayerState::Playing { track_id, play_request_id, - position_ms: stream_position_ms, - duration_ms, - }); - } else { - error!("Player::pause called from invalid state: {:?}", self.state); + stream_position_ms, + .. + } => { + self.state.playing_to_paused(); + + self.ensure_sink_stopped(false); + self.send_event(PlayerEvent::Paused { + track_id, + play_request_id, + position_ms: stream_position_ms, + }); + } + _ => error!("Player::pause called from invalid state: {:?}", self.state), } } @@ -1555,7 +1635,7 @@ impl PlayerInternal { if let Err(e) = self.sink.write(packet, &mut self.converter) { error!("{}", e); - exit(1); + self.handle_pause(); } } } @@ -1587,6 +1667,10 @@ impl PlayerInternal { loaded_track: PlayerLoadedTrackData, start_playback: bool, ) { + let audio_item = Box::new(loaded_track.audio_item.clone()); + + self.send_event(PlayerEvent::TrackChanged { audio_item }); + let position_ms = loaded_track.stream_position_ms; let mut config = self.config.clone(); @@ -1602,18 +1686,17 @@ impl PlayerInternal { if start_playback { self.ensure_sink_running(); - self.send_event(PlayerEvent::Playing { track_id, play_request_id, position_ms, - duration_ms: loaded_track.duration_ms, }); self.state = PlayerState::Playing { track_id, play_request_id, decoder: loaded_track.decoder, + audio_item: loaded_track.audio_item, normalisation_data: loaded_track.normalisation_data, normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, @@ -1632,6 +1715,7 @@ impl PlayerInternal { track_id, play_request_id, decoder: loaded_track.decoder, + audio_item: loaded_track.audio_item, normalisation_data: loaded_track.normalisation_data, normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, @@ -1646,7 +1730,6 @@ impl PlayerInternal { track_id, play_request_id, position_ms, - duration_ms: loaded_track.duration_ms, }); } } @@ -1661,38 +1744,12 @@ impl PlayerInternal { if !self.config.gapless { self.ensure_sink_stopped(play); } - // emit the correct player event - match self.state { - PlayerState::Playing { - track_id: old_track_id, - .. - } - | PlayerState::Paused { - track_id: old_track_id, - .. - } - | PlayerState::EndOfTrack { - track_id: old_track_id, - .. - } - | PlayerState::Loading { - track_id: old_track_id, - .. - } => self.send_event(PlayerEvent::Changed { - old_track_id, - new_track_id: track_id, - }), - PlayerState::Stopped => self.send_event(PlayerEvent::Started { - track_id, - play_request_id, - position_ms, - }), - PlayerState::Invalid { .. } => { - return Err(Error::internal(format!( - "Player::handle_command_load called from invalid state: {:?}", - self.state - ))); - } + + if matches!(self.state, PlayerState::Invalid { .. }) { + return Err(Error::internal(format!( + "Player::handle_command_load called from invalid state: {:?}", + self.state + ))); } // Now we check at different positions whether we already have a pre-loaded version @@ -1754,6 +1811,7 @@ impl PlayerInternal { if let PlayerState::Playing { stream_position_ms, decoder, + audio_item, stream_loader_controller, bytes_per_second, duration_ms, @@ -1764,6 +1822,7 @@ impl PlayerInternal { | PlayerState::Paused { stream_position_ms, decoder, + audio_item, stream_loader_controller, bytes_per_second, duration_ms, @@ -1776,6 +1835,7 @@ impl PlayerInternal { decoder, normalisation_data, stream_loader_controller, + audio_item, bytes_per_second, duration_ms, stream_position_ms, @@ -1925,14 +1985,24 @@ impl PlayerInternal { Ok(new_position_ms) => { if let PlayerState::Playing { ref mut stream_position_ms, + track_id, + play_request_id, .. } | PlayerState::Paused { ref mut stream_position_ms, + track_id, + play_request_id, .. } = self.state { *stream_position_ms = new_position_ms; + + self.send_event(PlayerEvent::Seeked { + play_request_id, + track_id, + position_ms: new_position_ms, + }); } } Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e), @@ -1945,35 +2015,12 @@ impl PlayerInternal { self.preload_data_before_playback()?; if let PlayerState::Playing { - track_id, - play_request_id, ref mut reported_nominal_start_time, - duration_ms, .. } = self.state { *reported_nominal_start_time = Instant::now().checked_sub(Duration::from_millis(position_ms as u64)); - self.send_event(PlayerEvent::Playing { - track_id, - play_request_id, - position_ms, - duration_ms, - }); - } - if let PlayerState::Paused { - track_id, - play_request_id, - duration_ms, - .. - } = self.state - { - self.send_event(PlayerEvent::Paused { - track_id, - play_request_id, - position_ms, - duration_ms, - }); } Ok(()) @@ -2003,34 +2050,78 @@ impl PlayerInternal { PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback, - PlayerCommand::EmitVolumeSetEvent(volume) => { - self.send_event(PlayerEvent::VolumeSet { volume }) + PlayerCommand::EmitVolumeChangedEvent(volume) => { + self.send_event(PlayerEvent::VolumeChanged { volume }) } + PlayerCommand::EmitRepeatChangedEvent(repeat) => { + self.send_event(PlayerEvent::RepeatChanged { repeat }) + } + + PlayerCommand::EmitShuffleChangedEvent(shuffle) => { + self.send_event(PlayerEvent::ShuffleChanged { shuffle }) + } + + PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => { + self.send_event(PlayerEvent::AutoPlayChanged { auto_play }) + } + + PlayerCommand::EmitSessionClientChangedEvent { + client_id, + client_name, + client_brand_name, + client_model_name, + } => self.send_event(PlayerEvent::SessionClientChanged { + client_id, + client_name, + client_brand_name, + client_model_name, + }), + + PlayerCommand::EmitSessionConnectedEvent { + connection_id, + user_name, + } => self.send_event(PlayerEvent::SessionConnected { + connection_id, + user_name, + }), + + PlayerCommand::EmitSessionDisconnectedEvent { + connection_id, + user_name, + } => self.send_event(PlayerEvent::SessionDisconnected { + connection_id, + user_name, + }), + 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, - }) + PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => { + self.send_event(PlayerEvent::FilterExplicitContentChanged { filter }); + + if filter { + 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, + }) + } } } } @@ -2133,7 +2224,7 @@ impl Drop for PlayerInternal { impl fmt::Debug for PlayerCommand { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { + match self { PlayerCommand::Load { track_id, play, @@ -2156,14 +2247,58 @@ impl fmt::Debug for PlayerCommand { PlayerCommand::SetSinkEventCallback(_) => { f.debug_tuple("SetSinkEventCallback").finish() } - PlayerCommand::EmitVolumeSetEvent(volume) => { - f.debug_tuple("VolumeSet").field(&volume).finish() - } + PlayerCommand::EmitVolumeChangedEvent(volume) => f + .debug_tuple("EmitVolumeChangedEvent") + .field(&volume) + .finish(), PlayerCommand::SetAutoNormaliseAsAlbum(setting) => f .debug_tuple("SetAutoNormaliseAsAlbum") .field(&setting) .finish(), - PlayerCommand::SkipExplicitContent() => f.debug_tuple("SkipExplicitContent").finish(), + PlayerCommand::EmitFilterExplicitContentChangedEvent(filter) => f + .debug_tuple("EmitFilterExplicitContentChangedEvent") + .field(&filter) + .finish(), + PlayerCommand::EmitSessionConnectedEvent { + connection_id, + user_name, + } => f + .debug_tuple("EmitSessionConnectedEvent") + .field(&connection_id) + .field(&user_name) + .finish(), + PlayerCommand::EmitSessionDisconnectedEvent { + connection_id, + user_name, + } => f + .debug_tuple("EmitSessionDisconnectedEvent") + .field(&connection_id) + .field(&user_name) + .finish(), + PlayerCommand::EmitSessionClientChangedEvent { + client_id, + client_name, + client_brand_name, + client_model_name, + } => f + .debug_tuple("EmitSessionClientChangedEvent") + .field(&client_id) + .field(&client_name) + .field(&client_brand_name) + .field(&client_model_name) + .finish(), + PlayerCommand::EmitShuffleChangedEvent(shuffle) => f + .debug_tuple("EmitShuffleChangedEvent") + .field(&shuffle) + .finish(), + PlayerCommand::EmitRepeatChangedEvent(repeat) => f + .debug_tuple("EmitRepeatChangedEvent") + .field(&repeat) + .finish(), + PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => f + .debug_tuple("EmitAutoPlayChangedEvent") + .field(&auto_play) + .finish(), } } } diff --git a/src/main.rs b/src/main.rs index e1af4c10..aac7119a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,6 @@ use futures_util::StreamExt; use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; -use tokio::sync::mpsc::UnboundedReceiver; use url::Url; use librespot::{ @@ -29,7 +28,7 @@ use librespot::{ }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{coefficient_to_duration, duration_to_coefficient, Player, PlayerEvent}, + player::{coefficient_to_duration, duration_to_coefficient, Player}, }, }; @@ -37,7 +36,7 @@ use librespot::{ use librespot::playback::mixer::alsamixer::AlsaMixer; mod player_event_handler; -use player_event_handler::{emit_sink_event, run_program_on_events}; +use player_event_handler::{run_program_on_sink_events, EventHandler}; fn device_id(name: &str) -> String { hex::encode(Sha1::digest(name.as_bytes())) @@ -1598,10 +1597,10 @@ async fn main() { let mut last_credentials = None; let mut spirc: Option = None; let mut spirc_task: Option> = None; - let mut player_event_channel: Option> = None; let mut auto_connect_times: Vec = vec![]; let mut discovery = None; let mut connecting = false; + let mut _event_handler: Option = None; let session = Session::new(setup.session_config.clone(), setup.cache.clone()); @@ -1669,32 +1668,21 @@ async fn main() { let format = setup.format; let backend = setup.backend; let device = setup.device.clone(); - let (player, event_channel) = - Player::new(player_config, session.clone(), soft_volume, move || { - (backend)(device, format) - }); + let player = Player::new(player_config, session.clone(), soft_volume, move || { + (backend)(device, format) + }); - if setup.emit_sink_events { - if let Some(player_event_program) = setup.player_event_program.clone() { + if let Some(player_event_program) = setup.player_event_program.clone() { + _event_handler = Some(EventHandler::new(player.get_player_event_channel(), &player_event_program)); + + if setup.emit_sink_events { 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); - }, - } + run_program_on_sink_events(sink_status, &player_event_program) }))); } }; - let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap(), player, mixer).await { + let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap_or_default(), player, mixer).await { Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_), Err(e) => { error!("could not initialize spirc: {}", e); @@ -1703,7 +1691,6 @@ async fn main() { }; spirc = Some(spirc_); spirc_task = Some(Box::pin(spirc_task_)); - player_event_channel = Some(event_channel); connecting = false; }, @@ -1732,41 +1719,6 @@ async fn main() { }, } }, - event = async { - match player_event_channel.as_mut() { - Some(p) => p.recv().await, - _ => None - } - }, if player_event_channel.is_some() => match event { - Some(event) => { - if let Some(program) = &setup.player_event_program { - if let Some(child) = run_program_on_events(event, program) { - if let Ok(mut child) = child { - 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); - }, - } - }); - } else { - warn!("On event program failed to start"); - } - } - } - }, - None => { - player_event_channel = None; - } - }, _ = tokio::signal::ctrl_c() => { break; }, diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 99b1645d..44d92eb5 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -1,148 +1,307 @@ -use log::info; +use log::{debug, error, warn}; -use std::{ - collections::HashMap, - io::{Error, ErrorKind, Result}, - process::{Command, ExitStatus}, +use std::{collections::HashMap, process::Command, thread}; + +use librespot::{ + metadata::audio::UniqueFields, + playback::player::{PlayerEvent, PlayerEventChannel, SinkStatus}, }; -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> { - let mut env_vars = HashMap::new(); - match event { - PlayerEvent::Changed { - old_track_id, - new_track_id, - } => match old_track_id.to_base62() { - Err(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Changed: Invalid old track id: {}", e), - ))) - } - Ok(old_id) => match new_track_id.to_base62() { - Err(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Changed: Invalid old track id: {}", e), - ))) - } - 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(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Started: Invalid track id: {}", e), - ))) - } - 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(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Stopped: Invalid track id: {}", e), - ))) - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "stopped".to_string()); - env_vars.insert("TRACK_ID", id); - } - }, - PlayerEvent::Playing { - track_id, - duration_ms, - position_ms, - .. - } => match track_id.to_base62() { - Err(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Playing: Invalid track id: {}", e), - ))) - } - 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, - .. - } => match track_id.to_base62() { - Err(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Paused: Invalid track id: {}", e), - ))) - } - 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(e) => { - return Some(Err(Error::new( - ErrorKind::InvalidData, - format!("PlayerEvent::Preloading: Invalid track id: {}", e), - ))) - } - 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()); - } - _ => return None, - } - - let mut v: Vec<&str> = onevent.split_whitespace().collect(); - info!("Running {:?} with environment variables {:?}", v, env_vars); - Some( - AsyncCommand::new(&v.remove(0)) - .args(&v) - .envs(env_vars.iter()) - .spawn(), - ) +pub struct EventHandler { + thread_handle: Option>, } -pub fn emit_sink_event(sink_status: SinkStatus, onevent: &str) -> Result { +impl EventHandler { + pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self { + let on_event = onevent.to_string(); + let thread_handle = Some(thread::spawn(move || loop { + match player_events.blocking_recv() { + None => break, + Some(event) => { + let mut env_vars = HashMap::new(); + + match event { + PlayerEvent::TrackChanged { audio_item } => { + match audio_item.track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::TrackChanged: Invalid track id: {}", e) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "track_changed".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("URI", audio_item.uri); + env_vars.insert("NAME", audio_item.name); + env_vars.insert( + "COVERS", + audio_item + .covers + .into_iter() + .map(|c| c.url) + .collect::>() + .join("\n"), + ); + env_vars.insert("LANGUAGE", audio_item.language.join("\n")); + env_vars + .insert("DURATION_MS", audio_item.duration_ms.to_string()); + env_vars + .insert("IS_EXPLICIT", audio_item.is_explicit.to_string()); + + match audio_item.unique_fields { + UniqueFields::Track { + artists, + album, + album_artists, + popularity, + number, + disc_number, + } => { + env_vars.insert("ITEM_TYPE", "Track".to_string()); + env_vars.insert( + "ARTISTS", + artists + .0 + .into_iter() + .map(|a| a.name) + .collect::>() + .join("\n"), + ); + env_vars + .insert("ALBUM_ARTISTS", album_artists.join("\n")); + env_vars.insert("ALBUM", album); + env_vars.insert("POPULARITY", popularity.to_string()); + env_vars.insert("NUMBER", number.to_string()); + env_vars.insert("DISC_NUMBER", disc_number.to_string()); + } + UniqueFields::Episode { + description, + publish_time, + show_name, + } => { + env_vars.insert("ITEM_TYPE", "Episode".to_string()); + env_vars.insert("DESCRIPTION", description); + env_vars.insert( + "PUBLISH_TIME", + publish_time.unix_timestamp().to_string(), + ); + env_vars.insert("SHOW_NAME", show_name); + } + } + } + } + } + PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "stopped".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Playing { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "playing".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Paused { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "paused".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "loading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { + match track_id.to_base62() { + Err(e) => warn!( + "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {}", + e + ), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preload_next".to_string()); + env_vars.insert("TRACK_ID", id); + } + } + } + PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::VolumeChanged { volume } => { + env_vars.insert("PLAYER_EVENT", "volume_changed".to_string()); + env_vars.insert("VOLUME", volume.to_string()); + } + PlayerEvent::Seeked { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {}", e), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "seeked".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::PositionCorrection { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::PositionCorrection: Invalid track id: {}", e) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "position_correction".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::SessionConnected { + connection_id, + user_name, + } => { + env_vars.insert("PLAYER_EVENT", "session_connected".to_string()); + env_vars.insert("CONNECTION_ID", connection_id); + env_vars.insert("USER_NAME", user_name); + } + PlayerEvent::SessionDisconnected { + connection_id, + user_name, + } => { + env_vars.insert("PLAYER_EVENT", "session_disconnected".to_string()); + env_vars.insert("CONNECTION_ID", connection_id); + env_vars.insert("USER_NAME", user_name); + } + PlayerEvent::SessionClientChanged { + client_id, + client_name, + client_brand_name, + client_model_name, + } => { + env_vars.insert("PLAYER_EVENT", "session_client_changed".to_string()); + env_vars.insert("CLIENT_ID", client_id); + env_vars.insert("CLIENT_NAME", client_name); + env_vars.insert("CLIENT_BRAND_NAME", client_brand_name); + env_vars.insert("CLIENT_MODEL_NAME", client_model_name); + } + PlayerEvent::ShuffleChanged { shuffle } => { + env_vars.insert("PLAYER_EVENT", "shuffle_changed".to_string()); + env_vars.insert("SHUFFLE", shuffle.to_string()); + } + PlayerEvent::RepeatChanged { repeat } => { + env_vars.insert("PLAYER_EVENT", "repeat_changed".to_string()); + env_vars.insert("REPEAT", repeat.to_string()); + } + PlayerEvent::AutoPlayChanged { auto_play } => { + env_vars.insert("PLAYER_EVENT", "auto_play_changed".to_string()); + env_vars.insert("AUTO_PLAY", auto_play.to_string()); + } + + PlayerEvent::FilterExplicitContentChanged { filter } => { + env_vars.insert( + "PLAYER_EVENT", + "filter_explicit_content_changed".to_string(), + ); + env_vars.insert("FILTER", filter.to_string()); + } + } + + if !env_vars.is_empty() { + run_program(env_vars, &on_event); + } + } + } + })); + + Self { thread_handle } + } +} + +impl Drop for EventHandler { + fn drop(&mut self) { + debug!("Shutting down EventHandler thread ..."); + if let Some(handle) = self.thread_handle.take() { + if let Err(e) = handle.join() { + error!("EventHandler thread Error: {:?}", e); + } + } + } +} + +pub fn run_program_on_sink_events(sink_status: SinkStatus, onevent: &str) { let mut env_vars = HashMap::new(); + env_vars.insert("PLAYER_EVENT", "sink".to_string()); + let sink_status = match sink_status { SinkStatus::Running => "running", SinkStatus::TemporarilyClosed => "temporarily_closed", SinkStatus::Closed => "closed", }; - env_vars.insert("SINK_STATUS", sink_status.to_string()); - let mut v: Vec<&str> = onevent.split_whitespace().collect(); - info!("Running {:?} with environment variables {:?}", v, env_vars); - Command::new(&v.remove(0)) + env_vars.insert("SINK_STATUS", sink_status.to_string()); + + run_program(env_vars, onevent); +} + +fn run_program(env_vars: HashMap<&str, String>, onevent: &str) { + let mut v: Vec<&str> = onevent.split_whitespace().collect(); + + debug!( + "Running {} with environment variables:\n{:#?}", + onevent, env_vars + ); + + match Command::new(&v.remove(0)) .args(&v) .envs(env_vars.iter()) - .spawn()? - .wait() + .spawn() + { + Err(e) => warn!("On event program {} failed to start: {}", onevent, e), + Ok(mut child) => match child.wait() { + Err(e) => warn!("On event program {} failed: {}", onevent, e), + Ok(e) if e.success() => (), + Ok(e) => { + if let Some(code) = e.code() { + warn!("On event program {} returned exit code {}", onevent, code); + } else { + warn!("On event program {} returned failure: {}", onevent, e); + } + } + }, + } } From d446258be5804affb81892cc48480299ecf807c6 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 22 Sep 2022 16:41:03 -0500 Subject: [PATCH 260/561] Fix clippy lint --- core/src/token.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/core/src/token.rs b/core/src/token.rs index bea37074..946367c3 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -54,12 +54,7 @@ struct TokenData { impl TokenProvider { 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 + (0..inner.tokens.len()).find(|&i| inner.tokens[i].in_scopes(scopes.clone())) }) } From eb1472c71337c58f8b28d99f2bfd714979e5a7ab Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 28 Sep 2022 21:25:56 +0200 Subject: [PATCH 261/561] Various loading improvements - Improve responsiveness by downloading the smallest possible chunk size when seeking or first loading. - Improve download time and decrease CPU usage by downloading the largest possible chunk size as throughput allows, still allowing for reasonable seek responsiveness (~1 second). - As a result, take refactoring opportunities: simplify prefetching logic, download threading, command sending, and some ergonomics. - Fix disappearing controls in the Spotify mobile UI while loading. - Fix handling of seek, pause, and play commands while loading. - Fix download rate calculation (don't use the Mercury rate). - Fix ping time calculation under lock contention. --- audio/src/fetch/mod.rs | 185 +++++++++++--------------- audio/src/fetch/receive.rs | 262 ++++++++++++++++++++----------------- audio/src/lib.rs | 5 +- connect/src/spirc.rs | 18 ++- core/src/channel.rs | 10 +- core/src/error.rs | 22 +++- playback/src/player.rs | 82 +++++++----- 7 files changed, 305 insertions(+), 279 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 30b8d859..e343ee1f 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -1,14 +1,14 @@ mod receive; use std::{ - cmp::{max, min}, + cmp::min, fs, io::{self, Read, Seek, SeekFrom}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, }, - time::{Duration, Instant}, + time::Duration, }; use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; @@ -16,7 +16,7 @@ use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, Statu use parking_lot::{Condvar, Mutex}; use tempfile::NamedTempFile; use thiserror::Error; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::{mpsc, oneshot, Semaphore}; use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session}; @@ -59,17 +59,11 @@ impl From for Error { /// 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; +pub const MINIMUM_DOWNLOAD_SIZE: usize = 64 * 1024; /// 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 -/// another position, then only this amount is requested on the first request. -pub const INITIAL_DOWNLOAD_SIZE: usize = 1024 * 8; +pub const MINIMUM_THROUGHPUT: usize = 8 * 1024; /// 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); @@ -83,45 +77,17 @@ pub const MAXIMUM_ASSUMED_PING_TIME: Duration = Duration::from_millis(1500); /// of audio data may be larger or smaller. pub const READ_AHEAD_BEFORE_PLAYBACK: Duration = Duration::from_secs(1); -/// 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; - /// 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); -/// 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; - /// 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 * * ` 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 * * ` -/// 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. -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, 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. -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((MINIMUM_DOWNLOAD_SIZE / MINIMUM_THROUGHPUT) as u64); @@ -137,15 +103,12 @@ pub struct StreamingRequest { initial_response: 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 - Close, // terminate and don't load any more data + Fetch(Range), // signal the stream loader to fetch a range of the file + Close, // terminate and don't load any more data } #[derive(Clone)] @@ -182,17 +145,15 @@ impl StreamLoaderController { pub fn range_to_end_available(&self) -> bool { match self.stream_shared { Some(ref shared) => { - let read_position = shared.read_position.load(Ordering::Acquire); + let read_position = shared.read_position(); self.range_available(Range::new(read_position, self.len() - read_position)) } None => true, } } - pub fn ping_time(&self) -> Duration { - Duration::from_millis(self.stream_shared.as_ref().map_or(0, |shared| { - shared.ping_time_ms.load(Ordering::Relaxed) as u64 - })) + pub fn ping_time(&self) -> Option { + self.stream_shared.as_ref().map(|shared| shared.ping_time()) } fn send_stream_loader_command(&self, command: StreamLoaderCommand) { @@ -252,31 +213,6 @@ 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(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(Ordering::Acquire), - length, - }; - self.fetch_blocking(range) - } - None => Ok(()), - } - } - pub fn fetch_next_and_wait( &self, request_length: usize, @@ -284,7 +220,7 @@ impl StreamLoaderController { ) -> AudioFileResult { match self.stream_shared { Some(ref shared) => { - let start = shared.read_position.load(Ordering::Acquire); + let start = shared.read_position(); let request_range = Range { start, @@ -304,12 +240,16 @@ impl StreamLoaderController { pub fn set_random_access_mode(&self) { // optimise download strategy for random access - self.send_stream_loader_command(StreamLoaderCommand::RandomAccessMode); + if let Some(ref shared) = self.stream_shared { + shared.set_download_streaming(false) + } } pub fn set_stream_mode(&self) { // optimise download strategy for streaming - self.send_stream_loader_command(StreamLoaderCommand::StreamMode); + if let Some(ref shared) = self.stream_shared { + shared.set_download_streaming(true) + } } pub fn close(&self) { @@ -337,9 +277,51 @@ struct AudioFileShared { cond: Condvar, download_status: Mutex, download_streaming: AtomicBool, - number_of_open_requests: AtomicUsize, + download_slots: Semaphore, ping_time_ms: AtomicUsize, read_position: AtomicUsize, + throughput: AtomicUsize, +} + +impl AudioFileShared { + fn is_download_streaming(&self) -> bool { + self.download_streaming.load(Ordering::Acquire) + } + + fn set_download_streaming(&self, streaming: bool) { + self.download_streaming.store(streaming, Ordering::Release) + } + + fn ping_time(&self) -> Duration { + let ping_time_ms = self.ping_time_ms.load(Ordering::Acquire); + if ping_time_ms > 0 { + Duration::from_millis(ping_time_ms as u64) + } else { + INITIAL_PING_TIME_ESTIMATE + } + } + + fn set_ping_time(&self, duration: Duration) { + self.ping_time_ms + .store(duration.as_millis() as usize, Ordering::Release) + } + + fn throughput(&self) -> usize { + self.throughput.load(Ordering::Acquire) + } + + fn set_throughput(&self, throughput: usize) { + self.throughput.store(throughput, Ordering::Release) + } + + fn read_position(&self) -> usize { + self.read_position.load(Ordering::Acquire) + } + + fn set_read_position(&self, position: u64) { + self.read_position + .store(position as usize, Ordering::Release) + } } impl AudioFile { @@ -420,12 +402,11 @@ impl AudioFileStreaming { let mut streamer = session .spclient() - .stream_from_cdn(&cdn_url, 0, INITIAL_DOWNLOAD_SIZE)?; + .stream_from_cdn(&cdn_url, 0, MINIMUM_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 code = response.status(); @@ -452,7 +433,6 @@ impl AudioFileStreaming { initial_response: Some(response), offset: 0, length: upper_bound + 1, - request_time, }; let shared = Arc::new(AudioFileShared { @@ -464,10 +444,11 @@ impl AudioFileStreaming { requested: RangeSet::new(), downloaded: RangeSet::new(), }), - 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), + download_streaming: AtomicBool::new(false), + download_slots: Semaphore::new(1), + ping_time_ms: AtomicUsize::new(0), read_position: AtomicUsize::new(0), + throughput: AtomicUsize::new(0), }); let write_file = NamedTempFile::new_in(session.config().tmp_dir.clone())?; @@ -509,20 +490,12 @@ impl Read for AudioFileStreaming { return Ok(0); } - 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 = if self.shared.is_download_streaming() { 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, - ); + + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * self.shared.bytes_per_second as f32) + as usize; + + // Due to the read-ahead stuff, we potentially request more than the actual request demanded. min(length_to_request, self.shared.file_size - offset) } else { length @@ -566,9 +539,7 @@ impl Read for AudioFileStreaming { let read_len = self.read_file.read(&mut output[..read_len])?; self.position += read_len as u64; - self.shared - .read_position - .store(self.position as usize, Ordering::Release); + self.shared.set_read_position(self.position); Ok(read_len) } @@ -601,23 +572,17 @@ impl Seek for AudioFileStreaming { // Ensure random access mode if we need to download this part. // Checking whether we are streaming now is a micro-optimization // to save an atomic load. - was_streaming = self.shared.download_streaming.load(Ordering::Acquire); + was_streaming = self.shared.is_download_streaming(); if was_streaming { - self.shared - .download_streaming - .store(false, Ordering::Release); + self.shared.set_download_streaming(false); } } self.position = self.read_file.seek(pos)?; - self.shared - .read_position - .store(self.position as usize, Ordering::Release); + self.shared.set_read_position(self.position); if !available && was_streaming { - self.shared - .download_streaming - .store(true, Ordering::Release); + self.shared.set_download_streaming(true); } Ok(self.position) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 2c58fbf8..d090d547 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -1,7 +1,7 @@ use std::{ cmp::{max, min}, io::{Seek, SeekFrom, Write}, - sync::{atomic::Ordering, Arc}, + sync::Arc, time::{Duration, Instant}, }; @@ -17,8 +17,8 @@ use crate::range_set::{Range, RangeSet}; use super::{ AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, StreamingRequest, - FAST_PREFETCH_THRESHOLD_FACTOR, MAXIMUM_ASSUMED_PING_TIME, MAX_PREFETCH_REQUESTS, - MINIMUM_DOWNLOAD_SIZE, PREFETCH_THRESHOLD_FACTOR, + MAXIMUM_ASSUMED_PING_TIME, MINIMUM_DOWNLOAD_SIZE, MINIMUM_THROUGHPUT, + PREFETCH_THRESHOLD_FACTOR, }; struct PartialFileData { @@ -27,10 +27,13 @@ struct PartialFileData { } enum ReceivedData { + Throughput(usize), ResponseTime(Duration), Data(PartialFileData), } +const ONE_SECOND: Duration = Duration::from_secs(1); + async fn receive_data( shared: Arc, file_data_tx: mpsc::UnboundedSender, @@ -39,15 +42,21 @@ async fn receive_data( let mut offset = request.offset; let mut actual_length = 0; - let old_number_of_request = shared - .number_of_open_requests - .fetch_add(1, Ordering::SeqCst); + let permit = shared.download_slots.acquire().await?; - let mut measure_ping_time = old_number_of_request == 0; + let request_time = Instant::now(); + let mut measure_ping_time = true; + let mut measure_throughput = true; let result: Result<_, Error> = loop { let response = match request.initial_response.take() { - Some(data) => data, + Some(data) => { + // the request was already made outside of this function + measure_ping_time = false; + measure_throughput = false; + + data + } None => match request.streamer.next().await { Some(Ok(response)) => response, Some(Err(e)) => break Err(e.into()), @@ -62,6 +71,15 @@ async fn receive_data( }, }; + if measure_ping_time { + let duration = Instant::now().duration_since(request_time); + // may be zero if we are handling an initial response + if duration.as_millis() > 0 { + file_data_tx.send(ReceivedData::ResponseTime(duration))?; + measure_ping_time = false; + } + } + let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { if code == StatusCode::TOO_MANY_REQUESTS { @@ -90,24 +108,18 @@ async fn receive_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 { - 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))?; - measure_ping_time = false; - } }; drop(request.streamer); + if measure_throughput { + let duration = Instant::now().duration_since(request_time).as_millis(); + if actual_length > 0 && duration > 0 { + let throughput = ONE_SECOND.as_millis() as usize * actual_length / duration as usize; + file_data_tx.send(ReceivedData::Throughput(throughput))?; + } + } + let bytes_remaining = request.length - actual_length; if bytes_remaining > 0 { { @@ -118,9 +130,7 @@ async fn receive_data( } } - shared - .number_of_open_requests - .fetch_sub(1, Ordering::SeqCst); + drop(permit); if let Err(e) = result { error!( @@ -151,8 +161,8 @@ enum ControlFlow { } impl AudioFileFetch { - fn is_download_streaming(&self) -> bool { - self.shared.download_streaming.load(Ordering::Acquire) + fn has_download_slots_available(&self) -> bool { + self.shared.download_slots.available_permits() > 0 } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { @@ -160,10 +170,17 @@ impl AudioFileFetch { length = MINIMUM_DOWNLOAD_SIZE; } + // If we are in streaming mode (so not seeking) then start downloading as large + // of chunks as possible for better throughput and improved CPU usage, while + // still being reasonably responsive (~1 second) in case we want to seek. + if self.shared.is_download_streaming() { + let throughput = self.shared.throughput(); + length = max(length, throughput); + } + if offset + length > self.shared.file_size { length = self.shared.file_size - offset; } - let mut ranges_to_request = RangeSet::new(); ranges_to_request.add_range(&Range::new(offset, length)); @@ -191,7 +208,6 @@ impl AudioFileFetch { initial_response: None, offset: range.start, length: range.length, - request_time: Instant::now(), }; self.session.spawn(receive_data( @@ -204,51 +220,36 @@ impl AudioFileFetch { Ok(()) } - 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; + fn pre_fetch_more_data(&mut self, bytes: usize) -> AudioFileResult { + // determine what is still missing + 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(); + missing_data.subtract_range_set(&download_status.downloaded); + missing_data.subtract_range_set(&download_status.requested); + } - while bytes_to_go > 0 && requests_to_go > 0 { - // determine what is still missing - 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(); - missing_data.subtract_range_set(&download_status.downloaded); - missing_data.subtract_range_set(&download_status.requested); - } + // download data from after the current read position first + let mut tail_end = RangeSet::new(); + let read_position = self.shared.read_position(); + tail_end.add_range(&Range::new( + read_position, + self.shared.file_size - read_position, + )); + let tail_end = tail_end.intersection(&missing_data); - // download data from after the current read position first - let mut tail_end = RangeSet::new(); - let read_position = self.shared.read_position.load(Ordering::Acquire); - tail_end.add_range(&Range::new( - read_position, - self.shared.file_size - read_position, - )); - let tail_end = tail_end.intersection(&missing_data); - - if !tail_end.is_empty() { - let range = tail_end.get_range(0); - let offset = range.start; - let length = min(range.length, bytes_to_go); - self.download_range(offset, length)?; - requests_to_go -= 1; - bytes_to_go -= length; - } else if !missing_data.is_empty() { - // ok, the tail is downloaded, download something fom the beginning. - let range = missing_data.get_range(0); - let offset = range.start; - let length = min(range.length, bytes_to_go); - self.download_range(offset, length)?; - requests_to_go -= 1; - bytes_to_go -= length; - } else { - break; - } + if !tail_end.is_empty() { + let range = tail_end.get_range(0); + let offset = range.start; + let length = min(range.length, bytes); + self.download_range(offset, length)?; + } else if !missing_data.is_empty() { + // ok, the tail is downloaded, download something fom the beginning. + let range = missing_data.get_range(0); + let offset = range.start; + let length = min(range.length, bytes); + self.download_range(offset, length)?; } Ok(()) @@ -256,8 +257,46 @@ 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); + ReceivedData::Throughput(mut throughput) => { + if throughput < MINIMUM_THROUGHPUT { + warn!( + "Throughput {} kbps lower than minimum {}, setting to minimum", + throughput / 1000, + MINIMUM_THROUGHPUT / 1000, + ); + throughput = MINIMUM_THROUGHPUT; + } + + let old_throughput = self.shared.throughput(); + let avg_throughput = if old_throughput > 0 { + (old_throughput + throughput) / 2 + } else { + throughput + }; + + // print when the new estimate deviates by more than 10% from the last + if f32::abs((avg_throughput as f32 - old_throughput as f32) / old_throughput as f32) + > 0.1 + { + trace!( + "Throughput now estimated as: {} kbps", + avg_throughput / 1000 + ); + } + + self.shared.set_throughput(avg_throughput); + } + ReceivedData::ResponseTime(mut response_time) => { + if response_time > MAXIMUM_ASSUMED_PING_TIME { + warn!( + "Time to first byte {} ms exceeds maximum {}, setting to maximum", + response_time.as_millis(), + MAXIMUM_ASSUMED_PING_TIME.as_millis() + ); + response_time = MAXIMUM_ASSUMED_PING_TIME; + } + + let old_ping_time_ms = self.shared.ping_time().as_millis(); // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { @@ -268,8 +307,8 @@ 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_ms = { - let response_time = match self.network_response_times.len() { + 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 => { @@ -278,22 +317,23 @@ impl AudioFileFetch { times[1] } _ => unreachable!(), - }; - response_time.as_millis() as usize + } }; // 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, + (ping_time.as_millis() 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); + trace!( + "Time to first byte now estimated as: {} ms", + ping_time.as_millis() + ); } // store our new estimate for everyone to see - self.shared - .ping_time_ms - .store(ping_time_ms, Ordering::Relaxed); + self.shared.set_ping_time(ping_time); } ReceivedData::Data(data) => { match self.output.as_mut() { @@ -333,14 +373,6 @@ impl AudioFileFetch { StreamLoaderCommand::Fetch(request) => { self.download_range(request.start, request.length)? } - 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), } @@ -426,40 +458,28 @@ pub(super) async fn audio_file_fetch( else => (), } - 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 { - let max_requests_to_send = MAX_PREFETCH_REQUESTS - number_of_open_requests; + if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() { + let bytes_pending: usize = { + let download_status = fetch.shared.download_status.lock(); - let bytes_pending: usize = { - let download_status = fetch.shared.download_status.lock(); + download_status + .requested + .minus(&download_status.downloaded) + .len() + }; - download_status - .requested - .minus(&download_status.downloaded) - .len() - }; + let ping_time_seconds = fetch.shared.ping_time().as_secs_f32(); + let throughput = fetch.shared.throughput(); - let ping_time_seconds = - 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.bytes_per_second as f32) as usize, + (ping_time_seconds * throughput as f32) as usize, + ); - let desired_pending_bytes = max( - (PREFETCH_THRESHOLD_FACTOR - * ping_time_seconds - * fetch.shared.bytes_per_second as f32) as usize, - (FAST_PREFETCH_THRESHOLD_FACTOR * ping_time_seconds * download_rate as f32) - as usize, - ); - - if bytes_pending < desired_pending_bytes { - fetch.pre_fetch_more_data( - desired_pending_bytes - bytes_pending, - max_requests_to_send, - )?; - } + if bytes_pending < desired_pending_bytes { + fetch.pre_fetch_more_data(desired_pending_bytes - bytes_pending)?; } } } diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 22bf2f0a..2a53c361 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -8,7 +8,4 @@ mod range_set; pub use decrypt::AudioDecrypt; pub use fetch::{AudioFile, AudioFileError, StreamLoaderController}; -pub use fetch::{ - MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, - READ_AHEAD_DURING_PLAYBACK, READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, -}; +pub use fetch::{MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_DURING_PLAYBACK}; diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 616a44e5..a9144568 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1502,13 +1502,17 @@ impl SpircTask { } fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> { - let status_string = match self.state.get_status() { - PlayStatus::kPlayStatusLoading => "kPlayStatusLoading", - PlayStatus::kPlayStatusPause => "kPlayStatusPause", - PlayStatus::kPlayStatusStop => "kPlayStatusStop", - PlayStatus::kPlayStatusPlay => "kPlayStatusPlay", - }; - trace!("Sending status to server: [{}]", status_string); + let status = self.state.get_status(); + + // When in loading state, the Spotify UI is disabled for interaction. + // On desktop this isn't so bad but on mobile it means that the bottom + // control disappears entirely. This is very confusing, so don't notify + // in this case. + if status == PlayStatus::kPlayStatusLoading { + return Ok(()); + } + + trace!("Sending status to server: [{:?}]", status); let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); if let Some(s) = recipient { cs = cs.recipient(s); diff --git a/core/src/channel.rs b/core/src/channel.rs index c601cd7a..86909978 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -3,7 +3,7 @@ use std::{ fmt, pin::Pin, task::{Context, Poll}, - time::Instant, + time::{Duration, Instant}, }; use byteorder::{BigEndian, ByteOrder}; @@ -27,7 +27,7 @@ component! { } } -const ONE_SECOND_IN_MS: usize = 1000; +const ONE_SECOND: Duration = Duration::from_secs(1); #[derive(Debug, Error, Hash, PartialEq, Eq, Copy, Clone)] pub struct ChannelError; @@ -92,10 +92,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 + if (current_time - download_measurement_start) > ONE_SECOND { + inner.download_rate_estimate = ONE_SECOND.as_millis() as usize * inner.download_measurement_bytes / (current_time - download_measurement_start).as_millis() as usize; inner.download_measurement_start = Some(current_time); diff --git a/core/src/error.rs b/core/src/error.rs index 700aed84..87cf3c86 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -14,7 +14,9 @@ use http::{ }; use protobuf::ProtobufError; use thiserror::Error; -use tokio::sync::{mpsc::error::SendError, oneshot::error::RecvError}; +use tokio::sync::{ + mpsc::error::SendError, oneshot::error::RecvError, AcquireError, TryAcquireError, +}; use url::ParseError; #[cfg(feature = "with-dns-sd")] @@ -451,6 +453,24 @@ impl From> for Error { } } +impl From for Error { + fn from(err: AcquireError) -> Self { + Self { + kind: ErrorKind::ResourceExhausted, + error: ErrorMessage(err.to_string()).into(), + } + } +} + +impl From for Error { + fn from(err: TryAcquireError) -> Self { + Self { + kind: ErrorKind::ResourceExhausted, + error: ErrorMessage(err.to_string()).into(), + } + } +} + impl From for Error { fn from(err: ToStrError) -> Self { Self::new(ErrorKind::FailedPrecondition, err) diff --git a/playback/src/player.rs b/playback/src/player.rs index cd08197c..f0f0d492 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1,5 +1,4 @@ use std::{ - cmp::max, collections::HashMap, fmt, future::Future, @@ -28,8 +27,7 @@ use tokio::sync::{mpsc, oneshot}; use crate::{ audio::{ AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK, - READ_AHEAD_BEFORE_PLAYBACK_ROUNDTRIPS, READ_AHEAD_DURING_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK_ROUNDTRIPS, + READ_AHEAD_DURING_PLAYBACK, }, audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, @@ -1096,7 +1094,7 @@ impl PlayerTrackLoader { // If the position is invalid just start from // the beginning of the track. let position_ms = if position_ms > duration_ms { - warn!("Invalid start position of {}ms exceeds track's duration of {}ms, starting track from the beginning", position_ms, duration_ms); + warn!("Invalid start position of {} ms exceeds track's duration of {} ms, starting track from the beginning", position_ms, duration_ms); 0 } else { position_ms @@ -1475,22 +1473,28 @@ impl PlayerInternal { } fn handle_play(&mut self) { - if let PlayerState::Paused { - track_id, - play_request_id, - stream_position_ms, - .. - } = self.state - { - self.state.paused_to_playing(); - self.send_event(PlayerEvent::Playing { + match self.state { + PlayerState::Paused { track_id, play_request_id, - position_ms: stream_position_ms, - }); - self.ensure_sink_running(); - } else { - error!("Player::play called from invalid state: {:?}", self.state); + stream_position_ms, + .. + } => { + self.state.paused_to_playing(); + self.send_event(PlayerEvent::Playing { + track_id, + play_request_id, + position_ms: stream_position_ms, + }); + self.ensure_sink_running(); + } + PlayerState::Loading { + ref mut start_playback, + .. + } => { + *start_playback = true; + } + _ => error!("Player::play called from invalid state: {:?}", self.state), } } @@ -1512,6 +1516,12 @@ impl PlayerInternal { position_ms: stream_position_ms, }); } + PlayerState::Loading { + ref mut start_playback, + .. + } => { + *start_playback = false; + } _ => error!("Player::pause called from invalid state: {:?}", self.state), } } @@ -1980,6 +1990,25 @@ impl PlayerInternal { } fn handle_command_seek(&mut self, position_ms: u32) -> PlayerResult { + // When we are still loading, the user may immediately ask to + // seek to another position yet the decoder won't be ready for + // that. In this case just restart the loading process but + // with the requested position. + if let PlayerState::Loading { + track_id, + play_request_id, + start_playback, + .. + } = self.state + { + return self.handle_command_load( + track_id, + play_request_id, + start_playback, + position_ms, + ); + } + if let Some(decoder) = self.state.decoder() { match decoder.seek(position_ms) { Ok(new_position_ms) => { @@ -2178,21 +2207,14 @@ 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 * ping_time * bytes_per_second as f32) - as usize, - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, - ); + let request_data_length = + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize; // 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 * ping_time * bytes_per_second as f32) - as usize, - (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize, - ); + let wait_for_data_length = + (READ_AHEAD_BEFORE_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize; + stream_loader_controller .fetch_next_and_wait(request_data_length, wait_for_data_length) .map_err(Into::into) From cc1fb5a406fd3d3da3a12dc37cf252ba530ef21f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 28 Sep 2022 22:17:43 +0200 Subject: [PATCH 262/561] Update changelog --- CHANGELOG.md | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1412875..6d3bd886 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,13 +32,13 @@ https://github.com/librespot-org/librespot ### Changed - [all] Assertions were changed into `Result` or removed (breaking) -- [all] Purge use of `unwrap`, `expect` and return `Result` +- [all] Purge use of `unwrap`, `expect` and return `Result` (breaking) - [all] `chrono` replaced with `time` (breaking) - [all] `time` updated (CVE-2020-26235) - [all] Improve lock contention and performance (breaking) - [audio] Files are now downloaded over the HTTPS CDN (breaking) -- [audio] Improve file opening and seeking performance -- [chore] MSRV is now 1.61 +- [audio] Improve file opening and seeking performance (breaking) +- [chore] MSRV is now 1.61 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [core] Message listeners are registered before authenticating. As a result there now is a separate `Session::new` and subsequent `session.connect`. @@ -63,6 +63,9 @@ https://github.com/librespot-org/librespot - [all] Check that array indexes are within bounds (panic safety) - [all] Wrap errors in librespot `Error` type (breaking) +- [connect] Add session events +- [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs` +- [contrib] Add `event_handler_example.py` - [core] Send metrics with metadata queries: client ID, country & product - [core] Verify Spotify server certificates (prevents man-in-the-middle attacks) - [core] User attributes are stored in `Session` upon login, accessible with a @@ -75,24 +78,28 @@ https://github.com/librespot-org/librespot It supports a lot of functionality, including audio previews and image downloads even if librespot doesn't use that for playback itself. - [core] Support downloading of lyrics +- [main] Add all player events to `player_event_handler.rs` +- [main] Add an event worker thread that runs async to the main thread(s) but + sync to itself to prevent potential data races for event consumers +- [metadata] All metadata fields in the protobufs are now exposed (breaking) - [playback] Explicit tracks are skipped if the controlling Connect client has disabled such content. Applications that use librespot as a library without Connect should use the 'filter-explicit-content' user attribute in the session. -- [metadata] All metadata fields in the protobufs are now exposed (breaking) -- [connect] Add session events - [playback] Add metadata support via a `TrackChanged` event -- [main] Add all player events to `player_event_handler.rs` -- [contrib] Add `event_handler_example.py` -- [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs` -- [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers ### Fixed -- [connect] Set `PlayStatus` to the correct value when Player is loading to avoid blanking out the controls when `self.play_status` is `LoadingPlay` or `LoadingPause` in `spirc.rs` -- [connect] Handle attempts to play local files better by basically ignoring attempts to load them in `handle_remote_update` in `spirc.rs` -- [playback] Handle invalid track start positions by just starting the track from the beginning -- [playback, connect] Clean up and de-noise events and event firing -- [playback] Handle disappearing and invalid devices better +- [connect] Set `PlayStatus` to the correct value when Player is loading to + avoid blanking out the controls when `self.play_status` is `LoadingPlay` or + `LoadingPause` in `spirc.rs` +- [connect] Handle attempts to play local files better by basically ignoring + attempts to load them in `handle_remote_update` in `spirc.rs` +- [connect, playback] Clean up and de-noise events and event firing +- [playback] Handle invalid track start positions by just starting the track + from the beginning +- [playback] Handle disappearing and invalid devices better +- [playback] Handle seek, pause, and play commands while loading + ### Removed - [main] `autoplay` is no longer a command-line option. Instead, librespot now From d07f58e6df5c09ebf047a65683a212121d9cc0b8 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 28 Sep 2022 22:17:55 +0200 Subject: [PATCH 263/561] Update crates --- Cargo.lock | 317 ++++++++++++++++++++++++++--------------------------- 1 file changed, 154 insertions(+), 163 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 43270907..fe16c165 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" [[package]] name = "array-init" @@ -79,9 +79,9 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" dependencies = [ "proc-macro2", "quote", @@ -128,9 +128,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdca834647821e0b13d9539a8634eb62d3501b6b6c2cec1722786ee6671b851" +checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" [[package]] name = "bindgen" @@ -159,24 +159,24 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" dependencies = [ "generic-array", ] [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytemuck" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "byteorder" @@ -186,9 +186,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cc" @@ -241,9 +241,9 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.3.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" dependencies = [ "glob", "libc", @@ -252,9 +252,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.4" +version = "4.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ "bytes", "memchr", @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" dependencies = [ "libc", ] @@ -422,9 +422,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.3" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" dependencies = [ "block-buffer", "crypto-common", @@ -461,9 +461,9 @@ dependencies = [ [[package]] name = "enum-iterator-derive" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13f1e69590421890f90448c3cd5f554746a31adc6dc0dac406ec6901db8dc25" +checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" dependencies = [ "proc-macro2", "quote", @@ -472,9 +472,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" dependencies = [ "atty", "humantime", @@ -506,19 +506,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "futures" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" dependencies = [ "futures-channel", "futures-core", @@ -531,9 +530,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", "futures-sink", @@ -541,15 +540,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-executor" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" dependencies = [ "futures-core", "futures-task", @@ -558,15 +557,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-macro" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" dependencies = [ "proc-macro2", "quote", @@ -575,15 +574,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-timer" @@ -593,9 +592,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-channel", "futures-core", @@ -611,9 +610,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" dependencies = [ "typenum", "version_check", @@ -873,9 +872,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -898,9 +897,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "headers" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64", "bitflags", @@ -909,7 +908,7 @@ dependencies = [ "http", "httpdate", "mime", - "sha-1", + "sha1", ] [[package]] @@ -986,9 +985,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -1085,11 +1084,10 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -1134,9 +1132,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" [[package]] name = "jack" @@ -1210,18 +1208,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -1243,9 +1241,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.126" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "libgit2-sys" @@ -1281,15 +1279,15 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.2" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" +checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" [[package]] name = "libmdns" -version = "0.7.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6fb7fd715150e59e9a74049d2b50e862c3959c139b95eea132a66ddae20c3d9" +checksum = "7cfa684d4e75145d6c1fbe9a2547c2fa4d4d4e5bd6eacce779fd86d5e1cd7cbe" dependencies = [ "byteorder", "futures-util", @@ -1562,9 +1560,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" dependencies = [ "autocfg", "scopeguard", @@ -1594,12 +1592,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "memchr" version = "2.5.0" @@ -1629,9 +1621,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -1952,9 +1944,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "openssl-probe" @@ -2024,9 +2016,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.7" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" [[package]] name = "pbkdf2" @@ -2055,9 +2047,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "petgraph" @@ -2154,10 +2146,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.1.3" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" dependencies = [ + "once_cell", "thiserror", "toml", ] @@ -2188,33 +2181,33 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.42" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" +checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf7e6d18738ecd0902d30d1ad232c9125985a3422929b16c65517b38adc14f96" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" [[package]] name = "protobuf-codegen" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aec1632b7c8f2e620343439a7dfd1f3c47b18906c4be58982079911482b5d707" +checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.27.1" +version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f8122fdb18e55190c796b088a16bdb70cd7acdcd48f7a8b796b58c62e532cc6" +checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" dependencies = [ "protobuf", "protobuf-codegen", @@ -2222,9 +2215,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9279fbdacaad3baf559d8cabe0acc3d06e30ea14931af31af79578ac0946decc" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" dependencies = [ "memchr", "serde", @@ -2232,9 +2225,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" dependencies = [ "proc-macro2", ] @@ -2262,9 +2255,9 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] @@ -2431,24 +2424,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7522c9de787ff061458fe9a829dc790a3f5b22dc571694fc5883f448b94d9a9" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" dependencies = [ "base64", ] [[package]] name = "rustversion" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" +checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" [[package]] name = "ryu" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] name = "same-file" @@ -2520,9 +2513,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -2543,18 +2536,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -2563,9 +2556,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -2585,9 +2578,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.1" +version = "0.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" dependencies = [ "cfg-if", "cpufeatures", @@ -2641,9 +2634,9 @@ checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi", @@ -2770,9 +2763,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" dependencies = [ "proc-macro2", "quote", @@ -2843,18 +2836,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.31" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -2874,9 +2867,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" dependencies = [ "itoa", "libc", @@ -2900,9 +2893,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" dependencies = [ "autocfg", "bytes", @@ -2910,7 +2903,6 @@ dependencies = [ "memchr", "mio", "num_cpus", - "once_cell", "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", @@ -2954,9 +2946,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" +checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" dependencies = [ "futures-core", "pin-project-lite", @@ -2981,9 +2973,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -3069,30 +3061,30 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "unicode-xid" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" [[package]] name = "untrusted" @@ -3102,13 +3094,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] @@ -3136,9 +3127,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "7.3.2" +version = "7.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbc4fafd30514504c7593cfa52eaf4d6c4ef660386e2ec54edc17f14aa08e8d" +checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" dependencies = [ "anyhow", "cfg-if", @@ -3191,9 +3182,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3201,9 +3192,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -3216,9 +3207,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3226,9 +3217,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -3239,15 +3230,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", From 6dc7a11b09b5eea8f333805374ce0b45757e9539 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 28 Sep 2022 22:59:03 +0200 Subject: [PATCH 264/561] Re-introduce `autoplay` command-line option as an override --- CHANGELOG.md | 13 ++++--------- connect/src/spirc.rs | 6 ++++++ core/src/config.rs | 2 ++ core/src/session.rs | 4 ++++ src/main.rs | 31 ++++++++++++++++++++++++++++++- 5 files changed, 46 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d3bd886..b65f536e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ https://github.com/librespot-org/librespot - [core] Cache resolved access points during runtime (breaking) - [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported. - [core] Report actual platform data on login +- [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot` + now follows the setting in the Connect client that controls it. (breaking) +- [metadata] Most metadata is now retrieved with the `spclient` (breaking) +- [metadata] Playlists are moved to the `playlist4_external` protobuf (breaking) - [playback] The audio decoder has been switched from `lewton` to `Symphonia`. This improves the Vorbis sound quality, adds support for MP3 as well as for FLAC in the future. (breaking) @@ -56,8 +60,6 @@ https://github.com/librespot-org/librespot - [playback] The passthrough decoder is now feature-gated (breaking) - [playback] `rodio`: call play and pause - [protocol] protobufs have been updated -- [metadata] Most metadata is now retrieved with the `spclient` (breaking) -- [metadata] Playlists are moved to the `playlist4_external` protobuf (breaking) ### Added @@ -100,13 +102,6 @@ https://github.com/librespot-org/librespot - [playback] Handle disappearing and invalid devices better - [playback] Handle seek, pause, and play commands while loading -### Removed - -- [main] `autoplay` is no longer a command-line option. Instead, librespot now - follows the setting in the Connect client that controls it. Applications that - use librespot as a library without Connect should now instead use the - 'autoplay' user attribute in the session. - ## [0.4.2] - 2022-07-29 Besides a couple of small fixes, this point release is mainly to blacklist the diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index a9144568..31223921 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -772,6 +772,12 @@ impl SpircTask { fn handle_user_attributes_mutation(&mut self, mutation: UserAttributesMutation) { for attribute in mutation.get_fields().iter() { let key = attribute.get_name(); + + if key == "autoplay" && self.session.config().autoplay.is_some() { + trace!("Autoplay override active. Ignoring mutation."); + continue; + } + if let Some(old_value) = self.session.user_data().attributes.get(key) { let new_value = match old_value.as_ref() { "0" => "1", diff --git a/core/src/config.rs b/core/src/config.rs index 7d45cdd7..ada5354b 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -13,6 +13,7 @@ pub struct SessionConfig { pub proxy: Option, pub ap_port: Option, pub tmp_dir: PathBuf, + pub autoplay: Option, } impl Default for SessionConfig { @@ -31,6 +32,7 @@ impl Default for SessionConfig { proxy: None, ap_port: None, tmp_dir: std::env::temp_dir(), + autoplay: None, } } } diff --git a/core/src/session.rs b/core/src/session.rs index 82c9e4a8..1936467e 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -438,6 +438,10 @@ impl Session { } pub fn autoplay(&self) -> bool { + if let Some(overide) = self.config().autoplay { + return overide; + } + match self.get_user_attribute("autoplay") { Some(value) => matches!(&*value, "1"), None => false, diff --git a/src/main.rs b/src/main.rs index aac7119a..5cd19bd2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -197,6 +197,7 @@ 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"; @@ -242,6 +243,7 @@ fn get_setup() -> Setup { // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; + const AUTOPLAY_SHORT: &str = "A"; const BACKEND_SHORT: &str = "B"; const BITRATE_SHORT: &str = "b"; const SYSTEM_CACHE_SHORT: &str = "C"; @@ -562,6 +564,12 @@ fn get_setup() -> Setup { AP_PORT, "Connect to an AP with a specified port 1 - 65535. Available ports are usually 80, 443 and 4070.", "PORT", + ) + .optopt( + AUTOPLAY_SHORT, + AUTOPLAY, + "Explicitly set autoplay {on|off}. Defaults to following the client setting.", + "OVERRIDE", ); #[cfg(feature = "passthrough-decoder")] @@ -1140,6 +1148,26 @@ fn get_setup() -> Setup { 0 }; + // #1046: not all connections are supplied an `autoplay` user attribute to run statelessly. + // This knob allows for a manual override. + let autoplay = match opt_str(AUTOPLAY) { + Some(value) => match value.as_ref() { + "on" => Some(true), + "off" => Some(false), + _ => { + invalid_error_msg( + AUTOPLAY, + AUTOPLAY_SHORT, + &opt_str(AUTOPLAY).unwrap_or_default(), + "on, off", + "", + ); + exit(1); + } + }, + None => SessionConfig::default().autoplay, + }; + let connect_config = { let connect_default_config = ConnectConfig::default(); @@ -1293,7 +1321,8 @@ fn get_setup() -> Setup { } }), tmp_dir, - ..SessionConfig::default() + autoplay, + ..SessionConfig::default() }; let player_config = { From bfb7d5689ccd7abcd4bdcbdf274bd30051d5840d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 30 Sep 2022 21:36:20 +0200 Subject: [PATCH 265/561] Retrieve autoplay contexts over HTTPS and fix repeat/prev/next Repeat, previous and next used to start playback regardless of the actual playback state. They now start playback only if we were already playing. --- CHANGELOG.md | 5 ++ connect/src/context.rs | 94 +++++++++++++--------- connect/src/spirc.rs | 179 ++++++++++++++++------------------------- core/src/spclient.rs | 14 +++- 4 files changed, 143 insertions(+), 149 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b65f536e..3f4dd956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ https://github.com/librespot-org/librespot - [audio] Improve file opening and seeking performance (breaking) - [chore] MSRV is now 1.61 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) +- [connect] Update and expose all `spirc` context fields (breaking) +- [connect] Add `Clone, Defaut` traits to `spirc` contexts +- [connect] Autoplay contexts are now retrieved with the `spclient` (breaking) - [core] Message listeners are registered before authenticating. As a result there now is a separate `Session::new` and subsequent `session.connect`. (breaking) @@ -96,6 +99,8 @@ https://github.com/librespot-org/librespot `LoadingPause` in `spirc.rs` - [connect] Handle attempts to play local files better by basically ignoring attempts to load them in `handle_remote_update` in `spirc.rs` +- [connect] Loading previous or next tracks, or looping back on repeat, will + only start playback when we were already playing - [connect, playback] Clean up and de-noise events and event firing - [playback] Handle invalid track start positions by just starting the track from the beginning diff --git a/connect/src/context.rs b/connect/src/context.rs index 928aec23..9428faac 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -8,67 +8,89 @@ use serde::{ Deserialize, }; -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default, Clone)] pub struct StationContext { - pub uri: Option, - pub next_page_url: String, - #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] - pub tracks: Vec, - // Not required for core functionality - // pub seeds: Vec, - // #[serde(rename = "imageUri")] - // pub image_uri: String, - // pub subtitle: Option, - // pub subtitles: Vec, - // #[serde(rename = "subtitleUri")] - // pub subtitle_uri: Option, - // pub title: String, - // #[serde(rename = "titleUri")] - // pub title_uri: String, - // pub related_artists: Vec, -} - -#[derive(Deserialize, Debug)] -pub struct PageContext { pub uri: String, - pub next_page_url: String, + pub title: String, + #[serde(rename = "titleUri")] + pub title_uri: String, + pub subtitles: Vec, + #[serde(rename = "imageUri")] + pub image_uri: String, + pub seeds: Vec, #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] pub tracks: Vec, - // Not required for core functionality - // pub url: String, - // // pub restrictions: + pub next_page_url: String, + pub correlation_id: String, + pub related_artists: Vec, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default, Clone)] +pub struct PageContext { + #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] + pub tracks: Vec, + pub next_page_url: String, + pub correlation_id: String, +} + +#[derive(Deserialize, Debug, Default, Clone)] pub struct TrackContext { - #[serde(rename = "original_gid")] - pub gid: String, pub uri: String, pub uid: String, - // Not required for core functionality - // pub album_uri: String, - // pub artist_uri: String, - // pub metadata: MetadataContext, + pub artist_uri: String, + pub album_uri: String, + #[serde(rename = "original_gid")] + pub gid: String, + pub metadata: MetadataContext, + pub name: String, } #[allow(dead_code)] -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default, Clone)] #[serde(rename_all = "camelCase")] pub struct ArtistContext { + #[serde(rename = "artistName")] artist_name: String, - artist_uri: String, + #[serde(rename = "imageUri")] image_uri: String, + #[serde(rename = "artistUri")] + artist_uri: String, } #[allow(dead_code)] -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Default, Clone)] pub struct MetadataContext { album_title: String, artist_name: String, artist_uri: String, image_url: String, title: String, - uid: String, + #[serde(deserialize_with = "bool_from_string")] + is_explicit: bool, + #[serde(deserialize_with = "bool_from_string")] + is_promotional: bool, + decision_id: String, +} + +#[allow(dead_code)] +#[derive(Deserialize, Debug, Default, Clone)] +pub struct SubtitleContext { + name: String, + uri: String, +} + +fn bool_from_string<'de, D>(de: D) -> Result +where + D: serde::Deserializer<'de>, +{ + match String::deserialize(de)?.as_ref() { + "true" => Ok(true), + "false" => Ok(false), + other => Err(D::Error::invalid_value( + Unexpected::Str(other), + &"true or false", + )), + } } #[allow(non_snake_case)] diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 31223921..0601320d 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -6,11 +6,7 @@ use std::{ time::{SystemTime, UNIX_EPOCH}, }; -use futures_util::{ - future::{self, FusedFuture}, - stream::FusedStream, - FutureExt, StreamExt, -}; +use futures_util::{stream::FusedStream, FutureExt, StreamExt}; use protobuf::{self, Message}; use rand::seq::SliceRandom; @@ -20,13 +16,10 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ config::ConnectConfig, - context::StationContext, + context::{PageContext, StationContext}, core::{ - authentication::Credentials, - mercury::{MercuryError, MercurySender}, - session::UserAttributes, - util::SeqGenerator, - version, Error, Session, SpotifyId, + authentication::Credentials, mercury::MercurySender, session::UserAttributes, + util::SeqGenerator, version, Error, Session, SpotifyId, }, playback::{ mixer::Mixer, @@ -81,7 +74,6 @@ enum SpircPlayStatus { }, } -type BoxedFuture = Pin + Send>>; type BoxedStream = Pin + Send>>; struct SpircTask { @@ -106,8 +98,8 @@ struct SpircTask { shutdown: bool, session: Session, - context_fut: BoxedFuture>, - autoplay_fut: BoxedFuture>, + resolve_context: Option, + autoplay_context: bool, context: Option, spirc_id: usize, @@ -132,6 +124,7 @@ pub enum SpircCommand { SetVolume(u16), } +const CONTEXT_TRACKS_COUNT: usize = 50; const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; @@ -376,8 +369,8 @@ impl Spirc { shutdown: false, session, - context_fut: Box::pin(future::pending()), - autoplay_fut: Box::pin(future::pending()), + resolve_context: None, + autoplay_context: false, context: None, spirc_id, @@ -504,10 +497,35 @@ impl SpircTask { error!("Cannot flush spirc event sender."); break; }, - context = &mut self.context_fut, if !self.context_fut.is_terminated() => { + context_uri = async { self.resolve_context.take() }, if self.resolve_context.is_some() => { + let context_uri = context_uri.unwrap(); + let is_next_page = context_uri.starts_with("hm://"); + + let context = if is_next_page { + self.session.spclient().get_next_page(&context_uri).await + } else { + let previous_tracks = self.state.get_track().iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); + self.session.spclient().get_apollo_station(&context_uri, CONTEXT_TRACKS_COUNT, previous_tracks, self.autoplay_context).await + }; + match context { Ok(value) => { - let r_context = serde_json::from_value::(value); + let r_context = if is_next_page { + match serde_json::from_slice::(&value) { + Ok(page_context) => { + // page contexts don't have the stations full metadata, so decorate it + let mut station_context = self.context.clone().unwrap_or_default(); + station_context.tracks = page_context.tracks; + station_context.next_page_url = page_context.next_page_url; + station_context.correlation_id = page_context.correlation_id; + Ok(station_context) + }, + Err(e) => Err(e), + } + } else { + serde_json::from_slice::(&value) + }; + self.context = match r_context { Ok(context) => { info!( @@ -522,28 +540,12 @@ impl SpircTask { None } }; - // It needn't be so verbose - can be as simple as - // if let Some(ref context) = r_context { - // info!("Got {:?} tracks from <{}>", context.tracks.len(), context.uri); - // } - // self.context = r_context; }, Err(err) => { error!("ContextError: {:?}", err) } } }, - autoplay = &mut self.autoplay_fut, if !self.autoplay_fut.is_terminated() => { - match autoplay { - Ok(autoplay_station_uri) => { - info!("Autoplay uri resolved to <{:?}>", autoplay_station_uri); - self.context_fut = self.resolve_station(&autoplay_station_uri); - }, - Err(err) => { - error!("AutoplayError: {:?}", err) - } - } - }, else => break } } @@ -953,7 +955,7 @@ impl SpircTask { MessageType::kMessageTypeShuffle => { let shuffle = update.get_state().get_shuffle(); self.state.set_shuffle(shuffle); - if self.state.get_shuffle() { + if shuffle { let current_index = self.state.get_playing_track_index(); let tracks = self.state.mut_track(); if !tracks.is_empty() { @@ -964,11 +966,7 @@ impl SpircTask { } self.state.set_playing_track_index(0); } - } else { - let context = self.state.get_context_uri(); - debug!("{:?}", context); } - self.player.emit_shuffle_changed_event(shuffle); self.notify(None) @@ -1191,40 +1189,41 @@ impl SpircTask { } fn handle_next(&mut self) { + let context_uri = self.state.get_context_uri().to_owned(); + let mut tracks_len = self.state.get_track().len() as u32; let mut new_index = self.consume_queued_track() as u32; - let mut continue_playing = true; - let tracks_len = self.state.get_track().len() as u32; + let mut continue_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; + + let update_tracks = + self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD; + debug!( "At track {:?} of {:?} <{:?}> update [{}]", new_index + 1, tracks_len, - self.state.get_context_uri(), - tracks_len - new_index < CONTEXT_FETCH_THRESHOLD + context_uri, + update_tracks, ); - let context_uri = self.state.get_context_uri().to_owned(); - if (context_uri.starts_with("spotify:station:") - || context_uri.starts_with("spotify:dailymix:") - // spotify:user:xxx:collection - || context_uri.starts_with(&format!("spotify:user:{}:collection",url_encode(&self.session.username())))) - && ((self.state.get_track().len() as u32) - new_index) < CONTEXT_FETCH_THRESHOLD - { - self.context_fut = self.resolve_station(&context_uri); + + // When in autoplay, keep topping up the playlist when it nears the end + if update_tracks { self.update_tracks_from_context(); + new_index = self.state.get_playing_track_index(); + tracks_len = self.state.get_track().len() as u32; } + // When not in autoplay, either start autoplay or loop back to the start if new_index >= tracks_len { if self.session.autoplay() { // Extend the playlist - debug!("Extending playlist <{}>", context_uri); + debug!("Starting autoplay for <{}>", context_uri); + self.autoplay_context = true; self.update_tracks_from_context(); self.player.set_auto_normalise_as_album(false); } else { new_index = 0; - continue_playing = self.state.get_repeat(); - debug!( - "Looping around back to start, repeat is {}", - continue_playing - ); + continue_playing &= self.state.get_repeat(); + debug!("Looping back to start, repeat is {}", continue_playing); } } @@ -1271,7 +1270,8 @@ impl SpircTask { self.state.set_playing_track_index(new_index); - self.load_track(true, 0); + let start_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, 0); } else { self.handle_seek(0); } @@ -1304,50 +1304,16 @@ impl SpircTask { } } - 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> { - 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?; - - if response.status_code == 200 { - let data = response.payload.first().ok_or(SpircError::NoData)?.to_vec(); - Ok(String::from_utf8(data)?) - } else { - warn!("No autoplay_uri found"); - Err(MercuryError::Response(response).into()) - } - } - .fuse(), - ) - } - - fn resolve_uri(&self, uri: &str) -> BoxedFuture> { - let request = self.session.mercury().get(uri); - - Box::pin( - async move { - let response = request?.await?; - - let data = response.payload.first().ok_or(SpircError::NoData)?; - let response: serde_json::Value = serde_json::from_slice(data)?; - - Ok(response) - } - .fuse(), - ) - } - fn update_tracks_from_context(&mut self) { if let Some(ref context) = self.context { - self.context_fut = self.resolve_uri(&context.next_page_url); + self.resolve_context = + if !self.autoplay_context || context.next_page_url.contains("autoplay=true") { + Some(context.next_page_url.to_owned()) + } else { + // this arm means: we need to resolve for autoplay, + // and were previously resolving for the original context + Some(context.uri.to_owned()) + }; let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to frame", new_tracks.len()); @@ -1381,15 +1347,10 @@ impl SpircTask { 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.session.autoplay() { - info!("Fetching autoplay context uri"); - // Get autoplay_station_uri for regular playlists - self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); - } + // First the tracks from the requested context, without autoplay. + // We will transition into autoplay after the latest track of this context. + self.autoplay_context = false; + self.resolve_context = Some(context_uri.clone()); self.player .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 4af0472f..9d3b144b 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -616,7 +616,7 @@ impl SpClient { pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult { let endpoint = format!( "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", - track_id.to_uri()? + track_id.to_base62()? ); self.request_as_json(&Method::GET, &endpoint, None, None) @@ -626,13 +626,13 @@ impl SpClient { pub async fn get_apollo_station( &self, context_uri: &str, - count: u32, - previous_tracks: Vec<&SpotifyId>, + count: usize, + previous_tracks: Vec, autoplay: bool, ) -> SpClientResult { let previous_track_str = previous_tracks .iter() - .map(|track| track.to_uri()) + .map(|track| track.to_base62()) .collect::, _>>()? .join(","); let endpoint = format!( @@ -644,6 +644,12 @@ impl SpClient { .await } + pub async fn get_next_page(&self, next_page_uri: &str) -> SpClientResult { + let endpoint = next_page_uri.trim_start_matches("hm:/"); + 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"; From f10b8f69f8a4ad0472d26ad5067e2dd40be697e0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 1 Oct 2022 23:01:17 +0200 Subject: [PATCH 266/561] Improvements towards supporting pagination Not there yet, as Apollo stations always return autoplay recommendations even if you set autoplay to false. Along the way as an effort to bring the protocol up to spec: - And support for and use different Apollo station scopes depending on whether we are using autoplay or not. For autoplay, get a "stations" scope and follow the "tracks" pages from there. Otherwise use "tracks" immediately for the active scope (playlist, album). - For the above point we only need the fields from `PageContext` so use that instead of a `StationContext`. - Add some documentation from API reverse engineering: things seen in the wild, some of them to do, others documented for posterity's sake. - Update the Spirc device state based on what the latest desktop client puts out. Unfortunately none of it seems to change the behavior necessary to support external episodes, shows, but at least we're doing the right thing. - Add a salt to HTTPS queries to defeat any caching. - Add country metrics to HTTPS queries. - Fix `get_radio_for_track` to use the right Spotify ID format. - Fix a bug from the previous commit, where the playback position might not advance when hitting next and the autoplay context is loaded initially. --- connect/src/spirc.rs | 149 +++++++++++++++++++++++++++---------------- core/src/spclient.rs | 55 +++++++++++++--- 2 files changed, 139 insertions(+), 65 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 0601320d..44a29d7c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -16,7 +16,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ config::ConnectConfig, - context::{PageContext, StationContext}, + context::PageContext, core::{ authentication::Credentials, mercury::MercurySender, session::UserAttributes, util::SeqGenerator, version, Error, Session, SpotifyId, @@ -100,7 +100,7 @@ struct SpircTask { session: Session, resolve_context: Option, autoplay_context: bool, - context: Option, + context: Option, spirc_id: usize, } @@ -124,7 +124,6 @@ pub enum SpircCommand { SetVolume(u16), } -const CONTEXT_TRACKS_COUNT: usize = 50; const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; @@ -184,6 +183,7 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { }; { let msg = repeated.push_default(); + // TODO: implement logout msg.set_typ(protocol::spirc::CapabilityType::kSupportsLogout); { let repeated = msg.mut_intValue(); @@ -224,17 +224,51 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { }; { let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kSupportedContexts); + msg.set_typ(protocol::spirc::CapabilityType::kSupportsExternalEpisodes); { - let repeated = msg.mut_stringValue(); - repeated.push(::std::convert::Into::into("album")); - repeated.push(::std::convert::Into::into("playlist")); - repeated.push(::std::convert::Into::into("search")); - repeated.push(::std::convert::Into::into("inbox")); - repeated.push(::std::convert::Into::into("toplist")); - repeated.push(::std::convert::Into::into("starred")); - repeated.push(::std::convert::Into::into("publishedstarred")); - repeated.push(::std::convert::Into::into("track")) + let repeated = msg.mut_intValue(); + repeated.push(1) + }; + msg + }; + { + let msg = repeated.push_default(); + // TODO: how would such a rename command be triggered? Handle it. + msg.set_typ(protocol::spirc::CapabilityType::kSupportsRename); + { + let repeated = msg.mut_intValue(); + repeated.push(1) + }; + msg + }; + { + let msg = repeated.push_default(); + msg.set_typ(protocol::spirc::CapabilityType::kCommandAcks); + { + let repeated = msg.mut_intValue(); + repeated.push(0) + }; + msg + }; + { + let msg = repeated.push_default(); + // TODO: does this mean local files or the local network? + // LAN may be an interesting privacy toggle. + msg.set_typ(protocol::spirc::CapabilityType::kRestrictToLocal); + { + let repeated = msg.mut_intValue(); + repeated.push(0) + }; + msg + }; + { + let msg = repeated.push_default(); + // TODO: what does this hide, or who do we hide from? + // May be an interesting privacy toggle. + msg.set_typ(protocol::spirc::CapabilityType::kHidden); + { + let repeated = msg.mut_intValue(); + repeated.push(0) }; msg }; @@ -243,9 +277,15 @@ fn initial_device_state(config: ConnectConfig) -> DeviceState { msg.set_typ(protocol::spirc::CapabilityType::kSupportedTypes); { let repeated = msg.mut_stringValue(); - repeated.push(::std::convert::Into::into("audio/track")); - repeated.push(::std::convert::Into::into("audio/episode")); - repeated.push(::std::convert::Into::into("track")) + repeated.push("audio/episode".to_string()); + repeated.push("audio/episode+track".to_string()); + repeated.push("audio/track".to_string()); + // other known types: + // - "audio/ad" + // - "audio/interruption" + // - "audio/local" + // - "video/ad" + // - "video/episode" }; msg }; @@ -498,35 +538,30 @@ impl SpircTask { break; }, context_uri = async { self.resolve_context.take() }, if self.resolve_context.is_some() => { - let context_uri = context_uri.unwrap(); - let is_next_page = context_uri.starts_with("hm://"); + let context_uri = context_uri.unwrap(); // guaranteed above + if context_uri.contains("spotify:show:") || context_uri.contains("spotify:episode:") { + continue; // not supported by apollo stations + } - let context = if is_next_page { + let context = if context_uri.starts_with("hm://") { self.session.spclient().get_next_page(&context_uri).await } else { - let previous_tracks = self.state.get_track().iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); - self.session.spclient().get_apollo_station(&context_uri, CONTEXT_TRACKS_COUNT, previous_tracks, self.autoplay_context).await + // only send previous tracks that were before the current playback position + let current_position = self.state.get_playing_track_index() as usize; + let previous_tracks = self.state.get_track()[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); + + let scope = if self.autoplay_context { + "stations" // this returns a `StationContext` but we deserialize it into a `PageContext` + } else { + "tracks" // this returns a `PageContext` + }; + + self.session.spclient().get_apollo_station(scope, &context_uri, None, previous_tracks, self.autoplay_context).await }; match context { Ok(value) => { - let r_context = if is_next_page { - match serde_json::from_slice::(&value) { - Ok(page_context) => { - // page contexts don't have the stations full metadata, so decorate it - let mut station_context = self.context.clone().unwrap_or_default(); - station_context.tracks = page_context.tracks; - station_context.next_page_url = page_context.next_page_url; - station_context.correlation_id = page_context.correlation_id; - Ok(station_context) - }, - Err(e) => Err(e), - } - } else { - serde_json::from_slice::(&value) - }; - - self.context = match r_context { + self.context = match serde_json::from_slice::(&value) { Ok(context) => { info!( "Resolved {:?} tracks from <{:?}>", @@ -829,7 +864,7 @@ impl SpircTask { for entry in update.get_device_state().get_metadata().iter() { match entry.get_field_type() { - "client-id" => self.session.set_client_id(entry.get_metadata()), + "client_id" => self.session.set_client_id(entry.get_metadata()), "brand_display_name" => self.session.set_client_brand_name(entry.get_metadata()), "model_display_name" => self.session.set_client_model_name(entry.get_metadata()), _ => (), @@ -1207,17 +1242,23 @@ impl SpircTask { // When in autoplay, keep topping up the playlist when it nears the end if update_tracks { - self.update_tracks_from_context(); - new_index = self.state.get_playing_track_index(); - tracks_len = self.state.get_track().len() as u32; + if let Some(ref context) = self.context { + self.resolve_context = Some(context.next_page_url.to_owned()); + self.update_tracks_from_context(); + tracks_len = self.state.get_track().len() as u32; + } } // When not in autoplay, either start autoplay or loop back to the start if new_index >= tracks_len { - if self.session.autoplay() { + // for some contexts there is no autoplay, such as shows and episodes + // in such cases there is no context in librespot. + if self.context.is_some() && self.session.autoplay() { // Extend the playlist debug!("Starting autoplay for <{}>", context_uri); + // force reloading the current context with an autoplay context self.autoplay_context = true; + self.resolve_context = Some(self.state.get_context_uri().to_owned()); self.update_tracks_from_context(); self.player.set_auto_normalise_as_album(false); } else { @@ -1306,17 +1347,10 @@ impl SpircTask { fn update_tracks_from_context(&mut self) { if let Some(ref context) = self.context { - self.resolve_context = - if !self.autoplay_context || context.next_page_url.contains("autoplay=true") { - Some(context.next_page_url.to_owned()) - } else { - // this arm means: we need to resolve for autoplay, - // and were previously resolving for the original context - Some(context.uri.to_owned()) - }; - let new_tracks = &context.tracks; + debug!("Adding {:?} tracks from context to frame", new_tracks.len()); + let mut track_vec = self.state.take_track().into_vec(); if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { track_vec.drain(0..head); @@ -1342,7 +1376,7 @@ impl SpircTask { 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 context_uri = frame.get_state().get_context_uri(); let tracks = frame.get_state().get_track(); trace!("Frame has {:?} tracks", tracks.len()); @@ -1350,14 +1384,14 @@ impl SpircTask { // First the tracks from the requested context, without autoplay. // We will transition into autoplay after the latest track of this context. self.autoplay_context = false; - self.resolve_context = Some(context_uri.clone()); + self.resolve_context = Some(context_uri.to_owned()); self.player .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); self.state.set_playing_track_index(index); self.state.set_track(tracks.iter().cloned().collect()); - self.state.set_context_uri(context_uri); + self.state.set_context_uri(context_uri.to_owned()); // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. @@ -1517,8 +1551,11 @@ struct CommandSender<'a> { impl<'a> CommandSender<'a> { fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender<'_> { let mut frame = protocol::spirc::Frame::new(); + // frame version frame.set_version(1); - frame.set_protocol_version(::std::convert::Into::into("2.0.0")); + // Latest known Spirc version is 3.2.6, but we need another interface to announce support for Spirc V3. + // Setting anything higher than 2.0.0 here just seems to limit it to 2.0.0. + frame.set_protocol_version("2.0.0".to_string()); frame.set_ident(spirc.ident.clone()); frame.set_seq_nr(spirc.sequence.get()); frame.set_typ(cmd); diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 9d3b144b..4324eb96 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -15,6 +15,7 @@ use hyper::{ Body, HeaderMap, Method, Request, }; use protobuf::{Message, ProtobufEnum}; +use rand::RngCore; use sha1::{Digest, Sha1}; use sysinfo::{System, SystemExt}; use thiserror::Error; @@ -435,13 +436,26 @@ 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 => "?", }; - let _ = write!(url, "{}product=0", separator); + + // 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. + // For the sake of documentation you could also do "product=free" but + // we only support premium anyway. + let _ = write!( + url, + "{}product=0&country={}", + separator, + self.session().country() + ); + + // Defeat caches. Spotify-generated URLs already contain this. + if !url.contains("salt=") { + let _ = write!(url, "&salt={}", rand::thread_rng().next_u32()); + } let mut request = Request::builder() .method(method) @@ -616,29 +630,49 @@ impl SpClient { pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult { let endpoint = format!( "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", - track_id.to_base62()? + track_id.to_uri()? ); self.request_as_json(&Method::GET, &endpoint, None, None) .await } + // Known working scopes: stations, tracks + // For others see: https://gist.github.com/roderickvd/62df5b74d2179a12de6817a37bb474f9 + // + // Seen-in-the-wild but unimplemented query parameters: + // - image_style=gradient_overlay + // - excludeClusters=true + // - language=en + // - count_tracks=0 + // - market=from_token pub async fn get_apollo_station( &self, + scope: &str, context_uri: &str, - count: usize, + count: Option, previous_tracks: Vec, autoplay: bool, ) -> SpClientResult { + let mut endpoint = format!( + "/radio-apollo/v3/{}/{}?autoplay={}", + scope, context_uri, autoplay, + ); + + // Spotify has a default of 50 + if let Some(count) = count { + let _ = write!(endpoint, "&count={}", count); + } + let previous_track_str = previous_tracks .iter() .map(|track| track.to_base62()) .collect::, _>>()? .join(","); - let endpoint = format!( - "/radio-apollo/v3/stations/{}?count={}&prev_tracks={}&autoplay={}", - context_uri, count, previous_track_str, autoplay, - ); + // better than checking `previous_tracks.len() > 0` because the `filter_map` could still return 0 items + if !previous_track_str.is_empty() { + let _ = write!(endpoint, "&prev_tracks={}", previous_track_str); + } self.request_as_json(&Method::GET, &endpoint, None, None) .await @@ -650,6 +684,9 @@ impl SpClient { .await } + // TODO: Seen-in-the-wild but unimplemented endpoints + // - /presence-view/v1/buddylist + // 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"; From bae304fdb0f656a31d73f13d0b130d0df33b97b9 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 2 Oct 2022 00:00:30 +0200 Subject: [PATCH 267/561] Update zeroconf fields and publish active user --- discovery/src/lib.rs | 13 ++++++------ discovery/src/server.rs | 44 +++++++++++++++++++++++++++++++---------- src/main.rs | 3 ++- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 718d9dec..3c01003b 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -77,13 +77,14 @@ impl From for Error { } impl Builder { - /// Starts a new builder using the provided device id. - pub fn new(device_id: impl Into) -> Self { + /// Starts a new builder using the provided device and client IDs. + pub fn new>(device_id: T, client_id: T) -> Self { Self { server_config: server::Config { name: "Librespot".into(), device_type: DeviceType::default(), device_id: device_id.into(), + client_id: client_id.into(), }, port: 0, } @@ -141,13 +142,13 @@ impl Builder { impl Discovery { /// Starts a [`Builder`] with the provided device id. - pub fn builder(device_id: impl Into) -> Builder { - Builder::new(device_id) + pub fn builder>(device_id: T, client_id: T) -> Builder { + Builder::new(device_id, client_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() + pub fn new>(device_id: T, client_id: T) -> Result { + Self::builder(device_id, client_id).launch() } } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 3edc2fb6..faea33a1 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -36,10 +36,12 @@ pub struct Config { pub name: Cow<'static, str>, pub device_type: DeviceType, pub device_id: String, + pub client_id: String, } struct RequestHandler { config: Config, + username: Option, keys: DhLocalKeys, tx: mpsc::UnboundedSender, } @@ -50,6 +52,7 @@ impl RequestHandler { let discovery = Self { config, + username: None, keys: DhLocalKeys::random(&mut rand::thread_rng()), tx, }; @@ -60,24 +63,45 @@ impl RequestHandler { 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 mut active_user = String::new(); + if let Some(username) = &self.username { + active_user = username.to_string(); + } + // See: https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf/ let body = json!({ "status": 101, - "statusString": "ERROR-OK", + "statusString": "OK", "spotifyError": 0, - "version": crate::core::version::SEMVER, + // departing from the Spotify documentation, Google Cast uses "5.0.0" + "version": "2.9.0", "deviceID": (self.config.device_id), - "remoteName": (self.config.name), - "activeUser": "", - "publicKey": (public_key), "deviceType": (device_type), - "libraryVersion": crate::core::version::SEMVER, - "accountReq": "PREMIUM", + "remoteName": (self.config.name), + // valid value seen in the wild: "empty" + "publicKey": (public_key), "brandDisplayName": "librespot", "modelDisplayName": "librespot", - "resolverVersion": "0", + "libraryVersion": crate::core::version::SEMVER, + "resolverVersion": "1", "groupStatus": "NONE", - "voiceSupport": "NO", + // valid value documented & seen in the wild: "accesstoken" + // Using it will cause clients to fail to connect. + "tokenType": "default", + "clientID": (self.config.client_id), + "productID": 0, + // Other known scope: client-authorization-universal + // Comma-separated. + "scope": "streaming", + "availability": "", + "supported_drm_media_formats": [], + // TODO: bitmask but what are the flags? + "supported_capabilities": 1, + // undocumented but should still work + "accountReq": "PREMIUM", + "activeUser": active_user, + // others seen-in-the-wild: + // - "deviceAPI_isGroup": False }) .to_string(); @@ -162,7 +186,7 @@ impl RequestHandler { let result = json!({ "status": 101, "spotifyError": 0, - "statusString": "ERROR-OK" + "statusString": "OK", }); let body = result.to_string(); diff --git a/src/main.rs b/src/main.rs index 5cd19bd2..d5c53925 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1635,7 +1635,8 @@ async fn main() { if setup.enable_discovery { let device_id = setup.session_config.device_id.clone(); - match librespot::discovery::Discovery::builder(device_id) + let client_id = setup.session_config.client_id.clone(); + match librespot::discovery::Discovery::builder(device_id, client_id) .name(setup.connect_config.name.clone()) .device_type(setup.connect_config.device_type) .port(setup.zeroconf_port) From 05dcb652a0acba529a5674e00312fba2a8ef20e6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 2 Oct 2022 00:08:36 +0200 Subject: [PATCH 268/561] Fix example --- discovery/examples/discovery.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs index f7dee532..28a99e1a 100644 --- a/discovery/examples/discovery.rs +++ b/discovery/examples/discovery.rs @@ -1,4 +1,5 @@ use futures::StreamExt; +use librespot_core::SessionConfig; use librespot_discovery::DeviceType; use sha1::{Digest, Sha1}; @@ -7,7 +8,7 @@ async fn main() { let name = "Librespot"; let device_id = hex::encode(Sha1::digest(name.as_bytes())); - let mut server = librespot_discovery::Discovery::builder(device_id) + let mut server = librespot_discovery::Discovery::builder(device_id, SessionConfig::default().client_id) .name(name) .device_type(DeviceType::Computer) .launch() From ce9eb4246946d42e4385737090f4a07924b890d2 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 1 Oct 2022 19:51:55 -0500 Subject: [PATCH 269/561] Fix fmt lint from https://github.com/librespot-org/librespot/commit/05dcb652a0acba529a5674e00312fba2a8ef20e6 --- discovery/examples/discovery.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs index 28a99e1a..0b9b5b24 100644 --- a/discovery/examples/discovery.rs +++ b/discovery/examples/discovery.rs @@ -8,11 +8,12 @@ async fn main() { let name = "Librespot"; let device_id = hex::encode(Sha1::digest(name.as_bytes())); - let mut server = librespot_discovery::Discovery::builder(device_id, SessionConfig::default().client_id) - .name(name) - .device_type(DeviceType::Computer) - .launch() - .unwrap(); + let mut server = + librespot_discovery::Discovery::builder(device_id, SessionConfig::default().client_id) + .name(name) + .device_type(DeviceType::Computer) + .launch() + .unwrap(); while let Some(x) = server.next().await { println!("Received {:?}", x); From 1a48bc87c879201b534b34e1995bd501d02f32ef Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Thu, 20 Oct 2022 17:17:46 +0200 Subject: [PATCH 270/561] core: workaround for Session::connect() future being !Send rsa::padding::PaddingScheme is !Send, making it impossible to call Session::connect() with an executor requiring Send futures, such as Rocket. Fix #1065 --- core/src/connection/handshake.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 680f512e..b720455c 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -77,9 +77,11 @@ pub async fn handshake( })?; let hash = Sha1::digest(&remote_key); - let padding = rsa::padding::PaddingScheme::new_pkcs1v15_sign(Some(rsa::hash::Hash::SHA1)); + let padding = PaddingScheme(rsa::padding::PaddingScheme::new_pkcs1v15_sign(Some( + rsa::hash::Hash::SHA1, + ))); public_key - .verify(padding, &hash, &remote_signature) + .verify(padding.0, &hash, &remote_signature) .map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, @@ -97,6 +99,13 @@ pub async fn handshake( Ok(codec.framed(connection)) } +// Workaround for https://github.com/RustCrypto/RSA/issues/214 +struct PaddingScheme(rsa::padding::PaddingScheme); + +/// # Safety +/// The `rsa::padding::PaddingScheme` variant we use is actually `Send`. +unsafe impl Send for PaddingScheme {} + async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> where T: AsyncWrite + Unpin, From 39b37d344f4ae40d331a596d2425855a2dd13848 Mon Sep 17 00:00:00 2001 From: killahtree <34866740+wholivesinapineappleunderthesea@users.noreply.github.com> Date: Thu, 3 Nov 2022 20:10:49 -0400 Subject: [PATCH 271/561] Update play.rs --- examples/play.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/play.rs b/examples/play.rs index 1209bf95..3cb338a4 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -25,7 +25,8 @@ async fn main() { } let credentials = Credentials::with_password(&args[1], &args[2]); - let track = SpotifyId::from_base62(&args[3]).unwrap(); + let mut track = SpotifyId::from_base62(&args[3]).unwrap(); + track.item_type = SpotifyItemType::Track; let backend = audio_backend::find(None).unwrap(); From b15490714d600fd12fc7040ba012e9b4fdccd692 Mon Sep 17 00:00:00 2001 From: killahtree <34866740+wholivesinapineappleunderthesea@users.noreply.github.com> Date: Thu, 3 Nov 2022 20:21:39 -0400 Subject: [PATCH 272/561] Update play.rs --- examples/play.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/play.rs b/examples/play.rs index 3cb338a4..37011d02 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -2,7 +2,7 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + authentication::Credentials, config::SessionConfig, session::Session, spotify_id::{SpotifyId, SpotifyItemType}, }, playback::{ audio_backend, From 68bbb4fbbcb13df4b072b2a4c2c4de3b6c1cec4f Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Tue, 22 Nov 2022 12:17:29 +0000 Subject: [PATCH 273/561] Fix latest clippy and fmt warnings --- examples/play.rs | 5 ++++- src/main.rs | 2 +- src/player_event_handler.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index 37011d02..eb7dc382 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -2,7 +2,10 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, config::SessionConfig, session::Session, spotify_id::{SpotifyId, SpotifyItemType}, + authentication::Credentials, + config::SessionConfig, + session::Session, + spotify_id::{SpotifyId, SpotifyItemType}, }, playback::{ audio_backend, diff --git a/src/main.rs b/src/main.rs index d5c53925..22a5fada 100644 --- a/src/main.rs +++ b/src/main.rs @@ -695,7 +695,7 @@ fn get_setup() -> Setup { // Don't log creds. trace!("\t\t{} \"XXXXXXXX\"", opt); } else { - let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string()); + let value = matches.opt_str(opt).unwrap_or_default(); if value.is_empty() { trace!("\t\t{}", opt); } else { diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 44d92eb5..d9d6de21 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -286,7 +286,7 @@ fn run_program(env_vars: HashMap<&str, String>, onevent: &str) { onevent, env_vars ); - match Command::new(&v.remove(0)) + match Command::new(v.remove(0)) .args(&v) .envs(env_vars.iter()) .spawn() From bf7cbbaadd96a29dc1cb34bc8ab3533f44e6fc86 Mon Sep 17 00:00:00 2001 From: setime Date: Fri, 25 Nov 2022 09:57:14 +0100 Subject: [PATCH 274/561] Add an option to specify IPs that zeroconf will bind to (#1071) * added an option to specify ip addresses to which mDNS should bind (ignored by `DNS-SD`) * changed command line option to `zeroconf-interface` to be consistent with `zeroconf-port` use builder pattern to DRY up the code used macro to print warning message * fixing register error * renamed `bind_ip` variables to match the option to `zeroconf_ip`, to be more consistent * Changed user help Modified comments Added block for condition to clean the code Added new modification to the change log Co-authored-by: setime --- CHANGELOG.md | 1 + discovery/src/lib.rs | 50 +++++++++++++++++++++++++++++++------------- src/main.rs | 36 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f4dd956..c7ae5e98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,7 @@ https://github.com/librespot-org/librespot - [all] Check that array indexes are within bounds (panic safety) - [all] Wrap errors in librespot `Error` type (breaking) +- [connect] Add option on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD. - [connect] Add session events - [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs` - [contrib] Add `event_handler_example.py` diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 3c01003b..1764640b 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -47,6 +47,7 @@ pub struct Discovery { pub struct Builder { server_config: server::Config, port: u16, + zeroconf_ip: Vec, } /// Errors that can occur while setting up a [`Discovery`] instance. @@ -87,6 +88,7 @@ impl Builder { client_id: client_id.into(), }, port: 0, + zeroconf_ip: vec![], } } @@ -102,6 +104,12 @@ impl Builder { self } + /// Set the ip addresses on which it should listen to incoming connections. The default is all interfaces. + pub fn zeroconf_ip(mut self, zeroconf_ip: Vec) -> Self { + self.zeroconf_ip = zeroconf_ip; + 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 { @@ -117,24 +125,38 @@ impl Builder { let mut port = self.port; let name = self.server_config.name.clone().into_owned(); let server = DiscoveryServer::new(self.server_config, &mut port)??; + let _zeroconf_ip = self.zeroconf_ip; + 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=/"], - )?; + { + svc = dns_sd::DNSService::register( + Some(name.as_ref()), + "_spotify-connect._tcp", + None, + None, + 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=/"], - ); + { + let _svc = if !_zeroconf_ip.is_empty() { + libmdns::Responder::spawn_with_ip_list( + &tokio::runtime::Handle::current(), + _zeroconf_ip, + )? + } else { + libmdns::Responder::spawn(&tokio::runtime::Handle::current())? + }; + svc = _svc.register( + "_spotify-connect._tcp".to_owned(), + name, + port, + &["VERSION=1.0", "CPath=/"], + ); + } Ok(Discovery { server, _svc: svc }) } diff --git a/src/main.rs b/src/main.rs index 22a5fada..5bf56e0c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -185,6 +185,7 @@ struct Setup { zeroconf_port: u16, player_event_program: Option, emit_sink_events: bool, + zeroconf_ip: Vec, } fn get_setup() -> Setup { @@ -240,6 +241,7 @@ fn get_setup() -> Setup { const VOLUME_CTRL: &str = "volume-ctrl"; const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; + const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; @@ -258,6 +260,7 @@ fn get_setup() -> Setup { const DISABLE_GAPLESS_SHORT: &str = "g"; const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; const HELP_SHORT: &str = "h"; + const ZEROCONF_INTERFACE_SHORT: &str = "i"; const CACHE_SIZE_LIMIT_SHORT: &str = "M"; const MIXER_TYPE_SHORT: &str = "m"; const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; @@ -570,6 +573,12 @@ fn get_setup() -> Setup { AUTOPLAY, "Explicitly set autoplay {on|off}. Defaults to following the client setting.", "OVERRIDE", + ) + .optopt( + ZEROCONF_INTERFACE_SHORT, + ZEROCONF_INTERFACE, + "Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.", + "IP" ); #[cfg(feature = "passthrough-decoder")] @@ -1168,6 +1177,31 @@ fn get_setup() -> Setup { None => SessionConfig::default().autoplay, }; + let zeroconf_ip: Vec = if opt_present(ZEROCONF_INTERFACE) { + if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) { + zeroconf_ip + .split(',') + .map(|s| { + s.trim().parse::().unwrap_or_else(|_| { + invalid_error_msg( + ZEROCONF_INTERFACE, + ZEROCONF_INTERFACE_SHORT, + s, + "IPv4 and IPv6 addresses", + "", + ); + exit(1); + }) + }) + .collect() + } else { + warn!("Unable to use zeroconf-interface option, default to all interfaces."); + vec![] + } + } else { + vec![] + }; + let connect_config = { let connect_default_config = ConnectConfig::default(); @@ -1608,6 +1642,7 @@ fn get_setup() -> Setup { zeroconf_port, player_event_program, emit_sink_events, + zeroconf_ip, } } @@ -1640,6 +1675,7 @@ async fn main() { .name(setup.connect_config.name.clone()) .device_type(setup.connect_config.device_type) .port(setup.zeroconf_port) + .zeroconf_ip(setup.zeroconf_ip) .launch() { Ok(d) => discovery = Some(d), From 5cfdb8c9dde7ab6722d3990601daa918798ed0e0 Mon Sep 17 00:00:00 2001 From: ealasu Date: Sun, 27 Nov 2022 20:03:36 -0700 Subject: [PATCH 275/561] Parse local file IDs --- CHANGELOG.md | 1 + core/src/spotify_id.rs | 43 +++++++++++++++++++++++++++++++++--------- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ae5e98..1903db1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ https://github.com/librespot-org/librespot It supports a lot of functionality, including audio previews and image downloads even if librespot doesn't use that for playback itself. - [core] Support downloading of lyrics +- [core] Support parsing `SpotifyId` for local files - [main] Add all player events to `player_event_handler.rs` - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 3e913ce1..5057c135 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -21,6 +21,7 @@ pub enum SpotifyItemType { Playlist, Show, Track, + Local, Unknown, } @@ -33,6 +34,7 @@ impl From<&str> for SpotifyItemType { "playlist" => Self::Playlist, "show" => Self::Show, "track" => Self::Track, + "local" => Self::Local, _ => Self::Unknown, } } @@ -47,6 +49,7 @@ impl From for &str { SpotifyItemType::Playlist => "playlist", SpotifyItemType::Show => "show", SpotifyItemType::Track => "track", + SpotifyItemType::Local => "local", _ => "unknown", } } @@ -167,24 +170,30 @@ impl SpotifyId { /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn from_uri(src: &str) -> SpotifyIdResult { - let mut uri_parts: Vec<&str> = src.split(':').collect(); - // At minimum, should be `spotify:{type}:{id}` - if uri_parts.len() < 3 { - return Err(SpotifyIdError::InvalidFormat.into()); - } + let (scheme, tail) = src.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; + let (item_type, id) = tail.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; - if uri_parts[0] != "spotify" { + if scheme != "spotify" { return Err(SpotifyIdError::InvalidRoot.into()); } - let id = uri_parts.pop().unwrap_or_default(); + let item_type = item_type.into(); + + // Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/ + // TODO: find a way to add this local file ID to SpotifyId. + // One possible solution would be to copy the contents of `id` to a new String field in SpotifyId, + // but then we would need to remove the derived Copy trait, which would be a breaking change. + if item_type == SpotifyItemType::Local { + return Ok(Self { item_type, id: 0 }); + } + if id.len() != Self::SIZE_BASE62 { return Err(SpotifyIdError::InvalidId.into()); } Ok(Self { - item_type: uri_parts.pop().unwrap_or_default().into(), + item_type, ..Self::from_base62(id)? }) } @@ -534,7 +543,7 @@ mod tests { raw: &'static [u8], } - static CONV_VALID: [ConversionCase; 4] = [ + static CONV_VALID: [ConversionCase; 5] = [ ConversionCase { id: 238762092608182713602505436543891614649, kind: SpotifyItemType::Track, @@ -575,6 +584,14 @@ mod tests { 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216, ], }, + ConversionCase { + id: 0, + kind: SpotifyItemType::Local, + uri: "spotify:local:0000000000000000000000", + base16: "00000000000000000000000000000000", + base62: "0000000000000000000000", + raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, ]; static CONV_INVALID: [ConversionCase; 3] = [ @@ -676,6 +693,14 @@ mod tests { } } + #[test] + fn from_local_uri() { + let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap(); + + assert_eq!(actual.id, 0); + assert_eq!(actual.item_type, SpotifyItemType::Local); + } + #[test] fn to_uri() { for c in &CONV_VALID { From 7d00881dcd6ae7de8aae4a40ce3dfff52f66267a Mon Sep 17 00:00:00 2001 From: ealasu Date: Fri, 2 Dec 2022 17:38:39 -0700 Subject: [PATCH 276/561] Fix build error due to GitHub Actions updating to Ubuntu 22.04 https://github.com/actions/runner-images/issues/6399#issuecomment-1286050292 --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45ceba00..69b0572f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: 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: sudo apt-get update && sudo apt install -y libunwind-dev && 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 build --workspace --examples - run: cargo test --workspace @@ -231,7 +231,7 @@ jobs: 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: sudo apt-get update && sudo apt install -y libunwind-dev && 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 From edf646d4bb4bd0c12f7e38d82de03a32f8cb346d Mon Sep 17 00:00:00 2001 From: Oliver Cooper Date: Sun, 4 Dec 2022 00:25:27 +1300 Subject: [PATCH 277/561] Add activate and load functions to `Spirc` (#1075) --- CHANGELOG.md | 1 + connect/src/spirc.rs | 173 +++++++++++++++++++++++++-------------- examples/play_connect.rs | 96 ++++++++++++++++++++++ 3 files changed, 210 insertions(+), 60 deletions(-) create mode 100644 examples/play_connect.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1903db1f..54eaf48b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,7 @@ https://github.com/librespot-org/librespot disabled such content. Applications that use librespot as a library without Connect should use the 'filter-explicit-content' user attribute in the session. - [playback] Add metadata support via a `TrackChanged` event +- [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions ### Fixed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 44a29d7c..84c5e3e6 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -122,6 +122,36 @@ pub enum SpircCommand { Disconnect, SetPosition(u32), SetVolume(u16), + Activate, + Load(SpircLoadCommand), +} + +#[derive(Debug)] +pub struct SpircLoadCommand { + pub context_uri: String, + /// Whether the given tracks should immediately start playing, or just be initially loaded. + pub start_playing: bool, + pub shuffle: bool, + pub repeat: bool, + pub playing_track_index: u32, + pub tracks: Vec, +} + +impl From for State { + fn from(command: SpircLoadCommand) -> Self { + let mut state = State::new(); + state.set_context_uri(command.context_uri); + state.set_status(if command.start_playing { + PlayStatus::kPlayStatusPlay + } else { + PlayStatus::kPlayStatusStop + }); + state.set_shuffle(command.shuffle); + state.set_repeat(command.repeat); + state.set_playing_track_index(command.playing_track_index); + state.set_track(command.tracks.into()); + state + } } const CONTEXT_TRACKS_HISTORY: usize = 10; @@ -469,6 +499,12 @@ impl Spirc { pub fn disconnect(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Disconnect)?) } + pub fn activate(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Activate)?) + } + pub fn load(&self, command: SpircLoadCommand) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Load(command))?) + } } impl SpircTask { @@ -666,11 +702,24 @@ impl SpircTask { self.set_volume(volume); self.notify(None) } + SpircCommand::Load(command) => { + self.handle_load(&command.into())?; + self.notify(None) + } _ => Ok(()), } } else { - warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); - Ok(()) + match cmd { + SpircCommand::Activate => { + trace!("Received SpircCommand::{:?}", cmd); + self.handle_activate(); + self.notify(None) + } + _ => { + warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); + Ok(()) + } + } } } @@ -889,57 +938,7 @@ impl SpircTask { MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeLoad => { - if !self.device.get_is_active() { - let now = self.now_ms(); - self.device.set_is_active(true); - self.device.set_became_active_at(now); - self.player.emit_session_connected_event( - self.session.connection_id(), - self.session.username(), - ); - self.player.emit_session_client_changed_event( - self.session.client_id(), - self.session.client_name(), - self.session.client_brand_name(), - self.session.client_model_name(), - ); - - self.player - .emit_volume_changed_event(self.device.get_volume() as u16); - - self.player - .emit_auto_play_changed_event(self.session.autoplay()); - - self.player.emit_filter_explicit_content_changed_event( - self.session.filter_explicit_content(), - ); - - self.player - .emit_shuffle_changed_event(self.state.get_shuffle()); - - self.player - .emit_repeat_changed_event(self.state.get_repeat()); - } - - let context_uri = update.get_state().get_context_uri().to_owned(); - - // completely ignore local playback. - if context_uri.starts_with("spotify:local-files") { - self.notify(None)?; - return Err(SpircError::UnsupportedLocalPlayBack.into()); - } - - self.update_tracks(&update); - - if !self.state.get_track().is_empty() { - let start_playing = - 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.handle_stop(); - } - + self.handle_load(update.get_state())?; self.notify(None) } @@ -1021,7 +1020,7 @@ impl SpircTask { return Err(SpircError::UnsupportedLocalPlayBack.into()); } - self.update_tracks(&update); + self.update_tracks(update.get_state()); if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -1075,6 +1074,60 @@ impl SpircTask { self.player.stop(); } + fn handle_activate(&mut self) { + let now = self.now_ms(); + self.device.set_is_active(true); + self.device.set_became_active_at(now); + self.player + .emit_session_connected_event(self.session.connection_id(), self.session.username()); + self.player.emit_session_client_changed_event( + self.session.client_id(), + self.session.client_name(), + self.session.client_brand_name(), + self.session.client_model_name(), + ); + + self.player + .emit_volume_changed_event(self.device.get_volume() as u16); + + self.player + .emit_auto_play_changed_event(self.session.autoplay()); + + self.player + .emit_filter_explicit_content_changed_event(self.session.filter_explicit_content()); + + self.player + .emit_shuffle_changed_event(self.state.get_shuffle()); + + self.player + .emit_repeat_changed_event(self.state.get_repeat()); + } + + fn handle_load(&mut self, state: &State) -> Result<(), Error> { + if !self.device.get_is_active() { + self.handle_activate(); + } + + let context_uri = state.get_context_uri().to_owned(); + + // completely ignore local playback. + if context_uri.starts_with("spotify:local-files") { + self.notify(None)?; + return Err(SpircError::UnsupportedLocalPlayBack.into()); + } + + self.update_tracks(state); + + if !self.state.get_track().is_empty() { + let start_playing = state.get_status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, state.get_position_ms()); + } else { + info!("No more tracks left in queue"); + self.handle_stop(); + } + Ok(()) + } + fn handle_play(&mut self) { match self.play_status { SpircPlayStatus::Paused { @@ -1372,12 +1425,12 @@ impl SpircTask { } } - fn update_tracks(&mut self, frame: &protocol::spirc::Frame) { - trace!("State: {:#?}", frame.get_state()); + fn update_tracks(&mut self, state: &State) { + trace!("State: {:#?}", state); - let index = frame.get_state().get_playing_track_index(); - let context_uri = frame.get_state().get_context_uri(); - let tracks = frame.get_state().get_track(); + let index = state.get_playing_track_index(); + let context_uri = state.get_context_uri(); + let tracks = state.get_track(); trace!("Frame has {:?} tracks", tracks.len()); @@ -1395,7 +1448,7 @@ impl SpircTask { // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. - let state = frame.get_state(); + let state = state; if state.get_repeat() { self.state.set_repeat(true); } diff --git a/examples/play_connect.rs b/examples/play_connect.rs new file mode 100644 index 00000000..2b23a7d3 --- /dev/null +++ b/examples/play_connect.rs @@ -0,0 +1,96 @@ +use librespot::{ + core::{ + authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + }, + playback::{ + audio_backend, + config::{AudioFormat, PlayerConfig}, + mixer::NoOpVolume, + player::Player, + }, +}; +use librespot_connect::{ + config::ConnectConfig, + spirc::{Spirc, SpircLoadCommand}, +}; +use librespot_metadata::{Album, Metadata}; +use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig}; +use librespot_protocol::spirc::TrackRef; +use std::env; +use tokio::join; + +#[tokio::main] +async fn main() { + let session_config = SessionConfig::default(); + let player_config = PlayerConfig::default(); + let audio_format = AudioFormat::default(); + let connect_config = ConnectConfig::default(); + + let mut args: Vec<_> = env::args().collect(); + let context_uri = if args.len() == 4 { + args.pop().unwrap() + } else if args.len() == 3 { + String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv") + } else { + eprintln!("Usage: {} USERNAME PASSWORD (ALBUM URI)", args[0]); + return; + }; + + let credentials = Credentials::with_password(&args[1], &args[2]); + let backend = audio_backend::find(None).unwrap(); + + println!("Connecting..."); + let session = Session::new(session_config, None); + + let player = Player::new( + player_config, + session.clone(), + Box::new(NoOpVolume), + move || backend(None, audio_format), + ); + + let (spirc, spirc_task) = Spirc::new( + connect_config, + session.clone(), + credentials, + player, + Box::new(SoftMixer::open(MixerConfig::default())), + ) + .await + .unwrap(); + + join!(spirc_task, async { + let album = Album::get(&session, &SpotifyId::from_uri(&context_uri).unwrap()) + .await + .unwrap(); + let tracks = album + .tracks() + .map(|track_id| { + let mut track = TrackRef::new(); + track.set_gid(Vec::from(track_id.to_raw())); + track + }) + .collect(); + + println!( + "Playing album: {} by {}", + &album.name, + album + .artists + .first() + .map_or("unknown", |artist| &artist.name) + ); + + spirc.activate().unwrap(); + spirc + .load(SpircLoadCommand { + context_uri, + start_playing: true, + shuffle: false, + repeat: false, + playing_track_index: 0, // the index specifies which track in the context starts playing, in this case the first in the album + tracks, + }) + .unwrap(); + }); +} From f72048e5e1a6f74c5476eef89a1de37fa7bb118f Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Mon, 26 Dec 2022 18:03:27 +0100 Subject: [PATCH 278/561] metadata: add lyrics Save users from doing all the parsing themselves. --- CHANGELOG.md | 1 + Cargo.lock | 2 ++ metadata/Cargo.toml | 2 ++ metadata/src/lib.rs | 2 ++ metadata/src/lyrics.rs | 77 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 84 insertions(+) create mode 100644 metadata/src/lyrics.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 54eaf48b..1b3e1066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ https://github.com/librespot-org/librespot Connect should use the 'filter-explicit-content' user attribute in the session. - [playback] Add metadata support via a `TrackChanged` event - [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions +- [metadata] Add `Lyrics` ### Fixed diff --git a/Cargo.lock b/Cargo.lock index fe16c165..295681e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1500,6 +1500,8 @@ dependencies = [ "librespot-protocol", "log", "protobuf", + "serde", + "serde_json", "thiserror", "uuid", ] diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 490b5227..4d977bea 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -16,6 +16,8 @@ log = "0.4" protobuf = "2" thiserror = "1" uuid = { version = "1", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" [dependencies.librespot-core] path = "../core" diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index ef8443db..e7263cae 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -18,6 +18,7 @@ pub mod episode; pub mod error; pub mod external_id; pub mod image; +pub mod lyrics; pub mod playlist; mod request; pub mod restriction; @@ -33,6 +34,7 @@ use request::RequestResult; pub use album::Album; pub use artist::Artist; pub use episode::Episode; +pub use lyrics::Lyrics; pub use playlist::Playlist; pub use show::Show; pub use track::Track; diff --git a/metadata/src/lyrics.rs b/metadata/src/lyrics.rs new file mode 100644 index 00000000..35263e2d --- /dev/null +++ b/metadata/src/lyrics.rs @@ -0,0 +1,77 @@ +use bytes::Bytes; + +use librespot_core::{Error, FileId, Session, SpotifyId}; + +impl Lyrics { + pub async fn get(session: &Session, id: &SpotifyId) -> Result { + let spclient = session.spclient(); + let lyrics = spclient.get_lyrics(id).await?; + Self::try_from(&lyrics) + } + + pub async fn get_for_image( + session: &Session, + id: &SpotifyId, + image_id: &FileId, + ) -> Result { + let spclient = session.spclient(); + let lyrics = spclient.get_lyrics_for_image(id, image_id).await?; + Self::try_from(&lyrics) + } +} + +impl TryFrom<&Bytes> for Lyrics { + type Error = Error; + + fn try_from(lyrics: &Bytes) -> Result { + serde_json::from_slice(lyrics).map_err(|err| err.into()) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Lyrics { + pub colors: Colors, + pub has_vocal_removal: bool, + pub lyrics: LyricsInner, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Colors { + pub background: i32, + pub highlight_text: i32, + pub text: i32, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct LyricsInner { + // TODO: 'alternatives' field as an array but I don't know what it's meant for + pub fullscreen_action: String, + pub is_dense_typeface: bool, + pub is_rtl_language: bool, + pub language: String, + pub lines: Vec, + pub provider: String, + pub provider_display_name: String, + pub provider_lyrics_id: String, + pub sync_lyrics_uri: String, + pub sync_type: SyncType, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum SyncType { + Unsynced, + LineSynced, +} + +#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Line { + pub start_time_ms: String, + pub end_time_ms: String, + pub words: String, + // TODO: 'syllables' array +} From 7a259ccc4bf2b6c4c2b9750b7513e67e153c8bbf Mon Sep 17 00:00:00 2001 From: George Hahn Date: Mon, 2 Jan 2023 16:24:49 -0700 Subject: [PATCH 279/561] Parse named ID URIs --- CHANGELOG.md | 1 + core/src/spotify_id.rs | 32 +++++++++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54eaf48b..8007f93e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,7 @@ https://github.com/librespot-org/librespot downloads even if librespot doesn't use that for playback itself. - [core] Support downloading of lyrics - [core] Support parsing `SpotifyId` for local files +- [core] Support parsing `SpotifyId` for named playlists - [main] Add all player events to `player_event_handler.rs` - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 5057c135..da28f98e 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -170,9 +170,24 @@ impl SpotifyId { /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn from_uri(src: &str) -> SpotifyIdResult { - // At minimum, should be `spotify:{type}:{id}` - let (scheme, tail) = src.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; - let (item_type, id) = tail.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; + // Basic: `spotify:{type}:{id}` + // Named: `spotify:user:{user}:{type}:{id}` + // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}` + let mut parts = src.split(':'); + + let scheme = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; + + let item_type = { + let next = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; + if next == "user" { + let _username = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; + parts.next().ok_or(SpotifyIdError::InvalidFormat)? + } else { + next + } + }; + + let id = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; if scheme != "spotify" { return Err(SpotifyIdError::InvalidRoot.into()); @@ -701,6 +716,17 @@ mod tests { assert_eq!(actual.item_type, SpotifyItemType::Local); } + #[test] + fn from_named_uri() { + let actual = + NamedSpotifyId::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI") + .unwrap(); + + assert_eq!(actual.id, 136159921382084734723401526672209703396); + assert_eq!(actual.item_type, SpotifyItemType::Playlist); + assert_eq!(actual.username, "spotify"); + } + #[test] fn to_uri() { for c in &CONV_VALID { From 7f2cb684c981d8146ceae853eab9a4b1a772f549 Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Mon, 2 Jan 2023 19:01:35 +0100 Subject: [PATCH 280/561] fix clippy warnings --- connect/src/spirc.rs | 2 +- core/src/authentication.rs | 2 +- discovery/src/server.rs | 6 +++--- metadata/src/audio/item.rs | 2 +- playback/src/convert.rs | 8 +------- playback/src/lib.rs | 2 +- playback/src/player.rs | 2 +- protocol/build.rs | 2 +- 8 files changed, 10 insertions(+), 16 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 84c5e3e6..5705c8c2 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1138,7 +1138,7 @@ impl SpircTask { self.state.set_status(PlayStatus::kPlayStatusPlay); self.update_state_position(position_ms); self.play_status = SpircPlayStatus::Playing { - nominal_start_time: self.now_ms() as i64 - position_ms as i64, + nominal_start_time: self.now_ms() - position_ms as i64, preloading_of_next_track_triggered, }; } diff --git a/core/src/authentication.rs b/core/src/authentication.rs index b2bcad94..89117860 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -173,5 +173,5 @@ where D: serde::Deserializer<'de>, { let v: String = serde::Deserialize::deserialize(de)?; - base64::decode(&v).map_err(|e| serde::de::Error::custom(e.to_string())) + base64::decode(v).map_err(|e| serde::de::Error::custom(e.to_string())) } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index faea33a1..379988aa 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -61,7 +61,7 @@ impl RequestHandler { } fn handle_get_info(&self) -> Response { - let public_key = base64::encode(&self.keys.public_key()); + let public_key = base64::encode(self.keys.public_key()); let device_type: &str = self.config.device_type.into(); let mut active_user = String::new(); if let Some(username) = &self.username { @@ -139,7 +139,7 @@ impl RequestHandler { 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 = Sha1::digest(shared_key); let base_key = &base_key[..16]; let checksum_key = { @@ -179,7 +179,7 @@ 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)?; diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index 2a672075..b2789008 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -109,7 +109,7 @@ impl AudioItem { ) }; - let popularity = track.popularity.max(0).min(100) as u8; + let popularity = track.popularity.clamp(0, 100) as u8; let number = track.number.max(0) as u32; let disc_number = track.disc_number.max(0) as u32; diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 1bc8a88e..a7efe452 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -76,13 +76,7 @@ impl Converter { let min = -factor; let max = factor - 1.0; - if int_value < min { - min - } else if int_value > max { - max - } else { - int_value - } + int_value.clamp(min, max) } pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { diff --git a/playback/src/lib.rs b/playback/src/lib.rs index a52ca2fa..43a5b4f0 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -15,6 +15,6 @@ pub mod player; pub const SAMPLE_RATE: u32 = 44100; pub const NUM_CHANNELS: u8 = 2; -pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE * NUM_CHANNELS as u32; pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; diff --git a/playback/src/player.rs b/playback/src/player.rs index f0f0d492..3fdf41b7 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1311,7 +1311,7 @@ impl Future for PlayerInternal { self.send_event(PlayerEvent::PositionCorrection { play_request_id, track_id, - position_ms: new_stream_position_ms as u32, + position_ms: new_stream_position_ms, }); } } diff --git a/protocol/build.rs b/protocol/build.rs index b7c3f44d..b63fa1aa 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -9,7 +9,7 @@ fn out_dir() -> PathBuf { } fn cleanup() { - let _ = fs::remove_dir_all(&out_dir()); + let _ = fs::remove_dir_all(out_dir()); } fn compile() { From 512c79926650ba81902accf3d7c96adaa33ef39c Mon Sep 17 00:00:00 2001 From: George Hahn Date: Tue, 3 Jan 2023 22:26:28 -0700 Subject: [PATCH 281/561] Handle microsecond timestamps --- metadata/src/playlist/list.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index c813a14d..36ff3a55 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -129,6 +129,23 @@ impl Metadata for Playlist { impl TryFrom<&::Message> for SelectedListContent { type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { + let timestamp = playlist.get_timestamp(); + + let timestamp = if timestamp > 1672809484000 { + // timestamp is way out of range for milliseconds. Some seem to be in microseconds? + // Observed on playlists where: + // format: "artist-mix-reader" + // format_attributes { + // key: "mediaListConfig" + // value: "spotify:medialistconfig:artist-seed-mix:default_v18" + // } + warn!("timestamp is very large; assuming it's in microseconds"); + timestamp / 1000 + } else { + timestamp + }; + let timestamp = Date::from_timestamp_ms(timestamp)?; + Ok(Self { revision: playlist.get_revision().to_owned(), length: playlist.get_length(), @@ -144,7 +161,7 @@ impl TryFrom<&::Message> for SelectedListContent { has_multiple_heads: playlist.get_multiple_heads(), is_up_to_date: playlist.get_up_to_date(), nonces: playlist.get_nonces().into(), - timestamp: Date::from_timestamp_ms(playlist.get_timestamp())?, + timestamp, owner_username: playlist.get_owner_username().to_owned(), has_abuse_reporting: playlist.get_abuse_reporting_enabled(), capabilities: playlist.get_capabilities().into(), From c2c31247b559ebcee13539feef0bdae0db569584 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Tue, 3 Jan 2023 22:28:36 -0700 Subject: [PATCH 282/561] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 54eaf48b..45b135c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,7 @@ https://github.com/librespot-org/librespot now follows the setting in the Connect client that controls it. (breaking) - [metadata] Most metadata is now retrieved with the `spclient` (breaking) - [metadata] Playlists are moved to the `playlist4_external` protobuf (breaking) +- [metadata] Handle playlists that are sent with microsecond-based timestamps - [playback] The audio decoder has been switched from `lewton` to `Symphonia`. This improves the Vorbis sound quality, adds support for MP3 as well as for FLAC in the future. (breaking) From ce4be7171909d9aec93f58b1f7fe684e3b085827 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Tue, 3 Jan 2023 22:38:54 -0700 Subject: [PATCH 283/561] fix timestamp heuristic (year 2200+) --- metadata/src/playlist/list.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 36ff3a55..dcbd9ea1 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -130,8 +130,7 @@ impl TryFrom<&::Message> for SelectedListContent { type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { let timestamp = playlist.get_timestamp(); - - let timestamp = if timestamp > 1672809484000 { + let timestamp = if timestamp > 9295169800000 { // timestamp is way out of range for milliseconds. Some seem to be in microseconds? // Observed on playlists where: // format: "artist-mix-reader" From 14ace17967c4746ce7df5b70da3a277ee7cbd8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Wed, 4 Jan 2023 20:39:30 +0100 Subject: [PATCH 284/561] Update sysinfo, fixing a future incompatibility warning (#1090) --- Cargo.lock | 16 ++++++++-------- core/Cargo.toml | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 295681e4..47a10698 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1241,9 +1241,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.133" +version = "0.2.139" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libgit2-sys" @@ -1750,9 +1750,9 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "ntapi" -version = "0.3.7" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" dependencies = [ "winapi", ] @@ -1946,9 +1946,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" [[package]] name = "openssl-probe" @@ -2788,9 +2788,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.25.3" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71eb43e528fdc239f08717ec2a378fdb017dddbc3412de15fff527554591a66c" +checksum = "17351d0e9eb8841897b14e9669378f3c69fb57779cc04f8ca9a9d512edfb2563" dependencies = [ "cfg-if", "core-foundation-sys", diff --git a/core/Cargo.toml b/core/Cargo.toml index 5974ef2a..0bd512da 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -49,7 +49,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = "0.10" shannon = "0.2" -sysinfo = { version = "0.25", default-features = false } +sysinfo = { version = "0.27", default-features = false } thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } From 64603f651e823287fa5135ab06f627288758a6ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Jan 2023 21:47:45 +0000 Subject: [PATCH 285/561] Bump tokio from 1.21.2 to 1.23.1 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.21.2 to 1.23.1. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.21.2...tokio-1.23.1) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 79 ++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 47a10698..b522f169 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1639,7 +1639,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -2013,7 +2013,7 @@ dependencies = [ "redox_syscall", "smallvec", "thread-id", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -2461,7 +2461,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" dependencies = [ "lazy_static", - "windows-sys", + "windows-sys 0.36.1", ] [[package]] @@ -2895,9 +2895,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.21.2" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e03c497dc955702ba729190dc4aac6f2a0ce97f913e5b1b5912fc5039d9099" +checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8" dependencies = [ "autocfg", "bytes", @@ -2910,7 +2910,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.42.0", ] [[package]] @@ -3303,43 +3303,100 @@ version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", + "windows_aarch64_msvc 0.36.1", + "windows_i686_gnu 0.36.1", + "windows_i686_msvc 0.36.1", + "windows_x86_64_gnu 0.36.1", + "windows_x86_64_msvc 0.36.1", ] +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc 0.42.0", + "windows_i686_gnu 0.42.0", + "windows_i686_msvc 0.42.0", + "windows_x86_64_gnu 0.42.0", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc 0.42.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + [[package]] name = "windows_i686_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + [[package]] name = "windows_i686_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + [[package]] name = "windows_x86_64_gnu" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + [[package]] name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" + [[package]] name = "zerocopy" version = "0.6.1" From 3662302196c1aeb35501db717b88152c50212117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tyrychtr?= Date: Tue, 17 Jan 2023 21:46:14 +0100 Subject: [PATCH 286/561] Update protobuf and related crates to 3.x (#1092) --- Cargo.lock | 95 +++++-- connect/Cargo.toml | 2 +- connect/src/spirc.rs | 416 ++++++++++++---------------- core/Cargo.toml | 2 +- core/src/authentication.rs | 6 +- core/src/cdn_url.rs | 11 +- core/src/connection/handshake.rs | 68 +++-- core/src/connection/mod.rs | 30 +- core/src/date.rs | 18 +- core/src/error.rs | 2 +- core/src/file_id.rs | 6 +- core/src/mercury/mod.rs | 4 +- core/src/spclient.rs | 91 +++--- core/src/spotify_id.rs | 24 +- metadata/Cargo.toml | 2 +- metadata/src/album.rs | 48 ++-- metadata/src/artist.rs | 67 ++--- metadata/src/audio/file.rs | 8 +- metadata/src/availability.rs | 6 +- metadata/src/content_rating.rs | 4 +- metadata/src/copyright.rs | 6 +- metadata/src/episode.rs | 48 ++-- metadata/src/external_id.rs | 4 +- metadata/src/image.rs | 14 +- metadata/src/lib.rs | 2 +- metadata/src/playlist/annotation.rs | 22 +- metadata/src/playlist/attribute.rs | 71 ++--- metadata/src/playlist/diff.rs | 21 +- metadata/src/playlist/item.rs | 22 +- metadata/src/playlist/list.rs | 38 ++- metadata/src/playlist/operation.rs | 44 +-- metadata/src/playlist/permission.rs | 18 +- metadata/src/restriction.rs | 27 +- metadata/src/sale_period.rs | 8 +- metadata/src/show.rs | 36 +-- metadata/src/track.rs | 49 ++-- protocol/Cargo.toml | 4 +- protocol/build.rs | 22 +- 38 files changed, 694 insertions(+), 672 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b522f169..0f24802d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -441,6 +441,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + [[package]] name = "encoding_rs" version = "0.8.31" @@ -1545,7 +1551,7 @@ version = "0.5.0-dev" dependencies = [ "glob", "protobuf", - "protobuf-codegen-pure", + "protobuf-codegen", ] [[package]] @@ -2183,36 +2189,62 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.44" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "2.28.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" - -[[package]] -name = "protobuf-codegen" -version = "2.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "033460afb75cf755fcfc16dfaed20b86468082a2ea24e05ac35ab4a099a017d6" +checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" dependencies = [ - "protobuf", + "once_cell", + "protobuf-support", + "thiserror", ] [[package]] -name = "protobuf-codegen-pure" -version = "2.28.0" +name = "protobuf-codegen" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a29399fc94bcd3eeaa951c715f7bea69409b2445356b00519740bcd6ddd865" +checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" dependencies = [ + "anyhow", + "once_cell", "protobuf", - "protobuf-codegen", + "protobuf-parse", + "regex", + "tempfile", + "thiserror", +] + +[[package]] +name = "protobuf-parse" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +dependencies = [ + "thiserror", ] [[package]] @@ -2227,9 +2259,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ "proc-macro2", ] @@ -2765,9 +2797,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.101" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" dependencies = [ "proc-macro2", "quote", @@ -2838,18 +2870,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ "proc-macro2", "quote", @@ -3063,9 +3095,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" [[package]] name = "unicode-normalization" @@ -3266,6 +3298,17 @@ dependencies = [ "untrusted", ] +[[package]] +name = "which" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +dependencies = [ + "either", + "libc", + "once_cell", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 108a3804..1e02df84 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" form_urlencoded = "1.0" futures-util = "0.3" log = "0.4" -protobuf = "2" +protobuf = "3" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 5705c8c2..857a4409 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -9,7 +9,7 @@ use std::{ use futures_util::{stream::FusedStream, FutureExt, StreamExt}; use protobuf::{self, Message}; -use rand::seq::SliceRandom; +use rand::prelude::SliceRandom; use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -149,7 +149,7 @@ impl From for State { state.set_shuffle(command.shuffle); state.set_repeat(command.repeat); state.set_playing_track_index(command.playing_track_index); - state.set_track(command.tracks.into()); + state.track = command.tracks; state } } @@ -174,154 +174,93 @@ fn initial_state() -> State { frame } +fn int_capability(typ: protocol::spirc::CapabilityType, val: i64) -> protocol::spirc::Capability { + let mut cap = protocol::spirc::Capability::new(); + cap.set_typ(typ); + cap.intValue.push(val); + cap +} + fn initial_device_state(config: ConnectConfig) -> DeviceState { - { - let mut msg = DeviceState::new(); - msg.set_sw_version(version::SEMVER.to_string()); - msg.set_is_active(false); - msg.set_can_play(true); - msg.set_volume(0); - msg.set_name(config.name); - { - let repeated = msg.mut_capabilities(); - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kCanBePlayer); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kDeviceType); - { - let repeated = msg.mut_intValue(); - repeated.push(config.device_type as i64) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kGaiaEqConnectId); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - // TODO: implement logout - msg.set_typ(protocol::spirc::CapabilityType::kSupportsLogout); - { - let repeated = msg.mut_intValue(); - repeated.push(0) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kIsObservable); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kVolumeSteps); - { - let repeated = msg.mut_intValue(); - if config.has_volume_ctrl { - repeated.push(VOLUME_STEPS) - } else { - repeated.push(0) - } - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kSupportsPlaylistV2); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kSupportsExternalEpisodes); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - // TODO: how would such a rename command be triggered? Handle it. - msg.set_typ(protocol::spirc::CapabilityType::kSupportsRename); - { - let repeated = msg.mut_intValue(); - repeated.push(1) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kCommandAcks); - { - let repeated = msg.mut_intValue(); - repeated.push(0) - }; - msg - }; - { - let msg = repeated.push_default(); - // TODO: does this mean local files or the local network? - // LAN may be an interesting privacy toggle. - msg.set_typ(protocol::spirc::CapabilityType::kRestrictToLocal); - { - let repeated = msg.mut_intValue(); - repeated.push(0) - }; - msg - }; - { - let msg = repeated.push_default(); - // TODO: what does this hide, or who do we hide from? - // May be an interesting privacy toggle. - msg.set_typ(protocol::spirc::CapabilityType::kHidden); - { - let repeated = msg.mut_intValue(); - repeated.push(0) - }; - msg - }; - { - let msg = repeated.push_default(); - msg.set_typ(protocol::spirc::CapabilityType::kSupportedTypes); - { - let repeated = msg.mut_stringValue(); - repeated.push("audio/episode".to_string()); - repeated.push("audio/episode+track".to_string()); - repeated.push("audio/track".to_string()); - // other known types: - // - "audio/ad" - // - "audio/interruption" - // - "audio/local" - // - "video/ad" - // - "video/episode" - }; - msg - }; - }; - msg - } + let mut msg = DeviceState::new(); + msg.set_sw_version(version::SEMVER.to_string()); + msg.set_is_active(false); + msg.set_can_play(true); + msg.set_volume(0); + msg.set_name(config.name); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kCanBePlayer, + 1, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kDeviceType, + config.device_type as i64, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kGaiaEqConnectId, + 1, + )); + // TODO: implement logout + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kSupportsLogout, + 0, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kIsObservable, + 1, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kVolumeSteps, + if config.has_volume_ctrl { + VOLUME_STEPS + } else { + 0 + }, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kSupportsPlaylistV2, + 1, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kSupportsExternalEpisodes, + 1, + )); + // TODO: how would such a rename command be triggered? Handle it. + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kSupportsRename, + 1, + )); + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kCommandAcks, + 0, + )); + // TODO: does this mean local files or the local network? + // LAN may be an interesting privacy toggle. + msg.capabilities.push(int_capability( + protocol::spirc::CapabilityType::kRestrictToLocal, + 0, + )); + // TODO: what does this hide, or who do we hide from? + // May be an interesting privacy toggle. + msg.capabilities + .push(int_capability(protocol::spirc::CapabilityType::kHidden, 0)); + let mut supported_types = protocol::spirc::Capability::new(); + supported_types.set_typ(protocol::spirc::CapabilityType::kSupportedTypes); + supported_types + .stringValue + .push("audio/episode".to_string()); + supported_types + .stringValue + .push("audio/episode+track".to_string()); + supported_types.stringValue.push("audio/track".to_string()); + // other known types: + // - "audio/ad" + // - "audio/interruption" + // - "audio/local" + // - "video/ad" + // - "video/episode" + msg.capabilities.push(supported_types); + msg } fn url_encode(bytes: impl AsRef<[u8]>) -> String { @@ -583,8 +522,8 @@ impl SpircTask { self.session.spclient().get_next_page(&context_uri).await } else { // only send previous tracks that were before the current playback position - let current_position = self.state.get_playing_track_index() as usize; - let previous_tracks = self.state.get_track()[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); + let current_position = self.state.playing_track_index() as usize; + let previous_tracks = self.state.track[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); let scope = if self.autoplay_context { "stations" // this returns a `StationContext` but we deserialize it into a `PageContext` @@ -602,7 +541,7 @@ impl SpircTask { info!( "Resolved {:?} tracks from <{:?}>", context.tracks.len(), - self.state.get_context_uri(), + self.state.context_uri(), ); Some(context) } @@ -651,7 +590,7 @@ impl SpircTask { rx.close() } Ok(()) - } else if self.device.get_is_active() { + } else if self.device.is_active() { trace!("Received SpircCommand::{:?}", cmd); match cmd { SpircCommand::Play => { @@ -848,16 +787,16 @@ impl SpircTask { fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { trace!("Received attributes update: {:#?}", update); let attributes: UserAttributes = update - .get_pairs() + .pairs .iter() - .map(|pair| (pair.get_key().to_owned(), pair.get_value().to_owned())) + .map(|pair| (pair.key().to_owned(), pair.value().to_owned())) .collect(); 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(); + for attribute in mutation.fields.iter() { + let key = &attribute.name; if key == "autoplay" && self.session.config().autoplay.is_some() { trace!("Autoplay override active. Ignoring mutation."); @@ -902,30 +841,29 @@ impl SpircTask { // First see if this update was intended for us. let device_id = &self.ident; - let ident = update.get_ident(); + let ident = update.ident(); if ident == device_id - || (!update.get_recipient().is_empty() && !update.get_recipient().contains(device_id)) + || (!update.recipient.is_empty() && !update.recipient.contains(device_id)) { return Err(SpircError::Ident(ident.to_string()).into()); } let old_client_id = self.session.client_id(); - for entry in update.get_device_state().get_metadata().iter() { - match entry.get_field_type() { - "client_id" => self.session.set_client_id(entry.get_metadata()), - "brand_display_name" => self.session.set_client_brand_name(entry.get_metadata()), - "model_display_name" => self.session.set_client_model_name(entry.get_metadata()), + for entry in update.device_state.metadata.iter() { + match entry.type_() { + "client_id" => self.session.set_client_id(entry.metadata()), + "brand_display_name" => self.session.set_client_brand_name(entry.metadata()), + "model_display_name" => self.session.set_client_model_name(entry.metadata()), _ => (), } } - self.session - .set_client_name(update.get_device_state().get_name()); + self.session.set_client_name(update.device_state.name()); let new_client_id = self.session.client_id(); - if self.device.get_is_active() && new_client_id != old_client_id { + if self.device.is_active() && new_client_id != old_client_id { self.player.emit_session_client_changed_event( new_client_id, self.session.client_name(), @@ -934,11 +872,11 @@ impl SpircTask { ); } - match update.get_typ() { + match update.typ() { MessageType::kMessageTypeHello => self.notify(Some(ident)), MessageType::kMessageTypeLoad => { - self.handle_load(update.get_state())?; + self.handle_load(update.state.get_or_default())?; self.notify(None) } @@ -978,7 +916,7 @@ impl SpircTask { } MessageType::kMessageTypeRepeat => { - let repeat = update.get_state().get_repeat(); + let repeat = update.state.repeat(); self.state.set_repeat(repeat); self.player.emit_repeat_changed_event(repeat); @@ -987,11 +925,11 @@ impl SpircTask { } MessageType::kMessageTypeShuffle => { - let shuffle = update.get_state().get_shuffle(); + let shuffle = update.state.shuffle(); self.state.set_shuffle(shuffle); if shuffle { - let current_index = self.state.get_playing_track_index(); - let tracks = self.state.mut_track(); + let current_index = self.state.playing_track_index(); + let tracks = &mut self.state.track; if !tracks.is_empty() { tracks.swap(0, current_index as usize); if let Some((_, rest)) = tracks.split_first_mut() { @@ -1007,12 +945,12 @@ impl SpircTask { } MessageType::kMessageTypeSeek => { - self.handle_seek(update.get_position()); + self.handle_seek(update.position()); self.notify(None) } MessageType::kMessageTypeReplace => { - let context_uri = update.get_state().get_context_uri().to_owned(); + let context_uri = update.state.context_uri().to_owned(); // completely ignore local playback. if context_uri.starts_with("spotify:local-files") { @@ -1020,7 +958,7 @@ impl SpircTask { return Err(SpircError::UnsupportedLocalPlayBack.into()); } - self.update_tracks(update.get_state()); + self.update_tracks(update.state.get_or_default()); if let SpircPlayStatus::Playing { preloading_of_next_track_triggered, @@ -1043,15 +981,14 @@ impl SpircTask { } MessageType::kMessageTypeVolume => { - self.set_volume(update.get_volume() as u16); + self.set_volume(update.volume() as u16); self.notify(None) } MessageType::kMessageTypeNotify => { - if self.device.get_is_active() - && update.get_device_state().get_is_active() - && self.device.get_became_active_at() - <= update.get_device_state().get_became_active_at() + if self.device.is_active() + && update.device_state.is_active() + && self.device.became_active_at() <= update.device_state.became_active_at() { self.handle_disconnect(); } @@ -1088,7 +1025,7 @@ impl SpircTask { ); self.player - .emit_volume_changed_event(self.device.get_volume() as u16); + .emit_volume_changed_event(self.device.volume() as u16); self.player .emit_auto_play_changed_event(self.session.autoplay()); @@ -1096,19 +1033,17 @@ impl SpircTask { self.player .emit_filter_explicit_content_changed_event(self.session.filter_explicit_content()); - self.player - .emit_shuffle_changed_event(self.state.get_shuffle()); + self.player.emit_shuffle_changed_event(self.state.shuffle()); - self.player - .emit_repeat_changed_event(self.state.get_repeat()); + self.player.emit_repeat_changed_event(self.state.repeat()); } fn handle_load(&mut self, state: &State) -> Result<(), Error> { - if !self.device.get_is_active() { + if !self.device.is_active() { self.handle_activate(); } - let context_uri = state.get_context_uri().to_owned(); + let context_uri = state.context_uri().to_owned(); // completely ignore local playback. if context_uri.starts_with("spotify:local-files") { @@ -1118,9 +1053,9 @@ impl SpircTask { self.update_tracks(state); - if !self.state.get_track().is_empty() { - let start_playing = state.get_status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, state.get_position_ms()); + if !self.state.track.is_empty() { + let start_playing = state.status() == PlayStatus::kPlayStatusPlay; + self.load_track(start_playing, state.position_ms()); } else { info!("No more tracks left in queue"); self.handle_stop(); @@ -1216,11 +1151,9 @@ impl SpircTask { fn consume_queued_track(&mut self) -> usize { // Removes current track if it is queued // Returns the index of the next track - let current_index = self.state.get_playing_track_index() as usize; - if (current_index < self.state.get_track().len()) - && self.state.get_track()[current_index].get_queued() - { - self.state.mut_track().remove(current_index); + let current_index = self.state.playing_track_index() as usize; + if (current_index < self.state.track.len()) && self.state.track[current_index].queued() { + self.state.track.remove(current_index); current_index } else { current_index + 1 @@ -1228,7 +1161,7 @@ impl SpircTask { } fn preview_next_track(&mut self) -> Option { - self.get_track_id_to_play_from_playlist(self.state.get_playing_track_index() + 1) + self.get_track_id_to_play_from_playlist(self.state.playing_track_index() + 1) .map(|(track_id, _)| track_id) } @@ -1260,27 +1193,23 @@ impl SpircTask { let unavailables = self.get_track_index_for_spotify_id(&track_id, 0); for &index in unavailables.iter() { let mut unplayable_track_ref = TrackRef::new(); - unplayable_track_ref.set_gid(self.state.get_track()[index].get_gid().to_vec()); + unplayable_track_ref.set_gid(self.state.track[index].gid().to_vec()); // Misuse context field to flag the track unplayable_track_ref.set_context(String::from("NonPlayable")); - std::mem::swap( - &mut self.state.mut_track()[index], - &mut unplayable_track_ref, - ); + std::mem::swap(&mut self.state.track[index], &mut unplayable_track_ref); debug!( "Marked <{:?}> at {:?} as NonPlayable", - self.state.get_track()[index], - index, + self.state.track[index], index, ); } self.handle_preload_next_track(); } fn handle_next(&mut self) { - let context_uri = self.state.get_context_uri().to_owned(); - let mut tracks_len = self.state.get_track().len() as u32; + let context_uri = self.state.context_uri().to_owned(); + let mut tracks_len = self.state.track.len() as u32; let mut new_index = self.consume_queued_track() as u32; - let mut continue_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; + let mut continue_playing = self.state.status() == PlayStatus::kPlayStatusPlay; let update_tracks = self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD; @@ -1298,7 +1227,7 @@ impl SpircTask { if let Some(ref context) = self.context { self.resolve_context = Some(context.next_page_url.to_owned()); self.update_tracks_from_context(); - tracks_len = self.state.get_track().len() as u32; + tracks_len = self.state.track.len() as u32; } } @@ -1311,12 +1240,12 @@ impl SpircTask { debug!("Starting autoplay for <{}>", context_uri); // force reloading the current context with an autoplay context self.autoplay_context = true; - self.resolve_context = Some(self.state.get_context_uri().to_owned()); + self.resolve_context = Some(self.state.context_uri().to_owned()); self.update_tracks_from_context(); self.player.set_auto_normalise_as_album(false); } else { new_index = 0; - continue_playing &= self.state.get_repeat(); + continue_playing &= self.state.repeat(); debug!("Looping back to start, repeat is {}", continue_playing); } } @@ -1342,29 +1271,29 @@ impl SpircTask { let mut queue_tracks = Vec::new(); { let queue_index = self.consume_queued_track(); - let tracks = self.state.mut_track(); - while queue_index < tracks.len() && tracks[queue_index].get_queued() { + let tracks = &mut self.state.track; + while queue_index < tracks.len() && tracks[queue_index].queued() { queue_tracks.push(tracks.remove(queue_index)); } } - let current_index = self.state.get_playing_track_index(); + let current_index = self.state.playing_track_index(); let new_index = if current_index > 0 { current_index - 1 - } else if self.state.get_repeat() { - self.state.get_track().len() as u32 - 1 + } else if self.state.repeat() { + self.state.track.len() as u32 - 1 } else { 0 }; // Reinsert queued tracks after the new playing track. let mut pos = (new_index + 1) as usize; for track in queue_tracks { - self.state.mut_track().insert(pos, track); + self.state.track.insert(pos, track); pos += 1; } self.state.set_playing_track_index(new_index); - let start_playing = self.state.get_status() == PlayStatus::kPlayStatusPlay; + let start_playing = self.state.status() == PlayStatus::kPlayStatusPlay; self.load_track(start_playing, 0); } else { self.handle_seek(0); @@ -1372,12 +1301,12 @@ impl SpircTask { } fn handle_volume_up(&mut self) { - let volume = (self.device.get_volume() as u16).saturating_add(VOLUME_STEP_SIZE); + let volume = (self.device.volume() as u16).saturating_add(VOLUME_STEP_SIZE); self.set_volume(volume); } fn handle_volume_down(&mut self) { - let volume = (self.device.get_volume() as u16).saturating_sub(VOLUME_STEP_SIZE); + let volume = (self.device.volume() as u16).saturating_sub(VOLUME_STEP_SIZE); self.set_volume(volume); } @@ -1404,18 +1333,17 @@ impl SpircTask { debug!("Adding {:?} tracks from context to frame", new_tracks.len()); - let mut track_vec = self.state.take_track().into_vec(); + let mut track_vec = self.state.track.clone(); if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { track_vec.drain(0..head); } track_vec.extend_from_slice(new_tracks); - self.state - .set_track(protobuf::RepeatedField::from_vec(track_vec)); + self.state.track = track_vec; // Update playing index if let Some(new_index) = self .state - .get_playing_track_index() + .playing_track_index() .checked_sub(CONTEXT_TRACKS_HISTORY as u32) { self.state.set_playing_track_index(new_index); @@ -1428,9 +1356,9 @@ impl SpircTask { fn update_tracks(&mut self, state: &State) { trace!("State: {:#?}", state); - let index = state.get_playing_track_index(); - let context_uri = state.get_context_uri(); - let tracks = state.get_track(); + let index = state.playing_track_index(); + let context_uri = state.context_uri(); + let tracks = &state.track; trace!("Frame has {:?} tracks", tracks.len()); @@ -1443,16 +1371,16 @@ impl SpircTask { .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); self.state.set_playing_track_index(index); - self.state.set_track(tracks.iter().cloned().collect()); + self.state.track = tracks.to_vec(); self.state.set_context_uri(context_uri.to_owned()); // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. let state = state; - if state.get_repeat() { + if state.repeat() { self.state.set_repeat(true); } - if state.get_shuffle() { + if state.shuffle() { self.state.set_shuffle(true); } } @@ -1463,10 +1391,10 @@ impl SpircTask { track_id: &SpotifyId, start_index: usize, ) -> Vec { - let index: Vec = self.state.get_track()[start_index..] + let index: Vec = self.state.track[start_index..] .iter() .enumerate() - .filter(|&(_, track_ref)| track_ref.get_gid() == track_id.to_raw()) + .filter(|&(_, track_ref)| track_ref.gid() == track_id.to_raw()) .map(|(idx, _)| start_index + idx) .collect(); index @@ -1474,11 +1402,11 @@ impl SpircTask { // Broken out here so we can refactor this later when we move to SpotifyObjectID or similar fn track_ref_is_unavailable(&self, track_ref: &TrackRef) -> bool { - track_ref.get_context() == "NonPlayable" + track_ref.context() == "NonPlayable" } fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { - let tracks_len = self.state.get_track().len(); + let tracks_len = self.state.track.len(); // Guard against tracks_len being zero to prevent // 'index out of bounds: the len is 0 but the index is 0' @@ -1500,7 +1428,7 @@ impl SpircTask { // tracks in each frame either have a gid or uri (that may or may not be a valid track) // E.g - context based frames sometimes contain tracks with - let mut track_ref = self.state.get_track()[new_playlist_index].clone(); + let mut track_ref = self.state.track[new_playlist_index].clone(); let mut track_id = SpotifyId::try_from(&track_ref); while self.track_ref_is_unavailable(&track_ref) || track_id.is_err() { warn!( @@ -1517,7 +1445,7 @@ impl SpircTask { warn!("No playable track found in state: {:?}", self.state); return None; } - track_ref = self.state.get_track()[new_playlist_index].clone(); + track_ref = self.state.track[new_playlist_index].clone(); track_id = SpotifyId::try_from(&track_ref); } @@ -1528,7 +1456,7 @@ impl SpircTask { } fn load_track(&mut self, start_playing: bool, position_ms: u32) { - let index = self.state.get_playing_track_index(); + let index = self.state.playing_track_index(); match self.get_track_id_to_play_from_playlist(index) { Some((track, index)) => { @@ -1556,7 +1484,7 @@ impl SpircTask { } fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> { - let status = self.state.get_status(); + let status = self.state.status(); // When in loading state, the Spotify UI is disabled for interaction. // On desktop this isn't so bad but on mobile it means that the bottom @@ -1575,7 +1503,7 @@ impl SpircTask { } fn set_volume(&mut self, volume: u16) { - let old_volume = self.device.get_volume(); + let old_volume = self.device.volume(); let new_volume = volume as u32; if old_volume != new_volume { self.device.set_volume(new_volume); @@ -1583,7 +1511,7 @@ impl SpircTask { if let Some(cache) = self.session.cache() { cache.save_volume(volume) } - if self.device.get_is_active() { + if self.device.is_active() { self.player.emit_volume_changed_event(volume); } } @@ -1612,25 +1540,25 @@ impl<'a> CommandSender<'a> { frame.set_ident(spirc.ident.clone()); frame.set_seq_nr(spirc.sequence.get()); frame.set_typ(cmd); - frame.set_device_state(spirc.device.clone()); + *frame.device_state.mut_or_insert_default() = spirc.device.clone(); frame.set_state_update_id(spirc.now_ms()); CommandSender { spirc, frame } } fn recipient(mut self, recipient: &'a str) -> CommandSender<'_> { - self.frame.mut_recipient().push(recipient.to_owned()); + self.frame.recipient.push(recipient.to_owned()); self } #[allow(dead_code)] fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> { - self.frame.set_state(state); + *self.frame.state.mut_or_insert_default() = state; 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()); + if self.frame.state.is_none() && self.spirc.device.is_active() { + *self.frame.state.mut_or_insert_default() = self.spirc.state.clone(); } self.spirc.sender.send(self.frame.write_to_bytes()?) diff --git a/core/Cargo.toml b/core/Cargo.toml index 0bd512da..26c4b5b1 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] } priority-queue = "1.2" -protobuf = "2" +protobuf = "3" quick-xml = { version = "0.23", features = ["serialize"] } rand = "0.8" rsa = "0.6" diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 89117860..655a5311 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -4,7 +4,7 @@ use aes::Aes192; use byteorder::{BigEndian, ByteOrder}; use hmac::Hmac; use pbkdf2::pbkdf2; -use protobuf::ProtobufEnum; +use protobuf::Enum; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; use thiserror::Error; @@ -145,7 +145,7 @@ impl Credentials { fn serialize_protobuf_enum(v: &T, ser: S) -> Result where - T: ProtobufEnum, + T: Enum, S: serde::Serializer, { serde::Serialize::serialize(&v.value(), ser) @@ -153,7 +153,7 @@ where fn deserialize_protobuf_enum<'de, T, D>(de: D) -> Result where - T: ProtobufEnum, + T: Enum, D: serde::Deserializer<'de>, { let v: i32 = serde::Deserialize::deserialize(de)?; diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index e7016143..39e596a6 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -10,8 +10,8 @@ use url::Url; use super::{date::Date, Error, FileId, Session}; use librespot_protocol as protocol; +use protocol::storage_resolve::storage_resolve_response::Result as StorageResolveResponse_Result; use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; -use protocol::storage_resolve::StorageResolveResponse_Result; #[derive(Debug, Clone)] pub struct MaybeExpiringUrl(pub String, pub Option); @@ -100,14 +100,17 @@ impl CdnUrl { impl TryFrom for MaybeExpiringUrls { type Error = crate::Error; fn try_from(msg: CdnUrlMessage) -> Result { - if !matches!(msg.get_result(), StorageResolveResponse_Result::CDN) { + if !matches!( + msg.result.enum_value_or_default(), + StorageResolveResponse_Result::CDN + ) { return Err(CdnUrlError::Storage.into()); } - let is_expiring = !msg.get_fileid().is_empty(); + let is_expiring = !msg.fileid.is_empty(); let result = msg - .get_cdnurl() + .cdnurl .iter() .map(|cdn_url| { let url = Url::parse(cdn_url)?; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index b720455c..52b32778 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -54,16 +54,22 @@ pub async fn handshake( let mut accumulator = client_hello(&mut connection, gc).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; let remote_key = message - .get_challenge() - .get_login_crypto_challenge() - .get_diffie_hellman() - .get_gs() + .challenge + .get_or_default() + .login_crypto_challenge + .get_or_default() + .diffie_hellman + .get_or_default() + .gs() .to_owned(); let remote_signature = message - .get_challenge() - .get_login_crypto_challenge() - .get_diffie_hellman() - .get_gs_signature() + .challenge + .get_or_default() + .login_crypto_challenge + .get_or_default() + .diffie_hellman + .get_or_default() + .gs_signature() .to_owned(); // Prevent man-in-the-middle attacks: check server signature @@ -151,35 +157,45 @@ where let mut packet = ClientHello::new(); packet - .mut_build_info() + .build_info + .mut_or_insert_default() // 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); + .build_info + .mut_or_insert_default() + .product_flags + .push(PRODUCT_FLAGS.into()); packet - .mut_build_info() + .build_info + .mut_or_insert_default() + .set_platform(platform); + packet + .build_info + .mut_or_insert_default() .set_version(version::SPOTIFY_VERSION); packet - .mut_cryptosuites_supported() - .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON); + .cryptosuites_supported + .push(protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON.into()); packet - .mut_login_crypto_hello() - .mut_diffie_hellman() + .login_crypto_hello + .mut_or_insert_default() + .diffie_hellman + .mut_or_insert_default() .set_gc(gc); packet - .mut_login_crypto_hello() - .mut_diffie_hellman() + .login_crypto_hello + .mut_or_insert_default() + .diffie_hellman + .mut_or_insert_default() .set_server_keys_known(1); packet.set_client_nonce(client_nonce); packet.set_padding(vec![0x1e]); let mut buffer = vec![0, 4]; let size = 2 + 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size)?; + as WriteBytesExt>::write_u32::(&mut buffer, size.try_into().unwrap())?; packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; @@ -192,15 +208,15 @@ where { let mut packet = ClientResponsePlaintext::new(); packet - .mut_login_crypto_response() - .mut_diffie_hellman() + .login_crypto_response + .mut_or_insert_default() + .diffie_hellman + .mut_or_insert_default() .set_hmac(challenge); - packet.mut_pow_response(); - packet.mut_crypto_response(); let mut buffer = vec![]; let size = 4 + packet.compute_size(); - as WriteBytesExt>::write_u32::(&mut buffer, size)?; + as WriteBytesExt>::write_u32::(&mut buffer, size.try_into().unwrap())?; packet.write_to_vec(&mut buffer)?; connection.write_all(&buffer[..]).await?; diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index a0ea8b79..e7aa1693 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -58,7 +58,7 @@ impl From for Error { impl From for AuthenticationError { fn from(login_failure: APLoginFailed) -> Self { - Self::LoginFailed(login_failure.get_error_code()) + Self::LoginFailed(login_failure.error_code()) } } @@ -100,25 +100,33 @@ pub async fn authenticate( let mut packet = ClientResponseEncrypted::new(); packet - .mut_login_credentials() + .login_credentials + .mut_or_insert_default() .set_username(credentials.username); packet - .mut_login_credentials() + .login_credentials + .mut_or_insert_default() .set_typ(credentials.auth_type); packet - .mut_login_credentials() + .login_credentials + .mut_or_insert_default() .set_auth_data(credentials.auth_data); - packet.mut_system_info().set_cpu_family(cpu_family); - packet.mut_system_info().set_os(os); packet - .mut_system_info() + .system_info + .mut_or_insert_default() + .set_cpu_family(cpu_family); + packet.system_info.mut_or_insert_default().set_os(os); + packet + .system_info + .mut_or_insert_default() .set_system_information_string(format!( "librespot-{}-{}", version::SHA_SHORT, version::BUILD_ID )); packet - .mut_system_info() + .system_info + .mut_or_insert_default() .set_device_id(device_id.to_string()); packet.set_version_string(format!("librespot {}", version::SEMVER)); @@ -136,9 +144,9 @@ pub async fn authenticate( let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { - username: welcome_data.get_canonical_username().to_owned(), - auth_type: welcome_data.get_reusable_auth_credentials_type(), - auth_data: welcome_data.get_reusable_auth_credentials().to_owned(), + username: welcome_data.canonical_username().to_owned(), + auth_type: welcome_data.reusable_auth_credentials_type(), + auth_data: welcome_data.reusable_auth_credentials().to_owned(), }; Ok(reusable_credentials) diff --git a/core/src/date.rs b/core/src/date.rs index a3c1b8d7..5f08d4e8 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,8 +1,4 @@ -use std::{ - convert::{TryFrom, TryInto}, - fmt::Debug, - ops::Deref, -}; +use std::{convert::TryFrom, fmt::Debug, ops::Deref}; use time::{ error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime, @@ -63,21 +59,17 @@ impl TryFrom<&DateMessage> for Date { fn try_from(msg: &DateMessage) -> Result { // 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 + msg.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 day = if msg.has_day() { msg.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)?; + let date = _Date::from_calendar_date(msg.year(), month.try_into()?, day)?; + let time = Time::from_hms(msg.hour() as u8, msg.minute() as u8, 0)?; Ok(Self::from_utc(PrimitiveDateTime::new(date, time))) } } diff --git a/core/src/error.rs b/core/src/error.rs index 87cf3c86..ca58e779 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -12,7 +12,7 @@ use http::{ status::InvalidStatusCode, uri::{InvalidUri, InvalidUriParts}, }; -use protobuf::ProtobufError; +use protobuf::Error as ProtobufError; use thiserror::Error; use tokio::sync::{ mpsc::error::SendError, oneshot::error::RecvError, AcquireError, TryAcquireError, diff --git a/core/src/file_id.rs b/core/src/file_id.rs index 1e84b489..61b33125 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -39,18 +39,18 @@ impl From<&[u8]> for FileId { } impl From<&protocol::metadata::Image> for FileId { fn from(image: &protocol::metadata::Image) -> Self { - Self::from(image.get_file_id()) + Self::from(image.file_id()) } } impl From<&protocol::metadata::AudioFile> for FileId { fn from(file: &protocol::metadata::AudioFile) -> Self { - Self::from(file.get_file_id()) + Self::from(file.file_id()) } } impl From<&protocol::metadata::VideoFile> for FileId { fn from(video: &protocol::metadata::VideoFile) -> Self { - Self::from(video.get_file_id()) + Self::from(video.file_id()) } } diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 44e8de9c..760ea233 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -231,8 +231,8 @@ impl MercuryManager { let header = protocol::mercury::Header::parse_from_bytes(&header_data)?; let response = MercuryResponse { - uri: header.get_uri().to_string(), - status_code: header.get_status_code(), + uri: header.uri().to_string(), + status_code: header.status_code(), payload: pending.parts, }; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 4324eb96..7716fef9 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryInto, env::consts::OS, fmt::Write, time::{Duration, Instant}, @@ -14,7 +13,7 @@ use hyper::{ header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, Body, HeaderMap, Method, Request, }; -use protobuf::{Message, ProtobufEnum}; +use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; use sha1::{Digest, Sha1}; use sysinfo::{System, SystemExt}; @@ -150,7 +149,7 @@ impl SpClient { Ok(()) } - async fn client_token_request(&self, message: &dyn Message) -> Result { + async fn client_token_request(&self, message: &M) -> Result { let body = message.write_to_bytes()?; let request = Request::builder() @@ -179,10 +178,11 @@ impl SpClient { debug!("Client token unavailable or expired, requesting new token."); let mut request = ClientTokenRequest::new(); - request.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); + request.request_type = ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST.into(); let client_data = request.mut_client_data(); - client_data.set_client_version(spotify_version()); + + client_data.client_version = spotify_version(); // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, // so it seems a good idea to mimick the real clients. `self.session().client_id()` returns the @@ -194,12 +194,14 @@ impl SpClient { "macos" | "windows" => self.session().client_id(), _ => SessionConfig::default().client_id, }; - client_data.set_client_id(client_id); + client_data.client_id = client_id; let connectivity_data = client_data.mut_connectivity_sdk_data(); - connectivity_data.set_device_id(self.session().device_id().to_string()); + connectivity_data.device_id = self.session().device_id().to_string(); - let platform_data = connectivity_data.mut_platform_specific_data(); + let platform_data = connectivity_data + .platform_specific_data + .mut_or_insert_default(); let sys = System::new(); let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); @@ -218,41 +220,41 @@ impl SpClient { }; let windows_data = platform_data.mut_desktop_windows(); - windows_data.set_os_version(os_version); - windows_data.set_os_build(kernel_version); - 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); + windows_data.os_version = os_version; + windows_data.os_build = kernel_version; + windows_data.platform_id = 2; + windows_data.unknown_value_6 = 9; + windows_data.image_file_machine = image_file; + windows_data.pe_machine = pe; + windows_data.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(os_version); + ios_data.user_interface_idiom = 0; + ios_data.target_iphone_simulator = false; + ios_data.hw_machine = "iPhone14,5".to_string(); + ios_data.system_version = os_version; } "android" => { let android_data = platform_data.mut_android(); - android_data.set_android_version(os_version); - 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()); + android_data.android_version = os_version; + android_data.api_version = 31; + android_data.device_name = "Pixel".to_owned(); + android_data.model_str = "GF5KQ".to_owned(); + android_data.vendor = "Google".to_owned(); } "macos" => { let macos_data = platform_data.mut_desktop_macos(); - macos_data.set_system_version(os_version); - macos_data.set_hw_model("iMac21,1".to_string()); - macos_data.set_compiled_cpu_type(std::env::consts::ARCH.to_string()); + macos_data.system_version = os_version; + macos_data.hw_model = "iMac21,1".to_string(); + macos_data.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(kernel_version); - linux_data.set_system_version(os_version); - linux_data.set_hardware(std::env::consts::ARCH.to_string()); + linux_data.system_name = "Linux".to_string(); + linux_data.system_release = kernel_version; + linux_data.system_version = os_version; + linux_data.hardware = std::env::consts::ARCH.to_string(); } } @@ -272,10 +274,10 @@ impl SpClient { Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { debug!("Received a hash cash challenge, solving..."); - let challenges = message.get_challenges().clone(); - let state = challenges.get_state(); + let challenges = message.challenges().clone(); + let state = challenges.state; if let Some(challenge) = challenges.challenges.first() { - let hash_cash_challenge = challenge.get_evaluate_hashcash_parameters(); + let hash_cash_challenge = challenge.evaluate_hashcash_parameters(); let ctx = vec![]; let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { @@ -295,15 +297,16 @@ impl SpClient { let suffix = hex::encode(suffix).to_uppercase(); let mut answer_message = ClientTokenRequest::new(); - answer_message.set_request_type( - ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, - ); + answer_message.request_type = + ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST + .into(); let challenge_answers = answer_message.mut_challenge_answers(); let mut challenge_answer = ChallengeAnswer::new(); challenge_answer.mut_hash_cash().suffix = suffix.to_string(); - challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; + challenge_answer.ChallengeType = + ChallengeType::CHALLENGE_HASH_CASH.into(); challenge_answers.state = state.to_string(); challenge_answers.answers.push(challenge_answer); @@ -355,21 +358,21 @@ impl SpClient { } }; - let granted_token = token_response.get_granted_token(); - let access_token = granted_token.get_token().to_owned(); + let granted_token = token_response.granted_token(); + let access_token = granted_token.token.to_owned(); self.lock(|inner| { let client_token = Token { access_token: access_token.clone(), expires_in: Duration::from_secs( granted_token - .get_refresh_after_seconds() + .refresh_after_seconds .try_into() .unwrap_or(7200), ), token_type: "client-token".to_string(), scopes: granted_token - .get_domains() + .domains .iter() .map(|d| d.domain.clone()) .collect(), @@ -384,12 +387,12 @@ impl SpClient { Ok(access_token) } - pub async fn request_with_protobuf( + pub async fn request_with_protobuf( &self, method: &Method, endpoint: &str, headers: Option, - message: &dyn Message, + message: &M, ) -> SpClientResult { let body = protobuf::text_format::print_to_string(message); diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index da28f98e..f362fed4 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -423,12 +423,12 @@ impl TryFrom<&Vec> for SpotifyId { impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { type Error = crate::Error; fn try_from(track: &protocol::spirc::TrackRef) -> Result { - match SpotifyId::from_raw(track.get_gid()) { + match SpotifyId::from_raw(track.gid()) { Ok(mut id) => { id.item_type = SpotifyItemType::Track; Ok(id) } - Err(_) => SpotifyId::from_uri(track.get_uri()), + Err(_) => SpotifyId::from_uri(track.uri()), } } } @@ -438,7 +438,7 @@ impl TryFrom<&protocol::metadata::Album> for SpotifyId { fn try_from(album: &protocol::metadata::Album) -> Result { Ok(Self { item_type: SpotifyItemType::Album, - ..Self::from_raw(album.get_gid())? + ..Self::from_raw(album.gid())? }) } } @@ -448,7 +448,7 @@ impl TryFrom<&protocol::metadata::Artist> for SpotifyId { fn try_from(artist: &protocol::metadata::Artist) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.get_gid())? + ..Self::from_raw(artist.gid())? }) } } @@ -458,7 +458,7 @@ impl TryFrom<&protocol::metadata::Episode> for SpotifyId { fn try_from(episode: &protocol::metadata::Episode) -> Result { Ok(Self { item_type: SpotifyItemType::Episode, - ..Self::from_raw(episode.get_gid())? + ..Self::from_raw(episode.gid())? }) } } @@ -468,7 +468,7 @@ impl TryFrom<&protocol::metadata::Track> for SpotifyId { fn try_from(track: &protocol::metadata::Track) -> Result { Ok(Self { item_type: SpotifyItemType::Track, - ..Self::from_raw(track.get_gid())? + ..Self::from_raw(track.gid())? }) } } @@ -478,7 +478,7 @@ impl TryFrom<&protocol::metadata::Show> for SpotifyId { fn try_from(show: &protocol::metadata::Show) -> Result { Ok(Self { item_type: SpotifyItemType::Show, - ..Self::from_raw(show.get_gid())? + ..Self::from_raw(show.gid())? }) } } @@ -488,7 +488,7 @@ impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { Ok(Self { item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.get_artist_gid())? + ..Self::from_raw(artist.artist_gid())? }) } } @@ -498,7 +498,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { fn try_from(item: &protocol::playlist4_external::Item) -> Result { Ok(Self { item_type: SpotifyItemType::Track, - ..Self::from_uri(item.get_uri())? + ..Self::from_uri(item.uri())? }) } } @@ -508,7 +508,7 @@ impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { impl TryFrom<&protocol::playlist4_external::MetaItem> for SpotifyId { type Error = crate::Error; fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { - Self::try_from(item.get_revision()) + Self::try_from(item.revision()) } } @@ -518,7 +518,7 @@ impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyId { fn try_from( playlist: &protocol::playlist4_external::SelectedListContent, ) -> Result { - Self::try_from(playlist.get_revision()) + Self::try_from(playlist.revision()) } } @@ -530,7 +530,7 @@ impl TryFrom<&protocol::playlist_annotate3::TranscodedPicture> for SpotifyId { fn try_from( picture: &protocol::playlist_annotate3::TranscodedPicture, ) -> Result { - Self::from_base62(picture.get_uri()) + Self::from_base62(picture.uri()) } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 4d977bea..9da0ce42 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -13,7 +13,7 @@ async-trait = "0.1" byteorder = "1" bytes = "1" log = "0.4" -protobuf = "2" +protobuf = "3" thiserror = "1" uuid = { version = "1", default-features = false } serde = { version = "1.0", features = ["derive"] } diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 11f13331..8b33571e 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -21,7 +21,7 @@ use crate::{ use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_protocol as protocol; -pub use protocol::metadata::Album_Type as AlbumType; +pub use protocol::metadata::album::Type as AlbumType; use protocol::metadata::Disc as DiscMessage; #[derive(Debug, Clone)] @@ -90,26 +90,26 @@ impl TryFrom<&::Message> for Album { 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().try_into()?, - popularity: album.get_popularity(), - genres: album.get_genre().to_vec(), - covers: album.get_cover_group().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().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().try_into()?, + name: album.name().to_owned(), + artists: album.artist.as_slice().try_into()?, + album_type: album.type_(), + label: album.label().to_owned(), + date: album.date.get_or_default().try_into()?, + popularity: album.popularity(), + genres: album.genre.to_vec(), + covers: album.cover_group.get_or_default().into(), + external_ids: album.external_id.as_slice().into(), + discs: album.disc.as_slice().try_into()?, + reviews: album.review.to_vec(), + copyrights: album.copyright.as_slice().into(), + restrictions: album.restriction.as_slice().into(), + related: album.related.as_slice().try_into()?, + sale_periods: album.sale_period.as_slice().try_into()?, + cover_group: album.cover_group.image.as_slice().into(), + original_title: album.original_title().to_owned(), + version_title: album.version_title().to_owned(), + type_str: album.type_str().to_owned(), + availability: album.availability.as_slice().try_into()?, }) } } @@ -120,9 +120,9 @@ impl TryFrom<&DiscMessage> for Disc { type Error = librespot_core::Error; fn try_from(disc: &DiscMessage) -> Result { Ok(Self { - number: disc.get_number(), - name: disc.get_name().to_owned(), - tracks: disc.get_track().try_into()?, + number: disc.number(), + name: disc.name().to_owned(), + tracks: disc.track.as_slice().try_into()?, }) } } diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 2f50488b..d3162fc6 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -20,7 +20,7 @@ use crate::{ use librespot_core::{Error, Session, SpotifyId}; use librespot_protocol as protocol; -pub use protocol::metadata::ArtistWithRole_ArtistRole as ArtistRole; +pub use protocol::metadata::artist_with_role::ArtistRole; use protocol::metadata::ActivityPeriod as ActivityPeriodMessage; use protocol::metadata::AlbumGroup as AlbumGroupMessage; @@ -187,24 +187,29 @@ impl TryFrom<&::Message> for Artist { fn try_from(artist: &::Message) -> Result { Ok(Self { id: artist.try_into()?, - name: artist.get_name().to_owned(), - popularity: artist.get_popularity(), - top_tracks: artist.get_top_track().try_into()?, - albums: artist.get_album_group().try_into()?, - singles: artist.get_single_group().try_into()?, - compilations: artist.get_compilation_group().try_into()?, - appears_on_albums: artist.get_appears_on_group().try_into()?, - genre: artist.get_genre().to_vec(), - external_ids: artist.get_external_id().into(), - portraits: artist.get_portrait().into(), - biographies: artist.get_biography().into(), - activity_periods: artist.get_activity_period().try_into()?, - restrictions: artist.get_restriction().into(), - related: artist.get_related().try_into()?, - is_portrait_album_cover: artist.get_is_portrait_album_cover(), - portrait_group: artist.get_portrait_group().get_image().into(), - sales_periods: artist.get_sale_period().try_into()?, - availabilities: artist.get_availability().try_into()?, + name: artist.name().to_owned(), + popularity: artist.popularity(), + top_tracks: artist.top_track.as_slice().try_into()?, + albums: artist.album_group.as_slice().try_into()?, + singles: artist.single_group.as_slice().try_into()?, + compilations: artist.compilation_group.as_slice().try_into()?, + appears_on_albums: artist.appears_on_group.as_slice().try_into()?, + genre: artist.genre.to_vec(), + external_ids: artist.external_id.as_slice().into(), + portraits: artist.portrait.as_slice().into(), + biographies: artist.biography.as_slice().into(), + activity_periods: artist.activity_period.as_slice().try_into()?, + restrictions: artist.restriction.as_slice().into(), + related: artist.related.as_slice().try_into()?, + is_portrait_album_cover: artist.is_portrait_album_cover(), + portrait_group: artist + .portrait_group + .get_or_default() + .image + .as_slice() + .into(), + sales_periods: artist.sale_period.as_slice().try_into()?, + availabilities: artist.availability.as_slice().try_into()?, }) } } @@ -216,8 +221,8 @@ impl TryFrom<&ArtistWithRoleMessage> for ArtistWithRole { 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(), + name: artist_with_role.artist_name().to_owned(), + role: artist_with_role.role(), }) } } @@ -228,8 +233,8 @@ impl TryFrom<&TopTracksMessage> for TopTracks { type Error = librespot_core::Error; fn try_from(top_tracks: &TopTracksMessage) -> Result { Ok(Self { - country: top_tracks.get_country().to_owned(), - tracks: top_tracks.get_track().try_into()?, + country: top_tracks.country().to_owned(), + tracks: top_tracks.track.as_slice().try_into()?, }) } } @@ -239,7 +244,7 @@ impl_try_from_repeated!(TopTracksMessage, CountryTopTracks); impl TryFrom<&AlbumGroupMessage> for AlbumGroup { type Error = librespot_core::Error; fn try_from(album_groups: &AlbumGroupMessage) -> Result { - Ok(Self(album_groups.get_album().try_into()?)) + Ok(Self(album_groups.album.as_slice().try_into()?)) } } @@ -257,14 +262,14 @@ impl_try_from_repeated!(AlbumGroupMessage, AlbumGroups); impl From<&BiographyMessage> for Biography { fn from(biography: &BiographyMessage) -> Self { let portrait_group = biography - .get_portrait_group() + .portrait_group .iter() - .map(|it| it.get_image().into()) + .map(|it| it.image.as_slice().into()) .collect(); Self { - text: biography.get_text().to_owned(), - portraits: biography.get_portrait().into(), + text: biography.text().to_owned(), + portraits: biography.portrait.as_slice().into(), portrait_group, } } @@ -282,11 +287,11 @@ impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod { period.has_end_year(), ) { // (decade, start_year, end_year) - (true, false, false) => Self::Decade(period.get_decade().try_into()?), + (true, false, false) => Self::Decade(period.decade().try_into()?), (false, true, closed_period) => Self::Timespan { - start_year: period.get_start_year().try_into()?, + start_year: period.start_year().try_into()?, end_year: closed_period - .then(|| period.get_end_year().try_into()) + .then(|| period.end_year().try_into()) .transpose()?, }, _ => { diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index 08017187..e8fe822b 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -7,8 +7,8 @@ use std::{ use librespot_core::FileId; use librespot_protocol as protocol; +pub use protocol::metadata::audio_file::Format as AudioFileFormat; use protocol::metadata::AudioFile as AudioFileMessage; -pub use protocol::metadata::AudioFile_Format as AudioFileFormat; use crate::util::impl_deref_wrapped; @@ -45,12 +45,12 @@ impl AudioFiles { impl From<&[AudioFileMessage]> for AudioFiles { fn from(files: &[AudioFileMessage]) -> Self { - let audio_files = files + let audio_files: HashMap = files .iter() .filter_map(|file| { - let file_id = FileId::from(file.get_file_id()); + let file_id = FileId::from(file.file_id()); if file.has_format() { - Some((file.get_format(), file_id)) + Some((file.format(), file_id)) } else { trace!("Ignoring file <{}> with unspecified format", file_id); None diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 20727f8c..6713da28 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,5 +1,5 @@ use std::{ - convert::{TryFrom, TryInto}, + convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; @@ -42,8 +42,8 @@ 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().try_into()?, + catalogue_strs: availability.catalogue_str.to_vec(), + start: availability.start.get_or_default().try_into()?, }) } } diff --git a/metadata/src/content_rating.rs b/metadata/src/content_rating.rs index 42d0ad5e..29693e43 100644 --- a/metadata/src/content_rating.rs +++ b/metadata/src/content_rating.rs @@ -22,8 +22,8 @@ impl_deref_wrapped!(ContentRatings, Vec); 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(), + country: content_rating.country().to_owned(), + tags: content_rating.tag.to_vec(), } } } diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index 360fd994..5a5ab4db 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -6,8 +6,8 @@ use std::{ use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_protocol as protocol; +pub use protocol::metadata::copyright::Type as CopyrightType; use protocol::metadata::Copyright as CopyrightMessage; -pub use protocol::metadata::Copyright_Type as CopyrightType; #[derive(Debug, Clone)] pub struct Copyright { @@ -23,8 +23,8 @@ impl_deref_wrapped!(Copyrights, Vec); impl From<&CopyrightMessage> for Copyright { fn from(copyright: &CopyrightMessage) -> Self { Self { - copyright_type: copyright.get_field_type(), - text: copyright.get_text().to_owned(), + copyright_type: copyright.type_(), + text: copyright.text().to_owned(), } } } diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index fe795a25..8e200802 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -19,7 +19,7 @@ use crate::{ use librespot_core::{date::Date, Error, Session, SpotifyId}; use librespot_protocol as protocol; -pub use protocol::metadata::Episode_EpisodeType as EpisodeType; +pub use protocol::metadata::episode::EpisodeType; #[derive(Debug, Clone)] pub struct Episode { @@ -72,29 +72,29 @@ impl TryFrom<&::Message> for Episode { 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().try_into()?, - covers: episode.get_cover_image().get_image().into(), - language: episode.get_language().to_owned(), - is_explicit: episode.get_explicit().to_owned(), - show_name: episode.get_show().get_name().to_owned(), - 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().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(), - content_rating: episode.get_content_rating().into(), - is_audiobook_chapter: episode.get_is_audiobook_chapter(), + name: episode.name().to_owned(), + duration: episode.duration().to_owned(), + audio: episode.audio.as_slice().into(), + description: episode.description().to_owned(), + number: episode.number(), + publish_time: episode.publish_time.get_or_default().try_into()?, + covers: episode.cover_image.image.as_slice().into(), + language: episode.language().to_owned(), + is_explicit: episode.explicit().to_owned(), + show_name: episode.show.name().to_owned(), + videos: episode.video.as_slice().into(), + video_previews: episode.video_preview.as_slice().into(), + audio_previews: episode.audio_preview.as_slice().into(), + restrictions: episode.restriction.as_slice().into(), + freeze_frames: episode.freeze_frame.image.as_slice().into(), + keywords: episode.keyword.to_vec(), + allow_background_playback: episode.allow_background_playback(), + availability: episode.availability.as_slice().try_into()?, + external_url: episode.external_url().to_owned(), + episode_type: episode.type_(), + has_music_and_talk: episode.music_and_talk(), + content_rating: episode.content_rating.as_slice().into(), + is_audiobook_chapter: episode.is_audiobook_chapter(), }) } } diff --git a/metadata/src/external_id.rs b/metadata/src/external_id.rs index ce8d4fd8..b5f50993 100644 --- a/metadata/src/external_id.rs +++ b/metadata/src/external_id.rs @@ -22,8 +22,8 @@ impl_deref_wrapped!(ExternalIds, Vec); 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(), + external_type: external_id.type_().to_owned(), + id: external_id.id().to_owned(), } } } diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 0bbe5010..be0137e7 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -9,9 +9,9 @@ use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated use librespot_core::{FileId, SpotifyId}; use librespot_protocol as protocol; +pub use protocol::metadata::image::Size as ImageSize; use protocol::metadata::Image as ImageMessage; use protocol::metadata::ImageGroup; -pub use protocol::metadata::Image_Size as ImageSize; use protocol::playlist4_external::PictureSize as PictureSizeMessage; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; @@ -60,9 +60,9 @@ 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(), + size: image.size(), + width: image.width(), + height: image.height(), } } } @@ -72,8 +72,8 @@ impl_from_repeated!(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(), + target_name: size.target_name().to_owned(), + url: size.url().to_owned(), } } } @@ -84,7 +84,7 @@ impl TryFrom<&TranscodedPictureMessage> for TranscodedPicture { type Error = librespot_core::Error; fn try_from(picture: &TranscodedPictureMessage) -> Result { Ok(Self { - target_name: picture.get_target_name().to_owned(), + target_name: picture.target_name().to_owned(), uri: picture.try_into()?, }) } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index e7263cae..d4cbd4ec 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -41,7 +41,7 @@ pub use track::Track; #[async_trait] pub trait Metadata: Send + Sized + 'static { - type Message: protobuf::Message; + type Message: protobuf::Message + std::fmt::Debug; // Request a protobuf async fn request(session: &Session, id: &SpotifyId) -> RequestResult; diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index 9cb7f144..d73f7b93 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -1,4 +1,4 @@ -use std::convert::{TryFrom, TryInto}; +use std::convert::TryFrom; use std::fmt::Debug; use protobuf::Message; @@ -34,11 +34,11 @@ impl Metadata for PlaylistAnnotation { 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(), + description: msg.description().to_owned(), + picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI? + transcoded_pictures: msg.transcoded_picture.as_slice().try_into()?, + has_abuse_reporting: msg.is_abuse_reporting_enabled(), + abuse_report_state: msg.abuse_report_state(), }) } } @@ -77,11 +77,11 @@ impl TryFrom<&::Message> for PlaylistAnnotation 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(), + description: annotation.description().to_owned(), + picture: annotation.picture().to_owned(), + transcoded_pictures: annotation.transcoded_picture.as_slice().try_into()?, + has_abuse_reporting: annotation.is_abuse_reporting_enabled(), + abuse_report_state: annotation.abuse_report_state(), }) } } diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index b5849a66..6da2be83 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -1,6 +1,6 @@ use std::{ collections::HashMap, - convert::{TryFrom, TryInto}, + convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; @@ -99,16 +99,16 @@ impl TryFrom<&PlaylistAttributesMessage> for PlaylistAttributes { type Error = librespot_core::Error; fn try_from(attributes: &PlaylistAttributesMessage) -> Result { Ok(Self { - name: attributes.get_name().to_owned(), - description: attributes.get_description().to_owned(), - 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(), - 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(), + name: attributes.name().to_owned(), + description: attributes.description().to_owned(), + picture: attributes.picture().to_owned(), + is_collaborative: attributes.collaborative(), + pl3_version: attributes.pl3_version().to_owned(), + is_deleted_by_owner: attributes.deleted_by_owner(), + client_id: attributes.client_id().to_owned(), + format: attributes.format().to_owned(), + format_attributes: attributes.format_attributes.as_slice().into(), + picture_sizes: attributes.picture_size.as_slice().into(), }) } } @@ -117,12 +117,7 @@ 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(), - ) - }) + .map(|attribute| (attribute.key().to_owned(), attribute.value().to_owned())) .collect(); PlaylistFormatAttribute(format_attributes) @@ -133,12 +128,12 @@ impl TryFrom<&PlaylistItemAttributesMessage> for PlaylistItemAttributes { type Error = librespot_core::Error; fn try_from(attributes: &PlaylistItemAttributesMessage) -> Result { Ok(Self { - added_by: attributes.get_added_by().to_owned(), - 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().to_owned(), + added_by: attributes.added_by().to_owned(), + timestamp: Date::from_timestamp_ms(attributes.timestamp())?, + seen_at: Date::from_timestamp_ms(attributes.seen_at())?, + is_public: attributes.public(), + format_attributes: attributes.format_attributes.as_slice().into(), + item_id: attributes.item_id().to_owned(), }) } } @@ -146,8 +141,14 @@ impl TryFrom<&PlaylistPartialAttributesMessage> for PlaylistPartialAttributes { type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialAttributesMessage) -> Result { Ok(Self { - values: attributes.get_values().try_into()?, - no_value: attributes.get_no_value().into(), + values: attributes.values.get_or_default().try_into()?, + no_value: attributes + .no_value + .iter() + .map(|v| v.enum_value_or_default()) + .collect::>() + .as_slice() + .into(), }) } } @@ -156,8 +157,14 @@ impl TryFrom<&PlaylistPartialItemAttributesMessage> for PlaylistPartialItemAttri type Error = librespot_core::Error; fn try_from(attributes: &PlaylistPartialItemAttributesMessage) -> Result { Ok(Self { - values: attributes.get_values().try_into()?, - no_value: attributes.get_no_value().into(), + values: attributes.values.get_or_default().try_into()?, + no_value: attributes + .no_value + .iter() + .map(|v| v.enum_value_or_default()) + .collect::>() + .as_slice() + .into(), }) } } @@ -166,8 +173,8 @@ impl TryFrom<&PlaylistUpdateAttributesMessage> for PlaylistUpdateAttributes { type Error = librespot_core::Error; fn try_from(update: &PlaylistUpdateAttributesMessage) -> Result { Ok(Self { - new_attributes: update.get_new_attributes().try_into()?, - old_attributes: update.get_old_attributes().try_into()?, + new_attributes: update.new_attributes.get_or_default().try_into()?, + old_attributes: update.old_attributes.get_or_default().try_into()?, }) } } @@ -176,9 +183,9 @@ impl TryFrom<&PlaylistUpdateItemAttributesMessage> for PlaylistUpdateItemAttribu type Error = librespot_core::Error; 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()?, + index: update.index(), + new_attributes: update.new_attributes.get_or_default().try_into()?, + old_attributes: update.old_attributes.get_or_default().try_into()?, }) } } diff --git a/metadata/src/playlist/diff.rs b/metadata/src/playlist/diff.rs index 4e40d2a5..c4967d85 100644 --- a/metadata/src/playlist/diff.rs +++ b/metadata/src/playlist/diff.rs @@ -1,7 +1,4 @@ -use std::{ - convert::{TryFrom, TryInto}, - fmt::Debug, -}; +use std::{convert::TryFrom, fmt::Debug}; use super::operation::PlaylistOperations; @@ -21,9 +18,19 @@ impl TryFrom<&DiffMessage> for PlaylistDiff { type Error = librespot_core::Error; 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()?, + from_revision: diff + .from_revision + .clone() + .unwrap_or_default() + .as_slice() + .try_into()?, + operations: diff.ops.as_slice().try_into()?, + to_revision: diff + .to_revision + .clone() + .unwrap_or_default() + .as_slice() + .try_into()?, }) } } diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index f684f771..4fb892c6 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -58,7 +58,7 @@ impl TryFrom<&PlaylistItemMessage> for PlaylistItem { fn try_from(item: &PlaylistItemMessage) -> Result { Ok(Self { id: item.try_into()?, - attributes: item.get_attributes().try_into()?, + attributes: item.attributes.get_or_default().try_into()?, }) } } @@ -69,10 +69,10 @@ impl TryFrom<&PlaylistItemsMessage> for PlaylistItemList { type Error = librespot_core::Error; 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()?, + position: list_items.pos(), + is_truncated: list_items.truncated(), + items: list_items.items.as_slice().try_into()?, + meta_items: list_items.meta_items.as_slice().try_into()?, }) } } @@ -82,12 +82,12 @@ impl TryFrom<&PlaylistMetaItemMessage> for PlaylistMetaItem { fn try_from(item: &PlaylistMetaItemMessage) -> Result { Ok(Self { revision: item.try_into()?, - attributes: item.get_attributes().try_into()?, - length: item.get_length(), - 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(), + attributes: item.attributes.get_or_default().try_into()?, + length: item.length(), + timestamp: Date::from_timestamp_ms(item.timestamp())?, + owner_username: item.owner_username().to_owned(), + has_abuse_reporting: item.abuse_reporting_enabled(), + capabilities: item.capabilities.get_or_default().into(), }) } } diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index dcbd9ea1..b4eaf9b6 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -129,7 +129,7 @@ impl Metadata for Playlist { impl TryFrom<&::Message> for SelectedListContent { type Error = librespot_core::Error; fn try_from(playlist: &::Message) -> Result { - let timestamp = playlist.get_timestamp(); + let timestamp = playlist.timestamp(); let timestamp = if timestamp > 9295169800000 { // timestamp is way out of range for milliseconds. Some seem to be in microseconds? // Observed on playlists where: @@ -146,25 +146,37 @@ impl TryFrom<&::Message> for SelectedListContent { let timestamp = Date::from_timestamp_ms(timestamp)?; Ok(Self { - revision: playlist.get_revision().to_owned(), - length: playlist.get_length(), - attributes: playlist.get_attributes().try_into()?, - contents: playlist.get_contents().try_into()?, + revision: playlist.revision().to_owned(), + length: playlist.length(), + attributes: playlist.attributes.get_or_default().try_into()?, + contents: playlist.contents.get_or_default().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(), + resulting_revisions: Playlists( + playlist + .resulting_revisions + .iter() + .map(|p| p.try_into()) + .collect::, Error>>()?, + ), + has_multiple_heads: playlist.multiple_heads(), + is_up_to_date: playlist.up_to_date(), + nonces: playlist.nonces.clone(), timestamp, - 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(), + owner_username: playlist.owner_username().to_owned(), + has_abuse_reporting: playlist.abuse_reporting_enabled(), + capabilities: playlist.capabilities.get_or_default().into(), + geoblocks: Geoblocks( + playlist + .geoblock + .iter() + .map(|b| b.enum_value_or_default()) + .collect(), + ), }) } } diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index 06264aca..5f7c7551 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -1,5 +1,5 @@ use std::{ - convert::{TryFrom, TryInto}, + convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; @@ -13,10 +13,10 @@ use crate::{ }; use librespot_protocol as protocol; +pub use protocol::playlist4_external::op::Kind as PlaylistOperationKind; use protocol::playlist4_external::Add as PlaylistAddMessage; use protocol::playlist4_external::Mov as PlaylistMoveMessage; use protocol::playlist4_external::Op as PlaylistOperationMessage; -pub use protocol::playlist4_external::Op_Kind as PlaylistOperationKind; use protocol::playlist4_external::Rem as PlaylistRemoveMessage; #[derive(Debug, Clone)] @@ -61,12 +61,18 @@ impl TryFrom<&PlaylistOperationMessage> for PlaylistOperation { type Error = librespot_core::Error; 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()?, + kind: operation.kind(), + add: operation.add.get_or_default().try_into()?, + rem: operation.rem.get_or_default().try_into()?, + mov: operation.mov.get_or_default().into(), + update_item_attributes: operation + .update_item_attributes + .get_or_default() + .try_into()?, + update_list_attributes: operation + .update_list_attributes + .get_or_default() + .try_into()?, }) } } @@ -77,10 +83,10 @@ impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { type Error = librespot_core::Error; 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(), + from_index: add.from_index(), + items: add.items.as_slice().try_into()?, + add_last: add.add_last(), + add_first: add.add_first(), }) } } @@ -88,9 +94,9 @@ impl TryFrom<&PlaylistAddMessage> for PlaylistOperationAdd { 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(), + from_index: mov.from_index(), + length: mov.length(), + to_index: mov.to_index(), } } } @@ -99,10 +105,10 @@ impl TryFrom<&PlaylistRemoveMessage> for PlaylistOperationRemove { type Error = librespot_core::Error; 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(), + from_index: remove.from_index(), + length: remove.length(), + items: remove.items.as_slice().try_into()?, + has_items_as_key: remove.items_as_key(), }) } } diff --git a/metadata/src/playlist/permission.rs b/metadata/src/playlist/permission.rs index 47ddbdee..ebb179a8 100644 --- a/metadata/src/playlist/permission.rs +++ b/metadata/src/playlist/permission.rs @@ -27,12 +27,18 @@ impl_deref_wrapped!(PermissionLevels, Vec); 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(), + can_view: playlist.can_view(), + can_administrate_permissions: playlist.can_administrate_permissions(), + grantable_levels: PermissionLevels( + playlist + .grantable_level + .iter() + .map(|l| l.enum_value_or_default()) + .collect(), + ), + can_edit_metadata: playlist.can_edit_metadata(), + can_edit_items: playlist.can_edit_items(), + can_cancel_membership: playlist.can_cancel_membership(), } } } diff --git a/metadata/src/restriction.rs b/metadata/src/restriction.rs index 565010ff..df9de425 100644 --- a/metadata/src/restriction.rs +++ b/metadata/src/restriction.rs @@ -9,8 +9,8 @@ use crate::util::{impl_from_repeated, impl_from_repeated_copy}; 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; +pub use protocol::metadata::restriction::Catalogue as RestrictionCatalogue; +pub use protocol::metadata::restriction::Type as RestrictionType; #[derive(Debug, Clone)] pub struct Restriction { @@ -43,25 +43,30 @@ impl Restriction { 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(), - )) + Some(Self::parse_country_codes(restriction.countries_allowed())) } else { None }; let countries_forbidden = if restriction.has_countries_forbidden() { - Some(Self::parse_country_codes( - restriction.get_countries_forbidden(), - )) + Some(Self::parse_country_codes(restriction.countries_forbidden())) } else { None }; Self { - catalogues: restriction.get_catalogue().into(), - restriction_type: restriction.get_field_type(), - catalogue_strs: restriction.get_catalogue_str().to_vec(), + catalogues: restriction + .catalogue + .iter() + .map(|c| c.enum_value_or_default()) + .collect::>() + .as_slice() + .into(), + restriction_type: restriction + .type_ + .unwrap_or_default() + .enum_value_or_default(), + catalogue_strs: restriction.catalogue_str.to_vec(), countries_allowed, countries_forbidden, } diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index 911e1d04..a9ee317c 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,5 +1,5 @@ use std::{ - convert::{TryFrom, TryInto}, + convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; @@ -30,9 +30,9 @@ 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().try_into()?, - end: sale_period.get_end().try_into()?, + restrictions: sale_period.restriction.as_slice().into(), + start: sale_period.start.get_or_default().try_into()?, + end: sale_period.end.get_or_default().try_into()?, }) } } diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 0d3acef8..5c86856c 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -11,8 +11,8 @@ use crate::{ 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; +pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder; +pub use protocol::metadata::show::MediaType as ShowMediaType; #[derive(Debug, Clone)] pub struct Show { @@ -53,22 +53,22 @@ impl TryFrom<&::Message> for Show { 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().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(), + name: show.name().to_owned(), + description: show.description().to_owned(), + publisher: show.publisher().to_owned(), + language: show.language().to_owned(), + is_explicit: show.explicit(), + covers: show.cover_image.image.as_slice().into(), + episodes: show.episode.as_slice().try_into()?, + copyrights: show.copyright.as_slice().into(), + restrictions: show.restriction.as_slice().into(), + keywords: show.keyword.to_vec(), + media_type: show.media_type(), + consumption_order: show.consumption_order(), + availability: show.availability.as_slice().try_into()?, + trailer_uri: SpotifyId::from_uri(show.trailer_uri())?, + has_music_and_talk: show.music_and_talk(), + is_audiobook: show.is_audiobook(), }) } } diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 03fab92c..dd945bed 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -73,31 +73,30 @@ impl TryFrom<&::Message> for Track { 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().try_into()?, - previews: track.get_preview().into(), - tags: track.get_tags().to_vec(), - 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()) - .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()?, + name: track.name().to_owned(), + album: track.album.get_or_default().try_into()?, + artists: track.artist.as_slice().try_into()?, + number: track.number(), + disc_number: track.disc_number(), + duration: track.duration(), + popularity: track.popularity(), + is_explicit: track.explicit(), + external_ids: track.external_id.as_slice().into(), + restrictions: track.restriction.as_slice().into(), + files: track.file.as_slice().into(), + alternatives: track.alternative.as_slice().try_into()?, + sale_periods: track.sale_period.as_slice().try_into()?, + previews: track.preview.as_slice().into(), + tags: track.tags.to_vec(), + earliest_live_timestamp: Date::from_timestamp_ms(track.earliest_live_timestamp())?, + has_lyrics: track.has_lyrics(), + availability: track.availability.as_slice().try_into()?, + licensor: Uuid::from_slice(track.licensor.uuid()).unwrap_or_else(|_| Uuid::nil()), + language_of_performance: track.language_of_performance.to_vec(), + content_ratings: track.content_rating.as_slice().into(), + original_title: track.original_title().to_owned(), + version_title: track.version_title().to_owned(), + artists_with_role: track.artist_with_role.as_slice().try_into()?, }) } } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 36d00c2b..5ca462d3 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -10,8 +10,8 @@ repository = "https://github.com/librespot-org/librespot" edition = "2021" [dependencies] -protobuf = "2" +protobuf = "3" [build-dependencies] glob = "0.3" -protobuf-codegen-pure = "2" +protobuf-codegen = "3" diff --git a/protocol/build.rs b/protocol/build.rs index b63fa1aa..e1378d37 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -46,7 +46,8 @@ fn compile() { let out_dir = out_dir(); fs::create_dir(&out_dir).expect("create_dir"); - protobuf_codegen_pure::Codegen::new() + protobuf_codegen::Codegen::new() + .pure() .out_dir(&out_dir) .inputs(&slices) .include(&proto_dir) @@ -54,26 +55,7 @@ fn compile() { .expect("Codegen failed."); } -fn generate_mod_rs() { - let out_dir = out_dir(); - - let mods = glob::glob(&out_dir.join("*.rs").to_string_lossy()) - .expect("glob") - .filter_map(|p| { - p.ok() - .map(|p| format!("pub mod {};", p.file_stem().unwrap().to_string_lossy())) - }) - .collect::>() - .join("\n"); - - let mod_rs = out_dir.join("mod.rs"); - fs::write(&mod_rs, format!("// @generated\n{}\n", mods)).expect("write"); - - println!("cargo:rustc-env=PROTO_MOD_RS={}", mod_rs.to_string_lossy()); -} - fn main() { cleanup(); compile(); - generate_mod_rs(); } From 6c1030e820e915b5f29b077819bc2a5d7e4e2ba9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Jan 2023 22:22:11 +0000 Subject: [PATCH 287/561] Bump bumpalo from 3.11.0 to 3.12.0 Bumps [bumpalo](https://github.com/fitzgen/bumpalo) from 3.11.0 to 3.12.0. - [Release notes](https://github.com/fitzgen/bumpalo/releases) - [Changelog](https://github.com/fitzgen/bumpalo/blob/main/CHANGELOG.md) - [Commits](https://github.com/fitzgen/bumpalo/compare/3.11.0...3.12.0) --- updated-dependencies: - dependency-name: bumpalo dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f24802d..09c31d76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -168,9 +168,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" From c1f232f15d0e08ec1879b8c2757f585fee63f95a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Jan 2023 23:45:21 +0000 Subject: [PATCH 288/561] Bump libgit2-sys from 0.13.4+1.4.2 to 0.13.5+1.4.5 Bumps [libgit2-sys](https://github.com/rust-lang/git2-rs) from 0.13.4+1.4.2 to 0.13.5+1.4.5. - [Release notes](https://github.com/rust-lang/git2-rs/releases) - [Commits](https://github.com/rust-lang/git2-rs/compare/libgit2-sys-0.13.4...libgit2-sys-0.13.5) --- updated-dependencies: - dependency-name: libgit2-sys dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0f24802d..2f6648b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1253,9 +1253,9 @@ checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" [[package]] name = "libgit2-sys" -version = "0.13.4+1.4.2" +version = "0.13.5+1.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" +checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba" dependencies = [ "cc", "libc", From 7a4807eb8a3593fa13b4afe44ebbc627131c8467 Mon Sep 17 00:00:00 2001 From: Ovenoboyo Date: Sat, 21 Jan 2023 21:56:50 +0530 Subject: [PATCH 289/561] Add back pow_response and crypto_response Probably was accidentally removed in 3662302196c1aeb35501db717b88152c50212117 Signed-off-by: Ovenoboyo --- core/src/connection/handshake.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 52b32778..abcc0a3d 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -214,6 +214,9 @@ where .mut_or_insert_default() .set_hmac(challenge); + packet.pow_response.mut_or_insert_default(); + packet.crypto_response.mut_or_insert_default(); + let mut buffer = vec![]; let size = 4 + packet.compute_size(); as WriteBytesExt>::write_u32::(&mut buffer, size.try_into().unwrap())?; From c600297f52711ab2e9a3b8a2a28d3145c54fff0f Mon Sep 17 00:00:00 2001 From: Petr Tesarik Date: Fri, 27 Jan 2023 23:15:51 +0100 Subject: [PATCH 290/561] Fix newly reported clippy errors - Use variables directly in format strings. As reported by clippy, variables can be used directly in the `format!` string. - Use rewind() instead of seeking to 0. - Remove superfluous & and ref. Signed-off-by: Petr Tesarik --- audio/src/fetch/receive.rs | 4 +-- audio/src/range_set.rs | 2 +- core/build.rs | 2 +- core/src/apresolve.rs | 8 ++--- core/src/cache.rs | 4 +-- core/src/mercury/mod.rs | 2 +- core/src/proxytunnel.rs | 2 +- core/src/spclient.rs | 31 +++++++---------- metadata/src/request.rs | 6 ++-- playback/src/audio_backend/alsa.rs | 4 +-- playback/src/audio_backend/gstreamer.rs | 2 +- playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/portaudio.rs | 10 +++--- playback/src/audio_backend/pulseaudio.rs | 2 +- playback/src/audio_backend/rodio.rs | 19 +++++------ playback/src/audio_backend/sdl.rs | 2 +- playback/src/decoder/passthrough_decoder.rs | 5 ++- playback/src/decoder/symphonia_decoder.rs | 14 +++----- src/main.rs | 38 +++++++++------------ 19 files changed, 69 insertions(+), 90 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index d090d547..39ad84c6 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -62,7 +62,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 {actual_length} bytes"); break Err(Error::data_loss(msg)); } @@ -385,7 +385,7 @@ impl AudioFileFetch { let complete_tx = self.complete_tx.take(); if let Some(mut output) = output { - output.seek(SeekFrom::Start(0))?; + output.rewind()?; if let Some(complete_tx) = complete_tx { complete_tx .send(output) diff --git a/audio/src/range_set.rs b/audio/src/range_set.rs index ee9136f2..3d1dd83a 100644 --- a/audio/src/range_set.rs +++ b/audio/src/range_set.rs @@ -35,7 +35,7 @@ impl fmt::Display for RangeSet { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "(")?; for range in self.ranges.iter() { - write!(f, "{}", range)?; + write!(f, "{range}")?; } write!(f, ")") } diff --git a/core/build.rs b/core/build.rs index 784f7c2f..0e84d8fc 100644 --- a/core/build.rs +++ b/core/build.rs @@ -22,5 +22,5 @@ fn main() { .collect(), }; - println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id); + println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={build_id}"); } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index c02ec1c8..8eb1c4eb 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -137,17 +137,13 @@ impl ApResolver { "spclient" => inner.data.spclient.pop_front(), _ => { return Err(Error::unimplemented(format!( - "No implementation to resolve access point {}", - endpoint + "No implementation to resolve access point {endpoint}" ))) } }; let access_point = access_point.ok_or_else(|| { - Error::unavailable(format!( - "No access point available for endpoint {}", - endpoint - )) + Error::unavailable(format!("No access point available for endpoint {endpoint}")) })?; Ok(access_point) diff --git a/core/src/cache.rs b/core/src/cache.rs index 11bfdf47..af373ff1 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -328,7 +328,7 @@ impl Cache { if let Some(location) = &self.credentials_location { let result = File::create(location).and_then(|mut file| { let data = serde_json::to_string(cred)?; - write!(file, "{}", data) + write!(file, "{data}") }); if let Err(e) = result { @@ -360,7 +360,7 @@ impl Cache { pub fn save_volume(&self, volume: u16) { if let Some(ref location) = self.volume_location { - let result = File::create(location).and_then(|mut file| write!(file, "{}", volume)); + let result = File::create(location).and_then(|mut file| write!(file, "{volume}")); if let Err(e) = result { warn!("Cannot save volume to cache: {}", e); } diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 760ea233..03bada3c 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -263,7 +263,7 @@ impl MercuryManager { let mut found = false; self.lock(|inner| { - inner.subscriptions.retain(|&(ref prefix, ref sub)| { + inner.subscriptions.retain(|(prefix, sub)| { if encoded_uri.starts_with(prefix) { found = true; diff --git a/core/src/proxytunnel.rs b/core/src/proxytunnel.rs index 6f1587f0..af51bbb7 100644 --- a/core/src/proxytunnel.rs +++ b/core/src/proxytunnel.rs @@ -38,7 +38,7 @@ pub async fn proxy_connect( Some(200) => Ok(proxy_connection), // Proxy says all is well Some(code) => { let reason = response.reason.unwrap_or("no reason"); - let msg = format!("Proxy responded with {}: {}", code, reason); + let msg = format!("Proxy responded with {code}: {reason}"); Err(io::Error::new(io::ErrorKind::Other, msg)) } None => Err(io::Error::new( diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 7716fef9..d6c5ffb1 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -125,8 +125,7 @@ impl SpClient { let suffix = loop { if now.elapsed().as_secs() >= TIMEOUT { return Err(Error::deadline_exceeded(format!( - "{} seconds expired", - TIMEOUT + "{TIMEOUT} seconds expired" ))); } @@ -282,8 +281,7 @@ impl SpClient { let ctx = vec![]; let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { Error::failed_precondition(format!( - "Unable to decode hash cash challenge: {}", - e + "Unable to decode hash cash challenge: {e}" )) })?; let length = hash_cash_challenge.length; @@ -339,8 +337,7 @@ impl SpClient { response = self.client_token_request(&request).await?; } else { return Err(Error::failed_precondition(format!( - "Unable to solve any of {} hash cash challenges", - MAX_TRIES + "Unable to solve any of {MAX_TRIES} hash cash challenges" ))); } } else { @@ -350,8 +347,7 @@ impl SpClient { Some(unknown) => { return Err(Error::unimplemented(format!( - "Unknown client token response type: {:?}", - unknown + "Unknown client token response type: {unknown:?}" ))) } None => return Err(Error::failed_precondition("No client token response type")), @@ -595,20 +591,20 @@ impl SpClient { playlist_limit: Option, artist_limit: Option, ) -> SpClientResult { - let mut endpoint = format!("/user-profile-view/v3/profile/{}", username); + let mut endpoint = format!("/user-profile-view/v3/profile/{username}"); if playlist_limit.is_some() || artist_limit.is_some() { let _ = write!(endpoint, "?"); if let Some(limit) = playlist_limit { - let _ = write!(endpoint, "playlist_limit={}", limit); + let _ = write!(endpoint, "playlist_limit={limit}"); if artist_limit.is_some() { let _ = write!(endpoint, "&"); } } if let Some(limit) = artist_limit { - let _ = write!(endpoint, "artist_limit={}", limit); + let _ = write!(endpoint, "artist_limit={limit}"); } } @@ -617,14 +613,14 @@ impl SpClient { } pub async fn get_user_followers(&self, username: &str) -> SpClientResult { - let endpoint = format!("/user-profile-view/v3/profile/{}/followers", username); + let endpoint = format!("/user-profile-view/v3/profile/{username}/followers"); self.request_as_json(&Method::GET, &endpoint, None, None) .await } pub async fn get_user_following(&self, username: &str) -> SpClientResult { - let endpoint = format!("/user-profile-view/v3/profile/{}/following", username); + let endpoint = format!("/user-profile-view/v3/profile/{username}/following"); self.request_as_json(&Method::GET, &endpoint, None, None) .await @@ -657,14 +653,11 @@ impl SpClient { previous_tracks: Vec, autoplay: bool, ) -> SpClientResult { - let mut endpoint = format!( - "/radio-apollo/v3/{}/{}?autoplay={}", - scope, context_uri, autoplay, - ); + let mut endpoint = format!("/radio-apollo/v3/{scope}/{context_uri}?autoplay={autoplay}"); // Spotify has a default of 50 if let Some(count) = count { - let _ = write!(endpoint, "&count={}", count); + let _ = write!(endpoint, "&count={count}"); } let previous_track_str = previous_tracks @@ -674,7 +667,7 @@ impl SpClient { .join(","); // better than checking `previous_tracks.len() > 0` because the `filter_map` could still return 0 items if !previous_track_str.is_empty() { - let _ = write!(endpoint, "&prev_tracks={}", previous_track_str); + let _ = write!(endpoint, "&prev_tracks={previous_track_str}"); } self.request_as_json(&Method::GET, &endpoint, None, None) diff --git a/metadata/src/request.rs b/metadata/src/request.rs index 503dcf88..fc2c998f 100644 --- a/metadata/src/request.rs +++ b/metadata/src/request.rs @@ -15,10 +15,10 @@ pub trait MercuryRequest { Some(_) => "&", None => "?", }; - let _ = write!(metrics_uri, "{}country={}", separator, session.country()); + let _ = write!(metrics_uri, "{separator}country={}", session.country()); if let Some(product) = session.get_user_attribute("type") { - let _ = write!(metrics_uri, "&product={}", product); + let _ = write!(metrics_uri, "&product={product}"); } trace!("Requesting {}", metrics_uri); @@ -28,7 +28,7 @@ pub trait MercuryRequest { match response.payload.first() { Some(data) => { let data = data.to_vec().into(); - trace!("Received metadata: {:?}", data); + trace!("Received metadata: {data:?}"); Ok(data) } None => Err(Error::unavailable(MetadataError::Empty)), diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 49cc579e..fada2580 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -131,12 +131,12 @@ fn list_compatible_devices() -> SinkResult<()> { AudioFormat::F64, ] { if hwp.test_format(Format::from(*f)).is_ok() { - supported_formats.push(format!("{:?}", f)); + supported_formats.push(format!("{f:?}")); } } if !supported_formats.is_empty() { - println!("\tDevice:\n\n\t\t{}\n", name); + println!("\tDevice:\n\n\t\t{name}\n"); println!( "\tDescription:\n\n\t\t{}\n", diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index ad2a7591..82a7661e 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -27,7 +27,7 @@ pub struct GstreamerSink { impl Open for GstreamerSink { fn open(device: Option, format: AudioFormat) -> Self { - info!("Using GStreamer sink with format: {:?}", format); + info!("Using GStreamer sink with format: {format:?}"); gst::init().expect("failed to init GStreamer!"); let gst_format = match format { diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index b4d24949..9d40ee82 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -40,7 +40,7 @@ impl ProcessHandler for JackData { impl Open for JackSink { fn open(client_name: Option, format: AudioFormat) -> Self { if format != AudioFormat::F32 { - warn!("JACK currently does not support {:?} output", format); + warn!("JACK currently does not support {format:?} output"); } info!("Using JACK sink with format {:?}", AudioFormat::F32); diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 1681ad07..c44245cf 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -27,7 +27,7 @@ fn output_devices() -> Box> { let count = portaudio_rs::device::get_count().unwrap(); let devices = (0..count) .filter_map(|idx| portaudio_rs::device::get_info(idx).map(|info| (idx, info))) - .filter(|&(_, ref info)| info.max_output_channels > 0); + .filter(|(_, info)| info.max_output_channels > 0); Box::new(devices) } @@ -46,13 +46,13 @@ fn list_outputs() { fn find_output(device: &str) -> Option { output_devices() - .find(|&(_, ref info)| info.name == device) + .find(|(_, info)| info.name == device) .map(|(idx, _)| idx) } impl<'a> Open for PortAudioSink<'a> { fn open(device: Option, format: AudioFormat) -> PortAudioSink<'a> { - info!("Using PortAudio sink with format: {:?}", format); + info!("Using PortAudio sink with format: {format:?}"); portaudio_rs::initialize().unwrap(); @@ -88,7 +88,7 @@ impl<'a> Open for PortAudioSink<'a> { AudioFormat::S32 => open_sink!(Self::S32, i32), AudioFormat::S16 => open_sink!(Self::S16, i16), _ => { - unimplemented!("PortAudio currently does not support {:?} output", format) + unimplemented!("PortAudio currently does not support {format:?} output") } } } @@ -168,7 +168,7 @@ impl<'a> Sink for PortAudioSink<'a> { match result { Ok(_) => (), Err(portaudio_rs::PaError::OutputUnderflowed) => error!("PortAudio write underflow"), - Err(e) => panic!("PortAudio error {}", e), + Err(e) => panic!("PortAudio error {e}"), }; Ok(()) diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index b92acefa..43d7ec07 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -64,7 +64,7 @@ impl Open for PulseAudioSink { actual_format = AudioFormat::F32; } - info!("Using PulseAudioSink with format: {:?}", actual_format); + info!("Using PulseAudioSink with format: {actual_format:?}"); Self { sink: None, diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 54851d8b..2632f54a 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -69,11 +69,11 @@ fn list_formats(device: &rodio::Device) { match device.default_output_config() { Ok(cfg) => { debug!(" Default config:"); - debug!(" {:?}", cfg); + debug!(" {cfg:?}"); } Err(e) => { // Use loglevel debug, since even the output is only debug - debug!("Error getting default rodio::Sink config: {}", e); + debug!("Error getting default rodio::Sink config: {e}"); } }; @@ -81,17 +81,17 @@ fn list_formats(device: &rodio::Device) { Ok(mut cfgs) => { if let Some(first) = cfgs.next() { debug!(" Available configs:"); - debug!(" {:?}", first); + debug!(" {first:?}"); } else { return; } for cfg in cfgs { - debug!(" {:?}", cfg); + debug!(" {cfg:?}"); } } Err(e) => { - debug!("Error getting supported rodio::Sink configs: {}", e); + debug!("Error getting supported rodio::Sink configs: {e}"); } } } @@ -117,11 +117,11 @@ fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> { match device.name() { Ok(name) if Some(&name) == default_device_name.as_ref() => (), Ok(name) => { - println!(" {}", name); + println!(" {name}"); list_formats(&device); } Err(e) => { - warn!("Cannot get device name: {}", e); + warn!("Cannot get device name: {e}"); println!(" [unknown name]"); list_formats(&device); } @@ -139,7 +139,7 @@ fn create_sink( Some("?") => match list_outputs(host) { Ok(()) => exit(0), Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } }, @@ -166,8 +166,7 @@ fn create_sink( pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> RodioSink { info!( - "Using Rodio sink with format {:?} and cpal host: {}", - format, + "Using Rodio sink with format {format:?} and cpal host: {}", host.id().name() ); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 4e390262..0d220928 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -45,7 +45,7 @@ impl Open for SdlSink { AudioFormat::S32 => open_sink!(Self::S32, i32), AudioFormat::S16 => open_sink!(Self::S16, i16), _ => { - unimplemented!("SDL currently does not support {:?} output", format) + unimplemented!("SDL currently does not support {format:?} output") } } } diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index b04b8e0d..984999c4 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -49,8 +49,7 @@ impl PassthroughDecoder { pub fn new(rdr: R, format: AudioFileFormat) -> DecoderResult { if !AudioFiles::is_ogg_vorbis(format) { return Err(DecoderError::PassthroughDecoder(format!( - "Passthrough decoder is not implemented for format {:?}", - format + "Passthrough decoder is not implemented for format {format:?}" ))); } @@ -60,7 +59,7 @@ impl PassthroughDecoder { .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; let stream_serial = since_epoch.as_millis() as u32; - info!("Starting passthrough track with serial {}", stream_serial); + info!("Starting passthrough track with serial {stream_serial}"); // search for ident, comment, setup let ident = get_header(1, &mut rdr)?; diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index b0d47acd..6e8bf027 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -51,8 +51,7 @@ impl SymphoniaDecoder { Box::new(Mp3Reader::try_new(mss, &format_opts)?) } else { return Err(DecoderError::SymphoniaDecoder(format!( - "Unsupported format: {:?}", - file_format + "Unsupported format: {file_format:?}" ))); }; @@ -67,8 +66,7 @@ impl SymphoniaDecoder { Box::new(Mp3Decoder::try_new(&track.codec_params, &decoder_opts)?) } else { return Err(DecoderError::SymphoniaDecoder(format!( - "Unsupported decoder: {:?}", - file_format + "Unsupported decoder: {file_format:?}" ))); }; @@ -77,8 +75,7 @@ impl SymphoniaDecoder { })?; if rate != SAMPLE_RATE { return Err(DecoderError::SymphoniaDecoder(format!( - "Unsupported sample rate: {}", - rate + "Unsupported sample rate: {rate}" ))); } @@ -87,8 +84,7 @@ impl SymphoniaDecoder { })?; if channels.count() != NUM_CHANNELS as usize { return Err(DecoderError::SymphoniaDecoder(format!( - "Unsupported number of channels: {}", - channels + "Unsupported number of channels: {channels}" ))); } @@ -215,7 +211,7 @@ impl AudioDecoder for SymphoniaDecoder { 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); + warn!("Skipping malformed audio packet at {position_ms} ms"); skipped = true; continue; } diff --git a/src/main.rs b/src/main.rs index 5bf56e0c..e4727ba8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,10 +46,7 @@ fn usage(program: &str, opts: &getopts::Options) -> String { let repo_home = env!("CARGO_PKG_REPOSITORY"); let desc = env!("CARGO_PKG_DESCRIPTION"); let version = get_version_string(); - let brief = format!( - "{}\n\n{}\n\n{}\n\nUsage: {} []", - version, desc, repo_home, program - ); + let brief = format!("{version}\n\n{desc}\n\n{repo_home}\n\nUsage: {program} []"); opts.usage(&brief) } @@ -87,9 +84,9 @@ fn list_backends() { println!("Available backends: "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { - println!("- {} (default)", name); + println!("- {name} (default)"); } else { - println!("- {}", name); + println!("- {name}"); } } } @@ -593,8 +590,7 @@ fn get_setup() -> Setup { Ok(valid) => Some(valid), Err(s) => { eprintln!( - "Command line argument was not valid Unicode and will not be evaluated: {:?}", - s + "Command line argument was not valid Unicode and will not be evaluated: {s:?}" ); None } @@ -604,7 +600,7 @@ fn get_setup() -> Setup { let matches = match opts.parse(&args[1..]) { Ok(m) => m, Err(e) => { - eprintln!("Error parsing command line options: {}", e); + eprintln!("Error parsing command line options: {e}"); println!("\n{}", usage(&args[0], &opts)); exit(1); } @@ -624,7 +620,7 @@ fn get_setup() -> Setup { match v.into_string() { Ok(value) => Some((key, value)), Err(s) => { - eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {key}={s:?}"); None } } @@ -669,11 +665,11 @@ fn get_setup() -> Setup { for (k, v) in &env_vars { if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { - trace!("\t\t{}=\"XXXXXXXX\"", k); + trace!("\t\t{k}=\"XXXXXXXX\""); } else if v.is_empty() { - trace!("\t\t{}=", k); + trace!("\t\t{k}="); } else { - trace!("\t\t{}=\"{}\"", k, v); + trace!("\t\t{k}=\"{v}\""); } } } @@ -702,13 +698,13 @@ fn get_setup() -> Setup { { if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { // Don't log creds. - trace!("\t\t{} \"XXXXXXXX\"", opt); + trace!("\t\t{opt} \"XXXXXXXX\""); } else { let value = matches.opt_str(opt).unwrap_or_default(); if value.is_empty() { - trace!("\t\t{}", opt); + trace!("\t\t{opt}"); } else { - trace!("\t\t{} \"{}\"", opt, value); + trace!("\t\t{opt} \"{value}\""); } } } @@ -736,19 +732,19 @@ fn get_setup() -> Setup { let invalid_error_msg = |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { - error!("Invalid `--{}` / `-{}`: \"{}\"", long, short, invalid); + error!("Invalid `--{long}` / `-{short}`: \"{invalid}\""); if !valid_values.is_empty() { - println!("Valid `--{}` / `-{}` values: {}", long, short, valid_values); + println!("Valid `--{long}` / `-{short}` values: {valid_values}"); } if !default_value.is_empty() { - println!("Default: {}", default_value); + println!("Default: {default_value}"); } }; let empty_string_error_msg = |long: &str, short: &str| { - error!("`--{}` / `-{}` can not be an empty string", long, short); + error!("`--{long}` / `-{short}` can not be an empty string"); exit(1); }; @@ -1095,7 +1091,7 @@ fn get_setup() -> Setup { match cached_creds { Some(creds) if username == creds.username => Some(creds), _ => { - let prompt = &format!("Password for {}: ", username); + let prompt = &format!("Password for {username}: "); match rpassword::prompt_password(prompt) { Ok(password) => { if !password.is_empty() { From e8f93c44fdbe130d8dc4d57b41d411d835c97509 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Sat, 28 Jan 2023 02:53:23 -0700 Subject: [PATCH 291/561] Fix NamedSpotifyId::to_uri --- CHANGELOG.md | 1 + core/src/spotify_id.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 798fbb04..090ce2e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ https://github.com/librespot-org/librespot from the beginning - [playback] Handle disappearing and invalid devices better - [playback] Handle seek, pause, and play commands while loading +- [metadata] Fix missing colon when converting named spotify IDs to URIs ## [0.4.2] - 2022-07-29 diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index f362fed4..2918f568 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -345,6 +345,7 @@ impl NamedSpotifyId { 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(':'); dst.push_str(item_type); dst.push(':'); let base_62 = self.to_base62()?; From e6830bd09f87c4c9a68f8b20e17d28ac04c36841 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 4 Feb 2023 00:55:05 +0000 Subject: [PATCH 292/561] Bump tokio from 1.23.1 to 1.24.2 Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.23.1 to 1.24.2. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/commits) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 51a346ec..68ab78a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2927,9 +2927,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.23.1" +version = "1.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38a54aca0c15d014013256222ba0ebed095673f89345dd79119d912eb561b7a8" +checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" dependencies = [ "autocfg", "bytes", From a50d0d45107b1119020b340d042bafa7183e7805 Mon Sep 17 00:00:00 2001 From: Federico Date: Fri, 17 Mar 2023 14:57:40 +0100 Subject: [PATCH 293/561] Update rodio to 0.17.1 and cpal to 0.15.1 #1191 Inherit upstream fix https://github.com/RustAudio/cpal/pull/648 --- playback/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index a8d43b68..7982c83a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -43,8 +43,8 @@ gstreamer-audio = { version = "0.18", optional = true } glib = { version = "0.15", optional = true } # Rodio dependencies -rodio = { version = "0.15", optional = true, default-features = false } -cpal = { version = "0.13", optional = true } +rodio = { version = "0.17.1", optional = true, default-features = false } +cpal = { version = "0.15.1", optional = true } # Container and audio decoder symphonia = { version = "0.5", default-features = false, features = ["mp3", "ogg", "vorbis"] } From 9836db2c2c9fbf7cff6343d0e7cdecc78318c064 Mon Sep 17 00:00:00 2001 From: eladyn Date: Wed, 22 Mar 2023 13:35:15 +0100 Subject: [PATCH 294/561] update jack to match alsa requirement --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index aaaf3293..4ed1bbf5 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -33,7 +33,7 @@ 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.10", optional = true } +jack = { version = "0.11", optional = true } sdl2 = { version = "0.35", optional = true } gstreamer = { version = "0.18", optional = true } gstreamer-app = { version = "0.18", optional = true } From 4a4f5d0add0ddac5d4b9c2f4256199f1e07e78ef Mon Sep 17 00:00:00 2001 From: francesco Date: Sat, 25 Mar 2023 19:28:47 +0100 Subject: [PATCH 295/561] Fixes libpulse backend linking error by installing libpulse0 package for all architectures (excepts mipsel, which remains broken) --- contrib/Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index aa29183c..8baa36ae 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -25,6 +25,7 @@ RUN apt-get update RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf crossbuild-essential-mipsel pkg-config RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel +RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf RUN curl https://sh.rustup.rs -sSf | sh -s -- -y ENV PATH="/root/.cargo/bin/:${PATH}" From a7fb7ee67319f503f2b0d0fefa4d56762366d99a Mon Sep 17 00:00:00 2001 From: yubiuser Date: Tue, 11 Apr 2023 20:20:09 +0200 Subject: [PATCH 296/561] Update github actions and add Dependabot (#1141) Update GitHub actions and add Dependabot --- .github/dependabot.yml | 10 ++++++++++ .github/workflows/test.yml | 34 +++++++++++++++++----------------- 2 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89d9269a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + day: saturday + time: "10:00" + open-pull-requests-limit: 10 + target-branch: dev diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 69b0572f..1db716e8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,10 +65,10 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@v1.0.6 with: profile: minimal toolchain: ${{ matrix.toolchain }} @@ -76,7 +76,7 @@ jobs: - name: Get Rustc version id: get-rustc-version - run: echo "::set-output name=version::$(rustc -V)" + run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT shell: bash - name: Cache Rust dependencies @@ -117,10 +117,10 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@v1.0.6 with: toolchain: ${{ matrix.toolchain }} profile: minimal @@ -128,11 +128,11 @@ jobs: - name: Get Rustc version id: get-rustc-version - run: echo "::set-output name=version::$(rustc -V)" + run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT shell: bash - name: Cache Rust dependencies - uses: actions/cache@v2 + uses: actions/cache@v3.3.1 with: path: | ~/.cargo/registry/index @@ -164,10 +164,10 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@v1.0.6 with: profile: minimal target: ${{ matrix.target }} @@ -176,11 +176,11 @@ jobs: - name: Get Rustc version id: get-rustc-version - run: echo "::set-output name=version::$(rustc -V)" + run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT shell: bash - name: Cache Rust dependencies - uses: actions/cache@v2 + uses: actions/cache@v3.3.1 with: path: | ~/.cargo/registry/index @@ -205,10 +205,10 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@v1.0.6 with: profile: minimal toolchain: ${{ matrix.toolchain }} @@ -217,11 +217,11 @@ jobs: - name: Get Rustc version id: get-rustc-version - run: echo "::set-output name=version::$(rustc -V)" + run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT shell: bash - name: Cache Rust dependencies - uses: actions/cache@v2 + uses: actions/cache@v3.3.1 with: path: | ~/.cargo/registry/index @@ -247,9 +247,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1 + uses: actions-rs/toolchain@v1.0.6 with: profile: minimal toolchain: stable From c3df88115204e84e6c3f30e37611c14aead7fa1d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Apr 2023 18:20:35 +0000 Subject: [PATCH 297/561] Bump actions-rs/toolchain from 1.0.6 to 1.0.7 Bumps [actions-rs/toolchain](https://github.com/actions-rs/toolchain) from 1.0.6 to 1.0.7. - [Release notes](https://github.com/actions-rs/toolchain/releases) - [Changelog](https://github.com/actions-rs/toolchain/blob/master/CHANGELOG.md) - [Commits](https://github.com/actions-rs/toolchain/compare/v1.0.6...v1.0.7) --- updated-dependencies: - dependency-name: actions-rs/toolchain dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1db716e8..ae7832e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,7 +68,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.6 + uses: actions-rs/toolchain@v1.0.7 with: profile: minimal toolchain: ${{ matrix.toolchain }} @@ -120,7 +120,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.6 + uses: actions-rs/toolchain@v1.0.7 with: toolchain: ${{ matrix.toolchain }} profile: minimal @@ -167,7 +167,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.6 + uses: actions-rs/toolchain@v1.0.7 with: profile: minimal target: ${{ matrix.target }} @@ -208,7 +208,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.6 + uses: actions-rs/toolchain@v1.0.7 with: profile: minimal toolchain: ${{ matrix.toolchain }} @@ -249,7 +249,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.6 + uses: actions-rs/toolchain@v1.0.7 with: profile: minimal toolchain: stable From 0a62d8a90e4c286f16869d81f03c080e24ed1aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 11 Apr 2023 20:29:14 +0200 Subject: [PATCH 298/561] Update actions/cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .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 1db716e8..bb4e6483 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -80,7 +80,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v2 + uses: actions/cache@v3.3.1 with: path: | ~/.cargo/registry/index From e14dac3ff39ae8373e8bec9755b1b6761c62fbd9 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Tue, 11 Apr 2023 20:33:45 +0200 Subject: [PATCH 299/561] Remove and update dependencies (#1140) --- .github/workflows/test.yml | 6 +- Cargo.lock | 1348 ++++++++++--------- Cargo.toml | 2 +- core/Cargo.toml | 9 +- core/src/authentication.rs | 5 +- playback/Cargo.toml | 12 +- playback/src/audio_backend/gstreamer.rs | 3 +- playback/src/decoder/passthrough_decoder.rs | 16 +- playback/src/decoder/symphonia_decoder.rs | 8 +- protocol/Cargo.toml | 1 - 10 files changed, 707 insertions(+), 703 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1db716e8..198436fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.61" # MSRV (Minimum supported rust version) + - "1.64" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -113,7 +113,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.61" # MSRV (Minimum supported rust version) + - "1.64" # 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.61" # MSRV (Minimum supported rust version) + - "1.64" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/Cargo.lock b/Cargo.lock index 68ab78a6..6c1d9516 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.17.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" dependencies = [ "gimli", ] @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" dependencies = [ "cfg-if", "cipher", @@ -30,23 +30,23 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] [[package]] name = "alsa" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5915f52fe2cf65e83924d037b6c5290b7cee097c6b5c8700746e6168a343fd6b" +checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" dependencies = [ "alsa-sys", "bitflags", "libc", - "nix", + "nix 0.24.3", ] [[package]] @@ -61,15 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" - -[[package]] -name = "array-init" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" [[package]] name = "arrayvec" @@ -79,25 +73,20 @@ checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "async-trait" -version = "0.1.57" +version = "0.1.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76464446b8bc32758d7e88ee1a804d9914cd9b1cb264c029899680b0be29826f" +checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.12", ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic_refcell" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] +checksum = "857253367827bd9d0fd973f0ef15506a96e79e41b0ad7aa691203a4e3214f6c8" [[package]] name = "autocfg" @@ -107,9 +96,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" dependencies = [ "addr2line", "cc", @@ -122,21 +111,27 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.0" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" [[package]] name = "base64ct" -version = "1.5.2" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea2b2456fd614d856680dcd9fcc660a51a820fa09daef2e49772b56a193c8474" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" -version = "0.59.2" +version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ "bitflags", "cexpr", @@ -149,6 +144,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", + "syn 1.0.109", ] [[package]] @@ -159,9 +155,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" -version = "0.10.3" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] @@ -174,9 +170,9 @@ checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" [[package]] name = "bytemuck" -version = "1.12.1" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" [[package]] name = "byteorder" @@ -186,15 +182,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" dependencies = [ "jobserver", ] @@ -216,9 +212,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.10.3" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +checksum = "a35b255461940a32985c627ce82900867c61db1659764d3675ea81963f72a4c6" dependencies = [ "smallvec", ] @@ -231,9 +227,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "cipher" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1873270f8f7942c191139cb8a40fd228da6c3fd2fc376d7e92d47aa14aeb59e" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", @@ -241,13 +237,13 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.4.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" dependencies = [ "glob", "libc", - "libloading 0.7.3", + "libloading", ] [[package]] @@ -272,10 +268,16 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys", + "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" @@ -284,54 +286,55 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreaudio-rs" -version = "0.10.0" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11894b20ebfe1ff903cbdc52259693389eea03b94918a2def2c30c3bf227ad88" +checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" dependencies = [ "bitflags", + "core-foundation-sys 0.6.2", "coreaudio-sys", ] [[package]] name = "coreaudio-sys" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dff444d80630d7073077d38d40b4501fd518bd2b922c2a55edcc8b0f7be57e6" +checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" dependencies = [ "bindgen", ] [[package]] name = "cpal" -version = "0.13.5" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116" +checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" dependencies = [ "alsa", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "coreaudio-rs", - "jack 0.8.3", - "jni", + "dasp_sample", + "jack", + "jni 0.19.0", "js-sys", - "lazy_static", "libc", - "mach", + "mach2", "ndk", - "ndk-glue", - "nix", + "ndk-context", "oboe", - "parking_lot 0.11.2", - "stdweb", - "thiserror", + "once_cell", + "parking_lot", + "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", - "winapi", + "windows", ] [[package]] name = "cpufeatures" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" dependencies = [ "libc", ] @@ -367,47 +370,18 @@ dependencies = [ [[package]] name = "ctr" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d14f329cfbaf5d0e06b5e87fff7e265d2673c5ea7d2c27691a2c107db1442a0" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" dependencies = [ "cipher", ] [[package]] -name = "darling" -version = "0.13.4" +name = "dasp_sample" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" -dependencies = [ - "darling_core", - "quote", - "syn", -] +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "der" @@ -422,9 +396,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adfbc57365a37acbd2ebf2b64d7e69bb766e2fea813521ed536f5d0520dcf86c" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", "crypto-common", @@ -443,15 +417,15 @@ dependencies = [ [[package]] name = "either" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" [[package]] name = "encoding_rs" -version = "0.8.31" +version = "0.8.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" dependencies = [ "cfg-if", ] @@ -467,33 +441,54 @@ dependencies = [ [[package]] name = "enum-iterator-derive" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "828de45d0ca18782232dfb8f3ea9cc428e8ced380eb26a520baaacfc70de39ce" +checksum = "355f93763ef7b0ae1c43c4d8eccc9d5848d84ad1a1d8ce61c421d1ac85a19d05" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "env_logger" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" dependencies = [ - "atty", "humantime", + "is-terminal", "log", "regex", "termcolor", ] [[package]] -name = "fastrand" -version = "1.8.0" +name = "errno" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" dependencies = [ "instant", ] @@ -521,9 +516,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" dependencies = [ "futures-channel", "futures-core", @@ -536,9 +531,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" dependencies = [ "futures-core", "futures-sink", @@ -546,15 +541,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" dependencies = [ "futures-core", "futures-task", @@ -563,32 +558,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" [[package]] name = "futures-macro" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.12", ] [[package]] name = "futures-sink" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" +checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" [[package]] name = "futures-timer" @@ -598,9 +593,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" dependencies = [ "futures-channel", "futures-core", @@ -616,9 +611,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -635,9 +630,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", "libc", @@ -653,14 +648,27 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "gimli" -version = "0.26.2" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "gio-sys" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1d43b0d7968b48455244ecafe41192871257f5740aa6b095eb19db78e362a5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] [[package]] name = "git2" @@ -677,19 +685,22 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.12" +version = "0.17.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "cfb53061756195d76969292c2d2e329e01259276524a9bae6c9b73af62854773" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-executor", "futures-task", + "futures-util", + "gio-sys", "glib-macros", "glib-sys", "gobject-sys", "libc", + "memchr", "once_cell", "smallvec", "thiserror", @@ -697,9 +708,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.15.11" +version = "0.17.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a68131a662b04931e71891fb14aaf65ee4b44d08e8abc10f49e77418c86c64" +checksum = "32e73a9790e243f6d55d8e302426419f6084a1de7a84cd07f7268300408a19de" dependencies = [ "anyhow", "heck", @@ -707,14 +718,14 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "glib-sys" -version = "0.15.10" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4b192f8e65e9cf76cbf4ea71fa8e3be4a0e18ffe3d68b8da6836974cc5bad4" +checksum = "49f00ad0a1bf548e61adfff15d83430941d9e1bb620e334f779edd1c745680a5" dependencies = [ "libc", "system-deps", @@ -722,15 +733,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.15.10" +version = "0.17.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d57ce44246becd17153bd035ab4d32cfee096a657fc01f2231c9278378d1e0a" +checksum = "15e75b0000a64632b2d8ca3cf856af9308e3a970844f6e9659bd197f026793d0" dependencies = [ "glib-sys", "libc", @@ -739,24 +750,25 @@ dependencies = [ [[package]] name = "governor" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19775995ee20209163239355bc3ad2f33f83da35d9ef72dea26e5af753552c87" +checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" dependencies = [ + "cfg-if", "futures", "futures-timer", "no-std-compat", "nonzero_ext", - "parking_lot 0.12.1", + "parking_lot", "rand", "smallvec", ] [[package]] name = "gstreamer" -version = "0.18.8" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d66363bacf5e4f6eb281564adc2902e44c52ae5c45082423e7439e9012b75456" +checksum = "4c46cc10a7ab79329feb68bef54a242ced84c3147cc1b81bc5c6140346a1dbf9" dependencies = [ "bitflags", "cfg-if", @@ -773,14 +785,15 @@ dependencies = [ "option-operations", "paste", "pretty-hex", + "smallvec", "thiserror", ] [[package]] name = "gstreamer-app" -version = "0.18.7" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664adf6abc6546c1ad54492a067dcbc605032c9c789ce8f6f78cb9ddeef4b684" +checksum = "aa1550d18fe8d97900148cc97d63a3212c3d53169c8469b9bf617de8953c05a8" dependencies = [ "bitflags", "futures-core", @@ -795,9 +808,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b401f21d731b3e5de802487f25507fabd34de2dd007d582f440fb1c66a4fbb" +checksum = "d7cb5375aa8c23012ec5fadde1a37b972e87680776772669d2628772867056e2" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -808,11 +821,10 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.18.7" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ceb43e669be4c33c38b273fd4ca0511c0a7748987835233c529fc3c805c807e" +checksum = "8ca6d26ab15835a268939e2367ed4ddb1e7157b03d0bb56ba4a0b036c1ac8393" dependencies = [ - "array-init", "bitflags", "cfg-if", "glib", @@ -825,9 +837,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.18.3" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34258fb53c558c0f41dad194037cbeaabf49d347570df11b8bd1c4897cf7d7c" +checksum = "9d4001b779e4707b32acd6ec0960e327b926369c1a34f7c41d477ac42b2670e8" dependencies = [ "glib-sys", "gobject-sys", @@ -839,10 +851,11 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224f35f36582407caf58ded74854526beeecc23d0cf64b8d1c3e00584ed6863f" +checksum = "5598bfedbff12675a6cddbe420b6a3ba5039c64aaf7df130db6339d09b634b0e" dependencies = [ + "atomic_refcell", "bitflags", "cfg-if", "glib", @@ -853,9 +866,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a083493c3c340e71fa7c66eebda016e9fafc03eb1b4804cf9b2bad61994b078e" +checksum = "26114ed96f6668380f5a1554128159e98e06c3a7a8460f216d7cd6dce28f928c" dependencies = [ "glib-sys", "gobject-sys", @@ -866,9 +879,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3517a65d3c2e6f8905b456eba5d53bda158d664863aef960b44f651cb7d33e2" +checksum = "e56fe047adef7d47dbafa8bc1340fddb53c325e16574763063702fc94b5786d2" dependencies = [ "glib-sys", "gobject-sys", @@ -878,9 +891,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.14" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" dependencies = [ "bytes", "fnv", @@ -907,7 +920,7 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ - "base64", + "base64 0.13.1", "bitflags", "bytes", "headers-core", @@ -928,19 +941,25 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.1.19" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" dependencies = [ "libc", ] +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + [[package]] name = "hex" version = "0.4.3" @@ -969,9 +988,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes", "fnv", @@ -1009,9 +1028,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" dependencies = [ "bytes", "futures-channel", @@ -1069,25 +1088,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" dependencies = [ "http", "hyper", "log", - "rustls 0.20.6", + "rustls 0.20.8", "rustls-native-certs 0.6.2", "tokio", "tokio-rustls 0.23.4", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "0.3.0" @@ -1110,9 +1123,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown", @@ -1136,33 +1149,43 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +dependencies = [ + "hermit-abi 0.3.1", + "io-lifetimes", + "rustix", + "windows-sys 0.45.0", +] + [[package]] name = "itoa" -version = "1.0.3" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "jack" -version = "0.8.3" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3902a02287c3dcad784edd1ecc5f487774b63a5fe77dd9842ea9a993d0a4a23" +checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" dependencies = [ "bitflags", - "jack-sys 0.2.2", - "lazy_static", - "libc", - "log", -] - -[[package]] -name = "jack" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce722655a29b13bb98ec7e8ba9dc65d670b9b37c7b1c09775c7f7516811c5a36" -dependencies = [ - "bitflags", - "jack-sys 0.4.0", + "jack-sys", "lazy_static", "libc", "log", @@ -1170,25 +1193,15 @@ dependencies = [ [[package]] name = "jack-sys" -version = "0.2.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57983f0d72dfecf2b719ed39bc9cacd85194e1a94cb3f9146009eff9856fef41" -dependencies = [ - "lazy_static", - "libc", - "libloading 0.6.7", -] - -[[package]] -name = "jack-sys" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d70559ff166d148ccb750ddd77702af760718f3a752c731add168c22c16a9f" +checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" dependencies = [ "bitflags", "lazy_static", "libc", - "libloading 0.7.3", + "libloading", + "log", "pkg-config", ] @@ -1206,6 +1219,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "jni" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +dependencies = [ + "cesu8", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", +] + [[package]] name = "jni-sys" version = "0.3.0" @@ -1214,18 +1241,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" dependencies = [ "wasm-bindgen", ] @@ -1247,9 +1274,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" [[package]] name = "libgit2-sys" @@ -1265,19 +1292,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.6.7" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351a32417a12d5f7e82c368a66781e307834dae04c6ce0cd4456d52989229883" -dependencies = [ - "cfg-if", - "winapi", -] - -[[package]] -name = "libloading" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", "winapi", @@ -1285,15 +1302,15 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "292a948cd991e376cf75541fe5b97a1081d713c618b4f1b9500f8844e49eb565" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" [[package]] name = "libmdns" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cfa684d4e75145d6c1fbe9a2547c2fa4d4d4e5bd6eacce779fd86d5e1cd7cbe" +checksum = "0b04ae6b56b3b19ade26f0e7e7c1360a1713514f326c5ed0797cf2c109c9e010" dependencies = [ "byteorder", "futures-util", @@ -1301,17 +1318,19 @@ dependencies = [ "if-addrs", "log", "multimap", + "nix 0.23.2", "rand", "socket2", "thiserror", "tokio", + "winapi", ] [[package]] name = "libpulse-binding" -version = "2.26.0" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663" +checksum = "1745b20bfc194ac12ef828f144f0ec2d4a7fe993281fa3567a0bd4969aee6890" dependencies = [ "bitflags", "libc", @@ -1323,9 +1342,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.25.0" +version = "2.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cbf1a1dfd69a48cb60906399fa1d17f1b75029ef51c0789597be792dfd0bcd5" +checksum = "5ced94199e6e44133431374e4043f34e1f0697ebfb7b7d6c244a65bfaedf0e31" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1334,9 +1353,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c73f96f9ca34809692c4760cfe421225860aa000de50edab68a16221fd27cc1" +checksum = "84e423d9c619c908ce9b4916080e65ab586ca55b8c4939379f15e6e72fb43842" dependencies = [ "libpulse-sys", "pkg-config", @@ -1344,9 +1363,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.3" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30" +checksum = "2191e6880818d1df4cf72eac8e91dce7a5a52ba0da4b2a5cdafabc22b937eadb" dependencies = [ "libc", "num-derive", @@ -1391,7 +1410,7 @@ dependencies = [ "hyper", "librespot-core", "log", - "parking_lot 0.12.1", + "parking_lot", "tempfile", "thiserror", "tokio", @@ -1421,7 +1440,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64", + "base64 0.13.1", "byteorder", "bytes", "dns-sd", @@ -1436,17 +1455,16 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", - "hyper-rustls 0.23.0", + "hyper-rustls 0.23.2", "librespot-protocol", "log", "nonzero_ext", - "num", "num-bigint", "num-derive", "num-integer", "num-traits", "once_cell", - "parking_lot 0.12.1", + "parking_lot", "pbkdf2", "priority-queue", "protobuf", @@ -1474,7 +1492,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64", + "base64 0.13.1", "cfg-if", "ctr", "dns-sd", @@ -1524,7 +1542,7 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-audio", - "jack 0.10.0", + "jack", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -1532,7 +1550,7 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot 0.12.1", + "parking_lot", "portaudio-rs", "rand", "rand_distr", @@ -1549,7 +1567,6 @@ dependencies = [ name = "librespot-protocol" version = "0.5.0-dev" dependencies = [ - "glob", "protobuf", "protobuf-codegen", ] @@ -1566,6 +1583,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + [[package]] name = "lock_api" version = "0.4.9" @@ -1586,10 +1609,10 @@ dependencies = [ ] [[package]] -name = "mach" -version = "0.3.2" +name = "mach2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" dependencies = [ "libc", ] @@ -1617,9 +1640,9 @@ dependencies = [ [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minimal-lexical" @@ -1629,30 +1652,30 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.5.4" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" dependencies = [ "libc", "log", "wasi", - "windows-sys 0.36.1", + "windows-sys 0.45.0", ] [[package]] name = "muldiv" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5136edda114182728ccdedb9f5eda882781f35fa6e80cc360af12a8932507f3" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "multimap" @@ -1665,14 +1688,15 @@ dependencies = [ [[package]] name = "ndk" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ "bitflags", "jni-sys", "ndk-sys", "num_enum", + "raw-window-handle", "thiserror", ] @@ -1682,48 +1706,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" -[[package]] -name = "ndk-glue" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f" -dependencies = [ - "lazy_static", - "libc", - "log", - "ndk", - "ndk-context", - "ndk-macro", - "ndk-sys", -] - -[[package]] -name = "ndk-macro" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" -dependencies = [ - "darling", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ndk-sys" -version = "0.3.0" +version = "0.4.1+23.1.7779620" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" dependencies = [ "jni-sys", ] [[package]] name = "nix" -version = "0.23.1" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ "bitflags", "cc", @@ -1732,6 +1728,17 @@ dependencies = [ "memoffset", ] +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -1740,9 +1747,9 @@ checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" [[package]] name = "nom" -version = "7.1.1" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ "memchr", "minimal-lexical", @@ -1763,20 +1770,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", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.3" @@ -1791,9 +1784,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" dependencies = [ "byteorder", "lazy_static", @@ -1806,15 +1799,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-complex" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" -dependencies = [ - "num-traits", -] - [[package]] name = "num-derive" version = "0.3.3" @@ -1823,7 +1807,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] @@ -1854,7 +1838,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" dependencies = [ "autocfg", - "num-bigint", "num-integer", "num-traits", ] @@ -1871,60 +1854,51 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" dependencies = [ - "hermit-abi", + "hermit-abi 0.2.6", "libc", ] [[package]] name = "num_enum" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.7" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", + "syn 1.0.109", ] [[package]] name = "object" -version = "0.29.0" +version = "0.30.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" dependencies = [ "memchr", ] [[package]] name = "oboe" -version = "0.4.6" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27f63c358b4fa0fbcfefd7c8be5cfc39c08ce2389f5325687e7762a48d30a5c1" +checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" dependencies = [ - "jni", + "jni 0.20.0", "ndk", "ndk-context", "num-derive", @@ -1934,27 +1908,27 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.5" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3370abb7372ed744232c12954d920d1a40f1c4686de9e79e800021ef492294bd" +checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" dependencies = [ "cc", ] [[package]] name = "ogg" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6951b4e8bf21c8193da321bcce9c9dd2e13c858fe078bf9054a288b419ae5d6e" +checksum = "960d0efc0531a452c442c777288f704b300a5f743c04a14eba71f9aabc4897ac" dependencies = [ "byteorder", ] [[package]] name = "once_cell" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" [[package]] name = "openssl-probe" @@ -1964,24 +1938,13 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "option-operations" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b01597916c91a493b1e8a2fde64fec1764be3259abc1f06efc99c274f150a2" +checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" dependencies = [ "paste", ] -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.5", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -1989,50 +1952,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.3", + "parking_lot_core", ] [[package]] name = "parking_lot_core" -version = "0.8.5" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall", + "redox_syscall 0.2.16", "smallvec", "thread-id", - "windows-sys 0.36.1", + "windows-sys 0.45.0", ] [[package]] name = "paste" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" [[package]] name = "pbkdf2" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" dependencies = [ "digest", "hmac", @@ -2061,9 +2010,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "petgraph" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", "indexmap", @@ -2105,9 +2054,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.25" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" [[package]] name = "portaudio-rs" @@ -2132,9 +2081,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty-hex" @@ -2144,9 +2093,9 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "priority-queue" -version = "1.2.3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "815082d99af3acc75a3e67efd2a07f72e67b4e81b4344eb8ca34c6ebf3dfa9c5" +checksum = "5ca9c6be70d989d21a136eb86c2d83e4b328447fac4a88dace2143c179c86267" dependencies = [ "autocfg", "indexmap", @@ -2154,13 +2103,12 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.2.1" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "thiserror", - "toml", + "toml_edit", ] [[package]] @@ -2172,7 +2120,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "version_check", ] @@ -2189,9 +2137,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.49" +version = "1.0.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" dependencies = [ "unicode-ident", ] @@ -2259,9 +2207,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" dependencies = [ "proc-macro2", ] @@ -2306,6 +2254,12 @@ dependencies = [ "rand", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "redox_syscall" version = "0.2.16" @@ -2316,10 +2270,19 @@ dependencies = [ ] [[package]] -name = "regex" -version = "1.6.0" +name = "redox_syscall" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" dependencies = [ "aho-corasick", "memchr", @@ -2328,18 +2291,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "ring" @@ -2358,20 +2312,21 @@ dependencies = [ [[package]] name = "rodio" -version = "0.15.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0939e9f626e6c6f1989adb6226a039c855ca483053f0ee7c98b90e41cf731e" +checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" dependencies = [ "cpal", ] [[package]] name = "rpassword" -version = "7.0.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b763cb66df1c928432cc35053f8bd4cec3335d8559fc16010017d16b3c1680" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" dependencies = [ "libc", + "rtoolbox", "winapi", ] @@ -2396,10 +2351,20 @@ dependencies = [ ] [[package]] -name = "rustc-demangle" -version = "0.1.21" +name = "rtoolbox" +version = "0.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" [[package]] name = "rustc-hash" @@ -2407,13 +2372,27 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.37.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + [[package]] name = "rustls" version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ - "base64", + "base64 0.13.1", "log", "ring", "sct 0.6.1", @@ -2422,9 +2401,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.6" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" dependencies = [ "log", "ring", @@ -2458,24 +2437,24 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64", + "base64 0.21.0", ] [[package]] name = "rustversion" -version = "1.0.9" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" [[package]] name = "ryu" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" [[package]] name = "same-file" @@ -2488,12 +2467,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" dependencies = [ - "lazy_static", - "windows-sys 0.36.1", + "windows-sys 0.42.0", ] [[package]] @@ -2547,52 +2525,52 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.7.0" +version = "2.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "security-framework-sys", ] [[package]] name = "security-framework-sys" -version = "2.6.1" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", ] [[package]] name = "serde" -version = "1.0.145" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.12", ] [[package]] name = "serde_json" -version = "1.0.85" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" dependencies = [ "itoa", "ryu", @@ -2600,14 +2578,12 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "serde_spanned" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" dependencies = [ - "cfg-if", - "cpufeatures", - "digest", + "serde", ] [[package]] @@ -2644,33 +2620,33 @@ checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "slab" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "socket2" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" dependencies = [ "libc", "winapi", @@ -2692,18 +2668,6 @@ dependencies = [ "der", ] -[[package]] -name = "stdweb" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "subtle" version = "2.4.1" @@ -2712,9 +2676,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "symphonia" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17033fe05e4f7f10a6ad602c272bafd2520b2e5cdd9feb61494d9cdce08e002f" +checksum = "3671dd6f64f4f9d5c87179525054cfc1f60de23ba1f193bd6ceab812737403f1" dependencies = [ "lazy_static", "symphonia-bundle-mp3", @@ -2726,9 +2690,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db5d3d53535ae2b7d0e39e82f683cac5398a6c8baca25ff1183e107d13959d3e" +checksum = "55a0846e7a2c9a8081ff799fc83a975170417ad2a143f644a77ec2e3e82a2b73" dependencies = [ "bitflags", "lazy_static", @@ -2739,9 +2703,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323b94435a1a807e1001e29490aeaef2660fb72b145d47497e8429a6cb1d67c3" +checksum = "7dfed6f7b6bfa21d7cef1acefc8eae5db80df1608a1aca91871b07cbd28d7b74" dependencies = [ "log", "symphonia-core", @@ -2750,9 +2714,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "199a6417cd4115bac79289b64b859358ea050b7add0ceb364dc991f628c5b347" +checksum = "6b9567e2d8a5f866b2f94f5d366d811e0c6826babcff6d37de9e1a6690d38869" dependencies = [ "arrayvec", "bitflags", @@ -2763,9 +2727,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d2f741469a0f103607ed1f2605f7f00b13ba044ea9ddc616764558c6d3d9b7d" +checksum = "474df6e86b871dcb56913130bada1440245f483057c4a2d8a2981455494c4439" dependencies = [ "log", "symphonia-core", @@ -2775,9 +2739,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed71acf6b5e6e8bee1509597b86365a06b78c1d73218df47357620a6fe5997b" +checksum = "acd35c263223ef6161000be79b124a75de3e065eea563bf3ef169b3e94c7bb2e" dependencies = [ "encoding_rs", "lazy_static", @@ -2787,9 +2751,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73cbb0766ce77a8aef535f9438db645e7b6f1b2c4cf3be9bf246b4e11a7d5531" +checksum = "ce340a6c33ac06cb42de01220308ec056e8a2a3d5cc664aaf34567392557136b" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -2797,9 +2761,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.107" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", @@ -2807,25 +2771,24 @@ dependencies = [ ] [[package]] -name = "synstructure" -version = "0.12.6" +name = "syn" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" dependencies = [ "proc-macro2", "quote", - "syn", - "unicode-xid", + "unicode-ident", ] [[package]] name = "sysinfo" -version = "0.27.2" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17351d0e9eb8841897b14e9669378f3c69fb57779cc04f8ca9a9d512edfb2563" +checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" dependencies = [ "cfg-if", - "core-foundation-sys", + "core-foundation-sys 0.8.3", "libc", "ntapi", "once_cell", @@ -2834,9 +2797,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.2" +version = "6.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a45a1c4c9015217e12347f2a411b57ce2c4fc543913b14b6fe40483328e709" +checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f" dependencies = [ "cfg-expr", "heck", @@ -2847,45 +2810,44 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "redox_syscall 0.3.5", + "rustix", + "windows-sys 0.45.0", ] [[package]] name = "termcolor" -version = "1.1.3" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.12", ] [[package]] @@ -2895,19 +2857,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" dependencies = [ "libc", - "redox_syscall", + "redox_syscall 0.2.16", "winapi", ] [[package]] name = "time" -version = "0.3.14" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3f9a28b618c3a6b9251b6908e9c99e04b9e5c02e6581ccbb67d59c34ef7f9b" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", - "libc", - "num_threads", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", ] [[package]] @@ -2921,39 +2899,38 @@ dependencies = [ [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.24.2" +version = "1.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a12a59981d9e3c38d216785b0c37399f6e415e8d0712047620f189371b0bb" +checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" dependencies = [ "autocfg", "bytes", "libc", - "memchr", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.45.0", ] [[package]] name = "tokio-macros" -version = "1.8.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.12", ] [[package]] @@ -2973,16 +2950,16 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "rustls 0.20.6", + "rustls 0.20.8", "tokio", "webpki 0.22.0", ] [[package]] name = "tokio-stream" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6edf2d6bc038a43d31353570e27270603f4648d18f5ed10c0e179abe43255af" +checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" dependencies = [ "futures-core", "pin-project-lite", @@ -2991,13 +2968,13 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.17.2" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" +checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" dependencies = [ "futures-util", "log", - "rustls 0.20.6", + "rustls 0.20.8", "rustls-native-certs 0.6.2", "tokio", "tokio-rustls 0.23.4", @@ -3007,9 +2984,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.4" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" +checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" dependencies = [ "bytes", "futures-core", @@ -3021,11 +2998,36 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", ] [[package]] @@ -3036,9 +3038,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.36" +version = "0.1.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", "pin-project-lite", @@ -3047,34 +3049,34 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.29" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.17.3" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" +checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" dependencies = [ - "base64", + "base64 0.13.1", "byteorder", "bytes", "http", "httparse", "log", "rand", - "rustls 0.20.6", - "sha-1", + "rustls 0.20.8", + "sha1", "thiserror", "url", "utf-8", @@ -3083,21 +3085,21 @@ dependencies = [ [[package]] name = "typenum" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.6" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unicode-normalization" @@ -3114,12 +3116,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "unicode-xid" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" - [[package]] name = "untrusted" version = "0.7.1" @@ -3145,9 +3141,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.1.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" dependencies = [ "getrandom", "rand", @@ -3177,9 +3173,9 @@ dependencies = [ [[package]] name = "version-compare" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe88247b92c1df6b6de80ddc290f3976dbdf2f5f5d3fd049a9fb598c6dd5ca73" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" [[package]] name = "version_check" @@ -3189,12 +3185,11 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.2" +version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" dependencies = [ "same-file", - "winapi", "winapi-util", ] @@ -3216,9 +3211,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3226,24 +3221,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-shared", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.83" +name = "wasm-bindgen-futures" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3251,28 +3258,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.83" +version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" [[package]] name = "web-sys" -version = "0.3.60" +version = "0.3.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" +checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" dependencies = [ "js-sys", "wasm-bindgen", @@ -3300,9 +3307,9 @@ dependencies = [ [[package]] name = "which" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", @@ -3341,16 +3348,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.36.1" +name = "windows" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets", ] [[package]] @@ -3360,85 +3363,88 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" +name = "winnow" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] [[package]] name = "zerocopy" @@ -3452,17 +3458,17 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0fbc82b82efe24da867ee52e015e58178684bd9dd64c34e66bdf21da2582a9f" +checksum = "6505e6815af7de1746a08f69c69606bb45695a17149517680f3b2149713b19a3" dependencies = [ "proc-macro2", - "syn", - "synstructure", + "quote", + "syn 1.0.109", ] [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/Cargo.toml b/Cargo.toml index 7a423d76..98b8405b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ path = "protocol" version = "0.5.0-dev" [dependencies] -env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] } +env_logger = { version = "0.10", default-features = false, features = ["color", "humantime", "auto-color"] } futures-util = { version = "0.3", default_features = false } getopts = "0.2" hex = "0.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index 26c4b5b1..db6ee7a7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,7 +22,7 @@ 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"] } -governor = { version = "0.4", default-features = false, features = ["std", "jitter"] } +governor = { version = "0.5", default-features = false, features = ["std", "jitter"] } hex = "0.4" hmac = "0.12" httparse = "1.7" @@ -32,14 +32,13 @@ hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] hyper-rustls = { version = "0.23", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" -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" parking_lot = { version = "0.12", features = ["deadlock_detection"] } -pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] } +pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "1.2" protobuf = "3" quick-xml = { version = "0.23", features = ["serialize"] } @@ -49,7 +48,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = "0.10" shannon = "0.2" -sysinfo = { version = "0.27", default-features = false } +sysinfo = { version = "0.28", default-features = false } thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } @@ -64,7 +63,7 @@ rand = "0.8" vergen = { version = "7", default-features = false, features = ["build", "git"] } [dev-dependencies] -env_logger = "0.9" +env_logger = "0.10" tokio = { version = "1", features = ["macros", "parking_lot"] } [features] diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 655a5311..42326e6b 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -2,8 +2,7 @@ use std::io::{self, Read}; use aes::Aes192; use byteorder::{BigEndian, ByteOrder}; -use hmac::Hmac; -use pbkdf2::pbkdf2; +use pbkdf2::pbkdf2_hmac; use protobuf::Enum; use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; @@ -96,7 +95,7 @@ impl Credentials { return Err(AuthenticationError::Key.into()); } - pbkdf2::>(&secret, username.as_bytes(), 0x100, &mut key[0..20]); + pbkdf2_hmac::(&secret, username.as_bytes(), 0x100, &mut key[0..20]); let hash = &Sha1::digest(&key[..20]); key[..20].copy_from_slice(hash); diff --git a/playback/Cargo.toml b/playback/Cargo.toml index a1cd10aa..5ef5b4f7 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -31,16 +31,16 @@ tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sy zerocopy = "0.6" # Backends -alsa = { version = "0.6", optional = true } +alsa = { version = "0.7", 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.11", optional = true } sdl2 = { version = "0.35", optional = true } -gstreamer = { version = "0.18", optional = true } -gstreamer-app = { version = "0.18", optional = true } -gstreamer-audio = { version = "0.18", optional = true } -glib = { version = "0.15", optional = true } +gstreamer = { version = "0.20", optional = true } +gstreamer-app = { version = "0.20", optional = true } +gstreamer-audio = { version = "0.20", optional = true } +glib = { version = "0.17", optional = true } # Rodio dependencies rodio = { version = "0.17.1", optional = true, default-features = false } @@ -50,7 +50,7 @@ cpal = { version = "0.15.1", optional = true } 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 } +ogg = { version = "0.9", optional = true } # Dithering rand = { version = "0.8", features = ["small_rng"] } diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 82a7661e..e3cc78cf 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -48,7 +48,8 @@ impl Open for GstreamerSink { let gst_bytes = NUM_CHANNELS as usize * 2048 * sample_size; let pipeline = gst::Pipeline::new(None); - let appsrc = gst::ElementFactory::make("appsrc", None) + let appsrc = gst::ElementFactory::make("appsrc") + .build() .expect("Failed to create GStreamer appsrc element") .downcast::() .expect("couldn't cast AppSrc element at runtime!"); diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 984999c4..59a72163 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -14,7 +14,7 @@ use crate::{ MS_PER_PAGE, PAGES_PER_MS, }; -fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> +fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where T: Read + Seek, { @@ -29,19 +29,19 @@ where return Err(DecoderError::PassthroughDecoder("Invalid Data".into())); } - Ok(pck.data.into_boxed_slice()) + Ok(pck.data) } pub struct PassthroughDecoder { rdr: PacketReader, - wtr: PacketWriter>, + wtr: PacketWriter<'static, Vec>, eos: bool, bos: bool, ofsgp_page: u64, stream_serial: u32, - ident: Box<[u8]>, - comment: Box<[u8]>, - setup: Box<[u8]>, + ident: Vec, + comment: Vec, + setup: Vec, } impl PassthroughDecoder { @@ -98,7 +98,7 @@ impl AudioDecoder for PassthroughDecoder { let absgp_page = pck.absgp_page() - self.ofsgp_page; self.wtr .write_packet( - pck.data.into_boxed_slice(), + pck.data, self.stream_serial, PacketWriteEndInfo::EndStream, absgp_page, @@ -196,7 +196,7 @@ impl AudioDecoder for PassthroughDecoder { self.wtr .write_packet( - pck.data.into_boxed_slice(), + pck.data, self.stream_serial, inf, pckgp_page - self.ofsgp_page, diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 6e8bf027..2bf2517d 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -11,8 +11,8 @@ use symphonia::{ units::Time, }, default::{ - codecs::{Mp3Decoder, VorbisDecoder}, - formats::{Mp3Reader, OggReader}, + codecs::{MpaDecoder, VorbisDecoder}, + formats::{MpaReader, OggReader}, }, }; @@ -48,7 +48,7 @@ impl SymphoniaDecoder { 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)?) + Box::new(MpaReader::try_new(mss, &format_opts)?) } else { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported format: {file_format:?}" @@ -63,7 +63,7 @@ impl SymphoniaDecoder { 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)?) + Box::new(MpaDecoder::try_new(&track.codec_params, &decoder_opts)?) } else { return Err(DecoderError::SymphoniaDecoder(format!( "Unsupported decoder: {file_format:?}" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 5ca462d3..b86a8225 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -13,5 +13,4 @@ edition = "2021" protobuf = "3" [build-dependencies] -glob = "0.3" protobuf-codegen = "3" From cd56225935a1f86e1da2dd73d102c7631bcbe095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 11 Apr 2023 22:34:58 +0200 Subject: [PATCH 300/561] Remove actions-rs/toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/test.yml | 35 ++++++----------------------------- 1 file changed, 6 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2ca06bb8..823cc193 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -68,11 +68,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - toolchain: ${{ matrix.toolchain }} - override: true + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - name: Get Rustc version id: get-rustc-version @@ -120,11 +116,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - toolchain: ${{ matrix.toolchain }} - profile: minimal - override: true + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - name: Get Rustc version id: get-rustc-version @@ -167,12 +159,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - target: ${{ matrix.target }} - toolchain: ${{ matrix.toolchain }} - override: true + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - name: Get Rustc version id: get-rustc-version @@ -208,12 +195,7 @@ jobs: uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - toolchain: ${{ matrix.toolchain }} - override: true - components: clippy + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y - name: Get Rustc version id: get-rustc-version @@ -243,16 +225,11 @@ jobs: fmt: needs: clippy - name: cargo +${{ matrix.toolchain }} fmt + name: cargo fmt runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v3.5.0 - name: Install toolchain - uses: actions-rs/toolchain@v1.0.7 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check From 453ef48385f5642e1c0e8a859c7335c4237e98ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 11 Apr 2023 22:21:37 +0200 Subject: [PATCH 301/561] Update rsa MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 253 +++++++++++++++++++------------ core/Cargo.toml | 4 +- core/src/connection/handshake.rs | 15 +- 3 files changed, 165 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c1d9516..51c73c7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.7.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" [[package]] name = "core-foundation" @@ -268,7 +268,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", ] @@ -280,9 +280,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "coreaudio-rs" @@ -311,7 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" dependencies = [ "alsa", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "coreaudio-rs", "dasp_sample", "jack", @@ -339,16 +339,6 @@ dependencies = [ "libc", ] -[[package]] -name = "crypto-bigint" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" -dependencies = [ - "generic-array", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -385,13 +375,13 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "der" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", - "crypto-bigint", "pem-rfc7468", + "zeroize", ] [[package]] @@ -401,6 +391,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", + "const-oid", "crypto-common", "subtle", ] @@ -432,9 +423,9 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "1.1.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" +checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" dependencies = [ "enum-iterator-derive", ] @@ -465,13 +456,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -570,7 +561,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -630,9 +621,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -672,9 +663,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.14.4" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" dependencies = [ "bitflags", "libc", @@ -708,9 +699,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e73a9790e243f6d55d8e302426419f6084a1de7a84cd07f7268300408a19de" +checksum = "bc4cf346122086f196260783aa58987190dbd5f43bfab01946d2bf9786e8d9ef" dependencies = [ "anyhow", "heck", @@ -821,9 +812,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6d26ab15835a268939e2367ed4ddb1e7157b03d0bb56ba4a0b036c1ac8393" +checksum = "06b5a8658e575f6469053026ac663a348d5a562c9fce20ab2ca0c349e05d079e" dependencies = [ "bitflags", "cfg-if", @@ -1151,25 +1142,25 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1274,15 +1265,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "libgit2-sys" -version = "0.13.5+1.4.5" +version = "0.14.2+1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" dependencies = [ "cc", "libc", @@ -1995,9 +1986,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem-rfc7468" -version = "0.3.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" dependencies = [ "base64ct", ] @@ -2032,24 +2023,24 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.3.3" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" dependencies = [ "der", "pkcs8", + "spki", "zeroize", ] [[package]] name = "pkcs8" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ "der", "spki", - "zeroize", ] [[package]] @@ -2137,9 +2128,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.54" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2332,9 +2323,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.6.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" dependencies = [ "byteorder", "digest", @@ -2345,7 +2336,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", - "smallvec", + "signature", "subtle", "zeroize", ] @@ -2374,16 +2365,16 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.6" +version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2531,7 +2522,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", "security-framework-sys", ] @@ -2542,28 +2533,28 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", ] [[package]] name = "serde" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -2627,6 +2618,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.8" @@ -2660,9 +2661,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.5.4" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" dependencies = [ "base64ct", "der", @@ -2772,9 +2773,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" dependencies = [ "proc-macro2", "quote", @@ -2788,7 +2789,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" dependencies = [ "cfg-if", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", "ntapi", "once_cell", @@ -2847,7 +2848,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -2930,7 +2931,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -3141,9 +3142,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" dependencies = [ "getrandom", "rand", @@ -3157,9 +3158,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "7.4.2" +version = "7.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749" dependencies = [ "anyhow", "cfg-if", @@ -3353,7 +3354,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", ] [[package]] @@ -3362,13 +3363,13 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3377,7 +3378,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] @@ -3386,13 +3396,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -3401,42 +3426,84 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winnow" version = "0.4.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index db6ee7a7..479961d7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -43,10 +43,10 @@ priority-queue = "1.2" protobuf = "3" quick-xml = { version = "0.23", features = ["serialize"] } rand = "0.8" -rsa = "0.6" +rsa = "0.8.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -sha1 = "0.10" +sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" sysinfo = { version = "0.28", default-features = false } thiserror = "1.0" diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index abcc0a3d..e4fdba4f 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -4,7 +4,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; -use rsa::{BigUint, PublicKey}; +use rsa::{BigUint, Pkcs1v15Sign, PublicKey}; use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -83,11 +83,9 @@ pub async fn handshake( })?; let hash = Sha1::digest(&remote_key); - let padding = PaddingScheme(rsa::padding::PaddingScheme::new_pkcs1v15_sign(Some( - rsa::hash::Hash::SHA1, - ))); + let padding = Pkcs1v15Sign::new::(); public_key - .verify(padding.0, &hash, &remote_signature) + .verify(padding, &hash, &remote_signature) .map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, @@ -105,13 +103,6 @@ pub async fn handshake( Ok(codec.framed(connection)) } -// Workaround for https://github.com/RustCrypto/RSA/issues/214 -struct PaddingScheme(rsa::padding::PaddingScheme); - -/// # Safety -/// The `rsa::padding::PaddingScheme` variant we use is actually `Send`. -unsafe impl Send for PaddingScheme {} - async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> where T: AsyncWrite + Unpin, From 2e7944f43e442c78a77e7e1fad7b6912cc42e8be Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Apr 2023 17:09:42 +0000 Subject: [PATCH 302/561] Bump h2 from 0.3.16 to 0.3.17 Bumps [h2](https://github.com/hyperium/h2) from 0.3.16 to 0.3.17. - [Release notes](https://github.com/hyperium/h2/releases) - [Changelog](https://github.com/hyperium/h2/blob/master/CHANGELOG.md) - [Commits](https://github.com/hyperium/h2/compare/v0.3.16...v0.3.17) --- updated-dependencies: - dependency-name: h2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c1d9516..6e38ba87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -891,9 +891,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" dependencies = [ "bytes", "fnv", From e8c3ee07ef2b4e3f5cc13b9cb205c6efcd5ad3bf Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 13 Apr 2023 20:02:33 +0200 Subject: [PATCH 303/561] Update base64 (#1148) --- Cargo.lock | 202 ++++++++++++++++++++++++------------- core/Cargo.toml | 2 +- core/src/authentication.rs | 10 +- discovery/Cargo.toml | 2 +- discovery/src/server.rs | 9 +- 5 files changed, 149 insertions(+), 76 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6e38ba87..46d19fec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -268,7 +268,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", ] @@ -280,9 +280,9 @@ checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "coreaudio-rs" @@ -311,7 +311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" dependencies = [ "alsa", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "coreaudio-rs", "dasp_sample", "jack", @@ -432,9 +432,9 @@ dependencies = [ [[package]] name = "enum-iterator" -version = "1.1.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a0ac4aeb3a18f92eaf09c6bb9b3ac30ff61ca95514fc58cbead1c9a6bf5401" +checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" dependencies = [ "enum-iterator-derive", ] @@ -465,13 +465,13 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -570,7 +570,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -630,9 +630,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -672,9 +672,9 @@ dependencies = [ [[package]] name = "git2" -version = "0.14.4" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" +checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" dependencies = [ "bitflags", "libc", @@ -708,9 +708,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.6" +version = "0.17.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e73a9790e243f6d55d8e302426419f6084a1de7a84cd07f7268300408a19de" +checksum = "bc4cf346122086f196260783aa58987190dbd5f43bfab01946d2bf9786e8d9ef" dependencies = [ "anyhow", "heck", @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.20.2" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca6d26ab15835a268939e2367ed4ddb1e7157b03d0bb56ba4a0b036c1ac8393" +checksum = "06b5a8658e575f6469053026ac663a348d5a562c9fce20ab2ca0c349e05d079e" dependencies = [ "bitflags", "cfg-if", @@ -1151,25 +1151,25 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" dependencies = [ "hermit-abi 0.3.1", "libc", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "is-terminal" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256017f749ab3117e93acb91063009e1f1bb56d03965b14c2c8df4eb02c524d8" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" dependencies = [ "hermit-abi 0.3.1", "io-lifetimes", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1274,15 +1274,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" [[package]] name = "libgit2-sys" -version = "0.13.5+1.4.5" +version = "0.14.2+1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e5ea06c26926f1002dd553fded6cfcdc9784c1f60feeb58368b4d9b07b6dba" +checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" dependencies = [ "cc", "libc", @@ -1440,7 +1440,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.13.1", + "base64 0.21.0", "byteorder", "bytes", "dns-sd", @@ -1492,7 +1492,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.13.1", + "base64 0.21.0", "cfg-if", "ctr", "dns-sd", @@ -2137,9 +2137,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.54" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e472a104799c74b514a57226160104aa483546de37e839ec50e3c2e41dd87534" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" dependencies = [ "unicode-ident", ] @@ -2374,16 +2374,16 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.6" +version = "0.37.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d097081ed288dfe45699b72f5b5d648e5f15d64d900c7080273baa20c16a6849" +checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2531,7 +2531,7 @@ checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" dependencies = [ "bitflags", "core-foundation", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", "security-framework-sys", ] @@ -2542,28 +2542,28 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" dependencies = [ - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", ] [[package]] name = "serde" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.160" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -2772,9 +2772,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.12" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" dependencies = [ "proc-macro2", "quote", @@ -2788,7 +2788,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" dependencies = [ "cfg-if", - "core-foundation-sys 0.8.3", + "core-foundation-sys 0.8.4", "libc", "ntapi", "once_cell", @@ -2847,7 +2847,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -2930,7 +2930,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.12", + "syn 2.0.14", ] [[package]] @@ -3141,9 +3141,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" dependencies = [ "getrandom", "rand", @@ -3157,9 +3157,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "7.4.2" +version = "7.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ba753d713ec3844652ad2cb7eb56bc71e34213a14faddac7852a10ba88f61e" +checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749" dependencies = [ "anyhow", "cfg-if", @@ -3353,7 +3353,7 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", ] [[package]] @@ -3362,13 +3362,13 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", ] [[package]] @@ -3377,7 +3377,16 @@ version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" dependencies = [ - "windows-targets", + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", ] [[package]] @@ -3386,13 +3395,28 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", ] [[package]] @@ -3401,42 +3425,84 @@ version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" + [[package]] name = "winnow" version = "0.4.1" diff --git a/core/Cargo.toml b/core/Cargo.toml index db6ee7a7..b0c52236 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,7 +15,7 @@ version = "0.5.0-dev" [dependencies] aes = "0.8" -base64 = "0.13" +base64 = "0.21" byteorder = "1.4" bytes = "1" dns-sd = { version = "0.1", optional = true } diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 42326e6b..a536b558 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -1,6 +1,8 @@ use std::io::{self, Read}; use aes::Aes192; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::engine::Engine as _; use byteorder::{BigEndian, ByteOrder}; use pbkdf2::pbkdf2_hmac; use protobuf::Enum; @@ -108,7 +110,7 @@ impl Credentials { use aes::cipher::generic_array::GenericArray; use aes::cipher::{BlockDecrypt, BlockSizeUser, KeyInit}; - let mut data = base64::decode(encrypted_blob)?; + let mut data = BASE64.decode(encrypted_blob)?; let cipher = Aes192::new(GenericArray::from_slice(&key)); let block_size = Aes192::block_size(); @@ -164,7 +166,7 @@ where T: AsRef<[u8]>, S: serde::Serializer, { - serde::Serialize::serialize(&base64::encode(v.as_ref()), ser) + serde::Serialize::serialize(&BASE64.encode(v.as_ref()), ser) } fn deserialize_base64<'de, D>(de: D) -> Result, D::Error> @@ -172,5 +174,7 @@ where D: serde::Deserializer<'de>, { let v: String = serde::Deserialize::deserialize(de)?; - base64::decode(v).map_err(|e| serde::de::Error::custom(e.to_string())) + BASE64 + .decode(v) + .map_err(|e| serde::de::Error::custom(e.to_string())) } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index c2274fbe..b30ddd99 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [dependencies] aes = "0.8" -base64 = "0.13" +base64 = "0.21" cfg-if = "1.0" ctr = "0.9" dns-sd = { version = "0.1.3", optional = true } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 379988aa..21e8091a 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -9,6 +9,8 @@ use std::{ }; use aes::cipher::{KeyIvInit, StreamCipher}; +use base64::engine::general_purpose::STANDARD as BASE64; +use base64::engine::Engine as _; use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; @@ -16,6 +18,7 @@ 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}; @@ -61,7 +64,7 @@ impl RequestHandler { } fn handle_get_info(&self) -> Response { - let public_key = base64::encode(self.keys.public_key()); + let public_key = BASE64.encode(self.keys.public_key()); let device_type: &str = self.config.device_type.into(); let mut active_user = String::new(); if let Some(username) = &self.username { @@ -125,9 +128,9 @@ impl RequestHandler { .get(clientkey_key) .ok_or(DiscoveryError::ParamsError(clientkey_key))?; - let encrypted_blob = base64::decode(encrypted_blob.as_bytes())?; + let encrypted_blob = BASE64.decode(encrypted_blob.as_bytes())?; - let client_key = base64::decode(client_key.as_bytes())?; + 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(); From ae8387af1dfcd6523bffb9643894f2048e67fc85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Wed, 12 Apr 2023 22:29:25 +0200 Subject: [PATCH 304/561] Update vergen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 110 ++++++-------------------------------------- core/Cargo.toml | 2 +- core/build.rs | 19 +++----- core/src/version.rs | 4 +- 4 files changed, 25 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abb0e54c..bc01dee5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -421,26 +421,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-iterator" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "706d9e7cf1c7664859d79cd524e4e53ea2b67ea03c98cc2870c5e539695d597e" -dependencies = [ - "enum-iterator-derive", -] - -[[package]] -name = "enum-iterator-derive" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355f93763ef7b0ae1c43c4d8eccc9d5848d84ad1a1d8ce61c421d1ac85a19d05" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "env_logger" version = "0.10.0" @@ -561,7 +541,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -630,18 +610,6 @@ 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 1.0.109", -] - [[package]] name = "gimli" version = "0.27.2" @@ -661,24 +629,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "git2" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "url", -] - [[package]] name = "glib" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb53061756195d76969292c2d2e329e01259276524a9bae6c9b73af62854773" +checksum = "6cbc688e6c33f2fd89d83864a9e6caaaef83b5f608922b99ac184e436d15b623" dependencies = [ "bitflags", "futures-channel", @@ -699,9 +654,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4cf346122086f196260783aa58987190dbd5f43bfab01946d2bf9786e8d9ef" +checksum = "726f25f054ce331f0d971a82a6a85eb4955074d6afbe479e42a63ae9f15f6ac4" dependencies = [ "anyhow", "heck", @@ -1269,18 +1224,6 @@ version = "0.2.141" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" -[[package]] -name = "libgit2-sys" -version = "0.14.2+1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - [[package]] name = "libloading" version = "0.7.4" @@ -1562,18 +1505,6 @@ dependencies = [ "protobuf-codegen", ] -[[package]] -name = "libz-sys" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.3.1" @@ -2554,14 +2485,14 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -2773,9 +2704,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -2848,7 +2779,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -2931,7 +2862,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -3150,25 +3081,14 @@ dependencies = [ "rand", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vergen" -version = "7.5.1" +version = "8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21b881cd6636ece9735721cf03c1fe1e774fe258683d084bb2812ab67435749" +checksum = "c1b86a8af1dedf089b1c78338678e4c7492b6045649042d94faf19690499d236" dependencies = [ "anyhow", - "cfg-if", - "enum-iterator", - "getset", - "git2", "rustversion", - "thiserror", "time", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index 07da591d..6e704b9f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -60,7 +60,7 @@ uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] [build-dependencies] rand = "0.8" -vergen = { version = "7", default-features = false, features = ["build", "git"] } +vergen = { version = "8", default-features = false, features = ["build", "git", "gitcl"] } [dev-dependencies] env_logger = "0.10" diff --git a/core/build.rs b/core/build.rs index 0e84d8fc..920455d0 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,18 +1,13 @@ use rand::{distributions::Alphanumeric, Rng}; -use vergen::{vergen, Config, ShaKind, TimestampKind}; +use vergen::EmitBuilder; fn main() { - 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!"); - + EmitBuilder::builder() + .build_date() // outputs 'VERGEN_BUILD_DATE' + .git_sha(true) // outputs 'VERGEN_GIT_SHA', and sets the 'short' flag true + .git_commit_date() // outputs 'VERGEN_GIT_COMMIT_DATE' + .emit() + .expect("Unable to generate the cargo keys!"); let build_id = match std::env::var("SOURCE_DATE_EPOCH") { Ok(val) => val, Err(_) => rand::thread_rng() diff --git a/core/src/version.rs b/core/src/version.rs index f2369c91..28ff1c2d 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,11 +1,11 @@ /// Version string of the form "librespot-" -pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_GIT_SHA_SHORT")); +pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_GIT_SHA")); /// 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_GIT_SHA_SHORT"); +pub const SHA_SHORT: &str = env!("VERGEN_GIT_SHA"); /// Date of the latest git commit. pub const COMMIT_DATE: &str = env!("VERGEN_GIT_COMMIT_DATE"); From d83ed814a3c023b27600170baad94e074392d5cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Thu, 13 Apr 2023 21:20:55 +0200 Subject: [PATCH 305/561] Update hyper-rustls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 68 ++++++++++++++++++++++++++++++++++++------------- core/Cargo.toml | 2 +- 2 files changed, 51 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abb0e54c..902ece63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -561,7 +561,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -676,9 +676,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb53061756195d76969292c2d2e329e01259276524a9bae6c9b73af62854773" +checksum = "6cbc688e6c33f2fd89d83864a9e6caaaef83b5f608922b99ac184e436d15b623" dependencies = [ "bitflags", "futures-channel", @@ -699,9 +699,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4cf346122086f196260783aa58987190dbd5f43bfab01946d2bf9786e8d9ef" +checksum = "726f25f054ce331f0d971a82a6a85eb4955074d6afbe479e42a63ae9f15f6ac4" dependencies = [ "anyhow", "heck", @@ -1079,17 +1079,17 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.2" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" +checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" dependencies = [ "http", "hyper", "log", - "rustls 0.20.8", + "rustls 0.21.0", "rustls-native-certs 0.6.2", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.0", ] [[package]] @@ -1446,7 +1446,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", - "hyper-rustls 0.23.2", + "hyper-rustls 0.24.0", "librespot-protocol", "log", "nonzero_ext", @@ -2402,6 +2402,18 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "rustls" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct 0.7.0", +] + [[package]] name = "rustls-native-certs" version = "0.5.0" @@ -2435,6 +2447,16 @@ dependencies = [ "base64 0.21.0", ] +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.12" @@ -2554,14 +2576,14 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -2773,9 +2795,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -2848,7 +2870,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -2931,7 +2953,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -2956,6 +2978,16 @@ dependencies = [ "webpki 0.22.0", ] +[[package]] +name = "tokio-rustls" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" +dependencies = [ + "rustls 0.21.0", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.12" diff --git a/core/Cargo.toml b/core/Cargo.toml index 07da591d..bed200b8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -29,7 +29,7 @@ httparse = "1.7" http = "0.2" hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.23", features = ["http2"] } +hyper-rustls = { version = "0.24", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" num-bigint = { version = "0.4", features = ["rand"] } From d8547f176cefab178e79f4f2d9ceec5a4148a9aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Wed, 12 Apr 2023 23:42:42 +0200 Subject: [PATCH 306/561] Update quick-xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 34 +++++++++++++++++----------------- core/Cargo.toml | 2 +- core/src/session.rs | 10 ++++------ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abb0e54c..7f6e38c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -561,7 +561,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -676,9 +676,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb53061756195d76969292c2d2e329e01259276524a9bae6c9b73af62854773" +checksum = "6cbc688e6c33f2fd89d83864a9e6caaaef83b5f608922b99ac184e436d15b623" dependencies = [ "bitflags", "futures-channel", @@ -699,9 +699,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.7" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc4cf346122086f196260783aa58987190dbd5f43bfab01946d2bf9786e8d9ef" +checksum = "726f25f054ce331f0d971a82a6a85eb4955074d6afbe479e42a63ae9f15f6ac4" dependencies = [ "anyhow", "heck", @@ -1019,9 +1019,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" dependencies = [ "bytes", "futures-channel", @@ -2188,9 +2188,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.23.1" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", "serde", @@ -2554,14 +2554,14 @@ checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" dependencies = [ "itoa", "ryu", @@ -2773,9 +2773,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.14" +version = "2.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcf316d5356ed6847742d036f8a39c3b8435cac10bd528a4bd461928a6ab34d5" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" dependencies = [ "proc-macro2", "quote", @@ -2848,7 +2848,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] @@ -2931,7 +2931,7 @@ checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" dependencies = [ "proc-macro2", "quote", - "syn 2.0.14", + "syn 2.0.15", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 07da591d..23a4eff7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "1.2" protobuf = "3" -quick-xml = { version = "0.23", features = ["serialize"] } +quick-xml = { version = "0.28", features = ["serialize"] } rand = "0.8" rsa = "0.8.2" serde = { version = "1.0", features = ["derive"] } diff --git a/core/src/session.rs b/core/src/session.rs index 1936467e..de467a55 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -309,19 +309,17 @@ impl Session { let mut user_attributes: UserAttributes = HashMap::new(); loop { - match reader.read_event(&mut buf) { + match reader.read_event_into(&mut buf) { Ok(Event::Start(ref element)) => { - current_element = std::str::from_utf8(element.name())?.to_owned() + current_element = std::str::from_utf8(element)?.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)?, - ); + let _ = user_attributes + .insert(current_element.clone(), value.unescape()?.to_string()); } } Ok(Event::Eof) => break, From 83139f8d074cbb400abe545e89d3301e4ceab302 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Apr 2023 10:57:24 +0000 Subject: [PATCH 307/561] Bump actions/checkout from 3.5.0 to 3.5.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.0 to 3.5.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.5.0...v3.5.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 823cc193..a667128b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v3.5.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -113,7 +113,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v3.5.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -156,7 +156,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v3.5.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -192,7 +192,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v3.5.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -229,7 +229,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3.5.0 + uses: actions/checkout@v3.5.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check From b8eed83d384f4c99c491e09b7f5de24502ff0763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Wed, 12 Apr 2023 19:58:20 +0200 Subject: [PATCH 308/561] Check formatting and linting first on workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/test.yml | 118 ++++++++++++++++++------------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a667128b..4f1cbfda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,17 +39,71 @@ 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 +# The layering here is as follows: +# 1. code formatting +# 2. absence of lints +# 3. absence of errors and warnings on Linux/x86 +# 4. cross compilation on Windows and Linux/ARM jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3.5.2 + - name: Install toolchain + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y + - run: cargo fmt --all -- --check + + clippy: + needs: fmt + 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@v3.5.2 + + - name: Install toolchain + run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y + + - name: Get Rustc version + id: get-rustc-version + run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT + shell: bash + + - name: Cache Rust dependencies + uses: actions/cache@v3.3.1 + 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 install -y libunwind-dev && 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 + test-linux: name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} + needs: clippy strategy: fail-fast: false matrix: @@ -179,57 +233,3 @@ jobs: run: cargo install cross || true - name: Build run: cross build --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@v3.5.2 - - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y - - - name: Get Rustc version - id: get-rustc-version - run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache Rust dependencies - uses: actions/cache@v3.3.1 - 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 install -y libunwind-dev && 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 fmt - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v3.5.2 - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - - run: cargo fmt --all -- --check From 31d18f7e30b1e680bc8c56c0f5144ccb6f9f6aab Mon Sep 17 00:00:00 2001 From: AeRo <48489581+AER00@users.noreply.github.com> Date: Sat, 6 May 2023 09:47:08 +0200 Subject: [PATCH 309/561] Respect disabled normalisation with maximum volume (#1159) --- CHANGELOG.md | 1 + playback/src/player.rs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090ce2e0..12fa129b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ https://github.com/librespot-org/librespot from the beginning - [playback] Handle disappearing and invalid devices better - [playback] Handle seek, pause, and play commands while loading +- [playback] Handle disabled normalisation correctly when using fixed volume - [metadata] Fix missing colon when converting named spotify IDs to URIs ## [0.4.2] - 2022-07-29 diff --git a/playback/src/player.rs b/playback/src/player.rs index 3fdf41b7..34a4c5bf 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1545,9 +1545,11 @@ impl PlayerInternal { // dynamic method, there may still be peaks that we want to shave off. // No matter the case we apply volume attenuation last if there is any. - if !self.config.normalisation && volume < 1.0 { - for sample in data.iter_mut() { - *sample *= volume; + if !self.config.normalisation { + if volume < 1.0 { + for sample in data.iter_mut() { + *sample *= volume; + } } } else if self.config.normalisation_method == NormalisationMethod::Basic && (normalisation_factor < 1.0 || volume < 1.0) From f89c622e3033472967dffb8ff1595de71c645fba Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 13 May 2023 13:00:50 -0500 Subject: [PATCH 310/561] Read Normalisation Data in one go Read all the Normalisation Data in one shot to avoid 4 seperate reads. --- playback/src/player.rs | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 34a4c5bf..0790b40a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -15,7 +15,6 @@ use std::{ time::{Duration, Instant}, }; -use byteorder::{LittleEndian, ReadBytesExt}; use futures_util::{ future, future::FusedFuture, stream::futures_unordered::FuturesUnordered, StreamExt, TryFutureExt, @@ -306,29 +305,35 @@ impl Default for NormalisationData { impl NormalisationData { fn parse_from_ogg(mut file: T) -> io::Result { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; + const NORMALISATION_DATA_SIZE: usize = 16; + 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::()? 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 mut buf = [0u8; NORMALISATION_DATA_SIZE]; - let r = NormalisationData { + file.read_exact(&mut buf)?; + + let track_gain_db = f32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]) as f64; + let track_peak = f32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]) as f64; + let album_gain_db = f32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as f64; + let album_peak = f32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]) as f64; + + Ok(Self { track_gain_db, track_peak, album_gain_db, album_peak, - }; - - Ok(r) + }) } fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f64 { From c964102a349589d644baef5f43a566d6d1e151f1 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Sun, 14 May 2023 21:27:17 +0200 Subject: [PATCH 311/561] Update dependencies sysinfo and RSA (#1164) --- .github/workflows/test.yml | 6 +- CHANGELOG.md | 2 +- Cargo.lock | 228 ++++++++++++++++--------------- core/Cargo.toml | 4 +- core/src/connection/handshake.rs | 4 +- 5 files changed, 126 insertions(+), 118 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a667128b..5bcf49b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -55,7 +55,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.64" # MSRV (Minimum supported rust version) + - "1.65" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -109,7 +109,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.64" # MSRV (Minimum supported rust version) + - "1.65" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -152,7 +152,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.64" # MSRV (Minimum supported rust version) + - "1.65" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index 12fa129b..1ceca30a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ https://github.com/librespot-org/librespot - [all] Improve lock contention and performance (breaking) - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [chore] MSRV is now 1.61 (breaking) +- [core] MSRV is now 1.65 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts diff --git a/Cargo.lock b/Cargo.lock index 4b6b4e52..4d1e56b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.20" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" dependencies = [ "memchr", ] @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arrayvec" @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "atomic_refcell" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "857253367827bd9d0fd973f0ef15506a96e79e41b0ad7aa691203a4e3214f6c8" +checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31" [[package]] name = "autocfg" @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" [[package]] name = "bytemuck" @@ -212,11 +212,12 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.14.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35b255461940a32985c627ce82900867c61db1659764d3675ea81963f72a4c6" +checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" dependencies = [ "smallvec", + "target-lexicon", ] [[package]] @@ -332,9 +333,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -375,9 +376,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "der" -version = "0.6.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +checksum = "05e58dffcdcc8ee7b22f0c1f71a69243d7c2d9ad87b5a14361f2424a1565c219" dependencies = [ "const-oid", "pem-rfc7468", @@ -631,9 +632,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cbc688e6c33f2fd89d83864a9e6caaaef83b5f608922b99ac184e436d15b623" +checksum = "a7f1de7cbde31ea4f0a919453a2dcece5d54d5b70e08f8ad254dc4840f5f09b6" dependencies = [ "bitflags", "futures-channel", @@ -654,9 +655,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.8" +version = "0.17.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726f25f054ce331f0d971a82a6a85eb4955074d6afbe479e42a63ae9f15f6ac4" +checksum = "0a7206c5c03851ef126ea1444990e81fdd6765fb799d5bc694e4897ca01bb97f" dependencies = [ "anyhow", "heck", @@ -712,9 +713,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.20.3" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c46cc10a7ab79329feb68bef54a242ced84c3147cc1b81bc5c6140346a1dbf9" +checksum = "4530401c89be6dc10d77ae1587b811cf455c97dce7abf594cb9164527c7da7fc" dependencies = [ "bitflags", "cfg-if", @@ -797,9 +798,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.20.0" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5598bfedbff12675a6cddbe420b6a3ba5039c64aaf7df130db6339d09b634b0e" +checksum = "0b8ff5dfbf7bcaf1466a385b836bad0d8da25759f121458727fdda1f771c69b3" dependencies = [ "atomic_refcell", "bitflags", @@ -837,9 +838,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.17" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f" +checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" dependencies = [ "bytes", "fnv", @@ -1041,7 +1042,7 @@ dependencies = [ "http", "hyper", "log", - "rustls 0.21.0", + "rustls 0.21.1", "rustls-native-certs 0.6.2", "tokio", "tokio-rustls 0.24.0", @@ -1196,9 +1197,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.61" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" dependencies = [ "wasm-bindgen", ] @@ -1220,9 +1221,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libloading" @@ -1507,9 +1508,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" [[package]] name = "lock_api" @@ -1685,9 +1686,9 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "ntapi" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" dependencies = [ "winapi", ] @@ -1917,9 +1918,9 @@ checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" [[package]] name = "pem-rfc7468" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" dependencies = [ "base64ct", ] @@ -1954,21 +1955,20 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs1" -version = "0.4.1" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ "der", "pkcs8", "spki", - "zeroize", ] [[package]] name = "pkcs8" -version = "0.9.0" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ "der", "spki", @@ -1976,9 +1976,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "portaudio-rs" @@ -2129,9 +2129,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" dependencies = [ "proc-macro2", ] @@ -2202,9 +2202,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", @@ -2213,9 +2213,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "ring" @@ -2254,11 +2254,12 @@ dependencies = [ [[package]] name = "rsa" -version = "0.8.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", + "const-oid", "digest", "num-bigint-dig", "num-integer", @@ -2268,6 +2269,7 @@ dependencies = [ "pkcs8", "rand_core", "signature", + "spki", "subtle", "zeroize", ] @@ -2284,9 +2286,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a36c42d1873f9a77c53bde094f9664d9891bc604a45b4798fd2c389ed12e5b" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" [[package]] name = "rustc-hash" @@ -2296,9 +2298,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.11" +version = "0.37.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" dependencies = [ "bitflags", "errno", @@ -2335,9 +2337,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07180898a28ed6a7f7ba2311594308f595e3dd2e3c3812fa0a80a47b45f17e5d" +checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" dependencies = [ "log", "ring", @@ -2469,9 +2471,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" dependencies = [ "bitflags", "core-foundation", @@ -2482,9 +2484,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys 0.8.4", "libc", @@ -2492,18 +2494,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.163" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", @@ -2614,9 +2616,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "spki" -version = "0.6.0" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" dependencies = [ "base64ct", "der", @@ -2737,9 +2739,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.28.4" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c2f3ca6693feb29a89724516f016488e9aafc7f37264f898593ee4b942f31b" +checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" dependencies = [ "cfg-if", "core-foundation-sys 0.8.4", @@ -2751,9 +2753,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.0.4" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555fc8147af6256f3931a36bb83ad0023240ce9cf2b319dec8236fd1f220b05f" +checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" dependencies = [ "cfg-expr", "heck", @@ -2762,6 +2764,12 @@ dependencies = [ "version-compare", ] +[[package]] +name = "target-lexicon" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" + [[package]] name = "tempfile" version = "3.5.0" @@ -2817,9 +2825,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" dependencies = [ "itoa", "serde", @@ -2829,15 +2837,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" dependencies = [ "time-core", ] @@ -2859,9 +2867,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" dependencies = [ "autocfg", "bytes", @@ -2873,14 +2881,14 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", @@ -2915,15 +2923,15 @@ version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" dependencies = [ - "rustls 0.21.0", + "rustls 0.21.1", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", "pin-project-lite", @@ -2948,9 +2956,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" +checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" dependencies = [ "bytes", "futures-core", @@ -3013,9 +3021,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ "once_cell", ] @@ -3105,9 +3113,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" dependencies = [ "getrandom", "rand", @@ -3115,9 +3123,9 @@ dependencies = [ [[package]] name = "vergen" -version = "8.1.1" +version = "8.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b86a8af1dedf089b1c78338678e4c7492b6045649042d94faf19690499d236" +checksum = "6e03272e388fb78fc79481a493424f78d77be1d55f21bcd314b5a6716e195afe" dependencies = [ "anyhow", "rustversion", @@ -3164,9 +3172,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.84" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3174,24 +3182,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.84" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.15", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.34" +version = "0.4.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454" +checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" dependencies = [ "cfg-if", "js-sys", @@ -3201,9 +3209,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.84" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3211,28 +3219,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.84" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.15", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.84" +version = "0.2.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" +checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.62" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" dependencies = [ "js-sys", "wasm-bindgen", @@ -3458,9 +3466,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.1" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" dependencies = [ "memchr", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index d081d4ef..1f0673cb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -43,12 +43,12 @@ priority-queue = "1.2" protobuf = "3" quick-xml = { version = "0.28", features = ["serialize"] } rand = "0.8" -rsa = "0.8.2" +rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.28", default-features = false } +sysinfo = { version = "0.29", default-features = false } thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index e4fdba4f..9f4f39c0 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -4,7 +4,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac}; use protobuf::{self, Message}; use rand::{thread_rng, RngCore}; -use rsa::{BigUint, Pkcs1v15Sign, PublicKey}; +use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -75,7 +75,7 @@ pub async fn handshake( // 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(|_| { + let public_key = RsaPublicKey::new(n, e).map_err(|_| { io::Error::new( io::ErrorKind::InvalidData, HandshakeError::VerificationFailed, From 4d402e690c67457ca2d462670db39330bbceb4cf Mon Sep 17 00:00:00 2001 From: eladyn <59307989+eladyn@users.noreply.github.com> Date: Thu, 1 Jun 2023 21:39:35 +0200 Subject: [PATCH 312/561] add session timeout handling (#1129) --- CHANGELOG.md | 1 + core/src/mercury/mod.rs | 3 +-- core/src/session.rs | 59 ++++++++++++++++++++++++++++++++++------- core/src/version.rs | 2 +- src/main.rs | 21 ++++++++------- 5 files changed, 64 insertions(+), 22 deletions(-) mode change 100644 => 100755 core/src/session.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ceca30a..0de0ad1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,6 +87,7 @@ https://github.com/librespot-org/librespot - [core] Support downloading of lyrics - [core] Support parsing `SpotifyId` for local files - [core] Support parsing `SpotifyId` for named playlists +- [core] Add checks and handling for stale server connections. - [main] Add all player events to `player_event_handler.rs` - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 03bada3c..7fde9b7f 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -1,7 +1,6 @@ use std::{ collections::HashMap, future::Future, - mem, pin::Pin, task::{Context, Poll}, }; @@ -200,7 +199,7 @@ impl MercuryManager { for i in 0..count { let mut part = Self::parse_part(&mut data); - if let Some(mut partial) = mem::replace(&mut pending.partial, None) { + if let Some(mut partial) = pending.partial.take() { partial.extend_from_slice(&part); part = partial; } diff --git a/core/src/session.rs b/core/src/session.rs old mode 100644 new mode 100755 index de467a55..5f08b134 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -6,7 +6,7 @@ use std::{ process::exit, sync::{Arc, Weak}, task::{Context, Poll}, - time::{SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use byteorder::{BigEndian, ByteOrder}; @@ -18,7 +18,7 @@ use once_cell::sync::OnceCell; use parking_lot::RwLock; use quick_xml::events::Event; use thiserror::Error; -use tokio::sync::mpsc; +use tokio::{sync::mpsc, time::Instant}; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ @@ -80,6 +80,7 @@ struct SessionData { time_delta: i64, invalid: bool, user_data: UserData, + last_ping: Option, } struct SessionInternal { @@ -100,6 +101,15 @@ struct SessionInternal { handle: tokio::runtime::Handle, } +/// A shared reference to a Spotify session. +/// +/// After instantiating, you need to login via [Session::connect]. +/// You can either implement the whole playback logic yourself by using +/// this structs interface directly or hand it to a +/// `Player`. +/// +/// *Note*: [Session] instances cannot yet be reused once invalidated. After +/// an unexpectedly closed connection, you'll need to create a new [Session]. #[derive(Clone)] pub struct Session(Arc); @@ -181,9 +191,10 @@ impl Session { .map(Ok) .forward(sink); let receiver_task = DispatchTask(stream, self.weak()); + let timeout_task = Session::session_timeout(self.weak()); tokio::spawn(async move { - let result = future::try_join(sender_task, receiver_task).await; + let result = future::try_join3(sender_task, receiver_task, timeout_task).await; if let Err(e) = result { error!("{}", e); @@ -231,6 +242,33 @@ impl Session { .get_or_init(|| TokenProvider::new(self.weak())) } + /// Returns an error, when we haven't received a ping for too long (2 minutes), + /// which means that we silently lost connection to Spotify servers. + async fn session_timeout(session: SessionWeak) -> io::Result<()> { + // pings are sent every 2 minutes and a 5 second margin should be fine + const SESSION_TIMEOUT: Duration = Duration::from_secs(125); + + while let Some(session) = session.try_upgrade() { + if session.is_invalid() { + break; + } + let last_ping = session.0.data.read().last_ping.unwrap_or_else(Instant::now); + if last_ping.elapsed() >= SESSION_TIMEOUT { + session.shutdown(); + // TODO: Optionally reconnect (with cached/last credentials?) + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "session lost connection to server", + )); + } + // drop the strong reference before sleeping + drop(session); + // a potential timeout cannot occur at least until SESSION_TIMEOUT after the last_ping + tokio::time::sleep_until(last_ping + SESSION_TIMEOUT).await; + } + Ok(()) + } + pub fn time_delta(&self) -> i64 { self.0.data.read().time_delta } @@ -278,13 +316,16 @@ impl Session { 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, - Err(err) => err.duration(), - } - .as_secs() as i64; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() as i64; - self.0.data.write().time_delta = server_timestamp - timestamp; + { + let mut data = self.0.data.write(); + data.time_delta = server_timestamp.saturating_sub(timestamp); + data.last_ping = Some(Instant::now()); + } self.debug_info(); self.send_packet(Pong, vec![0, 0, 0, 0]) diff --git a/core/src/version.rs b/core/src/version.rs index 28ff1c2d..e5b4b0b0 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -1,4 +1,4 @@ -/// Version string of the form "librespot-" +/// Version string of the form "librespot-\" pub const VERSION_STRING: &str = concat!("librespot-", env!("VERGEN_GIT_SHA")); /// Generate a timestamp string representing the build date (UTC). diff --git a/src/main.rs b/src/main.rs index e4727ba8..48edf1c9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1662,7 +1662,7 @@ async fn main() { let mut connecting = false; let mut _event_handler: Option = None; - let session = Session::new(setup.session_config.clone(), setup.cache.clone()); + let mut session = Session::new(setup.session_config.clone(), setup.cache.clone()); if setup.enable_discovery { let device_id = setup.session_config.device_id.clone(); @@ -1721,6 +1721,10 @@ async fn main() { } }, _ = async {}, if connecting && last_credentials.is_some() => { + if session.is_invalid() { + session = Session::new(setup.session_config.clone(), setup.cache.clone()); + } + let mixer_config = setup.mixer_config.clone(); let mixer = (setup.mixer)(mixer_config); let player_config = setup.player_config.clone(); @@ -1770,15 +1774,12 @@ async fn main() { auto_connect_times.len() > RECONNECT_RATE_LIMIT }; - match last_credentials.clone() { - Some(_) if !reconnect_exceeds_rate_limit() => { - auto_connect_times.push(Instant::now()); - connecting = true; - }, - _ => { - error!("Spirc shut down too often. Not reconnecting automatically."); - exit(1); - }, + if last_credentials.is_some() && !reconnect_exceeds_rate_limit() { + auto_connect_times.push(Instant::now()); + connecting = true; + } else { + error!("Spirc shut down too often. Not reconnecting automatically."); + exit(1); } }, _ = tokio::signal::ctrl_c() => { From 1ac9818a00438efc5d692c873cb6905af2272d37 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 10 Jun 2023 11:02:28 +0000 Subject: [PATCH 313/561] Bump actions/checkout from 3.5.2 to 3.5.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.2 to 3.5.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.5.2...v3.5.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 92f79afe..710dda29 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.2 + uses: actions/checkout@v3.5.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From 9c30532fd4d41e256bac89b64b92325139498ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 25 Jun 2023 23:32:09 +0200 Subject: [PATCH 314/561] Update quick-xml to 0.29 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 447 ++++++++++++++++++++++++------------------------ core/Cargo.toml | 2 +- 2 files changed, 220 insertions(+), 229 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d1e56b1..51682c4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433cfd6710c9986c576a25ca913c39d66a6474107b406f34f91d4a8923395241" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" dependencies = [ "cfg-if", "cipher", @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] @@ -67,9 +67,9 @@ checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" [[package]] name = "arrayvec" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" @@ -79,7 +79,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -117,9 +117,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" [[package]] name = "base64ct" @@ -164,9 +164,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.2" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c6ed94e98ecff0c12dd1b04c15ec0d7d9458ca8fe806cea6f12954efe74c63b" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" @@ -212,9 +212,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.1" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8790cf1286da485c72cf5fc7aeba308438800036ec67d89425924c4807268c9" +checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c" dependencies = [ "smallvec", "target-lexicon", @@ -333,9 +333,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -375,10 +375,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] -name = "der" -version = "0.7.5" +name = "data-encoding" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05e58dffcdcc8ee7b22f0c1f71a69243d7c2d9ad87b5a14361f2424a1565c219" +checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" + +[[package]] +name = "der" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" dependencies = [ "const-oid", "pem-rfc7468", @@ -387,9 +393,9 @@ dependencies = [ [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "const-oid", @@ -435,6 +441,12 @@ dependencies = [ "termcolor", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -479,9 +491,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -542,7 +554,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -602,9 +614,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", @@ -613,15 +625,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" [[package]] name = "gio-sys" -version = "0.17.4" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1d43b0d7968b48455244ecafe41192871257f5740aa6b095eb19db78e362a5" +checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" dependencies = [ "glib-sys", "gobject-sys", @@ -632,9 +644,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.9" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1de7cbde31ea4f0a919453a2dcece5d54d5b70e08f8ad254dc4840f5f09b6" +checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" dependencies = [ "bitflags", "futures-channel", @@ -655,9 +667,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.9" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7206c5c03851ef126ea1444990e81fdd6765fb799d5bc694e4897ca01bb97f" +checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ "anyhow", "heck", @@ -670,9 +682,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.17.4" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f00ad0a1bf548e61adfff15d83430941d9e1bb620e334f779edd1c745680a5" +checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" dependencies = [ "libc", "system-deps", @@ -686,9 +698,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.17.4" +version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e75b0000a64632b2d8ca3cf856af9308e3a970844f6e9659bd197f026793d0" +checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" dependencies = [ "glib-sys", "libc", @@ -713,9 +725,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.20.5" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4530401c89be6dc10d77ae1587b811cf455c97dce7abf594cb9164527c7da7fc" +checksum = "3113531138b4e41968e33fd003a0d1a635ef6e0cbc309dd5004123000863ac54" dependencies = [ "bitflags", "cfg-if", @@ -848,7 +860,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -861,6 +873,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "headers" version = "0.3.8" @@ -1042,17 +1060,17 @@ dependencies = [ "http", "hyper", "log", - "rustls 0.21.1", - "rustls-native-certs 0.6.2", + "rustls 0.21.2", + "rustls-native-certs 0.6.3", "tokio", - "tokio-rustls 0.24.0", + "tokio-rustls 0.24.1", ] [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1075,7 +1093,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", ] [[package]] @@ -1098,9 +1126,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1197,9 +1225,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c16e1bfd491478ab155fd8b4896b86f9ede344949b641e61501e07c2b8b4d5" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] @@ -1221,9 +1249,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.144" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libloading" @@ -1237,9 +1265,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "libmdns" @@ -1375,7 +1403,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.0", + "base64 0.21.2", "byteorder", "bytes", "dns-sd", @@ -1427,7 +1455,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.0", + "base64 0.21.2", "cfg-if", "ctr", "dns-sd", @@ -1508,15 +1536,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1524,12 +1552,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" [[package]] name = "mach2" @@ -1584,14 +1609,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1808,9 +1832,9 @@ dependencies = [ [[package]] name = "object" -version = "0.30.3" +version = "0.30.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439" +checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" dependencies = [ "memchr", ] @@ -1849,9 +1873,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl-probe" @@ -1880,18 +1904,18 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", "thread-id", - "windows-sys 0.45.0", + "windows-targets 0.48.0", ] [[package]] @@ -1927,9 +1951,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" @@ -1938,7 +1962,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -2015,12 +2039,12 @@ checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" [[package]] name = "priority-queue" -version = "1.3.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca9c6be70d989d21a136eb86c2d83e4b328447fac4a88dace2143c179c86267" +checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61" dependencies = [ "autocfg", - "indexmap", + "indexmap 1.9.3", ] [[package]] @@ -2059,9 +2083,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] @@ -2099,7 +2123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" dependencies = [ "anyhow", - "indexmap", + "indexmap 1.9.3", "log", "protobuf", "protobuf-support", @@ -2119,9 +2143,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" dependencies = [ "memchr", "serde", @@ -2129,9 +2153,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -2202,9 +2226,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", @@ -2213,9 +2237,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "ring" @@ -2298,9 +2322,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ "bitflags", "errno", @@ -2325,21 +2349,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.20.8" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" -dependencies = [ - "log", - "ring", - "sct 0.7.0", - "webpki 0.22.0", -] - -[[package]] -name = "rustls" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c911ba11bc8433e811ce56fde130ccf32f5127cab0e0194e9c68c5a5b671791e" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" dependencies = [ "log", "ring", @@ -2361,9 +2373,9 @@ dependencies = [ [[package]] name = "rustls-native-certs" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", "rustls-pemfile", @@ -2377,7 +2389,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", ] [[package]] @@ -2471,9 +2483,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2855b3715770894e67cbfa3df957790aa0c9edc3bf06efa1a84d77fa0839d1" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ "bitflags", "core-foundation", @@ -2494,29 +2506,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -2525,9 +2537,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" dependencies = [ "serde", ] @@ -2626,15 +2638,15 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "symphonia" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3671dd6f64f4f9d5c87179525054cfc1f60de23ba1f193bd6ceab812737403f1" +checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" dependencies = [ "lazy_static", "symphonia-bundle-mp3", @@ -2646,9 +2658,9 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55a0846e7a2c9a8081ff799fc83a975170417ad2a143f644a77ec2e3e82a2b73" +checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" dependencies = [ "bitflags", "lazy_static", @@ -2659,9 +2671,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfed6f7b6bfa21d7cef1acefc8eae5db80df1608a1aca91871b07cbd28d7b74" +checksum = "3953397e3506aa01350c4205817e4f95b58d476877a42f0458d07b665749e203" dependencies = [ "log", "symphonia-core", @@ -2670,9 +2682,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9567e2d8a5f866b2f94f5d366d811e0c6826babcff6d37de9e1a6690d38869" +checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" dependencies = [ "arrayvec", "bitflags", @@ -2683,9 +2695,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474df6e86b871dcb56913130bada1440245f483057c4a2d8a2981455494c4439" +checksum = "9bf1a00ccd11452d44048a0368828040f778ae650418dbd9d8765b7ee2574c8d" dependencies = [ "log", "symphonia-core", @@ -2695,9 +2707,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acd35c263223ef6161000be79b124a75de3e065eea563bf3ef169b3e94c7bb2e" +checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" dependencies = [ "encoding_rs", "lazy_static", @@ -2707,9 +2719,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce340a6c33ac06cb42de01220308ec056e8a2a3d5cc664aaf34567392557136b" +checksum = "a450ca645b80d69aff8b35576cbfdc7f20940b29998202aab910045714c951f8" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -2728,9 +2740,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" dependencies = [ "proc-macro2", "quote", @@ -2739,9 +2751,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.0" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1" +checksum = "9557d0845b86eea8182f7b10dff120214fb6cd9fd937b6f4917714e546a38695" dependencies = [ "cfg-if", "core-foundation-sys 0.8.4", @@ -2753,9 +2765,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.1.0" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5fa6fb9ee296c0dc2df41a656ca7948546d061958115ddb0bcaae43ad0d17d2" +checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" dependencies = [ "cfg-expr", "heck", @@ -2766,21 +2778,22 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -2809,14 +2822,14 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] name = "thread-id" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" +checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73" dependencies = [ "libc", "redox_syscall 0.2.16", @@ -2825,9 +2838,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.21" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3403384eaacbca9923fa06940178ac13e4edb725486d70e8e15881d0c836cc" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" dependencies = [ "itoa", "serde", @@ -2867,9 +2880,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.1" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", @@ -2892,7 +2905,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", ] [[package]] @@ -2908,22 +2921,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.20.8", - "tokio", - "webpki 0.22.0", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d409377ff5b1e3ca6437aa86c1eb7d40c134bfec254e44c830defa92669db5" -dependencies = [ - "rustls 0.21.1", + "rustls 0.21.2", "tokio", ] @@ -2940,18 +2942,17 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd" +checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" dependencies = [ "futures-util", "log", - "rustls 0.20.8", - "rustls-native-certs 0.6.2", + "rustls 0.21.2", + "rustls-native-certs 0.6.3", "tokio", - "tokio-rustls 0.23.4", + "tokio-rustls 0.24.1", "tungstenite", - "webpki 0.22.0", ] [[package]] @@ -2970,9 +2971,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" dependencies = [ "serde", "serde_spanned", @@ -2982,20 +2983,20 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "serde", "serde_spanned", "toml_datetime", @@ -3036,18 +3037,18 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788" +checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" dependencies = [ - "base64 0.13.1", "byteorder", "bytes", + "data-encoding", "http", "httparse", "log", "rand", - "rustls 0.20.8", + "rustls 0.21.2", "sha1", "thiserror", "url", @@ -3069,9 +3070,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -3096,9 +3097,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -3113,9 +3114,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.2" +version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dad5567ad0cf5b760e5665964bec1b47dfd077ba8a2544b513f3556d3d239a2" +checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" dependencies = [ "getrandom", "rand", @@ -3123,9 +3124,9 @@ dependencies = [ [[package]] name = "vergen" -version = "8.1.3" +version = "8.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e03272e388fb78fc79481a493424f78d77be1d55f21bcd314b5a6716e195afe" +checksum = "8b3c89c2c7e50f33e4d35527e5bf9c11d6d132226dbbd1753f0fbe9f19ef88c6" dependencies = [ "anyhow", "rustversion", @@ -3156,11 +3157,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -3172,9 +3172,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b6cb788c4e39112fbe1822277ef6fb3c55cd86b95cb3d3c4c1c9597e4ac74b4" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3182,24 +3182,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e522ed4105a9d626d885b35d62501b30d9666283a5c8be12c14a8bdafe7822" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.35" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "083abe15c5d88556b77bdf7aef403625be9e327ad37c62c4e4129af740168163" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if", "js-sys", @@ -3209,9 +3209,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358a79a0cb89d21db8120cbfb91392335913e4890665b1a7981d9e956903b434" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3219,28 +3219,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4783ce29f09b9d93134d41297aded3a712b7b979e9c6f28c32cb88c973a94869" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.22", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.85" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a901d592cafaa4d711bc324edfaff879ac700b19c3dfd60058d2b445be2691eb" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "web-sys" -version = "0.3.62" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b5f940c7edfdc6d12126d98c9ef4d1b3d470011c47c76a6581df47ad9ba721" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", @@ -3332,15 +3332,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -3466,9 +3457,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index 1f0673cb..ff69eaae 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "1.2" protobuf = "3" -quick-xml = { version = "0.28", features = ["serialize"] } +quick-xml = { version = "0.29", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } From 4d6de15a97b7d13b595c2a0f88634ecef8ae888c Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Fri, 30 Jun 2023 02:30:14 -0500 Subject: [PATCH 315/561] Discovery retry (#1178) When started at boot as a service discovery may fail due to it trying to bind to interfaces before the network is actually up. This could be prevented in systemd by starting the service after network-online.target but it requires that a wait-online.service is also enabled which is not always the case since a wait-online.service can potentially hang the boot process until it times out in certain situations. This allows for discovery to retry every 10 secs in the 1st 60 secs of uptime before giving up thus papering over the issue and not holding up the boot process. --- CHANGELOG.md | 1 + Cargo.lock | 1 + Cargo.toml | 1 + src/main.rs | 56 ++++++++++++++++++++++++++++++++++++++-------------- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de0ad1f..5b057082 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ https://github.com/librespot-org/librespot - [playback] Add metadata support via a `TrackChanged` event - [connect] Add `activate` and `load` functions to `Spirc`, allowing control over local connect sessions - [metadata] Add `Lyrics` +- [discovery] Add discovery initialisation retries if within the 1st min of uptime ### Fixed diff --git a/Cargo.lock b/Cargo.lock index 51682c4a..1f301939 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,6 +1355,7 @@ dependencies = [ "log", "rpassword", "sha1", + "sysinfo", "thiserror", "tokio", "url", diff --git a/Cargo.toml b/Cargo.toml index 98b8405b..1f380a1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ hex = "0.4" log = "0.4" rpassword = "7.0" sha1 = "0.10" +sysinfo = { version = "0.29", default-features = false } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" diff --git a/src/main.rs b/src/main.rs index 48edf1c9..e8c2e66f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +use futures_util::StreamExt; +use log::{debug, error, info, trace, warn}; +use sha1::{Digest, Sha1}; use std::{ env, fs::create_dir_all, @@ -8,10 +11,7 @@ use std::{ str::FromStr, time::{Duration, Instant}, }; - -use futures_util::StreamExt; -use log::{error, info, trace, warn}; -use sha1::{Digest, Sha1}; +use sysinfo::{System, SystemExt}; use thiserror::Error; use url::Url; @@ -1646,6 +1646,7 @@ fn get_setup() -> Setup { async fn main() { const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; const RECONNECT_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(600); + const DISCOVERY_RETRY_TIMEOUT: Duration = Duration::from_secs(10); const RECONNECT_RATE_LIMIT: usize = 5; if env::var(RUST_BACKTRACE).is_err() { @@ -1664,18 +1665,43 @@ async fn main() { let mut session = Session::new(setup.session_config.clone(), setup.cache.clone()); + let mut sys = System::new(); + if setup.enable_discovery { - let device_id = setup.session_config.device_id.clone(); - let client_id = setup.session_config.client_id.clone(); - match librespot::discovery::Discovery::builder(device_id, client_id) - .name(setup.connect_config.name.clone()) - .device_type(setup.connect_config.device_type) - .port(setup.zeroconf_port) - .zeroconf_ip(setup.zeroconf_ip) - .launch() - { - Ok(d) => discovery = Some(d), - Err(err) => warn!("Could not initialise discovery: {}.", err), + // When started at boot as a service discovery may fail due to it + // trying to bind to interfaces before the network is actually up. + // This could be prevented in systemd by starting the service after + // network-online.target but it requires that a wait-online.service is + // also enabled which is not always the case since a wait-online.service + // can potentially hang the boot process until it times out in certain situations. + // This allows for discovery to retry every 10 secs in the 1st min of uptime + // before giving up thus papering over the issue and not holding up the boot process. + + discovery = loop { + let device_id = setup.session_config.device_id.clone(); + let client_id = setup.session_config.client_id.clone(); + + match librespot::discovery::Discovery::builder(device_id, client_id) + .name(setup.connect_config.name.clone()) + .device_type(setup.connect_config.device_type) + .port(setup.zeroconf_port) + .zeroconf_ip(setup.zeroconf_ip.clone()) + .launch() + { + Ok(d) => break Some(d), + Err(e) => { + sys.refresh_processes(); + + if sys.uptime() <= 1 { + debug!("Retrying to initialise discovery: {e}"); + tokio::time::sleep(DISCOVERY_RETRY_TIMEOUT).await; + } else { + debug!("System uptime > 1 min, not retrying to initialise discovery"); + warn!("Could not initialise discovery: {e}"); + break None; + } + } + } }; } From c491f90e09d6468829f004605fe14121a01c6674 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Tue, 4 Jul 2023 09:37:22 +0100 Subject: [PATCH 316/561] Parse expiry timestamp from spotifycdn.com CDN URLs (Fixes #1182) (#1183) The CDN URLs list now includes spotifycdn.com which has a different format. It was being erroneously interpreted using the scdn.co format and trying to parse non-digit characters as a timestamp. Also ignore expiry timestamps we can't parse for future new URLs. --- core/src/cdn_url.rs | 88 +++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 22 deletions(-) diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 39e596a6..417a3c73 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -5,6 +5,7 @@ use std::{ use protobuf::Message; use thiserror::Error; +use time::Duration; use url::Url; use super::{date::Date, Error, FileId, Session}; @@ -16,6 +17,8 @@ use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; #[derive(Debug, Clone)] pub struct MaybeExpiringUrl(pub String, pub Option); +const CDN_URL_EXPIRY_MARGIN: Duration = Duration::seconds(5 * 60); + #[derive(Debug, Clone)] pub struct MaybeExpiringUrls(pub Vec); @@ -114,55 +117,96 @@ impl TryFrom for MaybeExpiringUrls { .iter() .map(|cdn_url| { let url = Url::parse(cdn_url)?; + let mut expiry: Option = None; if is_expiring { - let expiry_str = if let Some(token) = url + let mut expiry_str: Option = None; + if let Some(token) = url .query_pairs() .into_iter() .find(|(key, _value)| key == "__token__") { + //"https://audio-ak-spotify-com.akamaized.net/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?__token__=exp=1688165560~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41", 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]) + expiry_str = Some(String::from(&slice[..end])); } else { - String::from(slice) + expiry_str = Some(String::from(slice)); } - } else { - String::new() } - } else { - String::new() + } + } else if let Some(token) = url + .query_pairs() + .into_iter() + .find(|(key, _value)| key == "Expires") + { + //"https://audio-gm-off.spotifycdn.com/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?Expires=1688165560~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM=", + if let Some(end) = token.1.find('~') { + // this is the only valid invariant for spotifycdn.com + let slice = &token.1[..end]; + expiry_str = Some(String::from(&slice[..end])); } } else if let Some(query) = url.query() { + //"https://audio4-fa.scdn.co/audio/4712bc9e47f7feb4ee3450ef2bb545e1d83c3d54?1688165560_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4=", let mut items = query.split('_'); if let Some(first) = items.next() { // this is the only valid invariant for scdn.co - String::from(first) + expiry_str = Some(String::from(first)); + } + } + + if let Some(exp_str) = expiry_str { + if let Ok(expiry_parsed) = exp_str.parse::() { + if let Ok(expiry_at) = Date::from_timestamp_ms(expiry_parsed * 1_000) { + let with_margin = expiry_at.saturating_sub(CDN_URL_EXPIRY_MARGIN); + expiry = Some(Date::from(with_margin)); + } } else { - String::new() + warn!("Cannot parse CDN URL expiry timestamp '{exp_str}' from '{cdn_url}'"); } } else { - String::new() - }; - - let mut expiry: i64 = expiry_str.parse()?; - - expiry -= 5 * 60; // seconds - - Ok(MaybeExpiringUrl( - cdn_url.to_owned(), - Some(Date::from_timestamp_ms(expiry * 1000)?), - )) - } else { - Ok(MaybeExpiringUrl(cdn_url.to_owned(), None)) + warn!("Unknown CDN URL format: {cdn_url}"); + } } + Ok(MaybeExpiringUrl(cdn_url.to_owned(), expiry)) }) .collect::, Error>>()?; Ok(Self(result)) } } + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_maybe_expiring_urls() { + let timestamp = 1688165560; + let mut msg = CdnUrlMessage::new(); + msg.result = StorageResolveResponse_Result::CDN.into(); + msg.cdnurl = vec![ + format!("https://audio-ak-spotify-com.akamaized.net/audio/foo?__token__=exp={timestamp}~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41"), + format!("https://audio-gm-off.spotifycdn.com/audio/foo?Expires={timestamp}~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM="), + format!("https://audio4-fa.scdn.co/audio/foo?{timestamp}_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4="), + "https://audio4-fa.scdn.co/foo?baz".to_string(), + ]; + msg.fileid = vec![0]; + + let urls = MaybeExpiringUrls::try_from(msg).expect("valid urls"); + assert_eq!(urls.len(), 4); + assert!(urls[0].1.is_some()); + assert!(urls[1].1.is_some()); + assert!(urls[2].1.is_some()); + assert!(urls[3].1.is_none()); + let timestamp_margin = Duration::seconds(timestamp) - CDN_URL_EXPIRY_MARGIN; + assert_eq!( + urls[0].1.unwrap().as_timestamp_ms() as i128, + timestamp_margin.whole_milliseconds() + ); + } +} From ebf600d96e8ff44b5ce6468458f36e0b66850fdf Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Fri, 14 Jul 2023 00:00:03 +0900 Subject: [PATCH 317/561] do not overwrite unchanged cached Credentials (#1168) --- core/src/authentication.rs | 2 +- core/src/session.rs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/src/authentication.rs b/core/src/authentication.rs index a536b558..8122d659 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -27,7 +27,7 @@ impl From for Error { } /// The credentials are used to log into the Spotify API. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub struct Credentials { pub username: String, diff --git a/core/src/session.rs b/core/src/session.rs index 5f08b134..69125e17 100755 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -176,7 +176,13 @@ impl Session { self.set_username(&reusable_credentials.username); if let Some(cache) = self.cache() { if store_credentials { - cache.save_credentials(&reusable_credentials); + let cred_changed = cache + .credentials() + .map(|c| c != reusable_credentials) + .unwrap_or(true); + if cred_changed { + cache.save_credentials(&reusable_credentials); + } } } From d6257c41ca2f53acf17d82f6d0cfcc5e24907b66 Mon Sep 17 00:00:00 2001 From: Wang Guan Date: Fri, 14 Jul 2023 00:21:37 +0900 Subject: [PATCH 318/561] make clippy happy: clippy::default-constructed-unit-structs --- playback/src/audio_backend/jackaudio.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 9d40ee82..5c7a28fb 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -47,8 +47,8 @@ impl Open for JackSink { let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap(); - let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); - let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); + let ch_r = client.register_port("out_0", AudioOut).unwrap(); + let ch_l = client.register_port("out_1", AudioOut).unwrap(); // buffer for samples from librespot (~10ms) let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { From 46195f18d6d2d55f7b5c296422e48d80ed259609 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 17:30:56 +0300 Subject: [PATCH 319/561] Handle play_request_id as an event --- connect/src/spirc.rs | 7 ++++++- playback/src/player.rs | 27 +++++++++++++++------------ src/player_event_handler.rs | 4 ++++ 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 857a4409..7e40d53b 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -663,6 +663,11 @@ impl SpircTask { } fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { + // update play_request_id + if let PlayerEvent::PlayRequestIdChanged { play_request_id } = event { + self.play_request_id = Some(play_request_id); + return Ok(()); + } // 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 @@ -1462,7 +1467,7 @@ impl SpircTask { Some((track, index)) => { self.state.set_playing_track_index(index); - self.play_request_id = Some(self.player.load(track, start_playing, position_ms)); + self.player.load(track, start_playing, position_ms); self.update_state_position(position_ms); if start_playing { diff --git a/playback/src/player.rs b/playback/src/player.rs index 0790b40a..6e93258c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -55,7 +55,6 @@ pub type PlayerResult = Result<(), Error>; pub struct Player { commands: Option>, thread_handle: Option>, - play_request_id_generator: SeqGenerator, } #[derive(PartialEq, Eq, Debug, Clone, Copy)] @@ -88,6 +87,7 @@ struct PlayerInternal { auto_normalise_as_album: bool, player_id: usize, + play_request_id_generator: SeqGenerator, } static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -95,7 +95,6 @@ static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); enum PlayerCommand { Load { track_id: SpotifyId, - play_request_id: u64, play: bool, position_ms: u32, }, @@ -132,6 +131,10 @@ enum PlayerCommand { #[derive(Debug, Clone)] pub enum PlayerEvent { + // Play request id changed + PlayRequestIdChanged { + play_request_id: u64, + }, // Fired when the player is stopped (e.g. by issuing a "stop" command to the player). Stopped { play_request_id: u64, @@ -475,6 +478,7 @@ impl Player { auto_normalise_as_album: false, player_id, + play_request_id_generator: SeqGenerator::new(0), }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -488,7 +492,6 @@ impl Player { Self { commands: Some(cmd_tx), thread_handle: Some(handle), - play_request_id_generator: SeqGenerator::new(0), } } @@ -500,16 +503,12 @@ impl Player { } } - pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 { - let play_request_id = self.play_request_id_generator.get(); + pub fn load(&self, track_id: SpotifyId, start_playing: bool, position_ms: u32) { self.command(PlayerCommand::Load { track_id, - play_request_id, play: start_playing, position_ms, }); - - play_request_id } pub fn preload(&self, track_id: SpotifyId) { @@ -1754,10 +1753,15 @@ impl PlayerInternal { fn handle_command_load( &mut self, track_id: SpotifyId, - play_request_id: u64, + play_request_id_option: Option, play: bool, position_ms: u32, ) -> PlayerResult { + let play_request_id = + play_request_id_option.unwrap_or(self.play_request_id_generator.get()); + + self.send_event(PlayerEvent::PlayRequestIdChanged { play_request_id }); + if !self.config.gapless { self.ensure_sink_stopped(play); } @@ -2010,7 +2014,7 @@ impl PlayerInternal { { return self.handle_command_load( track_id, - play_request_id, + Some(play_request_id), start_playback, position_ms, ); @@ -2067,10 +2071,9 @@ impl PlayerInternal { match cmd { PlayerCommand::Load { track_id, - play_request_id, play, position_ms, - } => self.handle_command_load(track_id, play_request_id, play, position_ms)?, + } => self.handle_command_load(track_id, None, play, position_ms)?, PlayerCommand::Preload { track_id } => self.handle_command_preload(track_id), diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index d9d6de21..3d0a47df 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -21,6 +21,10 @@ impl EventHandler { let mut env_vars = HashMap::new(); match event { + PlayerEvent::PlayRequestIdChanged { play_request_id } => { + env_vars.insert("PLAYER_EVENT", "play_request_id_changed".to_string()); + env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); + } PlayerEvent::TrackChanged { audio_item } => { match audio_item.track_id.to_base62() { Err(e) => { From ddadcc9ea050d33115e2fee5039481c63b0706cc Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 17:38:41 +0300 Subject: [PATCH 320/561] Add a health check method to player --- playback/src/player.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/playback/src/player.rs b/playback/src/player.rs index 6e93258c..b15a67c1 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -495,6 +495,13 @@ impl Player { } } + pub fn is_invalid(&self) -> bool { + if let Some(handle) = self.thread_handle.as_ref() { + return handle.is_finished(); + } + true + } + fn command(&self, cmd: PlayerCommand) { if let Some(commands) = self.commands.as_ref() { if let Err(e) = commands.send(cmd) { From c6b62b82d411b54b79d4cc69c66da9e6a9abbc7b Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 17:52:17 +0300 Subject: [PATCH 321/561] Allow player session changes --- playback/src/player.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/playback/src/player.rs b/playback/src/player.rs index b15a67c1..fcdb12b2 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -105,6 +105,7 @@ enum PlayerCommand { Pause, Stop, Seek(u32), + SetSession(Session), AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), EmitVolumeChangedEvent(u16), @@ -538,6 +539,10 @@ impl Player { self.command(PlayerCommand::Seek(position_ms)); } + pub fn set_session(&self, session: Session) { + self.command(PlayerCommand::SetSession(session)); + } + pub fn get_player_event_channel(&self) -> PlayerEventChannel { let (event_sender, event_receiver) = mpsc::unbounded_channel(); self.command(PlayerCommand::AddEventSender(event_sender)); @@ -2092,6 +2097,8 @@ impl PlayerInternal { PlayerCommand::Stop => self.handle_player_stop(), + PlayerCommand::SetSession(session) => self.session = session, + PlayerCommand::AddEventSender(sender) => self.event_senders.push(sender), PlayerCommand::SetSinkEventCallback(callback) => self.sink_event_callback = callback, @@ -2282,6 +2289,7 @@ impl fmt::Debug for PlayerCommand { PlayerCommand::Pause => f.debug_tuple("Pause").finish(), PlayerCommand::Stop => f.debug_tuple("Stop").finish(), PlayerCommand::Seek(position) => f.debug_tuple("Seek").field(&position).finish(), + PlayerCommand::SetSession(_) => f.debug_tuple("SetSession").finish(), PlayerCommand::AddEventSender(_) => f.debug_tuple("AddEventSender").finish(), PlayerCommand::SetSinkEventCallback(_) => { f.debug_tuple("SetSinkEventCallback").finish() From e3db0994bce1379521b4b3aeef601b4b649e7315 Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 17:56:40 +0300 Subject: [PATCH 322/561] Use single player and mixer instances --- connect/src/spirc.rs | 9 ++++--- examples/play.rs | 2 +- examples/play_connect.rs | 3 ++- playback/src/mixer/mod.rs | 10 +++++--- playback/src/player.rs | 10 +++----- src/main.rs | 53 +++++++++++++++++++++++---------------- 6 files changed, 48 insertions(+), 39 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 7e40d53b..be0a8b80 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -3,6 +3,7 @@ use std::{ future::Future, pin::Pin, sync::atomic::{AtomicUsize, Ordering}, + sync::Arc, time::{SystemTime, UNIX_EPOCH}, }; @@ -77,8 +78,8 @@ enum SpircPlayStatus { type BoxedStream = Pin + Send>>; struct SpircTask { - player: Player, - mixer: Box, + player: Arc, + mixer: Arc, sequence: SeqGenerator, @@ -272,8 +273,8 @@ impl Spirc { config: ConnectConfig, session: Session, credentials: Credentials, - player: Player, - mixer: Box, + player: Arc, + mixer: Arc, ) -> Result<(Spirc, impl Future), Error> { let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel); debug!("new Spirc[{}]", spirc_id); diff --git a/examples/play.rs b/examples/play.rs index eb7dc382..9e4e29af 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -40,7 +40,7 @@ async fn main() { exit(1); } - let mut player = Player::new(player_config, session, Box::new(NoOpVolume), move || { + let player = Player::new(player_config, session, Box::new(NoOpVolume), move || { backend(None, audio_format) }); diff --git a/examples/play_connect.rs b/examples/play_connect.rs index 2b23a7d3..a61d3d67 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -17,6 +17,7 @@ use librespot_metadata::{Album, Metadata}; use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig}; use librespot_protocol::spirc::TrackRef; use std::env; +use std::sync::Arc; use tokio::join; #[tokio::main] @@ -54,7 +55,7 @@ async fn main() { session.clone(), credentials, player, - Box::new(SoftMixer::open(MixerConfig::default())), + Arc::new(SoftMixer::open(MixerConfig::default())), ) .await .unwrap(); diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 0a8b8d6c..83d00853 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use crate::config::VolumeCtrl; pub mod mappings; @@ -5,7 +7,7 @@ use self::mappings::MappedCtrl; pub struct NoOpVolume; -pub trait Mixer: Send { +pub trait Mixer: Send + Sync { fn open(config: MixerConfig) -> Self where Self: Sized; @@ -55,10 +57,10 @@ impl Default for MixerConfig { } } -pub type MixerFn = fn(MixerConfig) -> Box; +pub type MixerFn = fn(MixerConfig) -> Arc; -fn mk_sink(config: MixerConfig) -> Box { - Box::new(M::open(config)) +fn mk_sink(config: MixerConfig) -> Arc { + Arc::new(M::open(config)) } pub const MIXERS: &[(&str, MixerFn)] = &[ diff --git a/playback/src/player.rs b/playback/src/player.rs index fcdb12b2..16275053 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -420,7 +420,7 @@ impl Player { session: Session, volume_getter: Box, sink_builder: F, - ) -> Self + ) -> Arc where F: FnOnce() -> Box + Send + 'static, { @@ -490,10 +490,10 @@ impl Player { debug!("PlayerInternal thread finished."); }); - Self { + Arc::new(Self { commands: Some(cmd_tx), thread_handle: Some(handle), - } + }) } pub fn is_invalid(&self) -> bool { @@ -1390,10 +1390,6 @@ impl Future for PlayerInternal { } } - if self.session.is_invalid() { - return Poll::Ready(()); - } - if (!self.state.is_playing()) && all_futures_completed_or_not_ready { return Poll::Pending; } diff --git a/src/main.rs b/src/main.rs index e8c2e66f..eab2f697 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1715,6 +1715,31 @@ async fn main() { exit(1); } + let mixer_config = setup.mixer_config.clone(); + let mixer = (setup.mixer)(mixer_config); + let player_config = setup.player_config.clone(); + + let soft_volume = mixer.get_soft_volume(); + let format = setup.format; + let backend = setup.backend; + let device = setup.device.clone(); + let player = Player::new(player_config, session.clone(), soft_volume, move || { + (backend)(device, format) + }); + + if let Some(player_event_program) = setup.player_event_program.clone() { + _event_handler = Some(EventHandler::new( + player.get_player_event_channel(), + &player_event_program, + )); + + if setup.emit_sink_events { + player.set_sink_event_callback(Some(Box::new(move |sink_status| { + run_program_on_sink_events(sink_status, &player_event_program) + }))); + } + } + loop { tokio::select! { credentials = async { @@ -1749,32 +1774,16 @@ async fn main() { _ = async {}, if connecting && last_credentials.is_some() => { if session.is_invalid() { session = Session::new(setup.session_config.clone(), setup.cache.clone()); + player.set_session(session.clone()); } - 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 soft_volume = mixer.get_soft_volume(); - let format = setup.format; - let backend = setup.backend; - let device = setup.device.clone(); - let player = Player::new(player_config, session.clone(), soft_volume, move || { - (backend)(device, format) - }); - - if let Some(player_event_program) = setup.player_event_program.clone() { - _event_handler = Some(EventHandler::new(player.get_player_event_channel(), &player_event_program)); - - if setup.emit_sink_events { - player.set_sink_event_callback(Some(Box::new(move |sink_status| { - run_program_on_sink_events(sink_status, &player_event_program) - }))); - } - }; - - let (spirc_, spirc_task_) = match Spirc::new(connect_config, session.clone(), last_credentials.clone().unwrap_or_default(), player, mixer).await { + let (spirc_, spirc_task_) = match Spirc::new(connect_config, + session.clone(), + last_credentials.clone().unwrap_or_default(), + player.clone(), + mixer.clone()).await { Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_), Err(e) => { error!("could not initialize spirc: {}", e); From e5abed7c9286abac0849273642c69039acced5ec Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 17:57:32 +0300 Subject: [PATCH 323/561] Exit with an error when the player instance is invalid --- src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main.rs b/src/main.rs index eab2f697..8054a236 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1817,6 +1817,10 @@ async fn main() { exit(1); } }, + _ = async {}, if player.is_invalid() => { + error!("Player shut down unexpectedly"); + exit(1); + }, _ = tokio::signal::ctrl_c() => { break; }, From 02c9be368da7460379ab22421ee28a4d259e398b Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 18:00:28 +0300 Subject: [PATCH 324/561] Reset session after spirc crash --- src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.rs b/src/main.rs index 8054a236..1467c177 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1811,6 +1811,9 @@ async fn main() { if last_credentials.is_some() && !reconnect_exceeds_rate_limit() { auto_connect_times.push(Instant::now()); + if !session.is_invalid() { + session.shutdown(); + } connecting = true; } else { error!("Spirc shut down too often. Not reconnecting automatically."); From c60c4f4c8c52ae94c0c6e5096961c802886ddcbd Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Tue, 13 Jun 2023 18:04:01 +0300 Subject: [PATCH 325/561] Start a new session on credentials change --- src/main.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main.rs b/src/main.rs index 1467c177..11574d92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1762,6 +1762,9 @@ async fn main() { // Continue shutdown in its own task tokio::spawn(spirc_task); } + if !session.is_invalid() { + session.shutdown(); + } connecting = true; }, From b1c66417234fe5e5031d83744fb297b235c1b33c Mon Sep 17 00:00:00 2001 From: Jarkko Lehtoranta Date: Wed, 19 Jul 2023 14:57:44 +0300 Subject: [PATCH 326/561] Update changelog (#1176) --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b057082..7ee47655 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,9 @@ https://github.com/librespot-org/librespot - [all] `chrono` replaced with `time` (breaking) - [all] `time` updated (CVE-2020-26235) - [all] Improve lock contention and performance (breaking) +- [all] Use a single `player` instance. Eliminates occasional `player` and + `audio backend` restarts, which can cause issues with some playback + configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) - [core] MSRV is now 1.65 (breaking) From d96695e413e79d22827bfa6bac24d9dcf12e08e5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 18:08:27 +0000 Subject: [PATCH 327/561] Bump rustls-webpki from 0.100.1 to 0.100.2 Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.100.1 to 0.100.2. - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](https://github.com/rustls/webpki/compare/v/0.100.1...v/0.100.2) --- updated-dependencies: - dependency-name: rustls-webpki dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1f301939..d6431014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2395,9 +2395,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.100.1" +version = "0.100.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" dependencies = [ "ring", "untrusted", From 6b0e12fe7b30d9ba45745736c515d6e54d6a6ba2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 11:13:32 +0200 Subject: [PATCH 328/561] Update governor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 500 +++++++++++++++++++++++------------------------- core/Cargo.toml | 2 +- 2 files changed, 240 insertions(+), 262 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6431014..8c082720 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" dependencies = [ "gimli", ] @@ -39,12 +39,12 @@ dependencies = [ [[package]] name = "alsa" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8512c9117059663fb5606788fbca3619e2a91dac0e3fe516242eab1fa6be5e44" +checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" dependencies = [ "alsa-sys", - "bitflags", + "bitflags 1.3.2", "libc", "nix 0.24.3", ] @@ -61,9 +61,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" [[package]] name = "arrayvec" @@ -73,13 +73,13 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", ] [[package]] @@ -96,9 +96,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.67" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" dependencies = [ "addr2line", "cc", @@ -133,7 +133,7 @@ version = "0.64.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cexpr", "clang-sys", "lazy_static", @@ -153,6 +153,12 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + [[package]] name = "block-buffer" version = "0.10.4" @@ -188,11 +194,12 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -212,9 +219,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215c0072ecc28f92eeb0eea38ba63ddfcb65c2828c46311d646f1a3ff5f9841c" +checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" dependencies = [ "smallvec", "target-lexicon", @@ -259,9 +266,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" [[package]] name = "core-foundation" @@ -291,7 +298,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation-sys 0.6.2", "coreaudio-sys", ] @@ -333,9 +340,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -382,15 +389,21 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" -version = "0.7.6" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56acb310e15652100da43d130af8d97b509e95af61aab1c5a7939ef24337ee17" +checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" + [[package]] name = "digest" version = "0.10.7" @@ -415,9 +428,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" @@ -443,19 +456,19 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -470,12 +483,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.9.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" -dependencies = [ - "instant", -] +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" [[package]] name = "fixedbitset" @@ -554,7 +564,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", ] [[package]] @@ -648,7 +658,7 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-channel", "futures-core", "futures-executor", @@ -725,11 +735,11 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.20.6" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3113531138b4e41968e33fd003a0d1a635ef6e0cbc309dd5004123000863ac54" +checksum = "c0a4150420d4aa1caf6fa15f0dba7a5007d4116380633bd1253acce206098fc9" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "futures-channel", "futures-core", @@ -750,11 +760,11 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.20.0" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa1550d18fe8d97900148cc97d63a3212c3d53169c8469b9bf617de8953c05a8" +checksum = "fe9a6d39222f708ba3a55f259261452daf4fc0d36693fe14126a630beb6461a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "futures-core", "futures-sink", "glib", @@ -780,11 +790,11 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.20.4" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06b5a8658e575f6469053026ac663a348d5a562c9fce20ab2ca0c349e05d079e" +checksum = "8448db43cee0270c6ca94e6771c92a4c3b14c51ac1e6605a5f2deef66f5516f1" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "glib", "gstreamer", @@ -810,17 +820,18 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.20.5" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8ff5dfbf7bcaf1466a385b836bad0d8da25759f121458727fdda1f771c69b3" +checksum = "c0896c4acff303dd21d6a96a7ea4cc9339f7096230fe1433720c9f0bed203985" dependencies = [ "atomic_refcell", - "bitflags", + "bitflags 1.3.2", "cfg-if", "glib", "gstreamer", "gstreamer-base-sys", "libc", + "once_cell", ] [[package]] @@ -850,9 +861,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d357c7ae988e7d2182f7d7871d0b963962420b0678b0997ce7de72001aeab782" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -886,7 +897,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -912,18 +923,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] - -[[package]] -name = "hermit-abi" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" [[package]] name = "hex" @@ -993,9 +995,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -1031,7 +1033,7 @@ dependencies = [ "tokio", "tokio-rustls 0.22.0", "tower-service", - "webpki 0.21.4", + "webpki", ] [[package]] @@ -1048,19 +1050,20 @@ dependencies = [ "rustls-native-certs 0.5.0", "tokio", "tokio-rustls 0.22.0", - "webpki 0.21.4", + "webpki", ] [[package]] name = "hyper-rustls" -version = "0.24.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0646026eb1b3eea4cd9ba47912ea5ce9cc07713d105b1a14698f4e6433d348b7" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" dependencies = [ + "futures-util", "http", "hyper", "log", - "rustls 0.21.2", + "rustls 0.21.6", "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", @@ -1115,43 +1118,22 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi 0.3.1", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "is-terminal" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.1", - "io-lifetimes", + "hermit-abi", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "itoa" -version = "1.0.6" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jack" @@ -1159,7 +1141,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jack-sys", "lazy_static", "libc", @@ -1172,7 +1154,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6013b7619b95a22b576dfb43296faa4ecbe40abbdb97dfd22ead520775fc86ab" dependencies = [ - "bitflags", + "bitflags 1.3.2", "lazy_static", "libc", "libloading", @@ -1291,11 +1273,11 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.27.1" +version = "2.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1745b20bfc194ac12ef828f144f0ec2d4a7fe993281fa3567a0bd4969aee6890" +checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libpulse-sys", "num-derive", @@ -1305,9 +1287,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.27.1" +version = "2.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ced94199e6e44133431374e4043f34e1f0697ebfb7b7d6c244a65bfaedf0e31" +checksum = "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1316,9 +1298,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.20.1" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e423d9c619c908ce9b4916080e65ab586ca55b8c4939379f15e6e72fb43842" +checksum = "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152" dependencies = [ "libpulse-sys", "pkg-config", @@ -1326,9 +1308,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.20.1" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2191e6880818d1df4cf72eac8e91dce7a5a52ba0da4b2a5cdafabc22b937eadb" +checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" dependencies = [ "libc", "num-derive", @@ -1355,7 +1337,6 @@ dependencies = [ "log", "rpassword", "sha1", - "sysinfo", "thiserror", "tokio", "url", @@ -1419,7 +1400,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", - "hyper-rustls 0.24.0", + "hyper-rustls 0.24.1", "librespot-protocol", "log", "nonzero_ext", @@ -1537,9 +1518,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -1601,9 +1582,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] @@ -1616,7 +1597,7 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1640,7 +1621,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "jni-sys", "ndk-sys", "num_enum", @@ -1669,7 +1650,7 @@ version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cc", "cfg-if", "libc", @@ -1682,7 +1663,7 @@ version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if", "libc", ] @@ -1732,9 +1713,9 @@ dependencies = [ [[package]] name = "num-bigint-dig" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" dependencies = [ "byteorder", "lazy_static", @@ -1792,9 +1773,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -1802,11 +1783,11 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.2.6", + "hermit-abi", "libc", ] @@ -1832,10 +1813,19 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.30.4" +name = "num_threads" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03b4680b86d9cfafba8fc491dc9b6df26b68cf40e9e6cd73909194759a63c385" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" dependencies = [ "memchr", ] @@ -1916,20 +1906,20 @@ dependencies = [ "redox_syscall 0.3.5", "smallvec", "thread-id", - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pbkdf2" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest", "hmac", @@ -1968,9 +1958,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" [[package]] name = "pin-utils" @@ -2011,7 +2001,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdb6b5eff96ccc9bf44d34c379ab03ae944426d83d1694345bdf8159d561d562" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "portaudio-sys", ] @@ -2084,9 +2074,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -2144,9 +2134,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.29.0" +version = "0.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" dependencies = [ "memchr", "serde", @@ -2154,9 +2144,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.28" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -2213,7 +2203,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2222,14 +2212,26 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.4" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ "aho-corasick", "memchr", @@ -2238,9 +2240,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" [[package]] name = "ring" @@ -2323,16 +2325,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.37.20" +version = "0.38.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" +checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" dependencies = [ - "bitflags", + "bitflags 2.3.3", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2345,14 +2346,14 @@ dependencies = [ "log", "ring", "sct 0.6.1", - "webpki 0.21.4", + "webpki", ] [[package]] name = "rustls" -version = "0.21.2" +version = "0.21.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ "log", "ring", @@ -2386,18 +2387,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ "base64 0.21.2", ] [[package]] name = "rustls-webpki" -version = "0.100.2" +version = "0.101.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff011474fa39949b7e5c0428f9b4937eda7da7848bbb947786b7be0b27dab" +checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" dependencies = [ "ring", "untrusted", @@ -2405,15 +2406,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.13" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "same-file" @@ -2426,18 +2427,18 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.42.0", + "windows-sys", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -2465,7 +2466,7 @@ version = "0.35.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "lazy_static", "libc", "sdl2-sys", @@ -2484,11 +2485,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys 0.8.4", "libc", @@ -2497,9 +2498,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys 0.8.4", "libc", @@ -2507,29 +2508,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.164" +version = "1.0.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" +checksum = "bdb30a74471f5b7a1fa299f40b4bf1be93af61116df95465b2b5fc419331e430" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.164" +version = "1.0.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" +checksum = "6f4c2c6ea4bc09b5c419012eafcdb0fcef1d9119d626c8f3a0708a5b92d38a70" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.99" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -2607,9 +2608,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" [[package]] name = "socket2" @@ -2663,7 +2664,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" dependencies = [ - "bitflags", + "bitflags 1.3.2", "lazy_static", "log", "symphonia-core", @@ -2688,7 +2689,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" dependencies = [ "arrayvec", - "bitflags", + "bitflags 1.3.2", "bytemuck", "lazy_static", "log", @@ -2741,9 +2742,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.22" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -2752,9 +2753,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.2" +version = "0.29.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9557d0845b86eea8182f7b10dff120214fb6cd9fd937b6f4917714e546a38695" +checksum = "165d6d8539689e3d3bc8b98ac59541e1f21c7de7c85d60dc80e43ae0ed2113db" dependencies = [ "cfg-if", "core-foundation-sys 0.8.4", @@ -2779,22 +2780,21 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.8" +version = "0.12.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" [[package]] name = "tempfile" -version = "3.6.0" +version = "3.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" dependencies = [ - "autocfg", "cfg-if", "fastrand", "redox_syscall 0.3.5", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2808,22 +2808,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", ] [[package]] @@ -2839,11 +2839,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ + "deranged", "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -2857,9 +2860,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -2881,11 +2884,12 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.28.2" +version = "1.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" +checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" dependencies = [ "autocfg", + "backtrace", "bytes", "libc", "mio", @@ -2895,7 +2899,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2906,7 +2910,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", ] [[package]] @@ -2917,7 +2921,7 @@ checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls 0.19.1", "tokio", - "webpki 0.21.4", + "webpki", ] [[package]] @@ -2926,7 +2930,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.2", + "rustls 0.21.6", "tokio", ] @@ -2943,13 +2947,13 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec509ac96e9a0c43427c74f003127d953a265737636129424288d27cb5c4b12c" +checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" dependencies = [ "futures-util", "log", - "rustls 0.21.2", + "rustls 0.21.6", "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", @@ -2972,9 +2976,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -2993,9 +2997,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.11" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap 2.0.0", "serde", @@ -3038,9 +3042,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.19.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15fba1a6d6bb030745759a9a2a588bfe8490fc8b4751a277db3a0be1c9ebbf67" +checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" dependencies = [ "byteorder", "bytes", @@ -3049,12 +3053,11 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.21.2", + "rustls 0.21.6", "sha1", "thiserror", "url", "utf-8", - "webpki 0.22.0", ] [[package]] @@ -3071,9 +3074,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-normalization" @@ -3115,9 +3118,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.3.4" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa2982af2eec27de306107c027578ff7f423d65f7250e40ce0fea8f45248b81" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom", "rand", @@ -3125,9 +3128,9 @@ dependencies = [ [[package]] name = "vergen" -version = "8.2.1" +version = "8.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3c89c2c7e50f33e4d35527e5bf9c11d6d132226dbbd1753f0fbe9f19ef88c6" +checksum = "bbc5ad0d9d26b2c49a5ab7da76c3e79d3ee37e7821799f8223fcb8f2f391a2e7" dependencies = [ "anyhow", "rustversion", @@ -3192,7 +3195,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -3226,7 +3229,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.22", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3257,16 +3260,6 @@ dependencies = [ "untrusted", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "which" version = "4.4.0" @@ -3318,28 +3311,13 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", + "windows-targets 0.48.1", ] [[package]] @@ -3359,9 +3337,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.0" +version = "0.48.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" dependencies = [ "windows_aarch64_gnullvm 0.48.0", "windows_aarch64_msvc 0.48.0", @@ -3458,9 +3436,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.7" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" dependencies = [ "memchr", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index ff69eaae..6a8ae0a8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,7 +22,7 @@ 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"] } -governor = { version = "0.5", default-features = false, features = ["std", "jitter"] } +governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } hex = "0.4" hmac = "0.12" httparse = "1.7" From 369c84cddb043f1730a631e34fb2304f13a9cd30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 11:25:26 +0200 Subject: [PATCH 329/561] Update num-derive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 6a8ae0a8..847bcc35 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -33,7 +33,7 @@ hyper-rustls = { version = "0.24", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" num-bigint = { version = "0.4", features = ["rand"] } -num-derive = "0.3" +num-derive = "0.4" num-integer = "0.1" num-traits = "0.2" once_cell = "1" From 0954de6d9e7992317352d720c1517df30e54ac4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 11:34:59 +0200 Subject: [PATCH 330/561] Update quick-xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index 847bcc35..de60b689 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "1.2" protobuf = "3" -quick-xml = { version = "0.29", features = ["serialize"] } +quick-xml = { version = "0.30", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } From 84804b6bfce39953234f74d812a8473bb3aa13bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 11:40:21 +0200 Subject: [PATCH 331/561] Update glib MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 5ef5b4f7..16d1b9a9 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -40,7 +40,7 @@ sdl2 = { version = "0.35", optional = true } gstreamer = { version = "0.20", optional = true } gstreamer-app = { version = "0.20", optional = true } gstreamer-audio = { version = "0.20", optional = true } -glib = { version = "0.17", optional = true } +glib = { version = "0.18.1", optional = true } # Rodio dependencies rodio = { version = "0.17.1", optional = true, default-features = false } From 1c214f7f0eb2bfc03b54317bee681aebca6c1cc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 12:13:57 +0200 Subject: [PATCH 332/561] Update MSRV to 1.67 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 710dda29..3a241e84 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.65" # MSRV (Minimum supported rust version) + - "1.67" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -163,7 +163,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.65" # MSRV (Minimum supported rust version) + - "1.67" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -206,7 +206,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.65" # MSRV (Minimum supported rust version) + - "1.67" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee47655..f1767552 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ https://github.com/librespot-org/librespot configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [core] MSRV is now 1.65 (breaking) +- [core] MSRV is now 1.67 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts From dd28667178214181af49cfd8bd44ec051fbdac18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 6 Aug 2023 12:44:49 +0200 Subject: [PATCH 333/561] Update MSRV to 1.70 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3a241e84..fbaff63a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.67" # MSRV (Minimum supported rust version) + - "1.70" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -163,7 +163,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.67" # MSRV (Minimum supported rust version) + - "1.70" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -206,7 +206,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.67" # MSRV (Minimum supported rust version) + - "1.70" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index f1767552..7e032876 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ https://github.com/librespot-org/librespot configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [core] MSRV is now 1.67 (breaking) +- [core] MSRV is now 1.70 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts From 6521c70db0d234e155f8534c738bfc5329167cfd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 9 Sep 2023 10:53:51 +0000 Subject: [PATCH 334/561] Bump actions/cache from 3.3.1 to 3.3.2 Bumps [actions/cache](https://github.com/actions/cache) from 3.3.1 to 3.3.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.3.1...v3.3.2) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 710dda29..a94b51bd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo/registry/index @@ -178,7 +178,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo/registry/index @@ -221,7 +221,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.1 + uses: actions/cache@v3.3.2 with: path: | ~/.cargo/registry/index From 357072a0d706bc5a1b0a9440710067d9759c11db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 21 Oct 2023 10:06:22 +0000 Subject: [PATCH 335/561] Bump actions/checkout from 3.5.3 to 4.1.1 Bumps [actions/checkout](https://github.com/actions/checkout) from 3.5.3 to 4.1.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3.5.3...v4.1.1) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 710dda29..97f57ba3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v3.5.3 + uses: actions/checkout@v4.1.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From 78ba1b259fe165cdaa7dddef69bb3a78f3b4ca6f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:41:30 +0000 Subject: [PATCH 336/561] Bump tungstenite from 0.20.0 to 0.20.1 Bumps [tungstenite](https://github.com/snapview/tungstenite-rs) from 0.20.0 to 0.20.1. - [Changelog](https://github.com/snapview/tungstenite-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/snapview/tungstenite-rs/compare/v0.20.0...v0.20.1) --- updated-dependencies: - dependency-name: tungstenite dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 173 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 138 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c082720..53b4df9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,8 +645,21 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "system-deps", "winapi", @@ -664,10 +677,33 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", + "gio-sys 0.17.10", + "glib-macros 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6" +dependencies = [ + "bitflags 2.3.3", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.18.1", + "glib-macros 0.18.3", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "memchr", "once_cell", @@ -683,13 +719,27 @@ checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ "anyhow", "heck", - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "glib-macros" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5" +dependencies = [ + "heck", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "glib-sys" version = "0.17.10" @@ -700,6 +750,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.1" @@ -712,16 +772,27 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" dependencies = [ - "glib-sys", + "glib-sys 0.17.10", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", "libc", "system-deps", ] [[package]] name = "governor" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" dependencies = [ "cfg-if", "futures", @@ -744,7 +815,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "glib", + "glib 0.17.10", "gstreamer-sys", "libc", "muldiv", @@ -767,7 +838,7 @@ dependencies = [ "bitflags 1.3.2", "futures-core", "futures-sink", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-app-sys", "gstreamer-base", @@ -781,7 +852,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cb5375aa8c23012ec5fadde1a37b972e87680776772669d2628772867056e2" dependencies = [ - "glib-sys", + "glib-sys 0.17.10", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -796,7 +867,7 @@ checksum = "8448db43cee0270c6ca94e6771c92a4c3b14c51ac1e6605a5f2deef66f5516f1" dependencies = [ "bitflags 1.3.2", "cfg-if", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-audio-sys", "gstreamer-base", @@ -810,8 +881,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4001b779e4707b32acd6ec0960e327b926369c1a34f7c41d477ac42b2670e8" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -827,7 +898,7 @@ dependencies = [ "atomic_refcell", "bitflags 1.3.2", "cfg-if", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-base-sys", "libc", @@ -840,8 +911,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26114ed96f6668380f5a1554128159e98e06c3a7a8460f216d7cd6dce28f928c" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "gstreamer-sys", "libc", "system-deps", @@ -853,8 +924,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e56fe047adef7d47dbafa8bc1340fddb53c325e16574763063702fc94b5786d2" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "libc", "system-deps", ] @@ -1280,7 +1351,7 @@ dependencies = [ "bitflags 1.3.2", "libc", "libpulse-sys", - "num-derive", + "num-derive 0.3.3", "num-traits", "winapi", ] @@ -1313,7 +1384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" dependencies = [ "libc", - "num-derive", + "num-derive 0.3.3", "num-traits", "pkg-config", "winapi", @@ -1337,6 +1408,7 @@ dependencies = [ "log", "rpassword", "sha1", + "sysinfo", "thiserror", "tokio", "url", @@ -1405,7 +1477,7 @@ dependencies = [ "log", "nonzero_ext", "num-bigint", - "num-derive", + "num-derive 0.4.1", "num-integer", "num-traits", "once_cell", @@ -1483,7 +1555,7 @@ dependencies = [ "byteorder", "cpal", "futures-util", - "glib", + "glib 0.18.3", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -1739,6 +1811,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1806,7 +1889,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", @@ -1839,7 +1922,7 @@ dependencies = [ "jni 0.20.0", "ndk", "ndk-context", - "num-derive", + "num-derive 0.3.3", "num-traits", "oboe-sys", ] @@ -2045,7 +2128,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.14", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", ] [[package]] @@ -2134,9 +2226,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", "serde", @@ -2983,14 +3075,14 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.14", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -3008,6 +3100,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -3042,9 +3145,9 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" dependencies = [ "byteorder", "bytes", From 3eef23b8225db1d1ca336f33c18b1b069e6013b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Nov 2023 19:42:08 +0000 Subject: [PATCH 337/561] Bump rustls-webpki from 0.101.2 to 0.101.7 Bumps [rustls-webpki](https://github.com/rustls/webpki) from 0.101.2 to 0.101.7. - [Release notes](https://github.com/rustls/webpki/releases) - [Commits](https://github.com/rustls/webpki/compare/v/0.101.2...v/0.101.7) --- updated-dependencies: - dependency-name: rustls-webpki dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 233 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 181 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8c082720..a4074264 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,9 +194,9 @@ checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" [[package]] name = "cc" -version = "1.0.81" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", "libc", @@ -645,8 +645,21 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "system-deps", "winapi", @@ -664,10 +677,33 @@ dependencies = [ "futures-executor", "futures-task", "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", + "gio-sys 0.17.10", + "glib-macros 0.17.10", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6" +dependencies = [ + "bitflags 2.3.3", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys 0.18.1", + "glib-macros 0.18.3", + "glib-sys 0.18.1", + "gobject-sys 0.18.0", "libc", "memchr", "once_cell", @@ -683,13 +719,27 @@ checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" dependencies = [ "anyhow", "heck", - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro-error", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "glib-macros" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5" +dependencies = [ + "heck", + "proc-macro-crate 2.0.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "glib-sys" version = "0.17.10" @@ -700,6 +750,16 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + [[package]] name = "glob" version = "0.3.1" @@ -712,16 +772,27 @@ version = "0.17.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" dependencies = [ - "glib-sys", + "glib-sys 0.17.10", + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys 0.18.1", "libc", "system-deps", ] [[package]] name = "governor" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c390a940a5d157878dd057c78680a33ce3415bcd05b4799509ea44210914b4d5" +checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" dependencies = [ "cfg-if", "futures", @@ -744,7 +815,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "glib", + "glib 0.17.10", "gstreamer-sys", "libc", "muldiv", @@ -767,7 +838,7 @@ dependencies = [ "bitflags 1.3.2", "futures-core", "futures-sink", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-app-sys", "gstreamer-base", @@ -781,7 +852,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7cb5375aa8c23012ec5fadde1a37b972e87680776772669d2628772867056e2" dependencies = [ - "glib-sys", + "glib-sys 0.17.10", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -796,7 +867,7 @@ checksum = "8448db43cee0270c6ca94e6771c92a4c3b14c51ac1e6605a5f2deef66f5516f1" dependencies = [ "bitflags 1.3.2", "cfg-if", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-audio-sys", "gstreamer-base", @@ -810,8 +881,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d4001b779e4707b32acd6ec0960e327b926369c1a34f7c41d477ac42b2670e8" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -827,7 +898,7 @@ dependencies = [ "atomic_refcell", "bitflags 1.3.2", "cfg-if", - "glib", + "glib 0.17.10", "gstreamer", "gstreamer-base-sys", "libc", @@ -840,8 +911,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26114ed96f6668380f5a1554128159e98e06c3a7a8460f216d7cd6dce28f928c" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "gstreamer-sys", "libc", "system-deps", @@ -853,8 +924,8 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e56fe047adef7d47dbafa8bc1340fddb53c325e16574763063702fc94b5786d2" dependencies = [ - "glib-sys", - "gobject-sys", + "glib-sys 0.17.10", + "gobject-sys 0.17.10", "libc", "system-deps", ] @@ -1220,7 +1291,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ - "spin", + "spin 0.5.2", ] [[package]] @@ -1231,9 +1302,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" [[package]] name = "libloading" @@ -1280,7 +1351,7 @@ dependencies = [ "bitflags 1.3.2", "libc", "libpulse-sys", - "num-derive", + "num-derive 0.3.3", "num-traits", "winapi", ] @@ -1313,7 +1384,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" dependencies = [ "libc", - "num-derive", + "num-derive 0.3.3", "num-traits", "pkg-config", "winapi", @@ -1337,6 +1408,7 @@ dependencies = [ "log", "rpassword", "sha1", + "sysinfo", "thiserror", "tokio", "url", @@ -1405,7 +1477,7 @@ dependencies = [ "log", "nonzero_ext", "num-bigint", - "num-derive", + "num-derive 0.4.1", "num-integer", "num-traits", "once_cell", @@ -1483,7 +1555,7 @@ dependencies = [ "byteorder", "cpal", "futures-util", - "glib", + "glib 0.18.3", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -1739,6 +1811,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1806,7 +1889,7 @@ version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 1.0.109", @@ -1839,7 +1922,7 @@ dependencies = [ "jni 0.20.0", "ndk", "ndk-context", - "num-derive", + "num-derive 0.3.3", "num-traits", "oboe-sys", ] @@ -2045,7 +2128,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.14", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +dependencies = [ + "toml_edit 0.20.7", ] [[package]] @@ -2134,9 +2226,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", "serde", @@ -2253,12 +2345,26 @@ dependencies = [ "cc", "libc", "once_cell", - "spin", - "untrusted", + "spin 0.5.2", + "untrusted 0.7.1", "web-sys", "winapi", ] +[[package]] +name = "ring" +version = "0.17.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted 0.9.0", + "windows-sys", +] + [[package]] name = "rodio" version = "0.17.1" @@ -2344,7 +2450,7 @@ checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64 0.13.1", "log", - "ring", + "ring 0.16.20", "sct 0.6.1", "webpki", ] @@ -2356,7 +2462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" dependencies = [ "log", - "ring", + "ring 0.16.20", "rustls-webpki", "sct 0.7.0", ] @@ -2396,12 +2502,12 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.2" +version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513722fd73ad80a71f72b61009ea1b584bcfa1483ca93949c8f290298837fa59" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring", - "untrusted", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -2446,8 +2552,8 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -2456,8 +2562,8 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] @@ -2628,6 +2734,12 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "spki" version = "0.7.2" @@ -2983,14 +3095,14 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.19.14", ] [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" dependencies = [ "serde", ] @@ -3008,6 +3120,17 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.20.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +dependencies = [ + "indexmap 2.0.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower-service" version = "0.3.2" @@ -3099,6 +3222,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.4.0" @@ -3256,8 +3385,8 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" dependencies = [ - "ring", - "untrusted", + "ring 0.16.20", + "untrusted 0.7.1", ] [[package]] From eae3a56c30caa607803d6139569a744c293c1dc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 22:47:45 +0100 Subject: [PATCH 338/561] Fix clippy warning, use unwrap_or_default() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/spclient.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index d6c5ffb1..fbb8ddc5 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -392,7 +392,7 @@ impl SpClient { ) -> SpClientResult { let body = protobuf::text_format::print_to_string(message); - let mut headers = headers.unwrap_or_else(HeaderMap::new); + let mut headers = headers.unwrap_or_default(); headers.insert( CONTENT_TYPE, HeaderValue::from_static("application/x-protobuf"), @@ -409,7 +409,7 @@ impl SpClient { headers: Option, body: Option<&str>, ) -> SpClientResult { - let mut headers = headers.unwrap_or_else(HeaderMap::new); + let mut headers = headers.unwrap_or_default(); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); self.request(method, endpoint, Some(headers), body).await From 663fb3b8614514d77a9b4e031e443f753a0c6535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 22:51:26 +0100 Subject: [PATCH 339/561] Update quick-xml to 0.31 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index de60b689..374ae75b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,7 +41,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "1.2" protobuf = "3" -quick-xml = { version = "0.30", features = ["serialize"] } +quick-xml = { version = "0.31", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } From 36c939c12a83d74c730ea60c2607a02730f7dd52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:06:00 +0100 Subject: [PATCH 340/561] Update alsa to 0.8.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 16d1b9a9..79bd17d1 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -31,7 +31,7 @@ tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sy zerocopy = "0.6" # Backends -alsa = { version = "0.7", optional = true } +alsa = { version = "0.8.1", 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 } From dd07be41af279593e0873b4afe52b9737634ee13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:13:25 +0100 Subject: [PATCH 341/561] Update gstreamer to 0.21.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 79bd17d1..979bf063 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -37,9 +37,9 @@ libpulse-binding = { version = "2", optional = true, default-features = f libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.11", optional = true } sdl2 = { version = "0.35", optional = true } -gstreamer = { version = "0.20", optional = true } -gstreamer-app = { version = "0.20", optional = true } -gstreamer-audio = { version = "0.20", optional = true } +gstreamer = { version = "0.21.2", optional = true } +gstreamer-app = { version = "0.21.2", optional = true } +gstreamer-audio = { version = "0.21.2", optional = true } glib = { version = "0.18.1", optional = true } # Rodio dependencies From c01742c010bc071ed9e51f9e5fca0b758a4c45dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:15:09 +0100 Subject: [PATCH 342/561] Update zerocopy to 0.7.26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 979bf063..230e5f85 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -28,7 +28,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } shell-words = "1.1" thiserror = "1" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = "0.6" +zerocopy = { version = "0.7.26", features = ["derive"] } # Backends alsa = { version = "0.8.1", optional = true } From ac9472f085c22cb6fe671bedaa6b6940c56c6051 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:36:18 +0100 Subject: [PATCH 343/561] Update Cargo.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 909 +++++++++++++++++++++++++---------------------------- 1 file changed, 436 insertions(+), 473 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d3cfc2b8..b8d89b7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] @@ -49,6 +49,18 @@ dependencies = [ "nix 0.24.3", ] +[[package]] +name = "alsa" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce34de545ad29bcc00cb1b87a94c132256dcf83aa7eeb9674482568405a6ff0a" +dependencies = [ + "alsa-sys", + "bitflags 2.4.1", + "libc", + "nix 0.26.4", +] + [[package]] name = "alsa-sys" version = "0.3.1" @@ -61,9 +73,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayvec" @@ -73,20 +85,20 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] name = "atomic_refcell" -version = "0.1.10" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79d6dc922a2792b006573f60b2648076355daeae5ce9cb59507e5908c9625d31" +checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" @@ -96,9 +108,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -117,9 +129,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" [[package]] name = "base64ct" @@ -129,11 +141,11 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" -version = "0.64.0" +version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.1", "cexpr", "clang-sys", "lazy_static", @@ -144,7 +156,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] @@ -155,9 +167,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" [[package]] name = "block-buffer" @@ -170,27 +182,27 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "bytemuck" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" [[package]] name = "byteorder" -version = "1.4.3" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" @@ -219,9 +231,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b40ccee03b5175c18cde8f37e7d2a33bcef6f8ec8f7cc0d81090d1bb380949c9" +checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" dependencies = [ "smallvec", "target-lexicon", @@ -266,9 +278,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "core-foundation" @@ -276,16 +288,10 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys", "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.4" @@ -294,20 +300,20 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "coreaudio-rs" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb17e2d1795b1996419648915df94bc7103c28f7b48062d7acf4652fc371b2ff" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys 0.6.2", + "core-foundation-sys", "coreaudio-sys", ] [[package]] name = "coreaudio-sys" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f034b2258e6c4ade2f73bf87b21047567fb913ee9550837c2316d139b0262b24" +checksum = "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d" dependencies = [ "bindgen", ] @@ -318,8 +324,8 @@ version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" dependencies = [ - "alsa", - "core-foundation-sys 0.8.4", + "alsa 0.7.1", + "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jack", @@ -340,9 +346,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" dependencies = [ "libc", ] @@ -389,9 +395,9 @@ checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" [[package]] name = "der" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", "pem-rfc7468", @@ -400,9 +406,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.7" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7684a49fb1af197853ef7b2ee694bc1f5b4179556f1e5710e1760c5db6f5e929" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "digest" @@ -434,18 +443,18 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] name = "env_logger" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" dependencies = [ "humantime", "is-terminal", @@ -462,30 +471,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "fastrand" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fixedbitset" @@ -510,9 +508,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" +checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" dependencies = [ "futures-channel", "futures-core", @@ -525,9 +523,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" dependencies = [ "futures-core", "futures-sink", @@ -535,15 +533,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" [[package]] name = "futures-executor" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" dependencies = [ "futures-core", "futures-task", @@ -552,32 +550,32 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" [[package]] name = "futures-macro" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" +checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] name = "futures-sink" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" [[package]] name = "futures-task" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" [[package]] name = "futures-timer" @@ -587,9 +585,9 @@ checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" [[package]] name = "futures-util" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" dependencies = [ "futures-channel", "futures-core", @@ -624,9 +622,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.10" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" dependencies = [ "cfg-if", "libc", @@ -635,22 +633,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" - -[[package]] -name = "gio-sys" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" -dependencies = [ - "glib-sys 0.17.10", - "gobject-sys 0.17.10", - "libc", - "system-deps", - "winapi", -] +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gio-sys" @@ -658,52 +643,29 @@ version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "glib-sys", + "gobject-sys", "libc", "system-deps", "winapi", ] -[[package]] -name = "glib" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" -dependencies = [ - "bitflags 1.3.2", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys 0.17.10", - "glib-macros 0.17.10", - "glib-sys 0.17.10", - "gobject-sys 0.17.10", - "libc", - "memchr", - "once_cell", - "smallvec", - "thiserror", -] - [[package]] name = "glib" version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.1", "futures-channel", "futures-core", "futures-executor", "futures-task", "futures-util", - "gio-sys 0.18.1", - "glib-macros 0.18.3", - "glib-sys 0.18.1", - "gobject-sys 0.18.0", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", "libc", "memchr", "once_cell", @@ -711,21 +673,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "glib-macros" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" -dependencies = [ - "anyhow", - "heck", - "proc-macro-crate 1.3.1", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "glib-macros" version = "0.18.3" @@ -737,17 +684,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.28", -] - -[[package]] -name = "glib-sys" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" -dependencies = [ - "libc", - "system-deps", + "syn 2.0.39", ] [[package]] @@ -766,24 +703,13 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "gobject-sys" -version = "0.17.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" -dependencies = [ - "glib-sys 0.17.10", - "libc", - "system-deps", -] - [[package]] name = "gobject-sys" version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ - "glib-sys 0.18.1", + "glib-sys", "libc", "system-deps", ] @@ -806,24 +732,24 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.20.7" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a4150420d4aa1caf6fa15f0dba7a5007d4116380633bd1253acce206098fc9" +checksum = "ed97f98d186e63e49079b26af1a1b73e70ab7a2f450eb46a136f2bffc2bf12d5" dependencies = [ - "bitflags 1.3.2", "cfg-if", "futures-channel", "futures-core", "futures-util", - "glib 0.17.10", + "glib", "gstreamer-sys", + "itertools", "libc", "muldiv", "num-integer", "num-rational", - "once_cell", "option-operations", "paste", + "pin-project-lite", "pretty-hex", "smallvec", "thiserror", @@ -831,28 +757,26 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.20.7" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9a6d39222f708ba3a55f259261452daf4fc0d36693fe14126a630beb6461a8" +checksum = "16bc8090a8806193237e7b6531ee429ff6e39686425f5c3eb06dfa75875390fb" dependencies = [ - "bitflags 1.3.2", "futures-core", "futures-sink", - "glib 0.17.10", + "glib", "gstreamer", "gstreamer-app-sys", "gstreamer-base", "libc", - "once_cell", ] [[package]] name = "gstreamer-app-sys" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cb5375aa8c23012ec5fadde1a37b972e87680776772669d2628772867056e2" +checksum = "aea07f07a3f17278e6998390ecaea127e476f0af0360c2d83d96e6d3a97fb75e" dependencies = [ - "glib-sys 0.17.10", + "glib-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -861,28 +785,26 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.20.7" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8448db43cee0270c6ca94e6771c92a4c3b14c51ac1e6605a5f2deef66f5516f1" +checksum = "36d1678eacb7677c1ffdcf220ada416b5fb68e87c33b77319f14bba169fbe3fc" dependencies = [ - "bitflags 1.3.2", "cfg-if", - "glib 0.17.10", + "glib", "gstreamer", "gstreamer-audio-sys", "gstreamer-base", "libc", - "once_cell", ] [[package]] name = "gstreamer-audio-sys" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d4001b779e4707b32acd6ec0960e327b926369c1a34f7c41d477ac42b2670e8" +checksum = "78bd94ae8b177377855b38c3d809c686526786cdb771e6d68510509634b955d1" dependencies = [ - "glib-sys 0.17.10", - "gobject-sys 0.17.10", + "glib-sys", + "gobject-sys", "gstreamer-base-sys", "gstreamer-sys", "libc", @@ -891,28 +813,26 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.20.7" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0896c4acff303dd21d6a96a7ea4cc9339f7096230fe1433720c9f0bed203985" +checksum = "cb150b6904a49052237fede7cc2e6479df6ced5043d95e6af8134bc141a3167f" dependencies = [ "atomic_refcell", - "bitflags 1.3.2", "cfg-if", - "glib 0.17.10", + "glib", "gstreamer", "gstreamer-base-sys", "libc", - "once_cell", ] [[package]] name = "gstreamer-base-sys" -version = "0.20.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26114ed96f6668380f5a1554128159e98e06c3a7a8460f216d7cd6dce28f928c" +checksum = "f4ca701f9078fe115b29b24c80910b577f9cb5b039182f050dbadf5933594b64" dependencies = [ - "glib-sys 0.17.10", - "gobject-sys 0.17.10", + "glib-sys", + "gobject-sys", "gstreamer-sys", "libc", "system-deps", @@ -920,21 +840,21 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.20.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e56fe047adef7d47dbafa8bc1340fddb53c325e16574763063702fc94b5786d2" +checksum = "564cda782b3e6eed1b81cb4798a06794db56440fb05b422505be689f34ce3bc4" dependencies = [ - "glib-sys 0.17.10", - "gobject-sys 0.17.10", + "glib-sys", + "gobject-sys", "libc", "system-deps", ] [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -957,18 +877,17 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" [[package]] name = "headers" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" +checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.13.1", - "bitflags 1.3.2", + "base64 0.21.5", "bytes", "headers-core", "http", @@ -994,9 +913,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1013,6 +932,15 @@ dependencies = [ "digest", ] +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys", +] + [[package]] name = "hostname" version = "0.3.1" @@ -1026,9 +954,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" dependencies = [ "bytes", "fnv", @@ -1054,9 +982,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "humantime" @@ -1081,7 +1009,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.10", "tokio", "tower-service", "tracing", @@ -1126,15 +1054,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http", "hyper", "log", - "rustls 0.21.6", + "rustls 0.21.8", "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", @@ -1172,12 +1100,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.2", ] [[package]] @@ -1200,6 +1128,15 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1269,18 +1206,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" dependencies = [ "wasm-bindgen", ] @@ -1318,9 +1255,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmdns" @@ -1336,7 +1273,7 @@ dependencies = [ "multimap", "nix 0.23.2", "rand", - "socket2", + "socket2 0.4.10", "thiserror", "tokio", "winapi", @@ -1457,7 +1394,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.2", + "base64 0.21.5", "byteorder", "bytes", "dns-sd", @@ -1472,7 +1409,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy", - "hyper-rustls 0.24.1", + "hyper-rustls 0.24.2", "librespot-protocol", "log", "nonzero_ext", @@ -1509,7 +1446,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.2", + "base64 0.21.5", "cfg-if", "ctr", "dns-sd", @@ -1551,11 +1488,11 @@ dependencies = [ name = "librespot-playback" version = "0.5.0-dev" dependencies = [ - "alsa", + "alsa 0.8.1", "byteorder", "cpal", "futures-util", - "glib 0.18.3", + "glib", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -1590,15 +1527,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" [[package]] name = "lock_api" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ "autocfg", "scopeguard", @@ -1606,9 +1543,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "mach2" @@ -1627,9 +1564,9 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" @@ -1663,9 +1600,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" dependencies = [ "libc", "wasi", @@ -1740,6 +1677,17 @@ dependencies = [ "libc", ] +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -1773,9 +1721,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -1819,7 +1767,7 @@ checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] @@ -1856,9 +1804,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", "libm", @@ -1906,9 +1854,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -1978,18 +1926,18 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.8" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "backtrace", "cfg-if", "libc", "petgraph", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", "thread-id", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -2031,19 +1979,19 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.1.0", ] [[package]] name = "pin-project-lite" -version = "0.2.11" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c516611246607d0c04186886dbb3a754368ef82c79e9827a802c6d836dd111c" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2099,6 +2047,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2128,7 +2082,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit 0.19.14", + "toml_edit 0.19.15", ] [[package]] @@ -2166,18 +2120,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55bad9126f378a853655831eb7363b7b01b81d19f8cb1218861086ca4a1a61e" +checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" dependencies = [ "once_cell", "protobuf-support", @@ -2186,9 +2140,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd418ac3c91caa4032d37cb80ff0d44e2ebe637b2fb243b6234bf89cdac4901" +checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" dependencies = [ "anyhow", "once_cell", @@ -2201,9 +2155,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d39b14605eaa1f6a340aec7f320b34064feb26c93aec35d6a9a2272a8ddfa49" +checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -2217,18 +2171,18 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d4d7b8601c814cfb36bcebb79f0e61e45e1e93640cf778837833bbed05c372" +checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" dependencies = [ "thiserror", ] [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", "serde", @@ -2236,9 +2190,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2291,27 +2245,18 @@ checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" [[package]] name = "redox_syscall" -version = "0.2.16" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.9.3" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", @@ -2321,9 +2266,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", @@ -2332,9 +2277,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ring" @@ -2367,36 +2312,34 @@ dependencies = [ [[package]] name = "rodio" -version = "0.17.1" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdf1d4dea18dff2e9eb6dca123724f8b60ef44ad74a9ad283cdfe025df7e73fa" +checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" dependencies = [ "cpal", ] [[package]] name = "rpassword" -version = "7.2.0" +version = "7.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" +checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" dependencies = [ "libc", "rtoolbox", - "winapi", + "windows-sys", ] [[package]] name = "rsa" -version = "0.9.2" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" +checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" dependencies = [ - "byteorder", "const-oid", "digest", "num-bigint-dig", "num-integer", - "num-iter", "num-traits", "pkcs1", "pkcs8", @@ -2409,12 +2352,12 @@ dependencies = [ [[package]] name = "rtoolbox" -version = "0.0.1" +version = "0.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" dependencies = [ "libc", - "winapi", + "windows-sys", ] [[package]] @@ -2431,11 +2374,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.7" +version = "0.38.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "172891ebdceb05aa0005f533a6cbfca599ddd7d966f6f5d4d9b2e70478e70399" +checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -2457,14 +2400,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.6" +version = "0.21.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1feddffcfcc0b33f5c6ce9a29e341e4cd59c3f78e7ee45f4a40c038b1d6cbb" +checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" dependencies = [ "log", - "ring 0.16.20", + "ring 0.17.5", "rustls-webpki", - "sct 0.7.0", + "sct 0.7.1", ] [[package]] @@ -2493,11 +2436,11 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", ] [[package]] @@ -2558,12 +2501,12 @@ dependencies = [ [[package]] name = "sct" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", + "ring 0.17.5", + "untrusted 0.9.0", ] [[package]] @@ -2597,7 +2540,7 @@ checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", - "core-foundation-sys 0.8.4", + "core-foundation-sys", "libc", "security-framework-sys", ] @@ -2608,35 +2551,35 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ - "core-foundation-sys 0.8.4", + "core-foundation-sys", "libc", ] [[package]] name = "serde" -version = "1.0.182" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb30a74471f5b7a1fa299f40b4bf1be93af61116df95465b2b5fc419331e430" +checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.182" +version = "1.0.192" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f4c2c6ea4bc09b5c419012eafcdb0fcef1d9119d626c8f3a0708a5b92d38a70" +checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", @@ -2645,18 +2588,18 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96426c9936fd7a0124915f9185ea1d20aa9445cc9821142f0a73bc9207a2e186" +checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" dependencies = [ "serde", ] [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -2680,9 +2623,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" [[package]] name = "signal-hook-registry" @@ -2695,9 +2638,9 @@ dependencies = [ [[package]] name = "signature" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", "rand_core", @@ -2705,29 +2648,39 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" [[package]] name = "socket2" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" dependencies = [ "libc", "winapi", ] +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -2854,9 +2807,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" dependencies = [ "proc-macro2", "quote", @@ -2865,12 +2818,12 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.7" +version = "0.29.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165d6d8539689e3d3bc8b98ac59541e1f21c7de7c85d60dc80e43ae0ed2113db" +checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" dependencies = [ "cfg-if", - "core-foundation-sys 0.8.4", + "core-foundation-sys", "libc", "ntapi", "once_cell", @@ -2879,9 +2832,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.1.1" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30c2de8a4d8f4b823d634affc9cd2a74ec98c53a756f317e529a48046cbf71f3" +checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" dependencies = [ "cfg-expr", "heck", @@ -2892,73 +2845,73 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.11" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" +checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" dependencies = [ "cfg-if", "fastrand", - "redox_syscall 0.3.5", + "redox_syscall", "rustix", "windows-sys", ] [[package]] name = "termcolor" -version = "1.2.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" dependencies = [ "winapi-util", ] [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] name = "thread-id" -version = "4.1.0" +version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ee93aa2b8331c0fec9091548843f2c90019571814057da3b783f9de09349d73" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" dependencies = [ "libc", - "redox_syscall 0.2.16", "winapi", ] [[package]] name = "time" -version = "0.3.25" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", "libc", "num_threads", + "powerfmt", "serde", "time-core", "time-macros", @@ -2966,15 +2919,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" +checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" dependencies = [ "time-core", ] @@ -2996,11 +2949,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.34.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -3009,20 +2961,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.5", "tokio-macros", "windows-sys", ] [[package]] name = "tokio-macros" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", ] [[package]] @@ -3042,7 +2994,7 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.6", + "rustls 0.21.8", "tokio", ] @@ -3059,13 +3011,13 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", - "rustls 0.21.6", + "rustls 0.21.8", "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", @@ -3074,9 +3026,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -3088,14 +3040,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.19.14", + "toml_edit 0.21.0", ] [[package]] @@ -3109,13 +3061,11 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.0", - "serde", - "serde_spanned", + "indexmap 2.1.0", "toml_datetime", "winnow", ] @@ -3126,7 +3076,20 @@ version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ - "indexmap 2.0.0", + "indexmap 2.1.0", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap 2.1.0", + "serde", + "serde_spanned", "toml_datetime", "winnow", ] @@ -3139,20 +3102,19 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] @@ -3176,7 +3138,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.21.6", + "rustls 0.21.8", "sha1", "thiserror", "url", @@ -3185,9 +3147,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" @@ -3197,9 +3159,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" @@ -3212,9 +3174,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "untrusted" @@ -3230,9 +3192,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -3247,9 +3209,9 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "uuid" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" dependencies = [ "getrandom", "rand", @@ -3257,9 +3219,9 @@ dependencies = [ [[package]] name = "vergen" -version = "8.2.4" +version = "8.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbc5ad0d9d26b2c49a5ab7da76c3e79d3ee37e7821799f8223fcb8f2f391a2e7" +checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" dependencies = [ "anyhow", "rustversion", @@ -3280,9 +3242,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -3305,9 +3267,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3315,24 +3277,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.37" +version = "0.4.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" dependencies = [ "cfg-if", "js-sys", @@ -3342,9 +3304,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3352,28 +3314,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.39", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.87" +version = "0.2.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" +checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" [[package]] name = "web-sys" -version = "0.3.64" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" dependencies = [ "js-sys", "wasm-bindgen", @@ -3391,13 +3353,14 @@ dependencies = [ [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -3418,9 +3381,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -3446,7 +3409,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -3466,17 +3429,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3487,9 +3450,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -3499,9 +3462,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -3511,9 +3474,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -3523,9 +3486,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -3535,9 +3498,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -3547,9 +3510,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -3559,24 +3522,24 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.4" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acaaa1190073b2b101e15083c38ee8ec891b5e05cbee516521e94ec008f61e64" +checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" dependencies = [ "memchr", ] [[package]] name = "zerocopy" -version = "0.6.1" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "332f188cc1bcf1fe1064b8c58d150f497e697f49774aa846f2dc949d9a25f236" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" dependencies = [ "byteorder", "zerocopy-derive", @@ -3584,13 +3547,13 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.3.2" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6505e6815af7de1746a08f69c69606bb45695a17149517680f3b2149713b19a3" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.39", ] [[package]] From 88c8babdf1770e2b9cecf90201f336dbced0aba7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:44:21 +0100 Subject: [PATCH 344/561] gstreamer pipeline constructors don't take the optional name parameter anymore MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/src/audio_backend/gstreamer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e3cc78cf..e2111974 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -47,7 +47,7 @@ impl Open for GstreamerSink { let sample_size = format.size(); let gst_bytes = NUM_CHANNELS as usize * 2048 * sample_size; - let pipeline = gst::Pipeline::new(None); + let pipeline = gst::Pipeline::new(); let appsrc = gst::ElementFactory::make("appsrc") .build() .expect("Failed to create GStreamer appsrc element") From 3bec1eab0e95b6d4bfc26164f4caf2bdb85cd956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Tue, 14 Nov 2023 23:52:23 +0100 Subject: [PATCH 345/561] No redundant_locals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- connect/src/spirc.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index be0a8b80..e7880469 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1382,7 +1382,6 @@ impl SpircTask { // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. - let state = state; if state.repeat() { self.state.set_repeat(true); } From 60f6451c2eca84db9ba3e2404880f6e012b57bb4 Mon Sep 17 00:00:00 2001 From: eladyn Date: Sat, 11 Nov 2023 20:11:37 +0100 Subject: [PATCH 346/561] fix clippy errors --- connect/src/spirc.rs | 1 - core/src/spclient.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index be0a8b80..e7880469 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1382,7 +1382,6 @@ impl SpircTask { // has_shuffle/repeat seem to always be true in these replace msgs, // but to replicate the behaviour of the Android client we have to // ignore false values. - let state = state; if state.repeat() { self.state.set_repeat(true); } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index d6c5ffb1..fbb8ddc5 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -392,7 +392,7 @@ impl SpClient { ) -> SpClientResult { let body = protobuf::text_format::print_to_string(message); - let mut headers = headers.unwrap_or_else(HeaderMap::new); + let mut headers = headers.unwrap_or_default(); headers.insert( CONTENT_TYPE, HeaderValue::from_static("application/x-protobuf"), @@ -409,7 +409,7 @@ impl SpClient { headers: Option, body: Option<&str>, ) -> SpClientResult { - let mut headers = headers.unwrap_or_else(HeaderMap::new); + let mut headers = headers.unwrap_or_default(); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); self.request(method, endpoint, Some(headers), body).await From 9541e8820f6767a7f74d34deb15af931c41fca81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-R=C3=A9my=20Falleri?= Date: Wed, 15 Nov 2023 08:53:53 +0100 Subject: [PATCH 347/561] feat: do not stop sink in gapless mode. --- playback/src/player.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 16275053..b3541f23 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1910,10 +1910,6 @@ impl PlayerInternal { } } - // We need to load the track - either from scratch or by completing a preload. - // In any case we go into a Loading state to load the track. - self.ensure_sink_stopped(play); - self.send_event(PlayerEvent::Loading { track_id, play_request_id, From 6f496334d2e930eb5e683bd921e53e9ee4690fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Wed, 15 Nov 2023 11:47:56 +0100 Subject: [PATCH 348/561] Add webkpi 0.22.4 to dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 23 +++++++++++++++++------ Cargo.toml | 1 + 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8d89b7d..3f226c00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1032,7 +1032,7 @@ dependencies = [ "tokio", "tokio-rustls 0.22.0", "tower-service", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -1049,7 +1049,7 @@ dependencies = [ "rustls-native-certs 0.5.0", "tokio", "tokio-rustls 0.22.0", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -1349,6 +1349,7 @@ dependencies = [ "thiserror", "tokio", "url", + "webpki 0.22.4", ] [[package]] @@ -2374,9 +2375,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.23" +version = "0.38.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" dependencies = [ "bitflags 2.4.1", "errno", @@ -2395,7 +2396,7 @@ dependencies = [ "log", "ring 0.16.20", "sct 0.6.1", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -2985,7 +2986,7 @@ checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" dependencies = [ "rustls 0.19.1", "tokio", - "webpki", + "webpki 0.21.4", ] [[package]] @@ -3351,6 +3352,16 @@ dependencies = [ "untrusted 0.7.1", ] +[[package]] +name = "webpki" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" +dependencies = [ + "ring 0.17.5", + "untrusted 0.9.0", +] + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 1f380a1c..d59ec91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ sysinfo = { version = "0.29", default-features = false } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" +webpki = "0.22.4" [features] alsa-backend = ["librespot-playback/alsa-backend"] From e6d8efeb2b97d96b42d1343c3baecaefba8f6eb0 Mon Sep 17 00:00:00 2001 From: Kees Hink Date: Sat, 18 Nov 2023 14:06:49 +0100 Subject: [PATCH 349/561] Fix spelling: replace "it's" with "its" where a possessive is meant --- PUBLISHING.md | 2 +- docs/connection.md | 2 +- metadata/src/artist.rs | 2 +- playback/src/audio_backend/alsa.rs | 4 ++-- playback/src/player.rs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/PUBLISHING.md b/PUBLISHING.md index 9d32caad..3859c90a 100644 --- a/PUBLISHING.md +++ b/PUBLISHING.md @@ -4,7 +4,7 @@ Read through this paragraph in its entirety before running anything. -The Bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows from the project root: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script. +The Bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and its corresponding crates. the command should be used as follows from the project root: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script. Make sure that you are are starting from a clean working directory for both `dev` and `master`, completely up to date with remote and all local changes either committed and pushed or stashed. diff --git a/docs/connection.md b/docs/connection.md index e64fac7f..9f5fcc93 100644 --- a/docs/connection.md +++ b/docs/connection.md @@ -31,7 +31,7 @@ The client solves a challenge based on these two packets, and sends it back usin It also computes the shared keys used to encrypt the rest of the communication. ## Login challenge and cipher key computation. -The client starts by computing the DH shared secret using it's private key and the server's public key. +The client starts by computing the DH shared secret using its private key and the server's public key. HMAC-SHA1 is then used to compute the send and receive keys, as well as the login challenge. ``` diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index d3162fc6..b06b22bc 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -91,7 +91,7 @@ impl_deref_wrapped!(AlbumGroup, Albums); /// [Album1], [Album2-relelease, Album2-older-release], [Album3] /// ] /// ``` -/// In most cases only the current variant of each album is needed. A list of every album in it's +/// In most cases only the current variant of each album is needed. A list of every album in its /// current release variant can be obtained by using [`AlbumGroups::current_releases`] #[derive(Debug, Clone, Default)] pub struct AlbumGroups(pub Vec); diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index fada2580..dc0b87c0 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -262,7 +262,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> } } } else { - trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); + trace!("The device's min reported Buffer size was greater than or equal to its max reported Buffer size."); ZERO_FRAMES }; @@ -328,7 +328,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> } } } else { - trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); + trace!("The device's min reported Period size was greater than or equal to its max reported Period size,"); trace!("or the desired min Period size was greater than or equal to the desired max Period size."); ZERO_FRAMES }; diff --git a/playback/src/player.rs b/playback/src/player.rs index b3541f23..457f1896 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -386,7 +386,7 @@ impl NormalisationData { let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs(); warn!( - "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.", + "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at its peak.", factor_db, limiting_db ); } else if factor > threshold_ratio { @@ -395,7 +395,7 @@ impl NormalisationData { + config.normalisation_threshold_dbfs.abs(); info!( - "This track may be subject to {:.2} dB of dynamic limiting at it's peak.", + "This track may be subject to {:.2} dB of dynamic limiting at its peak.", limiting_db ); } From ef1f35ba9dda5f63eb6654387994f4abb3459100 Mon Sep 17 00:00:00 2001 From: Domenico Cerasuolo Date: Sun, 3 Dec 2023 11:56:25 +0100 Subject: [PATCH 350/561] Fix official uris-ids docs outdated links --- core/src/spotify_id.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 2918f568..93b0b8f6 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -100,7 +100,7 @@ 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 + /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base16(src: &str) -> SpotifyIdResult { let mut dst: u128 = 0; @@ -125,7 +125,7 @@ impl SpotifyId { /// /// `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 + /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base62(src: &str) -> SpotifyIdResult { let mut dst: u128 = 0; @@ -168,7 +168,7 @@ impl SpotifyId { /// Note that this should not be used for playlists, which have the form of /// `spotify:playlist:{id}`. /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_uri(src: &str) -> SpotifyIdResult { // Basic: `spotify:{type}:{id}` // Named: `spotify:user:{user}:{type}:{id}` @@ -223,7 +223,7 @@ impl SpotifyId { /// Returns the `SpotifyId` as a [canonically] base62 encoded, `SpotifyId::SIZE_BASE62` (22) /// character long `String`. /// - /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + /// [canonically]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids #[allow(clippy::wrong_self_convention)] pub fn to_base62(&self) -> Result { let mut dst = [0u8; 22]; @@ -280,7 +280,7 @@ impl SpotifyId { /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will /// be encoded as `unknown`. /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids #[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 From 098c056677e254606d3e8d8a1e06ea3c4b238c25 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Wed, 6 Dec 2023 11:58:08 +0000 Subject: [PATCH 351/561] discovery::server: fix startup log --- discovery/src/server.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 21e8091a..ccdbe685 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -269,10 +269,10 @@ impl DiscoveryServer { tokio::spawn(async { let result = server .with_graceful_shutdown(async { - debug!("Shutting down discovery server"); - if close_rx.await.is_ok() { - debug!("unable to close discovery Rx channel completely"); + if let Err(e) = close_rx.await { + debug!("unable to close discovery Rx channel completely: {e}"); } + debug!("Shutting down discovery server"); }) .await; From ba314b63e49ede9a4ee40e1edd281f8532b548f2 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Fri, 1 Dec 2023 16:38:59 +0000 Subject: [PATCH 352/561] Replace the apparently unmaintained hex crate with data-encoding data-encoding was already a transitive dependency via tungstenite --- Cargo.lock | 8 ++++---- Cargo.toml | 2 +- core/Cargo.toml | 2 +- core/src/spclient.rs | 19 +++++++++++-------- src/main.rs | 3 ++- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f226c00..89a26a57 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -389,9 +389,9 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" [[package]] name = "der" @@ -1331,10 +1331,10 @@ dependencies = [ name = "librespot" version = "0.5.0-dev" dependencies = [ + "data-encoding", "env_logger", "futures-util", "getopts", - "hex", "librespot-audio", "librespot-connect", "librespot-core", @@ -1398,13 +1398,13 @@ dependencies = [ "base64 0.21.5", "byteorder", "bytes", + "data-encoding", "dns-sd", "env_logger", "form_urlencoded", "futures-core", "futures-util", "governor", - "hex", "hmac", "http", "httparse", diff --git a/Cargo.toml b/Cargo.toml index d59ec91f..5b6ade9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,10 +50,10 @@ path = "protocol" version = "0.5.0-dev" [dependencies] +data-encoding = "2.5" env_logger = { version = "0.10", default-features = false, features = ["color", "humantime", "auto-color"] } futures-util = { version = "0.3", default_features = false } getopts = "0.2" -hex = "0.4" log = "0.4" rpassword = "7.0" sha1 = "0.10" diff --git a/core/Cargo.toml b/core/Cargo.toml index 374ae75b..c267f48a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,7 +23,6 @@ form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } -hex = "0.4" hmac = "0.12" httparse = "1.7" http = "0.2" @@ -57,6 +56,7 @@ tokio-tungstenite = { version = "*", default-features = false, features = ["rust tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } +data-encoding = "2.5" [build-dependencies] rand = "0.8" diff --git a/core/src/spclient.rs b/core/src/spclient.rs index fbb8ddc5..99e2c1c6 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -6,6 +6,7 @@ use std::{ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; +use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ @@ -279,20 +280,22 @@ impl SpClient { let hash_cash_challenge = challenge.evaluate_hashcash_parameters(); let ctx = vec![]; - let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { - Error::failed_precondition(format!( - "Unable to decode hash cash challenge: {e}" - )) - })?; + let prefix = HEXUPPER_PERMISSIVE + .decode(hash_cash_challenge.prefix.as_bytes()) + .map_err(|e| { + Error::failed_precondition(format!( + "Unable to decode hash cash challenge: {e}" + )) + })?; let length = hash_cash_challenge.length; - let mut suffix = vec![0; 0x10]; + let mut suffix = [0u8; 0x10]; let answer = Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); match answer { Ok(_) => { // the suffix must be in uppercase - let suffix = hex::encode(suffix).to_uppercase(); + let suffix = HEXUPPER_PERMISSIVE.encode(&suffix); let mut answer_message = ClientTokenRequest::new(); answer_message.request_type = @@ -302,7 +305,7 @@ impl SpClient { let challenge_answers = answer_message.mut_challenge_answers(); let mut challenge_answer = ChallengeAnswer::new(); - challenge_answer.mut_hash_cash().suffix = suffix.to_string(); + challenge_answer.mut_hash_cash().suffix = suffix; challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH.into(); diff --git a/src/main.rs b/src/main.rs index 11574d92..a0061719 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +use data_encoding::HEXLOWER; use futures_util::StreamExt; use log::{debug, error, info, trace, warn}; use sha1::{Digest, Sha1}; @@ -39,7 +40,7 @@ mod player_event_handler; use player_event_handler::{run_program_on_sink_events, EventHandler}; fn device_id(name: &str) -> String { - hex::encode(Sha1::digest(name.as_bytes())) + HEXLOWER.encode(&Sha1::digest(name.as_bytes())) } fn usage(program: &str, opts: &getopts::Options) -> String { From 29f3345030b1ef998e6a1a198e5810d43ca9fe3e Mon Sep 17 00:00:00 2001 From: David Sheets Date: Fri, 1 Dec 2023 16:40:00 +0000 Subject: [PATCH 353/561] spclient: improve token request logging --- core/src/spclient.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 99e2c1c6..44c92ba6 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -270,7 +270,10 @@ impl SpClient { match ClientTokenResponseType::from_i32(message.response_type.value()) { // depending on the platform, you're either given a token immediately // or are presented a hash cash challenge to solve first - Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, + Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => { + debug!("Received a granted token"); + break message; + } Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { debug!("Received a hash cash challenge, solving..."); @@ -480,11 +483,14 @@ impl SpClient { HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, ); - if let Ok(client_token) = self.client_token().await { - headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); - } else { - // currently these endpoints seem to work fine without it - warn!("Unable to get client token. Trying to continue without..."); + match self.client_token().await { + Ok(client_token) => { + let _ = headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?); + } + Err(e) => { + // currently these endpoints seem to work fine without it + warn!("Unable to get client token: {e} Trying to continue without...") + } } last_response = self.session().http_client().request_body(request).await; From 0fbd19b521b8294eaef4585faebe0c3e1eb6b7d4 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Wed, 6 Dec 2023 10:51:39 +0000 Subject: [PATCH 354/561] core: make it easier to change declared OS --- core/src/config.rs | 14 ++++++++++---- core/src/spclient.rs | 7 ++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index ada5354b..bd7136f1 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -16,17 +16,17 @@ pub struct SessionConfig { pub autoplay: Option, } -impl Default for SessionConfig { - fn default() -> SessionConfig { +impl SessionConfig { + pub(crate) fn default_for_os(os: &str) -> Self { let device_id = uuid::Uuid::new_v4().as_hyphenated().to_string(); - let client_id = match std::env::consts::OS { + let client_id = match os { "android" => ANDROID_CLIENT_ID, "ios" => IOS_CLIENT_ID, _ => KEYMASTER_CLIENT_ID, } .to_owned(); - SessionConfig { + Self { client_id, device_id, proxy: None, @@ -37,6 +37,12 @@ impl Default for SessionConfig { } } +impl Default for SessionConfig { + fn default() -> Self { + Self::default_for_os(std::env::consts::OS) + } +} + #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum DeviceType { Unknown = 0, diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 44c92ba6..f4b4ebc1 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -190,9 +190,10 @@ impl SpClient { // on macOS and Windows. On Android and iOS we can send a platform-specific client ID and are // then presented with a hash cash challenge. On Linux, we have to pass the old keymaster ID. // We delegate most of this logic to `SessionConfig`. - let client_id = match OS { + let os = OS; + let client_id = match os { "macos" | "windows" => self.session().client_id(), - _ => SessionConfig::default().client_id, + os => SessionConfig::default_for_os(os).client_id, }; client_data.client_id = client_id; @@ -207,7 +208,7 @@ impl SpClient { let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); let kernel_version = sys.kernel_version().unwrap_or_else(|| String::from("0")); - match OS { + match os { "windows" => { let os_version = os_version.parse::().unwrap_or(10.) as i32; let kernel_version = kernel_version.parse::().unwrap_or(21370); From 2bbd27bc5d6cd3e38e00be83cc00f19d8125e6ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 03:52:16 +0000 Subject: [PATCH 355/561] Bump zerocopy from 0.7.26 to 0.7.31 Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.26 to 0.7.31. - [Release notes](https://github.com/google/zerocopy/releases) - [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md) - [Commits](https://github.com/google/zerocopy/compare/v0.7.26...v0.7.31) --- updated-dependencies: - dependency-name: zerocopy dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- Cargo.lock | 8 ++++---- playback/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3f226c00..a922f376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3548,9 +3548,9 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" +checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" dependencies = [ "byteorder", "zerocopy-derive", @@ -3558,9 +3558,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.26" +version = "0.7.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" +checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" dependencies = [ "proc-macro2", "quote", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 230e5f85..74ee9f91 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -28,7 +28,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } shell-words = "1.1" thiserror = "1" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.7.26", features = ["derive"] } +zerocopy = { version = "0.7.31", features = ["derive"] } # Backends alsa = { version = "0.8.1", optional = true } From e175a88f5bc27589c93e97ba33278a9838236920 Mon Sep 17 00:00:00 2001 From: Domenico Cerasuolo Date: Sun, 17 Dec 2023 18:06:15 +0100 Subject: [PATCH 356/561] Make audio fetch parameters tunable This change collects all those audio fetch parameters that were defined as static constants into a dedicated struct, AudioFetchParams. This struct can be read and set statically, allowing a user of the library to modify those parameters without the need to recompile. --- audio/src/fetch/mod.rs | 108 +++++++++++++++------- audio/src/fetch/receive.rs | 29 +++--- audio/src/lib.rs | 3 +- playback/src/decoder/symphonia_decoder.rs | 2 +- playback/src/player.rs | 10 +- 5 files changed, 96 insertions(+), 56 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index e343ee1f..f6071936 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -6,7 +6,7 @@ use std::{ io::{self, Read, Seek, SeekFrom}, sync::{ atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, + Arc, OnceLock, }, time::Duration, }; @@ -55,42 +55,75 @@ 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 = 64 * 1024; +#[derive(Clone)] +pub struct AudioFetchParams { + /// 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 minimum_download_size: usize, -/// 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 = 8 * 1024; + /// 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 minimum_throughput: usize, -/// 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); + /// The ping time that is used for calculations before a ping time was actually measured. + pub initial_ping_time_estimate: Duration, -/// 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); + /// 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 maximum_assumed_ping_time: Duration, -/// 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); + /// 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 read_ahead_before_playback: Duration, -/// 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); + /// 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 read_ahead_during_playback: Duration, -/// 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 * * ` -pub const PREFETCH_THRESHOLD_FACTOR: f32 = 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 * * ` + pub prefetch_threshold_factor: f32, -/// The time we will wait to obtain status updates on downloading. -pub const DOWNLOAD_TIMEOUT: Duration = - Duration::from_secs((MINIMUM_DOWNLOAD_SIZE / MINIMUM_THROUGHPUT) as u64); + /// The time we will wait to obtain status updates on downloading. + pub download_timeout: Duration, +} + +impl Default for AudioFetchParams { + fn default() -> Self { + let minimum_download_size = 64 * 1024; + let minimum_throughput = 8 * 1024; + Self { + minimum_download_size, + minimum_throughput, + initial_ping_time_estimate: Duration::from_millis(500), + maximum_assumed_ping_time: Duration::from_millis(1500), + read_ahead_before_playback: Duration::from_secs(1), + read_ahead_during_playback: Duration::from_secs(5), + prefetch_threshold_factor: 4.0, + download_timeout: Duration::from_secs( + (minimum_download_size / minimum_throughput) as u64, + ), + } + } +} + +static AUDIO_FETCH_PARAMS: OnceLock = OnceLock::new(); + +impl AudioFetchParams { + pub fn set(params: AudioFetchParams) -> Result<(), AudioFetchParams> { + AUDIO_FETCH_PARAMS.set(params) + } + + pub fn get() -> &'static AudioFetchParams { + AUDIO_FETCH_PARAMS.get_or_init(AudioFetchParams::default) + } +} pub enum AudioFile { Cached(fs::File), @@ -183,6 +216,7 @@ impl StreamLoaderController { if let Some(ref shared) = self.stream_shared { let mut download_status = shared.download_status.lock(); + let download_timeout = AudioFetchParams::get().download_timeout; while range.length > download_status @@ -191,7 +225,7 @@ impl StreamLoaderController { { if shared .cond - .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .wait_for(&mut download_status, download_timeout) .timed_out() { return Err(AudioFileError::WaitTimeout.into()); @@ -297,7 +331,7 @@ impl AudioFileShared { if ping_time_ms > 0 { Duration::from_millis(ping_time_ms as u64) } else { - INITIAL_PING_TIME_ESTIMATE + AudioFetchParams::get().initial_ping_time_estimate } } @@ -395,6 +429,8 @@ impl AudioFileStreaming { trace!("Streaming from {}", url); } + let minimum_download_size = AudioFetchParams::get().minimum_download_size; + // 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 @@ -402,7 +438,7 @@ impl AudioFileStreaming { let mut streamer = session .spclient() - .stream_from_cdn(&cdn_url, 0, MINIMUM_DOWNLOAD_SIZE)?; + .stream_from_cdn(&cdn_url, 0, minimum_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 @@ -490,9 +526,10 @@ impl Read for AudioFileStreaming { return Ok(0); } + let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback; let length_to_request = if self.shared.is_download_streaming() { let length_to_request = length - + (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * self.shared.bytes_per_second as f32) + + (read_ahead_during_playback.as_secs_f32() * self.shared.bytes_per_second as f32) as usize; // Due to the read-ahead stuff, we potentially request more than the actual request demanded. @@ -515,11 +552,12 @@ impl Read for AudioFileStreaming { .map_err(|err| io::Error::new(io::ErrorKind::BrokenPipe, err))?; } + let download_timeout = AudioFetchParams::get().download_timeout; while !download_status.downloaded.contains(offset) { if self .shared .cond - .wait_for(&mut download_status, DOWNLOAD_TIMEOUT) + .wait_for(&mut download_status, download_timeout) .timed_out() { return Err(io::Error::new( diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 39ad84c6..0b001113 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -16,9 +16,8 @@ use librespot_core::{http_client::HttpClient, session::Session, Error}; use crate::range_set::{Range, RangeSet}; use super::{ - AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, StreamingRequest, - MAXIMUM_ASSUMED_PING_TIME, MINIMUM_DOWNLOAD_SIZE, MINIMUM_THROUGHPUT, - PREFETCH_THRESHOLD_FACTOR, + AudioFetchParams, AudioFileError, AudioFileResult, AudioFileShared, StreamLoaderCommand, + StreamingRequest, }; struct PartialFileData { @@ -151,6 +150,8 @@ struct AudioFileFetch { file_data_tx: mpsc::UnboundedSender, complete_tx: Option>, network_response_times: Vec, + + params: AudioFetchParams, } // Might be replaced by enum from std once stable @@ -166,8 +167,8 @@ impl AudioFileFetch { } fn download_range(&mut self, offset: usize, mut length: usize) -> AudioFileResult { - if length < MINIMUM_DOWNLOAD_SIZE { - length = MINIMUM_DOWNLOAD_SIZE; + if length < self.params.minimum_download_size { + length = self.params.minimum_download_size; } // If we are in streaming mode (so not seeking) then start downloading as large @@ -258,13 +259,13 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> Result { match data { ReceivedData::Throughput(mut throughput) => { - if throughput < MINIMUM_THROUGHPUT { + if throughput < self.params.minimum_throughput { warn!( "Throughput {} kbps lower than minimum {}, setting to minimum", throughput / 1000, - MINIMUM_THROUGHPUT / 1000, + self.params.minimum_throughput / 1000, ); - throughput = MINIMUM_THROUGHPUT; + throughput = self.params.minimum_throughput; } let old_throughput = self.shared.throughput(); @@ -287,13 +288,13 @@ impl AudioFileFetch { self.shared.set_throughput(avg_throughput); } ReceivedData::ResponseTime(mut response_time) => { - if response_time > MAXIMUM_ASSUMED_PING_TIME { + if response_time > self.params.maximum_assumed_ping_time { warn!( "Time to first byte {} ms exceeds maximum {}, setting to maximum", response_time.as_millis(), - MAXIMUM_ASSUMED_PING_TIME.as_millis() + self.params.maximum_assumed_ping_time.as_millis() ); - response_time = MAXIMUM_ASSUMED_PING_TIME; + response_time = self.params.maximum_assumed_ping_time; } let old_ping_time_ms = self.shared.ping_time().as_millis(); @@ -423,6 +424,8 @@ pub(super) async fn audio_file_fetch( initial_request, )); + let params = AudioFetchParams::get(); + let mut fetch = AudioFileFetch { session: session.clone(), shared, @@ -431,6 +434,8 @@ pub(super) async fn audio_file_fetch( file_data_tx, complete_tx: Some(complete_tx), network_response_times: Vec::with_capacity(3), + + params: params.clone(), }; loop { @@ -472,7 +477,7 @@ pub(super) async fn audio_file_fetch( let throughput = fetch.shared.throughput(); let desired_pending_bytes = max( - (PREFETCH_THRESHOLD_FACTOR + (params.prefetch_threshold_factor * ping_time_seconds * fetch.shared.bytes_per_second as f32) as usize, (ping_time_seconds * throughput as f32) as usize, diff --git a/audio/src/lib.rs b/audio/src/lib.rs index 2a53c361..99c41df2 100644 --- a/audio/src/lib.rs +++ b/audio/src/lib.rs @@ -7,5 +7,4 @@ mod fetch; mod range_set; pub use decrypt::AudioDecrypt; -pub use fetch::{AudioFile, AudioFileError, StreamLoaderController}; -pub use fetch::{MINIMUM_DOWNLOAD_SIZE, READ_AHEAD_BEFORE_PLAYBACK, READ_AHEAD_DURING_PLAYBACK}; +pub use fetch::{AudioFetchParams, AudioFile, AudioFileError, StreamLoaderController}; diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 2bf2517d..b6a8023e 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -36,7 +36,7 @@ impl SymphoniaDecoder { R: MediaSource + 'static, { let mss_opts = MediaSourceStreamOptions { - buffer_len: librespot_audio::MINIMUM_DOWNLOAD_SIZE, + buffer_len: librespot_audio::AudioFetchParams::get().minimum_download_size, }; let mss = MediaSourceStream::new(Box::new(input), mss_opts); diff --git a/playback/src/player.rs b/playback/src/player.rs index 457f1896..b55e1230 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -24,10 +24,7 @@ use symphonia::core::io::MediaSource; use tokio::sync::{mpsc, oneshot}; use crate::{ - audio::{ - AudioDecrypt, AudioFile, StreamLoaderController, READ_AHEAD_BEFORE_PLAYBACK, - READ_AHEAD_DURING_PLAYBACK, - }, + audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController}, audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, @@ -2223,13 +2220,14 @@ impl PlayerInternal { .. } = self.state { + let read_ahead_during_playback = AudioFetchParams::get().read_ahead_during_playback; // Request our read ahead range let request_data_length = - (READ_AHEAD_DURING_PLAYBACK.as_secs_f32() * bytes_per_second as f32) as usize; + (read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize; // 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 = - (READ_AHEAD_BEFORE_PLAYBACK.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_and_wait(request_data_length, wait_for_data_length) From dabb179eee8489fbc1207c2362ca4ba6dba7178d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Jan 2024 16:21:59 +0000 Subject: [PATCH 357/561] Bump h2 from 0.3.21 to 0.3.24 Bumps [h2](https://github.com/hyperium/h2) from 0.3.21 to 0.3.24. - [Release notes](https://github.com/hyperium/h2/releases) - [Changelog](https://github.com/hyperium/h2/blob/v0.3.24/CHANGELOG.md) - [Commits](https://github.com/hyperium/h2/compare/v0.3.21...v0.3.24) --- updated-dependencies: - dependency-name: h2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dffaaf25..a28f9a64 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -852,9 +852,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" +checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" dependencies = [ "bytes", "fnv", @@ -862,7 +862,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap 1.9.3", + "indexmap 2.1.0", "slab", "tokio", "tokio-util", From a2b6a9f4180512c4c90837723dffb8cec8c281b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Jan 2024 10:34:50 +0000 Subject: [PATCH 358/561] Bump actions/cache from 3.3.2 to 4.0.0 Bumps [actions/cache](https://github.com/actions/cache) from 3.3.2 to 4.0.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v3.3.2...v4.0.0) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3f6583b..8f6abed1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: | ~/.cargo/registry/index @@ -178,7 +178,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: | ~/.cargo/registry/index @@ -221,7 +221,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v3.3.2 + uses: actions/cache@v4.0.0 with: path: | ~/.cargo/registry/index From 2da4a8bfdc81b87cfb2daa1af561ad74a0abec8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 21:53:14 +0000 Subject: [PATCH 359/561] Bump shlex from 1.2.0 to 1.3.0 Bumps [shlex](https://github.com/comex/rust-shlex) from 1.2.0 to 1.3.0. - [Changelog](https://github.com/comex/rust-shlex/blob/master/CHANGELOG.md) - [Commits](https://github.com/comex/rust-shlex/commits) --- updated-dependencies: - dependency-name: shlex dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a28f9a64..a8ed2c93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2624,9 +2624,9 @@ checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" [[package]] name = "shlex" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" From 6532264b81762f21d546e1af144e07df02ab3050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Fri, 23 Feb 2024 23:05:41 +0100 Subject: [PATCH 360/561] Update env_logger to 11.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.toml | 2 +- core/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5b6ade9d..33c23447 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ version = "0.5.0-dev" [dependencies] data-encoding = "2.5" -env_logger = { version = "0.10", default-features = false, features = ["color", "humantime", "auto-color"] } +env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } futures-util = { version = "0.3", default_features = false } getopts = "0.2" log = "0.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index c267f48a..82cef71d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -63,7 +63,7 @@ rand = "0.8" vergen = { version = "8", default-features = false, features = ["build", "git", "gitcl"] } [dev-dependencies] -env_logger = "0.10" +env_logger = "0.11.2" tokio = { version = "1", features = ["macros", "parking_lot"] } [features] From 979d9d0aa061968071df535b6e9627ca9860509c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Fri, 23 Feb 2024 23:18:37 +0100 Subject: [PATCH 361/561] Update sysinfo to 0.30.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.toml | 2 +- core/Cargo.toml | 2 +- core/src/http_client.rs | 6 ++---- core/src/spclient.rs | 7 +++---- src/main.rs | 4 ++-- 5 files changed, 9 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 33c23447..0782b67f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ getopts = "0.2" log = "0.4" rpassword = "7.0" sha1 = "0.10" -sysinfo = { version = "0.29", default-features = false } +sysinfo = { version = "0.30.5", default-features = false } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index 82cef71d..99bdd0ed 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -47,7 +47,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.29", default-features = false } +sysinfo = { version = "0.30.5", default-features = false } thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 63f1d7a8..14efdcdf 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -20,7 +20,7 @@ use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use nonzero_ext::nonzero; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use sysinfo::{System, SystemExt}; +use sysinfo::System; use thiserror::Error; use url::Url; @@ -105,9 +105,7 @@ pub struct HttpClient { impl HttpClient { pub fn new(proxy_url: Option<&Url>) -> Self { let zero_str = String::from("0"); - let os_version = System::new() - .os_version() - .unwrap_or_else(|| zero_str.clone()); + let os_version = System::os_version().unwrap_or_else(|| zero_str.clone()); let (spotify_platform, os_version) = match OS { "android" => ("Android", os_version), diff --git a/core/src/spclient.rs b/core/src/spclient.rs index f4b4ebc1..e5550d88 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -17,7 +17,7 @@ use hyper::{ use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; use sha1::{Digest, Sha1}; -use sysinfo::{System, SystemExt}; +use sysinfo::System; use thiserror::Error; use crate::{ @@ -204,9 +204,8 @@ impl SpClient { .platform_specific_data .mut_or_insert_default(); - let sys = System::new(); - let os_version = sys.os_version().unwrap_or_else(|| String::from("0")); - let kernel_version = sys.kernel_version().unwrap_or_else(|| String::from("0")); + let os_version = System::os_version().unwrap_or_else(|| String::from("0")); + let kernel_version = System::kernel_version().unwrap_or_else(|| String::from("0")); match os { "windows" => { diff --git a/src/main.rs b/src/main.rs index a0061719..22c3abec 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use std::{ str::FromStr, time::{Duration, Instant}, }; -use sysinfo::{System, SystemExt}; +use sysinfo::System; use thiserror::Error; use url::Url; @@ -1693,7 +1693,7 @@ async fn main() { Err(e) => { sys.refresh_processes(); - if sys.uptime() <= 1 { + if System::uptime() <= 1 { debug!("Retrying to initialise discovery: {e}"); tokio::time::sleep(DISCOVERY_RETRY_TIMEOUT).await; } else { From d8e7f4f618812f27956cc34c5ad8f17ccd8d8ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 08:30:10 +0100 Subject: [PATCH 362/561] Update MSRV to 1.71.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f6abed1..64b9e492 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.70" # MSRV (Minimum supported rust version) + - "1.71" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -163,7 +163,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.70" # MSRV (Minimum supported rust version) + - "1.71" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -206,7 +206,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.70" # MSRV (Minimum supported rust version) + - "1.71" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e032876..11f0f1ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ https://github.com/librespot-org/librespot configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [core] MSRV is now 1.70 (breaking) +- [core] MSRV is now 1.71 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts From c28cf298585e3e494bc224b1c7805a4cffb606f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:01:09 +0100 Subject: [PATCH 363/561] Update libmdns to 0.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- discovery/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index b30ddd99..22de4402 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -19,7 +19,7 @@ futures-core = "0.3" futures-util = "0.3" hmac = "0.12" hyper = { version = "0.14", features = ["http1", "server", "tcp"] } -libmdns = "0.7" +libmdns = "0.8" log = "0.4" rand = "0.8" serde_json = "1.0" From caf8029953ced7e2e833d1e8f5c7d677b021b831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:08:01 +0100 Subject: [PATCH 364/561] Update alsa to 0.9.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 74ee9f91..17280942 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -31,7 +31,7 @@ tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sy zerocopy = { version = "0.7.31", features = ["derive"] } # Backends -alsa = { version = "0.8.1", optional = true } +alsa = { version = "0.9.0", 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 } From 88e9a9cc1e10de46eb89ff68a47d98d609934763 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:10:07 +0100 Subject: [PATCH 365/561] Update glib to 0.19.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 17280942..30a661bc 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -40,7 +40,7 @@ sdl2 = { version = "0.35", optional = true } gstreamer = { version = "0.21.2", optional = true } gstreamer-app = { version = "0.21.2", optional = true } gstreamer-audio = { version = "0.21.2", optional = true } -glib = { version = "0.18.1", optional = true } +glib = { version = "0.19.2", optional = true } # Rodio dependencies rodio = { version = "0.17.1", optional = true, default-features = false } From 47575092dcb3b06cb4a1be6f04f59b52aaf4e6ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:19:17 +0100 Subject: [PATCH 366/561] Update sdl2 to 0.36 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 30a661bc..d29a3af7 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -36,7 +36,7 @@ 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.11", optional = true } -sdl2 = { version = "0.35", optional = true } +sdl2 = { version = "0.36", optional = true } gstreamer = { version = "0.21.2", optional = true } gstreamer-app = { version = "0.21.2", optional = true } gstreamer-audio = { version = "0.21.2", optional = true } From 856bd5e7504dcd0f9c4cbbfbd14563e39a74e0b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:22:09 +0100 Subject: [PATCH 367/561] Update gstreamer to 0.22 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 6 +++--- playback/src/audio_backend/gstreamer.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index d29a3af7..63245bb0 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -37,9 +37,9 @@ libpulse-binding = { version = "2", optional = true, default-features = f libpulse-simple-binding = { version = "2", optional = true, default-features = false } jack = { version = "0.11", optional = true } sdl2 = { version = "0.36", optional = true } -gstreamer = { version = "0.21.2", optional = true } -gstreamer-app = { version = "0.21.2", optional = true } -gstreamer-audio = { version = "0.21.2", optional = true } +gstreamer = { version = "0.22.1", optional = true } +gstreamer-app = { version = "0.22.0", optional = true } +gstreamer-audio = { version = "0.22.0", optional = true } glib = { version = "0.19.2", optional = true } # Rodio dependencies diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index e2111974..3b82f735 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -60,13 +60,13 @@ impl Open for GstreamerSink { let sink = match device { None => { // no need to dither twice; use librespot dithering instead - gst::parse_bin_from_description( + 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) + Some(ref x) => gst::parse::bin_from_description(x, true) .expect("Failed to create custom GStreamer sink"), }; pipeline From fb264a7b71adbe07f6e845ae8303f449f6644cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 21:47:17 +0100 Subject: [PATCH 368/561] Update zerocopy to 0.7.32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- playback/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 63245bb0..dfb537c8 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -28,7 +28,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } shell-words = "1.1" thiserror = "1" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.7.31", features = ["derive"] } +zerocopy = { version = "0.7.32", features = ["derive"] } # Backends alsa = { version = "0.9.0", optional = true } From 942925862d15e5c851a70dc19b48efaa6001f5ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 24 Feb 2024 22:00:27 +0100 Subject: [PATCH 369/561] Update Cargo.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 1074 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 631 insertions(+), 443 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8ed2c93..03bbd1a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aes" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", @@ -51,14 +51,13 @@ dependencies = [ [[package]] name = "alsa" -version = "0.8.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce34de545ad29bcc00cb1b87a94c132256dcf83aa7eeb9674482568405a6ff0a" +checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.4.1", + "bitflags 2.4.2", "libc", - "nix 0.26.4", ] [[package]] @@ -72,10 +71,58 @@ dependencies = [ ] [[package]] -name = "anyhow" -version = "1.0.75" +name = "anstream" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" [[package]] name = "arrayvec" @@ -85,13 +132,13 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.74" +version = "0.1.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a66537f1bb974b254c98ed142ff995236e81b9d0fe4db0575f46612cb15eb0f9" +checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] @@ -129,9 +176,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.5" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64ct" @@ -141,22 +188,22 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" -version = "0.68.1" +version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "cexpr", "clang-sys", + "itertools", "lazy_static", "lazycell", - "peeking_take_while", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] @@ -167,9 +214,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block-buffer" @@ -182,15 +229,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.14.0" +version = "3.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" [[package]] name = "byteorder" @@ -206,11 +253,10 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "3286b845d0fccbdd15af433f61c5970e711987036cb468f437ff6badd70f4e24" dependencies = [ - "jobserver", "libc", ] @@ -231,9 +277,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.5" +version = "0.15.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03915af431787e6ffdcc74c645077518c6b6e01f80b761e0fbbfa288536311b3" +checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" dependencies = [ "smallvec", "target-lexicon", @@ -257,15 +303,21 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c688fc74432808e3eb684cae8830a86be1d66a2bd58e1f248ed0960a590baf6f" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" dependencies = [ "glob", "libc", - "libloading", + "libloading 0.8.1", ] +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "combine" version = "4.6.6" @@ -278,15 +330,15 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.5" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" [[package]] name = "core-foundation" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -294,9 +346,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "coreaudio-rs" @@ -311,9 +363,9 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.13" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8478e5bdad14dce236b9898ea002eabfa87cbe14f0aa538dbe3b6a4bec4332d" +checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" dependencies = [ "bindgen", ] @@ -341,14 +393,14 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows", + "windows 0.46.0", ] [[package]] name = "cpufeatures" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] @@ -406,9 +458,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" dependencies = [ "powerfmt", ] @@ -437,9 +489,9 @@ dependencies = [ [[package]] name = "either" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encoding_rs" @@ -451,16 +503,26 @@ dependencies = [ ] [[package]] -name = "env_logger" -version = "0.10.1" +name = "env_filter" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95b3f3e67048839cb0d0781f445682a35113da7121f7c949db0e2be96a4fbece" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" dependencies = [ - "humantime", - "is-terminal", "log", "regex", - "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", ] [[package]] @@ -471,12 +533,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -499,18 +561,18 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -523,9 +585,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -533,15 +595,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -550,44 +612,44 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] name = "futures-sink" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-timer" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -622,9 +684,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" dependencies = [ "cfg-if", "libc", @@ -633,30 +695,30 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.0" +version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "gio-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "glib" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58cf801b6f7829fa76db37449ab67c9c98a2b1bf21076d9113225621e61a0fa6" +checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "futures-channel", "futures-core", "futures-executor", @@ -668,30 +730,28 @@ dependencies = [ "gobject-sys", "libc", "memchr", - "once_cell", "smallvec", "thiserror", ] [[package]] name = "glib-macros" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72793962ceece3863c2965d7f10c8786323b17c7adea75a515809fa20ab799a5" +checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8" dependencies = [ "heck", - "proc-macro-crate 2.0.0", - "proc-macro-error", + "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] name = "glib-sys" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4" dependencies = [ "libc", "system-deps", @@ -705,9 +765,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979" dependencies = [ "glib-sys", "libc", @@ -716,9 +776,9 @@ dependencies = [ [[package]] name = "governor" -version = "0.6.0" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "821239e5672ff23e2a7060901fa622950bbd80b649cdaadd78d1c1767ed14eb4" +checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" dependencies = [ "cfg-if", "futures", @@ -726,15 +786,17 @@ dependencies = [ "no-std-compat", "nonzero_ext", "parking_lot", + "portable-atomic", "rand", "smallvec", + "spinning_top", ] [[package]] name = "gstreamer" -version = "0.21.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed97f98d186e63e49079b26af1a1b73e70ab7a2f450eb46a136f2bffc2bf12d5" +checksum = "c6948444004e5ddf3d735d9a3e9968948675c6bceb7f4b56e9dc12d2ff4440c1" dependencies = [ "cfg-if", "futures-channel", @@ -747,19 +809,19 @@ dependencies = [ "muldiv", "num-integer", "num-rational", + "once_cell", "option-operations", "paste", "pin-project-lite", - "pretty-hex", "smallvec", "thiserror", ] [[package]] name = "gstreamer-app" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bc8090a8806193237e7b6531ee429ff6e39686425f5c3eb06dfa75875390fb" +checksum = "50184e88d3462a796a5924fb329839c102b22f9383c1636323fa4ef5255dea92" dependencies = [ "futures-core", "futures-sink", @@ -772,9 +834,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aea07f07a3f17278e6998390ecaea127e476f0af0360c2d83d96e6d3a97fb75e" +checksum = "6771c0939f286fb261525494a0aad29435b37e802284756bab24afe3bbca7476" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -785,9 +847,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d1678eacb7677c1ffdcf220ada416b5fb68e87c33b77319f14bba169fbe3fc" +checksum = "e7529f913cb6cbf1305ebc58ace01391cf9bb5a833810cf6e7c09e9a37d130f2" dependencies = [ "cfg-if", "glib", @@ -795,13 +857,15 @@ dependencies = [ "gstreamer-audio-sys", "gstreamer-base", "libc", + "once_cell", + "smallvec", ] [[package]] name = "gstreamer-audio-sys" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bd94ae8b177377855b38c3d809c686526786cdb771e6d68510509634b955d1" +checksum = "34d92a1e2a915874f70f0a33c3ea4589bc6b66a138b6ec8bb6acedf49bdec2c3" dependencies = [ "glib-sys", "gobject-sys", @@ -813,9 +877,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb150b6904a49052237fede7cc2e6479df6ced5043d95e6af8134bc141a3167f" +checksum = "514c71195b53c7eced4842b66ca9149833e41cf6a1d949e45e2ca4a4fa929850" dependencies = [ "atomic_refcell", "cfg-if", @@ -827,9 +891,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.21.1" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ca701f9078fe115b29b24c80910b577f9cb5b039182f050dbadf5933594b64" +checksum = "286591e0f85bbda1adf9bab6f21d015acd9ca0a4d4acb61da65e3d0487e23c4e" dependencies = [ "glib-sys", "gobject-sys", @@ -840,9 +904,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.21.2" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "564cda782b3e6eed1b81cb4798a06794db56440fb05b422505be689f34ce3bc4" +checksum = "f62af07f6958a82eee1969beac63c1b0dc2d2a4a2594fe62fed147906cdf9664" dependencies = [ "glib-sys", "gobject-sys", @@ -861,8 +925,8 @@ dependencies = [ "futures-core", "futures-sink", "futures-util", - "http", - "indexmap 2.1.0", + "http 0.2.11", + "indexmap 2.2.3", "slab", "tokio", "tokio-util", @@ -877,9 +941,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.2" +version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7192158dbcda357bdec5fb5788eebf8bbac027f3f33e719d29135ae84156" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" @@ -887,10 +951,10 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", "bytes", "headers-core", - "http", + "http 0.2.11", "httpdate", "mime", "sha1", @@ -902,7 +966,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" dependencies = [ - "http", + "http 0.2.11", ] [[package]] @@ -913,9 +977,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" [[package]] name = "hex" @@ -934,11 +998,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.5" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -964,13 +1028,24 @@ dependencies = [ ] [[package]] -name = "http-body" -version = "0.4.5" +name = "http" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" dependencies = [ "bytes", - "http", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.11", "pin-project-lite", ] @@ -994,22 +1069,22 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.27" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http", + "http 0.2.11", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2 0.4.10", + "socket2", "tokio", "tower-service", "tracing", @@ -1025,7 +1100,7 @@ dependencies = [ "bytes", "futures", "headers", - "http", + "http 0.2.11", "hyper", "hyper-rustls 0.22.1", "rustls-native-certs 0.5.0", @@ -1059,10 +1134,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", - "http", + "http 0.2.11", "hyper", "log", - "rustls 0.21.8", + "rustls 0.21.10", "rustls-native-certs 0.6.3", "tokio", "tokio-rustls 0.24.1", @@ -1070,9 +1145,9 @@ dependencies = [ [[package]] name = "idna" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1080,12 +1155,12 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.7.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc0fa01ffc752e9dbc72818cdb072cd028b86be5e09dd04c5a643704fe101a9" +checksum = "5ad1fe622fcc3ccd2bc6d08f7485577535a15af46be880abb7535e5f3a4c322d" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1100,12 +1175,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.1.0" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" dependencies = [ "equivalent", - "hashbrown 0.14.2", + "hashbrown 0.14.3", ] [[package]] @@ -1117,31 +1192,20 @@ dependencies = [ "generic-array", ] -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix", - "windows-sys", -] - [[package]] name = "itertools" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "jack" @@ -1165,7 +1229,7 @@ dependencies = [ "bitflags 1.3.2", "lazy_static", "libc", - "libloading", + "libloading 0.7.4", "log", "pkg-config", ] @@ -1204,20 +1268,11 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" dependencies = [ "wasm-bindgen", ] @@ -1239,9 +1294,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" @@ -1253,6 +1308,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "libloading" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "libm" version = "0.2.8" @@ -1261,9 +1326,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmdns" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b04ae6b56b3b19ade26f0e7e7c1360a1713514f326c5ed0797cf2c109c9e010" +checksum = "8ed6677a7ef3e8d47432fc827d0ebf0ee77c3e3bc4a3e632d9b92433ef86f70c" dependencies = [ "byteorder", "futures-util", @@ -1271,9 +1336,9 @@ dependencies = [ "if-addrs", "log", "multimap", - "nix 0.23.2", + "nix 0.27.1", "rand", - "socket2 0.4.10", + "socket2", "thiserror", "tokio", "winapi", @@ -1395,7 +1460,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.5", + "base64 0.21.7", "byteorder", "bytes", "data-encoding", @@ -1406,7 +1471,7 @@ dependencies = [ "futures-util", "governor", "hmac", - "http", + "http 0.2.11", "httparse", "hyper", "hyper-proxy", @@ -1415,7 +1480,7 @@ dependencies = [ "log", "nonzero_ext", "num-bigint", - "num-derive 0.4.1", + "num-derive 0.4.2", "num-integer", "num-traits", "once_cell", @@ -1447,7 +1512,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.5", + "base64 0.21.7", "cfg-if", "ctr", "dns-sd", @@ -1489,7 +1554,7 @@ dependencies = [ name = "librespot-playback" version = "0.5.0-dev" dependencies = [ - "alsa 0.8.1", + "alsa 0.9.0", "byteorder", "cpal", "futures-util", @@ -1528,9 +1593,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" @@ -1550,9 +1615,9 @@ checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "mach2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0d1830bcd151a6fc4aea1369af235b36c1528fe976b8ff678683c9995eade8" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" dependencies = [ "libc", ] @@ -1565,15 +1630,15 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memoffset" -version = "0.6.5" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1592,22 +1657,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -1618,9 +1683,9 @@ checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "multimap" -version = "0.8.3" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +checksum = "e1a5d38b9b352dbd913288736af36af41c48d61b1a8cd34bcecd727561b7d511" dependencies = [ "serde", ] @@ -1654,19 +1719,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3790c00a0150112de0f4cd161e3d7fc4b2d8a5542ffc35f099a2562aecb35c" -dependencies = [ - "bitflags 1.3.2", - "cc", - "cfg-if", - "libc", - "memoffset", -] - [[package]] name = "nix" version = "0.24.3" @@ -1680,13 +1732,14 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "cfg-if", "libc", + "memoffset", ] [[package]] @@ -1749,6 +1802,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-derive" version = "0.3.3" @@ -1762,30 +1821,29 @@ dependencies = [ [[package]] name = "num-derive" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb77679af88f8b125209d354a202862602672222e7f2313fdd6dc349bad4712" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] name = "num-integer" -version = "0.1.45" +version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ - "autocfg", "num-traits", ] [[package]] name = "num-iter" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" dependencies = [ "autocfg", "num-integer", @@ -1805,9 +1863,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", "libm", @@ -1846,18 +1904,18 @@ dependencies = [ [[package]] name = "num_threads" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] [[package]] name = "object" -version = "0.32.1" +version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ "memchr", ] @@ -1887,18 +1945,18 @@ dependencies = [ [[package]] name = "ogg" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960d0efc0531a452c442c777288f704b300a5f743c04a14eba71f9aabc4897ac" +checksum = "5477016638150530ba21dec7caac835b29ef69b20865751d2973fce6be386cf1" dependencies = [ "byteorder", ] [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" @@ -1957,12 +2015,6 @@ dependencies = [ "hmac", ] -[[package]] -name = "peeking_take_while" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" - [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1974,9 +2026,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" @@ -1985,7 +2037,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.1.0", + "indexmap 2.2.3", ] [[package]] @@ -2023,9 +2075,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" [[package]] name = "portaudio-rs" @@ -2060,17 +2118,11 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" -[[package]] -name = "pretty-hex" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" - [[package]] name = "priority-queue" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff39edfcaec0d64e8d0da38564fad195d2d51b680940295fcc307366e101e61" +checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" dependencies = [ "autocfg", "indexmap 1.9.3", @@ -2088,42 +2140,18 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "2.0.0" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8366a6159044a37876a2b9817124296703c586a5c92e2c53751fa06d8d43e8" +checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" dependencies = [ - "toml_edit 0.20.7", -] - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", + "toml_edit 0.21.1", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -2191,9 +2219,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -2255,9 +2283,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.2" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" dependencies = [ "aho-corasick", "memchr", @@ -2267,9 +2295,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" dependencies = [ "aho-corasick", "memchr", @@ -2299,16 +2327,17 @@ dependencies = [ [[package]] name = "ring" -version = "0.17.5" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb0205304757e5d899b9c2e448b867ffd03ae7f988002e47cd24954391394d0b" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", "getrandom", "libc", "spin 0.9.8", "untrusted 0.9.0", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2328,14 +2357,14 @@ checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" dependencies = [ "libc", "rtoolbox", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] name = "rsa" -version = "0.9.3" +version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ef35bf3e7fe15a53c4ab08a998e42271eab13eb0db224126bc7bc4c4bad96d" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" dependencies = [ "const-oid", "digest", @@ -2358,7 +2387,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2375,15 +2404,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.24" +version = "0.38.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" +checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.4.2", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2401,16 +2430,30 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.8" +version = "0.21.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446e14c5cda4f3f30fe71863c34ec70f5ac79d6087097ad0bb433e1be5edf04c" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" dependencies = [ "log", - "ring 0.17.5", - "rustls-webpki", + "ring 0.17.8", + "rustls-webpki 0.101.7", "sct 0.7.1", ] +[[package]] +name = "rustls" +version = "0.22.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +dependencies = [ + "log", + "ring 0.17.8", + "rustls-pki-types", + "rustls-webpki 0.102.2", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.5.0" @@ -2430,7 +2473,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.0", + "rustls-pki-types", "schannel", "security-framework", ] @@ -2441,16 +2497,43 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" dependencies = [ - "base64 0.21.5", + "base64 0.21.7", ] +[[package]] +name = "rustls-pemfile" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" +dependencies = [ + "base64 0.21.7", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" + [[package]] name = "rustls-webpki" version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.5", + "ring 0.17.8", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +dependencies = [ + "ring 0.17.8", + "rustls-pki-types", "untrusted 0.9.0", ] @@ -2462,9 +2545,9 @@ checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "ryu" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -2477,11 +2560,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2506,15 +2589,15 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.5", + "ring 0.17.8", "untrusted 0.9.0", ] [[package]] name = "sdl2" -version = "0.35.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7959277b623f1fb9e04aea73686c3ca52f01b2145f8ea16f4ff30d8b7623b1a" +checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -2524,9 +2607,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.35.2" +version = "0.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3586be2cf6c0a8099a79a12b4084357aa9b3e0b0d7980e3b67aaf7a9d55f9f0" +checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29" dependencies = [ "cfg-if", "libc", @@ -2558,29 +2641,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.192" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.192" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] name = "serde_json" -version = "1.0.108" +version = "1.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" +checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" dependencies = [ "itoa", "ryu", @@ -2589,9 +2672,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -2658,28 +2741,18 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "socket2" -version = "0.4.10" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "winapi", -] - -[[package]] -name = "socket2" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" -dependencies = [ - "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -2695,10 +2768,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "spki" -version = "0.7.2" +name = "spinning_top" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", "der", @@ -2808,9 +2890,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" dependencies = [ "proc-macro2", "quote", @@ -2819,16 +2901,16 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.29.10" +version = "0.30.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a18d114d420ada3a891e6bc8e96a2023402203296a47cdd65083377dad18ba5" +checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" dependencies = [ "cfg-if", "core-foundation-sys", "libc", "ntapi", "once_cell", - "winapi", + "windows 0.52.0", ] [[package]] @@ -2846,50 +2928,40 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.12" +version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", - "windows-sys", -] - -[[package]] -name = "termcolor" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff1bc3d3f05aff0403e8ac0d92ced918ec05b666a43f83297ccef5bea8a3d449" -dependencies = [ - "winapi-util", + "windows-sys 0.52.0", ] [[package]] name = "thiserror" -version = "1.0.50" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.50" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] @@ -2904,13 +2976,14 @@ dependencies = [ [[package]] name = "time" -version = "0.3.30" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ "deranged", "itoa", "libc", + "num-conv", "num_threads", "powerfmt", "serde", @@ -2926,10 +2999,11 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.15" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" dependencies = [ + "num-conv", "time-core", ] @@ -2950,9 +3024,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.34.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -2962,9 +3036,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.5.5", + "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -2975,7 +3049,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] @@ -2995,7 +3069,18 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" dependencies = [ - "rustls 0.21.8", + "rustls 0.21.10", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.2", + "rustls-pki-types", "tokio", ] @@ -3012,16 +3097,17 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.21.8", - "rustls-native-certs 0.6.3", + "rustls 0.22.2", + "rustls-native-certs 0.7.0", + "rustls-pki-types", "tokio", - "tokio-rustls 0.24.1", + "tokio-rustls 0.25.0", "tungstenite", ] @@ -3041,14 +3127,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.21.0", + "toml_edit 0.22.6", ] [[package]] @@ -3066,33 +3152,33 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" +checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.3", "toml_datetime", - "winnow", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.21.0" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" dependencies = [ - "indexmap 2.1.0", + "indexmap 2.2.3", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.2", ] [[package]] @@ -3122,24 +3208,25 @@ dependencies = [ [[package]] name = "try-lock" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.20.1" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.0.0", "httparse", "log", "rand", - "rustls 0.21.8", + "rustls 0.22.2", + "rustls-pki-types", "sha1", "thiserror", "url", @@ -3154,9 +3241,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" @@ -3166,9 +3253,9 @@ checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.22" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] @@ -3193,9 +3280,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", @@ -3209,10 +3296,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "uuid" -version = "1.5.0" +name = "utf8parse" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "uuid" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" dependencies = [ "getrandom", "rand", @@ -3220,11 +3313,12 @@ dependencies = [ [[package]] name = "vergen" -version = "8.2.6" +version = "8.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1290fd64cc4e7d3c9b07d7f333ce0ce0007253e32870e632624835cc80b83939" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" dependencies = [ "anyhow", + "cfg-if", "rustversion", "time", ] @@ -3268,9 +3362,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3278,24 +3372,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.38" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afec9963e3d0994cac82455b2b3502b81a7f40f9a0d32181f7528d9f4b43e02" +checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" dependencies = [ "cfg-if", "js-sys", @@ -3305,9 +3399,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3315,28 +3409,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" dependencies = [ "js-sys", "wasm-bindgen", @@ -3358,7 +3452,7 @@ version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.5", + "ring 0.17.8", "untrusted 0.9.0", ] @@ -3414,6 +3508,25 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.3", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.3", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -3423,6 +3536,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -3453,6 +3575,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +dependencies = [ + "windows_aarch64_gnullvm 0.52.3", + "windows_aarch64_msvc 0.52.3", + "windows_i686_gnu 0.52.3", + "windows_i686_msvc 0.52.3", + "windows_x86_64_gnu 0.52.3", + "windows_x86_64_gnullvm 0.52.3", + "windows_x86_64_msvc 0.52.3", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -3465,6 +3602,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -3477,6 +3620,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -3489,6 +3638,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -3501,6 +3656,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -3513,6 +3674,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -3525,6 +3692,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -3538,19 +3711,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] -name = "winnow" -version = "0.5.19" +name = "windows_x86_64_msvc" +version = "0.52.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829846f3e3db426d4cee4510841b71a8e58aa2a76b1132579487ae430ccd9c7b" +checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" dependencies = [ "memchr", ] [[package]] name = "zerocopy" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4061bedbb353041c12f413700357bec76df2c7e2ca8e4df8bac24c6bf68e3d" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" dependencies = [ "byteorder", "zerocopy-derive", @@ -3558,17 +3746,17 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.31" +version = "0.7.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3c129550b3e6de3fd0ba67ba5c81818f9805e58b8d7fee80a3a59d2c9fc601a" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.50", ] [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" From 31e42879e2f0b8cc7681f685e9f08e5cf555b9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 26 Feb 2024 13:18:51 +0000 Subject: [PATCH 370/561] Add devcontainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .devcontainer/Dockerfile | 29 +++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 24 ++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..a88d49ae --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 +ARG debian_version=slim-bookworm +ARG rust_version=1.70.0 +FROM rust:${rust_version}-${debian_version} + +ARG DEBIAN_FRONTEND=noninteractive +ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + nano\ + openssh-server \ + # for rust-analyzer vscode plugin + pkg-config \ + # developer dependencies + libunwind-dev \ + libpulse-dev \ + portaudio19-dev \ + libasound2-dev \ + libsdl2-dev \ + gstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libavahi-compat-libdnssd-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN rustup component add rustfmt && \ + rustup component add clippy && \ + cargo install cargo-hack diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..ff6111a3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,24 @@ +{ + "name": "Librespot Devcontainer", + "dockerFile": "Dockerfile", + // Use 'postCreateCommand' to run commands after the container is created. + //"postCreateCommand": "", + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + "settings": { + "dev.containers.copyGitConfig": true + }, + "extensions": [ + "eamodio.gitlens", + "github.vscode-github-actions", + "rust-lang.rust-analyzer" + ] + } + }, + "containerEnv": { + "GIT_EDITOR": "nano" + } + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" + } From 5489bc84c63135f4a38c81339b6a095f6958dce6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 22:02:16 +0000 Subject: [PATCH 371/561] Bump mio from 0.8.9 to 0.8.11 Bumps [mio](https://github.com/tokio-rs/mio) from 0.8.9 to 0.8.11. - [Release notes](https://github.com/tokio-rs/mio/releases) - [Changelog](https://github.com/tokio-rs/mio/blob/master/CHANGELOG.md) - [Commits](https://github.com/tokio-rs/mio/compare/v0.8.9...v0.8.11) --- updated-dependencies: - dependency-name: mio dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8ed2c93..11e1ea70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1601,9 +1601,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.9" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dce281c5e46beae905d4de1870d8b1509a9142b62eedf18b443b011ca8343d0" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", "wasi", From 262fc2e8201dedfbfe1278af566cc78b6fcad94b Mon Sep 17 00:00:00 2001 From: Gabriel Pajot Date: Mon, 11 Mar 2024 10:34:20 +0100 Subject: [PATCH 372/561] fix: working armv6hf docker image for cross-compiling --- contrib/Dockerfile | 8 +-- contrib/cross-compile-armv6hf/Dockerfile | 49 +++++++++++++++++++ contrib/cross-compile-armv6hf/docker-build.sh | 14 ++++++ contrib/docker-build-pi-armv6hf.sh | 17 ------- 4 files changed, 64 insertions(+), 24 deletions(-) create mode 100644 contrib/cross-compile-armv6hf/Dockerfile create mode 100755 contrib/cross-compile-armv6hf/docker-build.sh delete mode 100755 contrib/docker-build-pi-armv6hf.sh diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 8baa36ae..1b420020 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -2,7 +2,7 @@ # Build the docker image from the root of the project with the following command : # $ docker build -t librespot-cross -f contrib/Dockerfile . # -# The resulting image can be used to build librespot for linux x86_64, armhf(with support for armv6hf), armel, mipsel, aarch64 +# The resulting image can be used to build librespot for linux x86_64, armhf, armel, mipsel, aarch64 # $ docker run -v /tmp/librespot-build:/build librespot-cross # # The compiled binaries will be located in /tmp/librespot-build @@ -13,8 +13,6 @@ # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend -# $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh - FROM debian:stretch RUN dpkg --add-architecture arm64 @@ -40,10 +38,6 @@ RUN mkdir /.cargo && \ echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config && \ echo '[target.mipsel-unknown-linux-gnu]\nlinker = "mipsel-linux-gnu-gcc"' >> /.cargo/config -RUN mkdir /build && \ - mkdir /pi-tools && \ - curl -L https://github.com/raspberrypi/tools/archive/648a6eeb1e3c2b40af4eb34d88941ee0edeb3e9a.tar.gz | tar xz --strip-components 1 -C /pi-tools - ENV CARGO_TARGET_DIR /build ENV CARGO_HOME /build/cache ENV PKG_CONFIG_ALLOW_CROSS=1 diff --git a/contrib/cross-compile-armv6hf/Dockerfile b/contrib/cross-compile-armv6hf/Dockerfile new file mode 100644 index 00000000..c7f73f4c --- /dev/null +++ b/contrib/cross-compile-armv6hf/Dockerfile @@ -0,0 +1,49 @@ +# Cross compilation environment for librespot in armv6hf. +# Build the docker image from the root of the project with the following command: +# $ docker build -t librespot-cross-armv6hf -f contrib/cross-compile-armv6hf/Dockerfile . +# +# The resulting image can be used to build librespot for armv6hf: +# $ docker run -v /tmp/librespot-build-armv6hf:/build librespot-cross-armv6hf +# +# The compiled binary will be located in /tmp/librespot-build-armv6hf/arm-unknown-linux-gnueabihf/release/librespot + +FROM --platform=linux/amd64 ubuntu:18.04 + +# Install common packages. +RUN apt-get update +RUN apt-get install -y -qq git curl build-essential libasound2-dev libssl-dev libpulse-dev libdbus-1-dev + +# Install armhf packages. +RUN echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ bionic main" | tee -a /etc/apt/sources.list +RUN apt-get update +RUN apt-get download libasound2:armhf libasound2-dev:armhf libssl-dev:armhf libssl1.1:armhf +RUN mkdir /sysroot && \ + dpkg -x libasound2_*.deb /sysroot/ && \ + dpkg -x libssl-dev*.deb /sysroot/ && \ + dpkg -x libssl1.1*.deb /sysroot/ && \ + dpkg -x libasound2-dev*.deb /sysroot/ + +# Install rust. +RUN curl https://sh.rustup.rs -sSf | sh -s -- -y +ENV PATH="/root/.cargo/bin/:${PATH}" +RUN rustup target add arm-unknown-linux-gnueabihf +RUN mkdir /.cargo && \ + echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config + +# Install Pi tools for armv6. +RUN mkdir /pi && \ + git -C /pi clone --depth=1 https://github.com/raspberrypi/tools.git + +# Build env variables. +ENV CARGO_TARGET_DIR /build +ENV CARGO_HOME /build/cache +ENV PATH="/pi/tools/arm-bcm2708/arm-linux-gnueabihf/bin:${PATH}" +ENV PKG_CONFIG_ALLOW_CROSS=1 +ENV PKG_CONFIG_PATH_arm-unknown-linux-gnueabihf=/usr/lib/arm-linux-gnueabihf/pkgconfig/ +ENV C_INCLUDE_PATH=/sysroot/usr/include +ENV OPENSSL_LIB_DIR=/sysroot/usr/lib/arm-linux-gnueabihf +ENV OPENSSL_INCLUDE_DIR=/sysroot/usr/include/arm-linux-gnueabihf + +ADD . /src +WORKDIR /src +CMD ["/src/contrib/cross-compile-armv6hf/docker-build.sh"] diff --git a/contrib/cross-compile-armv6hf/docker-build.sh b/contrib/cross-compile-armv6hf/docker-build.sh new file mode 100755 index 00000000..ab84175c --- /dev/null +++ b/contrib/cross-compile-armv6hf/docker-build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -eux + +PI1_TOOLS_DIR="/pi/tools/arm-bcm2708/arm-linux-gnueabihf" + +PI1_LIB_DIRS=( + "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/lib" + "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/usr/lib" + "/sysroot/usr/lib/arm-linux-gnueabihf" + "/sysroot/lib/arm-linux-gnueabihf" +) +export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}" + +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" diff --git a/contrib/docker-build-pi-armv6hf.sh b/contrib/docker-build-pi-armv6hf.sh deleted file mode 100755 index 676b84c5..00000000 --- a/contrib/docker-build-pi-armv6hf.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash - -# largerly inspired by https://github.com/Spotifyd/spotifyd/blob/993336f7/.github/workflows/cd.yml#L109 - -set -eux - -# See https://github.com/raspberrypi/tools/commit/5caa7046 -# Since this commit is not (yet) contained in what is downloaded in Dockerfile, we use the target of the symlink directly -PI1_TOOLS_DIR="/pi-tools/arm-bcm2708/arm-rpi-4.9.3-linux-gnueabihf" - -PI1_LIB_DIRS=( - "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/lib" - "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/usr/lib" -) -export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[@]/#/-L}" - -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" From f8b4d264006e91477e6f91aaf189a40205bc073a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Mar 2024 10:57:53 +0000 Subject: [PATCH 373/561] Bump actions/checkout from 4.1.1 to 4.1.2 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.1.2. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.1...v4.1.2) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8f6abed1..60f10907 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From d881f46ce744da452417d513f35dd0d070c16509 Mon Sep 17 00:00:00 2001 From: Antoine C Date: Tue, 19 Mar 2024 22:03:53 +0000 Subject: [PATCH 374/561] fix: change spotify version needed in clientoken to use semantic format --- core/src/version.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/src/version.rs b/core/src/version.rs index e5b4b0b0..332761b2 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -19,6 +19,9 @@ pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); /// The protocol version of the Spotify desktop client. pub const SPOTIFY_VERSION: u64 = 117300517; +/// The semantic version of the Spotify desktop client. +pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.31.1205.g4d59ad7c"; + /// The protocol version of the Spotify mobile app. pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; @@ -28,6 +31,6 @@ pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; pub fn spotify_version() -> String { match std::env::consts::OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), - _ => SPOTIFY_VERSION.to_string(), + _ => SPOTIFY_SEMANTIC_VERSION.to_string(), } } From a692b8cccc464136ded44a5a9dff130cc05254f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 31 Mar 2024 14:27:11 +0000 Subject: [PATCH 375/561] Bump rust version to 1.71 on devcontainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a88d49ae..31a4f0de 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.70.0 +ARG rust_version=1.71.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive From b24fb871aeff0925a2c912027798a6d6bdf47ac9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 31 Mar 2024 14:27:37 +0000 Subject: [PATCH 376/561] Add test.sh to allow to run test suite easily MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- test.sh | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100755 test.sh diff --git a/test.sh b/test.sh new file mode 100755 index 00000000..a3a0cbd5 --- /dev/null +++ b/test.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +# this script runs the tests and checks that also run as part of the`test.yml` github action workflow + +cargo fmt --all -- --check +cargo clippy -p librespot-core --no-default-features +cargo clippy -p librespot-core + +cargo hack clippy --each-feature -p librespot-discovery +cargo hack clippy --each-feature -p librespot-playback +cargo hack clippy --each-feature + +cargo build --workspace --examples +cargo test --workspace +cargo hack --workspace --remove-dev-deps +cargo check -p librespot-core --no-default-features +cargo check -p librespot-core +cargo hack check --each-feature -p librespot-discovery +cargo hack check --each-feature -p librespot-playback +cargo hack check --each-feature From 2cd2346edb71c3353feef005b080968f8f141ccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 31 Mar 2024 11:35:36 +0000 Subject: [PATCH 377/561] Fix map_clone and explicit truncate when file is create MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/session.rs | 8 +------- playback/src/audio_backend/pipe.rs | 1 + 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/core/src/session.rs b/core/src/session.rs index 69125e17..7801d68b 100755 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -513,13 +513,7 @@ impl Session { } pub fn get_user_attribute(&self, key: &str) -> Option { - self.0 - .data - .read() - .user_data - .attributes - .get(key) - .map(Clone::clone) + self.0.data.read().user_data.attributes.get(key).cloned() } fn weak(&self) -> SessionWeak { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index e0e8a77c..e680256d 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -66,6 +66,7 @@ impl Sink for StdoutSink { OpenOptions::new() .write(true) .create(true) + .truncate(true) .open(file) .map_err(|e| StdoutError::OpenFailure { file: file.to_string(), From 7bc7b46b9e02cb738446b071f490a118832ab02f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 31 Mar 2024 14:51:13 +0000 Subject: [PATCH 378/561] Don't remove dev dependencies but instead use --no-dev-deps option of cargo hack which will restore Cargo.toml after run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- test.sh | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test.sh b/test.sh index a3a0cbd5..0d6bb291 100755 --- a/test.sh +++ b/test.sh @@ -14,9 +14,8 @@ cargo hack clippy --each-feature cargo build --workspace --examples cargo test --workspace -cargo hack --workspace --remove-dev-deps cargo check -p librespot-core --no-default-features cargo check -p librespot-core -cargo hack check --each-feature -p librespot-discovery -cargo hack check --each-feature -p librespot-playback -cargo hack check --each-feature +cargo hack check --no-dev-deps --each-feature -p librespot-discovery +cargo hack check --no-dev-deps --each-feature -p librespot-playback +cargo hack check --no-dev-deps --each-feature From 63e249082113469f1c3d28174185d38124b44eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sun, 31 Mar 2024 16:33:28 +0000 Subject: [PATCH 379/561] Fix redundant import checking on beta toolchain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- connect/src/spirc.rs | 3 +-- core/src/cdn_url.rs | 5 +---- core/src/connection/handshake.rs | 2 +- core/src/connection/mod.rs | 2 +- core/src/date.rs | 2 +- core/src/spotify_id.rs | 6 +----- metadata/src/album.rs | 1 - metadata/src/artist.rs | 1 - metadata/src/availability.rs | 1 - metadata/src/episode.rs | 1 - metadata/src/image.rs | 1 - metadata/src/playlist/annotation.rs | 1 - metadata/src/playlist/attribute.rs | 1 - metadata/src/playlist/diff.rs | 2 +- metadata/src/playlist/item.rs | 1 - metadata/src/playlist/list.rs | 1 - metadata/src/playlist/operation.rs | 1 - metadata/src/sale_period.rs | 1 - metadata/src/show.rs | 5 +---- metadata/src/track.rs | 1 - 20 files changed, 8 insertions(+), 31 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e7880469..200c3f83 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryFrom, future::Future, pin::Pin, sync::atomic::{AtomicUsize, Ordering}, @@ -9,7 +8,7 @@ use std::{ use futures_util::{stream::FusedStream, FutureExt, StreamExt}; -use protobuf::{self, Message}; +use protobuf::Message; use rand::prelude::SliceRandom; use thiserror::Error; use tokio::sync::mpsc; diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 417a3c73..5a80528e 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -1,7 +1,4 @@ -use std::{ - convert::TryFrom, - ops::{Deref, DerefMut}, -}; +use std::ops::{Deref, DerefMut}; use protobuf::Message; use thiserror::Error; diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 9f4f39c0..d18f3df1 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -2,7 +2,7 @@ use std::{env::consts::ARCH, io}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac}; -use protobuf::{self, Message}; +use protobuf::Message; use rand::{thread_rng, RngCore}; use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use sha1::{Digest, Sha1}; diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index e7aa1693..4bac6e3e 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -7,7 +7,7 @@ use std::io; use futures_util::{SinkExt, StreamExt}; use num_traits::FromPrimitive; -use protobuf::{self, Message}; +use protobuf::Message; use thiserror::Error; use tokio::net::TcpStream; use tokio_util::codec::Framed; diff --git a/core/src/date.rs b/core/src/date.rs index 5f08d4e8..ddcaf320 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fmt::Debug, ops::Deref}; +use std::{fmt::Debug, ops::Deref}; use time::{ error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime, diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 93b0b8f6..9b2d78fd 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,8 +1,4 @@ -use std::{ - convert::{TryFrom, TryInto}, - fmt, - ops::Deref, -}; +use std::{fmt, ops::Deref}; use thiserror::Error; diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 8b33571e..ede9cc5b 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index b06b22bc..927846a3 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/availability.rs b/metadata/src/availability.rs index 6713da28..7ee84cf1 100644 --- a/metadata/src/availability.rs +++ b/metadata/src/availability.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 8e200802..dcdde70d 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/image.rs b/metadata/src/image.rs index be0137e7..97f1ccb8 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index d73f7b93..a21b9804 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -1,4 +1,3 @@ -use std::convert::TryFrom; use std::fmt::Debug; use protobuf::Message; diff --git a/metadata/src/playlist/attribute.rs b/metadata/src/playlist/attribute.rs index 6da2be83..86a8f6c5 100644 --- a/metadata/src/playlist/attribute.rs +++ b/metadata/src/playlist/attribute.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/playlist/diff.rs b/metadata/src/playlist/diff.rs index c4967d85..d47578c0 100644 --- a/metadata/src/playlist/diff.rs +++ b/metadata/src/playlist/diff.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fmt::Debug}; +use std::fmt::Debug; use super::operation::PlaylistOperations; diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 4fb892c6..8fa5e4bc 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index b4eaf9b6..b81e61e4 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index 5f7c7551..bcdb54b3 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/sale_period.rs b/metadata/src/sale_period.rs index a9ee317c..fa8a1183 100644 --- a/metadata/src/sale_period.rs +++ b/metadata/src/sale_period.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryFrom, fmt::Debug, ops::{Deref, DerefMut}, }; diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 5c86856c..62faa107 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -1,7 +1,4 @@ -use std::{ - convert::{TryFrom, TryInto}, - fmt::Debug, -}; +use std::fmt::Debug; use crate::{ availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images, diff --git a/metadata/src/track.rs b/metadata/src/track.rs index dd945bed..131cb1b3 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -1,5 +1,4 @@ use std::{ - convert::{TryFrom, TryInto}, fmt::Debug, ops::{Deref, DerefMut}, }; From 6bb953432bf631c73ba3576f51ef6815a60d5a96 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:15:55 +0000 Subject: [PATCH 380/561] Bump h2 from 0.3.24 to 0.3.26 Bumps [h2](https://github.com/hyperium/h2) from 0.3.24 to 0.3.26. - [Release notes](https://github.com/hyperium/h2/releases) - [Changelog](https://github.com/hyperium/h2/blob/v0.3.26/CHANGELOG.md) - [Commits](https://github.com/hyperium/h2/compare/v0.3.24...v0.3.26) --- updated-dependencies: - dependency-name: h2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ed359627..52784b2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -916,9 +916,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.24" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", From b2c87597e344038c2ec389d8edde1b20a641d355 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:37:49 +0000 Subject: [PATCH 381/561] Bump actions/checkout from 4.1.2 to 4.1.3 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.2 to 4.1.3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.2...v4.1.3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 096eb8fa..3452a2c6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.2 + uses: actions/checkout@v4.1.3 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From e34f521196eb4fdb8d7086ac82d9b6d8e35bfbea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 27 Apr 2024 10:19:49 +0000 Subject: [PATCH 382/561] Bump actions/checkout from 4.1.3 to 4.1.4 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.3 to 4.1.4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.3...v4.1.4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3452a2c6..5f1e86e1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.3 + uses: actions/checkout@v4.1.4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From a3b6e29520f3ea628ad7229f8221ec539838033e Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Thu, 9 May 2024 11:59:23 +0200 Subject: [PATCH 383/561] lyrics: remove fullscreen_action Looks like it's been removed from the JSON returned by Spotify. --- metadata/src/lyrics.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/metadata/src/lyrics.rs b/metadata/src/lyrics.rs index 35263e2d..4a3095e1 100644 --- a/metadata/src/lyrics.rs +++ b/metadata/src/lyrics.rs @@ -48,7 +48,6 @@ pub struct Colors { #[serde(rename_all = "camelCase")] pub struct LyricsInner { // TODO: 'alternatives' field as an array but I don't know what it's meant for - pub fullscreen_action: String, pub is_dense_typeface: bool, pub is_rtl_language: bool, pub language: String, From 7e3ed6f4f243dd8bfaf605ce17a989904b93a282 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 May 2024 10:29:42 +0000 Subject: [PATCH 384/561] Bump actions/checkout from 4.1.4 to 4.1.5 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.4 to 4.1.5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.4...v4.1.5) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5f1e86e1..025c807e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.4 + uses: actions/checkout@v4.1.5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From 103044cbd352e54214842489cba1d29cc7c7a53a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 11 May 2024 10:35:35 +0000 Subject: [PATCH 385/561] Bump actions/cache from 4.0.0 to 4.0.2 Bumps [actions/cache](https://github.com/actions/cache) from 4.0.0 to 4.0.2. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4.0.0...v4.0.2) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 025c807e..bc31750c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: | ~/.cargo/registry/index @@ -178,7 +178,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: | ~/.cargo/registry/index @@ -221,7 +221,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.0 + uses: actions/cache@v4.0.2 with: path: | ~/.cargo/registry/index From 5fc8d2d3e6d15ad958f9024171930a2710b0e37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 11 May 2024 12:29:25 +0000 Subject: [PATCH 386/561] Add rust env variables to devcontainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .devcontainer/Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 31a4f0de..20e041cf 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,6 +5,9 @@ FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" +ENV RUST_BACKTRACE=1 +ENV RUSTFLAGS=-Dwarnings + RUN apt-get update && \ apt-get install -y --no-install-recommends \ From 0884a0d76a24d2928a829c4971f309f2223b3a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 11 May 2024 18:53:46 +0000 Subject: [PATCH 387/561] Fix assigning_clones MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/session.rs | 14 +++++++------- core/src/spclient.rs | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/core/src/session.rs b/core/src/session.rs index 7801d68b..b8c55d20 100755 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -358,7 +358,7 @@ impl Session { loop { match reader.read_event_into(&mut buf) { Ok(Event::Start(ref element)) => { - current_element = std::str::from_utf8(element)?.to_owned() + std::str::from_utf8(element)?.clone_into(&mut current_element) } Ok(Event::End(_)) => { current_element = String::new(); @@ -428,7 +428,7 @@ impl Session { } pub fn set_client_id(&self, client_id: &str) { - self.0.data.write().client_id = client_id.to_owned(); + client_id.clone_into(&mut self.0.data.write().client_id); } pub fn client_name(&self) -> String { @@ -436,7 +436,7 @@ impl Session { } pub fn set_client_name(&self, client_name: &str) { - self.0.data.write().client_name = client_name.to_owned(); + client_name.clone_into(&mut self.0.data.write().client_name); } pub fn client_brand_name(&self) -> String { @@ -444,7 +444,7 @@ impl Session { } pub fn set_client_brand_name(&self, client_brand_name: &str) { - self.0.data.write().client_brand_name = client_brand_name.to_owned(); + client_brand_name.clone_into(&mut self.0.data.write().client_brand_name); } pub fn client_model_name(&self) -> String { @@ -452,7 +452,7 @@ impl Session { } pub fn set_client_model_name(&self, client_model_name: &str) { - self.0.data.write().client_model_name = client_model_name.to_owned(); + client_model_name.clone_into(&mut self.0.data.write().client_model_name); } pub fn connection_id(&self) -> String { @@ -460,7 +460,7 @@ impl Session { } pub fn set_connection_id(&self, connection_id: &str) { - self.0.data.write().connection_id = connection_id.to_owned(); + connection_id.clone_into(&mut self.0.data.write().connection_id); } pub fn username(&self) -> String { @@ -468,7 +468,7 @@ impl Session { } pub fn set_username(&self, username: &str) { - self.0.data.write().user_data.canonical_username = username.to_owned(); + username.clone_into(&mut self.0.data.write().user_data.canonical_username); } pub fn country(&self) -> String { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index e5550d88..072cf40c 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -239,9 +239,9 @@ impl SpClient { let android_data = platform_data.mut_android(); android_data.android_version = os_version; android_data.api_version = 31; - android_data.device_name = "Pixel".to_owned(); - android_data.model_str = "GF5KQ".to_owned(); - android_data.vendor = "Google".to_owned(); + "Pixel".clone_into(&mut android_data.device_name); + "GF5KQ".clone_into(&mut android_data.model_str); + "Google".clone_into(&mut android_data.vendor); } "macos" => { let macos_data = platform_data.mut_desktop_macos(); From ca035c9d17a7fc9958abe8a951203ca0054063c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 11 May 2024 19:03:06 +0000 Subject: [PATCH 388/561] Set MSRV in Cargo.toml files to 1.71 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.toml | 2 +- audio/Cargo.toml | 2 +- connect/Cargo.toml | 2 +- core/Cargo.toml | 2 +- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- playback/Cargo.toml | 2 +- protocol/Cargo.toml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0782b67f..9b4ae871 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index eaa84313..94b900c7 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-audio" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1e02df84..419a54d6 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-connect" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" diff --git a/core/Cargo.toml b/core/Cargo.toml index 99bdd0ed..2f515480 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-core" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 22de4402..f4414ebd 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-discovery" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 9da0ce42..a35a943f 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-metadata" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index dfb537c8..8d2d36d3 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-playback" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index b86a8225..f0ce2d7c 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-protocol" version = "0.5.0-dev" -rust-version = "1.61" +rust-version = "1.71" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From 99878e0f72184b1f1d0e65f1f32922a30e489da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 11 May 2024 19:13:08 +0000 Subject: [PATCH 389/561] Fix derivable_impls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/config.rs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index bd7136f1..674c5020 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -43,12 +43,13 @@ impl Default for SessionConfig { } } -#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq, Default)] pub enum DeviceType { Unknown = 0, Computer = 1, Tablet = 2, Smartphone = 3, + #[default] Speaker = 4, Tv = 5, Avr = 6, @@ -131,9 +132,3 @@ impl fmt::Display for DeviceType { f.write_str(str) } } - -impl Default for DeviceType { - fn default() -> DeviceType { - DeviceType::Speaker - } -} From 27d3c9e92fd060e6d316b46b9a4c8cdad6f809d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Sat, 11 May 2024 19:53:55 +0000 Subject: [PATCH 390/561] Fix clippy::to_string_trait_impl MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/mercury/types.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 9c7593fe..43b9c5cd 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -49,15 +49,14 @@ impl From for Error { } } -impl ToString for MercuryMethod { - fn to_string(&self) -> String { +impl std::fmt::Display for MercuryMethod { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match *self { - MercuryMethod::Get => "GET", - MercuryMethod::Sub => "SUB", - MercuryMethod::Unsub => "UNSUB", - MercuryMethod::Send => "SEND", + MercuryMethod::Get => write!(f, "GET"), + MercuryMethod::Sub => write!(f, "SUB"), + MercuryMethod::Unsub => write!(f, "UNSUB"), + MercuryMethod::Send => write!(f, "SEND"), } - .to_owned() } } From a58dfcc08317809881c5428ffa73388fe015fc2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 May 2024 10:43:27 +0000 Subject: [PATCH 391/561] Bump actions/checkout from 4.1.5 to 4.1.6 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.5 to 4.1.6. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.5...v4.1.6) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bc31750c..0c01778c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.5 + uses: actions/checkout@v4.1.6 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From ce5e2f2392b5680a40b88b28e88b7c9c5cb0587a Mon Sep 17 00:00:00 2001 From: Domenico Cerasuolo Date: Wed, 3 Jan 2024 11:04:49 +0100 Subject: [PATCH 392/561] Fix SpotifyId base 62 and 16 str decoding A SpotifyId is expected to be a 128 bits integer and can be parsed from a base 62 or 16 string. However the parsing functions only checked the validity of the characters of the string, but not its length. This could result in integer overflows or the parsing of incorrect strings as Spotify ids. This commit add some checks to the length of the input string passed to the parse functions, and also checks for integer overflows in case of base62 encoded strings. --- core/src/spotify_id.rs | 41 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 9b2d78fd..959b84ee 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -98,6 +98,9 @@ impl SpotifyId { /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base16(src: &str) -> SpotifyIdResult { + if src.len() != 32 { + return Err(SpotifyIdError::InvalidId.into()); + } let mut dst: u128 = 0; for c in src.as_bytes() { @@ -123,6 +126,9 @@ impl SpotifyId { /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base62(src: &str) -> SpotifyIdResult { + if src.len() != 22 { + return Err(SpotifyIdError::InvalidId.into()); + } let mut dst: u128 = 0; for c in src.as_bytes() { @@ -133,8 +139,8 @@ impl SpotifyId { _ => return Err(SpotifyIdError::InvalidId.into()), } as u128; - dst *= 62; - dst += p; + dst = dst.checked_mul(62).ok_or(SpotifyIdError::InvalidId)?; + dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?; } Ok(Self { @@ -606,7 +612,7 @@ mod tests { }, ]; - static CONV_INVALID: [ConversionCase; 3] = [ + static CONV_INVALID: [ConversionCase; 5] = [ ConversionCase { id: 0, kind: SpotifyItemType::Unknown, @@ -631,13 +637,40 @@ mod tests { 154, 27, 28, 251, ], }, + ConversionCase { + id: 0, + kind: SpotifyItemType::Unknown, + // Uri too short + uri: "spotify:azb:aRS48xBl0tH", + // too long, should return error but not panic overflow + base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + // too long, should return error but not panic overflow + base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + raw: &[ + // Invalid length. + 154, 27, 28, 251, + ], + }, ConversionCase { id: 0, kind: SpotifyItemType::Unknown, // Uri too short uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", - base62: "....................", + // too short to encode a 128 bits int + base62: "aa", + raw: &[ + // Invalid length. + 154, 27, 28, 251, + ], + }, + ConversionCase { + id: 0, + kind: SpotifyItemType::Unknown, + uri: "cleary invalid uri", + base16: "--------------------", + // too high of a value, this would need a 132 bits int + base62: "ZZZZZZZZZZZZZZZZZZZZZZ", raw: &[ // Invalid length. 154, 27, 28, 251, From 2a8c4bdc011d05c26df90387416b6a4bcb49cee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 27 May 2024 20:10:00 +0000 Subject: [PATCH 393/561] Apply reviewer's suggestion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- core/src/mercury/types.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 43b9c5cd..70fb3f86 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -51,12 +51,13 @@ impl From for Error { impl std::fmt::Display for MercuryMethod { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - MercuryMethod::Get => write!(f, "GET"), - MercuryMethod::Sub => write!(f, "SUB"), - MercuryMethod::Unsub => write!(f, "UNSUB"), - MercuryMethod::Send => write!(f, "SEND"), - } + let s = match *self { + MercuryMethod::Get => "GET", + MercuryMethod::Sub => "SUB", + MercuryMethod::Unsub => "UNSUB", + MercuryMethod::Send => "SEND", + }; + write!(f, "{}", s) } } From 98a97588d3692dc6413bb151f16b7c620179b830 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Thu, 6 Jun 2024 00:47:02 -0600 Subject: [PATCH 394/561] Enable deprecation warnings and address --- audio/Cargo.toml | 2 +- audio/src/fetch/receive.rs | 4 ++-- core/Cargo.toml | 2 +- core/src/http_client.rs | 10 +++------- discovery/Cargo.toml | 2 +- discovery/src/server.rs | 5 ++--- 6 files changed, 10 insertions(+), 15 deletions(-) diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 94b900c7..2740616c 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -19,7 +19,7 @@ bytes = "1" ctr = "0.9" futures-core = "0.3" futures-util = "0.3" -hyper = { version = "0.14", features = ["client"] } +hyper = { version = "0.14", features = ["client", "backports", "deprecated"] } log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 0b001113..c5e2456c 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -7,7 +7,7 @@ use std::{ use bytes::Bytes; use futures_util::StreamExt; -use hyper::StatusCode; +use hyper::{body::HttpBody, StatusCode}; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; @@ -97,7 +97,7 @@ async fn receive_data( } let body = response.into_body(); - let data = match hyper::body::to_bytes(body).await { + let data = match body.collect().await.map(|b| b.to_bytes()) { Ok(bytes) => bytes, Err(e) => break Err(e.into()), }; diff --git a/core/Cargo.toml b/core/Cargo.toml index 2f515480..e6e45f94 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -26,7 +26,7 @@ governor = { version = "0.6", default-features = false, features = ["std", "jitt hmac = "0.12" httparse = "1.7" http = "0.2" -hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] } +hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp", "backports", "deprecated"] } hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } hyper-rustls = { version = "0.24", features = ["http2"] } log = "0.4" diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 14efdcdf..aec65d4d 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -11,9 +11,7 @@ use governor::{ }; use http::{header::HeaderValue, Uri}; use hyper::{ - client::{HttpConnector, ResponseFuture}, - header::USER_AGENT, - Body, Client, HeaderMap, Request, Response, StatusCode, + body::HttpBody, client::{HttpConnector, ResponseFuture}, header::USER_AGENT, Body, Client, HeaderMap, Request, Response, StatusCode }; use hyper_proxy::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; @@ -178,9 +176,7 @@ impl HttpClient { // As correct as that may be technically, we now need all this boilerplate to clone it // ourselves, as any `Request` is moved in the loop. let (parts, body) = req.into_parts(); - let body_as_bytes = hyper::body::to_bytes(body) - .await - .unwrap_or_else(|_| Bytes::new()); + let body_as_bytes = body.collect().await.map(|b| b.to_bytes()).unwrap_or_default(); loop { let mut req = Request::builder() @@ -218,7 +214,7 @@ impl HttpClient { pub async fn request_body(&self, req: Request) -> Result { let response = self.request(req).await?; - Ok(hyper::body::to_bytes(response.into_body()).await?) + Ok(response.into_body().collect().await?.to_bytes()) } pub fn request_stream(&self, req: Request) -> Result, Error> { diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index f4414ebd..396a3f1c 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -18,7 +18,7 @@ form_urlencoded = "1.0" futures-core = "0.3" futures-util = "0.3" hmac = "0.12" -hyper = { version = "0.14", features = ["http1", "server", "tcp"] } +hyper = { version = "0.14", features = ["http1", "server", "tcp", "backports", "deprecated"] } libmdns = "0.8" log = "0.4" rand = "0.8" diff --git a/discovery/src/server.rs b/discovery/src/server.rs index ccdbe685..b1fe6ae0 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -15,8 +15,7 @@ use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; use hyper::{ - service::{make_service_fn, service_fn}, - Body, Method, Request, Response, StatusCode, + body::HttpBody, service::{make_service_fn, service_fn}, Body, Method, Request, Response, StatusCode }; use log::{debug, error, warn}; @@ -219,7 +218,7 @@ impl RequestHandler { debug!("{:?} {:?} {:?}", parts.method, parts.uri.path(), params); } - let body = hyper::body::to_bytes(body).await?; + let body = body.collect().await?.to_bytes(); params.extend(form_urlencoded::parse(&body)); From 0a7a874ca0bb6e6b27904268ded4f1c9609e2003 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Thu, 6 Jun 2024 01:38:46 -0600 Subject: [PATCH 395/561] Update core to hyper 1.x --- Cargo.lock | 388 +++++++++++++++------------------------- core/Cargo.toml | 10 +- core/src/apresolve.rs | 5 +- core/src/error.rs | 14 +- core/src/http_client.rs | 31 ++-- core/src/spclient.rs | 12 +- 6 files changed, 187 insertions(+), 273 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 52784b2c..6872098e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -138,9 +138,15 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atomic_refcell" version = "0.1.13" @@ -168,12 +174,6 @@ dependencies = [ "rustc-demangle", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.7" @@ -203,7 +203,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -415,15 +415,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ct-logs" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1a816186fa68d9e426e3cb4ae4dff1fcd8e4a2c34b781bf7a822574a0d0aac8" -dependencies = [ - "sct 0.6.1", -] - [[package]] name = "ctr" version = "0.9.2" @@ -624,7 +615,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -744,7 +735,7 @@ dependencies = [ "proc-macro-crate 3.1.0", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -916,16 +907,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" dependencies = [ + "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "futures-util", - "http 0.2.11", + "http", "indexmap 2.2.3", "slab", "tokio", @@ -947,14 +938,14 @@ checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" [[package]] name = "headers" -version = "0.3.9" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http 0.2.11", + "http", "httpdate", "mime", "sha1", @@ -962,11 +953,11 @@ dependencies = [ [[package]] name = "headers-core" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 0.2.11", + "http", ] [[package]] @@ -1016,17 +1007,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "http" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.0.0" @@ -1040,12 +1020,24 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.6" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 0.2.11", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", "pin-project-lite", ] @@ -1069,78 +1061,83 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.28" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" dependencies = [ "bytes", "futures-channel", - "futures-core", "futures-util", "h2", - "http 0.2.11", + "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", - "socket2", + "smallvec", "tokio", - "tower-service", - "tracing", "want", ] [[package]] -name = "hyper-proxy" -version = "0.9.1" +name = "hyper-proxy2" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca815a891b24fdfb243fa3239c86154392b0953ee584aa1a2a1f66d20cbe75cc" +checksum = "9043b7b23fb0bc4a1c7014c27b50a4fc42cc76206f71d34fc0dfe5b28ddc3faf" dependencies = [ "bytes", - "futures", + "futures-util", "headers", - "http 0.2.11", + "http", "hyper", - "hyper-rustls 0.22.1", - "rustls-native-certs 0.5.0", + "hyper-rustls", + "hyper-util", + "pin-project-lite", + "rustls-native-certs", "tokio", - "tokio-rustls 0.22.0", + "tokio-rustls", "tower-service", - "webpki 0.21.4", + "webpki", ] [[package]] name = "hyper-rustls" -version = "0.22.1" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ - "ct-logs", "futures-util", + "http", "hyper", + "hyper-util", "log", - "rustls 0.19.1", - "rustls-native-certs 0.5.0", + "rustls", + "rustls-native-certs", + "rustls-pki-types", "tokio", - "tokio-rustls 0.22.0", - "webpki 0.21.4", + "tokio-rustls", + "tower-service", ] [[package]] -name = "hyper-rustls" -version = "0.24.2" +name = "hyper-util" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" dependencies = [ + "bytes", + "futures-channel", "futures-util", - "http 0.2.11", + "http", + "http-body", "hyper", - "log", - "rustls 0.21.10", - "rustls-native-certs 0.6.3", + "pin-project-lite", + "socket2", "tokio", - "tokio-rustls 0.24.1", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -1414,7 +1411,7 @@ dependencies = [ "thiserror", "tokio", "url", - "webpki 0.22.4", + "webpki", ] [[package]] @@ -1428,6 +1425,7 @@ dependencies = [ "futures-core", "futures-util", "hyper", + "hyper-util", "librespot-core", "log", "parking_lot", @@ -1471,11 +1469,13 @@ dependencies = [ "futures-util", "governor", "hmac", - "http 0.2.11", + "http", + "http-body-util", "httparse", "hyper", - "hyper-proxy", - "hyper-rustls 0.24.2", + "hyper-proxy2", + "hyper-rustls", + "hyper-util", "librespot-protocol", "log", "nonzero_ext", @@ -1523,6 +1523,7 @@ dependencies = [ "hex", "hmac", "hyper", + "hyper-util", "libmdns", "librespot-core", "log", @@ -1827,7 +1828,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -2040,6 +2041,26 @@ dependencies = [ "indexmap 2.2.3", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2149,9 +2170,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.78" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" +checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" dependencies = [ "unicode-ident", ] @@ -2310,21 +2331,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" -[[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 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.8" @@ -2336,7 +2342,7 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -2415,31 +2421,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64 0.13.1", - "log", - "ring 0.16.20", - "sct 0.6.1", - "webpki 0.21.4", -] - -[[package]] -name = "rustls" -version = "0.21.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" -dependencies = [ - "log", - "ring 0.17.8", - "rustls-webpki 0.101.7", - "sct 0.7.1", -] - [[package]] name = "rustls" version = "0.22.2" @@ -2447,37 +2428,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.2", + "rustls-webpki", "subtle", "zeroize", ] -[[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 0.19.1", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework", -] - [[package]] name = "rustls-native-certs" version = "0.7.0" @@ -2485,21 +2442,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.0", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework", ] -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", -] - [[package]] name = "rustls-pemfile" version = "2.1.0" @@ -2516,25 +2464,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "rustls-webpki" version = "0.102.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -2573,26 +2511,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - [[package]] name = "sdl2" version = "0.36.0" @@ -2656,7 +2574,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -2890,9 +2808,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.50" +version = "2.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" +checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" dependencies = [ "proc-macro2", "quote", @@ -2961,7 +2879,7 @@ checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] @@ -3049,28 +2967,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", -] - -[[package]] -name = "tokio-rustls" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" -dependencies = [ - "rustls 0.19.1", - "tokio", - "webpki 0.21.4", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.10", - "tokio", + "syn 2.0.66", ] [[package]] @@ -3079,7 +2976,7 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls 0.22.2", + "rustls", "rustls-pki-types", "tokio", ] @@ -3103,11 +3000,11 @@ checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" dependencies = [ "futures-util", "log", - "rustls 0.22.2", - "rustls-native-certs 0.7.0", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.25.0", + "tokio-rustls", "tungstenite", ] @@ -3181,6 +3078,27 @@ dependencies = [ "winnow 0.6.2", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -3221,11 +3139,11 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.0.0", + "http", "httparse", "log", "rand", - "rustls 0.22.2", + "rustls", "rustls-pki-types", "sha1", "thiserror", @@ -3266,12 +3184,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -3381,7 +3293,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", "wasm-bindgen-shared", ] @@ -3415,7 +3327,7 @@ checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3436,24 +3348,14 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - [[package]] name = "webpki" version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -3752,7 +3654,7 @@ checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.50", + "syn 2.0.66", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index e6e45f94..c9fb2b02 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,10 +25,12 @@ futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstab governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } hmac = "0.12" httparse = "1.7" -http = "0.2" -hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp", "backports", "deprecated"] } -hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.24", features = ["http2"] } +http = "1.0" +hyper = { version = "1.3", features = ["http1", "http2"] } +hyper-util = { version = "0.1", features = ["client"] } +http-body-util = "0.1.1" +hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } +hyper-rustls = { version = "0.26", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" num-bigint = { version = "0.4", features = ["rand"] } diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 8eb1c4eb..37e4ba3d 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -1,6 +1,7 @@ use std::collections::VecDeque; -use hyper::{Body, Method, Request}; +use bytes::Bytes; +use hyper::{Method, Request}; use serde::Deserialize; use crate::Error; @@ -85,7 +86,7 @@ impl ApResolver { let req = Request::builder() .method(Method::GET) .uri("https://apresolve.spotify.com/?type=accesspoint&type=dealer&type=spclient") - .body(Body::empty())?; + .body(Bytes::new())?; 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/error.rs b/core/src/error.rs index ca58e779..a5273479 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -329,10 +329,6 @@ impl From for Error { 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); } @@ -349,6 +345,16 @@ impl From for Error { } } +impl From for Error { + fn from(err: hyper_util::client::legacy::Error) -> Self { + if err.is_connect() { + return Self::new(ErrorKind::Unavailable, err); + } + + Self::new(ErrorKind::Unknown, err) + } +} + impl From for Error { fn from(err: time::error::Parse) -> Self { Self::new(ErrorKind::FailedPrecondition, err) diff --git a/core/src/http_client.rs b/core/src/http_client.rs index aec65d4d..362bf453 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -10,11 +10,13 @@ use governor::{ clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, Quota, RateLimiter, }; use http::{header::HeaderValue, Uri}; +use http_body_util::{BodyExt, Full}; use hyper::{ - body::HttpBody, client::{HttpConnector, ResponseFuture}, header::USER_AGENT, Body, Client, HeaderMap, Request, Response, StatusCode + body::Incoming, header::USER_AGENT, HeaderMap, Request, Response, StatusCode }; -use hyper_proxy::{Intercept, Proxy, ProxyConnector}; +use hyper_proxy2::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +use hyper_util::{client::legacy::{connect::HttpConnector, Client, ResponseFuture}, rt::TokioExecutor}; use nonzero_ext::nonzero; use once_cell::sync::OnceCell; use parking_lot::Mutex; @@ -87,7 +89,7 @@ impl From for Error { } } -type HyperClient = Client>, Body>; +type HyperClient = Client>, Full>; pub struct HttpClient { user_agent: HeaderValue, @@ -144,7 +146,7 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process let https_connector = HttpsConnectorBuilder::new() - .with_native_roots() + .with_native_roots()? .https_or_http() .enable_http1() .enable_http2() @@ -158,7 +160,7 @@ impl HttpClient { }; let proxy_connector = ProxyConnector::from_proxy(https_connector, proxy)?; - let client = Client::builder() + let client = Client::builder(TokioExecutor::new()) .http2_adaptive_window(true) .build(proxy_connector); Ok(client) @@ -169,21 +171,20 @@ impl HttpClient { .get_or_try_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref())) } - pub async fn request(&self, req: Request) -> Result, Error> { + pub async fn request(&self, req: Request) -> Result, Error> { debug!("Requesting {}", req.uri().to_string()); // `Request` does not implement `Clone` because its `Body` may be a single-shot stream. // As correct as that may be technically, we now need all this boilerplate to clone it // ourselves, as any `Request` is moved in the loop. - let (parts, body) = req.into_parts(); - let body_as_bytes = body.collect().await.map(|b| b.to_bytes()).unwrap_or_default(); + let (parts, body_as_bytes) = req.into_parts(); loop { let mut req = Request::builder() .method(parts.method.clone()) .uri(parts.uri.clone()) .version(parts.version) - .body(Body::from(body_as_bytes.clone()))?; + .body(body_as_bytes.clone())?; *req.headers_mut() = parts.headers.clone(); let request = self.request_fut(req)?; @@ -208,20 +209,21 @@ impl HttpClient { } } - return Ok(response?); + let response = response?; + return 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?; Ok(response.into_body().collect().await?.to_bytes()) } - pub fn request_stream(&self, req: Request) -> Result, Error> { + 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 headers_mut = req.headers_mut(); headers_mut.insert(USER_AGENT, self.user_agent.clone()); @@ -247,7 +249,8 @@ impl HttpClient { )) })?; - Ok(self.hyper_client()?.request(req)) + + Ok(self.hyper_client()?.request(req.map(Full::new))) } pub fn get_retry_after(headers: &HeaderMap) -> Option { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 072cf40c..5a41d148 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -10,10 +10,10 @@ use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; use http::header::HeaderValue; use hyper::{ - client::ResponseFuture, header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, - Body, HeaderMap, Method, Request, + HeaderMap, Method, Request, }; +use hyper_util::client::legacy::ResponseFuture; use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; use sha1::{Digest, Sha1}; @@ -156,7 +156,7 @@ impl SpClient { .method(&Method::POST) .uri("https://clienttoken.spotify.com/v1/clienttoken") .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) - .body(Body::from(body))?; + .body(body.into())?; self.session().http_client().request_body(request).await } @@ -465,7 +465,7 @@ impl SpClient { let mut request = Request::builder() .method(method) .uri(url) - .body(Body::from(body.to_owned()))?; + .body(body.to_owned().into())?; // Reconnection logic: keep getting (cached) tokens because they might have expired. let token = self @@ -727,7 +727,7 @@ impl SpClient { RANGE, HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?, ) - .body(Body::empty())?; + .body(Bytes::new())?; let stream = self.session().http_client().request_stream(req)?; @@ -738,7 +738,7 @@ impl SpClient { let request = Request::builder() .method(&Method::GET) .uri(url) - .body(Body::empty())?; + .body(Bytes::new())?; self.session().http_client().request_body(request).await } From 6a4053e871db614be4cdc999ebe2418239a05f59 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Thu, 6 Jun 2024 01:41:48 -0600 Subject: [PATCH 396/561] Update audio to hyper 1.x --- Cargo.lock | 1 + audio/Cargo.toml | 4 +++- audio/src/fetch/mod.rs | 5 +++-- audio/src/fetch/receive.rs | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6872098e..f6d8c4ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1424,6 +1424,7 @@ dependencies = [ "ctr", "futures-core", "futures-util", + "http-body-util", "hyper", "hyper-util", "librespot-core", diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 2740616c..9d5ccd8a 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -19,7 +19,9 @@ bytes = "1" ctr = "0.9" futures-core = "0.3" futures-util = "0.3" -hyper = { version = "0.14", features = ["client", "backports", "deprecated"] } +hyper = { version = "1.3", features = [] } +hyper-util = { version = "0.1", features = ["client"] } +http-body-util = "0.1.1" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index f6071936..d1617106 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -12,7 +12,8 @@ use std::{ }; use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; -use hyper::{client::ResponseFuture, header::CONTENT_RANGE, Body, Response, StatusCode}; +use hyper::{body::Incoming, header::CONTENT_RANGE, Response, StatusCode}; +use hyper_util::client::legacy::ResponseFuture; use parking_lot::{Condvar, Mutex}; use tempfile::NamedTempFile; use thiserror::Error; @@ -133,7 +134,7 @@ pub enum AudioFile { #[derive(Debug)] pub struct StreamingRequest { streamer: IntoStream, - initial_response: Option>, + initial_response: Option>, offset: usize, length: usize, } diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index c5e2456c..4023060d 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -7,9 +7,10 @@ use std::{ use bytes::Bytes; use futures_util::StreamExt; -use hyper::{body::HttpBody, StatusCode}; +use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; +use http_body_util::BodyExt; use librespot_core::{http_client::HttpClient, session::Session, Error}; From da0deb1de6ce499f8ebfd4d4d76931efc3d25273 Mon Sep 17 00:00:00 2001 From: George Hahn Date: Thu, 6 Jun 2024 03:05:42 -0600 Subject: [PATCH 397/561] Convert discovery to hyper 1.x --- Cargo.lock | 10 ++-- discovery/Cargo.toml | 5 +- discovery/src/lib.rs | 2 +- discovery/src/server.rs | 119 +++++++++++++++++++++++++--------------- 4 files changed, 86 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f6d8c4ea..1fa25197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -942,7 +942,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", "http", @@ -1459,7 +1459,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.7", + "base64", "byteorder", "bytes", "data-encoding", @@ -1513,7 +1513,8 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64 0.21.7", + "base64", + "bytes", "cfg-if", "ctr", "dns-sd", @@ -1523,6 +1524,7 @@ dependencies = [ "futures-util", "hex", "hmac", + "http-body-util", "hyper", "hyper-util", "libmdns", @@ -2455,7 +2457,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" dependencies = [ - "base64 0.21.7", + "base64", "rustls-pki-types", ] diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 396a3f1c..345859b1 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -11,6 +11,7 @@ edition = "2021" [dependencies] aes = "0.8" base64 = "0.21" +bytes = "1" cfg-if = "1.0" ctr = "0.9" dns-sd = { version = "0.1.3", optional = true } @@ -18,7 +19,9 @@ form_urlencoded = "1.0" futures-core = "0.3" futures-util = "0.3" hmac = "0.12" -hyper = { version = "0.14", features = ["http1", "server", "tcp", "backports", "deprecated"] } +hyper = { version = "1.3", features = ["http1"] } +hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } +http-body-util = "0.1.1" libmdns = "0.8" log = "0.4" rand = "0.8" diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 1764640b..8caeadfb 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -124,7 +124,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 _zeroconf_ip = self.zeroconf_ip; let svc; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index b1fe6ae0..29fcbcff 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -2,7 +2,7 @@ use std::{ borrow::Cow, collections::BTreeMap, convert::Infallible, - net::{Ipv4Addr, SocketAddr}, + net::{Ipv4Addr, SocketAddr, TcpListener}, pin::Pin, sync::Arc, task::{Context, Poll}, @@ -11,13 +11,16 @@ use std::{ use aes::cipher::{KeyIvInit, StreamCipher}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::Engine as _; +use bytes::Bytes; use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; +use http_body_util::{BodyExt, Full}; use hyper::{ - body::HttpBody, service::{make_service_fn, service_fn}, Body, Method, Request, Response, StatusCode + body::Incoming, Method, Request, Response, StatusCode }; +use hyper_util::{rt::TokioIo, server::graceful::GracefulShutdown}; use log::{debug, error, warn}; use serde_json::json; use sha1::{Digest, Sha1}; @@ -62,7 +65,7 @@ impl RequestHandler { (discovery, rx) } - fn handle_get_info(&self) -> Response { + 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 mut active_user = String::new(); @@ -106,11 +109,11 @@ impl RequestHandler { // - "deviceAPI_isGroup": False }) .to_string(); - - Response::new(Body::from(body)) + let body = Bytes::from(body); + Response::new(Full::new(body)) } - fn handle_add_user(&self, params: &Params<'_>) -> Result, Error> { + fn handle_add_user(&self, params: &Params<'_>) -> Result>, Error> { let username_key = "userName"; let username = params .get(username_key) @@ -170,7 +173,8 @@ impl RequestHandler { }); let body = result.to_string(); - return Ok(Response::new(Body::from(body))); + let body = Bytes::from(body); + return Ok(Response::new(Full::new(body))); } let decrypted = { @@ -192,10 +196,11 @@ impl RequestHandler { }); let body = result.to_string(); - Ok(Response::new(Body::from(body))) + let body = Bytes::from(body); + Ok(Response::new(Full::new(body))) } - fn not_found(&self) -> Response { + fn not_found(&self) -> Response> { let mut res = Response::default(); *res.status_mut() = StatusCode::NOT_FOUND; res @@ -203,8 +208,8 @@ impl RequestHandler { async fn handle( self: Arc, - request: Request, - ) -> Result>, Error> { + request: Request, + ) -> Result>>, Error> { let mut params = Params::new(); let (parts, body) = request.into_parts(); @@ -238,52 +243,78 @@ pub struct DiscoveryServer { } impl DiscoveryServer { - pub fn new(config: Config, port: &mut u16) -> Result, Error> { + pub fn new(config: Config, port: &mut u16) -> Result { let (discovery, cred_rx) = RequestHandler::new(config); - let discovery = Arc::new(discovery); - + let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); + 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) - .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 - })) + let listener = match TcpListener::bind(address) { + Ok(listener) => listener, + Err(e) => { + warn!("Discovery server failed to start: {e}"); + return Err(e.into()); } - }); + }; - let server = hyper::Server::try_bind(&address)?.serve(make_service); + listener.set_nonblocking(true)?; + let listener = tokio::net::TcpListener::from_std(listener)?; - *port = server.local_addr().port(); - debug!("Zeroconf server listening on 0.0.0.0:{}", *port); + match listener.local_addr() { + Ok(addr) => { + *port = addr.port(); + debug!("Zeroconf server listening on 0.0.0.0:{}", *port); + } + Err(e) => { + warn!("Discovery server failed to start: {e}"); + return Err(e.into()); + } + } - tokio::spawn(async { - let result = server - .with_graceful_shutdown(async { - if let Err(e) = close_rx.await { - debug!("unable to close discovery Rx channel completely: {e}"); + + tokio::spawn(async move { + let discovery = Arc::new(discovery); + + let server = hyper::server::conn::http1::Builder::new(); + let graceful = GracefulShutdown::new(); + let mut close_rx = std::pin::pin!(close_rx); + loop { + tokio::select! { + Ok((stream, _)) = listener.accept() => { + let io = TokioIo::new(stream); + let discovery = discovery.clone(); + + let svc = hyper::service::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 + }); + + let conn = server.serve_connection(io, svc); + let fut = graceful.watch(conn); + tokio::spawn(async move { + // Errors are logged in the service_fn + let _ = fut.await; + }); } - debug!("Shutting down discovery server"); - }) - .await; - - if let Err(e) = result { - warn!("Discovery server failed: {}", e); + _ = &mut close_rx => { + debug!("Shutting down discovery server"); + break; + } + } } + + graceful.shutdown().await; + debug!("Discovery server stopped"); }); - Ok(Ok(Self { + Ok(Self { cred_rx, _close_tx: close_tx, - })) + }) } } From e1113dd5e2ec2eb803a1e5d18ca2b5d580c5552b Mon Sep 17 00:00:00 2001 From: George Hahn Date: Thu, 6 Jun 2024 21:37:26 -0600 Subject: [PATCH 398/561] cargo fmt --- audio/src/fetch/receive.rs | 2 +- core/src/http_client.rs | 10 +++++----- discovery/src/server.rs | 9 +++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 4023060d..5f92380a 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -7,10 +7,10 @@ use std::{ use bytes::Bytes; use futures_util::StreamExt; +use http_body_util::BodyExt; use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use http_body_util::BodyExt; use librespot_core::{http_client::HttpClient, session::Session, Error}; diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 362bf453..4b500cd6 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -11,12 +11,13 @@ use governor::{ }; use http::{header::HeaderValue, Uri}; use http_body_util::{BodyExt, Full}; -use hyper::{ - body::Incoming, header::USER_AGENT, HeaderMap, Request, Response, StatusCode -}; +use hyper::{body::Incoming, header::USER_AGENT, HeaderMap, Request, Response, StatusCode}; use hyper_proxy2::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; -use hyper_util::{client::legacy::{connect::HttpConnector, Client, ResponseFuture}, rt::TokioExecutor}; +use hyper_util::{ + client::legacy::{connect::HttpConnector, Client, ResponseFuture}, + rt::TokioExecutor, +}; use nonzero_ext::nonzero; use once_cell::sync::OnceCell; use parking_lot::Mutex; @@ -249,7 +250,6 @@ impl HttpClient { )) })?; - Ok(self.hyper_client()?.request(req.map(Full::new))) } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 29fcbcff..6167275d 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -16,9 +16,7 @@ use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; use http_body_util::{BodyExt, Full}; -use hyper::{ - body::Incoming, Method, Request, Response, StatusCode -}; +use hyper::{body::Incoming, Method, Request, Response, StatusCode}; use hyper_util::{rt::TokioIo, server::graceful::GracefulShutdown}; use log::{debug, error, warn}; @@ -246,7 +244,7 @@ impl DiscoveryServer { pub fn new(config: Config, port: &mut u16) -> Result { let (discovery, cred_rx) = RequestHandler::new(config); let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); - + let (close_tx, close_rx) = oneshot::channel(); let listener = match TcpListener::bind(address) { @@ -271,10 +269,9 @@ impl DiscoveryServer { } } - tokio::spawn(async move { let discovery = Arc::new(discovery); - + let server = hyper::server::conn::http1::Builder::new(); let graceful = GracefulShutdown::new(); let mut close_rx = std::pin::pin!(close_rx); From bd5c284790936fd3c3a97575f58bf684313702ba Mon Sep 17 00:00:00 2001 From: George Hahn Date: Fri, 7 Jun 2024 02:08:54 -0600 Subject: [PATCH 399/561] Fix build error when hyper's `server` feature isn't enabled hyper's `server` feature is not always enabled. The `hyper::Error::is_parse_too_large` function isn't included when `server` isn't enabled. Fortunately, it doesn't matter to us: `Error:is_parse` matches `parse_too_large` errors, so removing this check does not change behavior whatsoever. --- core/src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/error.rs b/core/src/error.rs index a5273479..13491a39 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -321,7 +321,7 @@ impl From for Error { 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() { + if err.is_parse() || err.is_parse_status() || err.is_user() { return Self::new(ErrorKind::Internal, err); } From b0d7fa62cf2b0fdf62d19afd8383e424c9a7e41b Mon Sep 17 00:00:00 2001 From: The DT <80153297+thedtvn@users.noreply.github.com> Date: Sun, 9 Jun 2024 14:20:37 +0700 Subject: [PATCH 400/561] Update mod.rs fix tokio-tungstenite with ver 0.23 --- core/src/dealer/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index b4cfec8e..8969f317 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -409,7 +409,7 @@ async fn connect( let stream = socket::connect(host, port, proxy).await?; - let (mut ws_tx, ws_rx) = tokio_tungstenite::client_async_tls(address, stream) + let (mut ws_tx, ws_rx) = tokio_tungstenite::client_async_tls(address.as_str(), stream) .await? .0 .split(); From 799bc38094b07f5ea5a9b9acda0d368b5fb9fe68 Mon Sep 17 00:00:00 2001 From: Dariusz Olszewski Date: Thu, 30 May 2024 22:00:13 +0200 Subject: [PATCH 401/561] Fix building Docker image for cross-compilation: * Debian stretch packages are now available only from archive.debian.org * Use rust 1.71 - MIPS target demoted to tier 3 at 1.72 Signed-off-by: Dariusz Olszewski (cherry picked from commit 3c0d998825cb52b49c14febe3e13ddf8f7c47e91) --- contrib/Dockerfile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 1b420020..4a3b4431 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -15,6 +15,10 @@ FROM debian:stretch +RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list +RUN echo "deb http://archive.debian.org/debian stretch-proposed-updates main" >> /etc/apt/sources.list +RUN echo "deb http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list + RUN dpkg --add-architecture arm64 RUN dpkg --add-architecture armhf RUN dpkg --add-architecture armel @@ -25,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.71 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi From 90625a71d0e8c7844292eaac552c9be4afac82fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 16:31:01 +0000 Subject: [PATCH 402/561] Update base64 to 0.22 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 14 ++++++++++---- core/Cargo.toml | 2 +- discovery/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1fa25197..087eab7d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -180,6 +180,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -942,7 +948,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", "http", @@ -1459,7 +1465,7 @@ name = "librespot-core" version = "0.5.0-dev" dependencies = [ "aes", - "base64", + "base64 0.22.1", "byteorder", "bytes", "data-encoding", @@ -1513,7 +1519,7 @@ name = "librespot-discovery" version = "0.5.0-dev" dependencies = [ "aes", - "base64", + "base64 0.22.1", "bytes", "cfg-if", "ctr", @@ -2457,7 +2463,7 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" dependencies = [ - "base64", + "base64 0.21.7", "rustls-pki-types", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index c9fb2b02..46b8a3f8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -15,7 +15,7 @@ version = "0.5.0-dev" [dependencies] aes = "0.8" -base64 = "0.21" +base64 = "0.22" byteorder = "1.4" bytes = "1" dns-sd = { version = "0.1", optional = true } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 345859b1..72732eff 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [dependencies] aes = "0.8" -base64 = "0.21" +base64 = "0.22" bytes = "1" cfg-if = "1.0" ctr = "0.9" From 8ec9868ad52048edd1459a17c3c43dbf10cbeda2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 17:51:44 +0000 Subject: [PATCH 403/561] Update priority-queue to 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 7 ++++--- core/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 087eab7d..58537f1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2150,12 +2150,13 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "priority-queue" -version = "1.4.0" +version = "2.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0bda9164fe05bc9225752d54aae413343c36f684380005398a6a8fde95fe785" +checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", - "indexmap 1.9.3", + "equivalent", + "indexmap 2.2.3", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 46b8a3f8..f43c4e9c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -40,7 +40,7 @@ num-traits = "0.2" once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } -priority-queue = "1.2" +priority-queue = "2.0" protobuf = "3" quick-xml = { version = "0.31", features = ["serialize"] } rand = "0.8" From 0990143fedc0f9072b9712df9966f3f07a8ef2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 18:06:07 +0000 Subject: [PATCH 404/561] Update quick-xml to 0.32 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 4 ++-- core/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58537f1b..d1a27e65 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2240,9 +2240,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.31.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" dependencies = [ "memchr", "serde", diff --git a/core/Cargo.toml b/core/Cargo.toml index f43c4e9c..af75452a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -42,7 +42,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "2.0" protobuf = "3" -quick-xml = { version = "0.31", features = ["serialize"] } +quick-xml = { version = "0.32", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } From 983777518fd48ffd90b1ea93e1717d02bd953040 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 18:44:06 +0000 Subject: [PATCH 405/561] Update rodio to 0.18.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 178 ++++++++++++++++++-------------------------- playback/Cargo.toml | 2 +- 2 files changed, 73 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d1a27e65..687bc144 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,18 +37,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "alsa" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2562ad8dcf0f789f65c6fdaad8a8a9708ed6b488e649da28c01656ad66b8b47" -dependencies = [ - "alsa-sys", - "bitflags 1.3.2", - "libc", - "nix 0.24.3", -] - [[package]] name = "alsa" version = "0.9.0" @@ -378,28 +366,26 @@ dependencies = [ [[package]] name = "cpal" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d959d90e938c5493000514b446987c07aed46c668faaa7d34d6c7a67b1a578c" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" dependencies = [ - "alsa 0.7.1", + "alsa", "core-foundation-sys", "coreaudio-rs", "dasp_sample", "jack", - "jni 0.19.0", + "jni", "js-sys", "libc", "mach2", "ndk", "ndk-context", "oboe", - "once_cell", - "parking_lot", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "windows 0.46.0", + "windows 0.54.0", ] [[package]] @@ -738,7 +724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8" dependencies = [ "heck", - "proc-macro-crate 3.1.0", + "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.66", @@ -1239,30 +1225,18 @@ dependencies = [ [[package]] name = "jni" -version = "0.19.0" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" -dependencies = [ - "cesu8", - "combine", - "jni-sys", - "log", - "thiserror", - "walkdir", -] - -[[package]] -name = "jni" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" dependencies = [ "cesu8", + "cfg-if", "combine", "jni-sys", "log", "thiserror", "walkdir", + "windows-sys 0.45.0", ] [[package]] @@ -1339,7 +1313,7 @@ dependencies = [ "if-addrs", "log", "multimap", - "nix 0.27.1", + "nix", "rand", "socket2", "thiserror", @@ -1564,7 +1538,7 @@ dependencies = [ name = "librespot-playback" version = "0.5.0-dev" dependencies = [ - "alsa 0.9.0", + "alsa", "byteorder", "cpal", "futures-util", @@ -1702,15 +1676,15 @@ dependencies = [ [[package]] name = "ndk" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.2", "jni-sys", + "log", "ndk-sys", "num_enum", - "raw-window-handle", "thiserror", ] @@ -1722,24 +1696,13 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.4.1+23.1.7779620" +version = "0.5.0+25.2.9519653" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" -dependencies = [ - "bitflags 1.3.2", - "cfg-if", - "libc", -] - [[package]] name = "nix" version = "0.27.1" @@ -1893,23 +1856,23 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.11" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.66", ] [[package]] @@ -1932,23 +1895,23 @@ dependencies = [ [[package]] name = "oboe" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" dependencies = [ - "jni 0.20.0", + "jni", "ndk", "ndk-context", - "num-derive 0.3.3", + "num-derive 0.4.2", "num-traits", "oboe-sys", ] [[package]] name = "oboe-sys" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f44155e7fb718d3cfddcf70690b2b51ac4412f347cd9e4fbe511abe9cd7b5f2" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" dependencies = [ "cc", ] @@ -2159,16 +2122,6 @@ dependencies = [ "indexmap 2.2.3", ] -[[package]] -name = "proc-macro-crate" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" -dependencies = [ - "once_cell", - "toml_edit 0.19.15", -] - [[package]] name = "proc-macro-crate" version = "3.1.0" @@ -2297,12 +2250,6 @@ dependencies = [ "rand", ] -[[package]] -name = "raw-window-handle" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" - [[package]] name = "redox_syscall" version = "0.4.1" @@ -2358,11 +2305,12 @@ dependencies = [ [[package]] name = "rodio" -version = "0.17.3" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b1bb7b48ee48471f55da122c0044fcc7600cfcc85db88240b89cb832935e611" +checksum = "d1fceb9d127d515af1586d8d0cc601e1245bdb0af38e75c865a156290184f5b3" dependencies = [ "cpal", + "thiserror", ] [[package]] @@ -3053,17 +3001,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_edit" -version = "0.19.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" -dependencies = [ - "indexmap 2.2.3", - "toml_datetime", - "winnow 0.5.40", -] - [[package]] name = "toml_edit" version = "0.21.1" @@ -3411,22 +3348,23 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ - "windows-core", + "windows-core 0.52.0", + "windows-targets 0.52.3", +] + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", "windows-targets 0.52.3", ] @@ -3439,6 +3377,34 @@ dependencies = [ "windows-targets 0.52.3", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.3", +] + +[[package]] +name = "windows-result" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +dependencies = [ + "windows-targets 0.52.3", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8d2d36d3..de9a4320 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -43,7 +43,7 @@ gstreamer-audio = { version = "0.22.0", optional = true } glib = { version = "0.19.2", optional = true } # Rodio dependencies -rodio = { version = "0.17.1", optional = true, default-features = false } +rodio = { version = "0.18.1", optional = true, default-features = false } cpal = { version = "0.15.1", optional = true } # Container and audio decoder From b0170d142b51f73047fd7da17cdee6b206ef81b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 18:50:38 +0000 Subject: [PATCH 406/561] Bump MSRV to 1.73 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- .devcontainer/Dockerfile | 2 +- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 2 +- Cargo.toml | 2 +- audio/Cargo.toml | 2 +- connect/Cargo.toml | 2 +- contrib/Dockerfile | 2 +- core/Cargo.toml | 2 +- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- playback/Cargo.toml | 2 +- protocol/Cargo.toml | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 20e041cf..8de63190 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.71.0 +ARG rust_version=1.73.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0c01778c..4056d994 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.71" # MSRV (Minimum supported rust version) + - "1.73" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -163,7 +163,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.71" # MSRV (Minimum supported rust version) + - "1.73" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -206,7 +206,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.71" # MSRV (Minimum supported rust version) + - "1.73" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index 11f0f1ee..6896180c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ https://github.com/librespot-org/librespot configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [core] MSRV is now 1.71 (breaking) +- [core] MSRV is now 1.73 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts diff --git a/Cargo.toml b/Cargo.toml index 9b4ae871..bb213136 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 9d5ccd8a..3610ed4c 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-audio" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 419a54d6..3c3bbe84 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-connect" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 4a3b4431..dc28bc60 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -29,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.71 -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.73 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi diff --git a/core/Cargo.toml b/core/Cargo.toml index af75452a..ea9a906e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-core" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 72732eff..e333a6eb 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-discovery" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index a35a943f..449cd3d5 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-metadata" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index de9a4320..ce60fd29 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-playback" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index f0ce2d7c..7e69971a 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-protocol" version = "0.5.0-dev" -rust-version = "1.71" +rust-version = "1.73" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From 419fd748f4f8b3c196edf997f7dd1b880f41704b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20K=C3=B6nig?= Date: Mon, 10 Jun 2024 19:36:20 +0000 Subject: [PATCH 407/561] Update cargo.lock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Christian König --- Cargo.lock | 1038 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 661 insertions(+), 377 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 687bc144..a677fda2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" dependencies = [ "gimli", ] @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -44,7 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" dependencies = [ "alsa-sys", - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", ] @@ -60,47 +60,48 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.12" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", + "is_terminal_polyfill", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" [[package]] name = "anstyle-parse" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -108,9 +109,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.80" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "arrayvec" @@ -120,9 +121,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-trait" -version = "0.1.77" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -143,15 +144,15 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" dependencies = [ "addr2line", "cc", @@ -186,10 +187,10 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cexpr", "clang-sys", - "itertools", + "itertools 0.12.1", "lazy_static", "lazycell", "proc-macro2", @@ -208,9 +209,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "block-buffer" @@ -223,15 +224,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.3" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.14.3" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" +checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" [[package]] name = "byteorder" @@ -241,17 +242,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cc" -version = "1.0.87" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3286b845d0fccbdd15af433f61c5970e711987036cb468f437ff6badd70f4e24" +checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" dependencies = [ + "jobserver", "libc", + "once_cell", ] [[package]] @@ -271,9 +274,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa50868b64a9a6fda9d593ce778849ea8715cd2a3d2cc17ffdb4a2f2f2f1961d" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", "target-lexicon", @@ -297,26 +300,26 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.7.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.1", + "libloading 0.8.3", ] [[package]] name = "colorchoice" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "combine" -version = "4.6.6" +version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", "memchr", @@ -424,15 +427,15 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" [[package]] name = "der" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", "pem-rfc7468", @@ -460,6 +463,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "dns-sd" version = "0.1.3" @@ -472,15 +486,15 @@ dependencies = [ [[package]] name = "either" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" +checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -497,9 +511,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "anstream", "anstyle", @@ -516,9 +530,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys 0.52.0", @@ -526,9 +540,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "fixedbitset" @@ -667,9 +681,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -678,15 +692,15 @@ dependencies = [ [[package]] name = "gimli" -version = "0.28.1" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" [[package]] name = "gio-sys" -version = "0.19.0" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf8e1d9219bb294636753d307b030c1e8a032062cba74f493c431a5c8b81ce4" +checksum = "d4bdbef451b0f0361e7f762987cc6bebd5facab1d535e85a3cf1115dfb08db40" dependencies = [ "glib-sys", "gobject-sys", @@ -697,11 +711,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.19.2" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab9e86540b5d8402e905ad4ce7d6aa544092131ab564f3102175af176b90a053" +checksum = "e52355166df21c7ed16b6a01f615669c7911ed74e27ef60eba339c0d2da12490" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "futures-channel", "futures-core", "futures-executor", @@ -719,9 +733,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.2" +version = "0.19.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f5897ca27a83e4cdc7b4666850bade0a2e73e17689aabafcc9acddad9d823b8" +checksum = "70025dbfa1275cf7d0531c3317ba6270dae15d87e63342229d638246ff45202e" dependencies = [ "heck", "proc-macro-crate", @@ -732,9 +746,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.19.0" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630f097773d7c7a0bb3258df4e8157b47dc98bbfa0e60ad9ab56174813feced4" +checksum = "767d23ead9bbdfcbb1c2242c155c8128a7d13dde7bf69c176f809546135e2282" dependencies = [ "libc", "system-deps", @@ -748,9 +762,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.19.0" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e2b1080b9418dd0c58b498da3a5c826030343e0ef07bde6a955d28de54979" +checksum = "c3787b0bfacca12bb25f8f822b0dbee9f7e4a86e6469a29976d332d2c14c945b" dependencies = [ "glib-sys", "libc", @@ -777,9 +791,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.22.1" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6948444004e5ddf3d735d9a3e9968948675c6bceb7f4b56e9dc12d2ff4440c1" +checksum = "56b59fdce2dfacda226d4b1b71ce4700b2f04228909b52252c197d8e30bd54a6" dependencies = [ "cfg-if", "futures-channel", @@ -787,7 +801,7 @@ dependencies = [ "futures-util", "glib", "gstreamer-sys", - "itertools", + "itertools 0.13.0", "libc", "muldiv", "num-integer", @@ -817,9 +831,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.22.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6771c0939f286fb261525494a0aad29435b37e802284756bab24afe3bbca7476" +checksum = "5da6c85b35835fa484e1bd554830c6e724c99a7a7fcd0208ff1663c3c6d3c9cd" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -830,9 +844,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.22.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7529f913cb6cbf1305ebc58ace01391cf9bb5a833810cf6e7c09e9a37d130f2" +checksum = "703e65074d4f5dce207e855d8f36697ec93f953e5412780dcffc70cb5483337e" dependencies = [ "cfg-if", "glib", @@ -846,9 +860,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.22.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34d92a1e2a915874f70f0a33c3ea4589bc6b66a138b6ec8bb6acedf49bdec2c3" +checksum = "2dfb1c22539b9b31fde5513fd68e2f0edeeaa5591d92daaaadbfa392736e820f" dependencies = [ "glib-sys", "gobject-sys", @@ -874,9 +888,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.22.0" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "286591e0f85bbda1adf9bab6f21d015acd9ca0a4d4acb61da65e3d0487e23c4e" +checksum = "2d8d11de9d94072657f2e9ca294b72326874a1e53de9f45613a9bf00773a5938" dependencies = [ "glib-sys", "gobject-sys", @@ -887,9 +901,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.22.1" +version = "0.22.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f62af07f6958a82eee1969beac63c1b0dc2d2a4a2594fe62fed147906cdf9664" +checksum = "4975a75279a9cf658bac1798dcf57100c6ec89fca7886572c8250ea4d94b76bd" dependencies = [ "glib-sys", "gobject-sys", @@ -909,7 +923,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.2.3", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -924,9 +938,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" -version = "0.14.3" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "headers" @@ -954,15 +968,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -1001,9 +1015,9 @@ dependencies = [ [[package]] name = "http" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b32afd38673a8016f7c9ae69e5af41a58f81b1d31689040f2f1959594ce194ea" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -1022,12 +1036,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http", "http-body", "pin-project-lite", @@ -1035,9 +1049,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.8.0" +version = "1.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" +checksum = "9f3935c160d00ac752e09787e6e6bfc26494c2183cc922f1bc678a60d4733bc2" [[package]] name = "httpdate" @@ -1088,7 +1102,7 @@ dependencies = [ "pin-project-lite", "rustls-native-certs", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tower-service", "webpki", ] @@ -1104,11 +1118,11 @@ dependencies = [ "hyper", "hyper-util", "log", - "rustls", + "rustls 0.22.4", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.25.0", "tower-service", ] @@ -1133,20 +1147,140 @@ dependencies = [ ] [[package]] -name = "idna" -version = "0.5.0" +name = "icu_collections" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "idna" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +dependencies = [ + "icu_normalizer", + "icu_properties", + "smallvec", + "utf8_iter", ] [[package]] name = "if-addrs" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad1fe622fcc3ccd2bc6d08f7485577535a15af46be880abb7535e5f3a4c322d" +checksum = "624c5448ba529e74f594c65b7024f31b2de7b64a9b228b8df26796bbb6e32c36" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1164,12 +1298,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.2.3" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "233cf39063f058ea2caae4091bf4a3ef70a653afbc026f5c4a4135d114e3c177" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", - "hashbrown 0.14.3", + "hashbrown 0.14.5", ] [[package]] @@ -1181,6 +1315,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + [[package]] name = "itertools" version = "0.12.1" @@ -1191,10 +1331,19 @@ dependencies = [ ] [[package]] -name = "itoa" -version = "1.0.10" +name = "itertools" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jack" @@ -1246,10 +1395,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] -name = "js-sys" -version = "0.3.68" +name = "jobserver" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -1271,9 +1429,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" @@ -1287,12 +1445,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets 0.52.5", ] [[package]] @@ -1577,15 +1735,21 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litemap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" [[package]] name = "lock_api" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" dependencies = [ "autocfg", "scopeguard", @@ -1593,9 +1757,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "mach2" @@ -1614,15 +1778,15 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -1641,9 +1805,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" dependencies = [ "adler", ] @@ -1680,7 +1844,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "jni-sys", "log", "ndk-sys", @@ -1709,7 +1873,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "libc", "memoffset", @@ -1748,11 +1912,10 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" dependencies = [ - "autocfg", "num-integer", "num-traits", "rand", @@ -1814,9 +1977,9 @@ dependencies = [ [[package]] name = "num-iter" -version = "0.1.44" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d869c01cc0c455284163fd0092f1f93835385ccab5a98a0dcc497b2f8bf055a9" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" dependencies = [ "autocfg", "num-integer", @@ -1825,20 +1988,19 @@ dependencies = [ [[package]] name = "num-rational" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ - "autocfg", "num-integer", "num-traits", ] [[package]] name = "num-traits" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", "libm", @@ -1886,9 +2048,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.2" +version = "0.35.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" dependencies = [ "memchr", ] @@ -1948,9 +2110,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -1958,9 +2120,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.9" +version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "backtrace", "cfg-if", @@ -1969,14 +2131,14 @@ dependencies = [ "redox_syscall", "smallvec", "thread-id", - "windows-targets 0.48.5", + "windows-targets 0.52.5", ] [[package]] name = "paste" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] name = "pbkdf2" @@ -2005,28 +2167,28 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "petgraph" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.3", + "indexmap 2.2.6", ] [[package]] name = "pin-project" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.3" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -2035,9 +2197,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2119,7 +2281,7 @@ checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" dependencies = [ "autocfg", "equivalent", - "indexmap 2.2.3", + "indexmap 2.2.6", ] [[package]] @@ -2142,9 +2304,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65f4a8ec18723a734e5dc09c173e0abf9690432da5340285d536edcb4dac190" +checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0" dependencies = [ "once_cell", "protobuf-support", @@ -2153,9 +2315,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e85514a216b1c73111d9032e26cc7a5ecb1bb3d4d9539e91fb72a4395060f78" +checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77" dependencies = [ "anyhow", "once_cell", @@ -2168,9 +2330,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77d6fbd6697c9e531873e81cec565a85e226b99a0f10e1acc079be057fe2fcba" +checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642" dependencies = [ "anyhow", "indexmap 1.9.3", @@ -2184,9 +2346,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6872f4d4f4b98303239a2b5838f5bbbb77b01ffc892d627957f37a22d7cfe69c" +checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7" dependencies = [ "thiserror", ] @@ -2203,9 +2365,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2252,18 +2414,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", ] [[package]] name = "regex" -version = "1.10.3" +version = "1.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" dependencies = [ "aho-corasick", "memchr", @@ -2273,9 +2435,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" dependencies = [ "aho-corasick", "memchr", @@ -2284,9 +2446,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "ring" @@ -2356,9 +2518,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" [[package]] name = "rustc-hash" @@ -2368,11 +2530,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -2381,9 +2543,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.22.2" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e87c9956bd9807afa1f77e0f7594af32566e830e088a5576d27c5b6f30f49d41" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", "ring", @@ -2393,6 +2555,19 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls" +version = "0.23.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + [[package]] name = "rustls-native-certs" version = "0.7.0" @@ -2408,25 +2583,25 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.0" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c333bb734fcdedcea57de1602543590f545f127dc8b533324318fd492c5c70b" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "048a63e5b3ac996d78d402940b5fa47973d2d080c6c6fffa1d0f19c4445310b7" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.2" +version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" +checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ "ring", "rustls-pki-types", @@ -2435,15 +2610,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.14" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" [[package]] name = "ryu" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" [[package]] name = "same-file" @@ -2489,16 +2664,16 @@ checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29" dependencies = [ "cfg-if", "libc", - "version-compare", + "version-compare 0.1.1", ] [[package]] name = "security-framework" -version = "2.9.2" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "core-foundation", "core-foundation-sys", "libc", @@ -2507,9 +2682,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" dependencies = [ "core-foundation-sys", "libc", @@ -2517,18 +2692,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.197" +version = "1.0.203" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", @@ -2537,9 +2712,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ "itoa", "ryu", @@ -2548,9 +2723,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" dependencies = [ "serde", ] @@ -2589,9 +2764,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" dependencies = [ "libc", ] @@ -2617,15 +2792,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2662,6 +2837,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "subtle" version = "2.5.0" @@ -2670,9 +2851,9 @@ checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "symphonia" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e48dba70095f265fdb269b99619b95d04c89e619538138383e63310b14d941" +checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9" dependencies = [ "lazy_static", "symphonia-bundle-mp3", @@ -2684,11 +2865,10 @@ dependencies = [ [[package]] name = "symphonia-bundle-mp3" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f31d7fece546f1e6973011a9eceae948133bbd18fd3d52f6073b1e38ae6368a" +checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4" dependencies = [ - "bitflags 1.3.2", "lazy_static", "log", "symphonia-core", @@ -2697,9 +2877,9 @@ dependencies = [ [[package]] name = "symphonia-codec-vorbis" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3953397e3506aa01350c4205817e4f95b58d476877a42f0458d07b665749e203" +checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30" dependencies = [ "log", "symphonia-core", @@ -2708,9 +2888,9 @@ dependencies = [ [[package]] name = "symphonia-core" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c73eb88fee79705268cc7b742c7bc93a7b76e092ab751d0833866970754142" +checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3" dependencies = [ "arrayvec", "bitflags 1.3.2", @@ -2721,9 +2901,9 @@ dependencies = [ [[package]] name = "symphonia-format-ogg" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bf1a00ccd11452d44048a0368828040f778ae650418dbd9d8765b7ee2574c8d" +checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931" dependencies = [ "log", "symphonia-core", @@ -2733,9 +2913,9 @@ dependencies = [ [[package]] name = "symphonia-metadata" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c3e1937e31d0e068bbe829f66b2f2bfaa28d056365279e0ef897172c3320c0" +checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c" dependencies = [ "encoding_rs", "lazy_static", @@ -2745,9 +2925,9 @@ dependencies = [ [[package]] name = "symphonia-utils-xiph" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a450ca645b80d69aff8b35576cbfdc7f20940b29998202aab910045714c951f8" +checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe" dependencies = [ "symphonia-core", "symphonia-metadata", @@ -2776,10 +2956,21 @@ dependencies = [ ] [[package]] -name = "sysinfo" -version = "0.30.5" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb4f3438c8f6389c864e61221cbc97e9bca98b4daf39a5beb7bea660f528bb2" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "sysinfo" +version = "0.30.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" dependencies = [ "cfg-if", "core-foundation-sys", @@ -2791,15 +2982,15 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.0" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2d580ff6a20c55dfb86be5f9c238f67835d0e81cbdea8bf5680e0897320331" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" dependencies = [ "cfg-expr", "heck", "pkg-config", "toml", - "version-compare", + "version-compare 0.2.0", ] [[package]] @@ -2810,9 +3001,9 @@ checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" [[package]] name = "tempfile" -version = "3.10.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", @@ -2822,18 +3013,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.57" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -2852,9 +3043,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -2875,34 +3066,29 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", ] [[package]] -name = "tinyvec" -version = "1.6.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.36.0" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" dependencies = [ "backtrace", "bytes", @@ -2919,9 +3105,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", @@ -2934,16 +3120,27 @@ version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ - "rustls", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.9", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", @@ -2952,51 +3149,50 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0" dependencies = [ "futures-util", "log", - "rustls", + "rustls 0.23.9", "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.0", "tungstenite", ] [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", "pin-project-lite", "tokio", - "tracing", ] [[package]] name = "toml" -version = "0.8.10" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.6", + "toml_edit 0.22.14", ] [[package]] name = "toml_datetime" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" dependencies = [ "serde", ] @@ -3007,22 +3203,22 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.6", "toml_datetime", "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.22.6" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6" +checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" dependencies = [ - "indexmap 2.2.3", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.2", + "winnow 0.6.13", ] [[package]] @@ -3079,9 +3275,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.21.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" dependencies = [ "byteorder", "bytes", @@ -3090,11 +3286,10 @@ dependencies = [ "httparse", "log", "rand", - "rustls", + "rustls 0.23.9", "rustls-pki-types", "sha1", "thiserror", - "url", "utf-8", ] @@ -3104,32 +3299,17 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicode-bidi" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" - [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" -[[package]] -name = "unicode-normalization" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-width" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" [[package]] name = "untrusted" @@ -3139,9 +3319,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" dependencies = [ "form_urlencoded", "idna", @@ -3155,16 +3335,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] -name = "utf8parse" -version = "0.2.1" +name = "utf16_iter" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "rand", @@ -3188,6 +3380,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" +[[package]] +name = "version-compare" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" + [[package]] name = "version_check" version = "0.9.4" @@ -3196,9 +3394,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", "winapi-util", @@ -3221,9 +3419,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -3231,9 +3429,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", @@ -3246,9 +3444,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -3258,9 +3456,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3268,9 +3466,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", @@ -3281,15 +3479,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.91" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "web-sys" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", @@ -3335,11 +3533,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" +checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -3355,7 +3553,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] @@ -3365,7 +3563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] @@ -3374,7 +3572,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] @@ -3384,16 +3582,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] name = "windows-result" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd19df78e5168dfb0aedc343d1d1b8d422ab2db6756d2dc3fef75035402a3f64" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] @@ -3420,7 +3618,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.3", + "windows-targets 0.52.5", ] [[package]] @@ -3455,17 +3653,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.3", - "windows_aarch64_msvc 0.52.3", - "windows_i686_gnu 0.52.3", - "windows_i686_msvc 0.52.3", - "windows_x86_64_gnu 0.52.3", - "windows_x86_64_gnullvm 0.52.3", - "windows_x86_64_msvc 0.52.3", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -3482,9 +3681,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -3500,9 +3699,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -3518,9 +3717,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -3536,9 +3741,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -3554,9 +3759,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -3572,9 +3777,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -3590,9 +3795,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.3" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winnow" @@ -3605,18 +3810,54 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.2" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4191c47f15cc3ec71fcb4913cb83d58def65dd3787610213c649283b5ce178" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" dependencies = [ "memchr", ] [[package]] -name = "zerocopy" -version = "0.7.32" +name = "write16" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.7.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" dependencies = [ "byteorder", "zerocopy-derive", @@ -3624,9 +3865,9 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", @@ -3634,7 +3875,50 @@ dependencies = [ ] [[package]] -name = "zeroize" -version = "1.7.0" +name = "zerofrom" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" +checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] From 4f8a84e7cb3f257bb4ca185e10ec9bd3cc7b1bbf Mon Sep 17 00:00:00 2001 From: Dariusz Olszewski Date: Tue, 11 Jun 2024 20:55:27 +0200 Subject: [PATCH 408/561] Docker cross compile - remove MIPS target platform Signed-off-by: Dariusz Olszewski --- contrib/Dockerfile | 12 ++++-------- contrib/docker-build.sh | 1 - 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index dc28bc60..448a8f38 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -2,7 +2,7 @@ # Build the docker image from the root of the project with the following command : # $ docker build -t librespot-cross -f contrib/Dockerfile . # -# The resulting image can be used to build librespot for linux x86_64, armhf, armel, mipsel, aarch64 +# The resulting image can be used to build librespot for linux x86_64, armhf, armel, aarch64 # $ docker run -v /tmp/librespot-build:/build librespot-cross # # The compiled binaries will be located in /tmp/librespot-build @@ -22,11 +22,10 @@ RUN echo "deb http://archive.debian.org/debian-security stretch/updates main" >> RUN dpkg --add-architecture arm64 RUN dpkg --add-architecture armhf RUN dpkg --add-architecture armel -RUN dpkg --add-architecture mipsel RUN apt-get update -RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf crossbuild-essential-mipsel pkg-config -RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf libasound2-dev:mipsel +RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf pkg-config +RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.73 -y @@ -34,13 +33,11 @@ ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi RUN rustup target add arm-unknown-linux-gnueabihf -RUN rustup target add mipsel-unknown-linux-gnu RUN mkdir /.cargo && \ echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ - echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config && \ - echo '[target.mipsel-unknown-linux-gnu]\nlinker = "mipsel-linux-gnu-gcc"' >> /.cargo/config + echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config ENV CARGO_TARGET_DIR /build ENV CARGO_HOME /build/cache @@ -48,7 +45,6 @@ ENV PKG_CONFIG_ALLOW_CROSS=1 ENV PKG_CONFIG_PATH_aarch64-unknown-linux-gnu=/usr/lib/aarch64-linux-gnu/pkgconfig/ ENV PKG_CONFIG_PATH_arm-unknown-linux-gnueabihf=/usr/lib/arm-linux-gnueabihf/pkgconfig/ ENV PKG_CONFIG_PATH_arm-unknown-linux-gnueabi=/usr/lib/arm-linux-gnueabi/pkgconfig/ -ENV PKG_CONFIG_PATH_mipsel-unknown-linux-gnu=/usr/lib/mipsel-linux-gnu/pkgconfig/ ADD . /src WORKDIR /src diff --git a/contrib/docker-build.sh b/contrib/docker-build.sh index cd5ef465..0d59e341 100755 --- a/contrib/docker-build.sh +++ b/contrib/docker-build.sh @@ -5,4 +5,3 @@ cargo build --release --no-default-features --features alsa-backend cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend -cargo build --release --target mipsel-unknown-linux-gnu --no-default-features --features alsa-backend From 0b3c3b6107841ba7a9c99bc2702c3471212d56b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jun 2024 10:02:55 +0000 Subject: [PATCH 409/561] Bump actions/checkout from 4.1.6 to 4.1.7 Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.6 to 4.1.7. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.6...v4.1.7) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4056d994..b22bd715 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -167,7 +167,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -210,7 +210,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.6 + uses: actions/checkout@v4.1.7 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From 180ffacde19c696e140d07f86fb2fe78f8137bb9 Mon Sep 17 00:00:00 2001 From: Joey Eamigh <55670930+JoeyEamigh@users.noreply.github.com> Date: Sat, 15 Jun 2024 23:36:47 -0400 Subject: [PATCH 410/561] adding flag to show connect device as group --- connect/src/config.rs | 2 ++ discovery/examples/discovery_group.rs | 22 ++++++++++++++++++++++ discovery/src/lib.rs | 7 +++++++ discovery/src/server.rs | 10 +++++++++- src/main.rs | 9 +++++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 discovery/examples/discovery_group.rs diff --git a/connect/src/config.rs b/connect/src/config.rs index 473fa173..278ecf17 100644 --- a/connect/src/config.rs +++ b/connect/src/config.rs @@ -4,6 +4,7 @@ use crate::core::config::DeviceType; pub struct ConnectConfig { pub name: String, pub device_type: DeviceType, + pub is_group: bool, pub initial_volume: Option, pub has_volume_ctrl: bool, } @@ -13,6 +14,7 @@ impl Default for ConnectConfig { ConnectConfig { name: "Librespot".to_string(), device_type: DeviceType::default(), + is_group: false, initial_volume: Some(50), has_volume_ctrl: true, } diff --git a/discovery/examples/discovery_group.rs b/discovery/examples/discovery_group.rs new file mode 100644 index 00000000..e98b1536 --- /dev/null +++ b/discovery/examples/discovery_group.rs @@ -0,0 +1,22 @@ +use futures::StreamExt; +use librespot_core::SessionConfig; +use librespot_discovery::DeviceType; +use sha1::{Digest, Sha1}; + +#[tokio::main(flavor = "current_thread")] +async fn main() { + let name = "Librespot Group"; + let device_id = hex::encode(Sha1::digest(name.as_bytes())); + + let mut server = + librespot_discovery::Discovery::builder(device_id, SessionConfig::default().client_id) + .name(name) + .device_type(DeviceType::Speaker) + .is_group(true) + .launch() + .unwrap(); + + while let Some(x) = server.next().await { + println!("Received {:?}", x); + } +} diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 8caeadfb..f1f0f692 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -84,6 +84,7 @@ impl Builder { server_config: server::Config { name: "Librespot".into(), device_type: DeviceType::default(), + is_group: false, device_id: device_id.into(), client_id: client_id.into(), }, @@ -104,6 +105,12 @@ impl Builder { self } + /// Sets whether the device is a group. This affects the icon in Spotify clients. Default is `false`. + pub fn is_group(mut self, is_group: bool) -> Self { + self.server_config.is_group = is_group; + self + } + /// Set the ip addresses on which it should listen to incoming connections. The default is all interfaces. pub fn zeroconf_ip(mut self, zeroconf_ip: Vec) -> Self { self.zeroconf_ip = zeroconf_ip; diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 6167275d..35f3993b 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -39,6 +39,7 @@ pub struct Config { pub name: Cow<'static, str>, pub device_type: DeviceType, pub device_id: String, + pub is_group: bool, pub client_id: String, } @@ -70,6 +71,12 @@ impl RequestHandler { if let Some(username) = &self.username { active_user = username.to_string(); } + // options based on zeroconf guide, search for `groupStatus` on page + let group_status = if self.config.is_group { + "GROUP" + } else { + "NONE" + }; // See: https://developer.spotify.com/documentation/commercial-hardware/implementation/guides/zeroconf/ let body = json!({ @@ -87,7 +94,8 @@ impl RequestHandler { "modelDisplayName": "librespot", "libraryVersion": crate::core::version::SEMVER, "resolverVersion": "1", - "groupStatus": "NONE", + // valid values are "GROUP" and "NONE" + "groupStatus": group_status, // valid value documented & seen in the wild: "accesstoken" // Using it will cause clients to fail to connect. "tokenType": "default", diff --git a/src/main.rs b/src/main.rs index 22c3abec..3e656be2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -203,6 +203,7 @@ fn get_setup() -> Setup { const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; + const DEVICE_IS_GROUP: &str = "group"; const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; const DISABLE_CREDENTIAL_CACHE: &str = "disable-credential-cache"; const DISABLE_DISCOVERY: &str = "disable-discovery"; @@ -409,6 +410,10 @@ fn get_setup() -> Setup { DEVICE_TYPE, "Displayed device type. Defaults to speaker.", "TYPE", + ).optflag( + "", + DEVICE_IS_GROUP, + "Whether the device represents a group. Defaults to false.", ) .optopt( TEMP_DIR_SHORT, @@ -1312,11 +1317,14 @@ fn get_setup() -> Setup { }) .unwrap_or_default(); + let is_group = opt_present(DEVICE_IS_GROUP); + let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); ConnectConfig { name, device_type, + is_group, initial_volume, has_volume_ctrl, } @@ -1685,6 +1693,7 @@ async fn main() { match librespot::discovery::Discovery::builder(device_id, client_id) .name(setup.connect_config.name.clone()) .device_type(setup.connect_config.device_type) + .is_group(setup.connect_config.is_group) .port(setup.zeroconf_port) .zeroconf_ip(setup.zeroconf_ip.clone()) .launch() From ba8ec55345a59ae31d552f2d61897a490e5631a1 Mon Sep 17 00:00:00 2001 From: DaXcess Date: Mon, 1 Jul 2024 15:38:50 +0200 Subject: [PATCH 411/561] Fix spotify version in client token request --- core/src/spclient.rs | 4 ++-- core/src/version.rs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 5a41d148..156cf9c8 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -35,7 +35,7 @@ use crate::{ extended_metadata::BatchedEntityRequest, }, token::Token, - version::spotify_version, + version::spotify_semantic_version, Error, FileId, SpotifyId, }; @@ -182,7 +182,7 @@ impl SpClient { let client_data = request.mut_client_data(); - client_data.client_version = spotify_version(); + client_data.client_version = spotify_semantic_version(); // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, // so it seems a good idea to mimick the real clients. `self.session().client_id()` returns the diff --git a/core/src/version.rs b/core/src/version.rs index 332761b2..d3870473 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -29,6 +29,13 @@ pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; pub fn spotify_version() -> String { + match std::env::consts::OS { + "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), + _ => SPOTIFY_VERSION.to_string(), + } +} + +pub fn spotify_semantic_version() -> String { match std::env::consts::OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), _ => SPOTIFY_SEMANTIC_VERSION.to_string(), From d909286fa5a58430095a4ca92aca35199ea4a369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:37:46 +0000 Subject: [PATCH 412/561] Bump zerovec from 0.10.2 to 0.10.4 Bumps [zerovec](https://github.com/unicode-org/icu4x) from 0.10.2 to 0.10.4. - [Release notes](https://github.com/unicode-org/icu4x/releases) - [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md) - [Commits](https://github.com/unicode-org/icu4x/commits/ind/zerovec@0.10.4) --- updated-dependencies: - dependency-name: zerovec dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a677fda2..b2e17be5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3903,9 +3903,9 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zerovec" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" dependencies = [ "yoke", "zerofrom", From b00889a08f9d324de90031f29f4a18bc035e47cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:46:19 +0000 Subject: [PATCH 413/561] Bump zerovec-derive from 0.10.2 to 0.10.3 Bumps [zerovec-derive](https://github.com/unicode-org/icu4x) from 0.10.2 to 0.10.3. - [Release notes](https://github.com/unicode-org/icu4x/releases) - [Changelog](https://github.com/unicode-org/icu4x/blob/main/CHANGELOG.md) - [Commits](https://github.com/unicode-org/icu4x/commits/ind/zerovec-derive@0.10.3) --- updated-dependencies: - dependency-name: zerovec-derive dependency-type: indirect ... Signed-off-by: dependabot[bot] --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a677fda2..bc3292c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3914,9 +3914,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", From 2c871bbc968391cfc7c20771bd1850a749659ef3 Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sat, 24 Aug 2024 17:31:34 +0100 Subject: [PATCH 414/561] Bump libmdns from 0.8.0 to 0.9.1 --- Cargo.lock | 49 +++++++++----------------------------------- Cargo.toml | 2 +- discovery/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 41 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a50203ce..24fe042c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1004,13 +1004,13 @@ dependencies = [ [[package]] name = "hostname" -version = "0.3.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" dependencies = [ + "cfg-if", "libc", - "match_cfg", - "winapi", + "windows 0.52.0", ] [[package]] @@ -1278,9 +1278,9 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624c5448ba529e74f594c65b7024f31b2de7b64a9b228b8df26796bbb6e32c36" +checksum = "bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -1461,9 +1461,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "libmdns" -version = "0.8.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6677a7ef3e8d47432fc827d0ebf0ee77c3e3bc4a3e632d9b92433ef86f70c" +checksum = "48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862" dependencies = [ "byteorder", "futures-util", @@ -1471,12 +1471,10 @@ dependencies = [ "if-addrs", "log", "multimap", - "nix", "rand", "socket2", "thiserror", "tokio", - "winapi", ] [[package]] @@ -1770,27 +1768,12 @@ dependencies = [ "libc", ] -[[package]] -name = "match_cfg" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" - [[package]] name = "memchr" version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.17" @@ -1831,9 +1814,9 @@ checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "multimap" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1a5d38b9b352dbd913288736af36af41c48d61b1a8cd34bcecd727561b7d511" +checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" dependencies = [ "serde", ] @@ -1867,18 +1850,6 @@ dependencies = [ "jni-sys", ] -[[package]] -name = "nix" -version = "0.27.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" -dependencies = [ - "bitflags 2.5.0", - "cfg-if", - "libc", - "memoffset", -] - [[package]] name = "no-std-compat" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index bb213136..b99531b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,7 @@ version = "0.5.0-dev" [dependencies] data-encoding = "2.5" env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } -futures-util = { version = "0.3", default_features = false } +futures-util = { version = "0.3", default-features = false } getopts = "0.2" log = "0.4" rpassword = "7.0" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index e333a6eb..8076d8e4 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -22,7 +22,7 @@ hmac = "0.12" hyper = { version = "1.3", features = ["http1"] } hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } http-body-util = "0.1.1" -libmdns = "0.8" +libmdns = "0.9" log = "0.4" rand = "0.8" serde_json = "1.0" From dcd592bafb7f85b0f032ba2dca4988ad29d3d8d0 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Tue, 27 Aug 2024 23:16:48 +0200 Subject: [PATCH 415/561] Update MSRV to 1.74.0 and sysinfo to 0.31.3 Signed-off-by: yubiuser --- .devcontainer/Dockerfile | 2 +- .github/workflows/test.yml | 6 +++--- CHANGELOG.md | 2 +- Cargo.lock | 9 ++++----- Cargo.toml | 4 ++-- audio/Cargo.toml | 2 +- connect/Cargo.toml | 2 +- contrib/Dockerfile | 2 +- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- playback/Cargo.toml | 2 +- protocol/Cargo.toml | 2 +- src/main.rs | 4 ++-- 14 files changed, 22 insertions(+), 23 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 8de63190..c5cf4356 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.73.0 +ARG rust_version=1.74.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b22bd715..f5d81398 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.73" # MSRV (Minimum supported rust version) + - "1.74" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -163,7 +163,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.73" # MSRV (Minimum supported rust version) + - "1.74" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -206,7 +206,7 @@ jobs: os: [ubuntu-latest] target: [armv7-unknown-linux-gnueabihf] toolchain: - - "1.73" # MSRV (Minimum supported rust version) + - "1.74" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index 6896180c..f320c104 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,7 +41,7 @@ https://github.com/librespot-org/librespot configurations. - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) -- [core] MSRV is now 1.73 (breaking) +- [core] MSRV is now 1.74 (breaking) - [connect] `DeviceType` moved out of `connect` into `core` (breaking) - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts diff --git a/Cargo.lock b/Cargo.lock index 24fe042c..ef1f4d75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2939,16 +2939,15 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.30.12" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae" +checksum = "2b92e0bdf838cbc1c4c9ba14f9c97a7ec6cdcd1ae66b10e1e42775a25553f45d" dependencies = [ - "cfg-if", "core-foundation-sys", "libc", + "memchr", "ntapi", - "once_cell", - "windows 0.52.0", + "windows 0.54.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b99531b6..00d94577 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -57,7 +57,7 @@ getopts = "0.2" log = "0.4" rpassword = "7.0" sha1 = "0.10" -sysinfo = { version = "0.30.5", default-features = false } +sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 3610ed4c..ec0945a5 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-audio" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 3c3bbe84..14fad592 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-connect" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 448a8f38..377ece7a 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -28,7 +28,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.73 -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.74 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi diff --git a/core/Cargo.toml b/core/Cargo.toml index ea9a906e..6be4d567 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-core" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -49,7 +49,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.30.5", default-features = false } +sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 8076d8e4..f8b317dc 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-discovery" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 449cd3d5..bdc2da38 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-metadata" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index ce60fd29..282d5078 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-playback" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 7e69971a..28fb3b7f 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-protocol" version = "0.5.0-dev" -rust-version = "1.73" +rust-version = "1.74" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" diff --git a/src/main.rs b/src/main.rs index 3e656be2..a6e54d44 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,7 +12,7 @@ use std::{ str::FromStr, time::{Duration, Instant}, }; -use sysinfo::System; +use sysinfo::{ProcessesToUpdate, System}; use thiserror::Error; use url::Url; @@ -1700,7 +1700,7 @@ async fn main() { { Ok(d) => break Some(d), Err(e) => { - sys.refresh_processes(); + sys.refresh_processes(ProcessesToUpdate::All); if System::uptime() <= 1 { debug!("Retrying to initialise discovery: {e}"); From dc229106735aba77bbcb2f72d6079ade9f44db5b Mon Sep 17 00:00:00 2001 From: yubiuser Date: Wed, 28 Aug 2024 13:13:29 +0200 Subject: [PATCH 416/561] Update vergen to version 9 by using the new vergen-gitctl libary Signed-off-by: yubiuser --- Cargo.lock | 149 ++++++++++++++++++++++++++++++++++++++++++++++-- core/Cargo.toml | 2 +- core/build.rs | 21 +++++-- 3 files changed, 161 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ef1f4d75..92cc377e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -419,6 +419,41 @@ dependencies = [ "cipher", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.66", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.66", +] + [[package]] name = "dasp_sample" version = "0.11.0" @@ -451,6 +486,37 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +dependencies = [ + "derive_builder_core", + "syn 2.0.66", +] + [[package]] name = "digest" version = "0.10.7" @@ -690,6 +756,18 @@ 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 1.0.109", +] + [[package]] name = "gimli" version = "0.29.0" @@ -1264,6 +1342,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.0" @@ -1641,7 +1725,7 @@ dependencies = [ "tokio-util", "url", "uuid", - "vergen", + "vergen-gitcl", ] [[package]] @@ -2264,6 +2348,30 @@ dependencies = [ "toml_edit 0.21.1", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.85" @@ -2814,6 +2922,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.5.0" @@ -3334,14 +3448,41 @@ dependencies = [ [[package]] name = "vergen" -version = "8.3.1" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +checksum = "c32e7318e93a9ac53693b6caccfb05ff22e04a44c7cf8a279051f24c09da286f" dependencies = [ "anyhow", - "cfg-if", + "derive_builder", "rustversion", "time", + "vergen-lib", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbdc9746577cb4767f218d320ee0b623d415e8130332f8f562b910b61cc2c4e" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib", +] + +[[package]] +name = "vergen-lib" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e06bee42361e43b60f363bad49d63798d0f42fb1768091812270eca00c784720" +dependencies = [ + "anyhow", + "derive_builder", + "getset", + "rustversion", ] [[package]] diff --git a/core/Cargo.toml b/core/Cargo.toml index 6be4d567..1f73669d 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -62,7 +62,7 @@ data-encoding = "2.5" [build-dependencies] rand = "0.8" -vergen = { version = "8", default-features = false, features = ["build", "git", "gitcl"] } +vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } [dev-dependencies] env_logger = "0.11.2" diff --git a/core/build.rs b/core/build.rs index 920455d0..008ec1d2 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,11 +1,19 @@ use rand::{distributions::Alphanumeric, Rng}; -use vergen::EmitBuilder; +use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; -fn main() { - EmitBuilder::builder() - .build_date() // outputs 'VERGEN_BUILD_DATE' - .git_sha(true) // outputs 'VERGEN_GIT_SHA', and sets the 'short' flag true - .git_commit_date() // outputs 'VERGEN_GIT_COMMIT_DATE' +fn main() -> Result<(), Box> { + let gitcl = GitclBuilder::default() + .sha(true) // outputs 'VERGEN_GIT_SHA', and sets the 'short' flag true + .commit_date(true) // outputs 'VERGEN_GIT_COMMIT_DATE' + .build()?; + + let build = BuildBuilder::default() + .build_date(true) // outputs 'VERGEN_BUILD_DATE' + .build()?; + + Emitter::default() + .add_instructions(&build)? + .add_instructions(&gitcl)? .emit() .expect("Unable to generate the cargo keys!"); let build_id = match std::env::var("SOURCE_DATE_EPOCH") { @@ -18,4 +26,5 @@ fn main() { }; println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={build_id}"); + Ok(()) } From bec54a64f1c18f7759e1c6e40ad7ed7f4db55f42 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Wed, 28 Aug 2024 13:42:13 +0200 Subject: [PATCH 417/561] Update quick-xml to 0.36.1 Signed-off-by: yubiuser --- Cargo.lock | 4 ++-- core/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92cc377e..97f35bf6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2434,9 +2434,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.32.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" dependencies = [ "memchr", "serde", diff --git a/core/Cargo.toml b/core/Cargo.toml index 1f73669d..d3b2db28 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -42,7 +42,7 @@ parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "2.0" protobuf = "3" -quick-xml = { version = "0.32", features = ["serialize"] } +quick-xml = { version = "0.36.1", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } From 3e85d77bfb3520e16c35bb7db3912c76e8cc90d7 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 15:32:16 +0200 Subject: [PATCH 418/561] Update hyper-rustls to 0.27.2 Signed-off-by: yubiuser --- Cargo.lock | 149 ++++++++++++++++++++++++++++++++++++++++-------- core/Cargo.toml | 2 +- 2 files changed, 127 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24fe042c..82e77452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -148,6 +148,33 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "aws-lc-rs" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + [[package]] name = "backtrace" version = "0.3.72" @@ -193,12 +220,15 @@ dependencies = [ "itertools 0.12.1", "lazy_static", "lazycell", + "log", + "prettyplease", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.66", + "syn 2.0.76", + "which", ] [[package]] @@ -309,6 +339,15 @@ dependencies = [ "libloading 0.8.3", ] +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.1" @@ -471,7 +510,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -484,6 +523,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.12.0" @@ -565,6 +610,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.30" @@ -621,7 +672,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -741,7 +792,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -1097,7 +1148,7 @@ dependencies = [ "headers", "http", "hyper", - "hyper-rustls", + "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", "rustls-native-certs", @@ -1126,6 +1177,25 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "log", + "rustls 0.23.9", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.5" @@ -1261,7 +1331,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -1611,7 +1681,7 @@ dependencies = [ "httparse", "hyper", "hyper-proxy2", - "hyper-rustls", + "hyper-rustls 0.27.2", "hyper-util", "librespot-protocol", "log", @@ -1806,6 +1876,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + [[package]] name = "muldiv" version = "1.0.1" @@ -1934,7 +2010,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -2005,7 +2081,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -2163,7 +2239,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -2244,6 +2320,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.76", +] + [[package]] name = "priority-queue" version = "2.0.3" @@ -2532,6 +2618,8 @@ version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" dependencies = [ + "aws-lc-rs", + "log", "once_cell", "rustls-pki-types", "rustls-webpki", @@ -2574,6 +2662,7 @@ version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ + "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -2678,7 +2767,7 @@ checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -2917,9 +3006,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.66" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -2934,7 +3023,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -2999,7 +3088,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -3082,7 +3171,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -3409,7 +3498,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -3443,7 +3532,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3820,7 +3909,7 @@ checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", "synstructure", ] @@ -3842,7 +3931,7 @@ checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -3862,7 +3951,7 @@ checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", "synstructure", ] @@ -3871,6 +3960,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.76", +] [[package]] name = "zerovec" @@ -3891,5 +3994,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] diff --git a/core/Cargo.toml b/core/Cargo.toml index ea9a906e..35f6205c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -30,7 +30,7 @@ hyper = { version = "1.3", features = ["http1", "http2"] } hyper-util = { version = "0.1", features = ["client"] } http-body-util = "0.1.1" hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.26", features = ["http2"] } +hyper-rustls = { version = "0.27.2", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" num-bigint = { version = "0.4", features = ["rand"] } From 8f0c7b2b0d91bb6f14d55e8b78ec59a9c8229c07 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 15:33:40 +0200 Subject: [PATCH 419/561] Install NASM on Windows CI job Signed-off-by: yubiuser --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b22bd715..d7b84d88 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -168,6 +168,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4.1.7 + + # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build + - name: Install NASM + uses: ilammy/setup-nasm@v1.5.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From 78a8c61f8544f57b46788e0458e36210d26b2bdb Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 15:46:03 +0200 Subject: [PATCH 420/561] Don't use 'cross' for cross-compilation Signed-off-by: yubiuser --- .cargo/config.toml | 3 +++ .github/workflows/test.yml | 14 +++++++++++--- .gitignore | 1 - 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..36056447 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ + +[target.armv7-unknown-linux-gnueabihf] +linker = "arm-linux-gnueabihf-gcc" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d7b84d88..b9e29240 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -233,7 +233,15 @@ jobs: ~/.cargo/git target key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - - name: Install cross - run: cargo install cross || true + + - name: Install the cross compiler targets + run: rustup target add ${{ matrix.target }} + + - name: Install cross compiler toolchain + run: sudo apt-get install -y gcc-arm-linux-gnueabihf + - name: Build - run: cross build --target ${{ matrix.target }} --no-default-features + run: cargo build --verbose --target ${{ matrix.target }} --no-default-features + + - name: Check binary + run: file target/${{ matrix.target }}/debug/librespot diff --git a/.gitignore b/.gitignore index eebf401d..8b5fdeb6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ target -.cargo spotify_appkey.key .vagrant/ .project From 102a8fd833942c994f28e43f93c360742fe9da7d Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 22:28:53 +0200 Subject: [PATCH 421/561] Add cargo clean to test.sh Signed-off-by: yubiuser --- test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.sh b/test.sh index 0d6bb291..0d0c34ad 100755 --- a/test.sh +++ b/test.sh @@ -3,7 +3,7 @@ set -e # this script runs the tests and checks that also run as part of the`test.yml` github action workflow - +cargo clean cargo fmt --all -- --check cargo clippy -p librespot-core --no-default-features cargo clippy -p librespot-core From 09ffc01e779401d5eec53b3cf5c86f38cd5c9409 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 22:35:45 +0200 Subject: [PATCH 422/561] Update rodio Signed-off-by: yubiuser --- Cargo.lock | 4 ++-- playback/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97f35bf6..7732e0a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2546,9 +2546,9 @@ dependencies = [ [[package]] name = "rodio" -version = "0.18.1" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1fceb9d127d515af1586d8d0cc601e1245bdb0af38e75c865a156290184f5b3" +checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" dependencies = [ "cpal", "thiserror", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 282d5078..3f87b939 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -43,7 +43,7 @@ gstreamer-audio = { version = "0.22.0", optional = true } glib = { version = "0.19.2", optional = true } # Rodio dependencies -rodio = { version = "0.18.1", optional = true, default-features = false } +rodio = { version = "0.19.0", optional = true, default-features = false } cpal = { version = "0.15.1", optional = true } # Container and audio decoder From ac02ca224e5c32594fd68e7fd6c5c13fc11dce4a Mon Sep 17 00:00:00 2001 From: yubiuser Date: Thu, 29 Aug 2024 22:40:35 +0200 Subject: [PATCH 423/561] Update sdl2 Signed-off-by: yubiuser --- Cargo.lock | 8 ++++---- playback/Cargo.toml | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7732e0a8..184d4cdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2725,9 +2725,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdl2" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8356b2697d1ead5a34f40bcc3c5d3620205fe0c7be0a14656223bfeec0258891" +checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -2737,9 +2737,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.36.0" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26bcacfdd45d539fb5785049feb0038a63931aa896c7763a2a12e125ec58bd29" +checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" dependencies = [ "cfg-if", "libc", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 3f87b939..3e6b3962 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -36,7 +36,7 @@ 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.11", optional = true } -sdl2 = { version = "0.36", optional = true } +sdl2 = { version = "0.37", optional = true } gstreamer = { version = "0.22.1", optional = true } gstreamer-app = { version = "0.22.0", optional = true } gstreamer-audio = { version = "0.22.0", optional = true } From ebd4f1cdc264d07c2dea706f65f36f8315c10e7f Mon Sep 17 00:00:00 2001 From: yubiuser Date: Sat, 7 Sep 2024 21:56:02 +0200 Subject: [PATCH 424/561] Allow renamed-and-removed-lints Signed-off-by: yubiuser --- .devcontainer/Dockerfile | 2 +- .github/workflows/test.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index c5cf4356..156630da 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" ENV RUST_BACKTRACE=1 -ENV RUSTFLAGS=-Dwarnings +ENV RUSTFLAGS="-D warnings -A renamed-and-removed-lints" RUN apt-get update && \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cadfd69e..66a430b4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ on: env: RUST_BACKTRACE: 1 - RUSTFLAGS: -D warnings + RUSTFLAGS: -D warnings -A renamed-and-removed-lints # The layering here is as follows: # 1. code formatting From 4f9151c6429e314c5916fdcd7f75ec5478624652 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Fri, 13 Sep 2024 06:35:55 +0100 Subject: [PATCH 425/561] Credentials with access token (oauth) (#1309) * core: Create credentials from access token via OAuth2 * core: Credentials.username is optional: not required for token auth. * core: store auth data within session. We might need this later if need to re-auth and original creds are no longer valid/available. * bin: New --token arg for using Spotify access token. Specify 0 to manually enter the auth code (headless). * bin: Added --enable-oauth / -j option. Using --password / -p option will error and exit. * core: reconnect session if using token authentication Token authenticated sessions cannot use keymaster. So reconnect using the reusable credentials we just obtained. Can perhaps remove this workaround once keymaster is replaced with login5. * examples: replace password login with token login --- CHANGELOG.md | 3 + Cargo.toml | 4 + core/Cargo.toml | 4 + core/src/authentication.rs | 22 ++- core/src/connection/mod.rs | 13 +- core/src/error.rs | 21 +++ core/src/session.rs | 76 ++++++++-- examples/get_token.rs | 37 +++-- examples/play.rs | 8 +- examples/play_connect.rs | 8 +- examples/playlist_tracks.rs | 8 +- oauth/Cargo.toml | 18 +++ oauth/examples/oauth.rs | 32 ++++ oauth/src/lib.rs | 287 ++++++++++++++++++++++++++++++++++++ publish.sh | 2 +- src/lib.rs | 1 + src/main.rs | 173 +++++++++++++++++----- 17 files changed, 629 insertions(+), 88 deletions(-) mode change 100755 => 100644 core/src/session.rs create mode 100644 oauth/Cargo.toml create mode 100644 oauth/examples/oauth.rs create mode 100644 oauth/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f320c104..681762d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ https://github.com/librespot-org/librespot - [core] Cache resolved access points during runtime (breaking) - [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported. - [core] Report actual platform data on login +- [core] Support `Session` authentication with a Spotify access token +- [core] `Credentials.username` is now an `Option` (breaking) - [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot` now follows the setting in the Connect client that controls it. (breaking) - [metadata] Most metadata is now retrieved with the `spclient` (breaking) @@ -95,6 +97,7 @@ https://github.com/librespot-org/librespot - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers - [metadata] All metadata fields in the protobufs are now exposed (breaking) +- [oauth] Standalone module to obtain Spotify access token using OAuth authorization code flow. - [playback] Explicit tracks are skipped if the controlling Connect client has disabled such content. Applications that use librespot as a library without Connect should use the 'filter-explicit-content' user attribute in the session. diff --git a/Cargo.toml b/Cargo.toml index 00d94577..fe2e38cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,10 @@ version = "0.5.0-dev" path = "protocol" version = "0.5.0-dev" +[dependencies.librespot-oauth] +path = "oauth" +version = "0.5.0-dev" + [dependencies] data-encoding = "2.5" env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 680e6b90..01919ba7 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -9,6 +9,10 @@ license = "MIT" repository = "https://github.com/librespot-org/librespot" edition = "2021" +[dependencies.librespot-oauth] +path = "../oauth" +version = "0.5.0-dev" + [dependencies.librespot-protocol] path = "../protocol" version = "0.5.0-dev" diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 8122d659..230661ef 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -29,7 +29,7 @@ impl From for Error { /// The credentials are used to log into the Spotify API. #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] pub struct Credentials { - pub username: String, + pub username: Option, #[serde(serialize_with = "serialize_protobuf_enum")] #[serde(deserialize_with = "deserialize_protobuf_enum")] @@ -50,19 +50,27 @@ impl Credentials { /// /// let creds = Credentials::with_password("my account", "my password"); /// ``` - pub fn with_password(username: impl Into, password: impl Into) -> Credentials { - Credentials { - username: username.into(), + pub fn with_password(username: impl Into, password: impl Into) -> Self { + Self { + username: Some(username.into()), auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, auth_data: password.into().into_bytes(), } } + pub fn with_access_token(token: impl Into) -> Self { + Self { + username: None, + auth_type: AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN, + auth_data: token.into().into_bytes(), + } + } + pub fn with_blob( username: impl Into, encrypted_blob: impl AsRef<[u8]>, device_id: impl AsRef<[u8]>, - ) -> Result { + ) -> Result { fn read_u8(stream: &mut R) -> io::Result { let mut data = [0u8]; stream.read_exact(&mut data)?; @@ -136,8 +144,8 @@ impl Credentials { read_u8(&mut cursor)?; let auth_data = read_bytes(&mut cursor)?; - Ok(Credentials { - username, + Ok(Self { + username: Some(username), auth_type, auth_data, }) diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 4bac6e3e..b2e0356b 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -99,10 +99,12 @@ pub async fn authenticate( }; let mut packet = ClientResponseEncrypted::new(); - packet - .login_credentials - .mut_or_insert_default() - .set_username(credentials.username); + if let Some(username) = credentials.username { + packet + .login_credentials + .mut_or_insert_default() + .set_username(username); + } packet .login_credentials .mut_or_insert_default() @@ -133,6 +135,7 @@ pub async fn authenticate( let cmd = PacketType::Login; let data = packet.write_to_bytes()?; + debug!("Authenticating with AP using {:?}", credentials.auth_type); transport.send((cmd as u8, data)).await?; let (cmd, data) = transport .next() @@ -144,7 +147,7 @@ pub async fn authenticate( let welcome_data = APWelcome::parse_from_bytes(data.as_ref())?; let reusable_credentials = Credentials { - username: welcome_data.canonical_username().to_owned(), + username: Some(welcome_data.canonical_username().to_owned()), auth_type: welcome_data.reusable_auth_credentials_type(), auth_data: welcome_data.reusable_auth_credentials().to_owned(), }; diff --git a/core/src/error.rs b/core/src/error.rs index 13491a39..b18ce91a 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -19,6 +19,8 @@ use tokio::sync::{ }; use url::ParseError; +use librespot_oauth::OAuthError; + #[cfg(feature = "with-dns-sd")] use dns_sd::DNSError; @@ -287,6 +289,25 @@ impl fmt::Display for Error { } } +impl From for Error { + fn from(err: OAuthError) -> Self { + use OAuthError::*; + match err { + AuthCodeBadUri { .. } + | AuthCodeNotFound { .. } + | AuthCodeListenerRead + | AuthCodeListenerParse => Error::unavailable(err), + AuthCodeStdinRead + | AuthCodeListenerBind { .. } + | AuthCodeListenerTerminated + | AuthCodeListenerWrite + | Recv + | ExchangeCode { .. } => Error::internal(err), + _ => Error::failed_precondition(err), + } + } +} + impl From for Error { fn from(err: DecodeError) -> Self { Self::new(ErrorKind::FailedPrecondition, err) diff --git a/core/src/session.rs b/core/src/session.rs old mode 100755 new mode 100644 index b8c55d20..3944ce83 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -13,6 +13,7 @@ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::TryStream; use futures_util::{future, ready, StreamExt, TryStreamExt}; +use librespot_protocol::authentication::AuthenticationType; use num_traits::FromPrimitive; use once_cell::sync::OnceCell; use parking_lot::RwLock; @@ -22,13 +23,13 @@ use tokio::{sync::mpsc, time::Instant}; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ - apresolve::ApResolver, + apresolve::{ApResolver, SocketAddress}, audio_key::AudioKeyManager, authentication::Credentials, cache::Cache, channel::ChannelManager, config::SessionConfig, - connection::{self, AuthenticationError}, + connection::{self, AuthenticationError, Transport}, http_client::HttpClient, mercury::MercuryManager, packet::PacketType, @@ -77,6 +78,7 @@ struct SessionData { client_brand_name: String, client_model_name: String, connection_id: String, + auth_data: Vec, time_delta: i64, invalid: bool, user_data: UserData, @@ -140,6 +142,46 @@ impl Session { })) } + async fn connect_inner( + &self, + access_point: SocketAddress, + credentials: Credentials, + ) -> Result<(Credentials, Transport), Error> { + let mut transport = connection::connect( + &access_point.0, + access_point.1, + self.config().proxy.as_ref(), + ) + .await?; + let mut reusable_credentials = connection::authenticate( + &mut transport, + credentials.clone(), + &self.config().device_id, + ) + .await?; + + // Might be able to remove this once keymaster is replaced with login5. + if credentials.auth_type == AuthenticationType::AUTHENTICATION_SPOTIFY_TOKEN { + trace!( + "Reconnect using stored credentials as token authed sessions cannot use keymaster." + ); + transport = connection::connect( + &access_point.0, + access_point.1, + self.config().proxy.as_ref(), + ) + .await?; + reusable_credentials = connection::authenticate( + &mut transport, + reusable_credentials.clone(), + &self.config().device_id, + ) + .await?; + } + + Ok((reusable_credentials, transport)) + } + pub async fn connect( &self, credentials: Credentials, @@ -148,17 +190,8 @@ impl Session { let (reusable_credentials, transport) = loop { let ap = self.apresolver().resolve("accesspoint").await?; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - let mut transport = - connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?; - - match connection::authenticate( - &mut transport, - credentials.clone(), - &self.config().device_id, - ) - .await - { - Ok(creds) => break (creds, transport), + match self.connect_inner(ap, credentials.clone()).await { + Ok(ct) => break ct, Err(e) => { if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) = e.error.downcast_ref::() @@ -172,8 +205,13 @@ impl Session { } }; - info!("Authenticated as \"{}\" !", reusable_credentials.username); - self.set_username(&reusable_credentials.username); + let username = reusable_credentials + .username + .as_ref() + .map_or("UNKNOWN", |s| s.as_str()); + info!("Authenticated as '{username}' !"); + self.set_username(username); + self.set_auth_data(&reusable_credentials.auth_data); if let Some(cache) = self.cache() { if store_credentials { let cred_changed = cache @@ -471,6 +509,14 @@ impl Session { username.clone_into(&mut self.0.data.write().user_data.canonical_username); } + pub fn auth_data(&self) -> Vec { + self.0.data.read().auth_data.clone() + } + + pub fn set_auth_data(&self, auth_data: &[u8]) { + self.0.data.write().auth_data = auth_data.to_owned(); + } + pub fn country(&self) -> String { self.0.data.read().user_data.country.clone() } diff --git a/examples/get_token.rs b/examples/get_token.rs index 0473e122..77b6c8f7 100644 --- a/examples/get_token.rs +++ b/examples/get_token.rs @@ -7,23 +7,34 @@ const SCOPES: &str = #[tokio::main] async fn main() { - let session_config = SessionConfig::default(); + let mut builder = env_logger::Builder::new(); + builder.parse_filters("librespot=trace"); + builder.init(); + + let mut session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 3 { - eprintln!("Usage: {} USERNAME PASSWORD", args[0]); + if args.len() == 3 { + // Only special client IDs have sufficient privileges e.g. Spotify's. + session_config.client_id = args[2].clone() + } else if args.len() != 2 { + eprintln!("Usage: {} ACCESS_TOKEN [CLIENT_ID]", args[0]); return; } + let access_token = &args[1]; - println!("Connecting..."); - let credentials = Credentials::with_password(&args[1], &args[2]); - let session = Session::new(session_config, None); - + // Now create a new session with that token. + let session = Session::new(session_config.clone(), None); + let credentials = Credentials::with_access_token(access_token); + println!("Connecting with token.."); match session.connect(credentials, false).await { - Ok(()) => println!( - "Token: {:#?}", - session.token_provider().get_token(SCOPES).await.unwrap() - ), - Err(e) => println!("Error connecting: {}", e), - } + Ok(()) => println!("Session username: {:#?}", session.username()), + Err(e) => { + println!("Error connecting: {e}"); + return; + } + }; + + let token = session.token_provider().get_token(SCOPES).await.unwrap(); + println!("Got me a token: {token:#?}"); } diff --git a/examples/play.rs b/examples/play.rs index 9e4e29af..46079632 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -22,13 +22,13 @@ async fn main() { let audio_format = AudioFormat::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 4 { - eprintln!("Usage: {} USERNAME PASSWORD TRACK", args[0]); + if args.len() != 3 { + eprintln!("Usage: {} ACCESS_TOKEN TRACK", args[0]); return; } - let credentials = Credentials::with_password(&args[1], &args[2]); + let credentials = Credentials::with_access_token(&args[1]); - let mut track = SpotifyId::from_base62(&args[3]).unwrap(); + let mut track = SpotifyId::from_base62(&args[2]).unwrap(); track.item_type = SpotifyItemType::Track; let backend = audio_backend::find(None).unwrap(); diff --git a/examples/play_connect.rs b/examples/play_connect.rs index a61d3d67..c46464fb 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -28,16 +28,16 @@ async fn main() { let connect_config = ConnectConfig::default(); let mut args: Vec<_> = env::args().collect(); - let context_uri = if args.len() == 4 { + let context_uri = if args.len() == 3 { args.pop().unwrap() - } else if args.len() == 3 { + } else if args.len() == 2 { String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv") } else { - eprintln!("Usage: {} USERNAME PASSWORD (ALBUM URI)", args[0]); + eprintln!("Usage: {} ACCESS_TOKEN (ALBUM URI)", args[0]); return; }; - let credentials = Credentials::with_password(&args[1], &args[2]); + let credentials = Credentials::with_access_token(&args[1]); let backend = audio_backend::find(None).unwrap(); println!("Connecting..."); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index ddf456ac..18fc2e37 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -13,13 +13,13 @@ async fn main() { let session_config = SessionConfig::default(); let args: Vec<_> = env::args().collect(); - if args.len() != 4 { - eprintln!("Usage: {} USERNAME PASSWORD PLAYLIST", args[0]); + if args.len() != 3 { + eprintln!("Usage: {} ACCESS_TOKEN PLAYLIST", args[0]); return; } - let credentials = Credentials::with_password(&args[1], &args[2]); + let credentials = Credentials::with_access_token(&args[1]); - let plist_uri = SpotifyId::from_uri(&args[3]).unwrap_or_else(|_| { + let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| { eprintln!( "PLAYLIST should be a playlist URI such as: \ \"spotify:playlist:37i9dQZF1DXec50AjHrNTq\"" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml new file mode 100644 index 00000000..646f0879 --- /dev/null +++ b/oauth/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "librespot-oauth" +version = "0.5.0-dev" +rust-version = "1.73" +authors = ["Nick Steel "] +description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +log = "0.4" +oauth2 = "4.4" +thiserror = "1.0" +url = "2.2" + +[dev-dependencies] +env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } \ No newline at end of file diff --git a/oauth/examples/oauth.rs b/oauth/examples/oauth.rs new file mode 100644 index 00000000..76ff088e --- /dev/null +++ b/oauth/examples/oauth.rs @@ -0,0 +1,32 @@ +use std::env; + +use librespot_oauth::get_access_token; + +const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login"; + +fn main() { + let mut builder = env_logger::Builder::new(); + builder.parse_filters("librespot=trace"); + builder.init(); + + let args: Vec<_> = env::args().collect(); + let (client_id, redirect_uri, scopes) = if args.len() == 4 { + // You can use your own client ID, along with it's associated redirect URI. + ( + args[1].as_str(), + args[2].as_str(), + args[3].split(',').collect::>(), + ) + } else if args.len() == 1 { + (SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"]) + } else { + eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]); + return; + }; + + match get_access_token(client_id, redirect_uri, scopes) { + Ok(token) => println!("Success: {token:#?}"), + Err(e) => println!("Failed: {e}"), + }; +} diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs new file mode 100644 index 00000000..591e6559 --- /dev/null +++ b/oauth/src/lib.rs @@ -0,0 +1,287 @@ +//! Provides a Spotify access token using the OAuth authorization code flow +//! with PKCE. +//! +//! Assuming sufficient scopes, the returned access token may be used with Spotify's +//! Web API, and/or to establish a new Session with [`librespot_core`]. +//! +//! The authorization code flow is an interactive process which requires a web browser +//! to complete. The resulting code must then be provided back from the browser to this +//! library for exchange into an access token. Providing the code can be automatic via +//! a spawned http server (mimicking Spotify's client), or manually via stdin. The latter +//! is appropriate for headless systems. + +use log::{error, info, trace}; +use oauth2::reqwest::http_client; +use oauth2::{ + basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, + RedirectUrl, Scope, TokenResponse, TokenUrl, +}; +use std::io; +use std::time::{Duration, Instant}; +use std::{ + io::{BufRead, BufReader, Write}, + net::{SocketAddr, TcpListener}, + sync::mpsc, +}; +use thiserror::Error; +use url::Url; + +#[derive(Debug, Error)] +pub enum OAuthError { + #[error("Unable to parse redirect URI {uri} ({e})")] + AuthCodeBadUri { uri: String, e: url::ParseError }, + + #[error("Auth code param not found in URI {uri}")] + AuthCodeNotFound { uri: String }, + + #[error("Failed to read redirect URI from stdin")] + AuthCodeStdinRead, + + #[error("Failed to bind server to {addr} ({e})")] + AuthCodeListenerBind { addr: SocketAddr, e: io::Error }, + + #[error("Listener terminated without accepting a connection")] + AuthCodeListenerTerminated, + + #[error("Failed to read redirect URI from HTTP request")] + AuthCodeListenerRead, + + #[error("Failed to parse redirect URI from HTTP request")] + AuthCodeListenerParse, + + #[error("Failed to write HTTP response")] + AuthCodeListenerWrite, + + #[error("Invalid Spotify OAuth URI")] + InvalidSpotifyUri, + + #[error("Invalid Redirect URI {uri} ({e})")] + InvalidRedirectUri { uri: String, e: url::ParseError }, + + #[error("Failed to receive code")] + Recv, + + #[error("Failed to exchange code for access token ({e})")] + ExchangeCode { e: String }, +} + +#[derive(Debug)] +pub struct OAuthToken { + pub access_token: String, + pub refresh_token: String, + pub expires_at: Instant, + pub token_type: String, + pub scopes: Vec, +} + +/// Return code query-string parameter from the redirect URI. +fn get_code(redirect_url: &str) -> Result { + let url = Url::parse(redirect_url).map_err(|e| OAuthError::AuthCodeBadUri { + uri: redirect_url.to_string(), + e, + })?; + let code = url + .query_pairs() + .find(|(key, _)| key == "code") + .map(|(_, code)| AuthorizationCode::new(code.into_owned())) + .ok_or(OAuthError::AuthCodeNotFound { + uri: redirect_url.to_string(), + })?; + + Ok(code) +} + +/// Prompt for redirect URI on stdin and return auth code. +fn get_authcode_stdin() -> Result { + println!("Provide redirect URL"); + let mut buffer = String::new(); + let stdin = io::stdin(); + stdin + .read_line(&mut buffer) + .map_err(|_| OAuthError::AuthCodeStdinRead)?; + + get_code(buffer.trim()) +} + +/// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code. +fn get_authcode_listener(socket_address: SocketAddr) -> Result { + let listener = + TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind { + addr: socket_address, + e, + })?; + info!("OAuth server listening on {:?}", socket_address); + + // The server will terminate itself after collecting the first code. + let mut stream = listener + .incoming() + .flatten() + .next() + .ok_or(OAuthError::AuthCodeListenerTerminated)?; + let mut reader = BufReader::new(&stream); + let mut request_line = String::new(); + reader + .read_line(&mut request_line) + .map_err(|_| OAuthError::AuthCodeListenerRead)?; + + let redirect_url = request_line + .split_whitespace() + .nth(1) + .ok_or(OAuthError::AuthCodeListenerParse)?; + let code = get_code(&("http://localhost".to_string() + redirect_url)); + + let message = "Go back to your terminal :)"; + let response = format!( + "HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}", + message.len(), + message + ); + stream + .write_all(response.as_bytes()) + .map_err(|_| OAuthError::AuthCodeListenerWrite)?; + + code +} + +// If the specified `redirect_uri` is HTTP, loopback, and contains a port, +// then the corresponding socket address is returned. +fn get_socket_address(redirect_uri: &str) -> Option { + let url = match Url::parse(redirect_uri) { + Ok(u) if u.scheme() == "http" && u.port().is_some() => u, + _ => return None, + }; + let socket_addr = match url.socket_addrs(|| None) { + Ok(mut addrs) => addrs.pop(), + _ => None, + }; + if let Some(s) = socket_addr { + if s.ip().is_loopback() { + return socket_addr; + } + } + None +} + +/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow. +/// The redirect_uri must match what is registered to the client ID. +pub fn get_access_token( + client_id: &str, + redirect_uri: &str, + scopes: Vec<&str>, +) -> Result { + let auth_url = AuthUrl::new("https://accounts.spotify.com/authorize".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let token_url = TokenUrl::new("https://accounts.spotify.com/api/token".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let redirect_url = + RedirectUrl::new(redirect_uri.to_string()).map_err(|e| OAuthError::InvalidRedirectUri { + uri: redirect_uri.to_string(), + e, + })?; + let client = BasicClient::new( + ClientId::new(client_id.to_string()), + None, + auth_url, + Some(token_url), + ) + .set_redirect_uri(redirect_url); + + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + + // Generate the full authorization URL. + // Some of these scopes are unavailable for custom client IDs. Which? + let request_scopes: Vec = scopes + .clone() + .into_iter() + .map(|s| Scope::new(s.into())) + .collect(); + let (auth_url, _) = client + .authorize_url(CsrfToken::new_random) + .add_scopes(request_scopes) + .set_pkce_challenge(pkce_challenge) + .url(); + + println!("Browse to: {}", auth_url); + + let code = match get_socket_address(redirect_uri) { + Some(addr) => get_authcode_listener(addr), + _ => get_authcode_stdin(), + }?; + trace!("Exchange {code:?} for access token"); + + // Do this sync in another thread because I am too stupid to make the async version work. + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || { + let resp = client + .exchange_code(code) + .set_pkce_verifier(pkce_verifier) + .request(http_client); + if let Err(e) = tx.send(resp) { + error!("OAuth channel send error: {e}"); + } + }); + let token_response = rx.recv().map_err(|_| OAuthError::Recv)?; + let token = token_response.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + trace!("Obtained new access token: {token:?}"); + + let token_scopes: Vec = match token.scopes() { + Some(s) => s.iter().map(|s| s.to_string()).collect(), + _ => scopes.into_iter().map(|s| s.to_string()).collect(), + }; + let refresh_token = match token.refresh_token() { + Some(t) => t.secret().to_string(), + _ => "".to_string(), // Spotify always provides a refresh token. + }; + Ok(OAuthToken { + access_token: token.access_token().secret().to_string(), + refresh_token, + expires_at: Instant::now() + + token + .expires_in() + .unwrap_or_else(|| Duration::from_secs(3600)), + token_type: format!("{:?}", token.token_type()).to_string(), // Urgh!? + scopes: token_scopes, + }) +} + +#[cfg(test)] +mod test { + use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; + + use super::*; + + #[test] + fn get_socket_address_none() { + // No port + assert_eq!(get_socket_address("http://127.0.0.1/foo"), None); + assert_eq!(get_socket_address("http://127.0.0.1:/foo"), None); + assert_eq!(get_socket_address("http://[::1]/foo"), None); + // Not localhost + assert_eq!(get_socket_address("http://56.0.0.1:1234/foo"), None); + assert_eq!( + get_socket_address("http://[3ffe:2a00:100:7031::1]:1234/foo"), + None + ); + // Not http + assert_eq!(get_socket_address("https://127.0.0.1/foo"), None); + } + + #[test] + fn get_socket_address_localhost() { + let localhost_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234); + let localhost_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8888); + + assert_eq!( + get_socket_address("http://127.0.0.1:1234/foo"), + Some(localhost_v4) + ); + assert_eq!( + get_socket_address("http://[0:0:0:0:0:0:0:1]:8888/foo"), + Some(localhost_v6) + ); + assert_eq!( + get_socket_address("http://[::1]:8888/foo"), + Some(localhost_v6) + ); + } +} diff --git a/publish.sh b/publish.sh index c39f1c96..c9982c97 100755 --- a/publish.sh +++ b/publish.sh @@ -6,7 +6,7 @@ DRY_RUN='false' WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR -crates=( "protocol" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) +crates=( "protocol" "core" "discovery" "oauth" "audio" "metadata" "playback" "connect" "librespot" ) OS=`uname` function replace_in_file() { diff --git a/src/lib.rs b/src/lib.rs index 75211282..f6a17654 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,5 +5,6 @@ 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_oauth as oauth; pub use librespot_playback as playback; pub use librespot_protocol as protocol; diff --git a/src/main.rs b/src/main.rs index a6e54d44..2735ddf7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -168,6 +168,37 @@ fn get_version_string() -> String { ) } +/// Spotify's Desktop app uses these. Some of these are only available when requested with Spotify's client IDs. +static OAUTH_SCOPES: &[&str] = &[ + //const OAUTH_SCOPES: Vec<&str> = vec![ + "app-remote-control", + "playlist-modify", + "playlist-modify-private", + "playlist-modify-public", + "playlist-read", + "playlist-read-collaborative", + "playlist-read-private", + "streaming", + "ugc-image-upload", + "user-follow-modify", + "user-follow-read", + "user-library-modify", + "user-library-read", + "user-modify", + "user-modify-playback-state", + "user-modify-private", + "user-personalized", + "user-read-birthdate", + "user-read-currently-playing", + "user-read-email", + "user-read-play-history", + "user-read-playback-position", + "user-read-playback-state", + "user-read-private", + "user-read-recently-played", + "user-top-read", +]; + struct Setup { format: AudioFormat, backend: SinkBuilder, @@ -179,6 +210,8 @@ struct Setup { connect_config: ConnectConfig, mixer_config: MixerConfig, credentials: Option, + enable_oauth: bool, + oauth_port: Option, enable_discovery: bool, zeroconf_port: u16, player_event_program: Option, @@ -195,6 +228,7 @@ fn get_setup() -> Setup { const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; + const ACCESS_TOKEN: &str = "access-token"; const AP_PORT: &str = "ap-port"; const AUTOPLAY: &str = "autoplay"; const BACKEND: &str = "backend"; @@ -210,6 +244,7 @@ fn get_setup() -> Setup { const DISABLE_GAPLESS: &str = "disable-gapless"; const DITHER: &str = "dither"; const EMIT_SINK_EVENTS: &str = "emit-sink-events"; + const ENABLE_OAUTH: &str = "enable-oauth"; const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation"; const FORMAT: &str = "format"; const HELP: &str = "help"; @@ -226,6 +261,7 @@ fn get_setup() -> Setup { const NORMALISATION_PREGAIN: &str = "normalisation-pregain"; const NORMALISATION_RELEASE: &str = "normalisation-release"; const NORMALISATION_THRESHOLD: &str = "normalisation-threshold"; + const OAUTH_PORT: &str = "oauth-port"; const ONEVENT: &str = "onevent"; #[cfg(feature = "passthrough-decoder")] const PASSTHROUGH: &str = "passthrough"; @@ -260,6 +296,9 @@ fn get_setup() -> Setup { const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; const HELP_SHORT: &str = "h"; const ZEROCONF_INTERFACE_SHORT: &str = "i"; + const ENABLE_OAUTH_SHORT: &str = "j"; + const OAUTH_PORT_SHORT: &str = "K"; + const ACCESS_TOKEN_SHORT: &str = "k"; const CACHE_SIZE_LIMIT_SHORT: &str = "M"; const MIXER_TYPE_SHORT: &str = "m"; const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; @@ -381,6 +420,11 @@ fn get_setup() -> Setup { ENABLE_VOLUME_NORMALISATION, "Play all tracks at approximately the same apparent volume.", ) + .optflag( + ENABLE_OAUTH_SHORT, + ENABLE_OAUTH, + "Perform interactive OAuth sign in.", + ) .optopt( NAME_SHORT, NAME, @@ -457,6 +501,18 @@ fn get_setup() -> Setup { "Password used to sign in with.", "PASSWORD", ) + .optopt( + ACCESS_TOKEN_SHORT, + ACCESS_TOKEN, + "Spotify access token to sign in with.", + "TOKEN", + ) + .optopt( + OAUTH_PORT_SHORT, + OAUTH_PORT, + "The port the oauth redirect server uses 1 - 65535. Ports <= 1024 may require root privileges.", + "PORT", + ) .optopt( ONEVENT_SHORT, ONEVENT, @@ -670,7 +726,10 @@ fn get_setup() -> Setup { trace!("Environment variable(s):"); for (k, v) in &env_vars { - if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + if matches!( + k.as_str(), + "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME" | "LIBRESPOT_ACCESS_TOKEN" + ) { trace!("\t\t{k}=\"XXXXXXXX\""); } else if v.is_empty() { trace!("\t\t{k}="); @@ -702,7 +761,15 @@ fn get_setup() -> Setup { && matches.opt_defined(opt) && matches.opt_present(opt) { - if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { + if matches!( + opt, + PASSWORD + | PASSWORD_SHORT + | USERNAME + | USERNAME_SHORT + | ACCESS_TOKEN + | ACCESS_TOKEN_SHORT + ) { // Don't log creds. trace!("\t\t{opt} \"XXXXXXXX\""); } else { @@ -1081,44 +1148,32 @@ fn get_setup() -> Setup { } }; + let enable_oauth = opt_present(ENABLE_OAUTH); + let credentials = { let cached_creds = cache.as_ref().and_then(Cache::credentials); - if let Some(username) = opt_str(USERNAME) { + if let Some(access_token) = opt_str(ACCESS_TOKEN) { + if access_token.is_empty() { + empty_string_error_msg(ACCESS_TOKEN, ACCESS_TOKEN_SHORT); + } + Some(Credentials::with_access_token(access_token)) + } else if let Some(username) = opt_str(USERNAME) { if username.is_empty() { empty_string_error_msg(USERNAME, USERNAME_SHORT); } - if let Some(password) = opt_str(PASSWORD) { - if password.is_empty() { - empty_string_error_msg(PASSWORD, PASSWORD_SHORT); + if opt_present(PASSWORD) { + error!("Invalid `--{PASSWORD}` / `-{PASSWORD_SHORT}`: Password authentication no longer supported, use OAuth"); + exit(1); + } + match cached_creds { + Some(creds) if Some(username) == creds.username => { + trace!("Using cached credentials for specified username."); + Some(creds) } - Some(Credentials::with_password(username, password)) - } else { - match cached_creds { - Some(creds) if username == creds.username => Some(creds), - _ => { - let prompt = &format!("Password for {username}: "); - match rpassword::prompt_password(prompt) { - Ok(password) => { - if !password.is_empty() { - Some(Credentials::with_password(username, password)) - } else { - trace!("Password was empty."); - if cached_creds.is_some() { - trace!("Using cached credentials."); - } - cached_creds - } - } - Err(e) => { - warn!("Cannot parse password: {}", e); - if cached_creds.is_some() { - trace!("Using cached credentials."); - } - cached_creds - } - } - } + _ => { + trace!("No cached credentials for specified username."); + None } } } else { @@ -1131,11 +1186,39 @@ fn get_setup() -> Setup { let enable_discovery = !opt_present(DISABLE_DISCOVERY); - if credentials.is_none() && !enable_discovery { - error!("Credentials are required if discovery is disabled."); + if credentials.is_none() && !enable_discovery && !enable_oauth { + error!("Credentials are required if discovery and oauth login are disabled."); exit(1); } + let oauth_port = if opt_present(OAUTH_PORT) { + if !enable_oauth { + warn!( + "Without the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", + ENABLE_OAUTH, ENABLE_OAUTH_SHORT, OAUTH_PORT, OAUTH_PORT_SHORT + ); + } + opt_str(OAUTH_PORT) + .map(|port| match port.parse::() { + Ok(value) => { + if value > 0 { + Some(value) + } else { + None + } + } + _ => { + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(OAUTH_PORT, OAUTH_PORT_SHORT, &port, valid_values, ""); + + exit(1); + } + }) + .unwrap_or(None) + } else { + Some(5588) + }; + if !enable_discovery && opt_present(ZEROCONF_PORT) { warn!( "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", @@ -1643,6 +1726,8 @@ fn get_setup() -> Setup { connect_config, mixer_config, credentials, + enable_oauth, + oauth_port, enable_discovery, zeroconf_port, player_event_program, @@ -1718,6 +1803,24 @@ async fn main() { if let Some(credentials) = setup.credentials { last_credentials = Some(credentials); connecting = true; + } else if setup.enable_oauth { + let port_str = match setup.oauth_port { + Some(port) => format!(":{port}"), + _ => String::new(), + }; + let access_token = match librespot::oauth::get_access_token( + &setup.session_config.client_id, + &format!("http://127.0.0.1{port_str}/login"), + OAUTH_SCOPES.to_vec(), + ) { + Ok(token) => token.access_token, + Err(e) => { + error!("Failed to get Spotify access token: {e}"); + exit(1); + } + }; + last_credentials = Some(Credentials::with_access_token(access_token)); + connecting = true; } else if discovery.is_none() { error!( "Discovery is unavailable and no credentials provided. Authentication is not possible." From b85bab50950bf58d8af554b4b83b413ba573d2a8 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Fri, 13 Sep 2024 07:40:22 +0200 Subject: [PATCH 426/561] Remove .cargo/config.toml and set link compiler on CI with CARGO_TARGET__LINKER (#1333) * Remove .cargo/config.toml and set link compiler on CI with CARGO_TARGET__LINKER * Run windows and cross builds parallel to linux tests Signed-off-by: yubiuser --- .cargo/config.toml | 3 --- .github/workflows/test.yml | 23 ++++++++++++++++++----- .gitignore | 1 + 3 files changed, 19 insertions(+), 8 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index 36056447..00000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,3 +0,0 @@ - -[target.armv7-unknown-linux-gnueabihf] -linker = "arm-linux-gnueabihf-gcc" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66a430b4..30f58fed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -154,7 +154,7 @@ jobs: - run: cargo hack check --each-feature test-windows: - needs: test-linux + needs: clippy name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) runs-on: ${{ matrix.os }} continue-on-error: false @@ -201,7 +201,7 @@ jobs: test-cross-arm: name: cross +${{ matrix.toolchain }} build ${{ matrix.target }} - needs: test-linux + needs: clippy runs-on: ${{ matrix.os }} continue-on-error: false strategy: @@ -234,11 +234,24 @@ jobs: target key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - - name: Install the cross compiler targets + - name: Install the cross compiler rust targets run: rustup target add ${{ matrix.target }} - - name: Install cross compiler toolchain - run: sudo apt-get install -y gcc-arm-linux-gnueabihf + - name: Install cross compiler + run: | + if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then + sudo apt-get install -y gcc-arm-linux-gnueabihf + fi + + - name: Set target link compiler + run: | + # Convert target to uppercase and replace - with _ + target=${{ matrix.target }} + target=${target^^} + target=${target//-/_} + if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then + echo "CARGO_TARGET_${target}_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV + fi - name: Build run: cargo build --verbose --target ${{ matrix.target }} --no-default-features diff --git a/.gitignore b/.gitignore index 8b5fdeb6..eebf401d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ target +.cargo spotify_appkey.key .vagrant/ .project From fdf62d199d211d7dda8fab9cb08b89e7f1bd753b Mon Sep 17 00:00:00 2001 From: yubiuser Date: Fri, 13 Sep 2024 07:41:40 +0200 Subject: [PATCH 427/561] Add alpine based Dockerfile for Devcontainer (#1332) --- .devcontainer/Dockerfile.alpine | 32 ++++++++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 2 +- test.sh | 2 +- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .devcontainer/Dockerfile.alpine diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine new file mode 100644 index 00000000..2fe0236c --- /dev/null +++ b/.devcontainer/Dockerfile.alpine @@ -0,0 +1,32 @@ +# syntax=docker/dockerfile:1 +ARG alpine_version=alpine3.18 +ARG rust_version=1.74.0 +FROM rust:${rust_version}-${alpine_version} + +ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" +ENV RUST_BACKTRACE=1 +ENV RUSTFLAGS="-D warnings -A renamed-and-removed-lints -C target-feature=-crt-static" + + +RUN apk add --no-cache \ + git \ + nano\ + openssh-server \ + # for rust-analyzer vscode plugin + pkgconf \ + musl-dev \ + # developer dependencies + libunwind-dev \ + pulseaudio-dev \ + portaudio-dev \ + alsa-lib-dev \ + sdl2-dev \ + gstreamer-dev \ + gst-plugins-base-dev \ + jack-dev \ + avahi-dev && \ + rm -rf /lib/apk/db/* + +RUN rustup component add rustfmt && \ + rustup component add clippy && \ + cargo install cargo-hack diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ff6111a3..c3ab756a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,6 +1,6 @@ { "name": "Librespot Devcontainer", - "dockerFile": "Dockerfile", + "dockerFile": "Dockerfile.alpine", // Use 'postCreateCommand' to run commands after the container is created. //"postCreateCommand": "", "customizations": { diff --git a/test.sh b/test.sh index 0d0c34ad..7925f873 100755 --- a/test.sh +++ b/test.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh set -e From 8fab3d66e2bb9eb3356010d799701cf8a271c0c2 Mon Sep 17 00:00:00 2001 From: David Sheets Date: Sat, 14 Sep 2024 17:44:07 +0100 Subject: [PATCH 428/561] discovery::server: fix activeUser field of getInfo (#1235) --- discovery/src/server.rs | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 35f3993b..0e235f77 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -4,7 +4,7 @@ use std::{ convert::Infallible, net::{Ipv4Addr, SocketAddr, TcpListener}, pin::Pin, - sync::Arc, + sync::{Arc, Mutex}, task::{Context, Poll}, }; @@ -45,7 +45,7 @@ pub struct Config { struct RequestHandler { config: Config, - username: Option, + username: Mutex>, keys: DhLocalKeys, tx: mpsc::UnboundedSender, } @@ -56,7 +56,7 @@ impl RequestHandler { let discovery = Self { config, - username: None, + username: Mutex::new(None), keys: DhLocalKeys::random(&mut rand::thread_rng()), tx, }; @@ -64,13 +64,20 @@ impl RequestHandler { (discovery, rx) } + fn active_user(&self) -> String { + if let Ok(maybe_username) = self.username.lock() { + maybe_username.clone().unwrap_or(String::new()) + } else { + warn!("username lock corrupted; read failed"); + String::from("!") + } + } + 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 mut active_user = String::new(); - if let Some(username) = &self.username { - active_user = username.to_string(); - } + let active_user = self.active_user(); + // options based on zeroconf guide, search for `groupStatus` on page let group_status = if self.config.is_group { "GROUP" @@ -193,7 +200,15 @@ impl RequestHandler { let credentials = Credentials::with_blob(username, decrypted, &self.config.device_id)?; - self.tx.send(credentials)?; + { + let maybe_username = self.username.lock(); + self.tx.send(credentials)?; + if let Ok(mut username_field) = maybe_username { + *username_field = Some(String::from(username)); + } else { + warn!("username lock corrupted; write failed"); + } + } let result = json!({ "status": 101, From 0be490f58d77487050ead7a2cbd4d22d9a872f13 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Sat, 14 Sep 2024 18:46:14 +0200 Subject: [PATCH 429/561] Add aarch64 and riscv64 cross compilation targets (#1334) --- .github/workflows/test.yml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30f58fed..4b507255 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -199,7 +199,7 @@ jobs: - run: cargo check --no-default-features - run: cargo check - test-cross-arm: + test-cross-linux: name: cross +${{ matrix.toolchain }} build ${{ matrix.target }} needs: clippy runs-on: ${{ matrix.os }} @@ -208,7 +208,10 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - target: [armv7-unknown-linux-gnueabihf] + target: + - armv7-unknown-linux-gnueabihf + - aarch64-unknown-linux-gnu + - riscv64gc-unknown-linux-gnu toolchain: - "1.74" # MSRV (Minimum supported rust version) - stable @@ -242,6 +245,12 @@ jobs: if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then sudo apt-get install -y gcc-arm-linux-gnueabihf fi + if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then + sudo apt-get install -y gcc-aarch64-linux-gnu + fi + if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then + sudo apt-get install -y gcc-riscv64-linux-gnu + fi - name: Set target link compiler run: | @@ -252,7 +261,13 @@ jobs: if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then echo "CARGO_TARGET_${target}_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV fi - + if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then + echo "CARGO_TARGET_${target}_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + fi + if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then + echo "CARGO_TARGET_${target}_LINKER=riscv64-linux-gnu-gcc" >> $GITHUB_ENV + fi + - name: Build run: cargo build --verbose --target ${{ matrix.target }} --no-default-features From 338d8b90b295704c5d9945634b2548a6199f1af9 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Mon, 16 Sep 2024 13:19:12 +0300 Subject: [PATCH 430/561] Update lockfile (#1337) --- Cargo.lock | 408 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 378 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90b6d2cf..e217bb38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.14" @@ -190,6 +205,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "base64" version = "0.21.7" @@ -318,6 +339,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.5", +] + [[package]] name = "cipher" version = "0.4.4" @@ -479,7 +515,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -490,7 +526,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -543,7 +579,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -553,7 +589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core", - "syn 2.0.66", + "syn 2.0.76", ] [[package]] @@ -803,8 +839,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1040,6 +1078,25 @@ dependencies = [ "system-deps", ] +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.2.6", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.5" @@ -1051,7 +1108,7 @@ dependencies = [ "fnv", "futures-core", "futures-sink", - "http", + "http 1.1.0", "indexmap 2.2.6", "slab", "tokio", @@ -1080,7 +1137,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http", + "http 1.1.0", "httpdate", "mime", "sha1", @@ -1092,7 +1149,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http", + "http 1.1.0", ] [[package]] @@ -1142,6 +1199,17 @@ dependencies = [ "windows 0.52.0", ] +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.1.0" @@ -1153,6 +1221,17 @@ dependencies = [ "itoa", ] +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + [[package]] name = "http-body" version = "1.0.0" @@ -1160,7 +1239,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http", + "http 1.1.0", ] [[package]] @@ -1171,8 +1250,8 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http", - "http-body", + "http 1.1.0", + "http-body 1.0.0", "pin-project-lite", ] @@ -1194,6 +1273,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + [[package]] name = "hyper" version = "1.3.1" @@ -1203,9 +1306,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", - "http", - "http-body", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.0", "httparse", "httpdate", "itoa", @@ -1224,8 +1327,8 @@ dependencies = [ "bytes", "futures-util", "headers", - "http", - "hyper", + "http 1.1.0", + "hyper 1.3.1", "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", @@ -1236,6 +1339,20 @@ dependencies = [ "webpki", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.26.0" @@ -1243,8 +1360,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.1.0", + "hyper 1.3.1", "hyper-util", "log", "rustls 0.22.4", @@ -1262,8 +1379,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" dependencies = [ "futures-util", - "http", - "hyper", + "http 1.1.0", + "hyper 1.3.1", "hyper-util", "log", "rustls 0.23.9", @@ -1283,9 +1400,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http", - "http-body", - "hyper", + "http 1.1.0", + "http-body 1.0.0", + "hyper 1.3.1", "pin-project-lite", "socket2", "tokio", @@ -1294,6 +1411,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core 0.52.0", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "1.5.0" @@ -1469,6 +1609,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipnet" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -1692,6 +1838,7 @@ dependencies = [ "librespot-core", "librespot-discovery", "librespot-metadata", + "librespot-oauth", "librespot-playback", "librespot-protocol", "log", @@ -1715,7 +1862,7 @@ dependencies = [ "futures-core", "futures-util", "http-body-util", - "hyper", + "hyper 1.3.1", "hyper-util", "librespot-core", "log", @@ -1760,13 +1907,14 @@ dependencies = [ "futures-util", "governor", "hmac", - "http", + "http 1.1.0", "http-body-util", "httparse", - "hyper", + "hyper 1.3.1", "hyper-proxy2", "hyper-rustls 0.27.2", "hyper-util", + "librespot-oauth", "librespot-protocol", "log", "nonzero_ext", @@ -1815,7 +1963,7 @@ dependencies = [ "hex", "hmac", "http-body-util", - "hyper", + "hyper 1.3.1", "hyper-util", "libmdns", "librespot-core", @@ -1844,6 +1992,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "librespot-oauth" +version = "0.5.0-dev" +dependencies = [ + "env_logger", + "log", + "oauth2", + "thiserror", + "url", +] + [[package]] name = "librespot-playback" version = "0.5.0-dev" @@ -2177,6 +2336,26 @@ dependencies = [ "libc", ] +[[package]] +name = "oauth2" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +dependencies = [ + "base64 0.13.1", + "chrono", + "getrandom", + "http 0.2.12", + "rand", + "reqwest", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "thiserror", + "url", +] + [[package]] name = "object" version = "0.35.0" @@ -2615,6 +2794,47 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + [[package]] name = "ring" version = "0.17.8" @@ -2706,6 +2926,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + [[package]] name = "rustls" version = "0.22.4" @@ -2715,7 +2947,7 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -2730,7 +2962,7 @@ dependencies = [ "log", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.102.4", "subtle", "zeroize", ] @@ -2742,12 +2974,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", - "rustls-pemfile", + "rustls-pemfile 2.1.2", "rustls-pki-types", "schannel", "security-framework", ] +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + [[package]] name = "rustls-pemfile" version = "2.1.2" @@ -2764,6 +3005,16 @@ version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.102.4" @@ -2812,6 +3063,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "sdl2" version = "0.37.0" @@ -2889,6 +3150,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.6" @@ -2898,6 +3169,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" @@ -2909,6 +3192,17 @@ dependencies = [ "digest", ] +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shannon" version = "0.2.0" @@ -3129,6 +3423,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "synstructure" version = "0.13.1" @@ -3153,6 +3453,27 @@ dependencies = [ "windows 0.54.0", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -3287,6 +3608,16 @@ dependencies = [ "syn 2.0.76", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -3455,7 +3786,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http", + "http 1.1.0", "httparse", "log", "rand", @@ -3499,6 +3830,7 @@ dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -3703,6 +4035,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + [[package]] name = "which" version = "4.4.2" @@ -4017,6 +4355,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "write16" version = "1.0.0" From 22a8850fe98641c25f4b056dbcf36e332367d4cd Mon Sep 17 00:00:00 2001 From: yubiuser Date: Mon, 16 Sep 2024 19:42:55 +0200 Subject: [PATCH 431/561] Revert lint exception by setting minor version of protobuf (#1339) --- .devcontainer/Dockerfile | 2 +- .devcontainer/Dockerfile.alpine | 2 +- .github/workflows/test.yml | 2 +- Cargo.lock | 1182 +++++++++++++------------------ connect/Cargo.toml | 2 +- core/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- protocol/Cargo.toml | 2 +- 8 files changed, 497 insertions(+), 699 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 156630da..a2825067 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -6,7 +6,7 @@ FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" ENV RUST_BACKTRACE=1 -ENV RUSTFLAGS="-D warnings -A renamed-and-removed-lints" +ENV RUSTFLAGS="-D warnings" RUN apt-get update && \ diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index 2fe0236c..5abd17ad 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -5,7 +5,7 @@ FROM rust:${rust_version}-${alpine_version} ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" ENV RUST_BACKTRACE=1 -ENV RUSTFLAGS="-D warnings -A renamed-and-removed-lints -C target-feature=-crt-static" +ENV RUSTFLAGS="-D warnings -C target-feature=-crt-static" RUN apk add --no-cache \ diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4b507255..4e6e9f7b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -37,7 +37,7 @@ on: env: RUST_BACKTRACE: 1 - RUSTFLAGS: -D warnings -A renamed-and-removed-lints + RUSTFLAGS: -D warnings # The layering here is as follows: # 1. code formatting diff --git a/Cargo.lock b/Cargo.lock index e217bb38..ead3df35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,18 +4,18 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" [[package]] name = "aes" @@ -39,12 +39,13 @@ dependencies = [ [[package]] name = "alsa" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37fe60779335388a88c01ac6c3be40304d1e349de3ada3b15f7808bb90fa9dce" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.5.0", + "bitflags 2.6.0", + "cfg-if", "libc", ] @@ -75,9 +76,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.14" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" dependencies = [ "anstyle", "anstyle-parse", @@ -90,33 +91,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" [[package]] name = "anstyle-parse" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad186efb764318d35165f1758e7dcef3b10628e26d41a44bc5550652e6804391" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" dependencies = [ "windows-sys 0.52.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" dependencies = [ "anstyle", "windows-sys 0.52.0", @@ -124,25 +125,25 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" [[package]] name = "arrayvec" -version = "0.7.4" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -165,9 +166,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "aws-lc-rs" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -177,11 +178,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" dependencies = [ - "bindgen", + "bindgen 0.69.4", "cc", "cmake", "dunce", @@ -192,17 +193,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.72" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17c6a35df3749d2e8bb1b7b21a976d82b15548788d2735b9d82f329268f71a11" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -235,7 +236,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -248,10 +249,28 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.76", + "syn 2.0.77", "which", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.77", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -260,9 +279,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "block-buffer" @@ -281,9 +300,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5" +checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" [[package]] name = "byteorder" @@ -293,19 +312,19 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" [[package]] name = "cc" -version = "1.0.99" +version = "1.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" +checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" dependencies = [ "jobserver", "libc", - "once_cell", + "shlex", ] [[package]] @@ -351,7 +370,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -372,7 +391,7 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.3", + "libloading 0.8.5", ] [[package]] @@ -386,9 +405,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" [[package]] name = "combine" @@ -418,9 +437,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreaudio-rs" @@ -435,11 +454,11 @@ dependencies = [ [[package]] name = "coreaudio-sys" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f01585027057ff5f0a5bf276174ae4c1594a2c5bde93d5f46a016d76270f5a9" +checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" dependencies = [ - "bindgen", + "bindgen 0.70.1", ] [[package]] @@ -468,9 +487,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -515,7 +534,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -526,7 +545,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -563,33 +582,33 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0350b5cb0331628a5916d6c5c0b72e97393b8b6b03b47a9284f4e7f5a405ffd7" +checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" +checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "derive_builder_macro" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" +checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -604,17 +623,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "displaydoc" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - [[package]] name = "dns-sd" version = "0.1.3" @@ -633,9 +641,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dca9240753cf90908d7e4aac30f630662b02aebaa1b58a3cadabdb23385b58b" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" @@ -648,9 +656,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "0.1.0" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", "regex", @@ -658,9 +666,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.3" +version = "0.11.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" dependencies = [ "anstream", "anstyle", @@ -687,9 +695,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "fixedbitset" @@ -774,7 +782,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -847,27 +855,27 @@ dependencies = [ [[package]] name = "getset" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" dependencies = [ - "proc-macro-error", + "proc-macro-error2", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.77", ] [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "gio-sys" -version = "0.19.5" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4bdbef451b0f0361e7f762987cc6bebd5facab1d535e85a3cf1115dfb08db40" +checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef" dependencies = [ "glib-sys", "gobject-sys", @@ -878,11 +886,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.19.7" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52355166df21c7ed16b6a01f615669c7911ed74e27ef60eba339c0d2da12490" +checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "futures-channel", "futures-core", "futures-executor", @@ -900,22 +908,22 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.19.7" +version = "0.19.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70025dbfa1275cf7d0531c3317ba6270dae15d87e63342229d638246ff45202e" +checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "glib-sys" -version = "0.19.5" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767d23ead9bbdfcbb1c2242c155c8128a7d13dde7bf69c176f809546135e2282" +checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5" dependencies = [ "libc", "system-deps", @@ -929,9 +937,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.19.5" +version = "0.19.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3787b0bfacca12bb25f8f822b0dbee9f7e4a86e6469a29976d332d2c14c945b" +checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e" dependencies = [ "glib-sys", "libc", @@ -958,9 +966,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.22.5" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56b59fdce2dfacda226d4b1b71ce4700b2f04228909b52252c197d8e30bd54a6" +checksum = "5ca0b90646bb67fccf80d228f5333f2a0745526818ccefbf5a97326c76d30e4d" dependencies = [ "cfg-if", "futures-channel", @@ -983,9 +991,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.22.0" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50184e88d3462a796a5924fb329839c102b22f9383c1636323fa4ef5255dea92" +checksum = "1363313eb1931d66ac0b82c9b477fdd066af9dc118ea844966f85b6d99f261fd" dependencies = [ "futures-core", "futures-sink", @@ -998,9 +1006,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da6c85b35835fa484e1bd554830c6e724c99a7a7fcd0208ff1663c3c6d3c9cd" +checksum = "ed667453517b47754b9f9d28c096074e5d565f1cc48c6fa2483b1ea10d7688d3" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1011,9 +1019,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703e65074d4f5dce207e855d8f36697ec93f953e5412780dcffc70cb5483337e" +checksum = "3cae69bbfce34108009117803fb808b1ef4d88d476c9e3e2f5f536aab1f6ae75" dependencies = [ "cfg-if", "glib", @@ -1027,9 +1035,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfb1c22539b9b31fde5513fd68e2f0edeeaa5591d92daaaadbfa392736e820f" +checksum = "b11267dce74a92bad96fbd58c37c43e330113dc460a0771283f7d6c390b827b7" dependencies = [ "glib-sys", "gobject-sys", @@ -1041,9 +1049,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.22.0" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514c71195b53c7eced4842b66ca9149833e41cf6a1d949e45e2ca4a4fa929850" +checksum = "39d55668b23fc69f1843daa42b43d289c00fe38e9586c5453b134783d2dd75a3" dependencies = [ "atomic_refcell", "cfg-if", @@ -1055,9 +1063,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d8d11de9d94072657f2e9ca294b72326874a1e53de9f45613a9bf00773a5938" +checksum = "5448abb00c197e3ad306710293bf757303cbeab4036b5ccad21c7642b8bf00c9" dependencies = [ "glib-sys", "gobject-sys", @@ -1068,9 +1076,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.22.5" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4975a75279a9cf658bac1798dcf57100c6ec89fca7886572c8250ea4d94b76bd" +checksum = "71f147e7c6bc9313d5569eb15da61f6f64026ec69791922749de230583a07286" dependencies = [ "glib-sys", "gobject-sys", @@ -1090,7 +1098,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.2.6", + "indexmap", "slab", "tokio", "tokio-util", @@ -1099,9 +1107,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa82e28a107a8cc405f0839610bdc9b15f1e25ec7d696aa5cf173edbcb1486ab" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" dependencies = [ "atomic-waker", "bytes", @@ -1109,19 +1117,13 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.2.6", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.14.5" @@ -1234,9 +1236,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http 1.1.0", @@ -1251,15 +1253,15 @@ dependencies = [ "bytes", "futures-util", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.2" +version = "1.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3935c160d00ac752e09787e6e6bfc26494c2183cc922f1bc678a60d4733bc2" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" [[package]] name = "httpdate" @@ -1299,16 +1301,16 @@ dependencies = [ [[package]] name = "hyper" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.5", + "h2 0.4.6", "http 1.1.0", - "http-body 1.0.0", + "http-body 1.0.1", "httparse", "httpdate", "itoa", @@ -1328,11 +1330,11 @@ dependencies = [ "futures-util", "headers", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "tokio", "tokio-rustls 0.25.0", "tower-service", @@ -1361,11 +1363,11 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "log", "rustls 0.22.4", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -1374,17 +1376,17 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.2" +version = "0.27.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "log", - "rustls 0.23.9", - "rustls-native-certs", + "rustls 0.23.13", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -1393,16 +1395,16 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", - "http-body 1.0.0", - "hyper 1.3.1", + "http-body 1.0.1", + "hyper 1.4.1", "pin-project-lite", "socket2", "tokio", @@ -1434,124 +1436,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" - -[[package]] -name = "icu_properties" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1560,14 +1444,12 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "icu_normalizer", - "icu_properties", - "smallvec", - "utf8_iter", + "unicode-bidi", + "unicode-normalization", ] [[package]] @@ -1582,22 +1464,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - -[[package]] -name = "indexmap" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown", ] [[package]] @@ -1617,9 +1489,9 @@ checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" [[package]] name = "is_terminal_polyfill" -version = "1.70.0" +version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" [[package]] name = "itertools" @@ -1696,29 +1568,29 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin 0.5.2", + "spin", ] [[package]] @@ -1729,9 +1601,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" @@ -1745,12 +1617,12 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -1862,7 +1734,7 @@ dependencies = [ "futures-core", "futures-util", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "librespot-core", "log", @@ -1910,9 +1782,9 @@ dependencies = [ "http 1.1.0", "http-body-util", "httparse", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-proxy2", - "hyper-rustls 0.27.2", + "hyper-rustls 0.27.3", "hyper-util", "librespot-oauth", "librespot-protocol", @@ -1963,7 +1835,7 @@ dependencies = [ "hex", "hmac", "http-body-util", - "hyper 1.3.1", + "hyper 1.4.1", "hyper-util", "libmdns", "librespot-core", @@ -2050,12 +1922,6 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" -[[package]] -name = "litemap" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704" - [[package]] name = "lock_api" version = "0.4.12" @@ -2068,9 +1934,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "mach2" @@ -2083,9 +1949,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.2" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "mime" @@ -2101,22 +1967,23 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87dfd01fe195c66b572b37921ad8803d010623c0aca821bea2302239d155cdae" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "0.8.11" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi", "libc", "wasi", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2146,7 +2013,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "jni-sys", "log", "ndk-sys", @@ -2202,9 +2069,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.5" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", @@ -2253,7 +2120,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2296,35 +2163,25 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" dependencies = [ "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2358,9 +2215,9 @@ dependencies = [ [[package]] name = "object" -version = "0.35.0" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ec7ab813848ba4522158d5517a6093db1ded27575b070f4177b8d12b41db5e" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -2399,9 +2256,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" [[package]] name = "openssl-probe" @@ -2441,7 +2298,7 @@ dependencies = [ "redox_syscall", "smallvec", "thread-id", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -2482,7 +2339,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap 2.2.6", + "indexmap", ] [[package]] @@ -2502,7 +2359,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -2546,9 +2403,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "portaudio-rs" @@ -2579,9 +2436,12 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.17" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] [[package]] name = "prettyplease" @@ -2590,67 +2450,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "priority-queue" -version = "2.0.3" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70c501afe3a2e25c9bd219aa56ec1e04cdb3fcdd763055be268778c13fa82c1f" +checksum = "560bcab673ff7f6ca9e270c17bf3affd8a05e3bd9207f123b0d45076fd8197e8" dependencies = [ "autocfg", "equivalent", - "indexmap 2.2.6", + "indexmap", ] [[package]] name = "proc-macro-crate" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d37c51ca738a55da99dc0c4a34860fd675453b8b36209178c2249bb13651284" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ - "toml_edit 0.21.1", + "toml_edit", ] [[package]] -name = "proc-macro-error" -version = "1.0.4" +name = "proc-macro-error-attr2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" dependencies = [ - "proc-macro-error-attr", "proc-macro2", "quote", - "syn 1.0.109", - "version_check", ] [[package]] -name = "proc-macro-error-attr" -version = "1.0.4" +name = "proc-macro-error2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" dependencies = [ + "proc-macro-error-attr2", "proc-macro2", "quote", - "version_check", + "syn 2.0.77", ] [[package]] name = "proc-macro2" -version = "1.0.85" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58678a64de2fced2bdec6bca052a6716a0efe692d6e3f53d1bda6a1def64cfc0" +checksum = "0bcc343da15609eaecd65f8aa76df8dc4209d325131d8219358c0aaaebab0bf6" dependencies = [ "once_cell", "protobuf-support", @@ -2659,9 +2517,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32777b0b3f6538d9d2e012b3fad85c7e4b9244b5958d04a6415f4333782b7a77" +checksum = "c4d0cde5642ea4df842b13eb9f59ea6fafa26dcb43e3e1ee49120e9757556189" dependencies = [ "anyhow", "once_cell", @@ -2674,12 +2532,12 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cb37955261126624a25b5e6bda40ae34cf3989d52a783087ca6091b29b5642" +checksum = "1b0e9b447d099ae2c4993c0cbb03c7a9d6c937b17f2d56cfc0b1550e6fcfdb76" dependencies = [ "anyhow", - "indexmap 1.9.3", + "indexmap", "log", "protobuf", "protobuf-support", @@ -2690,9 +2548,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.4.0" +version = "3.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ed294a835b0f30810e13616b1cd34943c6d1e84a8f3b0dcfe466d256c3e7e7" +checksum = "f0766e3675a627c327e4b3964582594b0e8741305d628a98a5de75a1d15f99b9" dependencies = [ "thiserror", ] @@ -2709,9 +2567,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -2758,18 +2616,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -2845,7 +2703,7 @@ dependencies = [ "cfg-if", "getrandom", "libc", - "spin 0.9.8", + "spin", "untrusted", "windows-sys 0.52.0", ] @@ -2915,11 +2773,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "errno", "libc", "linux-raw-sys", @@ -2947,34 +2805,47 @@ dependencies = [ "log", "ring", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] [[package]] name = "rustls" -version = "0.23.9" +version = "0.23.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a218f0f6d05669de4eabfb24f31ce802035c952429d037507b4a4a39f0e60c5b" +checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" dependencies = [ "aws-lc-rs", "log", "once_cell", "rustls-pki-types", - "rustls-webpki 0.102.4", + "rustls-webpki 0.102.8", "subtle", "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.7.0" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.2", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", "rustls-pki-types", "schannel", "security-framework", @@ -2991,9 +2862,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64 0.22.1", "rustls-pki-types", @@ -3001,9 +2872,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -3017,9 +2888,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "aws-lc-rs", "ring", @@ -3050,11 +2921,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3098,11 +2969,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.5.0", + "bitflags 2.6.0", "core-foundation", "core-foundation-sys", "libc", @@ -3111,9 +2982,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3121,31 +2992,32 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.210" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "serde_json" -version = "1.0.117" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -3162,9 +3034,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -3268,12 +3140,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spin" version = "0.9.8" @@ -3299,12 +3165,6 @@ dependencies = [ "der", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "strsim" version = "0.11.1" @@ -3313,9 +3173,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" -version = "2.5.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "symphonia" @@ -3414,9 +3274,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.76" +version = "2.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" dependencies = [ "proc-macro2", "quote", @@ -3429,28 +3289,17 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - [[package]] name = "sysinfo" -version = "0.31.3" +version = "0.31.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b92e0bdf838cbc1c4c9ba14f9c97a7ec6cdcd1ae66b10e1e42775a25553f45d" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" dependencies = [ "core-foundation-sys", "libc", "memchr", "ntapi", - "windows 0.54.0", + "windows 0.57.0", ] [[package]] @@ -3489,47 +3338,48 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.14" +version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] name = "thread-id" -version = "4.2.1" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" dependencies = [ "libc", "winapi", @@ -3569,43 +3419,47 @@ dependencies = [ ] [[package]] -name = "tinystr" -version = "0.7.6" +name = "tinyvec" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ - "displaydoc", - "zerovec", + "tinyvec_macros", ] [[package]] -name = "tokio" -version = "1.38.0" +name = "tinyvec_macros" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] name = "tokio-macros" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", ] [[package]] @@ -3635,16 +3489,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.9", + "rustls 0.23.13", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -3653,14 +3507,14 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "becd34a233e7e31a3dbf7c7241b38320f57393dcae8e7324b0167d21b8e320b0" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.9", - "rustls-native-certs", + "rustls 0.23.13", + "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -3669,9 +3523,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -3682,47 +3536,36 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit", ] [[package]] name = "toml_datetime" -version = "0.6.6" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.21.1" +version = "0.22.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" dependencies = [ - "indexmap 2.2.6", - "toml_datetime", - "winnow 0.5.40", -] - -[[package]] -name = "toml_edit" -version = "0.22.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" -dependencies = [ - "indexmap 2.2.6", + "indexmap", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow", ] [[package]] @@ -3742,15 +3585,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -3779,9 +3622,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.23.0" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" dependencies = [ "byteorder", "bytes", @@ -3790,7 +3633,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.23.9", + "rustls 0.23.13", "rustls-pki-types", "sha1", "thiserror", @@ -3804,10 +3647,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] -name = "unicode-ident" -version = "1.0.12" +name = "unicode-bidi" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode-width" @@ -3823,9 +3681,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.1" +version = "2.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" dependencies = [ "form_urlencoded", "idna", @@ -3839,18 +3697,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - [[package]] name = "utf8parse" version = "0.2.2" @@ -3859,9 +3705,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "rand", @@ -3920,9 +3766,9 @@ checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3951,34 +3797,35 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" dependencies = [ "cfg-if", "js-sys", @@ -3988,9 +3835,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3998,28 +3845,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", + "syn 2.0.77", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" dependencies = [ "js-sys", "wasm-bindgen", @@ -4071,11 +3918,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4091,7 +3938,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" dependencies = [ "windows-core 0.52.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4101,7 +3948,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" dependencies = [ "windows-core 0.54.0", - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", ] [[package]] @@ -4110,7 +3967,7 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4120,7 +3977,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ "windows-result", - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", ] [[package]] @@ -4129,7 +4020,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", ] [[package]] @@ -4156,7 +4047,16 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", ] [[package]] @@ -4191,18 +4091,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] [[package]] @@ -4219,9 +4119,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" @@ -4237,9 +4137,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" @@ -4255,15 +4155,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" @@ -4279,9 +4179,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" @@ -4297,9 +4197,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" @@ -4315,9 +4215,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" @@ -4333,24 +4233,15 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.5.40" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "0.6.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" dependencies = [ "memchr", ] @@ -4365,47 +4256,11 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "yoke" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", - "synstructure", -] - [[package]] name = "zerocopy" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae87e3fcd617500e5d106f0380cf7b77f3c6092aae37191433159dda23cfb087" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", "zerocopy-derive", @@ -4413,34 +4268,13 @@ dependencies = [ [[package]] name = "zerocopy-derive" -version = "0.7.34" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e934569e47891f7d9411f1a451d947a60e000ab3bd24fbb970f000387d1b3b" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.76", -] - -[[package]] -name = "zerofrom" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", - "synstructure", + "syn 2.0.77", ] [[package]] @@ -4448,39 +4282,3 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.76", -] diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 14fad592..9035002e 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" form_urlencoded = "1.0" futures-util = "0.3" log = "0.4" -protobuf = "3" +protobuf = "3.5" rand = "0.8" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index 01919ba7..2cf7dde8 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -45,7 +45,7 @@ once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } priority-queue = "2.0" -protobuf = "3" +protobuf = "3.5" quick-xml = { version = "0.36.1", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index bdc2da38..607b26c0 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -13,7 +13,7 @@ async-trait = "0.1" byteorder = "1" bytes = "1" log = "0.4" -protobuf = "3" +protobuf = "3.5" thiserror = "1" uuid = { version = "1", default-features = false } serde = { version = "1.0", features = ["derive"] } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 28fb3b7f..b58ebcbe 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/librespot-org/librespot" edition = "2021" [dependencies] -protobuf = "3" +protobuf = "3.5" [build-dependencies] protobuf-codegen = "3" From fb5c0efdd65d87f1f506b9198227d64370b69f18 Mon Sep 17 00:00:00 2001 From: Christoph Gysin Date: Tue, 17 Sep 2024 17:37:20 +0300 Subject: [PATCH 432/561] Freeze dependencies used in CI (#1338) --- .github/workflows/test.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4e6e9f7b..619c51ed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -142,7 +142,8 @@ jobs: - name: Install developer package dependencies run: sudo apt-get update && sudo apt install -y libunwind-dev && 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 build --workspace --examples + - run: cargo fetch --locked + - run: cargo build --frozen --workspace --examples - run: cargo test --workspace - run: cargo install cargo-hack @@ -191,7 +192,8 @@ jobs: target key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - - run: cargo build --workspace --examples + - run: cargo fetch --locked + - run: cargo build --frozen --workspace --examples - run: cargo test --workspace - run: cargo install cargo-hack @@ -268,8 +270,10 @@ jobs: echo "CARGO_TARGET_${target}_LINKER=riscv64-linux-gnu-gcc" >> $GITHUB_ENV fi + - name: Fetch + run: cargo fetch --locked - name: Build - run: cargo build --verbose --target ${{ matrix.target }} --no-default-features + run: cargo build --frozen --verbose --target ${{ matrix.target }} --no-default-features - name: Check binary run: file target/${{ matrix.target }}/debug/librespot From 67d31959f5125d3fe5fab5e6e95436a01bb3cef6 Mon Sep 17 00:00:00 2001 From: fivebanger <14848554+fivebanger@users.noreply.github.com> Date: Thu, 19 Sep 2024 21:59:20 +0200 Subject: [PATCH 433/561] Fix 96kbps playback issue (issues #1236) (#1342) - add missing audio formats to message AudioFile - ensure that unknown formats gets mapped to DEFAULT_FORMAT --- playback/src/player.rs | 14 +++++++++++--- protocol/proto/media_manifest.proto | 4 ++++ protocol/proto/metadata.proto | 7 ++++++- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index b55e1230..43f63610 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -901,7 +901,7 @@ impl PlayerTrackLoader { } } - fn stream_data_rate(&self, format: AudioFileFormat) -> usize { + fn stream_data_rate(&self, format: AudioFileFormat) -> Option { let kbps = match format { AudioFileFormat::OGG_VORBIS_96 => 12, AudioFileFormat::OGG_VORBIS_160 => 20, @@ -913,9 +913,17 @@ impl PlayerTrackLoader { AudioFileFormat::MP3_160_ENC => 20, AudioFileFormat::AAC_24 => 3, AudioFileFormat::AAC_48 => 6, + AudioFileFormat::AAC_160 => 20, + AudioFileFormat::AAC_320 => 40, + AudioFileFormat::MP4_128 => 16, + AudioFileFormat::OTHER5 => 40, AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average + AudioFileFormat::UNKNOWN_FORMAT => { + error!("Unknown stream data rate"); + return None; + } }; - kbps * 1024 + Some(kbps * 1024) } async fn load_track( @@ -993,7 +1001,7 @@ impl PlayerTrackLoader { } }; - let bytes_per_second = self.stream_data_rate(format); + let bytes_per_second = self.stream_data_rate(format)?; // This is only a loop to be able to reload the file if an error occurred // while opening a cached file. diff --git a/protocol/proto/media_manifest.proto b/protocol/proto/media_manifest.proto index 6e280259..e9fe45fd 100644 --- a/protocol/proto/media_manifest.proto +++ b/protocol/proto/media_manifest.proto @@ -18,6 +18,10 @@ message AudioFile { MP3_160_ENC = 7; AAC_24 = 8; AAC_48 = 9; + AAC_160 = 10; + AAC_320 = 11; + MP4_128 = 12; + OTHER5 = 13; FLAC_FLAC = 16; } } diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index 056dbcfa..4c52e0ed 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -288,7 +288,7 @@ message ExternalId { message AudioFile { optional bytes file_id = 1; - optional Format format = 2; + optional Format format = 2 [default = UNKNOWN_FORMAT]; enum Format { OGG_VORBIS_96 = 0; OGG_VORBIS_160 = 1; @@ -300,7 +300,12 @@ message AudioFile { MP3_160_ENC = 7; AAC_24 = 8; AAC_48 = 9; + AAC_160 = 10; + AAC_320 = 11; + MP4_128 = 12; + OTHER5 = 13; FLAC_FLAC = 16; + UNKNOWN_FORMAT = 255; } } From e02e4ae224173fe63612e7542d656b0eadabc719 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Mon, 23 Sep 2024 19:44:51 +0100 Subject: [PATCH 434/561] core: retry Session access-point connection (#1345) Tries multiple access-points with a retry for each one, unless there was a login failure. --- CHANGELOG.md | 3 ++- core/src/connection/mod.rs | 23 +++++++++++++++++++++++ core/src/session.rs | 29 +++++++++++++++++++++++------ 3 files changed, 48 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 681762d3..7c70c72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,7 +56,8 @@ https://github.com/librespot-org/librespot - [core] `FileId` is moved out of `SpotifyId`. For now it will be re-exported. - [core] Report actual platform data on login - [core] Support `Session` authentication with a Spotify access token -- [core] `Credentials.username` is now an `Option` (breaking) +- [core] `Credentials.username` is now an `Option` (breaking) +- [core] `Session::connect` tries multiple access points, retrying each one. - [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot` now follows the setting in the Connect client that controls it. (breaking) - [metadata] Most metadata is now retrieved with the `spclient` (breaking) diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index b2e0356b..8fb596f9 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -68,6 +68,29 @@ pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result, + max_retries: u8, +) -> io::Result { + let mut num_retries = 0; + loop { + match connect(host, port, proxy).await { + Ok(f) => return Ok(f), + Err(e) => { + debug!("Connection failed: {e}"); + if num_retries < max_retries { + num_retries += 1; + debug!("Retry access point..."); + continue; + } + return Err(e); + } + } + } +} + pub async fn authenticate( transport: &mut Transport, credentials: Credentials, diff --git a/core/src/session.rs b/core/src/session.rs index 3944ce83..f934ed7b 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -144,13 +144,15 @@ impl Session { async fn connect_inner( &self, - access_point: SocketAddress, + access_point: &SocketAddress, credentials: Credentials, ) -> Result<(Credentials, Transport), Error> { - let mut transport = connection::connect( + const MAX_RETRIES: u8 = 1; + let mut transport = connection::connect_with_retry( &access_point.0, access_point.1, self.config().proxy.as_ref(), + MAX_RETRIES, ) .await?; let mut reusable_credentials = connection::authenticate( @@ -165,10 +167,11 @@ impl Session { trace!( "Reconnect using stored credentials as token authed sessions cannot use keymaster." ); - transport = connection::connect( + transport = connection::connect_with_retry( &access_point.0, access_point.1, self.config().proxy.as_ref(), + MAX_RETRIES, ) .await?; reusable_credentials = connection::authenticate( @@ -187,19 +190,32 @@ impl Session { credentials: Credentials, store_credentials: bool, ) -> Result<(), Error> { + // There currently happen to be 6 APs but anything will do to avoid an infinite loop. + const MAX_AP_TRIES: u8 = 6; + let mut num_ap_tries = 0; let (reusable_credentials, transport) = loop { let ap = self.apresolver().resolve("accesspoint").await?; info!("Connecting to AP \"{}:{}\"", ap.0, ap.1); - match self.connect_inner(ap, credentials.clone()).await { + match self.connect_inner(&ap, credentials.clone()).await { Ok(ct) => break ct, Err(e) => { + num_ap_tries += 1; + if MAX_AP_TRIES == num_ap_tries { + error!("Tried too many access points"); + return Err(e); + } if let Some(AuthenticationError::LoginFailed(ErrorCode::TryAnotherAP)) = e.error.downcast_ref::() { warn!("Instructed to try another access point..."); continue; - } else { + } else if let Some(AuthenticationError::LoginFailed(..)) = + e.error.downcast_ref::() + { return Err(e); + } else { + warn!("Try another access point..."); + continue; } } } @@ -567,7 +583,7 @@ impl Session { } pub fn shutdown(&self) { - debug!("Invalidating session"); + debug!("Shutdown: Invalidating session"); self.0.data.write().invalid = true; self.mercury().shutdown(); self.channel().shutdown(); @@ -624,6 +640,7 @@ where return Poll::Ready(Ok(())); } Some(Err(e)) => { + error!("Connection to server closed."); session.shutdown(); return Poll::Ready(Err(e)); } From 118a9e1a516028b75f2dbf7c50ed8a4f28fe20f5 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Wed, 25 Sep 2024 20:34:59 +0100 Subject: [PATCH 435/561] core: AP connection attempts have 3 sec timeout. (#1350) --- CHANGELOG.md | 1 + core/src/connection/mod.rs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c70c72c..2fb79f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ https://github.com/librespot-org/librespot - [core] Support `Session` authentication with a Spotify access token - [core] `Credentials.username` is now an `Option` (breaking) - [core] `Session::connect` tries multiple access points, retrying each one. +- [core] Each access point connection now timesout after 3 seconds. - [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot` now follows the setting in the Connect client that controls it. (breaking) - [metadata] Most metadata is now retrieved with the `spclient` (breaking) diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 8fb596f9..c46e5ad8 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -3,7 +3,7 @@ mod handshake; pub use self::{codec::ApCodec, handshake::handshake}; -use std::io; +use std::{io, time::Duration}; use futures_util::{SinkExt, StreamExt}; use num_traits::FromPrimitive; @@ -63,7 +63,8 @@ impl From for AuthenticationError { } pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result { - let socket = crate::socket::connect(host, port, proxy).await?; + const TIMEOUT: Duration = Duration::from_secs(3); + let socket = tokio::time::timeout(TIMEOUT, crate::socket::connect(host, port, proxy)).await??; handshake(socket).await } From 2b2eb22846b86044b28316fb07202790293856c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 21:31:02 +0200 Subject: [PATCH 436/561] Bump actions/checkout from 4.1.7 to 4.2.0 (#1354) Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.7 to 4.2.0. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4.1.7...v4.2.0) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 619c51ed..5b705b3a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -168,7 +168,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build - name: Install NASM @@ -219,7 +219,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.1.7 + uses: actions/checkout@v4.2.0 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From b1180fb6742dfaa79fed35e9aec0c55fd6b5d141 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Mon, 30 Sep 2024 20:31:26 +0100 Subject: [PATCH 437/561] Remove unused deps (#1352) --- Cargo.lock | 204 ++++++++++--------------------------------- Cargo.toml | 2 - audio/Cargo.toml | 2 - core/Cargo.toml | 1 - discovery/Cargo.toml | 1 - metadata/Cargo.toml | 1 - playback/Cargo.toml | 4 +- 7 files changed, 49 insertions(+), 166 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ead3df35..6cec8e6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,9 +137,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -178,9 +178,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234314bd569802ec87011d653d6815c6d7b9ffb969e9fee5b8b20ef860e8dce9" +checksum = "b3ddc4a5b231dd6958b140ff3151b6412b3f4321fab354f399eec8f14b06df62" dependencies = [ "bindgen 0.69.4", "cc", @@ -312,15 +312,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.19" +version = "1.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800" +checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" dependencies = [ "jobserver", "libc", @@ -661,7 +661,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" dependencies = [ "log", - "regex", ] [[package]] @@ -853,18 +852,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "getset" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" -dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "gimli" version = "0.31.0" @@ -1395,9 +1382,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da62f120a8a37763efb0cf8fdf264b884c7b8b9ac8660b900c8661030c00e6ba" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -1408,16 +1395,15 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1601,9 +1587,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.158" +version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" [[package]] name = "libloading" @@ -1714,13 +1700,11 @@ dependencies = [ "librespot-playback", "librespot-protocol", "log", - "rpassword", "sha1", "sysinfo", "thiserror", "tokio", "url", - "webpki", ] [[package]] @@ -1728,10 +1712,8 @@ name = "librespot-audio" version = "0.5.0-dev" dependencies = [ "aes", - "byteorder", "bytes", "ctr", - "futures-core", "futures-util", "http-body-util", "hyper 1.4.1", @@ -1773,7 +1755,6 @@ dependencies = [ "bytes", "data-encoding", "dns-sd", - "env_logger", "form_urlencoded", "futures-core", "futures-util", @@ -1825,7 +1806,6 @@ dependencies = [ "aes", "base64 0.22.1", "bytes", - "cfg-if", "ctr", "dns-sd", "form_urlencoded", @@ -1852,7 +1832,6 @@ name = "librespot-metadata" version = "0.5.0-dev" dependencies = [ "async-trait", - "byteorder", "bytes", "librespot-core", "librespot-protocol", @@ -1880,10 +1859,8 @@ name = "librespot-playback" version = "0.5.0-dev" dependencies = [ "alsa", - "byteorder", "cpal", "futures-util", - "glib", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -2256,9 +2233,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl-probe" @@ -2342,26 +2319,6 @@ dependencies = [ "indexmap", ] -[[package]] -name = "pin-project" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2397,15 +2354,15 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "portable-atomic" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" +checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" [[package]] name = "portaudio-rs" @@ -2455,9 +2412,9 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560bcab673ff7f6ca9e270c17bf3affd8a05e3bd9207f123b0d45076fd8197e8" +checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" dependencies = [ "autocfg", "equivalent", @@ -2473,28 +2430,6 @@ dependencies = [ "toml_edit", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.77", -] - [[package]] name = "proc-macro2" version = "1.0.86" @@ -2557,9 +2492,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.36.1" +version = "0.36.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a05e2e8efddfa51a84ca47cec303fac86c8541b686d37cac5efc0e094417bc" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" dependencies = [ "memchr", "serde", @@ -2616,9 +2551,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.4" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0884ad60e090bf1345b93da0a5de8923c93884cd03f40dfcfddd3b4bee661853" +checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" dependencies = [ "bitflags 2.6.0", ] @@ -2718,17 +2653,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "rpassword" -version = "7.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80472be3c897911d0137b2d2b9055faf6eeac5b14e324073d83bc17b191d7e3f" -dependencies = [ - "libc", - "rtoolbox", - "windows-sys 0.48.0", -] - [[package]] name = "rsa" version = "0.9.6" @@ -2749,16 +2673,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rtoolbox" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c247d24e63230cdb56463ae328478bd5eac8b8faa8c69461a77e8e323afac90e" -dependencies = [ - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2982,9 +2896,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -3034,9 +2948,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" dependencies = [ "serde", ] @@ -3357,18 +3271,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -3557,9 +3471,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "serde", @@ -3568,27 +3482,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - [[package]] name = "tower-service" version = "0.3.3" @@ -3660,18 +3553,18 @@ checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "untrusted" @@ -3715,9 +3608,9 @@ dependencies = [ [[package]] name = "vergen" -version = "9.0.0" +version = "9.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c32e7318e93a9ac53693b6caccfb05ff22e04a44c7cf8a279051f24c09da286f" +checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" dependencies = [ "anyhow", "derive_builder", @@ -3728,9 +3621,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbdc9746577cb4767f218d320ee0b623d415e8130332f8f562b910b61cc2c4e" +checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" dependencies = [ "anyhow", "derive_builder", @@ -3742,13 +3635,12 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e06bee42361e43b60f363bad49d63798d0f42fb1768091812270eca00c784720" +checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" dependencies = [ "anyhow", "derive_builder", - "getset", "rustversion", ] @@ -4239,9 +4131,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index fe2e38cf..d23be2e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,13 +59,11 @@ env_logger = { version = "0.11.2", default-features = false, features = ["color futures-util = { version = "0.3", default-features = false } getopts = "0.2" log = "0.4" -rpassword = "7.0" sha1 = "0.10" sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" -webpki = "0.22.4" [features] alsa-backend = ["librespot-playback/alsa-backend"] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index ec0945a5..4874d963 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -14,10 +14,8 @@ version = "0.5.0-dev" [dependencies] aes = "0.8" -byteorder = "1.4" bytes = "1" ctr = "0.9" -futures-core = "0.3" futures-util = "0.3" hyper = { version = "1.3", features = [] } hyper-util = { version = "0.1", features = ["client"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 2cf7dde8..1c8728ec 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -69,7 +69,6 @@ rand = "0.8" vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } [dev-dependencies] -env_logger = "0.11.2" tokio = { version = "1", features = ["macros", "parking_lot"] } [features] diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index f8b317dc..15e2777c 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -12,7 +12,6 @@ edition = "2021" aes = "0.8" base64 = "0.22" bytes = "1" -cfg-if = "1.0" ctr = "0.9" dns-sd = { version = "0.1.3", optional = true } form_urlencoded = "1.0" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 607b26c0..a5d90460 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -10,7 +10,6 @@ edition = "2021" [dependencies] async-trait = "0.1" -byteorder = "1" bytes = "1" log = "0.4" protobuf = "3.5" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 3e6b3962..c567ee56 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -21,7 +21,6 @@ path = "../metadata" version = "0.5.0-dev" [dependencies] -byteorder = "1" futures-util = "0.3" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } @@ -40,7 +39,6 @@ sdl2 = { version = "0.37", optional = true } gstreamer = { version = "0.22.1", optional = true } gstreamer-app = { version = "0.22.0", optional = true } gstreamer-audio = { version = "0.22.0", optional = true } -glib = { version = "0.19.2", optional = true } # Rodio dependencies rodio = { version = "0.19.0", optional = true, default-features = false } @@ -64,6 +62,6 @@ jackaudio-backend = ["jack"] rodio-backend = ["rodio", "cpal"] rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio", "glib"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio"] passthrough-decoder = ["ogg"] From 3781a089a69ce9883a299dfd191d90c9a5348819 Mon Sep 17 00:00:00 2001 From: yubiuser Date: Mon, 30 Sep 2024 22:05:32 +0200 Subject: [PATCH 438/561] Update `glib` and `gstreamer` (#1327) Signed-off-by: yubiuser --- Cargo.lock | 158 ++++++++++++++++++++++---------------------- playback/Cargo.toml | 9 +-- 2 files changed, 85 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6cec8e6b..0a97b0f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -143,7 +143,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -160,9 +160,9 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" @@ -249,7 +249,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.77", + "syn 2.0.79", "which", ] @@ -268,7 +268,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -318,9 +318,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.21" +version = "1.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07b1695e2c7e8fc85310cde85aeaab7e3097f593c91d209d3f9df76c928100f0" +checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" dependencies = [ "jobserver", "libc", @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.15.8" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" dependencies = [ "smallvec", "target-lexicon", @@ -534,7 +534,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -545,7 +545,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -598,7 +598,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -608,7 +608,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" dependencies = [ "derive_builder_core", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -781,7 +781,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -860,9 +860,9 @@ checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "gio-sys" -version = "0.19.8" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd743ba4714d671ad6b6234e8ab2a13b42304d0e13ab7eba1dcdd78a7d6d4ef" +checksum = "4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779" dependencies = [ "glib-sys", "gobject-sys", @@ -873,9 +873,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.19.9" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39650279f135469465018daae0ba53357942a5212137515777d5fdca74984a44" +checksum = "adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff" dependencies = [ "bitflags 2.6.0", "futures-channel", @@ -890,27 +890,26 @@ dependencies = [ "libc", "memchr", "smallvec", - "thiserror", ] [[package]] name = "glib-macros" -version = "0.19.9" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4429b0277a14ae9751350ad9b658b1be0abb5b54faa5bcdf6e74a3372582fad7" +checksum = "a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] name = "glib-sys" -version = "0.19.8" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c2dc18d3a82b0006d470b13304fbbb3e0a9bd4884cf985a60a7ed733ac2c4a5" +checksum = "5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b" dependencies = [ "libc", "system-deps", @@ -924,9 +923,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "gobject-sys" -version = "0.19.8" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e697e252d6e0416fd1d9e169bda51c0f1c926026c39ca21fbe8b1bb5c3b8b9e" +checksum = "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462" dependencies = [ "glib-sys", "libc", @@ -953,9 +952,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.22.7" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0b90646bb67fccf80d228f5333f2a0745526818ccefbf5a97326c76d30e4d" +checksum = "49ecf3bcfc2ceb82ce02437f53ff2fcaee5e7d45ae697ab64a018408749779b9" dependencies = [ "cfg-if", "futures-channel", @@ -978,9 +977,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1363313eb1931d66ac0b82c9b477fdd066af9dc118ea844966f85b6d99f261fd" +checksum = "54a4ec9f0d2037349c82f589c1cbfe788a62f4941851924bb7c3929a6a790007" dependencies = [ "futures-core", "futures-sink", @@ -993,9 +992,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed667453517b47754b9f9d28c096074e5d565f1cc48c6fa2483b1ea10d7688d3" +checksum = "08d5cac633c1ab7030c777c8c58c682a0c763bbc4127bccc370dabe39c01a12d" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1006,9 +1005,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cae69bbfce34108009117803fb808b1ef4d88d476c9e3e2f5f536aab1f6ae75" +checksum = "36d39b07213f83055fc705a384fa32ad581776b8e5b04c86f3a419ec5dfc0f81" dependencies = [ "cfg-if", "glib", @@ -1022,9 +1021,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11267dce74a92bad96fbd58c37c43e330113dc460a0771283f7d6c390b827b7" +checksum = "d84744e7ac8f8bc0cf76b7be40f2d5be12e6cf197e4c6ca9d3438109c21e2f51" dependencies = [ "glib-sys", "gobject-sys", @@ -1036,9 +1035,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39d55668b23fc69f1843daa42b43d289c00fe38e9586c5453b134783d2dd75a3" +checksum = "46ce7330d2995138a77192ea20961422ddee1578e1a47480acb820c43ceb0e2d" dependencies = [ "atomic_refcell", "cfg-if", @@ -1050,9 +1049,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5448abb00c197e3ad306710293bf757303cbeab4036b5ccad21c7642b8bf00c9" +checksum = "7796e694c21c215447811c9cff694dce1fc6e02b0bbafb75cd8583b6aefe9e5f" dependencies = [ "glib-sys", "gobject-sys", @@ -1063,9 +1062,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.22.6" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71f147e7c6bc9313d5569eb15da61f6f64026ec69791922749de230583a07286" +checksum = "cb3859929db32f26a35818d0d9ed82f0887c9221ca402ddefaea2bb99833d535" dependencies = [ "glib-sys", "gobject-sys", @@ -1861,6 +1860,7 @@ dependencies = [ "alsa", "cpal", "futures-util", + "glib", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -2097,7 +2097,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -2158,7 +2158,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -2233,9 +2233,12 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" +dependencies = [ + "portable-atomic", +] [[package]] name = "openssl-probe" @@ -2360,9 +2363,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "portable-atomic" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30538d42559de6b034bc76fd6dd4c38961b1ee5c6c56e3808c50128fdbc22ce" +checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" [[package]] name = "portaudio-rs" @@ -2407,7 +2410,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" dependencies = [ "proc-macro2", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -2551,18 +2554,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.6" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355ae415ccd3a04315d3f8246e86d67689ea74d88d915576e1589a351062a13b" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", @@ -2572,9 +2575,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", @@ -2583,9 +2586,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" @@ -2746,7 +2749,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -2759,7 +2762,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" dependencies = [ "openssl-probe", - "rustls-pemfile 2.1.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", "security-framework", @@ -2776,19 +2779,18 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.3" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" dependencies = [ - "base64 0.22.1", "rustls-pki-types", ] [[package]] name = "rustls-pki-types" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" [[package]] name = "rustls-webpki" @@ -2921,7 +2923,7 @@ checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -3188,9 +3190,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.77" +version = "2.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" dependencies = [ "proc-macro2", "quote", @@ -3239,9 +3241,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "6.2.2" +version = "7.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" dependencies = [ "cfg-expr", "heck", @@ -3258,9 +3260,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.12.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", @@ -3286,7 +3288,7 @@ checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -3373,7 +3375,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -3709,7 +3711,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", "wasm-bindgen-shared", ] @@ -3743,7 +3745,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3892,7 +3894,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -3903,7 +3905,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] @@ -4166,7 +4168,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.77", + "syn 2.0.79", ] [[package]] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index c567ee56..053f7aa5 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -34,11 +34,12 @@ alsa = { version = "0.9.0", 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.11", optional = true } +jack = { version = "0.11", optional = true } # jack >0.11 requires a MSRV of 1.80 sdl2 = { version = "0.37", optional = true } -gstreamer = { version = "0.22.1", optional = true } -gstreamer-app = { version = "0.22.0", optional = true } -gstreamer-audio = { version = "0.22.0", optional = true } +gstreamer = { version = "0.23.1", optional = true } +gstreamer-app = { version = "0.23.0", optional = true } +gstreamer-audio = { version = "0.23.0", optional = true } +glib = { version = "0.20.3", optional = true } # Rodio dependencies rodio = { version = "0.19.0", optional = true, default-features = false } From 9e9040b29053ea8a89325b8c1b386ed4881ae83a Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Sun, 6 Oct 2024 20:57:40 +0100 Subject: [PATCH 439/561] github: better bug reports please, stop removing context from logs (#1358) --- .github/ISSUE_TEMPLATE/bug_report.md | 29 +++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index db1159ae..cd235728 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -7,22 +7,33 @@ assignees: '' --- -**Describe the bug** -A clear and concise description of what the bug is. +### Look for similar bugs +Please check if there's [already an issue](https://github.com/librespot-org/librespot/issues) for your problem. +If you've only a "me too" comment to make, consider if a :+1: [reaction](https://github.blog/news-insights/product-news/add-reactions-to-pull-requests-issues-and-comments/) +will suffice. -**To reproduce** -Steps to reproduce the behavior: +### Description +A clear and concise description of what the problem is. + +### Version +What version(s) of *librespot* does this problem exist in? + +### How to reproduce +Steps to reproduce the behavior in *librespot* e.g. 1. Launch `librespot` with '...' 2. Connect with '...' 3. In the client click on '...' -4. See error +4. See some error/problem -**Log** -A full log so we may trace your problem (launch `librespot` with `--verbose`). Format the log as code. +### Log +* A *full* **debug** log so we may trace your problem (launch `librespot` with `--verbose`). +* Ideally contains your above steps to reproduce. +* Format the log as code ([help](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks)) or use a *non-expiring* [pastebin](https://pastebin.com/). +* Redact data you consider personal but do not remove/trim anything else. -**Host (what you are running `librespot` on):** +### Host (what you are running `librespot` on): - OS: [e.g. Linux] - Platform: [e.g. RPi 3B+] -**Additional context** +### Additional context Add any other context about the problem here. If your issue is related to sound playback, at a minimum specify the type and make of your output device. From 54ea9266df3887b884666b4a832d3278d221e9ce Mon Sep 17 00:00:00 2001 From: splitti <45425109+splitti@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:06:15 +0200 Subject: [PATCH 440/561] Update README.md (#1361) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index acd78d63..3ccb19cf 100644 --- a/README.md +++ b/README.md @@ -111,10 +111,10 @@ This is a non exhaustive list of projects that either use or have modified libre - [raspotify](https://github.com/dtcooper/raspotify) - A Spotify Connect client that mostly Just Works™ - [Spotifyd](https://github.com/Spotifyd/spotifyd) - A stripped down librespot UNIX daemon. - [rpi-audio-receiver](https://github.com/nicokaiser/rpi-audio-receiver) - easy Raspbian install scripts for Spotifyd, Bluetooth, Shairport and other audio receivers -- [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No playback -functionality. +- [Spotcontrol](https://github.com/badfortrains/spotcontrol) - A golang implementation of a Spotify Connect controller. No Playback functionality. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. - [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop. - [Snapcast](https://github.com/badaix/snapcast) - synchronised multi-room audio player that uses librespot as its source for Spotify content +- [MuPiBox](https://mupibox.de/) - Portable music box for Spotify and local media based on Raspberry Pi. Operated via touchscreen. Suitable for children and older people. From 469442f681eb6347f8ad0151e55927f44fd38c99 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Sun, 6 Oct 2024 21:08:53 +0100 Subject: [PATCH 441/561] bin: warn if using oauth without credential caching (#1362) --- src/main.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2735ddf7..f87f332e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1092,6 +1092,8 @@ fn get_setup() -> Setup { tmp_dir }); + let enable_oauth = opt_present(ENABLE_OAUTH); + let cache = { let volume_dir = opt_str(SYSTEM_CACHE) .or_else(|| opt_str(CACHE)) @@ -1139,16 +1141,20 @@ fn get_setup() -> Setup { ); } - match Cache::new(cred_dir, volume_dir, audio_dir, limit) { + let cache = match Cache::new(cred_dir.clone(), volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { warn!("Cannot create cache: {}", e); None } - } - }; + }; - let enable_oauth = opt_present(ENABLE_OAUTH); + if enable_oauth && (cache.is_none() || cred_dir.is_none()) { + warn!("Credential caching is unavailable, but advisable when using OAuth login."); + } + + cache + }; let credentials = { let cached_creds = cache.as_ref().and_then(Cache::credentials); From 8dce30a1694eb0a063971fa9d52fbd496ab92525 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 22:09:10 +0200 Subject: [PATCH 442/561] Bump actions/cache from 4.0.2 to 4.1.0 (#1364) Bumps [actions/cache](https://github.com/actions/cache) from 4.0.2 to 4.1.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4.0.2...v4.1.0) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5b705b3a..9ab928c7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.0 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.0 with: path: | ~/.cargo/registry/index @@ -183,7 +183,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.0 with: path: | ~/.cargo/registry/index @@ -230,7 +230,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.0.2 + uses: actions/cache@v4.1.0 with: path: | ~/.cargo/registry/index From 353c696554caa9656c2193551c54b416011b7acd Mon Sep 17 00:00:00 2001 From: yubiuser Date: Sun, 6 Oct 2024 22:10:02 +0200 Subject: [PATCH 443/561] Fix elided_named_lifetimes (#1365) Signed-off-by: yubiuser --- connect/src/spirc.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 200c3f83..6eb3ab82 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1534,7 +1534,7 @@ struct CommandSender<'a> { } impl<'a> CommandSender<'a> { - fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> CommandSender<'_> { + fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> Self { let mut frame = protocol::spirc::Frame::new(); // frame version frame.set_version(1); @@ -1549,13 +1549,13 @@ impl<'a> CommandSender<'a> { CommandSender { spirc, frame } } - fn recipient(mut self, recipient: &'a str) -> CommandSender<'_> { + fn recipient(mut self, recipient: &'a str) -> Self { self.frame.recipient.push(recipient.to_owned()); self } #[allow(dead_code)] - fn state(mut self, state: protocol::spirc::State) -> CommandSender<'a> { + fn state(mut self, state: protocol::spirc::State) -> Self { *self.frame.state.mut_or_insert_default() = state; self } From 0ddb3b4cb6971aa0f90c5ea62a2050faa35e5e83 Mon Sep 17 00:00:00 2001 From: Ernst Date: Sun, 6 Oct 2024 22:11:57 +0200 Subject: [PATCH 444/561] Exposing service to both IPv4 and IPv6, addressing remaining issues from # (#1366) * create dual stack socket on non-windows systems only --------- Co-authored-by: Anders Ballegaard --- CHANGELOG.md | 1 + discovery/src/server.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb79f05..494c0e85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ https://github.com/librespot-org/librespot - [core] `Credentials.username` is now an `Option` (breaking) - [core] `Session::connect` tries multiple access points, retrying each one. - [core] Each access point connection now timesout after 3 seconds. +- [core] Listen on both IPV4 and IPV6 on non-windows hosts - [main] `autoplay {on|off}` now acts as an override. If unspecified, `librespot` now follows the setting in the Connect client that controls it. (breaking) - [metadata] Most metadata is now retrieved with the `spclient` (breaking) diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 0e235f77..f3c979b9 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -2,7 +2,7 @@ use std::{ borrow::Cow, collections::BTreeMap, convert::Infallible, - net::{Ipv4Addr, SocketAddr, TcpListener}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}, pin::Pin, sync::{Arc, Mutex}, task::{Context, Poll}, @@ -266,7 +266,12 @@ pub struct DiscoveryServer { impl DiscoveryServer { pub fn new(config: Config, port: &mut u16) -> Result { let (discovery, cred_rx) = RequestHandler::new(config); - let address = SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port); + let address = if cfg!(windows) { + SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port) + } else { + // this creates a dual stack socket on non-windows systems + SocketAddr::new(Ipv6Addr::UNSPECIFIED.into(), *port) + }; let (close_tx, close_rx) = oneshot::channel(); From 8b769e035b978deea95d105441f01b65ac15f302 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Mon, 7 Oct 2024 06:54:16 +0100 Subject: [PATCH 445/561] core: audio key response timeout after 1.5s (#1360) --- core/src/audio_key.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 74be4258..0ffa8383 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, io::Write}; +use std::{collections::HashMap, io::Write, time::Duration}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use bytes::Bytes; @@ -20,6 +20,8 @@ pub enum AudioKeyError { Packet(u8), #[error("sequence {0} not pending")] Sequence(u32), + #[error("audio key response timeout")] + Timeout, } impl From for Error { @@ -29,6 +31,7 @@ impl From for Error { AudioKeyError::Channel => Error::aborted(err), AudioKeyError::Sequence(_) => Error::aborted(err), AudioKeyError::Packet(_) => Error::unimplemented(err), + AudioKeyError::Timeout => Error::aborted(err), } } } @@ -89,7 +92,14 @@ impl AudioKeyManager { }); self.send_key_request(seq, track, file)?; - rx.await? + const KEY_RESPONSE_TIMEOUT: Duration = Duration::from_millis(1500); + match tokio::time::timeout(KEY_RESPONSE_TIMEOUT, rx).await { + Err(_) => { + error!("Audio key response timeout"); + Err(AudioKeyError::Timeout.into()) + } + Ok(k) => k?, + } } fn send_key_request(&self, seq: u32, track: SpotifyId, file: FileId) -> Result<(), Error> { From 1ac238eae91f257c49aa50463a74584ac15cae7e Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Wed, 9 Oct 2024 12:21:53 +0100 Subject: [PATCH 446/561] Update contrib/Dockerfile image (#1367) --- contrib/Dockerfile | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 377ece7a..cf972582 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -13,17 +13,18 @@ # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend -FROM debian:stretch +FROM debian:bookworm -RUN echo "deb http://archive.debian.org/debian stretch main" > /etc/apt/sources.list -RUN echo "deb http://archive.debian.org/debian stretch-proposed-updates main" >> /etc/apt/sources.list -RUN echo "deb http://archive.debian.org/debian-security stretch/updates main" >> /etc/apt/sources.list +RUN echo "deb http://deb.debian.org/debian bookworm main" > /etc/apt/sources.list +RUN echo "deb http://deb.debian.org/debian bookworm-updates main" >> /etc/apt/sources.list +RUN echo "deb http://deb.debian.org/debian-security bookworm-security main" >> /etc/apt/sources.list RUN dpkg --add-architecture arm64 RUN dpkg --add-architecture armhf RUN dpkg --add-architecture armel RUN apt-get update +RUN apt-get install -y cmake libclang-dev RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf pkg-config RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf @@ -33,14 +34,15 @@ ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi RUN rustup target add arm-unknown-linux-gnueabihf +RUN cargo install bindgen-cli RUN mkdir /.cargo && \ echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config -ENV CARGO_TARGET_DIR /build -ENV CARGO_HOME /build/cache +ENV CARGO_TARGET_DIR=/build +ENV CARGO_HOME=/build/cache ENV PKG_CONFIG_ALLOW_CROSS=1 ENV PKG_CONFIG_PATH_aarch64-unknown-linux-gnu=/usr/lib/aarch64-linux-gnu/pkgconfig/ ENV PKG_CONFIG_PATH_arm-unknown-linux-gnueabihf=/usr/lib/arm-linux-gnueabihf/pkgconfig/ From 8ed5fef624e00730c671b9f1541a55b9dd88a7d7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Oct 2024 18:45:30 +0200 Subject: [PATCH 447/561] Bump actions/cache from 4.1.0 to 4.1.1 (#1370) --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9ab928c7..49194857 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.0 + uses: actions/cache@v4.1.1 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.0 + uses: actions/cache@v4.1.1 with: path: | ~/.cargo/registry/index @@ -183,7 +183,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.0 + uses: actions/cache@v4.1.1 with: path: | ~/.cargo/registry/index @@ -230,7 +230,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.0 + uses: actions/cache@v4.1.1 with: path: | ~/.cargo/registry/index From f43ed299708d4f265e0fb69a0313ba2f22b74331 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 12 Oct 2024 18:45:41 +0200 Subject: [PATCH 448/561] Bump actions/checkout from 4.2.0 to 4.2.1 (#1369) --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 49194857..e0a0203a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -168,7 +168,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.1 # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build - name: Install NASM @@ -219,7 +219,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.0 + uses: actions/checkout@v4.2.1 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From ed766d2b2ad5b33e49b98e4b8a2ea47080116974 Mon Sep 17 00:00:00 2001 From: Benedikt Date: Tue, 15 Oct 2024 13:36:17 +0200 Subject: [PATCH 449/561] Rework session keep-alive logic (#1359) we don't really know what the server expects and how quickly it usually reacts, so add some safety margin to avoid timing out too early --- Cargo.lock | 1 + core/Cargo.toml | 1 + core/src/session.rs | 430 ++++++++++++++++++++++++++++---------------- 3 files changed, 276 insertions(+), 156 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a97b0f8..61a8a135 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1777,6 +1777,7 @@ dependencies = [ "once_cell", "parking_lot", "pbkdf2", + "pin-project-lite", "priority-queue", "protobuf", "quick-xml", diff --git a/core/Cargo.toml b/core/Cargo.toml index 1c8728ec..c62e54de 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -44,6 +44,7 @@ num-traits = "0.2" once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } +pin-project-lite = "0.2" priority-queue = "2.0" protobuf = "3.5" quick-xml = { version = "0.36.1", features = ["serialize"] } diff --git a/core/src/session.rs b/core/src/session.rs index f934ed7b..b2887503 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -12,14 +12,18 @@ use std::{ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::TryStream; -use futures_util::{future, ready, StreamExt, TryStreamExt}; +use futures_util::StreamExt; use librespot_protocol::authentication::AuthenticationType; use num_traits::FromPrimitive; use once_cell::sync::OnceCell; use parking_lot::RwLock; +use pin_project_lite::pin_project; use quick_xml::events::Event; use thiserror::Error; -use tokio::{sync::mpsc, time::Instant}; +use tokio::{ + sync::mpsc, + time::{sleep, Duration as TokioDuration, Instant as TokioInstant, Sleep}, +}; use tokio_stream::wrappers::UnboundedReceiverStream; use crate::{ @@ -82,7 +86,6 @@ struct SessionData { time_delta: i64, invalid: bool, user_data: UserData, - last_ping: Option, } struct SessionInternal { @@ -240,6 +243,8 @@ impl Session { } } + // This channel serves as a buffer for packets and serializes access to the TcpStream, such + // that `self.send_packet` can return immediately and needs no additional synchronization. let (tx_connection, rx_connection) = mpsc::unbounded_channel(); self.0 .tx_connection @@ -250,17 +255,20 @@ impl Session { let sender_task = UnboundedReceiverStream::new(rx_connection) .map(Ok) .forward(sink); - let receiver_task = DispatchTask(stream, self.weak()); - let timeout_task = Session::session_timeout(self.weak()); - + let session_weak = self.weak(); tokio::spawn(async move { - let result = future::try_join3(sender_task, receiver_task, timeout_task).await; - - if let Err(e) = result { + if let Err(e) = sender_task.await { error!("{}", e); + if let Some(session) = session_weak.try_upgrade() { + if !session.is_invalid() { + session.shutdown(); + } + } } }); + tokio::spawn(DispatchTask::new(self.weak(), stream)); + Ok(()) } @@ -302,33 +310,6 @@ impl Session { .get_or_init(|| TokenProvider::new(self.weak())) } - /// Returns an error, when we haven't received a ping for too long (2 minutes), - /// which means that we silently lost connection to Spotify servers. - async fn session_timeout(session: SessionWeak) -> io::Result<()> { - // pings are sent every 2 minutes and a 5 second margin should be fine - const SESSION_TIMEOUT: Duration = Duration::from_secs(125); - - while let Some(session) = session.try_upgrade() { - if session.is_invalid() { - break; - } - let last_ping = session.0.data.read().last_ping.unwrap_or_else(Instant::now); - if last_ping.elapsed() >= SESSION_TIMEOUT { - session.shutdown(); - // TODO: Optionally reconnect (with cached/last credentials?) - return Err(io::Error::new( - io::ErrorKind::TimedOut, - "session lost connection to server", - )); - } - // drop the strong reference before sleeping - drop(session); - // a potential timeout cannot occur at least until SESSION_TIMEOUT after the last_ping - tokio::time::sleep_until(last_ping + SESSION_TIMEOUT).await; - } - Ok(()) - } - pub fn time_delta(&self) -> i64 { self.0.data.read().time_delta } @@ -361,96 +342,6 @@ impl Session { } } - 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; - let timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::ZERO) - .as_secs() as i64; - - { - let mut data = self.0.data.write(); - data.time_delta = server_timestamp.saturating_sub(timestamp); - data.last_ping = Some(Instant::now()); - } - - self.debug_info(); - self.send_packet(Pong, vec![0, 0, 0, 0]) - } - Some(CountryCode) => { - let country = String::from_utf8(data.as_ref().to_owned())?; - info!("Country: {:?}", country); - self.0.data.write().user_data.country = country; - Ok(()) - } - 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(cmd, data) - } - Some(ProductInfo) => { - let data = std::str::from_utf8(&data)?; - 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_into(&mut buf) { - Ok(Event::Start(ref element)) => { - std::str::from_utf8(element)?.clone_into(&mut current_element) - } - 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()?.to_string()); - } - } - Ok(Event::Eof) => break, - Ok(_) => (), - Err(e) => warn!( - "Error parsing XML at position {}: {:?}", - reader.buffer_position(), - e - ), - } - } - - trace!("Received product info: {:#?}", user_attributes); - Self::check_catalogue(&user_attributes); - - self.0.data.write().user_data.attributes = user_attributes; - Ok(()) - } - Some(PongAck) - | Some(SecretBlock) - | Some(LegacyWelcome) - | Some(UnknownDataAllZeros) - | Some(LicenseVersion) => Ok(()), - _ => { - trace!("Ignoring {:?} packet with data {:#?}", cmd, data); - Err(SessionError::Packet(cmd as u8).into()) - } - } - } - pub fn send_packet(&self, cmd: PacketType, data: Vec) -> Result<(), Error> { match self.0.tx_connection.get() { Some(tx) => Ok(tx.send((cmd as u8, data))?), @@ -614,50 +505,277 @@ impl Drop for SessionInternal { } } -struct DispatchTask(S, SessionWeak) -where - S: TryStream + Unpin; +#[derive(Clone, Copy, Default, Debug, PartialEq)] +enum KeepAliveState { + #[default] + // Expecting a Ping from the server, either after startup or after a PongAck. + ExpectingPing, -impl Future for DispatchTask + // We need to send a Pong at the given time. + PendingPong, + + // We just sent a Pong and wait for it be ACK'd. + ExpectingPongAck, +} + +const INITIAL_PING_TIMEOUT: TokioDuration = TokioDuration::from_secs(20); +const PING_TIMEOUT: TokioDuration = TokioDuration::from_secs(80); // 60s expected + 20s buffer +const PONG_DELAY: TokioDuration = TokioDuration::from_secs(60); +const PONG_ACK_TIMEOUT: TokioDuration = TokioDuration::from_secs(20); + +impl KeepAliveState { + fn debug(&self, sleep: &Sleep) { + let delay = sleep + .deadline() + .checked_duration_since(TokioInstant::now()) + .map(|t| t.as_secs_f64()) + .unwrap_or(f64::INFINITY); + + trace!("keep-alive state: {:?}, timeout in {:.1}", self, delay); + } +} + +pin_project! { + struct DispatchTask + where + S: TryStream + { + session: SessionWeak, + keep_alive_state: KeepAliveState, + #[pin] + stream: S, + #[pin] + timeout: Sleep, + } + + impl PinnedDrop for DispatchTask + where + S: TryStream + { + fn drop(_this: Pin<&mut Self>) { + debug!("drop Dispatch"); + } + } +} + +impl DispatchTask where - S: TryStream + Unpin, - ::Ok: std::fmt::Debug, + S: TryStream, { - type Output = Result<(), S::Error>; + fn new(session: SessionWeak, stream: S) -> Self { + Self { + session, + keep_alive_state: KeepAliveState::ExpectingPing, + stream, + timeout: sleep(INITIAL_PING_TIMEOUT), + } + } - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let session = match self.1.try_upgrade() { - Some(session) => session, - None => return Poll::Ready(Ok(())), + fn dispatch( + mut self: Pin<&mut Self>, + session: &Session, + cmd: u8, + data: Bytes, + ) -> Result<(), Error> { + use KeepAliveState::*; + 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()); + } }; - loop { - let (cmd, data) = match ready!(self.0.try_poll_next_unpin(cx)) { - Some(Ok(t)) => t, - None => { - warn!("Connection to server closed."); - session.shutdown(); - return Poll::Ready(Ok(())); + match packet_type { + Some(Ping) => { + trace!("Received Ping"); + if self.keep_alive_state != ExpectingPing { + warn!("Received unexpected Ping from server") } - Some(Err(e)) => { - error!("Connection to server closed."); - session.shutdown(); - return Poll::Ready(Err(e)); - } - }; + let mut this = self.as_mut().project(); + *this.keep_alive_state = PendingPong; + this.timeout + .as_mut() + .reset(TokioInstant::now() + PONG_DELAY); + this.keep_alive_state.debug(&this.timeout); - if let Err(e) = session.dispatch(cmd, data) { - debug!("could not dispatch command: {}", e); + let server_timestamp = BigEndian::read_u32(data.as_ref()) as i64; + let timestamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() as i64; + { + let mut data = session.0.data.write(); + data.time_delta = server_timestamp.saturating_sub(timestamp); + } + + session.debug_info(); + + Ok(()) + } + Some(PongAck) => { + trace!("Received PongAck"); + if self.keep_alive_state != ExpectingPongAck { + warn!("Received unexpected PongAck from server") + } + let mut this = self.as_mut().project(); + *this.keep_alive_state = ExpectingPing; + this.timeout + .as_mut() + .reset(TokioInstant::now() + PING_TIMEOUT); + this.keep_alive_state.debug(&this.timeout); + + Ok(()) + } + Some(CountryCode) => { + let country = String::from_utf8(data.as_ref().to_owned())?; + info!("Country: {:?}", country); + session.0.data.write().user_data.country = country; + Ok(()) + } + Some(StreamChunkRes) | Some(ChannelError) => session.channel().dispatch(cmd, data), + Some(AesKey) | Some(AesKeyError) => session.audio_key().dispatch(cmd, data), + Some(MercuryReq) | Some(MercurySub) | Some(MercuryUnsub) | Some(MercuryEvent) => { + session.mercury().dispatch(cmd, data) + } + Some(ProductInfo) => { + let data = std::str::from_utf8(&data)?; + 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_into(&mut buf) { + Ok(Event::Start(ref element)) => { + std::str::from_utf8(element)?.clone_into(&mut current_element) + } + 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()?.to_string()); + } + } + Ok(Event::Eof) => break, + Ok(_) => (), + Err(e) => warn!( + "Error parsing XML at position {}: {:?}", + reader.buffer_position(), + e + ), + } + } + + trace!("Received product info: {:#?}", user_attributes); + Session::check_catalogue(&user_attributes); + + session.0.data.write().user_data.attributes = user_attributes; + Ok(()) + } + Some(SecretBlock) + | Some(LegacyWelcome) + | Some(UnknownDataAllZeros) + | Some(LicenseVersion) => Ok(()), + _ => { + trace!("Ignoring {:?} packet with data {:#?}", cmd, data); + Err(SessionError::Packet(cmd as u8).into()) } } } } -impl Drop for DispatchTask +impl Future for DispatchTask where - S: TryStream + Unpin, + S: TryStream, + ::Ok: std::fmt::Debug, { - fn drop(&mut self) { - debug!("drop Dispatch"); + type Output = Result<(), S::Error>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + use KeepAliveState::*; + + let session = match self.session.try_upgrade() { + Some(session) => session, + None => return Poll::Ready(Ok(())), + }; + + // Process all messages that are immediately ready + loop { + match self.as_mut().project().stream.try_poll_next(cx) { + Poll::Ready(Some(Ok((cmd, data)))) => { + let result = self.as_mut().dispatch(&session, cmd, data); + if let Err(e) = result { + debug!("could not dispatch command: {}", e); + } + } + Poll::Ready(None) => { + warn!("Connection to server closed."); + session.shutdown(); + return Poll::Ready(Ok(())); + } + Poll::Ready(Some(Err(e))) => { + error!("Connection to server closed."); + session.shutdown(); + return Poll::Ready(Err(e)); + } + Poll::Pending => break, + } + } + + // Handle the keep-alive sequence, returning an error when we haven't received a + // Ping/PongAck for too long. + // + // The expected keepalive sequence is + // - Server: Ping + // - wait 60s + // - Client: Pong + // - Server: PongAck + // - wait 60s + // - repeat + // + // This means that we silently lost connection to Spotify servers if + // - we don't receive Ping immediately after connecting, + // - we don't receive a Ping 60s after the last PongAck or + // - we don't receive a PongAck immediately after our Pong. + // + // Currently, we add a safety margin of 20s to these expected deadlines. + let mut this = self.as_mut().project(); + if let Poll::Ready(()) = this.timeout.as_mut().poll(cx) { + match this.keep_alive_state { + ExpectingPing | ExpectingPongAck => { + if !session.is_invalid() { + session.shutdown(); + } + // TODO: Optionally reconnect (with cached/last credentials?) + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::TimedOut, + format!( + "session lost connection to server ({:?})", + this.keep_alive_state + ), + ))); + } + PendingPong => { + trace!("Sending Pong"); + // TODO: Ideally, this should flush the `Framed as Sink` + // before starting the timeout. + let _ = session.send_packet(PacketType::Pong, vec![0, 0, 0, 0]); + *this.keep_alive_state = ExpectingPongAck; + this.timeout + .as_mut() + .reset(TokioInstant::now() + PONG_ACK_TIMEOUT); + this.keep_alive_state.debug(&this.timeout); + } + } + } + + Poll::Pending } } From b629f61f6762c4793edd98cb4d631f47b5f2b2c5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 19:00:51 +0200 Subject: [PATCH 450/561] Prepare for v0.5.0 release --- CHANGELOG.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 494c0e85..dc4a43a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,9 @@ All notable changes to this project will be documented in this file. 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. -## [0.5.0-dev] - YYYY-MM-DD +## [0.5.0-dev] - 2024-10-15 -This version will be a major departure from the architecture up until now. It +This version is be a major departure from the architecture up until now. It focuses on implementing the "new Spotify API". This means moving large parts of the Spotify protocol from Mercury to HTTP. A lot of this was reverse engineered before by @devgianlu of librespot-java. It was long overdue that we @@ -17,7 +17,7 @@ hopefully upcoming Spotify HiFi depend on it. Splitting up the work on the new Spotify API, v0.5.0 brings HTTP-based file downloads and metadata access. Implementing the "dealer" (replacing the current Mercury-based SPIRC message bus with WebSockets, also required for social plays) -is separate large effort, to be targeted for v0.6.0. +is a large and separate effort, slated for some later release. While at it, we are taking the liberty to do some major refactoring to make librespot more robust. Consequently not only the Spotify API changed but large @@ -39,6 +39,7 @@ https://github.com/librespot-org/librespot - [all] Use a single `player` instance. Eliminates occasional `player` and `audio backend` restarts, which can cause issues with some playback configurations. +- [all] Updated and removed unused dependencies - [audio] Files are now downloaded over the HTTPS CDN (breaking) - [audio] Improve file opening and seeking performance (breaking) - [core] MSRV is now 1.74 (breaking) @@ -46,6 +47,7 @@ https://github.com/librespot-org/librespot - [connect] Update and expose all `spirc` context fields (breaking) - [connect] Add `Clone, Defaut` traits to `spirc` contexts - [connect] Autoplay contexts are now retrieved with the `spclient` (breaking) +- [contrib] Updated Docker image - [core] Message listeners are registered before authenticating. As a result there now is a separate `Session::new` and subsequent `session.connect`. (breaking) @@ -77,6 +79,7 @@ https://github.com/librespot-org/librespot - [all] Check that array indexes are within bounds (panic safety) - [all] Wrap errors in librespot `Error` type (breaking) +- [audio] Make audio fetch parameters tunable - [connect] Add option on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD. - [connect] Add session events - [connect] Add `repeat`, `set_position_ms` and `set_volume` to `spirc.rs` @@ -96,6 +99,8 @@ https://github.com/librespot-org/librespot - [core] Support parsing `SpotifyId` for local files - [core] Support parsing `SpotifyId` for named playlists - [core] Add checks and handling for stale server connections. +- [core] Fix potential deadlock waiting for audio decryption keys. +- [discovery] Add option to show playback device as a group - [main] Add all player events to `player_event_handler.rs` - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers @@ -119,11 +124,15 @@ https://github.com/librespot-org/librespot - [connect] Loading previous or next tracks, or looping back on repeat, will only start playback when we were already playing - [connect, playback] Clean up and de-noise events and event firing +- [core] Fixed frequent disconnections for some users +- [core] More strict Spotify ID parsing +- [discovery] Update active user field upon connection - [playback] Handle invalid track start positions by just starting the track from the beginning - [playback] Handle disappearing and invalid devices better - [playback] Handle seek, pause, and play commands while loading - [playback] Handle disabled normalisation correctly when using fixed volume +- [playback] Do not stop sink in gapless mode - [metadata] Fix missing colon when converting named spotify IDs to URIs ## [0.4.2] - 2022-07-29 From 84d28e887b0c9b8e07dcbf890a17cdfe7f3cd8d1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 20:34:15 +0200 Subject: [PATCH 451/561] Update Cargo.lock --- Cargo.lock | 229 ++++++++++++++++++++--------------------- Cargo.toml | 18 ++-- Cargo.toml-e | 100 ++++++++++++++++++ audio/Cargo.toml | 4 +- audio/Cargo.toml-e | 27 +++++ connect/Cargo.toml | 8 +- connect/Cargo.toml-e | 33 ++++++ core/Cargo.toml | 6 +- core/Cargo.toml-e | 76 ++++++++++++++ discovery/Cargo.toml | 4 +- discovery/Cargo.toml-e | 42 ++++++++ metadata/Cargo.toml | 6 +- metadata/Cargo.toml-e | 27 +++++ oauth/Cargo.toml | 2 +- oauth/Cargo.toml-e | 18 ++++ playback/Cargo.toml | 8 +- playback/Cargo.toml-e | 68 ++++++++++++ protocol/Cargo.toml | 2 +- protocol/Cargo.toml-e | 16 +++ 19 files changed, 549 insertions(+), 145 deletions(-) create mode 100644 Cargo.toml-e create mode 100644 audio/Cargo.toml-e create mode 100644 connect/Cargo.toml-e create mode 100644 core/Cargo.toml-e create mode 100644 discovery/Cargo.toml-e create mode 100644 metadata/Cargo.toml-e create mode 100644 oauth/Cargo.toml-e create mode 100644 playback/Cargo.toml-e create mode 100644 protocol/Cargo.toml-e diff --git a/Cargo.lock b/Cargo.lock index 61a8a135..a20c2d43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 3 [[package]] name = "addr2line" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] @@ -166,9 +166,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -178,11 +178,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ddc4a5b231dd6958b140ff3151b6412b3f4321fab354f399eec8f14b06df62" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" dependencies = [ - "bindgen 0.69.4", + "bindgen 0.69.5", "cc", "cmake", "dunce", @@ -232,9 +232,9 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.6.0", "cexpr", @@ -300,9 +300,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bbb0ad554ad961ddc5da507a12a29b14e4ae5bda06b19f575a3e6079d2e2ae" +checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" [[package]] name = "byteorder" @@ -318,9 +318,9 @@ checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" [[package]] name = "cc" -version = "1.1.23" +version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bbb537bb4a30b90362caddba8f360c0a56bc13d3a5570028e7197204cb54a17" +checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ "jobserver", "libc", @@ -582,18 +582,18 @@ dependencies = [ [[package]] name = "derive_builder" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd33f37ee6a119146a1781d3356a7c26028f83d779b2e04ecd45fdc75c76877b" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" dependencies = [ "derive_builder_macro", ] [[package]] name = "derive_builder_core" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7431fa049613920234f22c47fdc33e6cf3ee83067091ea4277a3f8c4587aae38" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" dependencies = [ "darling", "proc-macro2", @@ -603,9 +603,9 @@ dependencies = [ [[package]] name = "derive_builder_macro" -version = "0.20.1" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4abae7035bf79b9877b779505d8cf3749285b80c43941eda66604841889451dc" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", "syn 2.0.79", @@ -727,9 +727,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -742,9 +742,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -752,15 +752,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -769,15 +769,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -786,15 +786,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -804,9 +804,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -854,9 +854,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" @@ -1112,9 +1112,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.5" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" [[package]] name = "headers" @@ -1245,9 +1245,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -1263,9 +1263,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -1287,9 +1287,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -1316,7 +1316,7 @@ dependencies = [ "futures-util", "headers", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", @@ -1335,7 +1335,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -1349,7 +1349,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", "rustls 0.22.4", @@ -1368,10 +1368,10 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -1390,7 +1390,7 @@ dependencies = [ "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2", "tokio", @@ -1449,9 +1449,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", "hashbrown", @@ -1468,9 +1468,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.10.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "is_terminal_polyfill" @@ -1562,9 +1562,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -1684,7 +1684,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "data-encoding", "env_logger", @@ -1708,14 +1708,14 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "aes", "bytes", "ctr", "futures-util", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "librespot-core", "log", @@ -1727,7 +1727,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "form_urlencoded", "futures-util", @@ -1746,7 +1746,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "aes", "base64 0.22.1", @@ -1762,7 +1762,7 @@ dependencies = [ "http 1.1.0", "http-body-util", "httparse", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-proxy2", "hyper-rustls 0.27.3", "hyper-util", @@ -1801,7 +1801,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "aes", "base64 0.22.1", @@ -1815,7 +1815,7 @@ dependencies = [ "hex", "hmac", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "libmdns", "librespot-core", @@ -1829,7 +1829,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "async-trait", "bytes", @@ -1845,7 +1845,7 @@ dependencies = [ [[package]] name = "librespot-oauth" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "env_logger", "log", @@ -1856,7 +1856,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "alsa", "cpal", @@ -1888,7 +1888,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.5.0-dev" +version = "0.5.0" dependencies = [ "protobuf", "protobuf-codegen", @@ -2193,9 +2193,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.4" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] @@ -2234,12 +2234,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.1" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1" -dependencies = [ - "portable-atomic", -] +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "openssl-probe" @@ -2436,18 +2433,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bcc343da15609eaecd65f8aa76df8dc4209d325131d8219358c0aaaebab0bf6" +checksum = "3018844a02746180074f621e847703737d27d89d7f0721a7a4da317f88b16385" dependencies = [ "once_cell", "protobuf-support", @@ -2456,9 +2453,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4d0cde5642ea4df842b13eb9f59ea6fafa26dcb43e3e1ee49120e9757556189" +checksum = "411c15a212b4de05eb8bc989fd066a74c86bd3c04e27d6e86bd7703b806d7734" dependencies = [ "anyhow", "once_cell", @@ -2471,9 +2468,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b0e9b447d099ae2c4993c0cbb03c7a9d6c937b17f2d56cfc0b1550e6fcfdb76" +checksum = "06f45f16b522d92336e839b5e40680095a045e36a1e7f742ba682ddc85236772" dependencies = [ "anyhow", "indexmap", @@ -2487,9 +2484,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.5.1" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0766e3675a627c327e4b3964582594b0e8741305d628a98a5de75a1d15f99b9" +checksum = "faf96d872914fcda2b66d66ea3fff2be7c66865d31c7bb2790cff32c0e714880" dependencies = [ "thiserror", ] @@ -2605,7 +2602,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -2730,9 +2727,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" dependencies = [ "aws-lc-rs", "log", @@ -2789,9 +2786,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -2817,9 +2814,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "ryu" @@ -2838,9 +2835,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9aaafd5a2b6e3d657ff009d82fbd630b6bd54dd4eb06f21693925cdf80f9b8b" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ "windows-sys 0.59.0", ] @@ -3406,7 +3403,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "tokio", ] @@ -3430,7 +3427,7 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -3529,7 +3526,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.23.13", + "rustls 0.23.14", "rustls-pki-types", "sha1", "thiserror", @@ -3544,9 +3541,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-ident" @@ -3692,9 +3689,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", "once_cell", @@ -3703,9 +3700,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", @@ -3718,9 +3715,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.43" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -3730,9 +3727,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3740,9 +3737,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", @@ -3753,15 +3750,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.93" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "web-sys" -version = "0.3.70" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index d23be2e1..c62b7bc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Librespot Org"] license = "MIT" @@ -23,35 +23,35 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-connect] path = "connect" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-core] path = "core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-metadata] path = "metadata" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-playback] path = "playback" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-oauth] path = "oauth" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] data-encoding = "2.5" diff --git a/Cargo.toml-e b/Cargo.toml-e new file mode 100644 index 00000000..d23be2e1 --- /dev/null +++ b/Cargo.toml-e @@ -0,0 +1,100 @@ +[package] +name = "librespot" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Librespot Org"] +license = "MIT" +description = "An open source client library for Spotify, with support for Spotify Connect" +keywords = ["spotify"] +repository = "https://github.com/librespot-org/librespot" +readme = "README.md" +edition = "2021" + +[workspace] + +[lib] +name = "librespot" +path = "src/lib.rs" + +[[bin]] +name = "librespot" +path = "src/main.rs" +doc = false + +[dependencies.librespot-audio] +path = "audio" +version = "0.5.0-dev" + +[dependencies.librespot-connect] +path = "connect" +version = "0.5.0-dev" + +[dependencies.librespot-core] +path = "core" +version = "0.5.0-dev" + +[dependencies.librespot-discovery] +path = "discovery" +version = "0.5.0-dev" + +[dependencies.librespot-metadata] +path = "metadata" +version = "0.5.0-dev" + +[dependencies.librespot-playback] +path = "playback" +version = "0.5.0-dev" + +[dependencies.librespot-protocol] +path = "protocol" +version = "0.5.0-dev" + +[dependencies.librespot-oauth] +path = "oauth" +version = "0.5.0-dev" + +[dependencies] +data-encoding = "2.5" +env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } +futures-util = { version = "0.3", default-features = false } +getopts = "0.2" +log = "0.4" +sha1 = "0.10" +sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } +thiserror = "1.0" +tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } +url = "2.2" + +[features] +alsa-backend = ["librespot-playback/alsa-backend"] +portaudio-backend = ["librespot-playback/portaudio-backend"] +pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] +jackaudio-backend = ["librespot-playback/jackaudio-backend"] +rodio-backend = ["librespot-playback/rodio-backend"] +rodiojack-backend = ["librespot-playback/rodiojack-backend"] +sdl-backend = ["librespot-playback/sdl-backend"] +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] +maintainer = "librespot-org" +copyright = "2018 Paul Liétar" +license-file = ["LICENSE", "4"] +depends = "$auto" +extended-description = """\ +librespot is an open source client library for Spotify. It enables applications \ +to use Spotify's service, without using the official but closed-source \ +libspotify. Additionally, it will provide extra features which are not \ +available in the official library.""" +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"] +] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 4874d963..f13ba679 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" @@ -10,7 +10,7 @@ edition = "2021" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] aes = "0.8" diff --git a/audio/Cargo.toml-e b/audio/Cargo.toml-e new file mode 100644 index 00000000..4874d963 --- /dev/null +++ b/audio/Cargo.toml-e @@ -0,0 +1,27 @@ +[package] +name = "librespot-audio" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Lietar "] +description = "The audio fetching logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies.librespot-core] +path = "../core" +version = "0.5.0-dev" + +[dependencies] +aes = "0.8" +bytes = "1" +ctr = "0.9" +futures-util = "0.3" +hyper = { version = "1.3", features = [] } +hyper-util = { version = "0.1", features = ["client"] } +http-body-util = "0.1.1" +log = "0.4" +parking_lot = { version = "0.12", features = ["deadlock_detection"] } +tempfile = "3" +thiserror = "1.0" +tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 9035002e..8e253e51 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" @@ -22,12 +22,12 @@ tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-playback] path = "../playback" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" diff --git a/connect/Cargo.toml-e b/connect/Cargo.toml-e new file mode 100644 index 00000000..9035002e --- /dev/null +++ b/connect/Cargo.toml-e @@ -0,0 +1,33 @@ +[package] +name = "librespot-connect" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Lietar "] +description = "The discovery and Spotify Connect logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +form_urlencoded = "1.0" +futures-util = "0.3" +log = "0.4" +protobuf = "3.5" +rand = "0.8" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } +tokio-stream = "0.1" + +[dependencies.librespot-core] +path = "../core" +version = "0.5.0-dev" + +[dependencies.librespot-playback] +path = "../playback" +version = "0.5.0-dev" + +[dependencies.librespot-protocol] +path = "../protocol" +version = "0.5.0-dev" diff --git a/core/Cargo.toml b/core/Cargo.toml index c62e54de..6c141c42 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] build = "build.rs" @@ -11,11 +11,11 @@ edition = "2021" [dependencies.librespot-oauth] path = "../oauth" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] aes = "0.8" diff --git a/core/Cargo.toml-e b/core/Cargo.toml-e new file mode 100644 index 00000000..c62e54de --- /dev/null +++ b/core/Cargo.toml-e @@ -0,0 +1,76 @@ +[package] +name = "librespot-core" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Lietar "] +build = "build.rs" +description = "The core functionality provided by librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies.librespot-oauth] +path = "../oauth" +version = "0.5.0-dev" + +[dependencies.librespot-protocol] +path = "../protocol" +version = "0.5.0-dev" + +[dependencies] +aes = "0.8" +base64 = "0.22" +byteorder = "1.4" +bytes = "1" +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"] } +governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } +hmac = "0.12" +httparse = "1.7" +http = "1.0" +hyper = { version = "1.3", features = ["http1", "http2"] } +hyper-util = { version = "0.1", features = ["client"] } +http-body-util = "0.1.1" +hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } +hyper-rustls = { version = "0.27.2", features = ["http2"] } +log = "0.4" +nonzero_ext = "0.3" +num-bigint = { version = "0.4", features = ["rand"] } +num-derive = "0.4" +num-integer = "0.1" +num-traits = "0.2" +once_cell = "1" +parking_lot = { version = "0.12", features = ["deadlock_detection"] } +pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } +pin-project-lite = "0.2" +priority-queue = "2.0" +protobuf = "3.5" +quick-xml = { version = "0.36.1", features = ["serialize"] } +rand = "0.8" +rsa = "0.9.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +sha1 = { version = "0.10", features = ["oid"] } +shannon = "0.2" +sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } +thiserror = "1.0" +time = { version = "0.3", features = ["formatting", "parsing"] } +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.7", features = ["codec"] } +url = "2" +uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } +data-encoding = "2.5" + +[build-dependencies] +rand = "0.8" +vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } + +[dev-dependencies] +tokio = { version = "1", features = ["macros", "parking_lot"] } + +[features] +with-dns-sd = ["dns-sd"] diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 15e2777c..12cb13cd 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery logic for librespot" @@ -31,7 +31,7 @@ tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dev-dependencies] futures = "0.3" diff --git a/discovery/Cargo.toml-e b/discovery/Cargo.toml-e new file mode 100644 index 00000000..15e2777c --- /dev/null +++ b/discovery/Cargo.toml-e @@ -0,0 +1,42 @@ +[package] +name = "librespot-discovery" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Lietar "] +description = "The discovery logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +aes = "0.8" +base64 = "0.22" +bytes = "1" +ctr = "0.9" +dns-sd = { version = "0.1.3", optional = true } +form_urlencoded = "1.0" +futures-core = "0.3" +futures-util = "0.3" +hmac = "0.12" +hyper = { version = "1.3", features = ["http1"] } +hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } +http-body-util = "0.1.1" +libmdns = "0.9" +log = "0.4" +rand = "0.8" +serde_json = "1.0" +sha1 = "0.10" +thiserror = "1.0" +tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } + +[dependencies.librespot-core] +path = "../core" +version = "0.5.0-dev" + +[dev-dependencies] +futures = "0.3" +hex = "0.4" +tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } + +[features] +with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index a5d90460..b3e32c95 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The metadata logic for librespot" @@ -20,8 +20,8 @@ serde_json = "1.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" diff --git a/metadata/Cargo.toml-e b/metadata/Cargo.toml-e new file mode 100644 index 00000000..a5d90460 --- /dev/null +++ b/metadata/Cargo.toml-e @@ -0,0 +1,27 @@ +[package] +name = "librespot-metadata" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Lietar "] +description = "The metadata logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +async-trait = "0.1" +bytes = "1" +log = "0.4" +protobuf = "3.5" +thiserror = "1" +uuid = { version = "1", default-features = false } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[dependencies.librespot-core] +path = "../core" +version = "0.5.0-dev" + +[dependencies.librespot-protocol] +path = "../protocol" +version = "0.5.0-dev" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 646f0879..5dddedcb 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-oauth" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.73" authors = ["Nick Steel "] description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" diff --git a/oauth/Cargo.toml-e b/oauth/Cargo.toml-e new file mode 100644 index 00000000..646f0879 --- /dev/null +++ b/oauth/Cargo.toml-e @@ -0,0 +1,18 @@ +[package] +name = "librespot-oauth" +version = "0.5.0-dev" +rust-version = "1.73" +authors = ["Nick Steel "] +description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +log = "0.4" +oauth2 = "4.4" +thiserror = "1.0" +url = "2.2" + +[dev-dependencies] +env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } \ No newline at end of file diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 053f7aa5..03e25566 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" @@ -10,15 +10,15 @@ edition = "2021" [dependencies.librespot-audio] path = "../audio" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] futures-util = "0.3" diff --git a/playback/Cargo.toml-e b/playback/Cargo.toml-e new file mode 100644 index 00000000..053f7aa5 --- /dev/null +++ b/playback/Cargo.toml-e @@ -0,0 +1,68 @@ +[package] +name = "librespot-playback" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Sasha Hilton "] +description = "The audio playback logic for librespot" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies.librespot-audio] +path = "../audio" +version = "0.5.0-dev" + +[dependencies.librespot-core] +path = "../core" +version = "0.5.0-dev" + +[dependencies.librespot-metadata] +path = "../metadata" +version = "0.5.0-dev" + +[dependencies] +futures-util = "0.3" +log = "0.4" +parking_lot = { version = "0.12", features = ["deadlock_detection"] } +shell-words = "1.1" +thiserror = "1" +tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } +zerocopy = { version = "0.7.32", features = ["derive"] } + +# Backends +alsa = { version = "0.9.0", 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.11", optional = true } # jack >0.11 requires a MSRV of 1.80 +sdl2 = { version = "0.37", optional = true } +gstreamer = { version = "0.23.1", optional = true } +gstreamer-app = { version = "0.23.0", optional = true } +gstreamer-audio = { version = "0.23.0", optional = true } +glib = { version = "0.20.3", optional = true } + +# Rodio dependencies +rodio = { version = "0.19.0", optional = true, default-features = false } +cpal = { version = "0.15.1", optional = true } + +# Container and audio decoder +symphonia = { version = "0.5", default-features = false, features = ["mp3", "ogg", "vorbis"] } + +# Legacy Ogg container decoder for the passthrough decoder +ogg = { version = "0.9", optional = true } + +# Dithering +rand = { version = "0.8", features = ["small_rng"] } +rand_distr = "0.4" + +[features] +alsa-backend = ["alsa"] +portaudio-backend = ["portaudio-rs"] +pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] +jackaudio-backend = ["jack"] +rodio-backend = ["rodio", "cpal"] +rodiojack-backend = ["rodio", "cpal/jack"] +sdl-backend = ["sdl2"] +gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio"] + +passthrough-decoder = ["ogg"] diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index b58ebcbe..ec2496c6 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Liétar "] build = "build.rs" diff --git a/protocol/Cargo.toml-e b/protocol/Cargo.toml-e new file mode 100644 index 00000000..b58ebcbe --- /dev/null +++ b/protocol/Cargo.toml-e @@ -0,0 +1,16 @@ +[package] +name = "librespot-protocol" +version = "0.5.0-dev" +rust-version = "1.74" +authors = ["Paul Liétar "] +build = "build.rs" +description = "The protobuf logic for communicating with Spotify servers" +license = "MIT" +repository = "https://github.com/librespot-org/librespot" +edition = "2021" + +[dependencies] +protobuf = "3.5" + +[build-dependencies] +protobuf-codegen = "3" From c38a3be1b18252ea14c745fb5e4306f21fc2c232 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 20:37:47 +0200 Subject: [PATCH 452/561] Fix publish script: core depends on oauth --- publish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish.sh b/publish.sh index c9982c97..7a685e03 100755 --- a/publish.sh +++ b/publish.sh @@ -6,7 +6,7 @@ DRY_RUN='false' WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR -crates=( "protocol" "core" "discovery" "oauth" "audio" "metadata" "playback" "connect" "librespot" ) +crates=( "protocol" "oauth" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) OS=`uname` function replace_in_file() { From a974a71cc45f27fbea2353bc878f466e691c35be Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 20:38:42 +0200 Subject: [PATCH 453/561] Update Cargo.lock --- Cargo.toml-e | 18 +++++++++--------- audio/Cargo.toml-e | 4 ++-- connect/Cargo.toml-e | 8 ++++---- core/Cargo.toml-e | 6 +++--- discovery/Cargo.toml-e | 4 ++-- metadata/Cargo.toml-e | 6 +++--- oauth/Cargo.toml-e | 2 +- playback/Cargo.toml-e | 8 ++++---- protocol/Cargo.toml-e | 2 +- 9 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Cargo.toml-e b/Cargo.toml-e index d23be2e1..c62b7bc5 100644 --- a/Cargo.toml-e +++ b/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Librespot Org"] license = "MIT" @@ -23,35 +23,35 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-connect] path = "connect" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-core] path = "core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-metadata] path = "metadata" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-playback] path = "playback" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-oauth] path = "oauth" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] data-encoding = "2.5" diff --git a/audio/Cargo.toml-e b/audio/Cargo.toml-e index 4874d963..f13ba679 100644 --- a/audio/Cargo.toml-e +++ b/audio/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" @@ -10,7 +10,7 @@ edition = "2021" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] aes = "0.8" diff --git a/connect/Cargo.toml-e b/connect/Cargo.toml-e index 9035002e..8e253e51 100644 --- a/connect/Cargo.toml-e +++ b/connect/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" @@ -22,12 +22,12 @@ tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-playback] path = "../playback" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" diff --git a/core/Cargo.toml-e b/core/Cargo.toml-e index c62e54de..6c141c42 100644 --- a/core/Cargo.toml-e +++ b/core/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] build = "build.rs" @@ -11,11 +11,11 @@ edition = "2021" [dependencies.librespot-oauth] path = "../oauth" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] aes = "0.8" diff --git a/discovery/Cargo.toml-e b/discovery/Cargo.toml-e index 15e2777c..12cb13cd 100644 --- a/discovery/Cargo.toml-e +++ b/discovery/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The discovery logic for librespot" @@ -31,7 +31,7 @@ tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml-e b/metadata/Cargo.toml-e index a5d90460..b3e32c95 100644 --- a/metadata/Cargo.toml-e +++ b/metadata/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Lietar "] description = "The metadata logic for librespot" @@ -20,8 +20,8 @@ serde_json = "1.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0-dev" +version = "0.5.0" diff --git a/oauth/Cargo.toml-e b/oauth/Cargo.toml-e index 646f0879..5dddedcb 100644 --- a/oauth/Cargo.toml-e +++ b/oauth/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-oauth" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.73" authors = ["Nick Steel "] description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" diff --git a/playback/Cargo.toml-e b/playback/Cargo.toml-e index 053f7aa5..03e25566 100644 --- a/playback/Cargo.toml-e +++ b/playback/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" @@ -10,15 +10,15 @@ edition = "2021" [dependencies.librespot-audio] path = "../audio" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0-dev" +version = "0.5.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.5.0-dev" +version = "0.5.0" [dependencies] futures-util = "0.3" diff --git a/protocol/Cargo.toml-e b/protocol/Cargo.toml-e index b58ebcbe..ec2496c6 100644 --- a/protocol/Cargo.toml-e +++ b/protocol/Cargo.toml-e @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.5.0-dev" +version = "0.5.0" rust-version = "1.74" authors = ["Paul Liétar "] build = "build.rs" From e846900a2d7d8007ddcd2bcce1837abe2cc428ad Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 20:47:57 +0200 Subject: [PATCH 454/561] Specify tokio-tungstenite version crates.io does not allow wildcard versions --- core/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index c62e54de..cc3bb8fe 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -59,7 +59,7 @@ thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } 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-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } From d8d9ec73350b1a73f0433a6499d7289c6176c727 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 20:48:48 +0200 Subject: [PATCH 455/561] Update Cargo.lock --- core/Cargo.toml-e | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/Cargo.toml-e b/core/Cargo.toml-e index 6c141c42..7fca0094 100644 --- a/core/Cargo.toml-e +++ b/core/Cargo.toml-e @@ -59,7 +59,7 @@ thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } 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-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } From e73a08d43a3ad974764d59c23acf91e685290cc7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 21:23:09 +0200 Subject: [PATCH 456/561] Remove garbage toml-e files from last publish --- Cargo.toml-e | 100 ----------------------------------------- audio/Cargo.toml-e | 27 ----------- connect/Cargo.toml-e | 33 -------------- core/Cargo.toml-e | 76 ------------------------------- discovery/Cargo.toml-e | 42 ----------------- metadata/Cargo.toml-e | 27 ----------- oauth/Cargo.toml-e | 18 -------- playback/Cargo.toml-e | 68 ---------------------------- protocol/Cargo.toml-e | 16 ------- 9 files changed, 407 deletions(-) delete mode 100644 Cargo.toml-e delete mode 100644 audio/Cargo.toml-e delete mode 100644 connect/Cargo.toml-e delete mode 100644 core/Cargo.toml-e delete mode 100644 discovery/Cargo.toml-e delete mode 100644 metadata/Cargo.toml-e delete mode 100644 oauth/Cargo.toml-e delete mode 100644 playback/Cargo.toml-e delete mode 100644 protocol/Cargo.toml-e diff --git a/Cargo.toml-e b/Cargo.toml-e deleted file mode 100644 index c62b7bc5..00000000 --- a/Cargo.toml-e +++ /dev/null @@ -1,100 +0,0 @@ -[package] -name = "librespot" -version = "0.5.0" -rust-version = "1.74" -authors = ["Librespot Org"] -license = "MIT" -description = "An open source client library for Spotify, with support for Spotify Connect" -keywords = ["spotify"] -repository = "https://github.com/librespot-org/librespot" -readme = "README.md" -edition = "2021" - -[workspace] - -[lib] -name = "librespot" -path = "src/lib.rs" - -[[bin]] -name = "librespot" -path = "src/main.rs" -doc = false - -[dependencies.librespot-audio] -path = "audio" -version = "0.5.0" - -[dependencies.librespot-connect] -path = "connect" -version = "0.5.0" - -[dependencies.librespot-core] -path = "core" -version = "0.5.0" - -[dependencies.librespot-discovery] -path = "discovery" -version = "0.5.0" - -[dependencies.librespot-metadata] -path = "metadata" -version = "0.5.0" - -[dependencies.librespot-playback] -path = "playback" -version = "0.5.0" - -[dependencies.librespot-protocol] -path = "protocol" -version = "0.5.0" - -[dependencies.librespot-oauth] -path = "oauth" -version = "0.5.0" - -[dependencies] -data-encoding = "2.5" -env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } -futures-util = { version = "0.3", default-features = false } -getopts = "0.2" -log = "0.4" -sha1 = "0.10" -sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } -thiserror = "1.0" -tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } -url = "2.2" - -[features] -alsa-backend = ["librespot-playback/alsa-backend"] -portaudio-backend = ["librespot-playback/portaudio-backend"] -pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] -jackaudio-backend = ["librespot-playback/jackaudio-backend"] -rodio-backend = ["librespot-playback/rodio-backend"] -rodiojack-backend = ["librespot-playback/rodiojack-backend"] -sdl-backend = ["librespot-playback/sdl-backend"] -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] -maintainer = "librespot-org" -copyright = "2018 Paul Liétar" -license-file = ["LICENSE", "4"] -depends = "$auto" -extended-description = """\ -librespot is an open source client library for Spotify. It enables applications \ -to use Spotify's service, without using the official but closed-source \ -libspotify. Additionally, it will provide extra features which are not \ -available in the official library.""" -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"] -] diff --git a/audio/Cargo.toml-e b/audio/Cargo.toml-e deleted file mode 100644 index f13ba679..00000000 --- a/audio/Cargo.toml-e +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "librespot-audio" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Lietar "] -description = "The audio fetching logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies.librespot-core] -path = "../core" -version = "0.5.0" - -[dependencies] -aes = "0.8" -bytes = "1" -ctr = "0.9" -futures-util = "0.3" -hyper = { version = "1.3", features = [] } -hyper-util = { version = "0.1", features = ["client"] } -http-body-util = "0.1.1" -log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } -tempfile = "3" -thiserror = "1.0" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/Cargo.toml-e b/connect/Cargo.toml-e deleted file mode 100644 index 8e253e51..00000000 --- a/connect/Cargo.toml-e +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "librespot-connect" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Lietar "] -description = "The discovery and Spotify Connect logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies] -form_urlencoded = "1.0" -futures-util = "0.3" -log = "0.4" -protobuf = "3.5" -rand = "0.8" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -thiserror = "1.0" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } -tokio-stream = "0.1" - -[dependencies.librespot-core] -path = "../core" -version = "0.5.0" - -[dependencies.librespot-playback] -path = "../playback" -version = "0.5.0" - -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.5.0" diff --git a/core/Cargo.toml-e b/core/Cargo.toml-e deleted file mode 100644 index 7fca0094..00000000 --- a/core/Cargo.toml-e +++ /dev/null @@ -1,76 +0,0 @@ -[package] -name = "librespot-core" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Lietar "] -build = "build.rs" -description = "The core functionality provided by librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies.librespot-oauth] -path = "../oauth" -version = "0.5.0" - -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.5.0" - -[dependencies] -aes = "0.8" -base64 = "0.22" -byteorder = "1.4" -bytes = "1" -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"] } -governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } -hmac = "0.12" -httparse = "1.7" -http = "1.0" -hyper = { version = "1.3", features = ["http1", "http2"] } -hyper-util = { version = "0.1", features = ["client"] } -http-body-util = "0.1.1" -hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.27.2", features = ["http2"] } -log = "0.4" -nonzero_ext = "0.3" -num-bigint = { version = "0.4", features = ["rand"] } -num-derive = "0.4" -num-integer = "0.1" -num-traits = "0.2" -once_cell = "1" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } -pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } -pin-project-lite = "0.2" -priority-queue = "2.0" -protobuf = "3.5" -quick-xml = { version = "0.36.1", features = ["serialize"] } -rand = "0.8" -rsa = "0.9.2" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -sha1 = { version = "0.10", features = ["oid"] } -shannon = "0.2" -sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } -thiserror = "1.0" -time = { version = "0.3", features = ["formatting", "parsing"] } -tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } -tokio-stream = "0.1" -tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } -tokio-util = { version = "0.7", features = ["codec"] } -url = "2" -uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } -data-encoding = "2.5" - -[build-dependencies] -rand = "0.8" -vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } - -[dev-dependencies] -tokio = { version = "1", features = ["macros", "parking_lot"] } - -[features] -with-dns-sd = ["dns-sd"] diff --git a/discovery/Cargo.toml-e b/discovery/Cargo.toml-e deleted file mode 100644 index 12cb13cd..00000000 --- a/discovery/Cargo.toml-e +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "librespot-discovery" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Lietar "] -description = "The discovery logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies] -aes = "0.8" -base64 = "0.22" -bytes = "1" -ctr = "0.9" -dns-sd = { version = "0.1.3", optional = true } -form_urlencoded = "1.0" -futures-core = "0.3" -futures-util = "0.3" -hmac = "0.12" -hyper = { version = "1.3", features = ["http1"] } -hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } -http-body-util = "0.1.1" -libmdns = "0.9" -log = "0.4" -rand = "0.8" -serde_json = "1.0" -sha1 = "0.10" -thiserror = "1.0" -tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } - -[dependencies.librespot-core] -path = "../core" -version = "0.5.0" - -[dev-dependencies] -futures = "0.3" -hex = "0.4" -tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } - -[features] -with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] diff --git a/metadata/Cargo.toml-e b/metadata/Cargo.toml-e deleted file mode 100644 index b3e32c95..00000000 --- a/metadata/Cargo.toml-e +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "librespot-metadata" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Lietar "] -description = "The metadata logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies] -async-trait = "0.1" -bytes = "1" -log = "0.4" -protobuf = "3.5" -thiserror = "1" -uuid = { version = "1", default-features = false } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" - -[dependencies.librespot-core] -path = "../core" -version = "0.5.0" - -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.5.0" diff --git a/oauth/Cargo.toml-e b/oauth/Cargo.toml-e deleted file mode 100644 index 5dddedcb..00000000 --- a/oauth/Cargo.toml-e +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "librespot-oauth" -version = "0.5.0" -rust-version = "1.73" -authors = ["Nick Steel "] -description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies] -log = "0.4" -oauth2 = "4.4" -thiserror = "1.0" -url = "2.2" - -[dev-dependencies] -env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } \ No newline at end of file diff --git a/playback/Cargo.toml-e b/playback/Cargo.toml-e deleted file mode 100644 index 03e25566..00000000 --- a/playback/Cargo.toml-e +++ /dev/null @@ -1,68 +0,0 @@ -[package] -name = "librespot-playback" -version = "0.5.0" -rust-version = "1.74" -authors = ["Sasha Hilton "] -description = "The audio playback logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies.librespot-audio] -path = "../audio" -version = "0.5.0" - -[dependencies.librespot-core] -path = "../core" -version = "0.5.0" - -[dependencies.librespot-metadata] -path = "../metadata" -version = "0.5.0" - -[dependencies] -futures-util = "0.3" -log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } -shell-words = "1.1" -thiserror = "1" -tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.7.32", features = ["derive"] } - -# Backends -alsa = { version = "0.9.0", 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.11", optional = true } # jack >0.11 requires a MSRV of 1.80 -sdl2 = { version = "0.37", optional = true } -gstreamer = { version = "0.23.1", optional = true } -gstreamer-app = { version = "0.23.0", optional = true } -gstreamer-audio = { version = "0.23.0", optional = true } -glib = { version = "0.20.3", optional = true } - -# Rodio dependencies -rodio = { version = "0.19.0", optional = true, default-features = false } -cpal = { version = "0.15.1", optional = true } - -# Container and audio decoder -symphonia = { version = "0.5", default-features = false, features = ["mp3", "ogg", "vorbis"] } - -# Legacy Ogg container decoder for the passthrough decoder -ogg = { version = "0.9", optional = true } - -# Dithering -rand = { version = "0.8", features = ["small_rng"] } -rand_distr = "0.4" - -[features] -alsa-backend = ["alsa"] -portaudio-backend = ["portaudio-rs"] -pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] -jackaudio-backend = ["jack"] -rodio-backend = ["rodio", "cpal"] -rodiojack-backend = ["rodio", "cpal/jack"] -sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio"] - -passthrough-decoder = ["ogg"] diff --git a/protocol/Cargo.toml-e b/protocol/Cargo.toml-e deleted file mode 100644 index ec2496c6..00000000 --- a/protocol/Cargo.toml-e +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "librespot-protocol" -version = "0.5.0" -rust-version = "1.74" -authors = ["Paul Liétar "] -build = "build.rs" -description = "The protobuf logic for communicating with Spotify servers" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" - -[dependencies] -protobuf = "3.5" - -[build-dependencies] -protobuf-codegen = "3" From 1d80a4075f20b23f1a510ef7c024a14d53f7ba6e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 21:32:50 +0200 Subject: [PATCH 457/561] Create new changelog entry for next release --- CHANGELOG.md | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc4a43a3..fbdf1ef2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file. 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. -## [0.5.0-dev] - 2024-10-15 +## [Unreleased] + +### Added + +### Changed + +### Removed + +## [0.5.0] - 2024-10-15 This version is be a major departure from the architecture up until now. It focuses on implementing the "new Spotify API". This means moving large parts @@ -294,16 +302,17 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 -[0.5.0-dev]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD -[0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1..v0.4.2 -[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1 -[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0 -[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 -[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 -[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 -[0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 -[0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3..v0.1.5 -[0.1.3]: https://github.com/librespot-org/librespot/compare/v0.1.2..v0.1.3 -[0.1.2]: https://github.com/librespot-org/librespot/compare/v0.1.1..v0.1.2 -[0.1.1]: https://github.com/librespot-org/librespot/compare/v0.1.0..v0.1.1 +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.5.0...HEAD +[0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 +[0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2 +[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0...v0.4.1 +[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1...v0.4.0 +[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0...v0.3.0 +[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6...v0.2.0 +[0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5...v0.1.6 +[0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3...v0.1.5 +[0.1.3]: https://github.com/librespot-org/librespot/compare/v0.1.2...v0.1.3 +[0.1.2]: https://github.com/librespot-org/librespot/compare/v0.1.1...v0.1.2 +[0.1.1]: https://github.com/librespot-org/librespot/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/librespot-org/librespot/releases/tag/v0.1.0 From 09e4c3e12b557519d94ca29e3850a7723cc98e78 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 15 Oct 2024 21:47:02 +0200 Subject: [PATCH 458/561] Prevent garbage toml-e files on macOS Newer macOS versions output `uname` as "Darwin", while older versions output "darwin". --- publish.sh | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/publish.sh b/publish.sh index 7a685e03..d1d8f783 100755 --- a/publish.sh +++ b/publish.sh @@ -8,15 +8,19 @@ cd $WORKINGDIR crates=( "protocol" "oauth" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) -OS=`uname` function replace_in_file() { - if [ "$OS" == 'darwin' ]; then - # for MacOS - sed -i '' -e "$1" "$2" - else - # for Linux and Windows - sed -i'' -e "$1" "$2" - fi + OS=`uname` + shopt -s nocasematch + case "$OS" in + darwin) + # for macOS + sed -i '' -e "$1" "$2" + ;; + *) + # for Linux and Windows + sed -i'' -e "$1" "$2" + ;; + esac } function switchBranch { From 1912065248add7595e1f8bb2458391718b989326 Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Thu, 17 Oct 2024 11:01:56 -0400 Subject: [PATCH 459/561] Cargo: use rust-version from workspace (#1375) Make it easier to check and update. --- Cargo.toml | 3 +++ audio/Cargo.toml | 2 +- connect/Cargo.toml | 2 +- core/Cargo.toml | 2 +- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 2 +- oauth/Cargo.toml | 2 +- playback/Cargo.toml | 2 +- protocol/Cargo.toml | 2 +- 9 files changed, 11 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c62b7bc5..fa5da5c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,3 +98,6 @@ assets = [ ["contrib/librespot.service", "lib/systemd/system/", "644"], ["contrib/librespot.user.service", "lib/systemd/user/", "644"] ] + +[workspace.package] +rust-version = "1.74" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index f13ba679..713a499b 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-audio" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" license = "MIT" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 8e253e51..bb055db1 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-connect" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" diff --git a/core/Cargo.toml b/core/Cargo.toml index 7fca0094..93357f93 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-core" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 12cb13cd..863f5d8f 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-discovery" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index b3e32c95..813be783 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-metadata" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 5dddedcb..3d52555e 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-oauth" version = "0.5.0" -rust-version = "1.73" +rust-version.workspace = true authors = ["Nick Steel "] description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" license = "MIT" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 03e25566..4a9f3109 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-playback" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index ec2496c6..60c48866 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot-protocol" version = "0.5.0" -rust-version = "1.74" +rust-version.workspace = true authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From d8e84238abf39878e382a4008f2672ddb4260e80 Mon Sep 17 00:00:00 2001 From: "Artur H." <62839385+BeeJay28@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:02:56 +0200 Subject: [PATCH 460/561] Fix initial volume showing zero but playing full volume (#1373) --- CHANGELOG.md | 4 ++++ connect/src/spirc.rs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fbdf1ef2..e3d84ab9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +### Fixed + +- [connect] Fixes initial volume showing zero despite playing in full volume instead + ## [0.5.0] - 2024-10-15 This version is be a major departure from the architecture up until now. It diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 6eb3ab82..4da7c23c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1509,7 +1509,7 @@ impl SpircTask { fn set_volume(&mut self, volume: u16) { let old_volume = self.device.volume(); let new_volume = volume as u32; - if old_volume != new_volume { + if old_volume != new_volume || self.mixer.volume() != volume { self.device.set_volume(new_volume); self.mixer.set_volume(volume); if let Some(cache) = self.session.cache() { From 4580dab73f92f9b3eb1a9d2d741cb94c46b29aa1 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sat, 19 Oct 2024 20:27:26 +0200 Subject: [PATCH 461/561] Get access token via login5 (#1344) * core: Obtain spclient access token using login5 instead of keymaster (Fixes #1179) * core: move solving hashcash into util * login5: add login for mobile --------- Co-authored-by: Nick Steel --- CHANGELOG.md | 6 +- connect/src/spirc.rs | 20 ++- core/src/http_client.rs | 2 + core/src/lib.rs | 1 + core/src/login5.rs | 265 ++++++++++++++++++++++++++++++++++++++++ core/src/session.rs | 9 ++ core/src/spclient.rs | 56 +-------- core/src/util.rs | 51 +++++++- protocol/build.rs | 7 ++ 9 files changed, 362 insertions(+), 55 deletions(-) create mode 100644 core/src/login5.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e3d84ab9..9cb8a266 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- [core] The `access_token` for http requests is now acquired by `login5` + ### Added -### Changed +- [core] Add `login` (mobile) and `auth_token` retrieval via login5 ### Removed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 4da7c23c..c3942651 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -337,6 +337,9 @@ impl Spirc { }), ); + // pre-acquire client_token, preventing multiple request while running + let _ = session.spclient().client_token().await?; + // Connect *after* all message listeners are registered session.connect(credentials, true).await?; @@ -490,7 +493,22 @@ impl SpircTask { }, connection_id_update = self.connection_id_update.next() => match connection_id_update { Some(result) => match result { - Ok(connection_id) => self.handle_connection_id_update(connection_id), + Ok(connection_id) => { + self.handle_connection_id_update(connection_id); + + // pre-acquire access_token, preventing multiple request while running + // pre-acquiring for the access_token will only last for one hour + // + // we need to fire the request after connecting, but can't do it right + // after, because by that we would miss certain packages, like this one + match self.session.login5().auth_token().await { + Ok(_) => debug!("successfully pre-acquire access_token and client_token"), + Err(why) => { + error!("{why}"); + break + } + } + }, Err(e) => error!("could not parse connection ID update: {}", e), } None => { diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 4b500cd6..e7e25e92 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -109,7 +109,9 @@ impl HttpClient { let os_version = System::os_version().unwrap_or_else(|| zero_str.clone()); let (spotify_platform, os_version) = match OS { + // example os_version: 30 "android" => ("Android", os_version), + // example os_version: 17 "ios" => ("iOS", os_version), "macos" => ("OSX", zero_str), "windows" => ("Win32", zero_str), diff --git a/core/src/lib.rs b/core/src/lib.rs index f0ee345c..9894bb70 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -22,6 +22,7 @@ pub mod diffie_hellman; pub mod error; pub mod file_id; pub mod http_client; +pub mod login5; pub mod mercury; pub mod packet; mod proxytunnel; diff --git a/core/src/login5.rs b/core/src/login5.rs new file mode 100644 index 00000000..b13ed985 --- /dev/null +++ b/core/src/login5.rs @@ -0,0 +1,265 @@ +use crate::spclient::CLIENT_TOKEN; +use crate::token::Token; +use crate::{util, Error, SessionConfig}; +use bytes::Bytes; +use http::{header::ACCEPT, HeaderValue, Method, Request}; +use librespot_protocol::login5::login_response::Response; +use librespot_protocol::{ + client_info::ClientInfo, + credentials::{Password, StoredCredential}, + hashcash::HashcashSolution, + login5::{ + login_request::Login_method, ChallengeSolution, LoginError, LoginOk, LoginRequest, + LoginResponse, + }, +}; +use protobuf::well_known_types::duration::Duration as ProtoDuration; +use protobuf::{Message, MessageField}; +use std::env::consts::OS; +use std::time::{Duration, Instant}; +use thiserror::Error; +use tokio::time::sleep; + +const MAX_LOGIN_TRIES: u8 = 3; +const LOGIN_TIMEOUT: Duration = Duration::from_secs(3); + +component! { + Login5Manager : Login5ManagerInner { + auth_token: Option = None, + } +} + +#[derive(Debug, Error)] +enum Login5Error { + #[error("Login request was denied: {0:?}")] + FaultyRequest(LoginError), + #[error("Code challenge is not supported")] + CodeChallenge, + #[error("Tried to acquire token without stored credentials")] + NoStoredCredentials, + #[error("Couldn't successfully authenticate after {0} times")] + RetriesFailed(u8), + #[error("Login via login5 is only allowed for android or ios")] + OnlyForMobile, +} + +impl From for Error { + fn from(err: Login5Error) -> Self { + match err { + Login5Error::NoStoredCredentials | Login5Error::OnlyForMobile => { + Error::unavailable(err) + } + Login5Error::RetriesFailed(_) | Login5Error::FaultyRequest(_) => { + Error::failed_precondition(err) + } + Login5Error::CodeChallenge => Error::unimplemented(err), + } + } +} + +impl Login5Manager { + async fn request(&self, message: &LoginRequest) -> Result { + let client_token = self.session().spclient().client_token().await?; + let body = message.write_to_bytes()?; + + let request = Request::builder() + .method(&Method::POST) + .uri("https://login5.spotify.com/v3/login") + .header(ACCEPT, HeaderValue::from_static("application/x-protobuf")) + .header(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?) + .body(body.into())?; + + self.session().http_client().request_body(request).await + } + + async fn login5_request(&self, login: Login_method) -> Result { + let client_id = match OS { + "macos" | "windows" => self.session().client_id(), + _ => SessionConfig::default().client_id, + }; + + let mut login_request = LoginRequest { + client_info: MessageField::some(ClientInfo { + client_id, + device_id: self.session().device_id().to_string(), + special_fields: Default::default(), + }), + login_method: Some(login), + ..Default::default() + }; + + let mut response = self.request(&login_request).await?; + let mut count = 0; + + loop { + count += 1; + + let message = LoginResponse::parse_from_bytes(&response)?; + if let Some(Response::Ok(ok)) = message.response { + break Ok(ok); + } + + if message.has_error() { + match message.error() { + LoginError::TIMEOUT | LoginError::TOO_MANY_ATTEMPTS => { + sleep(LOGIN_TIMEOUT).await + } + others => return Err(Login5Error::FaultyRequest(others).into()), + } + } + + if message.has_challenges() { + // handles the challenges, and updates the login context with the response + Self::handle_challenges(&mut login_request, message)?; + } + + if count < MAX_LOGIN_TRIES { + response = self.request(&login_request).await?; + } else { + return Err(Login5Error::RetriesFailed(MAX_LOGIN_TRIES).into()); + } + } + } + + /// Login for android and ios + /// + /// This request doesn't require a connected session as it is the entrypoint for android or ios + /// + /// This request will only work when: + /// - client_id => android or ios | can be easily adjusted in [SessionConfig::default_for_os] + /// - user-agent => android or ios | has to be adjusted in [HttpClient::new](crate::http_client::HttpClient::new) + pub async fn login( + &self, + id: impl Into, + password: impl Into, + ) -> Result<(Token, Vec), Error> { + if !matches!(OS, "android" | "ios") { + // by manipulating the user-agent and client-id it can be also used/tested on desktop + return Err(Login5Error::OnlyForMobile.into()); + } + + let method = Login_method::Password(Password { + id: id.into(), + password: password.into(), + ..Default::default() + }); + + let token_response = self.login5_request(method).await?; + let auth_token = Self::token_from_login( + token_response.access_token, + token_response.access_token_expires_in, + ); + + Ok((auth_token, token_response.stored_credential)) + } + + /// Retrieve the access_token via login5 + /// + /// This request will only work when the store credentials match the client-id. Meaning that + /// stored credentials generated with the keymaster client-id will not work, for example, with + /// the android client-id. + pub async fn auth_token(&self) -> Result { + let auth_data = self.session().auth_data(); + if auth_data.is_empty() { + return Err(Login5Error::NoStoredCredentials.into()); + } + + let auth_token = self.lock(|inner| { + if let Some(token) = &inner.auth_token { + if token.is_expired() { + inner.auth_token = None; + } + } + inner.auth_token.clone() + }); + + if let Some(auth_token) = auth_token { + return Ok(auth_token); + } + + let method = Login_method::StoredCredential(StoredCredential { + username: self.session().username().to_string(), + data: auth_data, + ..Default::default() + }); + + let token_response = self.login5_request(method).await?; + let auth_token = Self::token_from_login( + token_response.access_token, + token_response.access_token_expires_in, + ); + + let token = self.lock(|inner| { + inner.auth_token = Some(auth_token.clone()); + inner.auth_token.clone() + }); + + trace!("Got auth token: {:?}", auth_token); + + token.ok_or(Login5Error::NoStoredCredentials.into()) + } + + fn handle_challenges( + login_request: &mut LoginRequest, + message: LoginResponse, + ) -> Result<(), Error> { + let challenges = message.challenges(); + debug!( + "Received {} challenges, solving...", + challenges.challenges.len() + ); + + for challenge in &challenges.challenges { + if challenge.has_code() { + return Err(Login5Error::CodeChallenge.into()); + } else if !challenge.has_hashcash() { + debug!("Challenge was empty, skipping..."); + continue; + } + + let hash_cash_challenge = challenge.hashcash(); + + let mut suffix = [0u8; 0x10]; + let duration = util::solve_hash_cash( + &message.login_context, + &hash_cash_challenge.prefix, + hash_cash_challenge.length, + &mut suffix, + )?; + + let (seconds, nanos) = (duration.as_secs() as i64, duration.subsec_nanos() as i32); + debug!("Solving hashcash took {seconds}s {nanos}ns"); + + let mut solution = ChallengeSolution::new(); + solution.set_hashcash(HashcashSolution { + suffix: Vec::from(suffix), + duration: MessageField::some(ProtoDuration { + seconds, + nanos, + ..Default::default() + }), + ..Default::default() + }); + + login_request + .challenge_solutions + .mut_or_insert_default() + .solutions + .push(solution); + } + + login_request.login_context = message.login_context; + + Ok(()) + } + + fn token_from_login(token: String, expires_in: i32) -> Token { + Token { + access_token: token, + expires_in: Duration::from_secs(expires_in.try_into().unwrap_or(3600)), + token_type: "Bearer".to_string(), + scopes: vec![], + timestamp: Instant::now(), + } + } +} diff --git a/core/src/session.rs b/core/src/session.rs index b2887503..defdf61b 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -35,6 +35,7 @@ use crate::{ config::SessionConfig, connection::{self, AuthenticationError, Transport}, http_client::HttpClient, + login5::Login5Manager, mercury::MercuryManager, packet::PacketType, protocol::keyexchange::ErrorCode, @@ -101,6 +102,7 @@ struct SessionInternal { mercury: OnceCell, spclient: OnceCell, token_provider: OnceCell, + login5: OnceCell, cache: Option>, handle: tokio::runtime::Handle, @@ -141,6 +143,7 @@ impl Session { mercury: OnceCell::new(), spclient: OnceCell::new(), token_provider: OnceCell::new(), + login5: OnceCell::new(), handle: tokio::runtime::Handle::current(), })) } @@ -310,6 +313,12 @@ impl Session { .get_or_init(|| TokenProvider::new(self.weak())) } + pub fn login5(&self) -> &Login5Manager { + self.0 + .login5 + .get_or_init(|| Login5Manager::new(self.weak())) + } + pub fn time_delta(&self) -> i64 { self.0.data.read().time_delta } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 156cf9c8..8725c45e 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -4,7 +4,6 @@ use std::{ time::{Duration, Instant}, }; -use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; @@ -16,7 +15,6 @@ use hyper::{ use hyper_util::client::legacy::ResponseFuture; use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; -use sha1::{Digest, Sha1}; use sysinfo::System; use thiserror::Error; @@ -35,6 +33,7 @@ use crate::{ extended_metadata::BatchedEntityRequest, }, token::Token, + util, version::spotify_semantic_version, Error, FileId, SpotifyId, }; @@ -50,7 +49,7 @@ component! { pub type SpClientResult = Result; #[allow(clippy::declare_interior_mutable_const)] -const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); +pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); #[derive(Debug, Error)] pub enum SpClientError { @@ -108,47 +107,6 @@ impl SpClient { Ok(format!("https://{}:{}", ap.0, ap.1)) } - fn solve_hash_cash( - ctx: &[u8], - prefix: &[u8], - length: i32, - dst: &mut [u8], - ) -> Result<(), Error> { - // after a certain number of seconds, the challenge expires - const TIMEOUT: u64 = 5; // seconds - let now = Instant::now(); - - let md = Sha1::digest(ctx); - - let mut counter: i64 = 0; - let target: i64 = BigEndian::read_i64(&md[12..20]); - - let suffix = loop { - if now.elapsed().as_secs() >= TIMEOUT { - return Err(Error::deadline_exceeded(format!( - "{TIMEOUT} seconds expired" - ))); - } - - let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); - - let mut hasher = Sha1::new(); - hasher.update(prefix); - hasher.update(&suffix); - let md = hasher.finalize(); - - if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) { - break suffix; - } - - counter += 1; - }; - - dst.copy_from_slice(&suffix); - - Ok(()) - } - async fn client_token_request(&self, message: &M) -> Result { let body = message.write_to_bytes()?; @@ -233,10 +191,12 @@ impl SpClient { ios_data.user_interface_idiom = 0; ios_data.target_iphone_simulator = false; ios_data.hw_machine = "iPhone14,5".to_string(); + // example system_version: 17 ios_data.system_version = os_version; } "android" => { let android_data = platform_data.mut_android(); + // example android_version: 30 android_data.android_version = os_version; android_data.api_version = 31; "Pixel".clone_into(&mut android_data.device_name); @@ -293,7 +253,7 @@ impl SpClient { let length = hash_cash_challenge.length; let mut suffix = [0u8; 0x10]; - let answer = Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); + let answer = util::solve_hash_cash(&ctx, &prefix, length, &mut suffix); match answer { Ok(_) => { @@ -468,11 +428,7 @@ impl SpClient { .body(body.to_owned().into())?; // Reconnection logic: keep getting (cached) tokens because they might have expired. - let token = self - .session() - .token_provider() - .get_token("playlist-read") - .await?; + let token = self.session().login5().auth_token().await?; let headers_mut = request.headers_mut(); if let Some(ref hdrs) = headers { diff --git a/core/src/util.rs b/core/src/util.rs index a01f8b56..31cdd962 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,12 +1,16 @@ +use crate::Error; +use byteorder::{BigEndian, ByteOrder}; +use futures_core::ready; +use futures_util::{future, FutureExt, Sink, SinkExt}; +use hmac::digest::Digest; +use sha1::Sha1; +use std::time::{Duration, Instant}; use std::{ future::Future, mem, pin::Pin, task::{Context, Poll}, }; - -use futures_core::ready; -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. @@ -120,3 +124,44 @@ impl SeqGenerator { mem::replace(&mut self.0, value) } } + +pub fn solve_hash_cash( + ctx: &[u8], + prefix: &[u8], + length: i32, + dst: &mut [u8], +) -> Result { + // after a certain number of seconds, the challenge expires + const TIMEOUT: u64 = 5; // seconds + let now = Instant::now(); + + let md = Sha1::digest(ctx); + + let mut counter: i64 = 0; + let target: i64 = BigEndian::read_i64(&md[12..20]); + + let suffix = loop { + if now.elapsed().as_secs() >= TIMEOUT { + return Err(Error::deadline_exceeded(format!( + "{TIMEOUT} seconds expired" + ))); + } + + let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); + + let mut hasher = Sha1::new(); + hasher.update(prefix); + hasher.update(&suffix); + let md = hasher.finalize(); + + if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) { + break suffix; + } + + counter += 1; + }; + + dst.copy_from_slice(&suffix); + + Ok(now.elapsed()) +} diff --git a/protocol/build.rs b/protocol/build.rs index e1378d37..8a0a8138 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -28,6 +28,13 @@ fn compile() { proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), proto_dir.join("spotify/clienttoken/v0/clienttoken_http.proto"), + proto_dir.join("spotify/login5/v3/challenges/code.proto"), + proto_dir.join("spotify/login5/v3/challenges/hashcash.proto"), + proto_dir.join("spotify/login5/v3/client_info.proto"), + proto_dir.join("spotify/login5/v3/credentials/credentials.proto"), + proto_dir.join("spotify/login5/v3/identifiers/identifiers.proto"), + proto_dir.join("spotify/login5/v3/login5.proto"), + proto_dir.join("spotify/login5/v3/user_info.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 From 2e655e7f80b459235a1d05f6133bcad16ad2231c Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Mon, 21 Oct 2024 22:11:38 +0200 Subject: [PATCH 462/561] Easier mocking of platforms (#1378) * core: move OS info into config.rs --- CHANGELOG.md | 1 + core/src/config.rs | 16 +++++++++++++++- core/src/connection/handshake.rs | 2 +- core/src/connection/mod.rs | 2 +- core/src/http_client.rs | 7 ++----- core/src/login5.rs | 2 +- core/src/spclient.rs | 6 ++---- core/src/version.rs | 4 ++-- 8 files changed, 25 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9cb8a266..fdedb554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [core] Add `login` (mobile) and `auth_token` retrieval via login5 +- [core] Add `OS` and `os_version` to `config.rs` ### Removed diff --git a/core/src/config.rs b/core/src/config.rs index 674c5020..1160c0f5 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -6,6 +6,20 @@ pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; pub(crate) const ANDROID_CLIENT_ID: &str = "9a8d2f0ce77a4e248bb71fefcb557637"; pub(crate) const IOS_CLIENT_ID: &str = "58bd3c95768941ea9eb4350aaa033eb3"; +// Easily adjust the current platform to mock the behavior on it. If for example +// android or ios needs to be mocked, the `os_version` has to be set to a valid version. +// Otherwise, client-token or login5 requests will fail with a generic invalid-credential error. +/// See [std::env::consts::OS] +pub const OS: &str = std::env::consts::OS; + +// valid versions for some os: +// 'android': 30 +// 'ios': 17 +/// See [sysinfo::System::os_version] +pub fn os_version() -> String { + sysinfo::System::os_version().unwrap_or("0".into()) +} + #[derive(Clone, Debug)] pub struct SessionConfig { pub client_id: String, @@ -39,7 +53,7 @@ impl SessionConfig { impl Default for SessionConfig { fn default() -> Self { - Self::default_for_os(std::env::consts::OS) + Self::default_for_os(OS) } } diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index d18f3df1..03b35598 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -110,7 +110,7 @@ where let mut client_nonce = vec![0; 0x10]; thread_rng().fill_bytes(&mut client_nonce); - let platform = match std::env::consts::OS { + let platform = match crate::config::OS { "android" => Platform::PLATFORM_ANDROID_ARM, "freebsd" | "netbsd" | "openbsd" => match ARCH { "x86_64" => Platform::PLATFORM_FREEBSD_X86_64, diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index c46e5ad8..2e9bbdb4 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -112,7 +112,7 @@ pub async fn authenticate( _ => CpuFamily::CPU_UNKNOWN, }; - let os = match std::env::consts::OS { + let os = match crate::config::OS { "android" => Os::OS_ANDROID, "freebsd" | "netbsd" | "openbsd" => Os::OS_FREEBSD, "ios" => Os::OS_IPHONE, diff --git a/core/src/http_client.rs b/core/src/http_client.rs index e7e25e92..8645d3a3 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - env::consts::OS, time::{Duration, Instant}, }; @@ -21,11 +20,11 @@ use hyper_util::{ use nonzero_ext::nonzero; use once_cell::sync::OnceCell; use parking_lot::Mutex; -use sysinfo::System; use thiserror::Error; use url::Url; use crate::{ + config::{os_version, OS}, date::Date, version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING}, Error, @@ -106,12 +105,10 @@ pub struct HttpClient { impl HttpClient { pub fn new(proxy_url: Option<&Url>) -> Self { let zero_str = String::from("0"); - let os_version = System::os_version().unwrap_or_else(|| zero_str.clone()); + let os_version = os_version(); let (spotify_platform, os_version) = match OS { - // example os_version: 30 "android" => ("Android", os_version), - // example os_version: 17 "ios" => ("iOS", os_version), "macos" => ("OSX", zero_str), "windows" => ("Win32", zero_str), diff --git a/core/src/login5.rs b/core/src/login5.rs index b13ed985..dca8f27e 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -1,3 +1,4 @@ +use crate::config::OS; use crate::spclient::CLIENT_TOKEN; use crate::token::Token; use crate::{util, Error, SessionConfig}; @@ -15,7 +16,6 @@ use librespot_protocol::{ }; use protobuf::well_known_types::duration::Duration as ProtoDuration; use protobuf::{Message, MessageField}; -use std::env::consts::OS; use std::time::{Duration, Instant}; use thiserror::Error; use tokio::time::sleep; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 8725c45e..a23b52f0 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -1,5 +1,4 @@ use std::{ - env::consts::OS, fmt::Write, time::{Duration, Instant}, }; @@ -18,6 +17,7 @@ use rand::RngCore; use sysinfo::System; use thiserror::Error; +use crate::config::{os_version, OS}; use crate::{ apresolve::SocketAddress, cdn_url::CdnUrl, @@ -162,7 +162,7 @@ impl SpClient { .platform_specific_data .mut_or_insert_default(); - let os_version = System::os_version().unwrap_or_else(|| String::from("0")); + let os_version = os_version(); let kernel_version = System::kernel_version().unwrap_or_else(|| String::from("0")); match os { @@ -191,12 +191,10 @@ impl SpClient { ios_data.user_interface_idiom = 0; ios_data.target_iphone_simulator = false; ios_data.hw_machine = "iPhone14,5".to_string(); - // example system_version: 17 ios_data.system_version = os_version; } "android" => { let android_data = platform_data.mut_android(); - // example android_version: 30 android_data.android_version = os_version; android_data.api_version = 31; "Pixel".clone_into(&mut android_data.device_name); diff --git a/core/src/version.rs b/core/src/version.rs index d3870473..3439662c 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -29,14 +29,14 @@ pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)"; pub fn spotify_version() -> String { - match std::env::consts::OS { + match crate::config::OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), _ => SPOTIFY_VERSION.to_string(), } } pub fn spotify_semantic_version() -> String { - match std::env::consts::OS { + match crate::config::OS { "android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(), _ => SPOTIFY_SEMANTIC_VERSION.to_string(), } From 082141e6a569c4a7c4e2897785f2fe308160e709 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:35:10 +0200 Subject: [PATCH 463/561] Bump actions/checkout from 4.2.1 to 4.2.2 (#1384) --- .github/workflows/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0a0203a..2902c4f4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.2.1 + uses: actions/checkout@v4.2.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.2.1 + uses: actions/checkout@v4.2.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.2.1 + uses: actions/checkout@v4.2.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -168,7 +168,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.1 + uses: actions/checkout@v4.2.2 # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build - name: Install NASM @@ -219,7 +219,7 @@ jobs: - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.1 + uses: actions/checkout@v4.2.2 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y From d2324ddd1b7cc4fb0ebd0375c81b347ed40754e9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 26 Oct 2024 16:35:32 +0200 Subject: [PATCH 464/561] Bump actions/cache from 4.1.1 to 4.1.2 (#1383) --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2902c4f4..3b43611f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: | ~/.cargo/registry/index @@ -183,7 +183,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: | ~/.cargo/registry/index @@ -230,7 +230,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: | ~/.cargo/registry/index From 94d174c33d30a4baa30799b4e9a85b63d4331b2c Mon Sep 17 00:00:00 2001 From: Benedikt Date: Sat, 26 Oct 2024 16:45:02 +0200 Subject: [PATCH 465/561] Discovery: Refactor and add Avahi DBus backend (#1347) * discovery: use opaque error type for DnsSdError This helps to decouple discovery and core by not leaking implementation details of the zeroconf backend into Error conversion impls in core. * discovery: map all MDNS/DNS-SD errors to DiscoveryError::DnsSdError previously, libmdns errors would use a generic conversion from std::io::Error to core::Error * discovery: use an opaque type for the handle to the DNS-SD service * discovery: make features additive i.e. add with-libmdns instead of using not(with-dns-sd). The logic is such that enabling with-dns-sd in addition to the default with-libmdns will still end up using dns-sd, as before. If only with-libmdns is enabled, that will be the default. If none of the features is enabled, attempting to build a `Discovery` will yield an error. * discovery: add --zeroconf-backend CLI flag * discovery: Add minimal Avahi zeroconf backend * bump MSRV to 1.75 required by zbus >= 4 * discovery: ensure that server and dns-sd backend shutdown gracefully Previously, on drop the the shutdown_tx/close_tx, it wasn't guaranteed the corresponding tasks would continue to be polled until they actually completed their shutdown. Since dns_sd::Service is not Send and non-async, and because libmdns is non-async, put them on their own threads. * discovery: use a shared channel for server and zeroconf status messages * discovery: add Avahi reconnection logic This deals gracefully with the case where the Avahi daemon is restarted or not running initially. * discovery: allow running when compiled without zeroconf backend... ...but exit with an error if there's no way to authenticate * better error messages for invalid options with no short flag --- .devcontainer/Dockerfile | 2 +- .github/workflows/test.yml | 6 +- CHANGELOG.md | 5 + COMPILING.md | 11 + Cargo.lock | 444 ++++++++++++++++++++++++++++++++++++- Cargo.toml | 11 +- contrib/Dockerfile | 2 +- core/Cargo.toml | 4 - core/src/error.rs | 10 - discovery/Cargo.toml | 11 +- discovery/src/avahi.rs | 151 +++++++++++++ discovery/src/lib.rs | 437 ++++++++++++++++++++++++++++++++---- discovery/src/server.rs | 66 +++--- oauth/Cargo.toml | 2 +- src/main.rs | 123 ++++++++-- 15 files changed, 1156 insertions(+), 129 deletions(-) create mode 100644 discovery/src/avahi.rs diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index a2825067..ce845a52 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.74.0 +ARG rust_version=1.75.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3b43611f..2c90553e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.74" # MSRV (Minimum supported rust version) + - "1.75" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -164,7 +164,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.74" # MSRV (Minimum supported rust version) + - "1.75" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -215,7 +215,7 @@ jobs: - aarch64-unknown-linux-gnu - riscv64gc-unknown-linux-gnu toolchain: - - "1.74" # MSRV (Minimum supported rust version) + - "1.75" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index fdedb554..e585ea8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,11 +10,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [core] The `access_token` for http requests is now acquired by `login5` +- [core] MSRV is now 1.75 (breaking) +- [discovery] librespot can now be compiled with multiple MDNS/DNS-SD backends + (avahi, dns_sd, libmdns) which can be selected using a CLI flag. The defaults + are unchanged (breaking). ### Added - [core] Add `login` (mobile) and `auth_token` retrieval via login5 - [core] Add `OS` and `os_version` to `config.rs` +- [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus. ### Removed diff --git a/COMPILING.md b/COMPILING.md index d5b94b0e..4b4b58af 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -56,6 +56,17 @@ On Fedora systems: sudo dnf install alsa-lib-devel ``` +### Zeroconf library dependencies +Depending on the chosen backend, specific development libraries are required. + +*_Note this is an non-exhaustive list, open a PR to add to it!_* + +| Zeroconf backend | Debian/Ubuntu | Fedora | macOS | +|--------------------|------------------------------|-----------------------------------|-------------| +|avahi | | | | +|dns_sd | `libavahi-compat-libdnssd-dev pkg-config` | `avahi-compat-libdns_sd-devel` | | +|libmdns (default) | | | | + ### Getting the Source 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. diff --git a/Cargo.lock b/Cargo.lock index a20c2d43..7da57b92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,6 +135,114 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "async-broadcast" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", + "tracing", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "async-signal" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.83" @@ -292,6 +400,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -358,6 +479,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.38" @@ -419,6 +546,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -494,6 +630,12 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crypto-common" version = "0.1.6" @@ -654,6 +796,33 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" + +[[package]] +name = "enumflags2" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "env_filter" version = "0.1.2" @@ -692,6 +861,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -773,6 +963,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1152,6 +1355,12 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + [[package]] name = "hex" version = "0.4.3" @@ -1753,7 +1962,6 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "dns-sd", "form_urlencoded", "futures-core", "futures-util", @@ -1821,10 +2029,13 @@ dependencies = [ "librespot-core", "log", "rand", + "serde", "serde_json", + "serde_repr", "sha1", "thiserror", "tokio", + "zbus", ] [[package]] @@ -1931,6 +2142,15 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -1958,7 +2178,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.9", "libc", "wasi", "windows-sys 0.52.0", @@ -2014,6 +2234,19 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + [[package]] name = "no-std-compat" version = "0.4.1" @@ -2253,6 +2486,22 @@ dependencies = [ "paste", ] +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.3" @@ -2332,6 +2581,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -2359,6 +2619,21 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "portable-atomic" version = "1.9.0" @@ -2442,9 +2717,9 @@ dependencies = [ [[package]] name = "protobuf" -version = "3.6.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3018844a02746180074f621e847703737d27d89d7f0721a7a4da317f88b16385" +checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" dependencies = [ "once_cell", "protobuf-support", @@ -2453,9 +2728,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.6.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "411c15a212b4de05eb8bc989fd066a74c86bd3c04e27d6e86bd7703b806d7734" +checksum = "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144" dependencies = [ "anyhow", "once_cell", @@ -2468,9 +2743,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.6.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06f45f16b522d92336e839b5e40680095a045e36a1e7f742ba682ddc85236772" +checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257" dependencies = [ "anyhow", "indexmap", @@ -2484,9 +2759,9 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.6.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf96d872914fcda2b66d66ea3fff2be7c66865d31c7bb2790cff32c0e714880" +checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" dependencies = [ "thiserror", ] @@ -2946,6 +3221,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -3079,6 +3365,12 @@ dependencies = [ "der", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "strsim" version = "0.11.1" @@ -3362,6 +3654,7 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", + "tracing", "windows-sys 0.52.0", ] @@ -3495,9 +3788,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -3539,6 +3844,17 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.17" @@ -3598,9 +3914,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom", "rand", @@ -4148,6 +4464,73 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-process", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tokio", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.79", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -4174,3 +4557,40 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.79", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] diff --git a/Cargo.toml b/Cargo.toml index fa5da5c3..a6d216b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot" version = "0.5.0" -rust-version = "1.74" +rust-version = "1.75" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -36,6 +36,7 @@ version = "0.5.0" [dependencies.librespot-discovery] path = "discovery" version = "0.5.0" +default-features = false [dependencies.librespot-metadata] path = "metadata" @@ -62,7 +63,7 @@ log = "0.4" sha1 = "0.10" sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } thiserror = "1.0" -tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } +tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" [features] @@ -75,11 +76,13 @@ rodiojack-backend = ["librespot-playback/rodiojack-backend"] sdl-backend = ["librespot-playback/sdl-backend"] gstreamer-backend = ["librespot-playback/gstreamer-backend"] -with-dns-sd = ["librespot-core/with-dns-sd", "librespot-discovery/with-dns-sd"] +with-avahi = ["librespot-discovery/with-avahi"] +with-dns-sd = ["librespot-discovery/with-dns-sd"] +with-libmdns = ["librespot-discovery/with-libmdns"] passthrough-decoder = ["librespot-playback/passthrough-decoder"] -default = ["rodio-backend"] +default = ["rodio-backend", "with-libmdns"] [package.metadata.deb] maintainer = "librespot-org" diff --git a/contrib/Dockerfile b/contrib/Dockerfile index cf972582..a36fef88 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -29,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.74 -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.75 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi diff --git a/core/Cargo.toml b/core/Cargo.toml index 93357f93..a9208bdf 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,7 +22,6 @@ aes = "0.8" base64 = "0.22" byteorder = "1.4" bytes = "1" -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"] } @@ -71,6 +70,3 @@ vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build [dev-dependencies] tokio = { version = "1", features = ["macros", "parking_lot"] } - -[features] -with-dns-sd = ["dns-sd"] diff --git a/core/src/error.rs b/core/src/error.rs index b18ce91a..6b0178c9 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -21,9 +21,6 @@ use url::ParseError; use librespot_oauth::OAuthError; -#[cfg(feature = "with-dns-sd")] -use dns_sd::DNSError; - #[derive(Debug)] pub struct Error { pub kind: ErrorKind, @@ -314,13 +311,6 @@ 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 863f5d8f..fa0746ef 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -21,13 +21,16 @@ hmac = "0.12" hyper = { version = "1.3", features = ["http1"] } hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } http-body-util = "0.1.1" -libmdns = "0.9" +libmdns = { version = "0.9", optional = true } log = "0.4" rand = "0.8" +serde = { version = "1", default-features = false, features = ["derive"], optional = true } +serde_repr = "0.1" serde_json = "1.0" sha1 = "0.10" thiserror = "1.0" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } +zbus = { version = "4", default-features = false, features = ["tokio"], optional = true } [dependencies.librespot-core] path = "../core" @@ -39,4 +42,8 @@ hex = "0.4" tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } [features] -with-dns-sd = ["dns-sd", "librespot-core/with-dns-sd"] +with-avahi = ["zbus", "serde"] +with-dns-sd = ["dns-sd"] +with-libmdns = ["libmdns"] + +default = ["with-libmdns"] diff --git a/discovery/src/avahi.rs b/discovery/src/avahi.rs new file mode 100644 index 00000000..7c098168 --- /dev/null +++ b/discovery/src/avahi.rs @@ -0,0 +1,151 @@ +#![cfg(feature = "with-avahi")] + +#[allow(unused)] +pub use server::ServerProxy; + +#[allow(unused)] +pub use entry_group::{ + EntryGroupProxy, EntryGroupState, StateChangedStream as EntryGroupStateChangedStream, +}; + +mod server { + // This is not the full interface, just the methods we need! + // Avahi also implements a newer version of the interface ("org.freedesktop.Avahi.Server2"), but + // the additions are not relevant for us, and the older version is not intended to be deprecated. + // cf. the release notes for 0.8 at https://github.com/avahi/avahi/blob/master/docs/NEWS + #[zbus::proxy( + interface = "org.freedesktop.Avahi.Server", + default_service = "org.freedesktop.Avahi", + default_path = "/", + gen_blocking = false + )] + trait Server { + /// EntryGroupNew method + #[zbus(object = "super::entry_group::EntryGroup")] + fn entry_group_new(&self); + + /// GetState method + fn get_state(&self) -> zbus::Result; + + /// StateChanged signal + #[zbus(signal)] + fn state_changed(&self, state: i32, error: &str) -> zbus::Result<()>; + } +} + +mod entry_group { + use serde_repr::Deserialize_repr; + use zbus::zvariant; + + #[derive(Clone, Copy, Debug, Deserialize_repr)] + #[repr(i32)] + pub enum EntryGroupState { + // The group has not yet been committed, the user must still call avahi_entry_group_commit() + Uncommited = 0, + // The entries of the group are currently being registered + Registering = 1, + // The entries have successfully been established + Established = 2, + // A name collision for one of the entries in the group has been detected, the entries have been withdrawn + Collision = 3, + // Some kind of failure happened, the entries have been withdrawn + Failure = 4, + } + + impl zvariant::Type for EntryGroupState { + fn signature() -> zvariant::Signature<'static> { + zvariant::Signature::try_from("i").unwrap() + } + } + + #[zbus::proxy( + interface = "org.freedesktop.Avahi.EntryGroup", + default_service = "org.freedesktop.Avahi", + gen_blocking = false + )] + trait EntryGroup { + /// AddAddress method + fn add_address( + &self, + interface: i32, + protocol: i32, + flags: u32, + name: &str, + address: &str, + ) -> zbus::Result<()>; + + /// AddRecord method + #[allow(clippy::too_many_arguments)] + fn add_record( + &self, + interface: i32, + protocol: i32, + flags: u32, + name: &str, + clazz: u16, + type_: u16, + ttl: u32, + rdata: &[u8], + ) -> zbus::Result<()>; + + /// AddService method + #[allow(clippy::too_many_arguments)] + fn add_service( + &self, + interface: i32, + protocol: i32, + flags: u32, + name: &str, + type_: &str, + domain: &str, + host: &str, + port: u16, + txt: &[&[u8]], + ) -> zbus::Result<()>; + + /// AddServiceSubtype method + #[allow(clippy::too_many_arguments)] + fn add_service_subtype( + &self, + interface: i32, + protocol: i32, + flags: u32, + name: &str, + type_: &str, + domain: &str, + subtype: &str, + ) -> zbus::Result<()>; + + /// Commit method + fn commit(&self) -> zbus::Result<()>; + + /// Free method + fn free(&self) -> zbus::Result<()>; + + /// GetState method + fn get_state(&self) -> zbus::Result; + + /// IsEmpty method + fn is_empty(&self) -> zbus::Result; + + /// Reset method + fn reset(&self) -> zbus::Result<()>; + + /// UpdateServiceTxt method + #[allow(clippy::too_many_arguments)] + fn update_service_txt( + &self, + interface: i32, + protocol: i32, + flags: u32, + name: &str, + type_: &str, + domain: &str, + txt: &[&[u8]], + ) -> zbus::Result<()>; + + /// StateChanged signal + #[zbus(signal)] + fn state_changed(&self, state: EntryGroupState, error: &str) -> zbus::Result<()>; + } +} diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index f1f0f692..d829e0f5 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -7,17 +7,19 @@ //! 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. +mod avahi; mod server; use std::{ borrow::Cow, - io, + error::Error as StdError, pin::Pin, task::{Context, Poll}, }; use futures_core::Stream; use thiserror::Error; +use tokio::sync::{mpsc, oneshot}; use self::server::DiscoveryServer; @@ -30,6 +32,88 @@ pub use crate::core::authentication::Credentials; /// Determining the icon in the list of available devices. pub use crate::core::config::DeviceType; +pub enum DiscoveryEvent { + Credentials(Credentials), + ServerError(DiscoveryError), + ZeroconfError(DiscoveryError), +} + +enum ZeroconfCmd { + Shutdown, +} + +pub struct DnsSdHandle { + task_handle: tokio::task::JoinHandle<()>, + shutdown_tx: oneshot::Sender, +} + +impl DnsSdHandle { + async fn shutdown(self) { + log::debug!("Shutting down zeroconf responder"); + let Self { + task_handle, + shutdown_tx, + } = self; + if shutdown_tx.send(ZeroconfCmd::Shutdown).is_err() { + log::warn!("Zeroconf responder unexpectedly disappeared"); + } else { + let _ = task_handle.await; + log::debug!("Zeroconf responder stopped"); + } + } +} + +pub type DnsSdServiceBuilder = fn( + Cow<'static, str>, + Vec, + u16, + mpsc::UnboundedSender, +) -> Result; + +// Default goes first: This matches the behaviour when feature flags were exlusive, i.e. when there +// was only `feature = "with-dns-sd"` or `not(feature = "with-dns-sd")` +pub const BACKENDS: &[( + &str, + // If None, the backend is known but wasn't compiled. + Option, +)] = &[ + #[cfg(feature = "with-avahi")] + ("avahi", Some(launch_avahi)), + #[cfg(not(feature = "with-avahi"))] + ("avahi", None), + #[cfg(feature = "with-dns-sd")] + ("dns-sd", Some(launch_dns_sd)), + #[cfg(not(feature = "with-dns-sd"))] + ("dns-sd", None), + #[cfg(feature = "with-libmdns")] + ("libmdns", Some(launch_libmdns)), + #[cfg(not(feature = "with-libmdns"))] + ("libmdns", None), +]; + +pub fn find(name: Option<&str>) -> Result { + if let Some(ref name) = name { + match BACKENDS.iter().find(|(id, _)| name == id) { + Some((_id, Some(launch_svc))) => Ok(*launch_svc), + Some((_id, None)) => Err(Error::unavailable(format!( + "librespot built without '{}' support", + name + ))), + None => Err(Error::not_found(format!( + "unknown zeroconf backend '{}'", + name + ))), + } + } else { + BACKENDS + .iter() + .find_map(|(_, launch_svc)| *launch_svc) + .ok_or(Error::unavailable( + "librespot built without zeroconf backends", + )) + } +} + /// Makes this device visible to Spotify clients in the local network. /// /// `Discovery` implements the [`Stream`] trait. Every time this device @@ -37,10 +121,11 @@ pub use crate::core::config::DeviceType; pub struct Discovery { server: DiscoveryServer, - #[cfg(not(feature = "with-dns-sd"))] - _svc: libmdns::Service, - #[cfg(feature = "with-dns-sd")] - _svc: dns_sd::DNSService, + /// An opaque handle to the DNS-SD service. Dropping this will unregister the service. + #[allow(unused)] + svc: DnsSdHandle, + + event_rx: mpsc::UnboundedReceiver, } /// A builder for [`Discovery`]. @@ -48,6 +133,7 @@ pub struct Builder { server_config: server::Config, port: u16, zeroconf_ip: Vec, + zeroconf_backend: Option, } /// Errors that can occur while setting up a [`Discovery`] instance. @@ -55,16 +141,27 @@ pub struct Builder { pub enum DiscoveryError { #[error("Creating SHA1 block cipher failed")] AesError(#[from] aes::cipher::InvalidLength), + #[error("Setting up dns-sd failed: {0}")] - DnsSdError(#[from] io::Error), + DnsSdError(#[source] Box), + #[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), } +#[cfg(feature = "with-avahi")] +impl From for DiscoveryError { + fn from(error: zbus::Error) -> Self { + Self::DnsSdError(Box::new(error)) + } +} + impl From for Error { fn from(err: DiscoveryError) -> Self { match err { @@ -77,6 +174,264 @@ impl From for Error { } } +#[allow(unused)] +const DNS_SD_SERVICE_NAME: &str = "_spotify-connect._tcp"; +#[allow(unused)] +const TXT_RECORD: [&str; 2] = ["VERSION=1.0", "CPath=/"]; + +#[cfg(feature = "with-avahi")] +async fn avahi_task( + name: Cow<'static, str>, + port: u16, + entry_group: &mut Option>, +) -> Result<(), DiscoveryError> { + use self::avahi::{EntryGroupState, ServerProxy}; + use futures_util::StreamExt; + + let conn = zbus::Connection::system().await?; + + // Wait for the daemon to show up. + // On error: Failed to listen for NameOwnerChanged signal => Fatal DBus issue + let bus = zbus::fdo::DBusProxy::new(&conn).await?; + let mut stream = bus + .receive_name_owner_changed_with_args(&[(0, "org.freedesktop.Avahi")]) + .await?; + + loop { + // Wait for Avahi daemon to be started + 'wait_avahi: { + while let Poll::Ready(Some(_)) = futures_util::poll!(stream.next()) { + // Drain queued name owner changes, since we're going to connect in a second + } + + // Ping after we connected to the signal since it might have shown up in the meantime + if let Ok(avahi_peer) = + zbus::fdo::PeerProxy::new(&conn, "org.freedesktop.Avahi", "/").await + { + if avahi_peer.ping().await.is_ok() { + log::debug!("Pinged Avahi: Available"); + break 'wait_avahi; + } + } + log::warn!("Failed to connect to Avahi, zeroconf discovery will not work until avahi-daemon is started. Check that it is installed and running"); + + // If it didn't, wait for the signal + match stream.next().await { + Some(_signal) => { + log::debug!("Avahi appeared"); + break 'wait_avahi; + } + // The stream ended, but this should never happen + None => { + return Err(zbus::Error::Failure("DBus disappeared".to_owned()).into()); + } + } + } + + // Connect to Avahi and publish the service + let avahi_server = ServerProxy::new(&conn).await?; + log::trace!("Connected to Avahi"); + + *entry_group = Some(avahi_server.entry_group_new().await?); + + let mut entry_group_state_stream = entry_group + .as_mut() + .unwrap() + .receive_state_changed() + .await?; + + entry_group + .as_mut() + .unwrap() + .add_service( + -1, // AVAHI_IF_UNSPEC + -1, // IPv4 and IPv6 + 0, // flags + &name, + DNS_SD_SERVICE_NAME, // type + "", // domain: let the server choose + "", // host: let the server choose + port, + &TXT_RECORD.map(|s| s.as_bytes()), + ) + .await?; + + entry_group.as_mut().unwrap().commit().await?; + log::debug!("Commited zeroconf service with name {}", &name); + + 'monitor_service: loop { + tokio::select! { + Some(state_changed) = entry_group_state_stream.next() => { + let (state, error) = match state_changed.args() { + Ok(sc) => (sc.state, sc.error), + Err(e) => { + log::warn!("Error on receiving EntryGroup state from Avahi: {}", e); + continue 'monitor_service; + } + }; + match state { + EntryGroupState::Uncommited | EntryGroupState::Registering => { + // Not yet registered, ignore. + } + EntryGroupState::Established => { + log::info!("Published zeroconf service"); + } + EntryGroupState::Collision => { + // This most likely means that librespot has unintentionally been started twice. + // Thus, don't retry with a new name, but abort. + // + // Note that the error would usually already be returned by + // entry_group.add_service above, so this state_changed handler + // won't be hit. + // + // EntryGroup has been withdrawn at this point already! + log::error!("zeroconf collision for name '{}'", &name); + return Err(zbus::Error::Failure(format!("zeroconf collision for name: {}", name)).into()); + } + EntryGroupState::Failure => { + // TODO: Back off/treat as fatal? + // EntryGroup has been withdrawn at this point already! + // There seems to be no code in Avahi that actually sets this state. + log::error!("zeroconf failure: {}", error); + return Err(zbus::Error::Failure(format!("zeroconf failure: {}", error)).into()); + } + } + } + _name_owner_change = stream.next() => { + break 'monitor_service; + } + } + } + + // Avahi disappeared (or the service was immediately taken over by a + // new daemon) => drop all handles, and reconnect + log::info!("Avahi disappeared, trying to reconnect"); + } +} + +#[cfg(feature = "with-avahi")] +fn launch_avahi( + name: Cow<'static, str>, + _zeroconf_ip: Vec, + port: u16, + status_tx: mpsc::UnboundedSender, +) -> Result { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let task_handle = tokio::spawn(async move { + let mut entry_group = None; + tokio::select! { + res = avahi_task(name, port, &mut entry_group) => { + if let Err(e) = res { + log::error!("Avahi error: {}", e); + let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e)); + } + }, + _ = shutdown_rx => { + if let Some(entry_group) = entry_group.as_mut() { + if let Err(e) = entry_group.free().await { + log::warn!("Failed to un-publish zeroconf service: {}", e); + } else { + log::debug!("Un-published zeroconf service"); + } + } + }, + } + }); + + Ok(DnsSdHandle { + task_handle, + shutdown_tx, + }) +} + +#[cfg(feature = "with-dns-sd")] +fn launch_dns_sd( + name: Cow<'static, str>, + _zeroconf_ip: Vec, + port: u16, + status_tx: mpsc::UnboundedSender, +) -> Result { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let task_handle = tokio::task::spawn_blocking(move || { + let inner = move || -> Result<(), DiscoveryError> { + let svc = dns_sd::DNSService::register( + Some(name.as_ref()), + DNS_SD_SERVICE_NAME, + None, + None, + port, + &TXT_RECORD, + ) + .map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?; + + let _ = shutdown_rx.blocking_recv(); + + std::mem::drop(svc); + + Ok(()) + }; + + if let Err(e) = inner() { + log::error!("dns_sd error: {}", e); + let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e)); + } + }); + + Ok(DnsSdHandle { + shutdown_tx, + task_handle, + }) +} + +#[cfg(feature = "with-libmdns")] +fn launch_libmdns( + name: Cow<'static, str>, + zeroconf_ip: Vec, + port: u16, + status_tx: mpsc::UnboundedSender, +) -> Result { + let (shutdown_tx, shutdown_rx) = oneshot::channel(); + + let task_handle = tokio::task::spawn_blocking(move || { + let inner = move || -> Result<(), DiscoveryError> { + let svc = if !zeroconf_ip.is_empty() { + libmdns::Responder::spawn_with_ip_list( + &tokio::runtime::Handle::current(), + zeroconf_ip, + ) + } else { + libmdns::Responder::spawn(&tokio::runtime::Handle::current()) + } + .map_err(|e| DiscoveryError::DnsSdError(Box::new(e))) + .unwrap() + .register( + DNS_SD_SERVICE_NAME.to_owned(), + name.into_owned(), + port, + &TXT_RECORD, + ); + + let _ = shutdown_rx.blocking_recv(); + + std::mem::drop(svc); + + Ok(()) + }; + + if let Err(e) = inner() { + log::error!("libmdns error: {}", e); + let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e)); + } + }); + + Ok(DnsSdHandle { + shutdown_tx, + task_handle, + }) +} + impl Builder { /// Starts a new builder using the provided device and client IDs. pub fn new>(device_id: T, client_id: T) -> Self { @@ -90,6 +445,7 @@ impl Builder { }, port: 0, zeroconf_ip: vec![], + zeroconf_backend: None, } } @@ -117,6 +473,12 @@ impl Builder { self } + /// Set the zeroconf (MDNS and DNS-SD) implementation to use. + pub fn zeroconf_backend(mut self, zeroconf_backend: DnsSdServiceBuilder) -> Self { + self.zeroconf_backend = Some(zeroconf_backend); + 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 { @@ -129,43 +491,21 @@ impl Builder { /// # Errors /// If setting up the mdns service or creating the server fails, this function returns an error. pub fn launch(self) -> Result { + let name = self.server_config.name.clone(); + let zeroconf_ip = self.zeroconf_ip; + + let (event_tx, event_rx) = mpsc::unbounded_channel(); + let mut port = self.port; - let name = self.server_config.name.clone().into_owned(); - let server = DiscoveryServer::new(self.server_config, &mut port)?; - let _zeroconf_ip = self.zeroconf_ip; - let svc; + let server = DiscoveryServer::new(self.server_config, &mut port, event_tx.clone())?; - #[cfg(feature = "with-dns-sd")] - { - svc = dns_sd::DNSService::register( - Some(name.as_ref()), - "_spotify-connect._tcp", - None, - None, - port, - &["VERSION=1.0", "CPath=/"], - )?; - } - - #[cfg(not(feature = "with-dns-sd"))] - { - let _svc = if !_zeroconf_ip.is_empty() { - libmdns::Responder::spawn_with_ip_list( - &tokio::runtime::Handle::current(), - _zeroconf_ip, - )? - } else { - libmdns::Responder::spawn(&tokio::runtime::Handle::current())? - }; - svc = _svc.register( - "_spotify-connect._tcp".to_owned(), - name, - port, - &["VERSION=1.0", "CPath=/"], - ); - } - - Ok(Discovery { server, _svc: svc }) + let launch_svc = self.zeroconf_backend.unwrap_or(find(None)?); + let svc = launch_svc(name, zeroconf_ip, port, event_tx)?; + Ok(Discovery { + server, + svc, + event_rx, + }) } } @@ -179,12 +519,25 @@ impl Discovery { pub fn new>(device_id: T, client_id: T) -> Result { Self::builder(device_id, client_id).launch() } + + pub async fn shutdown(self) { + tokio::join!(self.server.shutdown(), self.svc.shutdown(),); + } } 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) + match Pin::new(&mut self.event_rx).poll_recv(cx) { + // Yields credentials + Poll::Ready(Some(DiscoveryEvent::Credentials(creds))) => Poll::Ready(Some(creds)), + // Also terminate the stream on fatal server or MDNS/DNS-SD errors. + Poll::Ready(Some( + DiscoveryEvent::ServerError(_) | DiscoveryEvent::ZeroconfError(_), + )) => Poll::Ready(None), + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } } } diff --git a/discovery/src/server.rs b/discovery/src/server.rs index f3c979b9..aa66fcb2 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -1,18 +1,14 @@ use std::{ borrow::Cow, collections::BTreeMap, - convert::Infallible, net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpListener}, - pin::Pin, sync::{Arc, Mutex}, - task::{Context, Poll}, }; use aes::cipher::{KeyIvInit, StreamCipher}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::Engine as _; use bytes::Bytes; -use futures_core::Stream; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; use http_body_util::{BodyExt, Full}; @@ -24,7 +20,7 @@ use serde_json::json; use sha1::{Digest, Sha1}; use tokio::sync::{mpsc, oneshot}; -use super::DiscoveryError; +use super::{DiscoveryError, DiscoveryEvent}; use crate::{ core::config::DeviceType, @@ -47,21 +43,17 @@ struct RequestHandler { config: Config, username: Mutex>, keys: DhLocalKeys, - tx: mpsc::UnboundedSender, + event_tx: mpsc::UnboundedSender, } impl RequestHandler { - fn new(config: Config) -> (Self, mpsc::UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded_channel(); - - let discovery = Self { + fn new(config: Config, event_tx: mpsc::UnboundedSender) -> Self { + Self { config, username: Mutex::new(None), keys: DhLocalKeys::random(&mut rand::thread_rng()), - tx, - }; - - (discovery, rx) + event_tx, + } } fn active_user(&self) -> String { @@ -202,7 +194,8 @@ impl RequestHandler { { let maybe_username = self.username.lock(); - self.tx.send(credentials)?; + self.event_tx + .send(DiscoveryEvent::Credentials(credentials))?; if let Ok(mut username_field) = maybe_username { *username_field = Some(String::from(username)); } else { @@ -258,14 +251,22 @@ impl RequestHandler { } } +pub(crate) enum DiscoveryServerCmd { + Shutdown, +} + pub struct DiscoveryServer { - cred_rx: mpsc::UnboundedReceiver, - _close_tx: oneshot::Sender, + close_tx: oneshot::Sender, + task_handle: tokio::task::JoinHandle<()>, } impl DiscoveryServer { - pub fn new(config: Config, port: &mut u16) -> Result { - let (discovery, cred_rx) = RequestHandler::new(config); + pub fn new( + config: Config, + port: &mut u16, + event_tx: mpsc::UnboundedSender, + ) -> Result { + let discovery = RequestHandler::new(config, event_tx); let address = if cfg!(windows) { SocketAddr::new(Ipv4Addr::UNSPECIFIED.into(), *port) } else { @@ -297,7 +298,7 @@ impl DiscoveryServer { } } - tokio::spawn(async move { + let task_handle = tokio::spawn(async move { let discovery = Arc::new(discovery); let server = hyper::server::conn::http1::Builder::new(); @@ -326,27 +327,32 @@ impl DiscoveryServer { }); } _ = &mut close_rx => { - debug!("Shutting down discovery server"); break; } } } graceful.shutdown().await; - debug!("Discovery server stopped"); }); Ok(Self { - cred_rx, - _close_tx: close_tx, + close_tx, + task_handle, }) } -} -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) + pub async fn shutdown(self) { + let Self { + close_tx, + task_handle, + .. + } = self; + log::debug!("Shutting down discovery server"); + if close_tx.send(DiscoveryServerCmd::Shutdown).is_err() { + log::warn!("Discovery server unexpectedly disappeared"); + } else { + let _ = task_handle.await; + log::debug!("Discovery server stopped"); + } } } diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 3d52555e..7426b87f 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -15,4 +15,4 @@ thiserror = "1.0" url = "2.2" [dev-dependencies] -env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } \ No newline at end of file +env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } diff --git a/src/main.rs b/src/main.rs index f87f332e..2da9323a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ use librespot::{ authentication::Credentials, cache::Cache, config::DeviceType, version, Session, SessionConfig, }, + discovery::DnsSdServiceBuilder, playback::{ audio_backend::{self, SinkBuilder, BACKENDS}, config::{ @@ -212,11 +213,11 @@ struct Setup { credentials: Option, enable_oauth: bool, oauth_port: Option, - enable_discovery: bool, zeroconf_port: u16, player_event_program: Option, emit_sink_events: bool, zeroconf_ip: Vec, + zeroconf_backend: Option, } fn get_setup() -> Setup { @@ -277,6 +278,7 @@ fn get_setup() -> Setup { const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; + const ZEROCONF_BACKEND: &str = "zeroconf-backend"; // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; @@ -327,6 +329,7 @@ fn get_setup() -> Setup { const NORMALISATION_RELEASE_SHORT: &str = "y"; const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; const ZEROCONF_PORT_SHORT: &str = "z"; + const ZEROCONF_BACKEND_SHORT: &str = ""; // no short flag // Options that have different descriptions // depending on what backends were enabled at build time. @@ -638,6 +641,12 @@ fn get_setup() -> Setup { ZEROCONF_INTERFACE, "Comma-separated interface IP addresses on which zeroconf will bind. Defaults to all interfaces. Ignored by DNS-SD.", "IP" + ) + .optopt( + ZEROCONF_BACKEND_SHORT, + ZEROCONF_BACKEND, + "Zeroconf (MDNS/DNS-SD) backend to use. Valid values are 'avahi', 'dns-sd' and 'libmdns', if librespot is compiled with the corresponding feature flags.", + "BACKEND" ); #[cfg(feature = "passthrough-decoder")] @@ -803,12 +812,22 @@ fn get_setup() -> Setup { exit(0); } + // Can't use `-> fmt::Arguments` due to https://github.com/rust-lang/rust/issues/92698 + fn format_flag(long: &str, short: &str) -> String { + if short.is_empty() { + format!("`--{long}`") + } else { + format!("`--{long}` / `-{short}`") + } + } + let invalid_error_msg = |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { - error!("Invalid `--{long}` / `-{short}`: \"{invalid}\""); + let flag = format_flag(long, short); + error!("Invalid {flag}: \"{invalid}\""); if !valid_values.is_empty() { - println!("Valid `--{long}` / `-{short}` values: {valid_values}"); + println!("Valid {flag} values: {valid_values}"); } if !default_value.is_empty() { @@ -1190,9 +1209,22 @@ fn get_setup() -> Setup { } }; - let enable_discovery = !opt_present(DISABLE_DISCOVERY); + let no_discovery_reason = if !cfg!(any( + feature = "with-libmdns", + feature = "with-dns-sd", + feature = "with-avahi" + )) { + Some("librespot compiled without zeroconf backend".to_owned()) + } else if opt_present(DISABLE_DISCOVERY) { + Some(format!( + "the `--{}` / `-{}` flag set", + DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, + )) + } else { + None + }; - if credentials.is_none() && !enable_discovery && !enable_oauth { + if credentials.is_none() && no_discovery_reason.is_some() && !enable_oauth { error!("Credentials are required if discovery and oauth login are disabled."); exit(1); } @@ -1225,14 +1257,16 @@ fn get_setup() -> Setup { Some(5588) }; - if !enable_discovery && opt_present(ZEROCONF_PORT) { - warn!( - "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", - DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT - ); + if let Some(reason) = no_discovery_reason.as_deref() { + if opt_present(ZEROCONF_PORT) { + warn!( + "With {} `--{}` / `-{}` has no effect.", + reason, ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + } } - let zeroconf_port = if enable_discovery { + let zeroconf_port = if no_discovery_reason.is_none() { opt_str(ZEROCONF_PORT) .map(|port| match port.parse::() { Ok(value) if value != 0 => value, @@ -1268,6 +1302,16 @@ fn get_setup() -> Setup { None => SessionConfig::default().autoplay, }; + if let Some(reason) = no_discovery_reason.as_deref() { + if opt_present(ZEROCONF_INTERFACE) { + warn!( + "With {} {} has no effect.", + reason, + format_flag(ZEROCONF_INTERFACE, ZEROCONF_INTERFACE_SHORT), + ); + } + } + let zeroconf_ip: Vec = if opt_present(ZEROCONF_INTERFACE) { if let Some(zeroconf_ip) = opt_str(ZEROCONF_INTERFACE) { zeroconf_ip @@ -1293,6 +1337,39 @@ fn get_setup() -> Setup { vec![] }; + if let Some(reason) = no_discovery_reason.as_deref() { + if opt_present(ZEROCONF_BACKEND) { + warn!( + "With {} `--{}` / `-{}` has no effect.", + reason, ZEROCONF_BACKEND, ZEROCONF_BACKEND_SHORT + ); + } + } + + let zeroconf_backend_name = opt_str(ZEROCONF_BACKEND); + let zeroconf_backend = no_discovery_reason.is_none().then(|| { + librespot::discovery::find(zeroconf_backend_name.as_deref()).unwrap_or_else(|_| { + let available_backends: Vec<_> = librespot::discovery::BACKENDS + .iter() + .filter_map(|(id, launch_svc)| launch_svc.map(|_| *id)) + .collect(); + let default_backend = librespot::discovery::BACKENDS + .iter() + .find_map(|(id, launch_svc)| launch_svc.map(|_| *id)) + .unwrap_or(""); + + invalid_error_msg( + ZEROCONF_BACKEND, + ZEROCONF_BACKEND_SHORT, + &zeroconf_backend_name.unwrap_or_default(), + &available_backends.join(", "), + default_backend, + ); + + exit(1); + }) + }); + let connect_config = { let connect_default_config = ConnectConfig::default(); @@ -1734,11 +1811,11 @@ fn get_setup() -> Setup { credentials, enable_oauth, oauth_port, - enable_discovery, zeroconf_port, player_event_program, emit_sink_events, zeroconf_ip, + zeroconf_backend, } } @@ -1767,7 +1844,7 @@ async fn main() { let mut sys = System::new(); - if setup.enable_discovery { + if let Some(zeroconf_backend) = setup.zeroconf_backend { // When started at boot as a service discovery may fail due to it // trying to bind to interfaces before the network is actually up. // This could be prevented in systemd by starting the service after @@ -1787,6 +1864,7 @@ async fn main() { .is_group(setup.connect_config.is_group) .port(setup.zeroconf_port) .zeroconf_ip(setup.zeroconf_ip.clone()) + .zeroconf_backend(zeroconf_backend) .launch() { Ok(d) => break Some(d), @@ -1955,18 +2033,25 @@ async fn main() { info!("Gracefully shutting down"); + let mut shutdown_tasks = tokio::task::JoinSet::new(); + // Shutdown spirc if necessary if let Some(spirc) = spirc { if let Err(e) = spirc.shutdown() { error!("error sending spirc shutdown message: {}", e); } - if let Some(mut spirc_task) = spirc_task { - tokio::select! { - _ = tokio::signal::ctrl_c() => (), - _ = spirc_task.as_mut() => (), - else => (), - } + if let Some(spirc_task) = spirc_task { + shutdown_tasks.spawn(spirc_task); } } + + if let Some(discovery) = discovery { + shutdown_tasks.spawn(discovery.shutdown()); + } + + tokio::select! { + _ = tokio::signal::ctrl_c() => (), + _ = shutdown_tasks.join_all() => (), + } } From cd57f706f64434f7724cd2f7c63e04a036c78f36 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 27 Oct 2024 15:50:27 +0100 Subject: [PATCH 466/561] Clarify dependency ordering --- publish.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/publish.sh b/publish.sh index d1d8f783..c2187dc5 100755 --- a/publish.sh +++ b/publish.sh @@ -6,6 +6,7 @@ DRY_RUN='false' WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR +# Order: dependencies first (so "librespot" using everything before it goes last) crates=( "protocol" "oauth" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) function replace_in_file() { From f96f36c064795011f9fee912291eecb1aa46fff6 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 27 Oct 2024 15:59:25 +0100 Subject: [PATCH 467/561] Fix "source slice length does not match destination" panic on some tracks Fixes #1188 Co-authored-by: thedtvn --- CHANGELOG.md | 5 +++++ core/src/file_id.rs | 12 +++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e585ea8b..dc6985b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- [core] Fix "source slice length (16) does not match destination slice length + (20)" panic on some tracks + ### Changed - [core] The `access_token` for http requests is now acquired by `login5` diff --git a/core/src/file_id.rs b/core/src/file_id.rs index 61b33125..ca23e84d 100644 --- a/core/src/file_id.rs +++ b/core/src/file_id.rs @@ -4,13 +4,19 @@ use librespot_protocol as protocol; use crate::{spotify_id::to_base16, Error}; +const RAW_LEN: usize = 20; + #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct FileId(pub [u8; 20]); +pub struct FileId(pub [u8; RAW_LEN]); impl FileId { pub fn from_raw(src: &[u8]) -> FileId { - let mut dst = [0u8; 20]; - dst.clone_from_slice(src); + let mut dst = [0u8; RAW_LEN]; + let len = src.len(); + // some tracks return 16 instead of 20 bytes: #1188 + if len <= RAW_LEN { + dst[..len].clone_from_slice(src); + } FileId(dst) } From 1d3771a83bd94025ee16a7367a556bfec7e4e4cc Mon Sep 17 00:00:00 2001 From: fivebanger <14848554+fivebanger@users.noreply.github.com> Date: Wed, 30 Oct 2024 20:24:01 +0100 Subject: [PATCH 468/561] Get token with client (#1385) Co-authored-by: fivebanger --- CHANGELOG.md | 1 + core/src/token.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc6985b4..4680fc7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [core] Add `get_token_with_client_id()` to get a token for a specific client ID - [core] Add `login` (mobile) and `auth_token` retrieval via login5 - [core] Add `OS` and `os_version` to `config.rs` - [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus. diff --git a/core/src/token.rs b/core/src/token.rs index 946367c3..445b8aeb 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -58,9 +58,20 @@ impl TokenProvider { }) } + // Not all combinations of scopes and client ID are allowed. + // Depending on the client ID currently used, the function may return an error for specific scopes. + // In this case get_token_with_client_id() can be used, where an appropriate client ID can be provided. // scopes must be comma-separated pub async fn get_token(&self, scopes: &str) -> Result { let client_id = self.session().client_id(); + self.get_token_with_client_id(scopes, &client_id).await + } + + pub async fn get_token_with_client_id( + &self, + scopes: &str, + client_id: &str, + ) -> Result { if client_id.is_empty() { return Err(Error::invalid_argument("Client ID cannot be empty")); } From 797efeafaf992ce2dae8072681949409fdec62a7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 30 Oct 2024 21:11:04 +0100 Subject: [PATCH 469/561] chore: prepare for 0.6.0 release --- CHANGELOG.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4680fc7c..13457c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. -## [Unreleased] +## [0.6.0] - 2024-10-30 -### Fixed - -- [core] Fix "source slice length (16) does not match destination slice length - (20)" panic on some tracks +This version takes another step into the direction of the HTTP API, fixes a +couple of bugs, and makes it easier for developers to mock a certain platform. +Also it adds the option to choose avahi, dnssd or libmdns as your zeroconf +backend for Spotify Connect discovery. ### Changed @@ -27,11 +27,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] Add `OS` and `os_version` to `config.rs` - [discovery] Added a new MDNS/DNS-SD backend which connects to Avahi via D-Bus. -### Removed - -### Fixed +### Fixed - [connect] Fixes initial volume showing zero despite playing in full volume instead +- [core] Fix "source slice length (16) does not match destination slice length + (20)" panic on some tracks ## [0.5.0] - 2024-10-15 @@ -322,7 +322,7 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.5.0...HEAD +[0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2 [0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0...v0.4.1 From 383a6f6969f23b3e3cbc693747101cb9c92463dc Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 30 Oct 2024 21:11:33 +0100 Subject: [PATCH 470/561] Update Cargo.lock --- Cargo.lock | 208 +++++++++++++++++++++---------------------- Cargo.toml | 18 ++-- audio/Cargo.toml | 4 +- connect/Cargo.toml | 8 +- core/Cargo.toml | 6 +- discovery/Cargo.toml | 4 +- metadata/Cargo.toml | 6 +- oauth/Cargo.toml | 2 +- playback/Cargo.toml | 8 +- protocol/Cargo.toml | 2 +- 10 files changed, 133 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7da57b92..b0997283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.15" +version = "0.6.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" dependencies = [ "anstyle", "anstyle-parse", @@ -91,43 +91,43 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" +checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" [[package]] name = "anstyle-parse" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.4" +version = "3.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arrayvec" @@ -216,7 +216,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -251,7 +251,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -357,7 +357,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.79", + "syn 2.0.85", "which", ] @@ -376,7 +376,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -433,15 +433,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" [[package]] name = "cc" -version = "1.1.30" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", @@ -532,9 +532,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" [[package]] name = "combine" @@ -676,7 +676,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -687,7 +687,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -740,7 +740,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -750,7 +750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -789,9 +789,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "encoding_rs" -version = "0.8.34" +version = "0.8.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" dependencies = [ "cfg-if", ] @@ -820,7 +820,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -965,9 +965,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.3.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" dependencies = [ "fastrand", "futures-core", @@ -984,7 +984,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -1063,9 +1063,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7efc368de04755344f0084104835b6bb71df2c1d41e37d863947392a894779" +checksum = "217f464cad5946ae4369c355155e2d16b488c08920601083cb4891e352ae777b" dependencies = [ "glib-sys", "gobject-sys", @@ -1076,9 +1076,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adcf1ec6d3650bf9fdbc6cee242d4fcebc6f6bfd9bea5b929b6a8b7344eb85ff" +checksum = "358431b0e0eb15b9d02db52e1f19c805b953c5c168099deb3de88beab761768c" dependencies = [ "bitflags 2.6.0", "futures-channel", @@ -1097,22 +1097,22 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6bf88f70cd5720a6197639dcabcb378dd528d0cb68cb1f45e3b358bcb841cd7" +checksum = "e7d21ca27acfc3e91da70456edde144b4ac7c36f78ee77b10189b3eb4901c156" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "glib-sys" -version = "0.20.4" +version = "0.20.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f9eca5d88cfa6a453b00d203287c34a2b7cac3a7831779aa2bb0b3c7233752b" +checksum = "8a5911863ab7ecd4a6f8d5976f12eeba076b23669c49b066d877e742544aa389" dependencies = [ "libc", "system-deps", @@ -1580,7 +1580,7 @@ dependencies = [ "hyper 1.5.0", "hyper-util", "log", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -1590,9 +1590,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", "futures-channel", @@ -1795,9 +1795,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.159" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" @@ -1821,9 +1821,9 @@ dependencies = [ [[package]] name = "libm" -version = "0.2.8" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" +checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" [[package]] name = "libmdns" @@ -1893,7 +1893,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.5.0" +version = "0.6.0" dependencies = [ "data-encoding", "env_logger", @@ -1917,7 +1917,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aes", "bytes", @@ -1936,7 +1936,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.5.0" +version = "0.6.0" dependencies = [ "form_urlencoded", "futures-util", @@ -1955,7 +1955,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aes", "base64 0.22.1", @@ -2009,7 +2009,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.5.0" +version = "0.6.0" dependencies = [ "aes", "base64 0.22.1", @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.5.0" +version = "0.6.0" dependencies = [ "async-trait", "bytes", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "librespot-oauth" -version = "0.5.0" +version = "0.6.0" dependencies = [ "env_logger", "log", @@ -2067,7 +2067,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.5.0" +version = "0.6.0" dependencies = [ "alsa", "cpal", @@ -2099,7 +2099,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.5.0" +version = "0.6.0" dependencies = [ "protobuf", "protobuf-codegen", @@ -2331,7 +2331,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2392,7 +2392,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2571,9 +2571,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" [[package]] name = "pin-utils" @@ -2678,12 +2678,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -2708,9 +2708,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -2836,9 +2836,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", @@ -2963,9 +2963,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.37" +version = "0.38.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" dependencies = [ "bitflags 2.6.0", "errno", @@ -3002,9 +3002,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.14" +version = "0.23.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8" +checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" dependencies = [ "aws-lc-rs", "log", @@ -3181,29 +3181,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -3229,7 +3229,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3480,9 +3480,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.79" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -3563,22 +3563,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3641,9 +3641,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.40.0" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -3666,7 +3666,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3696,7 +3696,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pki-types", "tokio", ] @@ -3720,7 +3720,7 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -3800,7 +3800,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -3831,7 +3831,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.23.14", + "rustls 0.23.16", "rustls-pki-types", "sha1", "thiserror", @@ -4025,7 +4025,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -4059,7 +4059,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4208,7 +4208,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4219,7 +4219,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4516,7 +4516,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "zvariant_utils", ] @@ -4549,7 +4549,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] [[package]] @@ -4580,7 +4580,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", "zvariant_utils", ] @@ -4592,5 +4592,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.79", + "syn 2.0.85", ] diff --git a/Cargo.toml b/Cargo.toml index a6d216b3..8c5be612 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.5.0" +version = "0.6.0" rust-version = "1.75" authors = ["Librespot Org"] license = "MIT" @@ -23,36 +23,36 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-connect] path = "connect" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-core] path = "core" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.5.0" +version = "0.6.0" default-features = false [dependencies.librespot-metadata] path = "metadata" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-playback] path = "playback" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-oauth] path = "oauth" -version = "0.5.0" +version = "0.6.0" [dependencies] data-encoding = "2.5" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 713a499b..3529cff1 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" @@ -10,7 +10,7 @@ edition = "2021" [dependencies.librespot-core] path = "../core" -version = "0.5.0" +version = "0.6.0" [dependencies] aes = "0.8" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index bb055db1..2c680399 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" @@ -22,12 +22,12 @@ tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-playback] path = "../playback" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0" +version = "0.6.0" diff --git a/core/Cargo.toml b/core/Cargo.toml index a9208bdf..4adf9cb9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Lietar "] build = "build.rs" @@ -11,11 +11,11 @@ edition = "2021" [dependencies.librespot-oauth] path = "../oauth" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0" +version = "0.6.0" [dependencies] aes = "0.8" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index fa0746ef..c42f2c2b 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery logic for librespot" @@ -34,7 +34,7 @@ zbus = { version = "4", default-features = false, features = ["tokio"], optional [dependencies.librespot-core] path = "../core" -version = "0.5.0" +version = "0.6.0" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 813be783..7e94da48 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Lietar "] description = "The metadata logic for librespot" @@ -20,8 +20,8 @@ serde_json = "1.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.5.0" +version = "0.6.0" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 7426b87f..e6c92248 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-oauth" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Nick Steel "] description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 4a9f3109..20f556df 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" @@ -10,15 +10,15 @@ edition = "2021" [dependencies.librespot-audio] path = "../audio" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-core] path = "../core" -version = "0.5.0" +version = "0.6.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.5.0" +version = "0.6.0" [dependencies] futures-util = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 60c48866..d982a408 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.5.0" +version = "0.6.0" rust-version.workspace = true authors = ["Paul Liétar "] build = "build.rs" From e2eca65d11f57fe13558f4bf277c044828bc3bc1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 31 Oct 2024 23:14:13 +0100 Subject: [PATCH 471/561] chore: bump to v0.6.0-dev --- CHANGELOG.md | 11 +++++++++++ Cargo.lock | 18 +++++++++--------- Cargo.toml | 20 ++++++++++---------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 8 ++++---- core/Cargo.toml | 6 +++--- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- oauth/Cargo.toml | 2 +- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 11 files changed, 50 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13457c7c..82bfb094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. 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] - YYYY-MM-DD + +### Changed + +### Added + +### Fixed + +### Removed + ## [0.6.0] - 2024-10-30 This version takes another step into the direction of the HTTP API, fixes a @@ -322,6 +332,7 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.6.0...HEAD [0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2 diff --git a/Cargo.lock b/Cargo.lock index b0997283..4938ac8d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1893,7 +1893,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "data-encoding", "env_logger", @@ -1917,7 +1917,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "aes", "bytes", @@ -1936,7 +1936,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "form_urlencoded", "futures-util", @@ -1955,7 +1955,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "aes", "base64 0.22.1", @@ -2009,7 +2009,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "aes", "base64 0.22.1", @@ -2040,7 +2040,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "async-trait", "bytes", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "librespot-oauth" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "env_logger", "log", @@ -2067,7 +2067,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "alsa", "cpal", @@ -2099,7 +2099,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.6.0" +version = "0.6.0-dev" dependencies = [ "protobuf", "protobuf-codegen", diff --git a/Cargo.toml b/Cargo.toml index 8c5be612..d0a558e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.6.0" +version = "0.6.0-dev" rust-version = "1.75" authors = ["Librespot Org"] license = "MIT" @@ -23,36 +23,36 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-connect] path = "connect" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-core] path = "core" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-discovery] path = "discovery" -version = "0.6.0" +version = "0.6.0-dev" default-features = false [dependencies.librespot-metadata] path = "metadata" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-playback] path = "playback" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-protocol] path = "protocol" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-oauth] path = "oauth" -version = "0.6.0" +version = "0.6.0-dev" [dependencies] data-encoding = "2.5" @@ -103,4 +103,4 @@ assets = [ ] [workspace.package] -rust-version = "1.74" +rust-version = "1.75" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 3529cff1..3ef84532 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Lietar "] description = "The audio fetching logic for librespot" @@ -10,7 +10,7 @@ edition = "2021" [dependencies.librespot-core] path = "../core" -version = "0.6.0" +version = "0.6.0-dev" [dependencies] aes = "0.8" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 2c680399..e35d3c77 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" @@ -22,12 +22,12 @@ tokio-stream = "0.1" [dependencies.librespot-core] path = "../core" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-playback] path = "../playback" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-protocol] path = "../protocol" -version = "0.6.0" +version = "0.6.0-dev" diff --git a/core/Cargo.toml b/core/Cargo.toml index 4adf9cb9..994f1ab2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Lietar "] build = "build.rs" @@ -11,11 +11,11 @@ edition = "2021" [dependencies.librespot-oauth] path = "../oauth" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-protocol] path = "../protocol" -version = "0.6.0" +version = "0.6.0-dev" [dependencies] aes = "0.8" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index c42f2c2b..01383f2e 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Lietar "] description = "The discovery logic for librespot" @@ -34,7 +34,7 @@ zbus = { version = "4", default-features = false, features = ["tokio"], optional [dependencies.librespot-core] path = "../core" -version = "0.6.0" +version = "0.6.0-dev" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 7e94da48..69efdf78 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Lietar "] description = "The metadata logic for librespot" @@ -20,8 +20,8 @@ serde_json = "1.0" [dependencies.librespot-core] path = "../core" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-protocol] path = "../protocol" -version = "0.6.0" +version = "0.6.0-dev" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index e6c92248..32148b59 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-oauth" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Nick Steel "] description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 20f556df..e9f6d438 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" @@ -10,15 +10,15 @@ edition = "2021" [dependencies.librespot-audio] path = "../audio" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-core] path = "../core" -version = "0.6.0" +version = "0.6.0-dev" [dependencies.librespot-metadata] path = "../metadata" -version = "0.6.0" +version = "0.6.0-dev" [dependencies] futures-util = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index d982a408..bfaf96bc 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.6.0" +version = "0.6.0-dev" rust-version.workspace = true authors = ["Paul Liétar "] build = "build.rs" From 82076e882f3cdebec863a3c0aa79888ec47b3c76 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 3 Nov 2024 20:15:48 +0100 Subject: [PATCH 472/561] docs: recommend at least one audio and discovery backend (#1390) --- COMPILING.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 4b4b58af..4d238862 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -97,15 +97,17 @@ You will most likely want to build debug builds when developing, as they compile There are also a number of compiler feature flags that you can add, in the event that you want to have certain additional features also compiled. The list of these is available on the [wiki](https://github.com/librespot-org/librespot/wiki/Compiling#addition-features). -By default, librespot compiles with the ```rodio-backend``` feature. To compile without default features, you can run with: +By default, librespot compiles with the ```rodio-backend``` and ```with-libmdns``` features. To compile without default features, you can run with: ```bash cargo build --no-default-features ``` -Similarly, to build with the ALSA backend: +Note that this will also disable zeroconf discovery backends for Spotify Connect. For normal use cases, select at least one audio and discovery backend. +For example, to build with the ALSA audio and libmdns discovery backend: + ```bash -cargo build --no-default-features --features "alsa-backend" +cargo build --no-default-features --features "alsa-backend with-libmdns" ``` ### Running From 4c0d8ebf1a6cfa60f0200082996bf7ae93186ddc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 5 Dec 2024 07:57:11 +0100 Subject: [PATCH 473/561] Bump hashbrown from 0.15.0 to 0.15.2 (#1409) Bumps [hashbrown](https://github.com/rust-lang/hashbrown) from 0.15.0 to 0.15.2. - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/commits) --- updated-dependencies: - dependency-name: hashbrown dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4938ac8d..3de6c41c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1315,9 +1315,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.0" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "headers" From 705e68ec658e4a11630fbc4371d0b53b1a76a9e4 Mon Sep 17 00:00:00 2001 From: SilverMira <66930495+SilverMira@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:11:40 +0800 Subject: [PATCH 474/561] feat: use webpki as rustls roots on non-desktop platforms (#1402) * feat: use webpki as rustls roots on non-desktop platforms Silently switch over to using `rustls-webpki` when building for target_os that is not Windows/Linux/Mac because `rustls-native-certs` doesn't support them. Ideally we should use `rustls-platform-verifier` as it's now the recommended crate even on `rustls-native-certs` repository, since it chooses the right implementation for the platform. But currently it doesn't seem like `hyper-proxy2` or `tokio-tungstenite` doesn't support them yet. * Fix "no native root CA certificates found" (#1399) --- CHANGELOG.md | 3 +++ Cargo.lock | 15 ++++++++++++++- core/Cargo.toml | 15 ++++++++++++--- core/src/http_client.rs | 16 ++++++++++------ 4 files changed, 39 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82bfb094..60d1eeaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- [core] Fix "no native root CA certificates found" on platforms unsupported + by `rustls-native-certs`. + ### Removed ## [0.6.0] - 2024-10-30 diff --git a/Cargo.lock b/Cargo.lock index 3de6c41c..6944c96d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1534,6 +1534,7 @@ dependencies = [ "tokio-rustls 0.25.0", "tower-service", "webpki", + "webpki-roots 0.26.7", ] [[package]] @@ -1567,6 +1568,7 @@ dependencies = [ "tokio", "tokio-rustls 0.25.0", "tower-service", + "webpki-roots 0.26.7", ] [[package]] @@ -1586,6 +1588,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.0", "tower-service", + "webpki-roots 0.26.7", ] [[package]] @@ -2900,7 +2903,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg", ] @@ -3726,6 +3729,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.0", "tungstenite", + "webpki-roots 0.26.7", ] [[package]] @@ -4096,6 +4100,15 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" diff --git a/core/Cargo.toml b/core/Cargo.toml index 994f1ab2..b76e6b8a 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -32,8 +32,6 @@ http = "1.0" hyper = { version = "1.3", features = ["http1", "http2"] } hyper-util = { version = "0.1", features = ["client"] } http-body-util = "0.1.1" -hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.27.2", features = ["http2"] } log = "0.4" nonzero_ext = "0.3" num-bigint = { version = "0.4", features = ["rand"] } @@ -58,12 +56,23 @@ thiserror = "1.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } tokio-stream = "0.1" -tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } data-encoding = "2.5" +# Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies +# but currently, hyper-proxy2 and tokio-tungstenite do not support it. +[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies] +hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } +hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "native-tokio", "http2"] } +tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } + +[target.'cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))'.dependencies] +hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls-webpki"] } +hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "webpki-tokio", "http2"] } +tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-webpki-roots"] } + [build-dependencies] rand = "0.8" vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 8645d3a3..88d41c92 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -145,12 +145,16 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process - let https_connector = HttpsConnectorBuilder::new() - .with_native_roots()? - .https_or_http() - .enable_http1() - .enable_http2() - .build(); + + // On supported platforms, use native roots + #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] + let tls = HttpsConnectorBuilder::new().with_native_roots()?; + + // Otherwise, use webpki roots + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + let tls = HttpsConnectorBuilder::new().with_webpki_roots(); + + let https_connector = tls.https_or_http().enable_http1().enable_http2().build(); // When not using a proxy a dummy proxy is configured that will not intercept any traffic. // This prevents needing to carry the Client Connector generics through the whole project From f646ef2b5ae388c49ef9224f79621257ad842144 Mon Sep 17 00:00:00 2001 From: SilverMira <66930495+SilverMira@users.noreply.github.com> Date: Thu, 5 Dec 2024 21:20:03 +0800 Subject: [PATCH 475/561] fix: streaming on Android devices (#1403) * fix: android Session::connect failure from TryAnotherAP It appears that a combination of `Platform::PLATFORM_ANDROID_ARM` and connecting with `Credentials::with_access_token` causes all AP to error TryAnotherAP * fix: getting api access token should respect client id If we are trying to get an access token from login5 using stored credentials, ie: from oauth flow, we should use the oauth's client ID, this matches with the semantics as described in `Login5Manager::auth_token` * fix: cpu_family arm64 should be aarch64 * Fix audio streaming on Android platform (#1399) --- CHANGELOG.md | 4 ++++ core/src/connection/handshake.rs | 8 ++++++-- core/src/connection/mod.rs | 2 +- core/src/login5.rs | 5 +++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 60d1eeaa..3fb81847 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] Fix "no native root CA certificates found" on platforms unsupported by `rustls-native-certs`. +- [core] Fix all APs rejecting with "TryAnotherAP" when connecting session + on Android platform. +- [core] Fix "Invalid Credentials" when using a Keymaster access token and + client ID on Android platform. ### Removed diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 03b35598..c94b1fb7 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -111,7 +111,6 @@ where thread_rng().fill_bytes(&mut client_nonce); let platform = match crate::config::OS { - "android" => Platform::PLATFORM_ANDROID_ARM, "freebsd" | "netbsd" | "openbsd" => match ARCH { "x86_64" => Platform::PLATFORM_FREEBSD_X86_64, _ => Platform::PLATFORM_FREEBSD_X86, @@ -120,7 +119,12 @@ where "aarch64" => Platform::PLATFORM_IPHONE_ARM64, _ => Platform::PLATFORM_IPHONE_ARM, }, - "linux" => match ARCH { + // Rather than sending `Platform::PLATFORM_ANDROID_ARM` for "android", + // we are spoofing "android" as "linux", as otherwise during Session::connect + // all APs will reject the client with TryAnotherAP, no matter the credentials + // used was obtained via OAuth using KEYMASTER or ANDROID's client ID or + // Login5Manager::login + "linux" | "android" => match ARCH { "arm" | "aarch64" => Platform::PLATFORM_LINUX_ARM, "blackfin" => Platform::PLATFORM_LINUX_BLACKFIN, "mips" => Platform::PLATFORM_LINUX_MIPS, diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 2e9bbdb4..6f2c7d44 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -101,7 +101,7 @@ pub async fn authenticate( let cpu_family = match std::env::consts::ARCH { "blackfin" => CpuFamily::CPU_BLACKFIN, - "arm" | "arm64" => CpuFamily::CPU_ARM, + "arm" | "aarch64" => CpuFamily::CPU_ARM, "ia64" => CpuFamily::CPU_IA64, "mips" => CpuFamily::CPU_MIPS, "ppc" => CpuFamily::CPU_PPC, diff --git a/core/src/login5.rs b/core/src/login5.rs index dca8f27e..75f739a1 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -75,6 +75,11 @@ impl Login5Manager { async fn login5_request(&self, login: Login_method) -> Result { let client_id = match OS { "macos" | "windows" => self.session().client_id(), + // StoredCredential is used to get an access_token from Session credentials. + // Using the session client_id allows user to use Keymaster on Android/IOS + // if their Credentials::with_access_token was obtained there, assuming + // they have overriden the SessionConfig::client_id with the Keymaster's. + _ if matches!(login, Login_method::StoredCredential(_)) => self.session().client_id(), _ => SessionConfig::default().client_id, }; From 5839b3619288088ba55fd4c8933bb7bf808923f7 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Tue, 10 Dec 2024 20:36:09 +0100 Subject: [PATCH 476/561] Spirc: Replace Mecury with Dealer (#1356) This was a huge effort by photovoltex@gmail.com with help from the community. Over 140 commits were squashed. Below, their commit messages are kept unchanged. --- * dealer wrapper for ease of use * improve sending protobuf requests * replace connect config with connect_state config * start integrating dealer into spirc * payload handling, gzip support * put connect state consistent * formatting * request payload handling, gzip support * expose dealer::protocol, move request in own file * integrate handle of connect-state commands * spirc: remove ident field * transfer playing state better * spirc: remove remote_update stream * spirc: replace command sender with connect state update * spirc: remove device state and remaining unused methods * spirc: remove mercury sender * add repeat track state * ConnectState: add methods to replace state in spirc * spirc: move context into connect_state, update load and next * spirc: remove state, adjust remaining methods * spirc: handle more dealer request commands * revert rustfmt.toml * spirc: impl shuffle - impl shuffle again - extracted fill up of next tracks in own method - moved queue revision update into next track fill up - removed unused method `set_playing_track_index` - added option to specify index when resetting the playback context - reshuffle after repeat context * spirc: handle device became inactive * dealer: adjust payload handling * spirc: better set volume handling * dealer: box PlayCommand (clippy warning) * dealer: always respect queued tracks * spirc: update duration of track * ConnectState: update more restrictions * cleanup * spirc: handle queue requests * spirc: skip next with track * proto: exclude spirc.proto - move "deserialize_with" functions into own file - replace TrackRef with ProvidedTrack * spirc: stabilize transfer/context handling * core: cleanup some remains * connect: improvements to code structure and performance - use VecDeque for next and prev tracks * connect: delayed volume update * connect: move context resolve into own function * connect: load context asynchronous * connect: handle reconnect - might currently steal the active devices playback * connect: some fixes and adjustments - fix wrong offset when transferring playback - fix missing displayed context in web-player - remove access_token from log - send correct state reason when updating volume - queue track correctly - fix wrong assumption for skip_to * connect: replace error case with option * connect: use own context state * connect: more stabilising - handle SkipTo having no Index - handle no transferred restrictions - handle no transferred index - update state before shutdown, for smoother reacquiring * connect: working autoplay * connect: handle repeat context/track * connect: some quick fixes - found self-named uid in collection after reconnecting * connect: handle add_to_queue via set_queue * fix clippy warnings * fix check errors, fix/update example * fix 1.75 specific error * connect: position update improvements * connect: handle unavailable * connect: fix incorrect status handling for desktop and mobile * core: fix dealer reconnect - actually acquire new token - use login5 token retrieval * connect: split state into multiple files * connect: encapsulate provider logic * connect: remove public access to next and prev tracks * connect: remove public access to player * connect: move state only commands into own file * connect: improve logging * connect: handle transferred queue again * connect: fix all-features specific error * connect: extract transfer handling into own file * connect: remove old context model * connect: handle more transfer cases correctly * connect: do auth_token pre-acquiring earlier * connect: handle play with skip_to by uid * connect: simplified cluster update log * core/connect: add remaining set value commands * connect: position update workaround/fix * connect: some queue cleanups * connect: add uid to queue * connect: duration as volume delay const * connect: some adjustments and todo cleanups - send volume update before general update - simplify queue revision to use the track uri - argument why copying the prev/next tracks is fine * connect: handle shuffle from set_options * connect: handle context update * connect: move other structs into model.rs * connect: reduce SpircCommand visibility * connect: fix visibility of model * connect: fix: shuffle on startup isn't applied * connect: prevent loading a context with no tracks * connect: use the first page of a context * connect: improve context resolving - support multiple pages - support page_url of context - handle single track * connect: prevent integer underflow * connect: rename method for better clarity * connect: handle mutate and update messages * connect: fix 1.75 problems * connect: fill, instead of replace next page * connect: reduce context update to single method * connect: remove unused SpircError, handle local files * connect: reduce nesting, adjust initial transfer handling * connect: don't update volume initially * core: disable trace logging of handled mercury responses * core/connect: prevent takeover from other clients, handle session-update * connect: add queue-uid for set_queue command * connect: adjust fields for PlayCommand * connect: preserve context position after update_context * connect: unify metadata modification - only handle `is_queued` `true` items for queue * connect: polish request command handling - reply to all request endpoints - adjust some naming - add some docs * connect: add uid to tracks without * connect: simpler update of current index * core/connect: update log msg, fix wrong behavior - handle became inactive separately - remove duplicate stop - adjust docs for websocket request * core: add option to request without metrics and salt * core/context: adjust context requests and update - search should now return the expected context - removed workaround for single track playback - move local playback check into update_context - check track uri for invalid characters - early return with `?` * connect: handle possible search context uri * connect: remove logout support - handle logout command - disable support for logout - add todos for logout * connect: adjust detailed tracks/context handling - always allow next - handle no prev track available - separate active and fill up context * connect: adjust context resolve handling, again * connect: add autoplay metadata to tracks - transfer into autoplay again * core/connect: cleanup session after spirc stops * update CHANGELOG.md * playback: fix clippy warnings * connect: adjust metadata - unify naming - move more metadata infos into metadata.rs * connect: add delimiter between context and autoplay playback * connect: stop and resume correctly * connect: adjust context resolving - improved certain logging parts - preload autoplay when autoplay attribute mutates - fix transfer context uri - fix typo - handle empty strings for resolve uri - fix unexpected stop of playback * connect: ignore failure during stop * connect: revert resolve_uri changes * connect: correct context reset * connect: reduce boiler code * connect: fix some incorrect states - uid getting replaced by empty value - shuffle/repeat clearing autoplay context - fill_up updating and using incorrect index * core: adjust incorrect separator * connect: move `add_to_queue` and `mark_unavailable` into tracks.rs * connect: refactor - directly modify PutStateRequest - replace `next_tracks`, `prev_tracks`, `player` and `device` with `request` - provide helper methods for the removed fields * connect: adjust handling of context metadata/restrictions * connect: fix incorrect context states * connect: become inactive when no cluster is reported * update CHANGELOG.md * core/playback: preemptively fix clippy warnings * connect: minor adjustment to session changed * connect: change return type changing active context * connect: handle unavailable contexts * connect: fix previous restrictions blocking load with shuffle * connect: update comments and logging * core/connect: reduce some more duplicate code * more docs around the dealer --- CHANGELOG.md | 9 + Cargo.lock | 35 +- connect/Cargo.toml | 3 +- connect/src/config.rs | 22 - connect/src/context.rs | 121 -- connect/src/lib.rs | 4 +- connect/src/model.rs | 188 +++ connect/src/spirc.rs | 1963 ++++++++++++----------- connect/src/state.rs | 448 ++++++ connect/src/state/context.rs | 415 +++++ connect/src/state/handle.rs | 65 + connect/src/state/metadata.rs | 84 + connect/src/state/options.rs | 88 + connect/src/state/provider.rs | 66 + connect/src/state/restrictions.rs | 61 + connect/src/state/tracks.rs | 422 +++++ connect/src/state/transfer.rs | 146 ++ core/Cargo.toml | 2 + core/src/config.rs | 27 + core/src/connection/handshake.rs | 4 +- core/src/dealer/manager.rs | 174 ++ core/src/dealer/maps.rs | 35 +- core/src/dealer/mod.rs | 144 +- core/src/dealer/protocol.rs | 178 +- core/src/dealer/protocol/request.rs | 208 +++ core/src/deserialize_with.rs | 94 ++ core/src/error.rs | 6 + core/src/http_client.rs | 2 +- core/src/lib.rs | 3 +- core/src/mercury/mod.rs | 9 +- core/src/session.rs | 54 +- core/src/spclient.rs | 218 ++- core/src/spotify_id.rs | 13 - core/src/util.rs | 7 + core/src/version.rs | 3 + docs/dealer.md | 79 + examples/play_connect.rs | 20 +- playback/src/audio_backend/portaudio.rs | 6 +- playback/src/audio_backend/rodio.rs | 2 +- playback/src/player.rs | 21 +- protocol/build.rs | 3 +- src/main.rs | 55 +- src/player_event_handler.rs | 5 +- 43 files changed, 4229 insertions(+), 1283 deletions(-) delete mode 100644 connect/src/config.rs delete mode 100644 connect/src/context.rs create mode 100644 connect/src/model.rs create mode 100644 connect/src/state.rs create mode 100644 connect/src/state/context.rs create mode 100644 connect/src/state/handle.rs create mode 100644 connect/src/state/metadata.rs create mode 100644 connect/src/state/options.rs create mode 100644 connect/src/state/provider.rs create mode 100644 connect/src/state/restrictions.rs create mode 100644 connect/src/state/tracks.rs create mode 100644 connect/src/state/transfer.rs create mode 100644 core/src/dealer/manager.rs create mode 100644 core/src/dealer/protocol/request.rs create mode 100644 core/src/deserialize_with.rs create mode 100644 docs/dealer.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fb81847..bb13097c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- [connect] Replaced `ConnectConfig` with `ConnectStateConfig` (breaking) +- [connect] Replaced `playing_track_index` field of `SpircLoadCommand` with `playing_track` (breaking) +- [connect] Replaced Mercury usage in `Spirc` with Dealer + ### Added +- [connect] Add `seek_to` field to `SpircLoadCommand` (breaking) +- [connect] Add `repeat_track` field to `SpircLoadCommand` (breaking) +- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) +- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` + ### Fixed - [core] Fix "no native root CA certificates found" on platforms unsupported diff --git a/Cargo.lock b/Cargo.lock index 6944c96d..36e67519 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -630,6 +630,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -894,6 +903,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1941,7 +1960,6 @@ dependencies = [ name = "librespot-connect" version = "0.6.0-dev" dependencies = [ - "form_urlencoded", "futures-util", "librespot-core", "librespot-playback", @@ -1949,11 +1967,11 @@ dependencies = [ "log", "protobuf", "rand", - "serde", "serde_json", "thiserror", "tokio", "tokio-stream", + "uuid", ] [[package]] @@ -1965,6 +1983,7 @@ dependencies = [ "byteorder", "bytes", "data-encoding", + "flate2", "form_urlencoded", "futures-core", "futures-util", @@ -1991,6 +2010,7 @@ dependencies = [ "pin-project-lite", "priority-queue", "protobuf", + "protobuf-json-mapping", "quick-xml", "rand", "rsa", @@ -2744,6 +2764,17 @@ dependencies = [ "thiserror", ] +[[package]] +name = "protobuf-json-mapping" +version = "3.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b445cf83c9303695e6c423d269759e139b6182d2f1171e18afda7078a764336" +dependencies = [ + "protobuf", + "protobuf-support", + "thiserror", +] + [[package]] name = "protobuf-parse" version = "3.7.1" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index e35d3c77..7ed3fab7 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -9,16 +9,15 @@ repository = "https://github.com/librespot-org/librespot" edition = "2021" [dependencies] -form_urlencoded = "1.0" futures-util = "0.3" log = "0.4" protobuf = "3.5" rand = "0.8" -serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio-stream = "0.1" +uuid = { version = "1.11.0", features = ["v4"] } [dependencies.librespot-core] path = "../core" diff --git a/connect/src/config.rs b/connect/src/config.rs deleted file mode 100644 index 278ecf17..00000000 --- a/connect/src/config.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::core::config::DeviceType; - -#[derive(Clone, Debug)] -pub struct ConnectConfig { - pub name: String, - pub device_type: DeviceType, - pub is_group: bool, - 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(), - is_group: false, - initial_volume: Some(50), - has_volume_ctrl: true, - } - } -} diff --git a/connect/src/context.rs b/connect/src/context.rs deleted file mode 100644 index 9428faac..00000000 --- a/connect/src/context.rs +++ /dev/null @@ -1,121 +0,0 @@ -// TODO : move to metadata - -use crate::core::spotify_id::SpotifyId; -use crate::protocol::spirc::TrackRef; - -use serde::{ - de::{Error, Unexpected}, - Deserialize, -}; - -#[derive(Deserialize, Debug, Default, Clone)] -pub struct StationContext { - pub uri: String, - pub title: String, - #[serde(rename = "titleUri")] - pub title_uri: String, - pub subtitles: Vec, - #[serde(rename = "imageUri")] - pub image_uri: String, - pub seeds: Vec, - #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] - pub tracks: Vec, - pub next_page_url: String, - pub correlation_id: String, - pub related_artists: Vec, -} - -#[derive(Deserialize, Debug, Default, Clone)] -pub struct PageContext { - #[serde(deserialize_with = "deserialize_protobuf_TrackRef")] - pub tracks: Vec, - pub next_page_url: String, - pub correlation_id: String, -} - -#[derive(Deserialize, Debug, Default, Clone)] -pub struct TrackContext { - pub uri: String, - pub uid: String, - pub artist_uri: String, - pub album_uri: String, - #[serde(rename = "original_gid")] - pub gid: String, - pub metadata: MetadataContext, - pub name: String, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug, Default, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ArtistContext { - #[serde(rename = "artistName")] - artist_name: String, - #[serde(rename = "imageUri")] - image_uri: String, - #[serde(rename = "artistUri")] - artist_uri: String, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug, Default, Clone)] -pub struct MetadataContext { - album_title: String, - artist_name: String, - artist_uri: String, - image_url: String, - title: String, - #[serde(deserialize_with = "bool_from_string")] - is_explicit: bool, - #[serde(deserialize_with = "bool_from_string")] - is_promotional: bool, - decision_id: String, -} - -#[allow(dead_code)] -#[derive(Deserialize, Debug, Default, Clone)] -pub struct SubtitleContext { - name: String, - uri: String, -} - -fn bool_from_string<'de, D>(de: D) -> Result -where - D: serde::Deserializer<'de>, -{ - match String::deserialize(de)?.as_ref() { - "true" => Ok(true), - "false" => Ok(false), - other => Err(D::Error::invalid_value( - Unexpected::Str(other), - &"true or false", - )), - } -} - -#[allow(non_snake_case)] -fn deserialize_protobuf_TrackRef<'d, D>(de: D) -> Result, D::Error> -where - D: serde::Deserializer<'d>, -{ - let v: Vec = serde::Deserialize::deserialize(de)?; - 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) - .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()); - Ok(t) - }) - .collect::, D::Error>>() -} diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 193e5db5..3cfbbca1 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -5,6 +5,6 @@ use librespot_core as core; use librespot_playback as playback; use librespot_protocol as protocol; -pub mod config; -pub mod context; +mod model; pub mod spirc; +pub mod state; diff --git a/connect/src/model.rs b/connect/src/model.rs new file mode 100644 index 00000000..f9165eae --- /dev/null +++ b/connect/src/model.rs @@ -0,0 +1,188 @@ +use crate::state::ConnectState; +use librespot_core::dealer::protocol::SkipTo; +use librespot_protocol::player::Context; +use std::fmt::{Display, Formatter}; +use std::hash::{Hash, Hasher}; + +#[derive(Debug)] +pub struct SpircLoadCommand { + pub context_uri: String, + /// Whether the given tracks should immediately start playing, or just be initially loaded. + pub start_playing: bool, + pub seek_to: u32, + pub shuffle: bool, + pub repeat: bool, + pub repeat_track: bool, + pub playing_track: PlayingTrack, +} + +#[derive(Debug)] +pub enum PlayingTrack { + Index(u32), + Uri(String), + Uid(String), +} + +impl From for PlayingTrack { + fn from(value: SkipTo) -> Self { + // order of checks is important, as the index can be 0, but still has an uid or uri provided, + // so we only use the index as last resort + if let Some(uri) = value.track_uri { + PlayingTrack::Uri(uri) + } else if let Some(uid) = value.track_uid { + PlayingTrack::Uid(uid) + } else { + PlayingTrack::Index(value.track_index.unwrap_or_else(|| { + warn!("SkipTo didn't provided any point to skip to, falling back to index 0"); + 0 + })) + } + } +} + +#[derive(Debug)] +pub(super) enum SpircPlayStatus { + Stopped, + LoadingPlay { + position_ms: u32, + }, + LoadingPause { + position_ms: u32, + }, + Playing { + nominal_start_time: i64, + preloading_of_next_track_triggered: bool, + }, + Paused { + position_ms: u32, + preloading_of_next_track_triggered: bool, + }, +} + +#[derive(Debug, Clone)] +pub(super) struct ResolveContext { + context: Context, + fallback: Option, + autoplay: bool, + /// if `true` updates the entire context, otherwise only fills the context from the next + /// retrieve page, it is usually used when loading the next page of an already established context + /// + /// like for example: + /// - playing an artists profile + update: bool, +} + +impl ResolveContext { + pub fn from_uri(uri: impl Into, fallback: impl Into, autoplay: bool) -> Self { + let fallback_uri = fallback.into(); + Self { + context: Context { + uri: uri.into(), + ..Default::default() + }, + fallback: (!fallback_uri.is_empty()).then_some(fallback_uri), + autoplay, + update: true, + } + } + + pub fn from_context(context: Context, autoplay: bool) -> Self { + Self { + context, + fallback: None, + autoplay, + update: true, + } + } + + // expected page_url: hm://artistplaycontext/v1/page/spotify/album/5LFzwirfFwBKXJQGfwmiMY/km_artist + pub fn from_page_url(page_url: String) -> Self { + let split = if let Some(rest) = page_url.strip_prefix("hm://") { + rest.split('/') + } else { + warn!("page_url didn't started with hm://. got page_url: {page_url}"); + page_url.split('/') + }; + + let uri = split + .skip_while(|s| s != &"spotify") + .take(3) + .collect::>() + .join(":"); + + trace!("created an ResolveContext from page_url <{page_url}> as uri <{uri}>"); + + Self { + context: Context { + uri, + ..Default::default() + }, + fallback: None, + update: false, + autoplay: false, + } + } + + /// the uri which should be used to resolve the context, might not be the context uri + pub fn resolve_uri(&self) -> Option<&String> { + // it's important to call this always, or at least for every ResolveContext + // otherwise we might not even check if we need to fallback and just use the fallback uri + ConnectState::get_context_uri_from_context(&self.context) + .and_then(|s| (!s.is_empty()).then_some(s)) + .or(self.fallback.as_ref()) + } + + /// the actual context uri + pub fn context_uri(&self) -> &str { + &self.context.uri + } + + pub fn autoplay(&self) -> bool { + self.autoplay + } + + pub fn update(&self) -> bool { + self.update + } +} + +impl Display for ResolveContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "resolve_uri: <{:?}>, context_uri: <{}>, autoplay: <{}>, update: <{}>", + self.resolve_uri(), + self.context.uri, + self.autoplay, + self.update + ) + } +} + +impl PartialEq for ResolveContext { + fn eq(&self, other: &Self) -> bool { + let eq_context = self.context_uri() == other.context_uri(); + let eq_resolve = self.resolve_uri() == other.resolve_uri(); + let eq_autoplay = self.autoplay == other.autoplay; + let eq_update = self.update == other.update; + + eq_context && eq_resolve && eq_autoplay && eq_update + } +} + +impl Eq for ResolveContext {} + +impl Hash for ResolveContext { + fn hash(&self, state: &mut H) { + self.context_uri().hash(state); + self.resolve_uri().hash(state); + self.autoplay.hash(state); + self.update.hash(state); + } +} + +impl From for Context { + fn from(value: ResolveContext) -> Self { + value.context + } +} diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index c3942651..b9240851 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,106 +1,115 @@ -use std::{ - future::Future, - pin::Pin, - sync::atomic::{AtomicUsize, Ordering}, - sync::Arc, - time::{SystemTime, UNIX_EPOCH}, -}; - -use futures_util::{stream::FusedStream, FutureExt, StreamExt}; - -use protobuf::Message; -use rand::prelude::SliceRandom; -use thiserror::Error; -use tokio::sync::mpsc; -use tokio_stream::wrappers::UnboundedReceiverStream; - +pub use crate::model::{PlayingTrack, SpircLoadCommand}; +use crate::state::{context::ResetContext, metadata::Metadata}; use crate::{ - config::ConnectConfig, - context::PageContext, core::{ - authentication::Credentials, mercury::MercurySender, session::UserAttributes, - util::SeqGenerator, version, Error, Session, SpotifyId, + authentication::Credentials, + dealer::{ + manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply}, + protocol::{Command, Message, Request}, + }, + session::UserAttributes, + Error, Session, SpotifyId, }, playback::{ mixer::Mixer, player::{Player, PlayerEvent, PlayerEventChannel}, }, protocol::{ - self, + autoplay_context_request::AutoplayContextRequest, + connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand}, explicit_content_pubsub::UserAttributesUpdate, - spirc::{DeviceState, Frame, MessageType, PlayStatus, State, TrackRef}, + player::{Context, TransferState}, + playlist4_external::PlaylistModificationInfo, + social_connect_v2::{session::_host_active_device_id, SessionUpdate}, user_attributes::UserAttributesMutation, }, }; +use crate::{ + model::{ResolveContext, SpircPlayStatus}, + state::{ + context::{ContextType, LoadNext, UpdateContext}, + provider::IsProvider, + {ConnectState, ConnectStateConfig}, + }, +}; +use futures_util::StreamExt; +use protobuf::MessageField; +use std::collections::HashMap; +use std::time::Instant; +use std::{ + future::Future, + sync::atomic::{AtomicUsize, Ordering}, + sync::Arc, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use thiserror::Error; +use tokio::{sync::mpsc, time::sleep}; #[derive(Debug, Error)] pub enum SpircError { #[error("response payload empty")] NoData, - #[error("playback of local files is not supported")] - UnsupportedLocalPlayBack, - #[error("message addressed at another ident: {0}")] - Ident(String), #[error("message pushed for another URI")] InvalidUri(String), + #[error("tried resolving not allowed context: {0:?}")] + NotAllowedContext(String), + #[error("failed to put connect state for new device")] + FailedDealerSetup, + #[error("unknown endpoint: {0:#?}")] + UnknownEndpoint(serde_json::Value), } impl From for Error { fn from(err: SpircError) -> Self { use SpircError::*; match err { - NoData | UnsupportedLocalPlayBack => Error::unavailable(err), - Ident(_) | InvalidUri(_) => Error::aborted(err), + NoData | NotAllowedContext(_) => Error::unavailable(err), + InvalidUri(_) | FailedDealerSetup => Error::aborted(err), + UnknownEndpoint(_) => Error::unimplemented(err), } } } -#[derive(Debug)] -enum SpircPlayStatus { - Stopped, - LoadingPlay { - position_ms: u32, - }, - LoadingPause { - position_ms: u32, - }, - Playing { - nominal_start_time: i64, - preloading_of_next_track_triggered: bool, - }, - Paused { - position_ms: u32, - preloading_of_next_track_triggered: bool, - }, -} - -type BoxedStream = Pin + Send>>; - struct SpircTask { player: Arc, mixer: Arc, - sequence: SeqGenerator, + /// the state management object + connect_state: ConnectState, - ident: String, - device: DeviceState, - state: State, play_request_id: Option, play_status: SpircPlayStatus, - remote_update: BoxedStream>, - connection_id_update: BoxedStream>, - user_attributes_update: BoxedStream>, - user_attributes_mutation: BoxedStream>, - sender: MercurySender, + connection_id_update: BoxedStreamResult, + connect_state_update: BoxedStreamResult, + connect_state_volume_update: BoxedStreamResult, + connect_state_logout_request: BoxedStreamResult, + playlist_update: BoxedStreamResult, + session_update: BoxedStreamResult, + connect_state_command: BoxedStream, + user_attributes_update: BoxedStreamResult, + user_attributes_mutation: BoxedStreamResult, + commands: Option>, player_events: Option, shutdown: bool, session: Session, - resolve_context: Option, - autoplay_context: bool, - context: Option, + + /// the list of contexts to resolve + resolve_context: Vec, + + /// contexts may not be resolvable at the moment so we should ignore any further request + /// + /// an unavailable context is retried after [RETRY_UNAVAILABLE] + unavailable_contexts: HashMap, + + /// is set when transferring, and used after resolving the contexts to finish the transfer + pub transfer_state: Option, + + /// when set to true, it will update the volume after [VOLUME_UPDATE_DELAY], + /// when no other future resolves, otherwise resets the delay + update_volume: bool, spirc_id: usize, } @@ -108,7 +117,7 @@ struct SpircTask { static SPIRC_COUNTER: AtomicUsize = AtomicUsize::new(0); #[derive(Debug)] -pub enum SpircCommand { +enum SpircCommand { Play, PlayPause, Pause, @@ -119,6 +128,7 @@ pub enum SpircCommand { Shutdown, Shuffle(bool), Repeat(bool), + RepeatTrack(bool), Disconnect, SetPosition(u32), SetVolume(u16), @@ -126,216 +136,78 @@ pub enum SpircCommand { Load(SpircLoadCommand), } -#[derive(Debug)] -pub struct SpircLoadCommand { - pub context_uri: String, - /// Whether the given tracks should immediately start playing, or just be initially loaded. - pub start_playing: bool, - pub shuffle: bool, - pub repeat: bool, - pub playing_track_index: u32, - pub tracks: Vec, -} +const CONTEXT_FETCH_THRESHOLD: usize = 2; -impl From for State { - fn from(command: SpircLoadCommand) -> Self { - let mut state = State::new(); - state.set_context_uri(command.context_uri); - state.set_status(if command.start_playing { - PlayStatus::kPlayStatusPlay - } else { - PlayStatus::kPlayStatusStop - }); - state.set_shuffle(command.shuffle); - state.set_repeat(command.repeat); - state.set_playing_track_index(command.playing_track_index); - state.track = command.tracks; - state - } -} - -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 +// delay to resolve a bundle of context updates, delaying the update prevents duplicate context updates of the same type +const RESOLVE_CONTEXT_DELAY: Duration = Duration::from_millis(500); +// time after which an unavailable context is retried +const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600); +// delay to update volume after a certain amount of time, instead on each update request +const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2); + pub struct Spirc { commands: mpsc::UnboundedSender, } -fn initial_state() -> State { - let mut frame = protocol::spirc::State::new(); - frame.set_repeat(false); - frame.set_shuffle(false); - frame.set_status(PlayStatus::kPlayStatusStop); - frame.set_position_ms(0); - frame.set_position_measured_at(0); - frame -} - -fn int_capability(typ: protocol::spirc::CapabilityType, val: i64) -> protocol::spirc::Capability { - let mut cap = protocol::spirc::Capability::new(); - cap.set_typ(typ); - cap.intValue.push(val); - cap -} - -fn initial_device_state(config: ConnectConfig) -> DeviceState { - let mut msg = DeviceState::new(); - msg.set_sw_version(version::SEMVER.to_string()); - msg.set_is_active(false); - msg.set_can_play(true); - msg.set_volume(0); - msg.set_name(config.name); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kCanBePlayer, - 1, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kDeviceType, - config.device_type as i64, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kGaiaEqConnectId, - 1, - )); - // TODO: implement logout - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kSupportsLogout, - 0, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kIsObservable, - 1, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kVolumeSteps, - if config.has_volume_ctrl { - VOLUME_STEPS - } else { - 0 - }, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kSupportsPlaylistV2, - 1, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kSupportsExternalEpisodes, - 1, - )); - // TODO: how would such a rename command be triggered? Handle it. - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kSupportsRename, - 1, - )); - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kCommandAcks, - 0, - )); - // TODO: does this mean local files or the local network? - // LAN may be an interesting privacy toggle. - msg.capabilities.push(int_capability( - protocol::spirc::CapabilityType::kRestrictToLocal, - 0, - )); - // TODO: what does this hide, or who do we hide from? - // May be an interesting privacy toggle. - msg.capabilities - .push(int_capability(protocol::spirc::CapabilityType::kHidden, 0)); - let mut supported_types = protocol::spirc::Capability::new(); - supported_types.set_typ(protocol::spirc::CapabilityType::kSupportedTypes); - supported_types - .stringValue - .push("audio/episode".to_string()); - supported_types - .stringValue - .push("audio/episode+track".to_string()); - supported_types.stringValue.push("audio/track".to_string()); - // other known types: - // - "audio/ad" - // - "audio/interruption" - // - "audio/local" - // - "video/ad" - // - "video/episode" - msg.capabilities.push(supported_types); - msg -} - -fn url_encode(bytes: impl AsRef<[u8]>) -> String { - form_urlencoded::byte_serialize(bytes.as_ref()).collect() -} - impl Spirc { pub async fn new( - config: ConnectConfig, + config: ConnectStateConfig, session: Session, credentials: Credentials, player: Arc, mixer: Arc, ) -> Result<(Spirc, impl Future), Error> { + fn extract_connection_id(msg: Message) -> Result { + let connection_id = msg + .headers + .get("Spotify-Connection-Id") + .ok_or_else(|| SpircError::InvalidUri(msg.uri.clone()))?; + Ok(connection_id.to_owned()) + } + let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel); debug!("new Spirc[{}]", spirc_id); - let ident = session.device_id().to_owned(); + let connect_state = ConnectState::new(config, &session); - let remote_update = Box::pin( - session - .mercury() - .listen_for("hm://remote/user/") - .map(UnboundedReceiverStream::new) - .flatten_stream() - .map(|response| -> Result<(String, Frame), Error> { - let uri_split: Vec<&str> = response.uri.split('/').collect(); - let username = match uri_split.get(4) { - Some(s) => s.to_string(), - None => String::new(), - }; + let connection_id_update = session + .dealer() + .listen_for("hm://pusher/v1/connections/", extract_connection_id)?; - let data = response.payload.first().ok_or(SpircError::NoData)?; - Ok((username, Frame::parse_from_bytes(data)?)) - }), - ); + let connect_state_update = session + .dealer() + .listen_for("hm://connect-state/v1/cluster", Message::from_raw)?; - let connection_id_update = Box::pin( - session - .mercury() - .listen_for("hm://pusher/v1/connections/") - .map(UnboundedReceiverStream::new) - .flatten_stream() - .map(|response| -> Result { - let connection_id = response - .uri - .strip_prefix("hm://pusher/v1/connections/") - .ok_or_else(|| SpircError::InvalidUri(response.uri.clone()))?; - Ok(connection_id.to_owned()) - }), - ); + let connect_state_volume_update = session + .dealer() + .listen_for("hm://connect-state/v1/connect/volume", Message::from_raw)?; - let user_attributes_update = Box::pin( - session - .mercury() - .listen_for("spotify:user:attributes:update") - .map(UnboundedReceiverStream::new) - .flatten_stream() - .map(|response| -> Result { - let data = response.payload.first().ok_or(SpircError::NoData)?; - Ok(UserAttributesUpdate::parse_from_bytes(data)?) - }), - ); + let connect_state_logout_request = session + .dealer() + .listen_for("hm://connect-state/v1/connect/logout", Message::from_raw)?; - let user_attributes_mutation = Box::pin( - session - .mercury() - .listen_for("spotify:user:attributes:mutated") - .map(UnboundedReceiverStream::new) - .flatten_stream() - .map(|response| -> Result { - let data = response.payload.first().ok_or(SpircError::NoData)?; - Ok(UserAttributesMutation::parse_from_bytes(data)?) - }), - ); + let playlist_update = session + .dealer() + .listen_for("hm://playlist/v2/playlist/", Message::from_raw)?; + + let session_update = session + .dealer() + .listen_for("social-connect/v2/session_update", Message::from_json)?; + + let user_attributes_update = session + .dealer() + .listen_for("spotify:user:attributes:update", Message::from_raw)?; + + // can be trigger by toggling autoplay in a desktop client + let user_attributes_mutation = session + .dealer() + .listen_for("spotify:user:attributes:mutated", Message::from_raw)?; + + let connect_state_command = session + .dealer() + .handle_for("hm://connect-state/v1/player/command")?; // pre-acquire client_token, preventing multiple request while running let _ = session.spclient().client_token().await?; @@ -343,61 +215,59 @@ impl Spirc { // Connect *after* all message listeners are registered session.connect(credentials, true).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); + // pre-acquire access_token (we need to be authenticated to retrieve a token) + let _ = session.login5().auth_token().await?; let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); - let initial_volume = config.initial_volume; - - let device = initial_device_state(config); - let player_events = player.get_player_event_channel(); let mut task = SpircTask { player, mixer, - sequence: SeqGenerator::new(1), + connect_state, - ident, - - device, - state: initial_state(), play_request_id: None, play_status: SpircPlayStatus::Stopped, - remote_update, connection_id_update, + connect_state_update, + connect_state_volume_update, + connect_state_logout_request, + playlist_update, + session_update, + connect_state_command, user_attributes_update, user_attributes_mutation, - sender, commands: Some(cmd_rx), player_events: Some(player_events), shutdown: false, session, - resolve_context: None, - autoplay_context: false, - context: None, + resolve_context: Vec::new(), + unavailable_contexts: HashMap::new(), + transfer_state: None, + update_volume: false, spirc_id, }; - 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 }; - task.hello()?; + let initial_volume = task.connect_state.device_info().volume; + task.connect_state.set_volume(0); + + match initial_volume.try_into() { + Ok(volume) => { + task.set_volume(volume); + // we don't want to update the volume initially, + // we just want to set the mixer to the correct volume + task.update_volume = false; + } + Err(why) => error!("failed to update initial volume: {why}"), + }; Ok((spirc, task.run())) } @@ -432,6 +302,9 @@ impl Spirc { pub fn repeat(&self, repeat: bool) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Repeat(repeat))?) } + pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::RepeatTrack(repeat))?) + } pub fn set_volume(&self, volume: u16) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::SetVolume(volume))?) } @@ -451,217 +324,360 @@ impl Spirc { impl SpircTask { async fn run(mut self) { + // simplify unwrapping of received item or parsed result + macro_rules! unwrap { + ( $next:expr, |$some:ident| $use_some:expr ) => { + match $next { + Some($some) => $use_some, + None => { + error!("{} selected, but none received", stringify!($next)); + break; + } + } + }; + ( $next:expr, match |$ok:ident| $use_ok:expr ) => { + unwrap!($next, |$ok| match $ok { + Ok($ok) => $use_ok, + Err(why) => error!("could not parse {}: {}", stringify!($ok), why), + }) + }; + } + + if let Err(why) = self.session.dealer().start().await { + error!("starting dealer failed: {why}"); + return; + } + while !self.session.is_invalid() && !self.shutdown { let commands = self.commands.as_mut(); let player_events = self.player_events.as_mut(); - tokio::select! { - remote_update = self.remote_update.next() => match remote_update { - Some(result) => match result { - Ok((username, frame)) => { - if username != self.session.username() { - warn!("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!("remote update selected, but none received"); - break; - } - }, - user_attributes_update = self.user_attributes_update.next() => match user_attributes_update { - 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(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(result) => match result { - Ok(connection_id) => { - self.handle_connection_id_update(connection_id); - // pre-acquire access_token, preventing multiple request while running - // pre-acquiring for the access_token will only last for one hour - // - // we need to fire the request after connecting, but can't do it right - // after, because by that we would miss certain packages, like this one - match self.session.login5().auth_token().await { - Ok(_) => debug!("successfully pre-acquire access_token and client_token"), - Err(why) => { - error!("{why}"); - break - } - } - }, - Err(e) => error!("could not parse connection ID update: {}", e), - } - None => { - error!("connection ID update selected, but none received"); + tokio::select! { + // startup of the dealer requires a connection_id, which is retrieved at the very beginning + connection_id_update = self.connection_id_update.next() => unwrap! { + connection_id_update, + match |connection_id| if let Err(why) = self.handle_connection_id_update(connection_id).await { + error!("failed handling connection id update: {why}"); break; } }, + // main dealer update of any remote device updates + cluster_update = self.connect_state_update.next() => unwrap! { + cluster_update, + match |cluster_update| if let Err(e) = self.handle_cluster_update(cluster_update).await { + error!("could not dispatch connect state update: {}", e); + } + }, + // main dealer request handling (dealer expects an answer) + request = self.connect_state_command.next() => unwrap! { + request, + |request| if let Err(e) = self.handle_connect_state_request(request).await { + error!("couldn't handle connect state command: {}", e); + } + }, + // volume request handling is send separately (it's more like a fire forget) + volume_update = self.connect_state_volume_update.next() => unwrap! { + volume_update, + match |volume_update| match volume_update.volume.try_into() { + Ok(volume) => self.set_volume(volume), + Err(why) => error!("can't update volume, failed to parse i32 to u16: {why}") + } + }, + logout_request = self.connect_state_logout_request.next() => unwrap! { + logout_request, + |logout_request| { + error!("received logout request, currently not supported: {logout_request:#?}"); + // todo: call logout handling + } + }, + playlist_update = self.playlist_update.next() => unwrap! { + playlist_update, + match |playlist_update| if let Err(why) = self.handle_playlist_modification(playlist_update) { + error!("failed to handle playlist modification: {why}") + } + }, + user_attributes_update = self.user_attributes_update.next() => unwrap! { + user_attributes_update, + match |attributes| self.handle_user_attributes_update(attributes) + }, + user_attributes_mutation = self.user_attributes_mutation.next() => unwrap! { + user_attributes_mutation, + match |attributes| self.handle_user_attributes_mutation(attributes) + }, + session_update = self.session_update.next() => unwrap! { + session_update, + match |session_update| self.handle_session_update(session_update) + }, cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd { - if let Err(e) = self.handle_command(cmd) { + if let Err(e) = self.handle_command(cmd).await { debug!("could not dispatch command: {}", e); } }, 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) { + if let Err(e) = self.handle_player_event(event).await { 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."); - break; + _ = async { sleep(RESOLVE_CONTEXT_DELAY).await }, if !self.resolve_context.is_empty() => { + if let Err(why) = self.handle_resolve_context().await { + error!("ContextError: {why}") + } }, - context_uri = async { self.resolve_context.take() }, if self.resolve_context.is_some() => { - let context_uri = context_uri.unwrap(); // guaranteed above - if context_uri.contains("spotify:show:") || context_uri.contains("spotify:episode:") { - continue; // not supported by apollo stations + _ = async { sleep(VOLUME_UPDATE_DELAY).await }, if self.update_volume => { + self.update_volume = false; + + info!("delayed volume update for all devices: volume is now {}", self.connect_state.device_info().volume); + if let Err(why) = self.connect_state.notify_volume_changed(&self.session).await { + error!("error updating connect state for volume update: {why}") } - let context = if context_uri.starts_with("hm://") { - self.session.spclient().get_next_page(&context_uri).await - } else { - // only send previous tracks that were before the current playback position - let current_position = self.state.playing_track_index() as usize; - let previous_tracks = self.state.track[..current_position].iter().filter_map(|t| SpotifyId::try_from(t).ok()).collect(); - - let scope = if self.autoplay_context { - "stations" // this returns a `StationContext` but we deserialize it into a `PageContext` - } else { - "tracks" // this returns a `PageContext` - }; - - self.session.spclient().get_apollo_station(scope, &context_uri, None, previous_tracks, self.autoplay_context).await - }; - - match context { - Ok(value) => { - self.context = match serde_json::from_slice::(&value) { - Ok(context) => { - info!( - "Resolved {:?} tracks from <{:?}>", - context.tracks.len(), - self.state.context_uri(), - ); - Some(context) - } - Err(e) => { - error!("Unable to parse JSONContext {:?}", e); - None - } - }; - }, - Err(err) => { - error!("ContextError: {:?}", err) - } + // for some reason the web-player does need two separate updates, so that the + // position of the current track is retained, other clients also send a state + // update before they send the volume update + if let Err(why) = self.notify().await { + error!("error updating connect state for volume update: {why}") } }, else => break } } - if self.sender.flush().await.is_err() { - warn!("Cannot flush spirc event sender when done."); + if !self.shutdown && self.connect_state.is_active() { + if let Err(why) = self.notify().await { + warn!("notify before unexpected shutdown couldn't be send: {why}") + } + } + + // clears the session id, leaving an empty state + if let Err(why) = self.session.spclient().delete_connect_state_request().await { + warn!("deleting connect_state failed before unexpected shutdown: {why}") + } + self.session.dealer().close().await; + } + + async fn handle_resolve_context(&mut self) -> Result<(), Error> { + let mut last_resolve = None::; + while let Some(resolve) = self.resolve_context.pop() { + if matches!(last_resolve, Some(ref last_resolve) if last_resolve == &resolve) { + debug!("did already update the context for {resolve}"); + continue; + } else { + last_resolve = Some(resolve.clone()); + + let resolve_uri = match resolve.resolve_uri() { + Some(resolve) => resolve, + None => { + warn!("tried to resolve context without resolve_uri: {resolve}"); + return Ok(()); + } + }; + + debug!("resolving: {resolve}"); + // the autoplay endpoint can return a 404, when it tries to retrieve an + // autoplay context for an empty playlist as it seems + if let Err(why) = self + .resolve_context( + resolve_uri, + resolve.context_uri(), + resolve.autoplay(), + resolve.update(), + ) + .await + { + error!("failed resolving context <{resolve}>: {why}"); + self.unavailable_contexts.insert(resolve, Instant::now()); + continue; + } + + self.connect_state.merge_context(Some(resolve.into())); + } + } + + if let Some(transfer_state) = self.transfer_state.take() { + self.connect_state.finish_transfer(transfer_state)? + } + + if matches!(self.connect_state.active_context, ContextType::Default) { + let ctx = self.connect_state.context.as_ref(); + if matches!(ctx, Some(ctx) if ctx.tracks.is_empty()) { + self.connect_state.clear_next_tracks(true); + self.handle_next(None)?; + } + } + + self.connect_state.fill_up_next_tracks()?; + self.connect_state.update_restrictions(); + self.connect_state.update_queue_revision(); + + self.preload_autoplay_when_required(); + + self.notify().await + } + + async fn resolve_context( + &mut self, + resolve_uri: &str, + context_uri: &str, + autoplay: bool, + update: bool, + ) -> Result<(), Error> { + if !autoplay { + let mut ctx = self.session.spclient().get_context(resolve_uri).await?; + + if update { + ctx.uri = context_uri.to_string(); + ctx.url = format!("context://{context_uri}"); + + self.connect_state + .update_context(ctx, UpdateContext::Default)? + } else if matches!(ctx.pages.first(), Some(p) if !p.tracks.is_empty()) { + debug!( + "update context from single page, context {} had {} pages", + ctx.uri, + ctx.pages.len() + ); + self.connect_state + .fill_context_from_page(ctx.pages.remove(0))?; + } else { + error!("resolving context should only update the tracks, but had no page, or track. {ctx:#?}"); + }; + + if let Err(why) = self.notify().await { + error!("failed to update connect state, after updating the context: {why}") + } + + return Ok(()); + } + + if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") { + // autoplay is not supported for podcasts + Err(SpircError::NotAllowedContext(resolve_uri.to_string()))? + } + + let previous_tracks = self.connect_state.prev_autoplay_track_uris(); + + debug!( + "requesting autoplay context <{resolve_uri}> with {} previous tracks", + previous_tracks.len() + ); + + let ctx_request = AutoplayContextRequest { + context_uri: Some(resolve_uri.to_string()), + recent_track_uri: previous_tracks, + ..Default::default() + }; + + let context = self + .session + .spclient() + .get_autoplay_context(&ctx_request) + .await?; + + self.connect_state + .update_context(context, UpdateContext::Autoplay) + } + + fn add_resolve_context(&mut self, resolve: ResolveContext) { + let last_try = self + .unavailable_contexts + .get(&resolve) + .map(|i| i.duration_since(Instant::now())); + + let last_try = if matches!(last_try, Some(last_try) if last_try > RETRY_UNAVAILABLE) { + let _ = self.unavailable_contexts.remove(&resolve); + debug!( + "context was requested {}s ago, trying again to resolve the requested context", + last_try.expect("checked by condition").as_secs() + ); + None + } else { + last_try + }; + + if last_try.is_none() { + debug!("add resolve request: {resolve}"); + self.resolve_context.push(resolve); + } else { + debug!("tried loading unavailable context: {resolve}") } } - fn now_ms(&mut self) -> i64 { - let dur = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(dur) => dur, - Err(err) => err.duration(), - }; + // todo: time_delta still necessary? + fn now_ms(&self) -> i64 { + let dur = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_else(|err| err.duration()); dur.as_millis() as i64 + 1000 * self.session.time_delta() } - fn update_state_position(&mut self, position_ms: u32) { - let now = self.now_ms(); - self.state.set_position_measured_at(now as u64); - self.state.set_position_ms(position_ms); - } - - fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { + async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { if matches!(cmd, SpircCommand::Shutdown) { trace!("Received SpircCommand::Shutdown"); - CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?; - self.handle_disconnect(); + self.handle_disconnect().await?; self.shutdown = true; if let Some(rx) = self.commands.as_mut() { rx.close() } Ok(()) - } else if self.device.is_active() { + } else if self.connect_state.is_active() { trace!("Received SpircCommand::{:?}", cmd); match cmd { SpircCommand::Play => { self.handle_play(); - self.notify(None) + self.notify().await } SpircCommand::PlayPause => { self.handle_play_pause(); - self.notify(None) + self.notify().await } SpircCommand::Pause => { self.handle_pause(); - self.notify(None) + self.notify().await } SpircCommand::Prev => { - self.handle_prev(); - self.notify(None) + self.handle_prev()?; + self.notify().await } SpircCommand::Next => { - self.handle_next(); - self.notify(None) + self.handle_next(None)?; + self.notify().await } SpircCommand::VolumeUp => { self.handle_volume_up(); - self.notify(None) + self.notify().await } SpircCommand::VolumeDown => { self.handle_volume_down(); - self.notify(None) + self.notify().await } SpircCommand::Disconnect => { - self.handle_disconnect(); - self.notify(None) + self.handle_disconnect().await?; + self.notify().await } SpircCommand::Shuffle(shuffle) => { - self.state.set_shuffle(shuffle); - self.notify(None) + self.connect_state.handle_shuffle(shuffle)?; + self.notify().await } SpircCommand::Repeat(repeat) => { - self.state.set_repeat(repeat); - self.notify(None) + self.connect_state.set_repeat_context(repeat); + self.notify().await + } + SpircCommand::RepeatTrack(repeat) => { + self.connect_state.set_repeat_track(repeat); + self.notify().await } SpircCommand::SetPosition(position) => { self.handle_seek(position); - self.notify(None) + self.notify().await } SpircCommand::SetVolume(volume) => { self.set_volume(volume); - self.notify(None) + self.notify().await } SpircCommand::Load(command) => { - self.handle_load(&command.into())?; - self.notify(None) + self.handle_load(command, None).await?; + self.notify().await } _ => Ok(()), } @@ -670,7 +686,7 @@ impl SpircTask { SpircCommand::Activate => { trace!("Received SpircCommand::{:?}", cmd); self.handle_activate(); - self.notify(None) + self.notify().await } _ => { warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); @@ -680,7 +696,12 @@ impl SpircTask { } } - fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { + async fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { + if let PlayerEvent::TrackChanged { audio_item } = event { + self.connect_state.update_duration(audio_item.duration_ms); + return Ok(()); + } + // update play_request_id if let PlayerEvent::PlayRequestIdChanged { play_request_id } = event { self.play_request_id = Some(play_request_id); @@ -693,26 +714,25 @@ impl SpircTask { if let Some(play_request_id) = event.get_play_request_id() { if Some(play_request_id) == self.play_request_id { match event { - PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track(), + PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track().await, PlayerEvent::Loading { .. } => { match self.play_status { SpircPlayStatus::LoadingPlay { position_ms } => { - self.update_state_position(position_ms); - self.state.set_status(PlayStatus::kPlayStatusPlay); + self.connect_state + .update_position(position_ms, self.now_ms()); trace!("==> kPlayStatusPlay"); } SpircPlayStatus::LoadingPause { position_ms } => { - self.update_state_position(position_ms); - self.state.set_status(PlayStatus::kPlayStatusPause); + self.connect_state + .update_position(position_ms, self.now_ms()); trace!("==> kPlayStatusPause"); } _ => { - self.state.set_status(PlayStatus::kPlayStatusLoading); - self.update_state_position(0); + self.connect_state.update_position(0, self.now_ms()); trace!("==> kPlayStatusLoading"); } } - self.notify(None) + self.notify().await } PlayerEvent::Playing { position_ms, .. } | PlayerEvent::PositionCorrection { position_ms, .. } @@ -726,21 +746,22 @@ 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) + self.connect_state + .update_position(position_ms, self.now_ms()); + self.notify().await } else { Ok(()) } } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { - self.state.set_status(PlayStatus::kPlayStatusPlay); - self.update_state_position(position_ms); + self.connect_state + .update_position(position_ms, self.now_ms()); self.play_status = SpircPlayStatus::Playing { nominal_start_time: new_nominal_start_time, preloading_of_next_track_triggered: false, }; - self.notify(None) + self.notify().await } _ => Ok(()), } @@ -752,23 +773,23 @@ impl SpircTask { trace!("==> kPlayStatusPause"); match self.play_status { SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => { - self.state.set_status(PlayStatus::kPlayStatusPause); - self.update_state_position(new_position_ms); + self.connect_state + .update_position(new_position_ms, self.now_ms()); self.play_status = SpircPlayStatus::Paused { position_ms: new_position_ms, preloading_of_next_track_triggered: false, }; - self.notify(None) + self.notify().await } SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { - self.state.set_status(PlayStatus::kPlayStatusPause); - self.update_state_position(new_position_ms); + self.connect_state + .update_position(new_position_ms, self.now_ms()); self.play_status = SpircPlayStatus::Paused { position_ms: new_position_ms, preloading_of_next_track_triggered: false, }; - self.notify(None) + self.notify().await } _ => Ok(()), } @@ -778,9 +799,8 @@ impl SpircTask { match self.play_status { SpircPlayStatus::Stopped => Ok(()), _ => { - self.state.set_status(PlayStatus::kPlayStatusStop); self.play_status = SpircPlayStatus::Stopped; - self.notify(None) + self.notify().await } } } @@ -789,8 +809,11 @@ impl SpircTask { Ok(()) } PlayerEvent::Unavailable { track_id, .. } => { - self.handle_unavailable(track_id); - Ok(()) + self.handle_unavailable(track_id)?; + if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? { + self.handle_next(None)?; + } + self.notify().await } _ => Ok(()), } @@ -802,9 +825,57 @@ impl SpircTask { } } - fn handle_connection_id_update(&mut self, connection_id: String) { + async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> { trace!("Received connection ID update: {:?}", connection_id); self.session.set_connection_id(&connection_id); + + let cluster = match self + .connect_state + .notify_new_device_appeared(&self.session) + .await + { + Ok(res) => Cluster::parse_from_bytes(&res).ok(), + Err(why) => { + error!("{why:?}"); + None + } + } + .ok_or(SpircError::FailedDealerSetup)?; + + debug!( + "successfully put connect state for {} with connection-id {connection_id}", + self.session.device_id() + ); + + let same_session = cluster.player_state.session_id == self.session.session_id() + || cluster.player_state.session_id.is_empty(); + if !cluster.active_device_id.is_empty() || !same_session { + info!( + "active device is <{}> with session <{}>", + cluster.active_device_id, cluster.player_state.session_id + ); + return Ok(()); + } else if cluster.transfer_data.is_empty() { + debug!("got empty transfer state, do nothing"); + return Ok(()); + } else { + info!( + "trying to take over control automatically, session_id: {}", + cluster.player_state.session_id + ) + } + + use protobuf::Message; + + // todo: handle received pages from transfer, important to not always shuffle the first 10 tracks + // also important when the dealer is restarted, currently we just shuffle again, but at least + // the 10 tracks provided should be used and after that the new shuffle context + match TransferState::parse_from_bytes(&cluster.transfer_data) { + Ok(transfer_state) => self.handle_transfer(transfer_state)?, + Err(why) => error!("failed to take over control: {why}"), + } + + Ok(()) } fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) { @@ -849,6 +920,8 @@ impl SpircTask { if key == "autoplay" && old_value != new_value { self.player .emit_auto_play_changed_event(matches!(new_value, "1")); + + self.preload_autoplay_when_required() } } else { trace!( @@ -859,185 +932,243 @@ impl SpircTask { } } - fn handle_remote_update(&mut self, update: Frame) -> Result<(), Error> { - trace!("Received update frame: {:#?}", update); + async fn handle_cluster_update( + &mut self, + mut cluster_update: ClusterUpdate, + ) -> Result<(), Error> { + let reason = cluster_update.update_reason.enum_value(); - // First see if this update was intended for us. - let device_id = &self.ident; - let ident = update.ident(); - if ident == device_id - || (!update.recipient.is_empty() && !update.recipient.contains(device_id)) - { - return Err(SpircError::Ident(ident.to_string()).into()); + let device_ids = cluster_update.devices_that_changed.join(", "); + debug!( + "cluster update: {reason:?} from {device_ids}, active device: {}", + cluster_update.cluster.active_device_id + ); + + if let Some(cluster) = cluster_update.cluster.take() { + let became_inactive = self.connect_state.is_active() + && cluster.active_device_id != self.session.device_id(); + if became_inactive { + info!("device became inactive"); + self.connect_state.became_inactive(&self.session).await?; + self.handle_stop() + } else if self.connect_state.is_active() { + // fixme: workaround fix, because of missing information why it behaves like it does + // background: when another device sends a connect-state update, some player's position de-syncs + // tried: providing session_id, playback_id, track-metadata "track_player" + self.notify().await?; + } + } else if self.connect_state.is_active() { + self.connect_state.became_inactive(&self.session).await?; } - let old_client_id = self.session.client_id(); - - for entry in update.device_state.metadata.iter() { - match entry.type_() { - "client_id" => self.session.set_client_id(entry.metadata()), - "brand_display_name" => self.session.set_client_brand_name(entry.metadata()), - "model_display_name" => self.session.set_client_model_name(entry.metadata()), - _ => (), - } - } - - self.session.set_client_name(update.device_state.name()); - - let new_client_id = self.session.client_id(); - - if self.device.is_active() && new_client_id != old_client_id { - self.player.emit_session_client_changed_event( - new_client_id, - self.session.client_name(), - self.session.client_brand_name(), - self.session.client_model_name(), - ); - } - - match update.typ() { - MessageType::kMessageTypeHello => self.notify(Some(ident)), - - MessageType::kMessageTypeLoad => { - self.handle_load(update.state.get_or_default())?; - self.notify(None) - } - - MessageType::kMessageTypePlay => { - self.handle_play(); - self.notify(None) - } - - MessageType::kMessageTypePlayPause => { - self.handle_play_pause(); - self.notify(None) - } - - MessageType::kMessageTypePause => { - self.handle_pause(); - self.notify(None) - } - - MessageType::kMessageTypeNext => { - self.handle_next(); - self.notify(None) - } - - MessageType::kMessageTypePrev => { - self.handle_prev(); - self.notify(None) - } - - MessageType::kMessageTypeVolumeUp => { - self.handle_volume_up(); - self.notify(None) - } - - MessageType::kMessageTypeVolumeDown => { - self.handle_volume_down(); - self.notify(None) - } - - MessageType::kMessageTypeRepeat => { - let repeat = update.state.repeat(); - self.state.set_repeat(repeat); - - self.player.emit_repeat_changed_event(repeat); - - self.notify(None) - } - - MessageType::kMessageTypeShuffle => { - let shuffle = update.state.shuffle(); - self.state.set_shuffle(shuffle); - if shuffle { - let current_index = self.state.playing_track_index(); - let tracks = &mut self.state.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.player.emit_shuffle_changed_event(shuffle); - - self.notify(None) - } - - MessageType::kMessageTypeSeek => { - self.handle_seek(update.position()); - self.notify(None) - } - - MessageType::kMessageTypeReplace => { - let context_uri = update.state.context_uri().to_owned(); - - // completely ignore local playback. - if context_uri.starts_with("spotify:local-files") { - self.notify(None)?; - return Err(SpircError::UnsupportedLocalPlayBack.into()); - } - - self.update_tracks(update.state.get_or_default()); - - if let SpircPlayStatus::Playing { - preloading_of_next_track_triggered, - .. - } - | SpircPlayStatus::Paused { - preloading_of_next_track_triggered, - .. - } = self.play_status - { - if preloading_of_next_track_triggered { - // Get the next track_id in the playlist - if let Some(track_id) = self.preview_next_track() { - self.player.preload(track_id); - } - } - } - - self.notify(None) - } - - MessageType::kMessageTypeVolume => { - self.set_volume(update.volume() as u16); - self.notify(None) - } - - MessageType::kMessageTypeNotify => { - if self.device.is_active() - && update.device_state.is_active() - && self.device.became_active_at() <= update.device_state.became_active_at() - { - self.handle_disconnect(); - } - self.notify(None) - } - - _ => Ok(()), - } + Ok(()) } - fn handle_disconnect(&mut self) { - self.device.set_is_active(false); + async fn handle_connect_state_request( + &mut self, + (request, sender): RequestReply, + ) -> Result<(), Error> { + self.connect_state.set_last_command(request.clone()); + + debug!( + "handling: '{}' from {}", + request.command, request.sent_by_device_id + ); + + let response = match self.handle_request(request).await { + Ok(_) => Reply::Success, + Err(why) => { + error!("failed to handle request: {why}"); + Reply::Failure + } + }; + + sender.send(response).map_err(Into::into) + } + + async fn handle_request(&mut self, request: Request) -> Result<(), Error> { + use Command::*; + + match request.command { + // errors and unknown commands + Transfer(transfer) if transfer.data.is_none() => { + warn!("transfer endpoint didn't contain any data to transfer"); + Err(SpircError::NoData)? + } + Unknown(unknown) => Err(SpircError::UnknownEndpoint(unknown))?, + // implicit update of the connect_state + UpdateContext(update_context) => { + if &update_context.context.uri != self.connect_state.context_uri() { + debug!( + "ignoring context update for <{}>, because it isn't the current context <{}>", + update_context.context.uri, self.connect_state.context_uri() + ) + } else { + self.add_resolve_context(ResolveContext::from_context( + update_context.context, + false, + )) + } + return Ok(()); + } + // modification and update of the connect_state + Transfer(transfer) => { + self.handle_transfer(transfer.data.expect("by condition checked"))? + } + Play(play) => { + let shuffle = play + .options + .player_options_override + .as_ref() + .map(|o| o.shuffling_context) + .unwrap_or_else(|| self.connect_state.shuffling_context()); + let repeat = play + .options + .player_options_override + .as_ref() + .map(|o| o.repeating_context) + .unwrap_or_else(|| self.connect_state.repeat_context()); + let repeat_track = play + .options + .player_options_override + .as_ref() + .map(|o| o.repeating_track) + .unwrap_or_else(|| self.connect_state.repeat_track()); + + self.handle_load( + SpircLoadCommand { + context_uri: play.context.uri.clone(), + start_playing: true, + seek_to: play.options.seek_to.unwrap_or_default(), + playing_track: play.options.skip_to.into(), + shuffle, + repeat, + repeat_track, + }, + Some(play.context), + ) + .await?; + + self.connect_state.set_origin(play.play_origin) + } + Pause(_) => self.handle_pause(), + SeekTo(seek_to) => { + // for some reason the position is stored in value, not in position + trace!("seek to {seek_to:?}"); + self.handle_seek(seek_to.value) + } + SetShufflingContext(shuffle) => self.connect_state.handle_shuffle(shuffle.value)?, + SetRepeatingContext(repeat_context) => self + .connect_state + .handle_set_repeat(Some(repeat_context.value), None)?, + SetRepeatingTrack(repeat_track) => self + .connect_state + .handle_set_repeat(None, Some(repeat_track.value))?, + AddToQueue(add_to_queue) => self.connect_state.add_to_queue(add_to_queue.track, true), + SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue), + SetOptions(set_options) => { + let context = set_options.repeating_context; + let track = set_options.repeating_track; + self.connect_state.handle_set_repeat(context, track)?; + + let shuffle = set_options.shuffling_context; + if let Some(shuffle) = shuffle { + self.connect_state.handle_shuffle(shuffle)?; + } + } + SkipNext(skip_next) => self.handle_next(skip_next.track.map(|t| t.uri))?, + SkipPrev(_) => self.handle_prev()?, + Resume(_) if matches!(self.play_status, SpircPlayStatus::Stopped) => { + self.load_track(true, 0)? + } + Resume(_) => self.handle_play(), + } + + self.notify().await + } + + fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> { + self.connect_state + .reset_context(ResetContext::WhenDifferent( + &transfer.current_session.context.uri, + )); + + let mut ctx_uri = transfer.current_session.context.uri.clone(); + + match self.connect_state.current_track_from_transfer(&transfer) { + Err(why) => warn!("didn't find initial track: {why}"), + Ok(track) => { + debug!("found initial track <{}>", track.uri); + self.connect_state.set_track(track) + } + }; + + let autoplay = self.connect_state.current_track(|t| t.is_from_autoplay()); + if autoplay { + ctx_uri = ctx_uri.replace("station:", ""); + } + + let fallback = self.connect_state.current_track(|t| &t.uri).clone(); + + self.add_resolve_context(ResolveContext::from_uri(ctx_uri.clone(), &fallback, false)); + + let timestamp = self.now_ms(); + let state = &mut self.connect_state; + + state.set_active(true); + state.handle_initial_transfer(&mut transfer); + + // update position if the track continued playing + let position = if transfer.playback.is_paused { + transfer.playback.position_as_of_timestamp.into() + } else if transfer.playback.position_as_of_timestamp > 0 { + let time_since_position_update = timestamp - transfer.playback.timestamp; + i64::from(transfer.playback.position_as_of_timestamp) + time_since_position_update + } else { + 0 + }; + + let is_playing = !transfer.playback.is_paused; + + if self.connect_state.current_track(|t| t.is_autoplay()) || autoplay { + debug!("currently in autoplay context, async resolving autoplay for {ctx_uri}"); + + self.add_resolve_context(ResolveContext::from_uri(ctx_uri, fallback, true)) + } + + self.transfer_state = Some(transfer); + + self.load_track(is_playing, position.try_into()?) + } + + async fn handle_disconnect(&mut self) -> Result<(), Error> { self.handle_stop(); + self.play_status = SpircPlayStatus::Stopped {}; + self.connect_state + .update_position_in_relation(self.now_ms()); + self.notify().await?; + + self.connect_state.became_inactive(&self.session).await?; + self.player .emit_session_disconnected_event(self.session.connection_id(), self.session.username()); + + Ok(()) } fn handle_stop(&mut self) { self.player.stop(); + self.connect_state.update_position(0, self.now_ms()); + self.connect_state.clear_next_tracks(true); + + if let Err(why) = self.connect_state.fill_up_next_tracks() { + warn!("failed filling up next_track during stopping: {why}") + } } fn handle_activate(&mut self) { - let now = self.now_ms(); - self.device.set_is_active(true); - self.device.set_became_active_at(now); + self.connect_state.set_active(true); self.player .emit_session_connected_event(self.session.connection_id(), self.session.username()); self.player.emit_session_client_changed_event( @@ -1048,7 +1179,7 @@ impl SpircTask { ); self.player - .emit_volume_changed_event(self.device.volume() as u16); + .emit_volume_changed_event(self.connect_state.device_info().volume as u16); self.player .emit_auto_play_changed_event(self.session.autoplay()); @@ -1056,33 +1187,94 @@ impl SpircTask { self.player .emit_filter_explicit_content_changed_event(self.session.filter_explicit_content()); - self.player.emit_shuffle_changed_event(self.state.shuffle()); + self.player + .emit_shuffle_changed_event(self.connect_state.shuffling_context()); - self.player.emit_repeat_changed_event(self.state.repeat()); + self.player.emit_repeat_changed_event( + self.connect_state.repeat_context(), + self.connect_state.repeat_track(), + ); } - fn handle_load(&mut self, state: &State) -> Result<(), Error> { - if !self.device.is_active() { + async fn handle_load( + &mut self, + cmd: SpircLoadCommand, + context: Option, + ) -> Result<(), Error> { + self.connect_state + .reset_context(ResetContext::WhenDifferent(&cmd.context_uri)); + + if !self.connect_state.is_active() { self.handle_activate(); } - let context_uri = state.context_uri().to_owned(); - - // completely ignore local playback. - if context_uri.starts_with("spotify:local-files") { - self.notify(None)?; - return Err(SpircError::UnsupportedLocalPlayBack.into()); - } - - self.update_tracks(state); - - if !self.state.track.is_empty() { - let start_playing = state.status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, state.position_ms()); + let current_context_uri = self.connect_state.context_uri(); + let fallback = if let Some(ref ctx) = context { + match ConnectState::get_context_uri_from_context(ctx) { + Some(ctx_uri) => ctx_uri, + None => Err(SpircError::InvalidUri(cmd.context_uri.clone()))?, + } } else { - info!("No more tracks left in queue"); - self.handle_stop(); + &cmd.context_uri } + .clone(); + + if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { + debug!("context <{current_context_uri}> didn't change, no resolving required") + } else { + debug!("resolving context for load command"); + self.resolve_context(&fallback, &cmd.context_uri, false, true) + .await?; + } + + // for play commands with skip by uid, the context of the command contains + // tracks with uri and uid, so we merge the new context with the resolved/existing context + self.connect_state.merge_context(context); + self.connect_state.clear_next_tracks(false); + self.connect_state.clear_restrictions(); + + debug!("play track <{:?}>", cmd.playing_track); + + let index = match cmd.playing_track { + PlayingTrack::Index(i) => i as usize, + PlayingTrack::Uri(uri) => { + let ctx = self.connect_state.context.as_ref(); + ConnectState::find_index_in_context(ctx, |t| t.uri == uri)? + } + PlayingTrack::Uid(uid) => { + let ctx = self.connect_state.context.as_ref(); + ConnectState::find_index_in_context(ctx, |t| t.uid == uid)? + } + }; + + debug!( + "loading with shuffle: <{}>, repeat track: <{}> context: <{}>", + cmd.shuffle, cmd.repeat, cmd.repeat_track + ); + + self.connect_state.set_shuffle(cmd.shuffle); + self.connect_state.set_repeat_context(cmd.repeat); + + if cmd.shuffle { + self.connect_state.set_current_track(index)?; + self.connect_state.shuffle()?; + } else { + // manually overwrite a possible current queued track + self.connect_state.set_current_track(index)?; + self.connect_state.reset_playback_to_position(Some(index))?; + } + + self.connect_state.set_repeat_track(cmd.repeat_track); + + if self.connect_state.current_track(MessageField::is_some) { + self.load_track(cmd.start_playing, cmd.seek_to)?; + } else { + info!("No active track, stopping"); + self.handle_stop() + } + + self.preload_autoplay_when_required(); + Ok(()) } @@ -1093,8 +1285,8 @@ impl SpircTask { preloading_of_next_track_triggered, } => { self.player.play(); - self.state.set_status(PlayStatus::kPlayStatusPlay); - self.update_state_position(position_ms); + self.connect_state + .update_position(position_ms, self.now_ms()); self.play_status = SpircPlayStatus::Playing { nominal_start_time: self.now_ms() - position_ms as i64, preloading_of_next_track_triggered, @@ -1132,9 +1324,9 @@ impl SpircTask { preloading_of_next_track_triggered, } => { self.player.pause(); - self.state.set_status(PlayStatus::kPlayStatusPause); let position_ms = (self.now_ms() - nominal_start_time) as u32; - self.update_state_position(position_ms); + self.connect_state + .update_position(position_ms, self.now_ms()); self.play_status = SpircPlayStatus::Paused { position_ms, preloading_of_next_track_triggered, @@ -1149,7 +1341,8 @@ impl SpircTask { } fn handle_seek(&mut self, position_ms: u32) { - self.update_state_position(position_ms); + self.connect_state + .update_position(position_ms, self.now_ms()); self.player.seek(position_ms); let now = self.now_ms(); match self.play_status { @@ -1171,23 +1364,6 @@ impl SpircTask { }; } - fn consume_queued_track(&mut self) -> usize { - // Removes current track if it is queued - // Returns the index of the next track - let current_index = self.state.playing_track_index() as usize; - if (current_index < self.state.track.len()) && self.state.track[current_index].queued() { - self.state.track.remove(current_index); - current_index - } else { - current_index + 1 - } - } - - fn preview_next_track(&mut self) -> Option { - self.get_track_id_to_play_from_playlist(self.state.playing_track_index() + 1) - .map(|(track_id, _)| track_id) - } - fn handle_preload_next_track(&mut self) { // Requests the player thread to preload the next track match self.play_status { @@ -1204,138 +1380,195 @@ impl SpircTask { _ => (), } - if let Some(track_id) = self.preview_next_track() { + if let Some(track_id) = self.connect_state.preview_next_track() { self.player.preload(track_id); - } else { - self.handle_stop(); } } // Mark unavailable tracks so we can skip them later - fn handle_unavailable(&mut self, track_id: SpotifyId) { - let unavailables = self.get_track_index_for_spotify_id(&track_id, 0); - for &index in unavailables.iter() { - let mut unplayable_track_ref = TrackRef::new(); - unplayable_track_ref.set_gid(self.state.track[index].gid().to_vec()); - // Misuse context field to flag the track - unplayable_track_ref.set_context(String::from("NonPlayable")); - std::mem::swap(&mut self.state.track[index], &mut unplayable_track_ref); - debug!( - "Marked <{:?}> at {:?} as NonPlayable", - self.state.track[index], index, - ); - } + fn handle_unavailable(&mut self, track_id: SpotifyId) -> Result<(), Error> { + self.connect_state.mark_unavailable(track_id)?; self.handle_preload_next_track(); + + Ok(()) } - fn handle_next(&mut self) { - let context_uri = self.state.context_uri().to_owned(); - let mut tracks_len = self.state.track.len() as u32; - let mut new_index = self.consume_queued_track() as u32; - let mut continue_playing = self.state.status() == PlayStatus::kPlayStatusPlay; + fn preload_autoplay_when_required(&mut self) { + let require_load_new = !self + .connect_state + .has_next_tracks(Some(CONTEXT_FETCH_THRESHOLD)); - let update_tracks = - self.autoplay_context && tracks_len - new_index < CONTEXT_FETCH_THRESHOLD; - - debug!( - "At track {:?} of {:?} <{:?}> update [{}]", - new_index + 1, - tracks_len, - context_uri, - update_tracks, - ); - - // When in autoplay, keep topping up the playlist when it nears the end - if update_tracks { - if let Some(ref context) = self.context { - self.resolve_context = Some(context.next_page_url.to_owned()); - self.update_tracks_from_context(); - tracks_len = self.state.track.len() as u32; - } + if !require_load_new { + return; } - // When not in autoplay, either start autoplay or loop back to the start - if new_index >= tracks_len { - // for some contexts there is no autoplay, such as shows and episodes - // in such cases there is no context in librespot. - if self.context.is_some() && self.session.autoplay() { - // Extend the playlist - debug!("Starting autoplay for <{}>", context_uri); - // force reloading the current context with an autoplay context - self.autoplay_context = true; - self.resolve_context = Some(self.state.context_uri().to_owned()); - self.update_tracks_from_context(); - self.player.set_auto_normalise_as_album(false); - } else { - new_index = 0; - continue_playing &= self.state.repeat(); - debug!("Looping back to start, repeat is {}", continue_playing); + match self.connect_state.try_load_next_context() { + Err(why) => error!("failed loading next context: {why}"), + Ok(next) => { + match next { + LoadNext::Done => info!("loaded next context"), + LoadNext::PageUrl(page_url) => { + self.add_resolve_context(ResolveContext::from_page_url(page_url)) + } + LoadNext::Empty if self.session.autoplay() => { + let current_context = self.connect_state.context_uri(); + let fallback = self.connect_state.current_track(|t| &t.uri); + let resolve = ResolveContext::from_uri(current_context, fallback, true); + + self.add_resolve_context(resolve) + } + LoadNext::Empty => { + debug!("next context is empty and autoplay isn't enabled, no preloading required") + } + } } } + } - if tracks_len > 0 { - self.state.set_playing_track_index(new_index); - self.load_track(continue_playing, 0); + fn is_playing(&self) -> bool { + matches!( + self.play_status, + SpircPlayStatus::Playing { .. } | SpircPlayStatus::LoadingPlay { .. } + ) + } + + fn handle_next(&mut self, track_uri: Option) -> Result<(), Error> { + let continue_playing = self.is_playing(); + + let current_uri = self.connect_state.current_track(|t| &t.uri); + let mut has_next_track = + matches!(track_uri, Some(ref track_uri) if current_uri == track_uri); + + if !has_next_track { + has_next_track = loop { + let index = self.connect_state.next_track()?; + + let current_uri = self.connect_state.current_track(|t| &t.uri); + if matches!(track_uri, Some(ref track_uri) if current_uri != track_uri) { + continue; + } else { + break index.is_some(); + } + }; + }; + + self.preload_autoplay_when_required(); + + if has_next_track { + self.load_track(continue_playing, 0) } else { info!("Not playing next track because there are no more tracks left in queue."); - self.state.set_playing_track_index(0); + self.connect_state.reset_playback_to_position(None)?; self.handle_stop(); + Ok(()) } } - fn handle_prev(&mut self) { + fn handle_prev(&mut self) -> Result<(), Error> { // Previous behaves differently based on the position // Under 3s it goes to the previous song (starts playing) // Over 3s it seeks to zero (retains previous play status) if self.position() < 3000 { - // Queued tracks always follow the currently playing track. - // They should not be considered when calculating the previous - // track so extract them beforehand and reinsert them after it. - let mut queue_tracks = Vec::new(); - { - let queue_index = self.consume_queued_track(); - let tracks = &mut self.state.track; - while queue_index < tracks.len() && tracks[queue_index].queued() { - queue_tracks.push(tracks.remove(queue_index)); + let repeat_context = self.connect_state.repeat_context(); + match self.connect_state.prev_track()? { + None if repeat_context => self.connect_state.reset_playback_to_position(None)?, + None => { + self.connect_state.reset_playback_to_position(None)?; + self.handle_stop() } + Some(_) => self.load_track(self.is_playing(), 0)?, } - let current_index = self.state.playing_track_index(); - let new_index = if current_index > 0 { - current_index - 1 - } else if self.state.repeat() { - self.state.track.len() as u32 - 1 - } else { - 0 - }; - // Reinsert queued tracks after the new playing track. - let mut pos = (new_index + 1) as usize; - for track in queue_tracks { - self.state.track.insert(pos, track); - pos += 1; - } - - self.state.set_playing_track_index(new_index); - - let start_playing = self.state.status() == PlayStatus::kPlayStatusPlay; - self.load_track(start_playing, 0); } else { self.handle_seek(0); } + + Ok(()) } fn handle_volume_up(&mut self) { - let volume = (self.device.volume() as u16).saturating_add(VOLUME_STEP_SIZE); + let volume = + (self.connect_state.device_info().volume as u16).saturating_add(VOLUME_STEP_SIZE); self.set_volume(volume); } fn handle_volume_down(&mut self) { - let volume = (self.device.volume() as u16).saturating_sub(VOLUME_STEP_SIZE); + let volume = + (self.connect_state.device_info().volume as u16).saturating_sub(VOLUME_STEP_SIZE); self.set_volume(volume); } - fn handle_end_of_track(&mut self) -> Result<(), Error> { - self.handle_next(); - self.notify(None) + async fn handle_end_of_track(&mut self) -> Result<(), Error> { + let next_track = self + .connect_state + .repeat_track() + .then(|| self.connect_state.current_track(|t| t.uri.clone())); + + self.handle_next(next_track)?; + self.notify().await + } + + fn handle_playlist_modification( + &mut self, + playlist_modification_info: PlaylistModificationInfo, + ) -> Result<(), Error> { + let uri = playlist_modification_info.uri.ok_or(SpircError::NoData)?; + let uri = String::from_utf8(uri)?; + + if self.connect_state.context_uri() != &uri { + debug!("ignoring playlist modification update for playlist <{uri}>, because it isn't the current context"); + return Ok(()); + } + + debug!("playlist modification for current context: {uri}"); + self.add_resolve_context(ResolveContext::from_uri( + uri, + self.connect_state.current_track(|t| &t.uri), + false, + )); + + Ok(()) + } + + fn handle_session_update(&mut self, mut session_update: SessionUpdate) { + let reason = session_update.reason.enum_value(); + + let mut session = match session_update.session.take() { + None => return, + Some(session) => session, + }; + + let active_device = session._host_active_device_id.take().map(|id| match id { + _host_active_device_id::HostActiveDeviceId(id) => id, + other => { + warn!("unexpected active device id {other:?}"); + String::new() + } + }); + + if matches!(active_device, Some(ref device) if device == self.session.device_id()) { + info!( + "session update: <{:?}> for self, current session_id {}, new session_id {}", + reason, + self.session.session_id(), + session.session_id + ); + + if self.session.session_id() != session.session_id { + self.session.set_session_id(session.session_id.clone()); + self.connect_state.set_session_id(session.session_id); + } + } else { + debug!("session update: <{reason:?}> from active session host: <{active_device:?}>"); + } + + // this seems to be used for jams or handling the current session_id + // + // handling this event was intended to keep the playback when other clients (primarily + // mobile) connects, otherwise they would steel the current playback when there was no + // session_id provided on the initial PutStateReason::NEW_DEVICE state update + // + // by generating an initial session_id from the get-go we prevent that behavior and + // currently don't need to handle this event, might still be useful for later "jam" support } fn position(&mut self) -> u32 { @@ -1350,190 +1583,57 @@ impl SpircTask { } } - fn update_tracks_from_context(&mut self) { - if let Some(ref context) = self.context { - let new_tracks = &context.tracks; - - debug!("Adding {:?} tracks from context to frame", new_tracks.len()); - - let mut track_vec = self.state.track.clone(); - if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { - track_vec.drain(0..head); - } - track_vec.extend_from_slice(new_tracks); - self.state.track = track_vec; - - // Update playing index - if let Some(new_index) = self - .state - .playing_track_index() - .checked_sub(CONTEXT_TRACKS_HISTORY as u32) - { - self.state.set_playing_track_index(new_index); - } - } else { - warn!("No context to update from!"); - } - } - - fn update_tracks(&mut self, state: &State) { - trace!("State: {:#?}", state); - - let index = state.playing_track_index(); - let context_uri = state.context_uri(); - let tracks = &state.track; - - trace!("Frame has {:?} tracks", tracks.len()); - - // First the tracks from the requested context, without autoplay. - // We will transition into autoplay after the latest track of this context. - self.autoplay_context = false; - self.resolve_context = Some(context_uri.to_owned()); - - self.player - .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); - - self.state.set_playing_track_index(index); - self.state.track = tracks.to_vec(); - self.state.set_context_uri(context_uri.to_owned()); - // has_shuffle/repeat seem to always be true in these replace msgs, - // but to replicate the behaviour of the Android client we have to - // ignore false values. - if state.repeat() { - self.state.set_repeat(true); - } - if state.shuffle() { - self.state.set_shuffle(true); - } - } - - // Helper to find corresponding index(s) for track_id - fn get_track_index_for_spotify_id( - &self, - track_id: &SpotifyId, - start_index: usize, - ) -> Vec { - let index: Vec = self.state.track[start_index..] - .iter() - .enumerate() - .filter(|&(_, track_ref)| track_ref.gid() == track_id.to_raw()) - .map(|(idx, _)| start_index + idx) - .collect(); - index - } - - // Broken out here so we can refactor this later when we move to SpotifyObjectID or similar - fn track_ref_is_unavailable(&self, track_ref: &TrackRef) -> bool { - track_ref.context() == "NonPlayable" - } - - fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { - let tracks_len = self.state.track.len(); - - // Guard against tracks_len being zero to prevent - // 'index out of bounds: the len is 0 but the index is 0' - // https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 - if tracks_len == 0 { - warn!("No playable track found in state: {:?}", self.state); - return None; - } - - let mut new_playlist_index = index as usize; - - if new_playlist_index >= tracks_len { - new_playlist_index = 0; - } - - let start_index = new_playlist_index; - - // Cycle through all tracks, break if we don't find any playable tracks - // tracks in each frame either have a gid or uri (that may or may not be a valid track) - // E.g - context based frames sometimes contain tracks with - - let mut track_ref = self.state.track[new_playlist_index].clone(); - 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 - ); - - new_playlist_index += 1; - if new_playlist_index >= tracks_len { - new_playlist_index = 0; - } - - if new_playlist_index == start_index { - warn!("No playable track found in state: {:?}", self.state); - return None; - } - track_ref = self.state.track[new_playlist_index].clone(); - track_id = SpotifyId::try_from(&track_ref); - } - - match track_id { - Ok(track_id) => Some((track_id, new_playlist_index as u32)), - Err(_) => None, - } - } - - fn load_track(&mut self, start_playing: bool, position_ms: u32) { - let index = self.state.playing_track_index(); - - match self.get_track_id_to_play_from_playlist(index) { - Some((track, index)) => { - self.state.set_playing_track_index(index); - - self.player.load(track, start_playing, position_ms); - - self.update_state_position(position_ms); - if start_playing { - self.state.set_status(PlayStatus::kPlayStatusPlay); - self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; - } else { - self.state.set_status(PlayStatus::kPlayStatusPause); - self.play_status = SpircPlayStatus::LoadingPause { position_ms }; - } - } - None => { - self.handle_stop(); - } - } - } - - fn hello(&mut self) -> Result<(), Error> { - CommandSender::new(self, MessageType::kMessageTypeHello).send() - } - - fn notify(&mut self, recipient: Option<&str>) -> Result<(), Error> { - let status = self.state.status(); - - // When in loading state, the Spotify UI is disabled for interaction. - // On desktop this isn't so bad but on mobile it means that the bottom - // control disappears entirely. This is very confusing, so don't notify - // in this case. - if status == PlayStatus::kPlayStatusLoading { + fn load_track(&mut self, start_playing: bool, position_ms: u32) -> Result<(), Error> { + if self.connect_state.current_track(MessageField::is_none) { + debug!("current track is none, stopping playback"); + self.handle_stop(); return Ok(()); } - trace!("Sending status to server: [{:?}]", status); - let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); - if let Some(s) = recipient { - cs = cs.recipient(s); + let current_uri = self.connect_state.current_track(|t| &t.uri); + let id = SpotifyId::from_uri(current_uri)?; + self.player.load(id, start_playing, position_ms); + + self.connect_state + .update_position(position_ms, self.now_ms()); + if start_playing { + self.play_status = SpircPlayStatus::LoadingPlay { position_ms }; + } else { + self.play_status = SpircPlayStatus::LoadingPause { position_ms }; } - cs.send() + self.connect_state.set_status(&self.play_status); + + Ok(()) + } + + async fn notify(&mut self) -> Result<(), Error> { + self.connect_state.set_status(&self.play_status); + + if self.is_playing() { + self.connect_state + .update_position_in_relation(self.now_ms()); + } + + self.connect_state.set_now(self.now_ms() as u64); + + self.connect_state + .send_state(&self.session) + .await + .map(|_| ()) } fn set_volume(&mut self, volume: u16) { - let old_volume = self.device.volume(); + let old_volume = self.connect_state.device_info().volume; let new_volume = volume as u32; if old_volume != new_volume || self.mixer.volume() != volume { - self.device.set_volume(new_volume); + self.update_volume = true; + + self.connect_state.set_volume(new_volume); self.mixer.set_volume(volume); if let Some(cache) = self.session.cache() { cache.save_volume(volume) } - if self.device.is_active() { + if self.connect_state.is_active() { self.player.emit_volume_changed_event(volume); } } @@ -1545,44 +1645,3 @@ impl Drop for SpircTask { debug!("drop Spirc[{}]", self.spirc_id); } } - -struct CommandSender<'a> { - spirc: &'a mut SpircTask, - frame: protocol::spirc::Frame, -} - -impl<'a> CommandSender<'a> { - fn new(spirc: &'a mut SpircTask, cmd: MessageType) -> Self { - let mut frame = protocol::spirc::Frame::new(); - // frame version - frame.set_version(1); - // Latest known Spirc version is 3.2.6, but we need another interface to announce support for Spirc V3. - // Setting anything higher than 2.0.0 here just seems to limit it to 2.0.0. - frame.set_protocol_version("2.0.0".to_string()); - frame.set_ident(spirc.ident.clone()); - frame.set_seq_nr(spirc.sequence.get()); - frame.set_typ(cmd); - *frame.device_state.mut_or_insert_default() = spirc.device.clone(); - frame.set_state_update_id(spirc.now_ms()); - CommandSender { spirc, frame } - } - - fn recipient(mut self, recipient: &'a str) -> Self { - self.frame.recipient.push(recipient.to_owned()); - self - } - - #[allow(dead_code)] - fn state(mut self, state: protocol::spirc::State) -> Self { - *self.frame.state.mut_or_insert_default() = state; - self - } - - fn send(mut self) -> Result<(), Error> { - if self.frame.state.is_none() && self.spirc.device.is_active() { - *self.frame.state.mut_or_insert_default() = self.spirc.state.clone(); - } - - self.spirc.sender.send(self.frame.write_to_bytes()?) - } -} diff --git a/connect/src/state.rs b/connect/src/state.rs new file mode 100644 index 00000000..28e57dad --- /dev/null +++ b/connect/src/state.rs @@ -0,0 +1,448 @@ +pub(super) mod context; +mod handle; +pub mod metadata; +mod options; +pub(super) mod provider; +mod restrictions; +mod tracks; +mod transfer; + +use crate::model::SpircPlayStatus; +use crate::state::{ + context::{ContextType, ResetContext, StateContext}, + provider::{IsProvider, Provider}, +}; +use librespot_core::{ + config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, version, + Error, Session, +}; +use librespot_protocol::connect::{ + Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest, +}; +use librespot_protocol::player::{ + ContextIndex, ContextPage, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, + Suppressions, +}; +use log::LevelFilter; +use protobuf::{EnumOrUnknown, MessageField}; +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; +use thiserror::Error; + +// these limitations are essential, otherwise to many tracks will overload the web-player +const SPOTIFY_MAX_PREV_TRACKS_SIZE: usize = 10; +const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80; + +#[derive(Debug, Error)] +pub enum StateError { + #[error("the current track couldn't be resolved from the transfer state")] + CouldNotResolveTrackFromTransfer, + #[error("message field {0} was not available")] + MessageFieldNone(String), + #[error("context is not available. type: {0:?}")] + NoContext(ContextType), + #[error("could not find track {0:?} in context of {1}")] + CanNotFindTrackInContext(Option, usize), + #[error("currently {action} is not allowed because {reason}")] + CurrentlyDisallowed { action: String, reason: String }, + #[error("the provided context has no tracks")] + ContextHasNoTracks, + #[error("playback of local files is not supported")] + UnsupportedLocalPlayBack, + #[error("track uri <{0}> contains invalid characters")] + InvalidTrackUri(String), +} + +impl From for Error { + fn from(err: StateError) -> Self { + use StateError::*; + match err { + CouldNotResolveTrackFromTransfer + | MessageFieldNone(_) + | NoContext(_) + | CanNotFindTrackInContext(_, _) + | ContextHasNoTracks + | InvalidTrackUri(_) => Error::failed_precondition(err), + CurrentlyDisallowed { .. } | UnsupportedLocalPlayBack => Error::unavailable(err), + } + } +} + +#[derive(Debug, Clone)] +pub struct ConnectStateConfig { + pub session_id: String, + pub initial_volume: u32, + pub name: String, + pub device_type: DeviceType, + pub volume_steps: i32, + pub is_group: bool, +} + +impl Default for ConnectStateConfig { + fn default() -> Self { + Self { + session_id: String::new(), + initial_volume: u32::from(u16::MAX) / 2, + name: "librespot".to_string(), + device_type: DeviceType::Speaker, + volume_steps: 64, + is_group: false, + } + } +} + +#[derive(Default, Debug)] +pub struct ConnectState { + /// the entire state that is updated to the remote server + request: PutStateRequest, + + unavailable_uri: Vec, + + pub active_since: Option, + queue_count: u64, + + // separation is necessary because we could have already loaded + // the autoplay context but are still playing from the default context + /// to update the active context use [switch_active_context](ConnectState::set_active_context) + pub active_context: ContextType, + pub fill_up_context: ContextType, + + /// the context from which we play, is used to top up prev and next tracks + pub context: Option, + /// upcoming contexts, directly provided by the context-resolver + next_contexts: Vec, + + /// a context to keep track of our shuffled context, + /// should be only available when `player.option.shuffling_context` is true + shuffle_context: Option, + /// a context to keep track of the autoplay context + autoplay_context: Option, +} + +impl ConnectState { + pub fn new(cfg: ConnectStateConfig, session: &Session) -> Self { + let device_info = DeviceInfo { + can_play: true, + volume: cfg.initial_volume, + name: cfg.name, + device_id: session.device_id().to_string(), + device_type: EnumOrUnknown::new(cfg.device_type.into()), + device_software_version: version::SEMVER.to_string(), + spirc_version: version::SPOTIFY_SPIRC_VERSION.to_string(), + client_id: session.client_id(), + is_group: cfg.is_group, + capabilities: MessageField::some(Capabilities { + volume_steps: cfg.volume_steps, + hidden: false, // could be exposed later to only observe the playback + gaia_eq_connect_id: true, + can_be_player: true, + + needs_full_player_state: true, + + is_observable: true, + is_controllable: true, + + supports_gzip_pushes: true, + // todo: enable after logout handling is implemented, see spirc logout_request + supports_logout: false, + supported_types: vec!["audio/episode".into(), "audio/track".into()], + supports_playlist_v2: true, + supports_transfer_command: true, + supports_command_request: true, + supports_set_options_command: true, + + is_voice_enabled: false, + restrict_to_local: false, + disable_volume: false, + connect_disabled: false, + supports_rename: false, + supports_external_episodes: false, + supports_set_backend_metadata: false, + supports_hifi: MessageField::none(), + + command_acks: true, + ..Default::default() + }), + ..Default::default() + }; + + let mut state = Self { + request: PutStateRequest { + member_type: EnumOrUnknown::new(MemberType::CONNECT_STATE), + put_state_reason: EnumOrUnknown::new(PutStateReason::PLAYER_STATE_CHANGED), + device: MessageField::some(Device { + device_info: MessageField::some(device_info), + player_state: MessageField::some(PlayerState { + session_id: cfg.session_id, + ..Default::default() + }), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }; + state.reset(); + state + } + + fn reset(&mut self) { + self.set_active(false); + self.queue_count = 0; + + // preserve the session_id + let session_id = self.player().session_id.clone(); + + self.device_mut().player_state = MessageField::some(PlayerState { + session_id, + is_system_initiated: true, + playback_speed: 1., + play_origin: MessageField::some(PlayOrigin::new()), + suppressions: MessageField::some(Suppressions::new()), + options: MessageField::some(ContextPlayerOptions::new()), + // + 1, so that we have a buffer where we can swap elements + prev_tracks: Vec::with_capacity(SPOTIFY_MAX_PREV_TRACKS_SIZE + 1), + next_tracks: Vec::with_capacity(SPOTIFY_MAX_NEXT_TRACKS_SIZE + 1), + ..Default::default() + }); + } + + fn device_mut(&mut self) -> &mut Device { + self.request + .device + .as_mut() + .expect("the request is always available") + } + + fn player_mut(&mut self) -> &mut PlayerState { + self.device_mut() + .player_state + .as_mut() + .expect("the player_state has to be always given") + } + + pub fn device_info(&self) -> &DeviceInfo { + &self.request.device.device_info + } + + pub fn player(&self) -> &PlayerState { + &self.request.device.player_state + } + + pub fn is_active(&self) -> bool { + self.request.is_active + } + + pub fn set_volume(&mut self, volume: u32) { + self.device_mut() + .device_info + .as_mut() + .expect("the device_info has to be always given") + .volume = volume; + } + + pub fn set_last_command(&mut self, command: Request) { + self.request.last_command_message_id = command.message_id; + self.request.last_command_sent_by_device_id = command.sent_by_device_id; + } + + pub fn set_now(&mut self, now: u64) { + self.request.client_side_timestamp = now; + + if let Some(active_since) = self.active_since { + if let Ok(active_since_duration) = active_since.duration_since(UNIX_EPOCH) { + match active_since_duration.as_millis().try_into() { + Ok(active_since_ms) => self.request.started_playing_at = active_since_ms, + Err(why) => warn!("couldn't update active since because {why}"), + } + } + } + } + + pub fn set_active(&mut self, value: bool) { + if value { + if self.request.is_active { + return; + } + + self.request.is_active = true; + self.active_since = Some(SystemTime::now()) + } else { + self.request.is_active = false; + self.active_since = None + } + } + + pub fn set_origin(&mut self, origin: PlayOrigin) { + self.player_mut().play_origin = MessageField::some(origin) + } + + pub fn set_session_id(&mut self, session_id: String) { + self.player_mut().session_id = session_id; + } + + pub(crate) fn set_status(&mut self, status: &SpircPlayStatus) { + let player = self.player_mut(); + player.is_paused = matches!( + status, + SpircPlayStatus::LoadingPause { .. } + | SpircPlayStatus::Paused { .. } + | SpircPlayStatus::Stopped + ); + + // desktop and mobile require all 'states' set to true, when we are paused, + // otherwise the play button (desktop) is grayed out or the preview (mobile) can't be opened + player.is_buffering = player.is_paused + || matches!( + status, + SpircPlayStatus::LoadingPause { .. } | SpircPlayStatus::LoadingPlay { .. } + ); + player.is_playing = player.is_paused + || matches!( + status, + SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::Playing { .. } + ); + + debug!( + "updated connect play status playing: {}, paused: {}, buffering: {}", + player.is_playing, player.is_paused, player.is_buffering + ); + + self.update_restrictions() + } + + /// index is 0 based, so the first track is index 0 + pub fn update_current_index(&mut self, f: impl Fn(&mut ContextIndex)) { + match self.player_mut().index.as_mut() { + Some(player_index) => f(player_index), + None => { + let mut new_index = ContextIndex::new(); + f(&mut new_index); + self.player_mut().index = MessageField::some(new_index) + } + } + } + + pub fn update_position(&mut self, position_ms: u32, timestamp: i64) { + let player = self.player_mut(); + player.position_as_of_timestamp = position_ms.into(); + player.timestamp = timestamp; + } + + pub fn update_duration(&mut self, duration: u32) { + self.player_mut().duration = duration.into() + } + + pub fn update_queue_revision(&mut self) { + let mut state = DefaultHasher::new(); + self.next_tracks() + .iter() + .for_each(|t| t.uri.hash(&mut state)); + self.player_mut().queue_revision = state.finish().to_string() + } + + pub fn reset_playback_to_position(&mut self, new_index: Option) -> Result<(), Error> { + let new_index = new_index.unwrap_or(0); + self.update_current_index(|i| i.track = new_index as u32); + self.update_context_index(self.active_context, new_index + 1)?; + + if !self.current_track(|t| t.is_queue()) { + self.set_current_track(new_index)?; + } + + self.clear_prev_track(); + + if new_index > 0 { + let context = self.get_context(&self.active_context)?; + + let before_new_track = context.tracks.len() - new_index; + self.player_mut().prev_tracks = context + .tracks + .iter() + .rev() + .skip(before_new_track) + .take(SPOTIFY_MAX_PREV_TRACKS_SIZE) + .rev() + .cloned() + .collect(); + debug!("has {} prev tracks", self.prev_tracks().len()) + } + + self.clear_next_tracks(true); + self.fill_up_next_tracks()?; + self.update_restrictions(); + + Ok(()) + } + + fn mark_as_unavailable_for_match(track: &mut ProvidedTrack, uri: &str) { + if track.uri == uri { + debug!("Marked <{}:{}> as unavailable", track.provider, track.uri); + track.set_provider(Provider::Unavailable); + } + } + + pub fn update_position_in_relation(&mut self, timestamp: i64) { + let player = self.player_mut(); + + let diff = timestamp - player.timestamp; + player.position_as_of_timestamp += diff; + + if log::max_level() >= LevelFilter::Debug { + let pos = Duration::from_millis(player.position_as_of_timestamp as u64); + let time = Date::from_timestamp_ms(timestamp) + .map(|d| d.time().to_string()) + .unwrap_or_else(|_| timestamp.to_string()); + + let sec = pos.as_secs(); + let (min, sec) = (sec / 60, sec % 60); + debug!("update position to {min}:{sec:0>2} at {time}"); + } + + player.timestamp = timestamp; + } + + pub async fn became_inactive(&mut self, session: &Session) -> SpClientResult { + self.reset(); + self.reset_context(ResetContext::Completely); + + session.spclient().put_connect_state_inactive(false).await + } + + async fn send_with_reason( + &mut self, + session: &Session, + reason: PutStateReason, + ) -> SpClientResult { + let prev_reason = self.request.put_state_reason; + + self.request.put_state_reason = EnumOrUnknown::new(reason); + let res = self.send_state(session).await; + + self.request.put_state_reason = prev_reason; + res + } + + /// Notifies the remote server about a new device + pub async fn notify_new_device_appeared(&mut self, session: &Session) -> SpClientResult { + self.send_with_reason(session, PutStateReason::NEW_DEVICE) + .await + } + + /// Notifies the remote server about a new volume + pub async fn notify_volume_changed(&mut self, session: &Session) -> SpClientResult { + self.send_with_reason(session, PutStateReason::VOLUME_CHANGED) + .await + } + + /// Sends the connect state for the connect session to the remote server + pub async fn send_state(&self, session: &Session) -> SpClientResult { + session + .spclient() + .put_connect_state_request(&self.request) + .await + } +} diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs new file mode 100644 index 00000000..3e9d720e --- /dev/null +++ b/connect/src/state/context.rs @@ -0,0 +1,415 @@ +use crate::state::{metadata::Metadata, provider::Provider, ConnectState, StateError}; +use librespot_core::{Error, SpotifyId}; +use librespot_protocol::player::{ + Context, ContextIndex, ContextPage, ContextTrack, ProvidedTrack, Restrictions, +}; +use protobuf::MessageField; +use std::collections::HashMap; +use uuid::Uuid; + +const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files"; +const SEARCH_IDENTIFIER: &str = "spotify:search"; + +#[derive(Debug, Clone)] +pub struct StateContext { + pub tracks: Vec, + pub metadata: HashMap, + pub restrictions: Option, + /// is used to keep track which tracks are already loaded into the next_tracks + pub index: ContextIndex, +} + +#[derive(Default, Debug, Copy, Clone)] +pub enum ContextType { + #[default] + Default, + Shuffle, + Autoplay, +} + +pub enum LoadNext { + Done, + PageUrl(String), + Empty, +} + +#[derive(Debug)] +pub enum UpdateContext { + Default, + Autoplay, +} + +pub enum ResetContext<'s> { + Completely, + DefaultIndex, + WhenDifferent(&'s str), +} + +impl ConnectState { + pub fn find_index_in_context bool>( + context: Option<&StateContext>, + f: F, + ) -> Result { + let ctx = context + .as_ref() + .ok_or(StateError::NoContext(ContextType::Default))?; + + ctx.tracks + .iter() + .position(f) + .ok_or(StateError::CanNotFindTrackInContext(None, ctx.tracks.len())) + } + + pub(super) fn get_context(&self, ty: &ContextType) -> Result<&StateContext, StateError> { + match ty { + ContextType::Default => self.context.as_ref(), + ContextType::Shuffle => self.shuffle_context.as_ref(), + ContextType::Autoplay => self.autoplay_context.as_ref(), + } + .ok_or(StateError::NoContext(*ty)) + } + + pub fn context_uri(&self) -> &String { + &self.player().context_uri + } + + pub fn reset_context(&mut self, mut reset_as: ResetContext) { + self.set_active_context(ContextType::Default); + self.fill_up_context = ContextType::Default; + + if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.context_uri() != ctx) { + reset_as = ResetContext::Completely + } + self.shuffle_context = None; + + match reset_as { + ResetContext::Completely => { + self.context = None; + self.autoplay_context = None; + self.next_contexts.clear(); + } + ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), + ResetContext::DefaultIndex => { + for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()] + .into_iter() + .flatten() + { + ctx.index.track = 0; + ctx.index.page = 0; + } + } + } + + self.update_restrictions() + } + + pub fn get_context_uri_from_context(context: &Context) -> Option<&String> { + if !context.uri.starts_with(SEARCH_IDENTIFIER) { + return Some(&context.uri); + } + + context + .pages + .first() + .and_then(|p| p.tracks.first().map(|t| &t.uri)) + } + + pub fn set_active_context(&mut self, new_context: ContextType) { + self.active_context = new_context; + + let ctx = match self.get_context(&new_context) { + Err(why) => { + debug!("couldn't load context info because: {why}"); + return; + } + Ok(ctx) => ctx, + }; + + let mut restrictions = ctx.restrictions.clone(); + let metadata = ctx.metadata.clone(); + + let player = self.player_mut(); + + player.context_metadata.clear(); + player.restrictions.clear(); + + if let Some(restrictions) = restrictions.take() { + player.restrictions = MessageField::some(restrictions); + } + + for (key, value) in metadata { + player.context_metadata.insert(key, value); + } + } + + pub fn update_context(&mut self, mut context: Context, ty: UpdateContext) -> Result<(), Error> { + if context.pages.iter().all(|p| p.tracks.is_empty()) { + error!("context didn't have any tracks: {context:#?}"); + return Err(StateError::ContextHasNoTracks.into()); + } else if context.uri.starts_with(LOCAL_FILES_IDENTIFIER) { + return Err(StateError::UnsupportedLocalPlayBack.into()); + } + + if matches!(ty, UpdateContext::Default) { + self.next_contexts.clear(); + } + + let mut first_page = None; + for page in context.pages { + if first_page.is_none() && !page.tracks.is_empty() { + first_page = Some(page); + } else { + self.next_contexts.push(page) + } + } + + let page = match first_page { + None => Err(StateError::ContextHasNoTracks)?, + Some(p) => p, + }; + + let prev_context = match ty { + UpdateContext::Default => self.context.as_ref(), + UpdateContext::Autoplay => self.autoplay_context.as_ref(), + }; + + debug!( + "updated context {ty:?} from <{}> ({} tracks) to <{}> ({} tracks)", + self.context_uri(), + prev_context + .map(|c| c.tracks.len().to_string()) + .unwrap_or_else(|| "-".to_string()), + context.uri, + page.tracks.len() + ); + + match ty { + UpdateContext::Default => { + let mut new_context = self.state_context_from_page( + page, + context.restrictions.take(), + Some(&context.uri), + None, + ); + + // when we update the same context, we should try to preserve the previous position + // otherwise we might load the entire context twice + if !self.context_uri().contains(SEARCH_IDENTIFIER) + && self.context_uri() == &context.uri + { + match Self::find_index_in_context(Some(&new_context), |t| { + self.current_track(|t| &t.uri) == &t.uri + }) { + Ok(new_pos) => { + debug!("found new index of current track, updating new_context index to {new_pos}"); + new_context.index.track = (new_pos + 1) as u32; + } + // the track isn't anymore in the context + Err(_) if matches!(self.active_context, ContextType::Default) => { + warn!("current track was removed, setting pos to last known index"); + new_context.index.track = self.player().index.track + } + Err(_) => {} + } + // enforce reloading the context + self.clear_next_tracks(true); + } + + self.context = Some(new_context); + + if !context.url.contains(SEARCH_IDENTIFIER) { + self.player_mut().context_url = context.url; + } else { + self.player_mut().context_url.clear() + } + self.player_mut().context_uri = context.uri; + } + UpdateContext::Autoplay => { + self.autoplay_context = Some(self.state_context_from_page( + page, + context.restrictions.take(), + Some(&context.uri), + Some(Provider::Autoplay), + )) + } + } + + Ok(()) + } + + fn state_context_from_page( + &mut self, + page: ContextPage, + restrictions: Option, + new_context_uri: Option<&str>, + provider: Option, + ) -> StateContext { + let new_context_uri = new_context_uri.unwrap_or(self.context_uri()); + + let tracks = page + .tracks + .iter() + .flat_map(|track| { + match self.context_to_provided_track(track, Some(new_context_uri), provider.clone()) + { + Ok(t) => Some(t), + Err(why) => { + error!("couldn't convert {track:#?} into ProvidedTrack: {why}"); + None + } + } + }) + .collect::>(); + + StateContext { + tracks, + restrictions, + metadata: page.metadata, + index: ContextIndex::new(), + } + } + + pub fn merge_context(&mut self, context: Option) -> Option<()> { + let mut context = context?; + if self.context_uri() != &context.uri { + return None; + } + + let current_context = self.context.as_mut()?; + let new_page = context.pages.pop()?; + + for new_track in new_page.tracks { + if new_track.uri.is_empty() { + continue; + } + + if let Ok(position) = + Self::find_index_in_context(Some(current_context), |t| t.uri == new_track.uri) + { + let context_track = current_context.tracks.get_mut(position)?; + + for (key, value) in new_track.metadata { + warn!("merging metadata {key} {value}"); + context_track.metadata.insert(key, value); + } + + // the uid provided from another context might be actual uid of an item + if !new_track.uid.is_empty() { + context_track.uid = new_track.uid; + } + } + } + + Some(()) + } + + pub(super) fn update_context_index( + &mut self, + ty: ContextType, + new_index: usize, + ) -> Result<(), StateError> { + let context = match ty { + ContextType::Default => self.context.as_mut(), + ContextType::Shuffle => self.shuffle_context.as_mut(), + ContextType::Autoplay => self.autoplay_context.as_mut(), + } + .ok_or(StateError::NoContext(ty))?; + + context.index.track = new_index as u32; + Ok(()) + } + + pub fn context_to_provided_track( + &self, + ctx_track: &ContextTrack, + context_uri: Option<&str>, + provider: Option, + ) -> Result { + let id = if !ctx_track.uri.is_empty() { + if ctx_track.uri.contains(['?', '%']) { + Err(StateError::InvalidTrackUri(ctx_track.uri.clone()))? + } + + SpotifyId::from_uri(&ctx_track.uri)? + } else if !ctx_track.gid.is_empty() { + SpotifyId::from_raw(&ctx_track.gid)? + } else { + Err(StateError::InvalidTrackUri(String::new()))? + }; + + let provider = if self.unavailable_uri.contains(&ctx_track.uri) { + Provider::Unavailable + } else { + provider.unwrap_or(Provider::Context) + }; + + // assumption: the uid is used as unique-id of any item + // - queue resorting is done by each client and orients itself by the given uid + // - if no uid is present, resorting doesn't work or behaves not as intended + let uid = if ctx_track.uid.is_empty() { + // so setting providing a unique id should allow to resort the queue + Uuid::new_v4().as_simple().to_string() + } else { + ctx_track.uid.to_string() + }; + + let mut metadata = HashMap::new(); + for (k, v) in &ctx_track.metadata { + metadata.insert(k.to_string(), v.to_string()); + } + + let mut track = ProvidedTrack { + uri: id.to_uri()?.replace("unknown", "track"), + uid, + metadata, + provider: provider.to_string(), + ..Default::default() + }; + + if let Some(context_uri) = context_uri { + track.set_context_uri(context_uri.to_string()); + track.set_entity_uri(context_uri.to_string()); + } + + if matches!(provider, Provider::Autoplay) { + track.set_autoplay(true) + } + + Ok(track) + } + + pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<(), Error> { + let context = self.state_context_from_page(page, None, None, None); + let ctx = self + .context + .as_mut() + .ok_or(StateError::NoContext(ContextType::Default))?; + + for t in context.tracks { + ctx.tracks.push(t) + } + + Ok(()) + } + + pub fn try_load_next_context(&mut self) -> Result { + let next = match self.next_contexts.first() { + None => return Ok(LoadNext::Empty), + Some(_) => self.next_contexts.remove(0), + }; + + if next.tracks.is_empty() { + if next.page_url.is_empty() { + Err(StateError::NoContext(ContextType::Default))? + } + + self.update_current_index(|i| i.page += 1); + return Ok(LoadNext::PageUrl(next.page_url)); + } + + self.fill_context_from_page(next)?; + self.fill_up_next_tracks()?; + + Ok(LoadNext::Done) + } +} diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs new file mode 100644 index 00000000..a69e1ebe --- /dev/null +++ b/connect/src/state/handle.rs @@ -0,0 +1,65 @@ +use crate::state::{context::ResetContext, ConnectState}; +use librespot_core::{dealer::protocol::SetQueueCommand, Error}; +use protobuf::MessageField; + +impl ConnectState { + pub fn handle_shuffle(&mut self, shuffle: bool) -> Result<(), Error> { + self.set_shuffle(shuffle); + + if shuffle { + return self.shuffle(); + } + + self.reset_context(ResetContext::DefaultIndex); + + if self.current_track(MessageField::is_none) { + return Ok(()); + } + + let ctx = self.context.as_ref(); + let current_index = + ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; + + self.reset_playback_to_position(Some(current_index)) + } + + pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) { + self.set_next_tracks(set_queue.next_tracks); + self.set_prev_tracks(set_queue.prev_tracks); + self.update_queue_revision(); + } + + pub fn handle_set_repeat( + &mut self, + context: Option, + track: Option, + ) -> Result<(), Error> { + // doesn't need any state updates, because it should only change how the current song is played + if let Some(track) = track { + self.set_repeat_track(track); + } + + if matches!(context, Some(context) if self.repeat_context() == context) || context.is_none() + { + return Ok(()); + } + + if let Some(context) = context { + self.set_repeat_context(context); + } + + if self.repeat_context() { + self.set_shuffle(false); + self.reset_context(ResetContext::DefaultIndex); + + let ctx = self.context.as_ref(); + let current_track = ConnectState::find_index_in_context(ctx, |t| { + self.current_track(|t| &t.uri) == &t.uri + })?; + self.reset_playback_to_position(Some(current_track)) + } else { + self.update_restrictions(); + Ok(()) + } + } +} diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs new file mode 100644 index 00000000..d3788b22 --- /dev/null +++ b/connect/src/state/metadata.rs @@ -0,0 +1,84 @@ +use librespot_protocol::player::{ContextTrack, ProvidedTrack}; +use std::collections::HashMap; + +const CONTEXT_URI: &str = "context_uri"; +const ENTITY_URI: &str = "entity_uri"; +const IS_QUEUED: &str = "is_queued"; +const IS_AUTOPLAY: &str = "autoplay.is_autoplay"; + +const HIDDEN: &str = "hidden"; +const ITERATION: &str = "iteration"; + +#[allow(dead_code)] +pub trait Metadata { + fn metadata(&self) -> &HashMap; + fn metadata_mut(&mut self) -> &mut HashMap; + + fn is_from_queue(&self) -> bool { + matches!(self.metadata().get(IS_QUEUED), Some(is_queued) if is_queued.eq("true")) + } + + fn is_from_autoplay(&self) -> bool { + matches!(self.metadata().get(IS_AUTOPLAY), Some(is_autoplay) if is_autoplay.eq("true")) + } + + fn is_hidden(&self) -> bool { + matches!(self.metadata().get(HIDDEN), Some(is_hidden) if is_hidden.eq("true")) + } + + fn get_context_uri(&self) -> Option<&String> { + self.metadata().get(CONTEXT_URI) + } + + fn get_iteration(&self) -> Option<&String> { + self.metadata().get(ITERATION) + } + + fn set_queued(&mut self, queued: bool) { + self.metadata_mut() + .insert(IS_QUEUED.to_string(), queued.to_string()); + } + + fn set_autoplay(&mut self, autoplay: bool) { + self.metadata_mut() + .insert(IS_AUTOPLAY.to_string(), autoplay.to_string()); + } + + fn set_hidden(&mut self, hidden: bool) { + self.metadata_mut() + .insert(HIDDEN.to_string(), hidden.to_string()); + } + + fn set_context_uri(&mut self, uri: String) { + self.metadata_mut().insert(CONTEXT_URI.to_string(), uri); + } + + fn set_entity_uri(&mut self, uri: String) { + self.metadata_mut().insert(ENTITY_URI.to_string(), uri); + } + + fn add_iteration(&mut self, iter: i64) { + self.metadata_mut() + .insert(ITERATION.to_string(), iter.to_string()); + } +} + +impl Metadata for ContextTrack { + fn metadata(&self) -> &HashMap { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut HashMap { + &mut self.metadata + } +} + +impl Metadata for ProvidedTrack { + fn metadata(&self) -> &HashMap { + &self.metadata + } + + fn metadata_mut(&mut self) -> &mut HashMap { + &mut self.metadata + } +} diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs new file mode 100644 index 00000000..b6bc331c --- /dev/null +++ b/connect/src/state/options.rs @@ -0,0 +1,88 @@ +use crate::state::context::ContextType; +use crate::state::{ConnectState, StateError}; +use librespot_core::Error; +use librespot_protocol::player::{ContextIndex, ContextPlayerOptions}; +use protobuf::MessageField; +use rand::prelude::SliceRandom; + +impl ConnectState { + fn add_options_if_empty(&mut self) { + if self.player().options.is_none() { + self.player_mut().options = MessageField::some(ContextPlayerOptions::new()) + } + } + + pub fn set_repeat_context(&mut self, repeat: bool) { + self.add_options_if_empty(); + if let Some(options) = self.player_mut().options.as_mut() { + options.repeating_context = repeat; + } + } + + pub fn set_repeat_track(&mut self, repeat: bool) { + self.add_options_if_empty(); + if let Some(options) = self.player_mut().options.as_mut() { + options.repeating_track = repeat; + } + } + + pub fn set_shuffle(&mut self, shuffle: bool) { + self.add_options_if_empty(); + if let Some(options) = self.player_mut().options.as_mut() { + options.shuffling_context = shuffle; + } + } + + pub fn shuffle(&mut self) -> Result<(), Error> { + if let Some(reason) = self + .player() + .restrictions + .disallow_toggling_shuffle_reasons + .first() + { + Err(StateError::CurrentlyDisallowed { + action: "shuffle".to_string(), + reason: reason.clone(), + })? + } + + self.clear_prev_track(); + self.clear_next_tracks(true); + + let current_uri = self.current_track(|t| &t.uri); + + let ctx = self + .context + .as_ref() + .ok_or(StateError::NoContext(ContextType::Default))?; + + let current_track = Self::find_index_in_context(Some(ctx), |t| &t.uri == current_uri)?; + + let mut shuffle_context = ctx.clone(); + // we don't need to include the current track, because it is already being played + shuffle_context.tracks.remove(current_track); + + let mut rng = rand::thread_rng(); + shuffle_context.tracks.shuffle(&mut rng); + shuffle_context.index = ContextIndex::new(); + + self.shuffle_context = Some(shuffle_context); + self.set_active_context(ContextType::Shuffle); + self.fill_up_context = ContextType::Shuffle; + self.fill_up_next_tracks()?; + + Ok(()) + } + + pub fn shuffling_context(&self) -> bool { + self.player().options.shuffling_context + } + + pub fn repeat_context(&self) -> bool { + self.player().options.repeating_context + } + + pub fn repeat_track(&self) -> bool { + self.player().options.repeating_track + } +} diff --git a/connect/src/state/provider.rs b/connect/src/state/provider.rs new file mode 100644 index 00000000..97eb7aa4 --- /dev/null +++ b/connect/src/state/provider.rs @@ -0,0 +1,66 @@ +use librespot_protocol::player::ProvidedTrack; +use std::fmt::{Display, Formatter}; + +// providers used by spotify +const PROVIDER_CONTEXT: &str = "context"; +const PROVIDER_QUEUE: &str = "queue"; +const PROVIDER_AUTOPLAY: &str = "autoplay"; + +// custom providers, used to identify certain states that we can't handle preemptively, yet +/// it seems like spotify just knows that the track isn't available, currently we don't have an +/// option to do the same, so we stay with the old solution for now +const PROVIDER_UNAVAILABLE: &str = "unavailable"; + +#[derive(Debug, Clone)] +pub enum Provider { + Context, + Queue, + Autoplay, + Unavailable, +} + +impl Display for Provider { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + Provider::Context => PROVIDER_CONTEXT, + Provider::Queue => PROVIDER_QUEUE, + Provider::Autoplay => PROVIDER_AUTOPLAY, + Provider::Unavailable => PROVIDER_UNAVAILABLE, + } + ) + } +} + +pub trait IsProvider { + fn is_autoplay(&self) -> bool; + fn is_context(&self) -> bool; + fn is_queue(&self) -> bool; + fn is_unavailable(&self) -> bool; + + fn set_provider(&mut self, provider: Provider); +} + +impl IsProvider for ProvidedTrack { + fn is_autoplay(&self) -> bool { + self.provider == PROVIDER_AUTOPLAY + } + + fn is_context(&self) -> bool { + self.provider == PROVIDER_CONTEXT + } + + fn is_queue(&self) -> bool { + self.provider == PROVIDER_QUEUE + } + + fn is_unavailable(&self) -> bool { + self.provider == PROVIDER_UNAVAILABLE + } + + fn set_provider(&mut self, provider: Provider) { + self.provider = provider.to_string() + } +} diff --git a/connect/src/state/restrictions.rs b/connect/src/state/restrictions.rs new file mode 100644 index 00000000..a0f26933 --- /dev/null +++ b/connect/src/state/restrictions.rs @@ -0,0 +1,61 @@ +use crate::state::provider::IsProvider; +use crate::state::ConnectState; +use librespot_protocol::player::Restrictions; +use protobuf::MessageField; + +impl ConnectState { + pub fn clear_restrictions(&mut self) { + let player = self.player_mut(); + + player.restrictions.clear(); + player.context_restrictions.clear(); + } + + pub fn update_restrictions(&mut self) { + const NO_PREV: &str = "no previous tracks"; + const AUTOPLAY: &str = "autoplay"; + const ENDLESS_CONTEXT: &str = "endless_context"; + + let prev_tracks_is_empty = self.prev_tracks().is_empty(); + let player = self.player_mut(); + if let Some(restrictions) = player.restrictions.as_mut() { + if player.is_playing { + restrictions.disallow_pausing_reasons.clear(); + restrictions.disallow_resuming_reasons = vec!["not_paused".to_string()] + } + + if player.is_paused { + restrictions.disallow_resuming_reasons.clear(); + restrictions.disallow_pausing_reasons = vec!["not_playing".to_string()] + } + } + + if player.restrictions.is_none() { + player.restrictions = MessageField::some(Restrictions::new()) + } + + if let Some(restrictions) = player.restrictions.as_mut() { + if prev_tracks_is_empty { + restrictions.disallow_peeking_prev_reasons = vec![NO_PREV.to_string()]; + restrictions.disallow_skipping_prev_reasons = vec![NO_PREV.to_string()]; + } else { + restrictions.disallow_peeking_prev_reasons.clear(); + restrictions.disallow_skipping_prev_reasons.clear(); + } + + if player.track.is_autoplay() { + restrictions.disallow_toggling_shuffle_reasons = vec![AUTOPLAY.to_string()]; + restrictions.disallow_toggling_repeat_context_reasons = vec![AUTOPLAY.to_string()]; + restrictions.disallow_toggling_repeat_track_reasons = vec![AUTOPLAY.to_string()]; + } else if player.options.repeating_context { + restrictions.disallow_toggling_shuffle_reasons = vec![ENDLESS_CONTEXT.to_string()] + } else { + restrictions.disallow_toggling_shuffle_reasons.clear(); + restrictions + .disallow_toggling_repeat_context_reasons + .clear(); + restrictions.disallow_toggling_repeat_track_reasons.clear(); + } + } + } +} diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs new file mode 100644 index 00000000..2dc1b9af --- /dev/null +++ b/connect/src/state/tracks.rs @@ -0,0 +1,422 @@ +use crate::state::{ + context::ContextType, + metadata::Metadata, + provider::{IsProvider, Provider}, + ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, +}; +use librespot_core::{Error, SpotifyId}; +use librespot_protocol::player::ProvidedTrack; +use protobuf::MessageField; + +// identifier used as part of the uid +pub const IDENTIFIER_DELIMITER: &str = "delimiter"; + +impl<'ct> ConnectState { + fn new_delimiter(iteration: i64) -> ProvidedTrack { + let mut delimiter = ProvidedTrack { + uri: format!("spotify:{IDENTIFIER_DELIMITER}"), + uid: format!("{IDENTIFIER_DELIMITER}{iteration}"), + provider: Provider::Context.to_string(), + ..Default::default() + }; + delimiter.set_hidden(true); + delimiter.add_iteration(iteration); + + delimiter + } + + fn push_prev(&mut self, prev: ProvidedTrack) { + let prev_tracks = self.prev_tracks_mut(); + // add prev track, while preserving a length of 10 + if prev_tracks.len() >= SPOTIFY_MAX_PREV_TRACKS_SIZE { + // todo: O(n), but technically only maximal O(SPOTIFY_MAX_PREV_TRACKS_SIZE) aka O(10) + let _ = prev_tracks.remove(0); + } + prev_tracks.push(prev) + } + + fn get_next_track(&mut self) -> Option { + if self.next_tracks().is_empty() { + None + } else { + // todo: O(n), but technically only maximal O(SPOTIFY_MAX_NEXT_TRACKS_SIZE) aka O(80) + Some(self.next_tracks_mut().remove(0)) + } + } + + /// bottom => top, aka the last track of the list is the prev track + fn prev_tracks_mut(&mut self) -> &mut Vec { + &mut self.player_mut().prev_tracks + } + + /// bottom => top, aka the last track of the list is the prev track + pub(super) fn prev_tracks(&self) -> &Vec { + &self.player().prev_tracks + } + + /// top => bottom, aka the first track of the list is the next track + fn next_tracks_mut(&mut self) -> &mut Vec { + &mut self.player_mut().next_tracks + } + + /// top => bottom, aka the first track of the list is the next track + pub(super) fn next_tracks(&self) -> &Vec { + &self.player().next_tracks + } + + pub fn set_current_track(&mut self, index: usize) -> Result<(), Error> { + let context = self.get_context(&self.active_context)?; + + let new_track = context + .tracks + .get(index) + .ok_or(StateError::CanNotFindTrackInContext( + Some(index), + context.tracks.len(), + ))?; + + debug!( + "set track to: {} at {} of {} tracks", + index, + new_track.uri, + context.tracks.len() + ); + + self.set_track(new_track.clone()); + + self.update_current_index(|i| i.track = index as u32); + + Ok(()) + } + + /// Move to the next track + /// + /// Updates the current track to the next track. Adds the old track + /// to prev tracks and fills up the next tracks from the current context + pub fn next_track(&mut self) -> Result, Error> { + // when we skip in repeat track, we don't repeat the current track anymore + if self.repeat_track() { + self.set_repeat_track(false); + } + + let old_track = self.player_mut().track.take(); + + if let Some(old_track) = old_track { + // only add songs from our context to our previous tracks + if old_track.is_context() || old_track.is_autoplay() { + self.push_prev(old_track) + } + } + + let new_track = loop { + match self.get_next_track() { + Some(next) if next.uid.starts_with(IDENTIFIER_DELIMITER) => { + self.push_prev(next); + continue; + } + Some(next) if next.is_unavailable() => continue, + other => break other, + }; + }; + + let new_track = match new_track { + None => return Ok(None), + Some(t) => t, + }; + + self.fill_up_next_tracks()?; + + let update_index = if new_track.is_queue() { + None + } else if new_track.is_autoplay() { + self.set_active_context(ContextType::Autoplay); + None + } else { + let ctx = self.context.as_ref(); + let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); + match new_index { + Ok(new_index) => Some(new_index as u32), + Err(why) => { + error!("didn't find the track in the current context: {why}"); + None + } + } + }; + + if let Some(update_index) = update_index { + self.update_current_index(|i| i.track = update_index) + } else { + self.player_mut().index.clear() + } + + self.set_track(new_track); + self.update_restrictions(); + + Ok(Some(self.player().index.track)) + } + + /// Move to the prev track + /// + /// Updates the current track to the prev track. Adds the old track + /// to next tracks (when from the context) and fills up the prev tracks from the + /// current context + pub fn prev_track(&mut self) -> Result>, Error> { + let old_track = self.player_mut().track.take(); + + if let Some(old_track) = old_track { + if old_track.is_context() || old_track.is_autoplay() { + // todo: O(n) + self.next_tracks_mut().insert(0, old_track); + } + } + + // handle possible delimiter + if matches!(self.prev_tracks().last(), Some(prev) if prev.uid.starts_with(IDENTIFIER_DELIMITER)) + { + let delimiter = self + .prev_tracks_mut() + .pop() + .expect("item that was prechecked"); + + let next_tracks = self.next_tracks_mut(); + if next_tracks.len() >= SPOTIFY_MAX_NEXT_TRACKS_SIZE { + let _ = next_tracks.pop(); + } + // todo: O(n) + next_tracks.insert(0, delimiter) + } + + while self.next_tracks().len() > SPOTIFY_MAX_NEXT_TRACKS_SIZE { + let _ = self.next_tracks_mut().pop(); + } + + let new_track = match self.prev_tracks_mut().pop() { + None => return Ok(None), + Some(t) => t, + }; + + if matches!(self.active_context, ContextType::Autoplay if new_track.is_context()) { + // transition back to default context + self.set_active_context(ContextType::Default); + } + + self.fill_up_next_tracks()?; + self.set_track(new_track); + + if self.player().index.track == 0 { + warn!("prev: trying to skip into negative, index update skipped") + } else { + self.update_current_index(|i| i.track -= 1) + } + + self.update_restrictions(); + + Ok(Some(self.current_track(|t| t))) + } + + pub fn current_track) -> R, R>( + &'ct self, + access: F, + ) -> R { + access(&self.player().track) + } + + pub fn set_track(&mut self, track: ProvidedTrack) { + self.player_mut().track = MessageField::some(track) + } + + pub fn set_next_tracks(&mut self, mut tracks: Vec) { + // mobile only sends a set_queue command instead of an add_to_queue command + // in addition to handling the mobile add_to_queue handling, this should also handle + // a mass queue addition + tracks + .iter_mut() + .filter(|t| t.is_from_queue()) + .for_each(|t| { + t.set_provider(Provider::Queue); + // technically we could preserve the queue-uid here, + // but it seems to work without that, so we just override it + t.uid = format!("q{}", self.queue_count); + self.queue_count += 1; + }); + + self.player_mut().next_tracks = tracks; + } + + pub fn set_prev_tracks(&mut self, tracks: Vec) { + self.player_mut().prev_tracks = tracks; + } + + pub fn clear_prev_track(&mut self) { + self.prev_tracks_mut().clear() + } + + pub fn clear_next_tracks(&mut self, keep_queued: bool) { + if !keep_queued { + self.next_tracks_mut().clear(); + return; + } + + // respect queued track and don't throw them out of our next played tracks + let first_non_queued_track = self + .next_tracks() + .iter() + .enumerate() + .find(|(_, track)| !track.is_queue()); + + if let Some((non_queued_track, _)) = first_non_queued_track { + while self.next_tracks().len() > non_queued_track + && self.next_tracks_mut().pop().is_some() + {} + } + } + + pub fn fill_up_next_tracks(&mut self) -> Result<(), StateError> { + let ctx = self.get_context(&self.fill_up_context)?; + let mut new_index = ctx.index.track as usize; + let mut iteration = ctx.index.page; + + while self.next_tracks().len() < SPOTIFY_MAX_NEXT_TRACKS_SIZE { + let ctx = self.get_context(&self.fill_up_context)?; + let track = match ctx.tracks.get(new_index) { + None if self.repeat_context() => { + let delimiter = Self::new_delimiter(iteration.into()); + iteration += 1; + new_index = 0; + delimiter + } + None if !matches!(self.fill_up_context, ContextType::Autoplay) + && self.autoplay_context.is_some() => + { + self.update_context_index(self.fill_up_context, new_index)?; + + // transition to autoplay as fill up context + self.fill_up_context = ContextType::Autoplay; + new_index = self.get_context(&ContextType::Autoplay)?.index.track as usize; + + // add delimiter to only display the current context + Self::new_delimiter(iteration.into()) + } + None if self.autoplay_context.is_some() => { + match self + .get_context(&ContextType::Autoplay)? + .tracks + .get(new_index) + { + None => break, + Some(ct) => { + new_index += 1; + ct.clone() + } + } + } + None => break, + Some(ct) if ct.is_unavailable() => { + new_index += 1; + continue; + } + Some(ct) => { + new_index += 1; + ct.clone() + } + }; + + self.next_tracks_mut().push(track); + } + + self.update_context_index(self.fill_up_context, new_index)?; + + // the web-player needs a revision update, otherwise the queue isn't updated in the ui + self.update_queue_revision(); + + Ok(()) + } + + pub fn preview_next_track(&mut self) -> Option { + let next = if self.repeat_track() { + self.current_track(|t| &t.uri) + } else { + &self.next_tracks().first()?.uri + }; + + SpotifyId::from_uri(next).ok() + } + + pub fn has_next_tracks(&self, min: Option) -> bool { + if let Some(min) = min { + self.next_tracks().len() >= min + } else { + !self.next_tracks().is_empty() + } + } + + pub fn prev_autoplay_track_uris(&self) -> Vec { + let mut prev = self + .prev_tracks() + .iter() + .flat_map(|t| t.is_autoplay().then_some(t.uri.clone())) + .collect::>(); + + if self.current_track(|t| t.is_autoplay()) { + prev.push(self.current_track(|t| t.uri.clone())); + } + + prev + } + + pub fn mark_unavailable(&mut self, id: SpotifyId) -> Result<(), Error> { + let uri = id.to_uri()?; + + debug!("marking {uri} as unavailable"); + + let next_tracks = self.next_tracks_mut(); + while let Some(pos) = next_tracks.iter().position(|t| t.uri == uri) { + let _ = next_tracks.remove(pos); + } + + for next_track in next_tracks { + Self::mark_as_unavailable_for_match(next_track, &uri) + } + + let prev_tracks = self.prev_tracks_mut(); + while let Some(pos) = prev_tracks.iter().position(|t| t.uri == uri) { + let _ = prev_tracks.remove(pos); + } + + for prev_track in prev_tracks { + Self::mark_as_unavailable_for_match(prev_track, &uri) + } + + self.unavailable_uri.push(uri); + self.fill_up_next_tracks()?; + self.update_queue_revision(); + + Ok(()) + } + + pub fn add_to_queue(&mut self, mut track: ProvidedTrack, rev_update: bool) { + track.uid = format!("q{}", self.queue_count); + self.queue_count += 1; + + track.set_provider(Provider::Queue); + if !track.is_from_queue() { + track.set_queued(true); + } + + let next_tracks = self.next_tracks_mut(); + if let Some(next_not_queued_track) = next_tracks.iter().position(|t| !t.is_queue()) { + next_tracks.insert(next_not_queued_track, track); + } else { + next_tracks.push(track) + } + + while next_tracks.len() > SPOTIFY_MAX_NEXT_TRACKS_SIZE { + next_tracks.pop(); + } + + if rev_update { + self.update_queue_revision(); + } + self.update_restrictions(); + } +} diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs new file mode 100644 index 00000000..c310e0b9 --- /dev/null +++ b/connect/src/state/transfer.rs @@ -0,0 +1,146 @@ +use crate::state::context::ContextType; +use crate::state::metadata::Metadata; +use crate::state::provider::{IsProvider, Provider}; +use crate::state::{ConnectState, StateError}; +use librespot_core::Error; +use librespot_protocol::player::{ProvidedTrack, TransferState}; +use protobuf::MessageField; + +impl ConnectState { + pub fn current_track_from_transfer( + &self, + transfer: &TransferState, + ) -> Result { + let track = if transfer.queue.is_playing_queue { + transfer.queue.tracks.first() + } else { + transfer.playback.current_track.as_ref() + } + .ok_or(StateError::CouldNotResolveTrackFromTransfer)?; + + self.context_to_provided_track( + track, + Some(&transfer.current_session.context.uri), + transfer.queue.is_playing_queue.then_some(Provider::Queue), + ) + } + + /// handles the initially transferable data + pub fn handle_initial_transfer(&mut self, transfer: &mut TransferState) { + let current_context_metadata = self.context.as_ref().map(|c| c.metadata.clone()); + let player = self.player_mut(); + + player.is_buffering = false; + + if let Some(options) = transfer.options.take() { + player.options = MessageField::some(options); + } + player.is_paused = transfer.playback.is_paused; + player.is_playing = !transfer.playback.is_paused; + + if transfer.playback.playback_speed != 0. { + player.playback_speed = transfer.playback.playback_speed + } else { + player.playback_speed = 1.; + } + + if let Some(session) = transfer.current_session.as_mut() { + player.play_origin = session.play_origin.take().into(); + player.suppressions = session.suppressions.take().into(); + + if let Some(mut ctx) = session.context.take() { + player.restrictions = ctx.restrictions.take().into(); + for (key, value) in ctx.metadata { + player.context_metadata.insert(key, value); + } + } + } + + player.context_url.clear(); + player.context_uri.clear(); + + if let Some(metadata) = current_context_metadata { + for (key, value) in metadata { + player.context_metadata.insert(key, value); + } + } + + self.clear_prev_track(); + self.clear_next_tracks(false); + } + + /// completes the transfer, loading the queue and updating metadata + pub fn finish_transfer(&mut self, transfer: TransferState) -> Result<(), Error> { + let track = match self.player().track.as_ref() { + None => self.current_track_from_transfer(&transfer)?, + Some(track) => track.clone(), + }; + + let context_ty = if self.current_track(|t| t.is_from_autoplay()) { + ContextType::Autoplay + } else { + ContextType::Default + }; + + self.set_active_context(context_ty); + self.fill_up_context = context_ty; + + let ctx = self.get_context(&self.active_context).ok(); + + let current_index = if track.is_queue() { + Self::find_index_in_context(ctx, |c| c.uid == transfer.current_session.current_uid) + .map(|i| if i > 0 { i - 1 } else { i }) + } else { + Self::find_index_in_context(ctx, |c| c.uri == track.uri || c.uid == track.uid) + }; + + debug!( + "active track is <{}> with index {current_index:?} in {:?} context, has {} tracks", + track.uri, + self.active_context, + ctx.map(|c| c.tracks.len()).unwrap_or_default() + ); + + if self.player().track.is_none() { + self.set_track(track); + } + + let current_index = current_index.ok(); + if let Some(current_index) = current_index { + self.update_current_index(|i| i.track = current_index as u32); + } + + debug!( + "setting up next and prev: index is at {current_index:?} while shuffle {}", + self.shuffling_context() + ); + + for (i, track) in transfer.queue.tracks.iter().enumerate() { + if transfer.queue.is_playing_queue && i == 0 { + // if we are currently playing from the queue, + // don't add the first queued item, because we are currently playing that item + continue; + } + + if let Ok(queued_track) = self.context_to_provided_track( + track, + Some(self.context_uri()), + Some(Provider::Queue), + ) { + self.add_to_queue(queued_track, false); + } + } + + if self.shuffling_context() { + self.set_current_track(current_index.unwrap_or_default())?; + self.set_shuffle(true); + self.shuffle()?; + } else { + self.reset_playback_to_position(current_index)?; + } + + self.update_restrictions(); + + Ok(()) + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index b76e6b8a..66d54a8b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -60,6 +60,8 @@ tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } data-encoding = "2.5" +flate2 = "1.0.33" +protobuf-json-mapping = "3.5" # Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies # but currently, hyper-proxy2 and tokio-tungstenite do not support it. diff --git a/core/src/config.rs b/core/src/config.rs index 1160c0f5..0b17690e 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -1,5 +1,6 @@ use std::{fmt, path::PathBuf, str::FromStr}; +use librespot_protocol::devices::DeviceType as ProtoDeviceType; use url::Url; pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; @@ -146,3 +147,29 @@ impl fmt::Display for DeviceType { f.write_str(str) } } + +impl From for ProtoDeviceType { + fn from(value: DeviceType) -> Self { + match value { + DeviceType::Unknown => ProtoDeviceType::UNKNOWN, + DeviceType::Computer => ProtoDeviceType::COMPUTER, + DeviceType::Tablet => ProtoDeviceType::TABLET, + DeviceType::Smartphone => ProtoDeviceType::SMARTPHONE, + DeviceType::Speaker => ProtoDeviceType::SPEAKER, + DeviceType::Tv => ProtoDeviceType::TV, + DeviceType::Avr => ProtoDeviceType::AVR, + DeviceType::Stb => ProtoDeviceType::STB, + DeviceType::AudioDongle => ProtoDeviceType::AUDIO_DONGLE, + DeviceType::GameConsole => ProtoDeviceType::GAME_CONSOLE, + DeviceType::CastAudio => ProtoDeviceType::CAST_VIDEO, + DeviceType::CastVideo => ProtoDeviceType::CAST_AUDIO, + DeviceType::Automobile => ProtoDeviceType::AUTOMOBILE, + DeviceType::Smartwatch => ProtoDeviceType::SMARTWATCH, + DeviceType::Chromebook => ProtoDeviceType::CHROMEBOOK, + DeviceType::UnknownSpotify => ProtoDeviceType::UNKNOWN_SPOTIFY, + DeviceType::CarThing => ProtoDeviceType::CAR_THING, + DeviceType::Observer => ProtoDeviceType::OBSERVER, + DeviceType::HomeThing => ProtoDeviceType::HOME_THING, + } + } +} diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index c94b1fb7..0d1da46c 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -233,8 +233,8 @@ where Ok(message) } -async fn read_into_accumulator<'a, 'b, T: AsyncRead + Unpin>( - connection: &'a mut T, +async fn read_into_accumulator<'b, T: AsyncRead + Unpin>( + connection: &mut T, size: usize, acc: &'b mut Vec, ) -> io::Result<&'b mut [u8]> { diff --git a/core/src/dealer/manager.rs b/core/src/dealer/manager.rs new file mode 100644 index 00000000..792deca3 --- /dev/null +++ b/core/src/dealer/manager.rs @@ -0,0 +1,174 @@ +use futures_core::Stream; +use futures_util::StreamExt; +use std::{cell::OnceCell, pin::Pin, str::FromStr}; +use thiserror::Error; +use tokio::sync::mpsc; +use tokio_stream::wrappers::UnboundedReceiverStream; +use url::Url; + +use super::{ + protocol::Message, Builder, Dealer, GetUrlResult, Request, RequestHandler, Responder, Response, + Subscription, +}; +use crate::{Error, Session}; + +component! { + DealerManager: DealerManagerInner { + builder: OnceCell = OnceCell::from(Builder::new()), + dealer: OnceCell = OnceCell::new(), + } +} + +pub type BoxedStream = Pin + Send>>; +pub type BoxedStreamResult = BoxedStream>; + +#[derive(Error, Debug)] +enum DealerError { + #[error("Builder wasn't available")] + BuilderNotAvailable, + #[error("Websocket couldn't be started because: {0}")] + LaunchFailure(Error), + #[error("Failed to set dealer")] + CouldNotSetDealer, +} + +impl From for Error { + fn from(err: DealerError) -> Self { + Error::failed_precondition(err) + } +} + +#[derive(Debug)] +pub enum Reply { + Success, + Failure, + Unanswered, +} + +pub type RequestReply = (Request, mpsc::UnboundedSender); +type RequestReceiver = mpsc::UnboundedReceiver; +type RequestSender = mpsc::UnboundedSender; + +struct DealerRequestHandler(RequestSender); + +impl DealerRequestHandler { + pub fn new() -> (Self, RequestReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + (DealerRequestHandler(tx), rx) + } +} + +impl RequestHandler for DealerRequestHandler { + fn handle_request(&self, request: Request, responder: Responder) { + let (tx, mut rx) = mpsc::unbounded_channel(); + + if let Err(why) = self.0.send((request, tx)) { + error!("failed sending dealer request {why}"); + responder.send(Response { success: false }); + return; + } + + tokio::spawn(async move { + let reply = rx.recv().await.unwrap_or(Reply::Failure); + debug!("replying to ws request: {reply:?}"); + match reply { + Reply::Unanswered => responder.force_unanswered(), + Reply::Success | Reply::Failure => responder.send(Response { + success: matches!(reply, Reply::Success), + }), + } + }); + } +} + +impl DealerManager { + async fn get_url(session: Session) -> GetUrlResult { + let (host, port) = session.apresolver().resolve("dealer").await?; + let token = session.login5().auth_token().await?.access_token; + let url = format!("wss://{host}:{port}/?access_token={token}"); + let url = Url::from_str(&url)?; + Ok(url) + } + + pub fn add_listen_for(&self, url: impl Into) -> Result { + let url = url.into(); + self.lock(|inner| { + if let Some(dealer) = inner.dealer.get() { + dealer.subscribe(&[&url]) + } else if let Some(builder) = inner.builder.get_mut() { + builder.subscribe(&[&url]) + } else { + Err(DealerError::BuilderNotAvailable.into()) + } + }) + } + + pub fn listen_for( + &self, + uri: impl Into, + t: impl Fn(Message) -> Result + Send + 'static, + ) -> Result, Error> { + Ok(Box::pin(self.add_listen_for(uri)?.map(t))) + } + + pub fn add_handle_for(&self, url: impl Into) -> Result { + let url = url.into(); + + let (handler, receiver) = DealerRequestHandler::new(); + self.lock(|inner| { + if let Some(dealer) = inner.dealer.get() { + dealer.add_handler(&url, handler).map(|_| receiver) + } else if let Some(builder) = inner.builder.get_mut() { + builder.add_handler(&url, handler).map(|_| receiver) + } else { + Err(DealerError::BuilderNotAvailable.into()) + } + }) + } + + pub fn handle_for(&self, uri: impl Into) -> Result, Error> { + Ok(Box::pin( + self.add_handle_for(uri).map(UnboundedReceiverStream::new)?, + )) + } + + pub fn handles(&self, uri: &str) -> bool { + self.lock(|inner| { + if let Some(dealer) = inner.dealer.get() { + dealer.handles(uri) + } else if let Some(builder) = inner.builder.get() { + builder.handles(uri) + } else { + false + } + }) + } + + pub async fn start(&self) -> Result<(), Error> { + debug!("Launching dealer"); + + let session = self.session(); + // the url has to be a function that can retrieve a new url, + // otherwise when we later try to reconnect with the initial url/token + // and the token is expired we will just get 401 error + let get_url = move || Self::get_url(session.clone()); + + let dealer = self + .lock(move |inner| inner.builder.take()) + .ok_or(DealerError::BuilderNotAvailable)? + .launch(get_url, None) + .await + .map_err(DealerError::LaunchFailure)?; + + self.lock(|inner| inner.dealer.set(dealer)) + .map_err(|_| DealerError::CouldNotSetDealer)?; + + Ok(()) + } + + pub async fn close(&self) { + if let Some(dealer) = self.lock(|inner| inner.dealer.take()) { + dealer.close().await + } + } +} diff --git a/core/src/dealer/maps.rs b/core/src/dealer/maps.rs index 4f719de7..23d21a11 100644 --- a/core/src/dealer/maps.rs +++ b/core/src/dealer/maps.rs @@ -1,8 +1,7 @@ use std::collections::HashMap; -use thiserror::Error; - use crate::Error; +use thiserror::Error; #[derive(Debug, Error)] pub enum HandlerMapError { @@ -28,6 +27,10 @@ impl Default for HandlerMap { } impl HandlerMap { + pub fn contains(&self, path: &str) -> bool { + matches!(self, HandlerMap::Branch(map) if map.contains_key(path)) + } + pub fn insert<'a>( &mut self, mut path: impl Iterator, @@ -107,6 +110,22 @@ impl SubscriberMap { } } + pub fn contains<'a>(&self, mut path: impl Iterator) -> bool { + if !self.subscribed.is_empty() { + return true; + } + + if let Some(next) = path.next() { + if let Some(next_map) = self.children.get(next) { + return next_map.contains(path); + } + } else { + return !self.is_empty(); + } + + false + } + pub fn is_empty(&self) -> bool { self.children.is_empty() && self.subscribed.is_empty() } @@ -115,16 +134,22 @@ impl SubscriberMap { &mut self, mut path: impl Iterator, fun: &mut impl FnMut(&T) -> bool, - ) { - self.subscribed.retain(|x| fun(x)); + ) -> bool { + let mut handled_by_any = false; + self.subscribed.retain(|x| { + handled_by_any = true; + fun(x) + }); if let Some(next) = path.next() { if let Some(y) = self.children.get_mut(next) { - y.retain(path, fun); + handled_by_any = handled_by_any || y.retain(path, fun); if y.is_empty() { self.children.remove(next); } } } + + handled_by_any } } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 8969f317..d5bff5b1 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -1,3 +1,4 @@ +pub mod manager; mod maps; pub mod protocol; @@ -28,8 +29,10 @@ use tokio_tungstenite::tungstenite; use tungstenite::error::UrlError; use url::Url; -use self::maps::*; -use self::protocol::*; +use self::{ + maps::*, + protocol::{Message, MessageOrRequest, Request, WebsocketMessage, WebsocketRequest}, +}; use crate::{ socket, @@ -39,7 +42,14 @@ use crate::{ type WsMessage = tungstenite::Message; type WsError = tungstenite::Error; -type WsResult = Result; +type WsResult = Result; +type GetUrlResult = Result; + +impl From for Error { + fn from(err: WsError) -> Self { + Error::failed_precondition(err) + } +} const WEBSOCKET_CLOSE_TIMEOUT: Duration = Duration::from_secs(3); @@ -48,11 +58,11 @@ const PING_TIMEOUT: Duration = Duration::from_secs(3); const RECONNECT_INTERVAL: Duration = Duration::from_secs(10); -pub struct Response { +struct Response { pub success: bool, } -pub struct Responder { +struct Responder { key: String, tx: mpsc::UnboundedSender, sent: bool, @@ -101,7 +111,7 @@ impl Drop for Responder { } } -pub trait IntoResponse { +trait IntoResponse { fn respond(self, responder: Responder); } @@ -132,7 +142,7 @@ where } } -pub trait RequestHandler: Send + 'static { +trait RequestHandler: Send + 'static { fn handle_request(&self, request: Request, responder: Responder); } @@ -156,8 +166,10 @@ impl Stream for Subscription { 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:") { + } else if let Some(rest) = s.strip_prefix("spotify:") { ("spotify", ':', rest) + } else if s.contains('/') { + ("", '/', s) } else { return None; }; @@ -169,7 +181,7 @@ fn split_uri(s: &str) -> Option> { } #[derive(Debug, Clone, Error)] -pub enum AddHandlerError { +enum AddHandlerError { #[error("There is already a handler for the given uri")] AlreadyHandled, #[error("The specified uri {0} is invalid")] @@ -186,7 +198,7 @@ impl From for Error { } #[derive(Debug, Clone, Error)] -pub enum SubscriptionError { +enum SubscriptionError { #[error("The specified uri is invalid")] InvalidUri(String), } @@ -224,8 +236,23 @@ fn subscribe( Ok(Subscription(rx)) } +fn handles( + req_map: &HandlerMap>, + msg_map: &SubscriberMap, + uri: &str, +) -> bool { + if req_map.contains(uri) { + return true; + } + + match split_uri(uri) { + None => false, + Some(mut split) => msg_map.contains(&mut split), + } +} + #[derive(Default)] -pub struct Builder { +struct Builder { message_handlers: SubscriberMap, request_handlers: HandlerMap>, } @@ -267,22 +294,26 @@ impl Builder { subscribe(&mut self.message_handlers, uris) } + pub fn handles(&self, uri: &str) -> bool { + handles(&self.request_handlers, &self.message_handlers, uri) + } + pub fn launch_in_background(self, get_url: F, proxy: Option) -> Dealer where - Fut: Future + Send + 'static, - F: (FnMut() -> Fut) + Send + 'static, + Fut: Future + Send + 'static, + F: (Fn() -> 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 + pub async fn launch(self, get_url: F, proxy: Option) -> WsResult where - Fut: Future + Send + 'static, - F: (FnMut() -> Fut) + Send + 'static, + Fut: Future + Send + 'static, + F: (Fn() -> Fut) + Send + 'static, { let dealer = create_dealer!(self, shared -> { // Try to connect. - let url = get_url().await; + let url = get_url().await?; let tasks = connect(&url, proxy.as_ref(), &shared).await?; // If a connection is established, continue in a background task. @@ -303,15 +334,47 @@ struct DealerShared { } impl DealerShared { - fn dispatch_message(&self, msg: Message) { + fn dispatch_message(&self, mut msg: WebsocketMessage) { + let msg = match msg.handle_payload() { + Ok(value) => Message { + headers: msg.headers, + payload: value, + uri: msg.uri, + }, + Err(why) => { + warn!("failure during data parsing for {}: {why}", msg.uri); + return; + } + }; + if let Some(split) = split_uri(&msg.uri) { - self.message_handlers + if self + .message_handlers .lock() - .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()); + .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()) + { + return; + } } + + warn!("No subscriber for msg.uri: {}", msg.uri); } - fn dispatch_request(&self, request: Request, send_tx: &mpsc::UnboundedSender) { + fn dispatch_request( + &self, + request: WebsocketRequest, + send_tx: &mpsc::UnboundedSender, + ) { + trace!("dealer request {}", &request.message_ident); + + let payload_request = match request.handle_payload() { + Ok(payload) => payload, + Err(why) => { + warn!("request payload handling failed because of {why}"); + return; + } + }; + // ResponseSender will automatically send "success: false" if it is dropped without an answer. let responder = Responder::new(request.key.clone(), send_tx.clone()); @@ -325,13 +388,11 @@ impl DealerShared { return; }; - { - let handler_map = self.request_handlers.lock(); + let handler_map = self.request_handlers.lock(); - if let Some(handler) = handler_map.get(split) { - handler.handle_request(request, responder); - return; - } + if let Some(handler) = handler_map.get(split) { + handler.handle_request(payload_request, responder); + return; } warn!("No handler for message_ident: {}", &request.message_ident); @@ -355,9 +416,9 @@ impl DealerShared { } } -pub struct Dealer { +struct Dealer { shared: Arc, - handle: TimeoutOnDrop<()>, + handle: TimeoutOnDrop>, } impl Dealer { @@ -376,6 +437,14 @@ impl Dealer { subscribe(&mut self.shared.message_handlers.lock(), uris) } + pub fn handles(&self, uri: &str) -> bool { + handles( + &self.shared.request_handlers.lock(), + &self.shared.message_handlers.lock(), + uri, + ) + } + pub async fn close(mut self) { debug!("closing dealer"); @@ -402,7 +471,7 @@ async fn connect( let default_port = match address.scheme() { "ws" => 80, "wss" => 443, - _ => return Err(WsError::Url(UrlError::UnsupportedUrlScheme)), + _ => return Err(WsError::Url(UrlError::UnsupportedUrlScheme).into()), }; let port = address.port().unwrap_or(default_port); @@ -484,13 +553,13 @@ async fn connect( 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), + Err(e) => warn!("Message couldn't be parsed: {e}. Message was {t}"), }, WsMessage::Binary(_) => { info!("Received invalid binary message"); } WsMessage::Pong(_) => { - debug!("Received pong"); + trace!("Received pong"); pong_received.store(true, atomic::Ordering::Relaxed); } _ => (), // tungstenite handles Close and Ping automatically @@ -522,7 +591,7 @@ async fn connect( break; } - debug!("Sent ping"); + trace!("Sent ping"); sleep(PING_TIMEOUT).await; @@ -556,8 +625,9 @@ async fn run( initial_tasks: Option<(JoinHandle<()>, JoinHandle<()>)>, mut get_url: F, proxy: Option, -) where - Fut: Future + Send + 'static, +) -> Result<(), Error> +where + Fut: Future + Send + 'static, F: (FnMut() -> Fut) + Send + 'static, { let init_task = |t| Some(TimeoutOnDrop::new(t, WEBSOCKET_CLOSE_TIMEOUT)); @@ -593,7 +663,7 @@ async fn run( break }, e = get_url() => e - }; + }?; match connect(&url, proxy.as_ref(), &shared).await { Ok((s, r)) => tasks = (init_task(s), init_task(r)), @@ -609,4 +679,6 @@ async fn run( let tasks = tasks.0.into_iter().chain(tasks.1); let _ = join_all(tasks).await; + + Ok(()) } diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs index 9e62a2e5..e6b7f2dc 100644 --- a/core/src/dealer/protocol.rs +++ b/core/src/dealer/protocol.rs @@ -1,19 +1,58 @@ +pub mod request; + +pub use request::*; + use std::collections::HashMap; +use std::io::{Error as IoError, Read}; +use crate::Error; +use base64::prelude::BASE64_STANDARD; +use base64::{DecodeError, Engine}; +use flate2::read::GzDecoder; +use log::LevelFilter; use serde::Deserialize; +use serde_json::Error as SerdeError; +use thiserror::Error; -pub type JsonValue = serde_json::Value; -pub type JsonObject = serde_json::Map; +const IGNORE_UNKNOWN: protobuf_json_mapping::ParseOptions = protobuf_json_mapping::ParseOptions { + ignore_unknown_fields: true, + _future_options: (), +}; -#[derive(Clone, Debug, Deserialize)] -pub struct Payload { - pub message_id: i32, - pub sent_by_device_id: String, - pub command: JsonObject, +type JsonValue = serde_json::Value; + +#[derive(Debug, Error)] +enum ProtocolError { + #[error("base64 decoding failed: {0}")] + Base64(DecodeError), + #[error("gzip decoding failed: {0}")] + GZip(IoError), + #[error("deserialization failed: {0}")] + Deserialization(SerdeError), + #[error("payload had more then one value. had {0} values")] + MoreThenOneValue(usize), + #[error("received unexpected data {0:#?}")] + UnexpectedData(PayloadValue), + #[error("payload was empty")] + Empty, +} + +impl From for Error { + fn from(err: ProtocolError) -> Self { + match err { + ProtocolError::UnexpectedData(_) => Error::unavailable(err), + _ => Error::failed_precondition(err), + } + } } #[derive(Clone, Debug, Deserialize)] -pub struct Request { +pub(super) struct Payload { + pub compressed: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub(super) struct WebsocketRequest { #[serde(default)] pub headers: HashMap, pub message_ident: String, @@ -22,18 +61,133 @@ pub struct Request { } #[derive(Clone, Debug, Deserialize)] -pub struct Message { +pub(super) struct WebsocketMessage { #[serde(default)] pub headers: HashMap, pub method: Option, #[serde(default)] - pub payloads: Vec, + pub payloads: Vec, pub uri: String, } +#[derive(Clone, Debug, Deserialize)] +#[serde(untagged)] +pub enum MessagePayloadValue { + String(String), + Bytes(Vec), + Json(JsonValue), +} + #[derive(Clone, Debug, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub(super) enum MessageOrRequest { - Message(Message), - Request(Request), + Message(WebsocketMessage), + Request(WebsocketRequest), +} + +#[derive(Clone, Debug)] +pub enum PayloadValue { + Empty, + Raw(Vec), + Json(String), +} + +#[derive(Clone, Debug)] +pub struct Message { + pub headers: HashMap, + pub payload: PayloadValue, + pub uri: String, +} + +impl Message { + pub fn from_json(value: Self) -> Result { + use protobuf_json_mapping::*; + match value.payload { + PayloadValue::Json(json) => match parse_from_str::(&json) { + Ok(message) => Ok(message), + Err(_) => match parse_from_str_with_options(&json, &IGNORE_UNKNOWN) { + Ok(message) => Ok(message), + Err(why) => Err(Error::failed_precondition(why)), + }, + }, + other => Err(ProtocolError::UnexpectedData(other).into()), + } + } + + pub fn from_raw(value: Self) -> Result { + match value.payload { + PayloadValue::Raw(bytes) => { + M::parse_from_bytes(&bytes).map_err(Error::failed_precondition) + } + other => Err(ProtocolError::UnexpectedData(other).into()), + } + } +} + +impl WebsocketMessage { + pub fn handle_payload(&mut self) -> Result { + if self.payloads.is_empty() { + return Ok(PayloadValue::Empty); + } else if self.payloads.len() > 1 { + return Err(ProtocolError::MoreThenOneValue(self.payloads.len()).into()); + } + + let payload = self.payloads.pop().ok_or(ProtocolError::Empty)?; + let bytes = match payload { + MessagePayloadValue::String(string) => BASE64_STANDARD + .decode(string) + .map_err(ProtocolError::Base64)?, + MessagePayloadValue::Bytes(bytes) => bytes, + MessagePayloadValue::Json(json) => return Ok(PayloadValue::Json(json.to_string())), + }; + + handle_transfer_encoding(&self.headers, bytes).map(PayloadValue::Raw) + } +} + +impl WebsocketRequest { + pub fn handle_payload(&self) -> Result { + let payload_bytes = BASE64_STANDARD + .decode(&self.payload.compressed) + .map_err(ProtocolError::Base64)?; + + let payload = handle_transfer_encoding(&self.headers, payload_bytes)?; + let payload = String::from_utf8(payload)?; + + if log::max_level() >= LevelFilter::Trace { + if let Ok(json) = serde_json::from_str::(&payload) { + trace!("websocket request: {json:#?}"); + } else { + trace!("websocket request: {payload}"); + } + } + + serde_json::from_str(&payload) + .map_err(ProtocolError::Deserialization) + .map_err(Into::into) + } +} + +fn handle_transfer_encoding( + headers: &HashMap, + data: Vec, +) -> Result, Error> { + let encoding = headers.get("Transfer-Encoding").map(String::as_str); + if let Some(encoding) = encoding { + trace!("message was send with {encoding} encoding "); + } + + if !matches!(encoding, Some("gzip")) { + return Ok(data); + } + + let mut gz = GzDecoder::new(&data[..]); + let mut bytes = vec![]; + match gz.read_to_end(&mut bytes) { + Ok(i) if i == bytes.len() => Ok(bytes), + Ok(_) => Err(Error::failed_precondition( + "read bytes mismatched with expected bytes", + )), + Err(why) => Err(ProtocolError::GZip(why).into()), + } } diff --git a/core/src/dealer/protocol/request.rs b/core/src/dealer/protocol/request.rs new file mode 100644 index 00000000..4d796469 --- /dev/null +++ b/core/src/dealer/protocol/request.rs @@ -0,0 +1,208 @@ +use crate::deserialize_with::*; +use librespot_protocol::player::{ + Context, ContextPlayerOptionOverrides, PlayOrigin, ProvidedTrack, TransferState, +}; +use serde::Deserialize; +use serde_json::Value; +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Debug, Deserialize)] +pub struct Request { + pub message_id: u32, + // todo: did only send target_alias_id: null so far, maybe we just ignore it, will see + // pub target_alias_id: Option<()>, + pub sent_by_device_id: String, + pub command: Command, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(tag = "endpoint", rename_all = "snake_case")] +pub enum Command { + Transfer(TransferCommand), + #[serde(deserialize_with = "boxed")] + Play(Box), + Pause(PauseCommand), + SeekTo(SeekToCommand), + SetShufflingContext(SetValueCommand), + SetRepeatingTrack(SetValueCommand), + SetRepeatingContext(SetValueCommand), + AddToQueue(AddToQueueCommand), + SetQueue(SetQueueCommand), + SetOptions(SetOptionsCommand), + UpdateContext(UpdateContextCommand), + SkipNext(SkipNextCommand), + // commands that don't send any context (at least not usually...) + SkipPrev(GenericCommand), + Resume(GenericCommand), + // catch unknown commands, so that we can implement them later + #[serde(untagged)] + Unknown(Value), +} + +impl Display for Command { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + use Command::*; + + write!( + f, + "endpoint: {}{}", + matches!(self, Unknown(_)) + .then_some("unknown ") + .unwrap_or_default(), + match self { + Transfer(_) => "transfer", + Play(_) => "play", + Pause(_) => "pause", + SeekTo(_) => "seek_to", + SetShufflingContext(_) => "set_shuffling_context", + SetRepeatingContext(_) => "set_repeating_context", + SetRepeatingTrack(_) => "set_repeating_track", + AddToQueue(_) => "add_to_queue", + SetQueue(_) => "set_queue", + SetOptions(_) => "set_options", + UpdateContext(_) => "update_context", + SkipNext(_) => "skip_next", + SkipPrev(_) => "skip_prev", + Resume(_) => "resume", + Unknown(json) => { + json.as_object() + .and_then(|obj| obj.get("endpoint").map(|v| v.as_str())) + .flatten() + .unwrap_or("???") + } + } + ) + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct TransferCommand { + #[serde(default, deserialize_with = "base64_proto")] + pub data: Option, + pub options: TransferOptions, + pub from_device_identifier: String, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct PlayCommand { + #[serde(deserialize_with = "json_proto")] + pub context: Context, + #[serde(deserialize_with = "json_proto")] + pub play_origin: PlayOrigin, + pub options: PlayOptions, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct PauseCommand { + // does send options with it, but seems to be empty, investigate which options are send here + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SeekToCommand { + pub value: u32, + pub position: u32, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SkipNextCommand { + #[serde(default, deserialize_with = "option_json_proto")] + pub track: Option, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SetValueCommand { + pub value: bool, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct AddToQueueCommand { + #[serde(deserialize_with = "json_proto")] + pub track: ProvidedTrack, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SetQueueCommand { + #[serde(deserialize_with = "vec_json_proto")] + pub next_tracks: Vec, + #[serde(deserialize_with = "vec_json_proto")] + pub prev_tracks: Vec, + // this queue revision is actually the last revision, so using it will not update the web ui + // might be that internally they use the last revision to create the next revision + pub queue_revision: String, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SetOptionsCommand { + pub shuffling_context: Option, + pub repeating_context: Option, + pub repeating_track: Option, + pub options: Option, + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct UpdateContextCommand { + #[serde(deserialize_with = "json_proto")] + pub context: Context, + pub session_id: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct GenericCommand { + pub logging_params: LoggingParams, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct TransferOptions { + pub restore_paused: String, + pub restore_position: String, + pub restore_track: String, + pub retain_session: String, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct PlayOptions { + pub skip_to: SkipTo, + #[serde(default, deserialize_with = "option_json_proto")] + pub player_options_override: Option, + pub license: Option, + // possible to send wie web-api + pub seek_to: Option, + // mobile + pub always_play_something: Option, + pub audio_stream: Option, + pub initially_paused: Option, + pub prefetch_level: Option, + pub system_initiated: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct OptionsOptions { + only_for_local_device: bool, + override_restrictions: bool, + system_initiated: bool, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct SkipTo { + pub track_uid: Option, + pub track_uri: Option, + pub track_index: Option, +} + +#[derive(Clone, Debug, Deserialize)] +pub struct LoggingParams { + pub interaction_ids: Option>, + pub device_identifier: Option, + pub command_initiated_time: Option, + pub page_instance_ids: Option>, + pub command_id: Option, +} diff --git a/core/src/deserialize_with.rs b/core/src/deserialize_with.rs new file mode 100644 index 00000000..11687f9b --- /dev/null +++ b/core/src/deserialize_with.rs @@ -0,0 +1,94 @@ +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use protobuf::MessageFull; +use serde::de::{Error, Unexpected}; +use serde::{Deserialize, Deserializer}; +use serde_json::Value; + +const IGNORE_UNKNOWN: protobuf_json_mapping::ParseOptions = protobuf_json_mapping::ParseOptions { + ignore_unknown_fields: true, + _future_options: (), +}; + +fn parse_value_to_msg( + value: &Value, +) -> Result { + protobuf_json_mapping::parse_from_str_with_options::(&value.to_string(), &IGNORE_UNKNOWN) +} + +pub fn base64_proto<'de, T, D>(de: D) -> Result, D::Error> +where + T: MessageFull, + D: Deserializer<'de>, +{ + let v: String = Deserialize::deserialize(de)?; + let bytes = BASE64_STANDARD + .decode(v) + .map_err(|e| Error::custom(e.to_string()))?; + + T::parse_from_bytes(&bytes).map(Some).map_err(Error::custom) +} + +pub fn json_proto<'de, T, D>(de: D) -> Result +where + T: MessageFull, + D: Deserializer<'de>, +{ + let v: Value = Deserialize::deserialize(de)?; + parse_value_to_msg(&v).map_err(|why| { + warn!("deserialize_json_proto: {v}"); + error!("deserialize_json_proto: {why}"); + Error::custom(why) + }) +} + +pub fn option_json_proto<'de, T, D>(de: D) -> Result, D::Error> +where + T: MessageFull, + D: Deserializer<'de>, +{ + let v: Value = Deserialize::deserialize(de)?; + parse_value_to_msg(&v).map(Some).map_err(Error::custom) +} + +pub fn vec_json_proto<'de, T, D>(de: D) -> Result, D::Error> +where + T: MessageFull, + D: Deserializer<'de>, +{ + let v: Value = Deserialize::deserialize(de)?; + let array = match v { + Value::Array(array) => array, + _ => return Err(Error::custom("the value wasn't an array")), + }; + + let res = array + .iter() + .flat_map(parse_value_to_msg) + .collect::>(); + + Ok(res) +} + +pub fn boxed<'de, T, D>(de: D) -> Result, D::Error> +where + T: Deserialize<'de>, + D: Deserializer<'de>, +{ + let v: T = Deserialize::deserialize(de)?; + Ok(Box::new(v)) +} + +pub fn bool_from_string<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + match String::deserialize(de)?.as_ref() { + "true" => Ok(true), + "false" => Ok(false), + other => Err(Error::invalid_value( + Unexpected::Str(other), + &"true or false", + )), + } +} diff --git a/core/src/error.rs b/core/src/error.rs index 6b0178c9..0ae2e74f 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -499,3 +499,9 @@ impl From for Error { Self::new(ErrorKind::FailedPrecondition, err) } } + +impl From for Error { + fn from(err: protobuf_json_mapping::ParseError) -> Self { + Self::failed_precondition(err) + } +} diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 88d41c92..0b932ad9 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -208,7 +208,7 @@ impl HttpClient { } } - if code != StatusCode::OK { + if !code.is_success() { return Err(HttpClientError::StatusCode(code).into()); } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 9894bb70..f2d6587e 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -16,7 +16,8 @@ pub mod config; mod connection; pub mod date; #[allow(dead_code)] -mod dealer; +pub mod dealer; +pub mod deserialize_with; #[doc(hidden)] pub mod diffie_hellman; pub mod error; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 7fde9b7f..76b060a3 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -276,12 +276,15 @@ impl MercuryManager { }); }); - if !found { + if found { + Ok(()) + } else if self.session().dealer().handles(&response.uri) { + trace!("mercury response <{}> is handled by dealer", response.uri); + Ok(()) + } else { 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)?; diff --git a/core/src/session.rs b/core/src/session.rs index defdf61b..45d54bc6 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -9,23 +9,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; -use byteorder::{BigEndian, ByteOrder}; -use bytes::Bytes; -use futures_core::TryStream; -use futures_util::StreamExt; -use librespot_protocol::authentication::AuthenticationType; -use num_traits::FromPrimitive; -use once_cell::sync::OnceCell; -use parking_lot::RwLock; -use pin_project_lite::pin_project; -use quick_xml::events::Event; -use thiserror::Error; -use tokio::{ - sync::mpsc, - time::{sleep, Duration as TokioDuration, Instant as TokioInstant, Sleep}, -}; -use tokio_stream::wrappers::UnboundedReceiverStream; - +use crate::dealer::manager::DealerManager; use crate::{ apresolve::{ApResolver, SocketAddress}, audio_key::AudioKeyManager, @@ -43,6 +27,23 @@ use crate::{ token::TokenProvider, Error, }; +use byteorder::{BigEndian, ByteOrder}; +use bytes::Bytes; +use futures_core::TryStream; +use futures_util::StreamExt; +use librespot_protocol::authentication::AuthenticationType; +use num_traits::FromPrimitive; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use pin_project_lite::pin_project; +use quick_xml::events::Event; +use thiserror::Error; +use tokio::{ + sync::mpsc, + time::{sleep, Duration as TokioDuration, Instant as TokioInstant, Sleep}, +}; +use tokio_stream::wrappers::UnboundedReceiverStream; +use uuid::Uuid; #[derive(Debug, Error)] pub enum SessionError { @@ -78,6 +79,7 @@ pub struct UserData { #[derive(Debug, Clone, Default)] struct SessionData { + session_id: String, client_id: String, client_name: String, client_brand_name: String, @@ -100,6 +102,7 @@ struct SessionInternal { audio_key: OnceCell, channel: OnceCell, mercury: OnceCell, + dealer: OnceCell, spclient: OnceCell, token_provider: OnceCell, login5: OnceCell, @@ -128,6 +131,8 @@ impl Session { let session_data = SessionData { client_id: config.client_id.clone(), + // can be any guid, doesn't need to be simple + session_id: Uuid::new_v4().as_simple().to_string(), ..SessionData::default() }; @@ -141,6 +146,7 @@ impl Session { audio_key: OnceCell::new(), channel: OnceCell::new(), mercury: OnceCell::new(), + dealer: OnceCell::new(), spclient: OnceCell::new(), token_provider: OnceCell::new(), login5: OnceCell::new(), @@ -303,6 +309,12 @@ impl Session { .get_or_init(|| MercuryManager::new(self.weak())) } + pub fn dealer(&self) -> &DealerManager { + self.0 + .dealer + .get_or_init(|| DealerManager::new(self.weak())) + } + pub fn spclient(&self) -> &SpClient { self.0.spclient.get_or_init(|| SpClient::new(self.weak())) } @@ -373,6 +385,14 @@ impl Session { self.0.data.read().user_data.clone() } + pub fn session_id(&self) -> String { + self.0.data.read().session_id.clone() + } + + pub fn set_session_id(&self, session_id: String) { + self.0.data.write().session_id = session_id.to_owned(); + } + pub fn device_id(&self) -> &str { &self.config().device_id } diff --git a/core/src/spclient.rs b/core/src/spclient.rs index a23b52f0..c818570a 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -3,20 +3,6 @@ use std::{ time::{Duration, Instant}, }; -use bytes::Bytes; -use data_encoding::HEXUPPER_PERMISSIVE; -use futures_util::future::IntoStream; -use http::header::HeaderValue; -use hyper::{ - header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, - HeaderMap, Method, Request, -}; -use hyper_util::client::legacy::ResponseFuture; -use protobuf::{Enum, Message, MessageFull}; -use rand::RngCore; -use sysinfo::System; -use thiserror::Error; - use crate::config::{os_version, OS}; use crate::{ apresolve::SocketAddress, @@ -37,6 +23,20 @@ use crate::{ version::spotify_semantic_version, Error, FileId, SpotifyId, }; +use bytes::Bytes; +use data_encoding::HEXUPPER_PERMISSIVE; +use futures_util::future::IntoStream; +use http::header::HeaderValue; +use hyper::{ + header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, + HeaderMap, Method, Request, +}; +use hyper_util::client::legacy::ResponseFuture; +use librespot_protocol::{autoplay_context_request::AutoplayContextRequest, player::Context}; +use protobuf::{Enum, Message, MessageFull}; +use rand::RngCore; +use sysinfo::System; +use thiserror::Error; component! { SpClient : SpClientInner { @@ -50,11 +50,20 @@ pub type SpClientResult = Result; #[allow(clippy::declare_interior_mutable_const)] pub const CLIENT_TOKEN: HeaderName = HeaderName::from_static("client-token"); +#[allow(clippy::declare_interior_mutable_const)] +const CONNECTION_ID: HeaderName = HeaderName::from_static("x-spotify-connection-id"); + +const NO_METRICS_AND_SALT: RequestOptions = RequestOptions { + metrics: false, + salt: false, +}; #[derive(Debug, Error)] pub enum SpClientError { #[error("missing attribute {0}")] Attribute(String), + #[error("expected data but received none")] + NoData, } impl From for Error { @@ -75,6 +84,20 @@ impl Default for RequestStrategy { } } +pub struct RequestOptions { + metrics: bool, + salt: bool, +} + +impl Default for RequestOptions { + fn default() -> Self { + Self { + metrics: true, + salt: true, + } + } +} + impl SpClient { pub fn set_strategy(&self, strategy: RequestStrategy) { self.lock(|inner| inner.strategy = strategy) @@ -354,7 +377,25 @@ impl SpClient { headers: Option, message: &M, ) -> SpClientResult { - let body = protobuf::text_format::print_to_string(message); + self.request_with_protobuf_and_options( + method, + endpoint, + headers, + message, + &Default::default(), + ) + .await + } + + pub async fn request_with_protobuf_and_options( + &self, + method: &Method, + endpoint: &str, + headers: Option, + message: &M, + options: &RequestOptions, + ) -> SpClientResult { + let body = message.write_to_bytes()?; let mut headers = headers.unwrap_or_default(); headers.insert( @@ -362,7 +403,7 @@ impl SpClient { HeaderValue::from_static("application/x-protobuf"), ); - self.request(method, endpoint, Some(headers), Some(&body)) + self.request_with_options(method, endpoint, Some(headers), Some(&body), options) .await } @@ -376,7 +417,8 @@ impl SpClient { let mut headers = headers.unwrap_or_default(); headers.insert(ACCEPT, HeaderValue::from_static("application/json")); - self.request(method, endpoint, Some(headers), body).await + self.request(method, endpoint, Some(headers), body.map(|s| s.as_bytes())) + .await } pub async fn request( @@ -384,7 +426,19 @@ impl SpClient { method: &Method, endpoint: &str, headers: Option, - body: Option<&str>, + body: Option<&[u8]>, + ) -> SpClientResult { + self.request_with_options(method, endpoint, headers, body, &Default::default()) + .await + } + + pub async fn request_with_options( + &self, + method: &Method, + endpoint: &str, + headers: Option, + body: Option<&[u8]>, + options: &RequestOptions, ) -> SpClientResult { let mut tries: usize = 0; let mut last_response; @@ -399,31 +453,33 @@ impl SpClient { let mut url = self.base_url().await?; url.push_str(endpoint); - let separator = match url.find('?') { - Some(_) => "&", - None => "?", - }; - // 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. // For the sake of documentation you could also do "product=free" but // we only support premium anyway. - let _ = write!( - url, - "{}product=0&country={}", - separator, - self.session().country() - ); + if options.metrics && !url.contains("product=0") { + let _ = write!( + url, + "{}product=0&country={}", + util::get_next_query_separator(&url), + self.session().country() + ); + } // Defeat caches. Spotify-generated URLs already contain this. - if !url.contains("salt=") { - let _ = write!(url, "&salt={}", rand::thread_rng().next_u32()); + if options.salt && !url.contains("salt=") { + let _ = write!( + url, + "{}salt={}", + util::get_next_query_separator(&url), + rand::thread_rng().next_u32() + ); } let mut request = Request::builder() .method(method) .uri(url) - .body(body.to_owned().into())?; + .body(Bytes::copy_from_slice(body))?; // Reconnection logic: keep getting (cached) tokens because they might have expired. let token = self.session().login5().auth_token().await?; @@ -481,20 +537,34 @@ impl SpClient { last_response } - pub async fn put_connect_state( - &self, - connection_id: &str, - state: &PutStateRequest, - ) -> SpClientResult { + pub async fn put_connect_state_request(&self, state: &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()?); + headers.insert(CONNECTION_ID, self.session().connection_id().parse()?); self.request_with_protobuf(&Method::PUT, &endpoint, Some(headers), state) .await } + pub async fn delete_connect_state_request(&self) -> SpClientResult { + let endpoint = format!("/connect-state/v1/devices/{}", self.session().device_id()); + self.request(&Method::DELETE, &endpoint, None, None).await + } + + pub async fn put_connect_state_inactive(&self, notify: bool) -> SpClientResult { + let endpoint = format!( + "/connect-state/v1/devices/{}/inactive?notify={notify}", + self.session().device_id() + ); + + let mut headers = HeaderMap::new(); + headers.insert(CONNECTION_ID, self.session().connection_id().parse()?); + + self.request(&Method::PUT, &endpoint, Some(headers), None) + .await + } + pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult { let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); self.request(&Method::GET, &endpoint, None, None).await @@ -738,4 +808,76 @@ impl SpClient { self.request_url(&url).await } + + /// Request the context for an uri + /// + /// ## Query entry found in the wild: + /// - include_video=true + /// ## Remarks: + /// - track + /// - returns a single page with a single track + /// - when requesting a single track with a query in the request, the returned track uri + /// **will** contain the query + /// - artists + /// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album + /// - remaining pages are albums of the artists and are only provided as page_url + /// - search + /// - is massively influenced by the provided query + /// - the query result shown by the search expects no query at all + /// - uri looks like "spotify:search:never+gonna" + pub async fn get_context(&self, uri: &str) -> Result { + let uri = format!("/context-resolve/v1/{uri}"); + + let res = self + .request_with_options(&Method::GET, &uri, None, None, &NO_METRICS_AND_SALT) + .await?; + let ctx_json = String::from_utf8(res.to_vec())?; + if ctx_json.is_empty() { + Err(SpClientError::NoData)? + } + + let ctx = protobuf_json_mapping::parse_from_str::(&ctx_json); + + if ctx.is_err() { + trace!("failed parsing context: {ctx_json}") + } + + Ok(ctx?) + } + + pub async fn get_autoplay_context( + &self, + context_request: &AutoplayContextRequest, + ) -> Result { + let res = self + .request_with_protobuf_and_options( + &Method::POST, + "/context-resolve/v1/autoplay", + None, + context_request, + &NO_METRICS_AND_SALT, + ) + .await?; + + let ctx_json = String::from_utf8(res.to_vec())?; + if ctx_json.is_empty() { + Err(SpClientError::NoData)? + } + + let ctx = protobuf_json_mapping::parse_from_str::(&ctx_json); + + if ctx.is_err() { + trace!("failed parsing context: {ctx_json}") + } + + Ok(ctx?) + } + + pub async fn get_rootlist(&self, from: usize, length: Option) -> SpClientResult { + let length = length.unwrap_or(120); + let user = self.session().username(); + let endpoint = format!("/playlist/v2/user/{user}/rootlist?decorate=revision,attributes,length,owner,capabilities,status_code&from={from}&length={length}"); + + self.request(&Method::GET, &endpoint, None, None).await + } } diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 959b84ee..f7478f54 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -423,19 +423,6 @@ impl TryFrom<&Vec> for SpotifyId { } } -impl TryFrom<&protocol::spirc::TrackRef> for SpotifyId { - type Error = crate::Error; - fn try_from(track: &protocol::spirc::TrackRef) -> Result { - match SpotifyId::from_raw(track.gid()) { - Ok(mut id) => { - id.item_type = SpotifyItemType::Track; - Ok(id) - } - Err(_) => SpotifyId::from_uri(track.uri()), - } - } -} - impl TryFrom<&protocol::metadata::Album> for SpotifyId { type Error = crate::Error; fn try_from(album: &protocol::metadata::Album) -> Result { diff --git a/core/src/util.rs b/core/src/util.rs index 31cdd962..c48378af 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -165,3 +165,10 @@ pub fn solve_hash_cash( Ok(now.elapsed()) } + +pub fn get_next_query_separator(url: &str) -> &'static str { + match url.find('?') { + Some(_) => "&", + None => "?", + } +} diff --git a/core/src/version.rs b/core/src/version.rs index 3439662c..4fce65ad 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -25,6 +25,9 @@ pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.31.1205.g4d59ad7c"; /// The protocol version of the Spotify mobile app. pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; +/// The general spirc version +pub const SPOTIFY_SPIRC_VERSION: &str = "3.2.6"; + /// 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/docs/dealer.md b/docs/dealer.md new file mode 100644 index 00000000..24704214 --- /dev/null +++ b/docs/dealer.md @@ -0,0 +1,79 @@ +# Dealer + +When talking about the dealer, we are speaking about a websocket that represents the player as +spotify-connect device. The dealer is primarily used to receive updates and not to update the +state. + +## Messages and Requests + +There are two types of messages that are received via the dealer, Messages and Requests. +Messages are fire-and-forget and don't need a responses, while request expect a reply if the +request was processed successfully or failed. + +Because we publish our device with support for gzip, the message payload might be BASE64 encoded +and gzip compressed. If that is the case, the related headers send an entry for "Transfer-Encoding" +with the value of "gzip". + +### Messages + +Most messages librespot handles send bytes that can be easily converted into their respective +protobuf definition. Some outliers send json that can be usually mapped to an existing protobuf +definition. We use `protobuf-json-mapping` to a similar protobuf definition + +> Note: The json sometimes doesn't map exactly and can provide more fields than the protobuf +> definition expects. For messages, we usually ignore unknown fields. + +There are two types of messages, "informational" and "fire and forget commands". + +**Informational:** + +Informational messages send any changes done by the current user or of a client where the current user +is logged in. These messages contain for example changes to a own playlist, additions to the liked songs +or any update that a client sends. + +**Fire and Forget commands:** + +These are messages that send information that are requests to the current player. These are only send to +the active player. Volume update requests and the logout request are send as fire-forget-commands. + +### Requests + +The request payload is sent as json. There are almost usable protobuf definitions (see +files named like `es_(_request).proto`) for the commands, but they don't +align up with the expected values and are missing some major information we need for handling some +commands. Because of that we have our own model for the specific commands, see +[core/src/dealer/protocol/request.rs](../core/src/dealer/protocol/request.rs). + +All request modify the player-state. + +## Details + +This sections is for details and special hiccups in regards to handling that isn't completely intuitive. + +### UIDs + +A spotify item is identifiable by their uri. The `ContextTrack` and `ProvidedTrack` both have a `uid` +field. When we receive a context via the `context-resolver` it can return items (`ContextTrack`) that +may have their respective uid set. Some context like the collection and albums don't provide this +information. + +When a `uid` is missing, resorting the next tracks in an official client gets confused and sends +incorrect data via the `set_queue` request. To prevent this behavior we generate a uid for each +track that doesn't have an uid. Queue items become a "queue-uid" which is just a `q` with an +incrementing number. + +### Metadata + +For some client's (especially mobile) the metadata of a track is very important to display the +context correct. For example the "autoplay" metadata is relevant to display the correct context +info. + +Metadata can also be used to store data like the iteration when repeating a context. + +### Repeat + +The context repeating implementation is partly mimicked from the official client. The official +client allows skipping into negative iterations, this is currently not supported. + +Repeating is realized by filling the next tracks with multiple contexts separated by delimiters. +By that we only have to handle the delimiter when skipping to the next and previous track. diff --git a/examples/play_connect.rs b/examples/play_connect.rs index c46464fb..9a033da2 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -9,13 +9,13 @@ use librespot::{ player::Player, }, }; +use librespot_connect::spirc::PlayingTrack; use librespot_connect::{ - config::ConnectConfig, spirc::{Spirc, SpircLoadCommand}, + state::ConnectStateConfig, }; use librespot_metadata::{Album, Metadata}; use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig}; -use librespot_protocol::spirc::TrackRef; use std::env; use std::sync::Arc; use tokio::join; @@ -25,7 +25,7 @@ async fn main() { let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); - let connect_config = ConnectConfig::default(); + let connect_config = ConnectStateConfig::default(); let mut args: Vec<_> = env::args().collect(); let context_uri = if args.len() == 3 { @@ -64,14 +64,6 @@ async fn main() { let album = Album::get(&session, &SpotifyId::from_uri(&context_uri).unwrap()) .await .unwrap(); - let tracks = album - .tracks() - .map(|track_id| { - let mut track = TrackRef::new(); - track.set_gid(Vec::from(track_id.to_raw())); - track - }) - .collect(); println!( "Playing album: {} by {}", @@ -87,10 +79,12 @@ async fn main() { .load(SpircLoadCommand { context_uri, start_playing: true, + seek_to: 0, shuffle: false, repeat: false, - playing_track_index: 0, // the index specifies which track in the context starts playing, in this case the first in the album - tracks, + repeat_track: false, + // the index specifies which track in the context starts playing, in this case the first in the album + playing_track: PlayingTrack::Index(0), }) .unwrap(); }); diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index c44245cf..29fba7d9 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -94,7 +94,7 @@ impl<'a> Open for PortAudioSink<'a> { } } -impl<'a> Sink for PortAudioSink<'a> { +impl Sink for PortAudioSink<'_> { fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { (ref mut $stream: ident, ref $parameters: ident) => {{ @@ -175,12 +175,12 @@ impl<'a> Sink for PortAudioSink<'a> { } } -impl<'a> Drop for PortAudioSink<'a> { +impl Drop for PortAudioSink<'_> { fn drop(&mut self) { portaudio_rs::terminate().unwrap(); } } -impl<'a> PortAudioSink<'a> { +impl PortAudioSink<'_> { pub const NAME: &'static str = "portaudio"; } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 2632f54a..f63fdef0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -145,7 +145,7 @@ fn create_sink( }, Some(device_name) => { host.output_devices()? - .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails + .find(|d| d.name().ok().is_some_and(|name| name == device_name)) // Ignore devices for which getting name fails .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? } None => host diff --git a/playback/src/player.rs b/playback/src/player.rs index 43f63610..6a4170f0 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -123,7 +123,10 @@ enum PlayerCommand { }, EmitFilterExplicitContentChangedEvent(bool), EmitShuffleChangedEvent(bool), - EmitRepeatChangedEvent(bool), + EmitRepeatChangedEvent { + context: bool, + track: bool, + }, EmitAutoPlayChangedEvent(bool), } @@ -218,7 +221,8 @@ pub enum PlayerEvent { shuffle: bool, }, RepeatChanged { - repeat: bool, + context: bool, + track: bool, }, AutoPlayChanged { auto_play: bool, @@ -607,8 +611,8 @@ impl Player { self.command(PlayerCommand::EmitShuffleChangedEvent(shuffle)); } - pub fn emit_repeat_changed_event(&self, repeat: bool) { - self.command(PlayerCommand::EmitRepeatChangedEvent(repeat)); + pub fn emit_repeat_changed_event(&self, context: bool, track: bool) { + self.command(PlayerCommand::EmitRepeatChangedEvent { context, track }); } pub fn emit_auto_play_changed_event(&self, auto_play: bool) { @@ -2104,8 +2108,8 @@ impl PlayerInternal { self.send_event(PlayerEvent::VolumeChanged { volume }) } - PlayerCommand::EmitRepeatChangedEvent(repeat) => { - self.send_event(PlayerEvent::RepeatChanged { repeat }) + PlayerCommand::EmitRepeatChangedEvent { context, track } => { + self.send_event(PlayerEvent::RepeatChanged { context, track }) } PlayerCommand::EmitShuffleChangedEvent(shuffle) => { @@ -2336,9 +2340,10 @@ impl fmt::Debug for PlayerCommand { .debug_tuple("EmitShuffleChangedEvent") .field(&shuffle) .finish(), - PlayerCommand::EmitRepeatChangedEvent(repeat) => f + PlayerCommand::EmitRepeatChangedEvent { context, track } => f .debug_tuple("EmitRepeatChangedEvent") - .field(&repeat) + .field(&context) + .field(&track) .finish(), PlayerCommand::EmitAutoPlayChangedEvent(auto_play) => f .debug_tuple("EmitAutoPlayChangedEvent") diff --git a/protocol/build.rs b/protocol/build.rs index 8a0a8138..43971bc8 100644 --- a/protocol/build.rs +++ b/protocol/build.rs @@ -37,6 +37,8 @@ fn compile() { proto_dir.join("spotify/login5/v3/user_info.proto"), proto_dir.join("storage-resolve.proto"), proto_dir.join("user_attributes.proto"), + proto_dir.join("autoplay_context_request.proto"), + proto_dir.join("social_connect_v2.proto"), // TODO: remove these legacy protobufs when we are on the new API completely proto_dir.join("authentication.proto"), proto_dir.join("canvaz.proto"), @@ -45,7 +47,6 @@ fn compile() { proto_dir.join("keyexchange.proto"), proto_dir.join("mercury.proto"), proto_dir.join("pubsub.proto"), - proto_dir.join("spirc.proto"), ]; let slices = files.iter().map(Deref::deref).collect::>(); diff --git a/src/main.rs b/src/main.rs index 2da9323a..6aaa72ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,3 @@ -use data_encoding::HEXLOWER; -use futures_util::StreamExt; -use log::{debug, error, info, trace, warn}; -use sha1::{Digest, Sha1}; use std::{ env, fs::create_dir_all, @@ -12,12 +8,13 @@ use std::{ str::FromStr, time::{Duration, Instant}, }; -use sysinfo::{ProcessesToUpdate, System}; -use thiserror::Error; -use url::Url; +use data_encoding::HEXLOWER; +use futures_util::StreamExt; +#[cfg(feature = "alsa-backend")] +use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::{ - connect::{config::ConnectConfig, spirc::Spirc}, + connect::{spirc::Spirc, state::ConnectStateConfig}, core::{ authentication::Credentials, cache::Cache, config::DeviceType, version, Session, SessionConfig, @@ -33,9 +30,11 @@ use librespot::{ player::{coefficient_to_duration, duration_to_coefficient, Player}, }, }; - -#[cfg(feature = "alsa-backend")] -use librespot::playback::mixer::alsamixer::AlsaMixer; +use log::{debug, error, info, trace, warn}; +use sha1::{Digest, Sha1}; +use sysinfo::{ProcessesToUpdate, System}; +use thiserror::Error; +use url::Url; mod player_event_handler; use player_event_handler::{run_program_on_sink_events, EventHandler}; @@ -208,7 +207,7 @@ struct Setup { cache: Option, player_config: PlayerConfig, session_config: SessionConfig, - connect_config: ConnectConfig, + connect_config: ConnectStateConfig, mixer_config: MixerConfig, credentials: Option, enable_oauth: bool, @@ -1371,7 +1370,7 @@ fn get_setup() -> Setup { }); let connect_config = { - let connect_default_config = ConnectConfig::default(); + let connect_default_config = ConnectStateConfig::default(); let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); @@ -1431,14 +1430,11 @@ fn get_setup() -> Setup { #[cfg(feature = "alsa-backend")] let default_value = &format!( "{}, or the current value when the alsa mixer is used.", - connect_default_config.initial_volume.unwrap_or_default() + connect_default_config.initial_volume ); #[cfg(not(feature = "alsa-backend"))] - let default_value = &connect_default_config - .initial_volume - .unwrap_or_default() - .to_string(); + let default_value = &connect_default_config.initial_volume.to_string(); invalid_error_msg( INITIAL_VOLUME, @@ -1485,14 +1481,21 @@ fn get_setup() -> Setup { let is_group = opt_present(DEVICE_IS_GROUP); - let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - - ConnectConfig { - name, - device_type, - is_group, - initial_volume, - has_volume_ctrl, + if let Some(initial_volume) = initial_volume { + ConnectStateConfig { + name, + device_type, + is_group, + initial_volume: initial_volume.into(), + ..Default::default() + } + } else { + ConnectStateConfig { + name, + device_type, + is_group, + ..Default::default() + } } }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 3d0a47df..21cfe01c 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -226,9 +226,10 @@ impl EventHandler { env_vars.insert("PLAYER_EVENT", "shuffle_changed".to_string()); env_vars.insert("SHUFFLE", shuffle.to_string()); } - PlayerEvent::RepeatChanged { repeat } => { + PlayerEvent::RepeatChanged { context, track } => { env_vars.insert("PLAYER_EVENT", "repeat_changed".to_string()); - env_vars.insert("REPEAT", repeat.to_string()); + env_vars.insert("REPEAT", context.to_string()); + env_vars.insert("REPEAT_TRACK", track.to_string()); } PlayerEvent::AutoPlayChanged { auto_play } => { env_vars.insert("PLAYER_EVENT", "auto_play_changed".to_string()); From 72b6ad939790fffa9b1e4fe364fb2444d14ac34a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:45:28 +0100 Subject: [PATCH 477/561] Bump actions/cache from 4.1.2 to 4.2.0 (#1412) Bumps [actions/cache](https://github.com/actions/cache) from 4.1.2 to 4.2.0. - [Release notes](https://github.com/actions/cache/releases) - [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md) - [Commits](https://github.com/actions/cache/compare/v4.1.2...v4.2.0) --- updated-dependencies: - dependency-name: actions/cache dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/test.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2c90553e..41928ed5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 with: path: | ~/.cargo/registry/index @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 with: path: | ~/.cargo/registry/index @@ -183,7 +183,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 with: path: | ~/.cargo/registry/index @@ -230,7 +230,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.1.2 + uses: actions/cache@v4.2.0 with: path: | ~/.cargo/registry/index From 00679fc78d8578f0dfc82cd6192a004fc82243fd Mon Sep 17 00:00:00 2001 From: Fabio Waljaard Date: Sat, 14 Dec 2024 22:28:53 +0100 Subject: [PATCH 478/561] Fix 'play' command requiring 'offset' field (#1418) * Fix 'play' command requiring 'offset' field * Derive 'Default' for SkipTo --- CHANGELOG.md | 1 + connect/src/spirc.rs | 2 +- core/src/dealer/protocol/request.rs | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb13097c..b2611eb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 on Android platform. - [core] Fix "Invalid Credentials" when using a Keymaster access token and client ID on Android platform. += [connect] Fix "play" command not handled if missing "offset" property ### Removed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index b9240851..e3892866 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1040,7 +1040,7 @@ impl SpircTask { context_uri: play.context.uri.clone(), start_playing: true, seek_to: play.options.seek_to.unwrap_or_default(), - playing_track: play.options.skip_to.into(), + playing_track: play.options.skip_to.unwrap_or_default().into(), shuffle, repeat, repeat_track, diff --git a/core/src/dealer/protocol/request.rs b/core/src/dealer/protocol/request.rs index 4d796469..67992437 100644 --- a/core/src/dealer/protocol/request.rs +++ b/core/src/dealer/protocol/request.rs @@ -170,7 +170,7 @@ pub struct TransferOptions { #[derive(Clone, Debug, Deserialize)] pub struct PlayOptions { - pub skip_to: SkipTo, + pub skip_to: Option, #[serde(default, deserialize_with = "option_json_proto")] pub player_options_override: Option, pub license: Option, @@ -191,7 +191,7 @@ pub struct OptionsOptions { system_initiated: bool, } -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Default)] pub struct SkipTo { pub track_uid: Option, pub track_uri: Option, From 2a6d7545ca7681cfd3463f2e3405afafff415cab Mon Sep 17 00:00:00 2001 From: yubiuser Date: Sat, 14 Dec 2024 22:29:55 +0100 Subject: [PATCH 479/561] Update/dependencies (#1416) * Update sysinfo to 0.33 * Update thiserror to v2.0 * Update Alpine Dockerfile to 1.75 (MSRV) * Update governor to 0.8 * Update quick-xml to 0.37 * Update rodio to 0.20.1 * Update zerocopy to 0.8.13 * Update alpine image to 3.19 --------- Signed-off-by: yubiuser --- .devcontainer/Dockerfile.alpine | 4 +- Cargo.lock | 184 ++++++++++++++++++------------ Cargo.toml | 4 +- audio/Cargo.toml | 2 +- connect/Cargo.toml | 2 +- core/Cargo.toml | 8 +- core/src/session.rs | 4 +- discovery/Cargo.toml | 4 +- metadata/Cargo.toml | 2 +- oauth/Cargo.toml | 2 +- playback/Cargo.toml | 6 +- playback/src/audio_backend/mod.rs | 2 +- playback/src/convert.rs | 4 +- src/main.rs | 2 +- test.sh | 12 +- 15 files changed, 146 insertions(+), 96 deletions(-) diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index 5abd17ad..fd3b75c3 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -ARG alpine_version=alpine3.18 -ARG rust_version=1.74.0 +ARG alpine_version=alpine3.19 +ARG rust_version=1.75.0 FROM rust:${rust_version}-${alpine_version} ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" diff --git a/Cargo.lock b/Cargo.lock index 36e67519..b3b25e5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,9 +161,9 @@ dependencies = [ [[package]] name = "async-io" -version = "2.3.4" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" dependencies = [ "async-lock", "cfg-if", @@ -216,7 +216,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -251,7 +251,7 @@ checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -357,7 +357,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.85", + "syn 2.0.90", "which", ] @@ -376,7 +376,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -685,7 +685,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -696,7 +696,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -749,7 +749,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -759,7 +759,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -829,7 +829,7 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -984,9 +984,9 @@ checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1fa2f9765705486b33fd2acf1577f8ec449c2ba1f318ae5447697b7c08d210" +checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" dependencies = [ "fastrand", "futures-core", @@ -1003,7 +1003,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1124,7 +1124,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -1156,13 +1156,14 @@ dependencies = [ [[package]] name = "governor" -version = "0.6.3" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a7f542ee6b35af73b06abc0dad1c1bae89964e4e253bc4b587b91c9637867b" +checksum = "842dc78579ce01e6a1576ad896edc92fca002dd60c9c3746b7fc2bec6fb429d0" dependencies = [ "cfg-if", - "futures", + "futures-sink", "futures-timer", + "futures-util", "no-std-compat", "nonzero_ext", "parking_lot", @@ -1194,7 +1195,7 @@ dependencies = [ "paste", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.65", ] [[package]] @@ -1771,7 +1772,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror", + "thiserror 1.0.65", "walkdir", "windows-sys 0.45.0", ] @@ -1817,9 +1818,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.161" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libloading" @@ -1861,7 +1862,7 @@ dependencies = [ "multimap", "rand", "socket2", - "thiserror", + "thiserror 1.0.65", "tokio", ] @@ -1932,7 +1933,7 @@ dependencies = [ "log", "sha1", "sysinfo", - "thiserror", + "thiserror 2.0.7", "tokio", "url", ] @@ -1952,7 +1953,7 @@ dependencies = [ "log", "parking_lot", "tempfile", - "thiserror", + "thiserror 2.0.7", "tokio", ] @@ -1968,7 +1969,7 @@ dependencies = [ "protobuf", "rand", "serde_json", - "thiserror", + "thiserror 2.0.7", "tokio", "tokio-stream", "uuid", @@ -2019,7 +2020,7 @@ dependencies = [ "sha1", "shannon", "sysinfo", - "thiserror", + "thiserror 2.0.7", "time", "tokio", "tokio-stream", @@ -2056,7 +2057,7 @@ dependencies = [ "serde_json", "serde_repr", "sha1", - "thiserror", + "thiserror 2.0.7", "tokio", "zbus", ] @@ -2073,7 +2074,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror", + "thiserror 2.0.7", "uuid", ] @@ -2084,7 +2085,7 @@ dependencies = [ "env_logger", "log", "oauth2", - "thiserror", + "thiserror 2.0.7", "url", ] @@ -2115,9 +2116,9 @@ dependencies = [ "sdl2", "shell-words", "symphonia", - "thiserror", + "thiserror 2.0.7", "tokio", - "zerocopy", + "zerocopy 0.8.13", ] [[package]] @@ -2239,7 +2240,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", - "thiserror", + "thiserror 1.0.65", ] [[package]] @@ -2354,7 +2355,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -2415,7 +2416,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -2443,7 +2444,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", - "thiserror", + "thiserror 1.0.65", "url", ] @@ -2644,9 +2645,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" -version = "3.7.3" +version = "3.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" dependencies = [ "cfg-if", "concurrent-queue", @@ -2696,7 +2697,7 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" dependencies = [ - "zerocopy", + "zerocopy 0.7.35", ] [[package]] @@ -2706,7 +2707,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" dependencies = [ "proc-macro2", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -2731,9 +2732,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.89" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" +checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] @@ -2746,7 +2747,7 @@ checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" dependencies = [ "once_cell", "protobuf-support", - "thiserror", + "thiserror 1.0.65", ] [[package]] @@ -2761,7 +2762,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror", + "thiserror 1.0.65", ] [[package]] @@ -2772,7 +2773,7 @@ checksum = "9b445cf83c9303695e6c423d269759e139b6182d2f1171e18afda7078a764336" dependencies = [ "protobuf", "protobuf-support", - "thiserror", + "thiserror 1.0.65", ] [[package]] @@ -2787,7 +2788,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror", + "thiserror 1.0.65", "which", ] @@ -2797,14 +2798,14 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" dependencies = [ - "thiserror", + "thiserror 1.0.65", ] [[package]] name = "quick-xml" -version = "0.36.2" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" dependencies = [ "memchr", "serde", @@ -2955,12 +2956,11 @@ dependencies = [ [[package]] name = "rodio" -version = "0.19.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6006a627c1a38d37f3d3a85c6575418cfe34a5392d60a686d0071e1c8d427acb" +checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" dependencies = [ "cpal", - "thiserror", ] [[package]] @@ -3230,7 +3230,7 @@ checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3263,7 +3263,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3514,9 +3514,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.85" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -3531,9 +3531,9 @@ checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] name = "sysinfo" -version = "0.31.4" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +checksum = "948512566b1895f93b1592c7574baeb2de842f224f2aab158799ecadb8ebbb46" dependencies = [ "core-foundation-sys", "libc", @@ -3601,7 +3601,16 @@ version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.65", +] + +[[package]] +name = "thiserror" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +dependencies = [ + "thiserror-impl 2.0.7", ] [[package]] @@ -3612,7 +3621,18 @@ checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -3700,7 +3720,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3835,7 +3855,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -3869,7 +3889,7 @@ dependencies = [ "rustls 0.23.16", "rustls-pki-types", "sha1", - "thiserror", + "thiserror 1.0.65", "utf-8", ] @@ -4060,7 +4080,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "wasm-bindgen-shared", ] @@ -4094,7 +4114,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4252,7 +4272,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4263,7 +4283,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] [[package]] @@ -4560,7 +4580,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "zvariant_utils", ] @@ -4582,7 +4602,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "byteorder", - "zerocopy-derive", + "zerocopy-derive 0.7.35", +] + +[[package]] +name = "zerocopy" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67914ab451f3bfd2e69e5e9d2ef3858484e7074d63f204fd166ec391b54de21d" +dependencies = [ + "zerocopy-derive 0.8.13", ] [[package]] @@ -4593,7 +4622,18 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7988d73a4303ca289df03316bc490e934accf371af6bc745393cf3c2c5c4f25d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", ] [[package]] @@ -4624,7 +4664,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", "zvariant_utils", ] @@ -4636,5 +4676,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" dependencies = [ "proc-macro2", "quote", - "syn 2.0.85", + "syn 2.0.90", ] diff --git a/Cargo.toml b/Cargo.toml index d0a558e2..1e63fb3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,8 +61,8 @@ futures-util = { version = "0.3", default-features = false } getopts = "0.2" log = "0.4" sha1 = "0.10" -sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } -thiserror = "1.0" +sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } +thiserror = "2.0" tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 3ef84532..b6da63b5 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -23,5 +23,5 @@ http-body-util = "0.1.1" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" -thiserror = "1.0" +thiserror = "2.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 7ed3fab7..ee076c3e 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -14,7 +14,7 @@ log = "0.4" protobuf = "3.5" rand = "0.8" serde_json = "1.0" -thiserror = "1.0" +thiserror = "2.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio-stream = "0.1" uuid = { version = "1.11.0", features = ["v4"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 66d54a8b..62355325 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -25,7 +25,7 @@ bytes = "1" form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } -governor = { version = "0.6", default-features = false, features = ["std", "jitter"] } +governor = { version = "0.8", default-features = false, features = ["std", "jitter"] } hmac = "0.12" httparse = "1.7" http = "1.0" @@ -44,15 +44,15 @@ pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pin-project-lite = "0.2" priority-queue = "2.0" protobuf = "3.5" -quick-xml = { version = "0.36.1", features = ["serialize"] } +quick-xml = { version = "0.37.1", features = ["serialize"] } rand = "0.8" rsa = "0.9.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.31.3", default-features = false, features = ["system"] } -thiserror = "1.0" +sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } +thiserror = "2.0" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } tokio-stream = "0.1" diff --git a/core/src/session.rs b/core/src/session.rs index 45d54bc6..5526da48 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -390,7 +390,7 @@ impl Session { } pub fn set_session_id(&self, session_id: String) { - self.0.data.write().session_id = session_id.to_owned(); + session_id.clone_into(&mut self.0.data.write().session_id); } pub fn device_id(&self) -> &str { @@ -450,7 +450,7 @@ impl Session { } pub fn set_auth_data(&self, auth_data: &[u8]) { - self.0.data.write().auth_data = auth_data.to_owned(); + auth_data.clone_into(&mut self.0.data.write().auth_data); } pub fn country(&self) -> String { diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 01383f2e..39c35a09 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -28,9 +28,9 @@ serde = { version = "1", default-features = false, features = ["derive"], option serde_repr = "0.1" serde_json = "1.0" sha1 = "0.10" -thiserror = "1.0" +thiserror = "2.0" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } -zbus = { version = "4", default-features = false, features = ["tokio"], optional = true } +zbus = { version = "4", default-features = false, features = ["tokio"], optional = true } # zbus > 4 requires a MSRV of 1.80 [dependencies.librespot-core] path = "../core" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 69efdf78..8be98fee 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -13,7 +13,7 @@ async-trait = "0.1" bytes = "1" log = "0.4" protobuf = "3.5" -thiserror = "1" +thiserror = "2.0" uuid = { version = "1", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 32148b59..c3d4a81b 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -11,7 +11,7 @@ edition = "2021" [dependencies] log = "0.4" oauth2 = "4.4" -thiserror = "1.0" +thiserror = "2.0" url = "2.2" [dev-dependencies] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index e9f6d438..433829be 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,9 +25,9 @@ futures-util = "0.3" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } shell-words = "1.1" -thiserror = "1" +thiserror = "2.0" tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.7.32", features = ["derive"] } +zerocopy = { version = "0.8.13", features = ["derive"] } # Backends alsa = { version = "0.9.0", optional = true } @@ -42,7 +42,7 @@ gstreamer-audio = { version = "0.23.0", optional = true } glib = { version = "0.20.3", optional = true } # Rodio dependencies -rodio = { version = "0.19.0", optional = true, default-features = false } +rodio = { version = "0.20.1", optional = true, default-features = false } cpal = { version = "0.15.1", optional = true } # Container and audio decoder diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 05822395..739938d3 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -48,7 +48,7 @@ macro_rules! sink_as_bytes { () => { fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::i24; - use zerocopy::AsBytes; + use zerocopy::IntoBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F64 => self.write_bytes(samples.as_bytes()), diff --git a/playback/src/convert.rs b/playback/src/convert.rs index a7efe452..70bbc6cc 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -1,7 +1,7 @@ use crate::dither::{Ditherer, DithererBuilder}; -use zerocopy::AsBytes; +use zerocopy::{Immutable, IntoBytes}; -#[derive(AsBytes, Copy, Clone, Debug)] +#[derive(Immutable, IntoBytes, Copy, Clone, Debug)] #[allow(non_camel_case_types)] #[repr(transparent)] pub struct i24([u8; 3]); diff --git a/src/main.rs b/src/main.rs index 6aaa72ce..2d6c9614 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1872,7 +1872,7 @@ async fn main() { { Ok(d) => break Some(d), Err(e) => { - sys.refresh_processes(ProcessesToUpdate::All); + sys.refresh_processes(ProcessesToUpdate::All, true); if System::uptime() <= 1 { debug!("Retrying to initialise discovery: {e}"); diff --git a/test.sh b/test.sh index 7925f873..d362a227 100755 --- a/test.sh +++ b/test.sh @@ -2,6 +2,16 @@ set -e +clean() { + # some shells will call EXIT after the INT signal + # causing EXIT trap to be executed, so we trap EXIT after INT + trap '' EXIT + + cargo clean +} + +trap clean INT QUIT TERM EXIT + # this script runs the tests and checks that also run as part of the`test.yml` github action workflow cargo clean cargo fmt --all -- --check @@ -18,4 +28,4 @@ cargo check -p librespot-core --no-default-features cargo check -p librespot-core cargo hack check --no-dev-deps --each-feature -p librespot-discovery cargo hack check --no-dev-deps --each-feature -p librespot-playback -cargo hack check --no-dev-deps --each-feature +cargo hack check --no-dev-deps --each-feature \ No newline at end of file From 597974f7d85eef71cee87f72652028eb3864d08d Mon Sep 17 00:00:00 2001 From: yubiuser Date: Mon, 16 Dec 2024 18:31:46 +0100 Subject: [PATCH 480/561] Remove session_id clones (#1422) Signed-off-by: yubiuser --- connect/src/spirc.rs | 2 +- core/src/session.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e3892866..1ed9bd12 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1554,7 +1554,7 @@ impl SpircTask { ); if self.session.session_id() != session.session_id { - self.session.set_session_id(session.session_id.clone()); + self.session.set_session_id(&session.session_id); self.connect_state.set_session_id(session.session_id); } } else { diff --git a/core/src/session.rs b/core/src/session.rs index 5526da48..92933e35 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -389,7 +389,7 @@ impl Session { self.0.data.read().session_id.clone() } - pub fn set_session_id(&self, session_id: String) { + pub fn set_session_id(&self, session_id: &str) { session_id.clone_into(&mut self.0.data.write().session_id); } From 755aa2e5a0c8c0e10be2dd4c518bd201f13a6300 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Tue, 17 Dec 2024 18:57:02 +0100 Subject: [PATCH 481/561] Dealer: Improve disconnect (#1420) * connect: adjust disconnect behavior * update CHANGELOG.md * core: adjust param of `set_session_id` * connect: move unexpected disconnect outside the loop again --- CHANGELOG.md | 1 + connect/src/spirc.rs | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2611eb0..72710acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Add `seek_to` field to `SpircLoadCommand` (breaking) - [connect] Add `repeat_track` field to `SpircLoadCommand` (breaking) +- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking) - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 1ed9bd12..32124570 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -129,7 +129,7 @@ enum SpircCommand { Shuffle(bool), Repeat(bool), RepeatTrack(bool), - Disconnect, + Disconnect { pause: bool }, SetPosition(u32), SetVolume(u16), Activate, @@ -311,8 +311,8 @@ impl Spirc { pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?) } - pub fn disconnect(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Disconnect)?) + pub fn disconnect(&self, pause: bool) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Disconnect { pause })?) } pub fn activate(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Activate)?) @@ -438,20 +438,17 @@ impl SpircTask { error!("error updating connect state for volume update: {why}") } }, - else => break + else => break, } } if !self.shutdown && self.connect_state.is_active() { - if let Err(why) = self.notify().await { - warn!("notify before unexpected shutdown couldn't be send: {why}") + warn!("unexpected shutdown"); + if let Err(why) = self.handle_disconnect().await { + error!("error during disconnecting: {why}") } } - // clears the session id, leaving an empty state - if let Err(why) = self.session.spclient().delete_connect_state_request().await { - warn!("deleting connect_state failed before unexpected shutdown: {why}") - } self.session.dealer().close().await; } @@ -651,7 +648,10 @@ impl SpircTask { self.handle_volume_down(); self.notify().await } - SpircCommand::Disconnect => { + SpircCommand::Disconnect { pause } => { + if pause { + self.handle_pause() + } self.handle_disconnect().await?; self.notify().await } @@ -1142,15 +1142,18 @@ impl SpircTask { } async fn handle_disconnect(&mut self) -> Result<(), Error> { - self.handle_stop(); - - self.play_status = SpircPlayStatus::Stopped {}; self.connect_state .update_position_in_relation(self.now_ms()); self.notify().await?; self.connect_state.became_inactive(&self.session).await?; + // this should clear the active session id, leaving an empty state + self.session + .spclient() + .delete_connect_state_request() + .await?; + self.player .emit_session_disconnected_event(self.session.connection_id(), self.session.username()); From d82d94b76cb6472621f797f481c67c98fcebcd70 Mon Sep 17 00:00:00 2001 From: Benedikt Date: Thu, 19 Dec 2024 22:31:28 +0100 Subject: [PATCH 482/561] discovery: don't panic on libmdns errors (#1427) On panic, the discovery task crashes, but the main program is not notified of this. Returning an error will result in the Discovery stream yielding None, serving as notification to the application (which might shutdown with error, for example, if no other means of authentication is available). --- CHANGELOG.md | 3 ++- discovery/src/lib.rs | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72710acc..a9821e2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 on Android platform. - [core] Fix "Invalid Credentials" when using a Keymaster access token and client ID on Android platform. -= [connect] Fix "play" command not handled if missing "offset" property +- [connect] Fix "play" command not handled if missing "offset" property +- [discovery] Fix libmdns zerconf setup errors not propagating to the main task. ### Removed diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index d829e0f5..c6d88a2e 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -396,7 +396,7 @@ fn launch_libmdns( let task_handle = tokio::task::spawn_blocking(move || { let inner = move || -> Result<(), DiscoveryError> { - let svc = if !zeroconf_ip.is_empty() { + let responder = if !zeroconf_ip.is_empty() { libmdns::Responder::spawn_with_ip_list( &tokio::runtime::Handle::current(), zeroconf_ip, @@ -404,9 +404,9 @@ fn launch_libmdns( } else { libmdns::Responder::spawn(&tokio::runtime::Handle::current()) } - .map_err(|e| DiscoveryError::DnsSdError(Box::new(e))) - .unwrap() - .register( + .map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?; + + let svc = responder.register( DNS_SD_SERVICE_NAME.to_owned(), name.into_owned(), port, From 0ad1f7249b8a1e9a71da4a3b527f793cb938bd62 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Mon, 23 Dec 2024 10:28:06 +0100 Subject: [PATCH 483/561] Update MSRV to 1.81 (#1428) * bump MSRV to 1.81 * Update zbus to v5 * Update jack to 0.13 --- .devcontainer/Dockerfile | 2 +- .devcontainer/Dockerfile.alpine | 2 +- .github/workflows/test.yml | 6 +- CHANGELOG.md | 1 + Cargo.lock | 201 +++++------------------- Cargo.toml | 4 +- contrib/Dockerfile | 2 +- discovery/Cargo.toml | 2 +- discovery/src/avahi.rs | 8 +- playback/Cargo.toml | 2 +- playback/src/audio_backend/jackaudio.rs | 4 +- 11 files changed, 54 insertions(+), 180 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ce845a52..233ac838 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.75.0 +ARG rust_version=1.81.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index fd3b75c3..1a908955 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG alpine_version=alpine3.19 -ARG rust_version=1.75.0 +ARG rust_version=1.81.0 FROM rust:${rust_version}-${alpine_version} ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 41928ed5..ff877e12 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.75" # MSRV (Minimum supported rust version) + - "1.81" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -164,7 +164,7 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.75" # MSRV (Minimum supported rust version) + - "1.81" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code @@ -215,7 +215,7 @@ jobs: - aarch64-unknown-linux-gnu - riscv64gc-unknown-linux-gnu toolchain: - - "1.75" # MSRV (Minimum supported rust version) + - "1.81" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code diff --git a/CHANGELOG.md b/CHANGELOG.md index a9821e2c..8e27f863 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- [core] MSRV is now 1.81 (breaking) - [connect] Replaced `ConnectConfig` with `ConnectStateConfig` (breaking) - [connect] Replaced `playing_track_index` field of `SpircLoadCommand` with `playing_track` (breaking) - [connect] Replaced Mercury usage in `Spirc` with Dealer diff --git a/Cargo.lock b/Cargo.lock index b3b25e5f..2d253687 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -147,67 +147,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-io" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix", - "tracing", -] - [[package]] name = "async-recursion" version = "1.1.1" @@ -219,30 +158,6 @@ dependencies = [ "syn 2.0.90", ] -[[package]] -name = "async-signal" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix", - "signal-hook-registry", - "slab", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - [[package]] name = "async-trait" version = "0.1.83" @@ -400,19 +315,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "blocking" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bumpalo" version = "3.16.0" @@ -607,7 +509,7 @@ dependencies = [ "core-foundation-sys", "coreaudio-rs", "dasp_sample", - "jack", + "jack 0.11.4", "jni", "js-sys", "libc", @@ -982,19 +884,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -1375,12 +1264,6 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.4.3" @@ -1747,6 +1630,19 @@ dependencies = [ "log", ] +[[package]] +name = "jack" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78a4ae24f4ee29676aef8330fed1104e72f314cab16643dbeb61bfd99b4a8273" +dependencies = [ + "bitflags 2.6.0", + "jack-sys", + "lazy_static", + "libc", + "log", +] + [[package]] name = "jack-sys" version = "0.5.1" @@ -2100,7 +1996,7 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-audio", - "jack", + "jack 0.13.0", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -2202,7 +2098,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ - "hermit-abi 0.3.9", + "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", @@ -2605,17 +2501,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.7.5" @@ -2643,21 +2528,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" -[[package]] -name = "polling" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi 0.4.0", - "pin-project-lite", - "rustix", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "portable-atomic" version = "1.9.0" @@ -4540,31 +4410,28 @@ dependencies = [ [[package]] name = "zbus" -version = "4.4.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" dependencies = [ "async-broadcast", - "async-process", "async-recursion", "async-trait", "enumflags2", "event-listener", "futures-core", - "futures-sink", "futures-util", "hex", "nix", "ordered-stream", - "rand", "serde", "serde_repr", - "sha1", "static_assertions", "tokio", "tracing", "uds_windows", - "windows-sys 0.52.0", + "windows-sys 0.59.0", + "winnow", "xdg-home", "zbus_macros", "zbus_names", @@ -4573,25 +4440,28 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "4.4.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn 2.0.90", + "zbus_names", + "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" -version = "3.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" dependencies = [ "serde", "static_assertions", + "winnow", "zvariant", ] @@ -4644,22 +4514,24 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] name = "zvariant" -version = "4.2.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" dependencies = [ "endi", "enumflags2", "serde", "static_assertions", + "winnow", "zvariant_derive", + "zvariant_utils", ] [[package]] name = "zvariant_derive" -version = "4.2.0" +version = "5.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4670,11 +4542,14 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "2.1.0" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" dependencies = [ "proc-macro2", "quote", + "serde", + "static_assertions", "syn 2.0.90", + "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index 1e63fb3c..ae89da48 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "librespot" version = "0.6.0-dev" -rust-version = "1.75" +rust-version = "1.81" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -103,4 +103,4 @@ assets = [ ] [workspace.package] -rust-version = "1.75" +rust-version = "1.81" diff --git a/contrib/Dockerfile b/contrib/Dockerfile index a36fef88..a450d6ca 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -29,7 +29,7 @@ RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 cross RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.75 -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add aarch64-unknown-linux-gnu RUN rustup target add arm-unknown-linux-gnueabi diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 39c35a09..8eac51a5 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -30,7 +30,7 @@ serde_json = "1.0" sha1 = "0.10" thiserror = "2.0" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } -zbus = { version = "4", default-features = false, features = ["tokio"], optional = true } # zbus > 4 requires a MSRV of 1.80 +zbus = { version = "5", default-features = false, features = ["tokio"], optional = true } [dependencies.librespot-core] path = "../core" diff --git a/discovery/src/avahi.rs b/discovery/src/avahi.rs index 7c098168..de720d65 100644 --- a/discovery/src/avahi.rs +++ b/discovery/src/avahi.rs @@ -19,7 +19,7 @@ mod server { default_path = "/", gen_blocking = false )] - trait Server { + pub trait Server { /// EntryGroupNew method #[zbus(object = "super::entry_group::EntryGroup")] fn entry_group_new(&self); @@ -53,9 +53,7 @@ mod entry_group { } impl zvariant::Type for EntryGroupState { - fn signature() -> zvariant::Signature<'static> { - zvariant::Signature::try_from("i").unwrap() - } + const SIGNATURE: &'static zvariant::Signature = &zvariant::Signature::I32; } #[zbus::proxy( @@ -63,7 +61,7 @@ mod entry_group { default_service = "org.freedesktop.Avahi", gen_blocking = false )] - trait EntryGroup { + pub trait EntryGroup { /// AddAddress method fn add_address( &self, diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 433829be..e24f9868 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -34,7 +34,7 @@ alsa = { version = "0.9.0", 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.11", optional = true } # jack >0.11 requires a MSRV of 1.80 +jack = { version = "0.13", optional = true } sdl2 = { version = "0.37", optional = true } gstreamer = { version = "0.23.1", optional = true } gstreamer-app = { version = "0.23.0", optional = true } diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 5c7a28fb..9d40ee82 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -47,8 +47,8 @@ impl Open for JackSink { let client_name = client_name.unwrap_or_else(|| "librespot".to_string()); let (client, _status) = Client::new(&client_name[..], ClientOptions::NO_START_SERVER).unwrap(); - let ch_r = client.register_port("out_0", AudioOut).unwrap(); - let ch_l = client.register_port("out_1", AudioOut).unwrap(); + let ch_r = client.register_port("out_0", AudioOut::default()).unwrap(); + let ch_l = client.register_port("out_1", AudioOut::default()).unwrap(); // buffer for samples from librespot (~10ms) let (tx, rx) = sync_channel::(NUM_CHANNELS as usize * 1024 * AudioFormat::F32.size()); let jack_data = JackData { From 2a574267ae4c04360242f1516f4caec98025b635 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Tue, 24 Dec 2024 09:39:49 +0100 Subject: [PATCH 484/561] Update protobuf files (#1424) * update protobuf definitions * add additionally required proto files * update version.rs * adjust code to protobuf changes * fix formatting * apply suggestions, improve errors --- CHANGELOG.md | 4 + connect/src/model.rs | 10 +- connect/src/spirc.rs | 79 ++++--- connect/src/state.rs | 44 ++-- connect/src/state/context.rs | 93 ++++---- connect/src/state/metadata.rs | 2 +- connect/src/state/options.rs | 2 +- connect/src/state/transfer.rs | 55 +++-- core/src/dealer/protocol/request.rs | 11 +- core/src/spclient.rs | 11 +- core/src/version.rs | 15 +- metadata/src/album.rs | 2 - metadata/src/artist.rs | 2 - playback/src/player.rs | 37 ++- protocol/build.rs | 16 ++ protocol/proto/apiv1.proto | 65 ++++- protocol/proto/audio_files_extension.proto | 8 +- protocol/proto/audio_format.proto | 35 +++ .../proto/autodownload_backend_service.proto | 2 +- protocol/proto/automix_mode.proto | 6 +- protocol/proto/autoplay_context_request.proto | 3 +- protocol/proto/autoplay_node.proto | 3 +- protocol/proto/canvaz.proto | 23 +- protocol/proto/client-tts.proto | 5 +- protocol/proto/client_config.proto | 2 +- protocol/proto/client_update.proto | 2 +- .../collection/album_collection_state.proto | 2 +- .../collection/artist_collection_state.proto | 3 +- .../collection/episode_collection_state.proto | 2 +- .../collection/show_collection_state.proto | 2 +- .../collection/track_collection_state.proto | 2 +- protocol/proto/collection2v2.proto | 10 +- .../collection_add_remove_items_request.proto | 9 +- protocol/proto/collection_ban_request.proto | 5 +- .../proto/collection_decoration_policy.proto | 25 +- .../proto/collection_get_bans_request.proto | 18 +- protocol/proto/collection_index.proto | 46 +++- protocol/proto/collection_item.proto | 34 ++- .../proto/collection_platform_items.proto | 19 ++ .../proto/collection_platform_requests.proto | 23 +- .../proto/collection_platform_responses.proto | 38 ++- protocol/proto/connect.proto | 99 +++----- protocol/proto/contains_request.proto | 2 +- protocol/proto/context.proto | 2 +- .../proto/context_application_desktop.proto | 3 +- protocol/proto/context_client_id.proto | 2 +- protocol/proto/context_device_desktop.proto | 2 +- protocol/proto/context_index.proto | 2 +- protocol/proto/context_installation_id.proto | 2 +- protocol/proto/context_monotonic_clock.proto | 2 +- protocol/proto/context_node.proto | 4 +- protocol/proto/context_page.proto | 2 +- protocol/proto/context_player_options.proto | 7 +- protocol/proto/context_processor.proto | 2 +- protocol/proto/context_sdk.proto | 2 +- protocol/proto/context_time.proto | 2 +- protocol/proto/context_track.proto | 2 +- protocol/proto/context_view.proto | 5 +- protocol/proto/context_view_cyclic_list.proto | 2 +- protocol/proto/context_view_entry.proto | 6 +- protocol/proto/context_view_entry_key.proto | 2 +- protocol/proto/cosmos_changes_request.proto | 3 +- protocol/proto/cosmos_decorate_request.proto | 2 +- .../proto/cosmos_get_album_list_request.proto | 3 +- .../cosmos_get_episode_list_request.proto | 2 +- .../proto/cosmos_get_tags_info_request.proto | 5 +- ...smos_get_track_list_metadata_request.proto | 2 +- .../proto/cosmos_get_track_list_request.proto | 5 +- ...cosmos_get_unplayed_episodes_request.proto | 2 +- protocol/proto/cuepoints.proto | 2 +- protocol/proto/decorate_request.proto | 37 ++- protocol/proto/devices.proto | 2 +- .../proto/display_segments_extension.proto | 29 +-- protocol/proto/entity_extension_data.proto | 3 +- protocol/proto/es_add_to_queue_request.proto | 2 +- protocol/proto/es_command_options.proto | 2 +- protocol/proto/es_context.proto | 4 +- protocol/proto/es_context_page.proto | 4 +- protocol/proto/es_context_player_error.proto | 12 +- .../proto/es_context_player_options.proto | 6 +- protocol/proto/es_context_player_state.proto | 21 +- protocol/proto/es_context_track.proto | 2 +- protocol/proto/es_delete_session.proto | 3 +- protocol/proto/es_get_error_request.proto | 3 +- protocol/proto/es_get_play_history.proto | 3 +- protocol/proto/es_get_position_state.proto | 7 +- protocol/proto/es_get_queue_request.proto | 3 +- protocol/proto/es_get_state_request.proto | 2 +- protocol/proto/es_optional.proto | 2 +- protocol/proto/es_pause.proto | 4 +- protocol/proto/es_pauseresume_origin.proto | 11 + protocol/proto/es_play.proto | 2 +- protocol/proto/es_play_origin.proto | 3 +- protocol/proto/es_prefs.proto | 4 +- protocol/proto/es_prepare_play.proto | 2 +- protocol/proto/es_prepare_play_options.proto | 8 +- protocol/proto/es_provided_track.proto | 2 +- protocol/proto/es_queue.proto | 2 +- protocol/proto/es_remote_config.proto | 22 +- protocol/proto/es_request_info.proto | 4 +- protocol/proto/es_response_with_reasons.proto | 6 +- protocol/proto/es_restrictions.proto | 14 +- protocol/proto/es_resume.proto | 10 +- protocol/proto/es_seek_to.proto | 13 +- protocol/proto/es_session_response.proto | 2 +- protocol/proto/es_set_options.proto | 4 +- protocol/proto/es_set_queue_request.proto | 2 +- protocol/proto/es_set_repeating_context.proto | 2 +- protocol/proto/es_set_repeating_track.proto | 2 +- protocol/proto/es_set_shuffling_context.proto | 2 +- protocol/proto/es_skip_next.proto | 2 +- protocol/proto/es_skip_prev.proto | 2 +- protocol/proto/es_skip_to_track.proto | 2 +- protocol/proto/es_stop.proto | 8 +- protocol/proto/es_storage.proto | 16 +- protocol/proto/es_update.proto | 2 +- protocol/proto/esperanto_options.proto | 6 + protocol/proto/event_entity.proto | 2 +- protocol/proto/explicit_content_pubsub.proto | 7 +- protocol/proto/extended_metadata.proto | 3 +- protocol/proto/extension_kind.proto | 158 ++++++++++++- protocol/proto/extracted_colors.proto | 2 +- protocol/proto/follow_request.proto | 7 +- protocol/proto/followed_users_request.proto | 2 +- protocol/proto/frecency.proto | 2 +- protocol/proto/frecency_storage.proto | 2 +- protocol/proto/gabito.proto | 11 +- protocol/proto/global_node.proto | 5 +- protocol/proto/google/protobuf/any.proto | 2 +- .../proto/google/protobuf/descriptor.proto | 222 ++++++++++++++---- protocol/proto/google/protobuf/duration.proto | 2 +- protocol/proto/google/protobuf/empty.proto | 4 +- .../proto/google/protobuf/field_mask.proto | 2 +- .../google/protobuf/source_context.proto | 2 +- .../proto/google/protobuf/timestamp.proto | 2 +- protocol/proto/google/protobuf/type.proto | 13 +- protocol/proto/google/protobuf/wrappers.proto | 2 +- protocol/proto/identity.proto | 18 +- protocol/proto/installation_data.proto | 6 +- protocol/proto/instrumentation_params.proto | 2 +- protocol/proto/lens-model.proto | 19 ++ protocol/proto/lfs_secret_provider.proto | 2 +- .../proto/liked_songs_tags_sync_state.proto | 2 +- .../proto/listen_later_cosmos_response.proto | 7 +- protocol/proto/local_bans_storage.proto | 6 +- protocol/proto/local_sync_state.proto | 2 +- protocol/proto/logging_params.proto | 4 +- protocol/proto/mdata.proto | 19 +- protocol/proto/mdata_cosmos.proto | 3 +- protocol/proto/mdata_storage.proto | 3 +- protocol/proto/media.proto | 19 ++ protocol/proto/media_manifest.proto | 31 +-- protocol/proto/media_type.proto | 5 +- protocol/proto/members_request.proto | 2 +- protocol/proto/members_response.proto | 2 +- protocol/proto/metadata.proto | 80 ++++--- protocol/proto/metadata/album_metadata.proto | 2 +- protocol/proto/metadata/artist_metadata.proto | 2 +- .../proto/metadata/episode_metadata.proto | 20 +- protocol/proto/metadata/extension.proto | 2 +- protocol/proto/metadata/image_group.proto | 2 +- protocol/proto/metadata/show_metadata.proto | 3 +- protocol/proto/metadata/track_metadata.proto | 7 +- protocol/proto/metadata_cosmos.proto | 2 +- protocol/proto/metadata_esperanto.proto | 2 +- protocol/proto/modification_request.proto | 39 ++- protocol/proto/net-fortune.proto | 4 +- protocol/proto/offline.proto | 13 +- .../proto/offline_playlists_containing.proto | 2 +- protocol/proto/on_demand_in_free_reason.proto | 3 +- .../proto/on_demand_set_cosmos_request.proto | 2 +- .../proto/on_demand_set_cosmos_response.proto | 2 +- protocol/proto/on_demand_set_response.proto | 2 +- protocol/proto/pause_resume_origin.proto | 12 + protocol/proto/pending_event_entity.proto | 2 +- protocol/proto/pin_request.proto | 27 ++- protocol/proto/play_history.proto | 20 ++ protocol/proto/play_origin.proto | 3 +- protocol/proto/play_queue_node.proto | 3 +- protocol/proto/play_reason.proto | 4 +- protocol/proto/playback.proto | 4 +- protocol/proto/playback_esperanto.proto | 46 +++- protocol/proto/playback_stack.proto | 13 + protocol/proto/playback_stack_v2.proto | 14 ++ protocol/proto/playback_state.proto | 14 ++ protocol/proto/played_state.proto | 2 +- .../played_state/episode_played_state.proto | 2 +- .../playability_restriction.proto | 3 +- .../played_state/show_played_state.proto | 17 +- .../played_state/track_played_state.proto | 2 +- protocol/proto/player.proto | 111 +++------ protocol/proto/player_license.proto | 2 +- protocol/proto/playlist4_external.proto | 93 +++++++- .../proto/playlist_contains_request.proto | 2 +- protocol/proto/playlist_folder_state.proto | 2 +- protocol/proto/playlist_get_request.proto | 26 +- protocol/proto/playlist_members_request.proto | 2 +- .../proto/playlist_modification_request.proto | 2 +- protocol/proto/playlist_offline_request.proto | 2 +- protocol/proto/playlist_permission.proto | 81 ++++++- protocol/proto/playlist_play_request.proto | 2 +- .../proto/playlist_playback_request.proto | 2 +- protocol/proto/playlist_playlist_state.proto | 4 +- protocol/proto/playlist_query.proto | 32 ++- protocol/proto/playlist_request.proto | 62 ++++- ...playlist_set_base_permission_request.proto | 2 +- ...aylist_set_member_permission_request.proto | 2 +- .../playlist_set_permission_request.proto | 2 +- protocol/proto/playlist_track_state.proto | 2 +- protocol/proto/playlist_user_state.proto | 2 +- protocol/proto/plugin.proto | 64 ++--- protocol/proto/podcast_ad_segments.proto | 3 +- protocol/proto/podcast_cta_cards.proto | 4 +- protocol/proto/podcast_poll.proto | 2 +- protocol/proto/podcast_qna.proto | 2 +- protocol/proto/podcast_subscription.proto | 4 +- protocol/proto/podcastextensions.proto | 5 +- .../policy/album_decoration_policy.proto | 2 +- .../policy/artist_decoration_policy.proto | 3 +- .../policy/episode_decoration_policy.proto | 15 +- .../policy/folder_decoration_policy.proto | 2 +- .../playlist_album_decoration_policy.proto | 2 +- .../policy/playlist_decoration_policy.proto | 9 +- .../playlist_episode_decoration_policy.proto | 4 +- .../playlist_request_decoration_policy.proto | 36 ++- .../playlist_track_decoration_policy.proto | 4 +- .../rootlist_folder_decoration_policy.proto | 2 +- .../rootlist_playlist_decoration_policy.proto | 2 +- .../rootlist_request_decoration_policy.proto | 2 +- .../proto/policy/show_decoration_policy.proto | 20 +- .../supported_link_types_in_playlists.proto | 17 ++ .../policy/track_decoration_policy.proto | 7 +- .../proto/policy/user_decoration_policy.proto | 2 +- protocol/proto/popcount2_external.proto | 19 +- protocol/proto/prepare_play_options.proto | 10 +- protocol/proto/profile_cosmos.proto | 2 +- protocol/proto/profile_service.proto | 4 +- protocol/proto/property_definition.proto | 47 ++-- protocol/proto/protobuf_delta.proto | 4 +- protocol/proto/queue.proto | 2 +- protocol/proto/rate_limited_events.proto | 2 +- protocol/proto/rcs.proto | 70 +++--- protocol/proto/recently_played.proto | 4 +- protocol/proto/recently_played_backend.proto | 2 +- protocol/proto/record_id.proto | 2 +- protocol/proto/remote.proto | 2 +- protocol/proto/request_failure.proto | 2 +- protocol/proto/resolve.proto | 78 +++--- protocol/proto/resource_type.proto | 2 +- protocol/proto/response_status.proto | 2 +- protocol/proto/restrictions.proto | 17 +- protocol/proto/rootlist_request.proto | 2 +- protocol/proto/seek_to_position.proto | 2 +- protocol/proto/sequence_number_entity.proto | 2 +- protocol/proto/session.proto | 5 +- .../proto/set_member_permission_request.proto | 2 +- protocol/proto/show_access.proto | 79 ++++++- protocol/proto/show_episode_state.proto | 2 +- protocol/proto/show_offline_state.proto | 9 + protocol/proto/show_request.proto | 68 ++++-- protocol/proto/show_show_state.proto | 5 +- protocol/proto/signal-model.proto | 16 ++ protocol/proto/skip_to_track.proto | 2 +- protocol/proto/social_connect_v2.proto | 10 +- protocol/proto/social_service.proto | 10 +- .../proto/socialgraph_response_status.proto | 2 +- protocol/proto/socialgraphv2.proto | 2 +- .../audiobookcashier/v1/audiobook_price.proto | 18 ++ .../ads_rules_inject_tracks.proto | 2 +- .../proto/state_restore/automix_rules.proto | 9 + .../state_restore/automix_talk_rules.proto | 11 + .../behavior_metadata_rules.proto | 2 +- .../state_restore/circuit_breaker_rules.proto | 2 +- .../proto/state_restore/context_loader.proto | 10 + .../context_player_restorable.proto | 23 ++ .../state_restore/context_player_rules.proto | 70 +++++- .../state_restore/context_player_state.proto | 59 +++++ .../explicit_content_rules.proto | 2 +- .../state_restore/kitteh_box_rules.proto | 30 +++ .../state_restore/mft_context_history.proto | 3 +- .../mft_context_switch_rules.proto | 2 + .../mft_fallback_page_history.proto | 2 +- .../proto/state_restore/mft_rules_core.proto | 2 +- .../mft_rules_inject_filler_tracks.proto | 2 +- protocol/proto/state_restore/mft_state.proto | 10 +- .../mod_interruption_state.proto | 2 +- .../mod_rules_interruptions.proto | 13 +- .../state_restore/music_injection_rules.proto | 2 +- .../proto/state_restore/playback_state.proto | 15 ++ .../proto/state_restore/player_model.proto | 38 +++ .../proto/state_restore/player_session.proto | 39 +++ .../state_restore/player_session_fake.proto | 13 + .../state_restore/player_session_queue.proto | 25 +- .../proto/state_restore/provided_track.proto | 2 +- .../proto/state_restore/random_source.proto | 2 +- .../remove_banned_tracks_rules.proto | 2 +- .../state_restore/resume_points_rules.proto | 2 +- .../state_restore/track_error_rules.proto | 2 +- protocol/proto/status.proto | 2 +- protocol/proto/status_code.proto | 9 +- protocol/proto/status_response.proto | 3 +- protocol/proto/storage-resolve.proto | 4 +- protocol/proto/storylines.proto | 2 +- protocol/proto/stream_end_request.proto | 5 +- protocol/proto/stream_handle.proto | 5 +- protocol/proto/stream_progress_request.proto | 13 +- protocol/proto/stream_seek_request.proto | 6 +- protocol/proto/stream_start_request.proto | 43 +++- protocol/proto/stream_start_response.proto | 2 +- protocol/proto/streaming_rule.proto | 3 +- protocol/proto/suppressions.proto | 2 +- protocol/proto/sync/album_sync_state.proto | 2 +- protocol/proto/sync/artist_sync_state.proto | 2 +- protocol/proto/sync/episode_sync_state.proto | 2 +- protocol/proto/sync/track_sync_state.proto | 2 +- protocol/proto/sync_request.proto | 2 +- protocol/proto/track_instance.proto | 5 +- protocol/proto/track_instantiator.proto | 2 +- protocol/proto/transfer_state.proto | 4 +- protocol/proto/tts-resolve.proto | 57 ++++- protocol/proto/ucs.proto | 15 +- .../proto/unfinished_episodes_request.proto | 4 +- protocol/proto/useraccount.proto | 2 +- protocol/proto/your_library_config.proto | 57 +++++ .../proto/your_library_contains_request.proto | 7 +- .../your_library_contains_response.proto | 5 +- .../proto/your_library_decorate_request.proto | 7 +- .../your_library_decorate_response.proto | 5 +- .../proto/your_library_decorated_entity.proto | 108 +++++++-- protocol/proto/your_library_entity.proto | 27 +-- protocol/proto/your_library_index.proto | 77 +++++- protocol/proto/your_library_request.proto | 45 +++- protocol/proto/your_library_response.proto | 31 ++- protocol/src/conversion.rs | 173 ++++++++++++++ protocol/src/lib.rs | 2 + 335 files changed, 3331 insertions(+), 1204 deletions(-) create mode 100644 protocol/proto/audio_format.proto create mode 100644 protocol/proto/collection_platform_items.proto create mode 100644 protocol/proto/es_pauseresume_origin.proto create mode 100644 protocol/proto/esperanto_options.proto create mode 100644 protocol/proto/lens-model.proto create mode 100644 protocol/proto/media.proto create mode 100644 protocol/proto/pause_resume_origin.proto create mode 100644 protocol/proto/play_history.proto create mode 100644 protocol/proto/playback_stack.proto create mode 100644 protocol/proto/playback_stack_v2.proto create mode 100644 protocol/proto/playback_state.proto create mode 100644 protocol/proto/policy/supported_link_types_in_playlists.proto create mode 100644 protocol/proto/show_offline_state.proto create mode 100644 protocol/proto/signal-model.proto create mode 100755 protocol/proto/spotify/audiobookcashier/v1/audiobook_price.proto create mode 100644 protocol/proto/state_restore/automix_rules.proto create mode 100644 protocol/proto/state_restore/automix_talk_rules.proto create mode 100644 protocol/proto/state_restore/context_loader.proto create mode 100644 protocol/proto/state_restore/context_player_restorable.proto create mode 100644 protocol/proto/state_restore/context_player_state.proto create mode 100644 protocol/proto/state_restore/kitteh_box_rules.proto create mode 100644 protocol/proto/state_restore/playback_state.proto create mode 100644 protocol/proto/state_restore/player_model.proto create mode 100644 protocol/proto/state_restore/player_session.proto create mode 100644 protocol/proto/state_restore/player_session_fake.proto create mode 100644 protocol/proto/your_library_config.proto create mode 100644 protocol/src/conversion.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e27f863..146b6fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed +- [core] Removed `get_canvases` from SpClient (breaking) +- [metadata] Removed `genres` from Album (breaking) +- [metadata] Removed `genre` from Artists (breaking) + ## [0.6.0] - 2024-10-30 This version takes another step into the direction of the HTTP API, fixes a diff --git a/connect/src/model.rs b/connect/src/model.rs index f9165eae..a080f968 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -1,6 +1,6 @@ use crate::state::ConnectState; use librespot_core::dealer::protocol::SkipTo; -use librespot_protocol::player::Context; +use librespot_protocol::context::Context; use std::fmt::{Display, Formatter}; use std::hash::{Hash, Hasher}; @@ -77,7 +77,7 @@ impl ResolveContext { let fallback_uri = fallback.into(); Self { context: Context { - uri: uri.into(), + uri: Some(uri.into()), ..Default::default() }, fallback: (!fallback_uri.is_empty()).then_some(fallback_uri), @@ -114,7 +114,7 @@ impl ResolveContext { Self { context: Context { - uri, + uri: Some(uri), ..Default::default() }, fallback: None, @@ -134,7 +134,7 @@ impl ResolveContext { /// the actual context uri pub fn context_uri(&self) -> &str { - &self.context.uri + self.context.uri.as_deref().unwrap_or_default() } pub fn autoplay(&self) -> bool { @@ -150,7 +150,7 @@ impl Display for ResolveContext { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "resolve_uri: <{:?}>, context_uri: <{}>, autoplay: <{}>, update: <{}>", + "resolve_uri: <{:?}>, context_uri: <{:?}>, autoplay: <{}>, update: <{}>", self.resolve_uri(), self.context.uri, self.autoplay, diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 32124570..e8286523 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -17,10 +17,11 @@ use crate::{ protocol::{ autoplay_context_request::AutoplayContextRequest, connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand}, + context::Context, explicit_content_pubsub::UserAttributesUpdate, - player::{Context, TransferState}, playlist4_external::PlaylistModificationInfo, - social_connect_v2::{session::_host_active_device_id, SessionUpdate}, + social_connect_v2::SessionUpdate, + transfer_state::TransferState, user_attributes::UserAttributesMutation, }, }; @@ -49,6 +50,8 @@ use tokio::{sync::mpsc, time::sleep}; pub enum SpircError { #[error("response payload empty")] NoData, + #[error("{0} had no uri")] + NoUri(&'static str), #[error("message pushed for another URI")] InvalidUri(String), #[error("tried resolving not allowed context: {0:?}")] @@ -63,7 +66,7 @@ impl From for Error { fn from(err: SpircError) -> Self { use SpircError::*; match err { - NoData | NotAllowedContext(_) => Error::unavailable(err), + NoData | NoUri(_) | NotAllowedContext(_) => Error::unavailable(err), InvalidUri(_) | FailedDealerSetup => Error::aborted(err), UnknownEndpoint(_) => Error::unimplemented(err), } @@ -522,14 +525,14 @@ impl SpircTask { let mut ctx = self.session.spclient().get_context(resolve_uri).await?; if update { - ctx.uri = context_uri.to_string(); - ctx.url = format!("context://{context_uri}"); + ctx.uri = Some(context_uri.to_string()); + ctx.url = Some(format!("context://{context_uri}")); self.connect_state .update_context(ctx, UpdateContext::Default)? } else if matches!(ctx.pages.first(), Some(p) if !p.tracks.is_empty()) { debug!( - "update context from single page, context {} had {} pages", + "update context from single page, context {:?} had {} pages", ctx.uri, ctx.pages.len() ); @@ -883,7 +886,7 @@ impl SpircTask { let attributes: UserAttributes = update .pairs .iter() - .map(|pair| (pair.key().to_owned(), pair.value().to_owned())) + .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect(); self.session.set_user_attributes(attributes) } @@ -998,9 +1001,10 @@ impl SpircTask { Unknown(unknown) => Err(SpircError::UnknownEndpoint(unknown))?, // implicit update of the connect_state UpdateContext(update_context) => { - if &update_context.context.uri != self.connect_state.context_uri() { + if matches!(update_context.context.uri, Some(ref uri) if uri != self.connect_state.context_uri()) + { debug!( - "ignoring context update for <{}>, because it isn't the current context <{}>", + "ignoring context update for <{:?}>, because it isn't the current context <{}>", update_context.context.uri, self.connect_state.context_uri() ) } else { @@ -1020,24 +1024,30 @@ impl SpircTask { .options .player_options_override .as_ref() - .map(|o| o.shuffling_context) + .map(|o| o.shuffling_context.unwrap_or_default()) .unwrap_or_else(|| self.connect_state.shuffling_context()); let repeat = play .options .player_options_override .as_ref() - .map(|o| o.repeating_context) + .map(|o| o.repeating_context.unwrap_or_default()) .unwrap_or_else(|| self.connect_state.repeat_context()); let repeat_track = play .options .player_options_override .as_ref() - .map(|o| o.repeating_track) + .map(|o| o.repeating_track.unwrap_or_default()) .unwrap_or_else(|| self.connect_state.repeat_track()); + let context_uri = play + .context + .uri + .clone() + .ok_or(SpircError::NoUri("context"))?; + self.handle_load( SpircLoadCommand { - context_uri: play.context.uri.clone(), + context_uri, start_playing: true, seek_to: play.options.seek_to.unwrap_or_default(), playing_track: play.options.skip_to.unwrap_or_default().into(), @@ -1088,12 +1098,13 @@ impl SpircTask { } fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> { - self.connect_state - .reset_context(ResetContext::WhenDifferent( - &transfer.current_session.context.uri, - )); + let mut ctx_uri = match transfer.current_session.context.uri { + None => Err(SpircError::NoUri("transfer context"))?, + Some(ref uri) => uri.clone(), + }; - let mut ctx_uri = transfer.current_session.context.uri.clone(); + self.connect_state + .reset_context(ResetContext::WhenDifferent(&ctx_uri)); match self.connect_state.current_track_from_transfer(&transfer) { Err(why) => warn!("didn't find initial track: {why}"), @@ -1118,17 +1129,18 @@ impl SpircTask { state.set_active(true); state.handle_initial_transfer(&mut transfer); - // update position if the track continued playing - let position = if transfer.playback.is_paused { - transfer.playback.position_as_of_timestamp.into() - } else if transfer.playback.position_as_of_timestamp > 0 { - let time_since_position_update = timestamp - transfer.playback.timestamp; - i64::from(transfer.playback.position_as_of_timestamp) + time_since_position_update - } else { - 0 + let transfer_timestamp = transfer.playback.timestamp.unwrap_or_default(); + let position = match transfer.playback.position_as_of_timestamp { + Some(position) if transfer.playback.is_paused.unwrap_or_default() => position.into(), + // update position if the track continued playing + Some(position) if position > 0 => { + let time_since_position_update = timestamp - transfer_timestamp; + i64::from(position) + time_since_position_update + } + _ => 0, }; - let is_playing = !transfer.playback.is_paused; + let is_playing = matches!(transfer.playback.is_paused, Some(is_playing) if is_playing); if self.connect_state.current_track(|t| t.is_autoplay()) || autoplay { debug!("currently in autoplay context, async resolving autoplay for {ctx_uri}"); @@ -1514,7 +1526,9 @@ impl SpircTask { &mut self, playlist_modification_info: PlaylistModificationInfo, ) -> Result<(), Error> { - let uri = playlist_modification_info.uri.ok_or(SpircError::NoData)?; + let uri = playlist_modification_info + .uri + .ok_or(SpircError::NoUri("playlist modification"))?; let uri = String::from_utf8(uri)?; if self.connect_state.context_uri() != &uri { @@ -1540,14 +1554,7 @@ impl SpircTask { Some(session) => session, }; - let active_device = session._host_active_device_id.take().map(|id| match id { - _host_active_device_id::HostActiveDeviceId(id) => id, - other => { - warn!("unexpected active device id {other:?}"); - String::new() - } - }); - + let active_device = session.host_active_device_id.take(); if matches!(active_device, Some(ref device) if device == self.session.device_id()) { info!( "session update: <{:?}> for self, current session_id {}, new session_id {}", diff --git a/connect/src/state.rs b/connect/src/state.rs index 28e57dad..eff83bac 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -8,21 +8,25 @@ mod tracks; mod transfer; use crate::model::SpircPlayStatus; -use crate::state::{ - context::{ContextType, ResetContext, StateContext}, - provider::{IsProvider, Provider}, -}; -use librespot_core::{ - config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, version, - Error, Session, -}; -use librespot_protocol::connect::{ - Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest, -}; -use librespot_protocol::player::{ - ContextIndex, ContextPage, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, - Suppressions, +use crate::{ + core::{ + config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, + version, Error, Session, + }, + protocol::{ + connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, + context_page::ContextPage, + player::{ + ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, + Suppressions, + }, + }, + state::{ + context::{ContextType, ResetContext, StateContext}, + provider::{IsProvider, Provider}, + }, }; + use log::LevelFilter; use protobuf::{EnumOrUnknown, MessageField}; use std::{ @@ -40,20 +44,21 @@ const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80; pub enum StateError { #[error("the current track couldn't be resolved from the transfer state")] CouldNotResolveTrackFromTransfer, - #[error("message field {0} was not available")] - MessageFieldNone(String), #[error("context is not available. type: {0:?}")] NoContext(ContextType), #[error("could not find track {0:?} in context of {1}")] CanNotFindTrackInContext(Option, usize), #[error("currently {action} is not allowed because {reason}")] - CurrentlyDisallowed { action: String, reason: String }, + CurrentlyDisallowed { + action: &'static str, + reason: String, + }, #[error("the provided context has no tracks")] ContextHasNoTracks, #[error("playback of local files is not supported")] UnsupportedLocalPlayBack, - #[error("track uri <{0}> contains invalid characters")] - InvalidTrackUri(String), + #[error("track uri <{0:?}> contains invalid characters")] + InvalidTrackUri(Option), } impl From for Error { @@ -61,7 +66,6 @@ impl From for Error { use StateError::*; match err { CouldNotResolveTrackFromTransfer - | MessageFieldNone(_) | NoContext(_) | CanNotFindTrackInContext(_, _) | ContextHasNoTracks diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 3e9d720e..11827cc5 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -1,7 +1,13 @@ -use crate::state::{metadata::Metadata, provider::Provider, ConnectState, StateError}; -use librespot_core::{Error, SpotifyId}; -use librespot_protocol::player::{ - Context, ContextIndex, ContextPage, ContextTrack, ProvidedTrack, Restrictions, +use crate::{ + core::{Error, SpotifyId}, + protocol::{ + context::Context, + context_page::ContextPage, + context_track::ContextTrack, + player::{ContextIndex, ProvidedTrack}, + restrictions::Restrictions, + }, + state::{metadata::Metadata, provider::Provider, ConnectState, StateError}, }; use protobuf::MessageField; use std::collections::HashMap; @@ -104,14 +110,16 @@ impl ConnectState { } pub fn get_context_uri_from_context(context: &Context) -> Option<&String> { - if !context.uri.starts_with(SEARCH_IDENTIFIER) { - return Some(&context.uri); + let context_uri = context.uri.as_ref()?; + + if !context_uri.starts_with(SEARCH_IDENTIFIER) { + return Some(context_uri); } context .pages .first() - .and_then(|p| p.tracks.first().map(|t| &t.uri)) + .and_then(|p| p.tracks.first().and_then(|t| t.uri.as_ref())) } pub fn set_active_context(&mut self, new_context: ContextType) { @@ -134,7 +142,7 @@ impl ConnectState { player.restrictions.clear(); if let Some(restrictions) = restrictions.take() { - player.restrictions = MessageField::some(restrictions); + player.restrictions = MessageField::some(restrictions.into()); } for (key, value) in metadata { @@ -146,7 +154,7 @@ impl ConnectState { if context.pages.iter().all(|p| p.tracks.is_empty()) { error!("context didn't have any tracks: {context:#?}"); return Err(StateError::ContextHasNoTracks.into()); - } else if context.uri.starts_with(LOCAL_FILES_IDENTIFIER) { + } else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) { return Err(StateError::UnsupportedLocalPlayBack.into()); } @@ -174,7 +182,7 @@ impl ConnectState { }; debug!( - "updated context {ty:?} from <{}> ({} tracks) to <{}> ({} tracks)", + "updated context {ty:?} from <{:?}> ({} tracks) to <{:?}> ({} tracks)", self.context_uri(), prev_context .map(|c| c.tracks.len().to_string()) @@ -188,14 +196,14 @@ impl ConnectState { let mut new_context = self.state_context_from_page( page, context.restrictions.take(), - Some(&context.uri), + context.uri.as_deref(), None, ); // when we update the same context, we should try to preserve the previous position // otherwise we might load the entire context twice if !self.context_uri().contains(SEARCH_IDENTIFIER) - && self.context_uri() == &context.uri + && matches!(context.uri, Some(ref uri) if uri == self.context_uri()) { match Self::find_index_in_context(Some(&new_context), |t| { self.current_track(|t| &t.uri) == &t.uri @@ -217,18 +225,18 @@ impl ConnectState { self.context = Some(new_context); - if !context.url.contains(SEARCH_IDENTIFIER) { - self.player_mut().context_url = context.url; + if !matches!(context.url, Some(ref url) if url.contains(SEARCH_IDENTIFIER)) { + self.player_mut().context_url = context.url.take().unwrap_or_default(); } else { self.player_mut().context_url.clear() } - self.player_mut().context_uri = context.uri; + self.player_mut().context_uri = context.uri.take().unwrap_or_default(); } UpdateContext::Autoplay => { self.autoplay_context = Some(self.state_context_from_page( page, context.restrictions.take(), - Some(&context.uri), + context.uri.as_deref(), Some(Provider::Autoplay), )) } @@ -271,7 +279,7 @@ impl ConnectState { pub fn merge_context(&mut self, context: Option) -> Option<()> { let mut context = context?; - if self.context_uri() != &context.uri { + if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) { return None; } @@ -279,12 +287,13 @@ impl ConnectState { let new_page = context.pages.pop()?; for new_track in new_page.tracks { - if new_track.uri.is_empty() { + if new_track.uri.is_none() || matches!(new_track.uri, Some(ref uri) if uri.is_empty()) { continue; } + let new_track_uri = new_track.uri.unwrap_or_default(); if let Ok(position) = - Self::find_index_in_context(Some(current_context), |t| t.uri == new_track.uri) + Self::find_index_in_context(Some(current_context), |t| t.uri == new_track_uri) { let context_track = current_context.tracks.get_mut(position)?; @@ -294,8 +303,10 @@ impl ConnectState { } // the uid provided from another context might be actual uid of an item - if !new_track.uid.is_empty() { - context_track.uid = new_track.uid; + if new_track.uid.is_some() + || matches!(new_track.uid, Some(ref uid) if uid.is_empty()) + { + context_track.uid = new_track.uid.unwrap_or_default(); } } } @@ -325,19 +336,19 @@ impl ConnectState { context_uri: Option<&str>, provider: Option, ) -> Result { - let id = if !ctx_track.uri.is_empty() { - if ctx_track.uri.contains(['?', '%']) { - Err(StateError::InvalidTrackUri(ctx_track.uri.clone()))? + let id = match (ctx_track.uri.as_ref(), ctx_track.gid.as_ref()) { + (None, None) => Err(StateError::InvalidTrackUri(None))?, + (Some(uri), _) if uri.contains(['?', '%']) => { + Err(StateError::InvalidTrackUri(Some(uri.clone())))? } - - SpotifyId::from_uri(&ctx_track.uri)? - } else if !ctx_track.gid.is_empty() { - SpotifyId::from_raw(&ctx_track.gid)? - } else { - Err(StateError::InvalidTrackUri(String::new()))? + (Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?, + (None, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?, + _ => Err(StateError::InvalidTrackUri(None))?, }; - let provider = if self.unavailable_uri.contains(&ctx_track.uri) { + let uri = id.to_uri()?.replace("unknown", "track"); + + let provider = if self.unavailable_uri.contains(&uri) { Provider::Unavailable } else { provider.unwrap_or(Provider::Context) @@ -346,11 +357,10 @@ impl ConnectState { // assumption: the uid is used as unique-id of any item // - queue resorting is done by each client and orients itself by the given uid // - if no uid is present, resorting doesn't work or behaves not as intended - let uid = if ctx_track.uid.is_empty() { - // so setting providing a unique id should allow to resort the queue - Uuid::new_v4().as_simple().to_string() - } else { - ctx_track.uid.to_string() + let uid = match ctx_track.uid.as_ref() { + Some(uid) if !uid.is_empty() => uid.to_string(), + // so providing a unique id should allow to resort the queue + _ => Uuid::new_v4().as_simple().to_string(), }; let mut metadata = HashMap::new(); @@ -359,7 +369,7 @@ impl ConnectState { } let mut track = ProvidedTrack { - uri: id.to_uri()?.replace("unknown", "track"), + uri, uid, metadata, provider: provider.to_string(), @@ -399,12 +409,13 @@ impl ConnectState { }; if next.tracks.is_empty() { - if next.page_url.is_empty() { - Err(StateError::NoContext(ContextType::Default))? - } + let next_page_url = match next.page_url { + Some(page_url) if !page_url.is_empty() => page_url, + _ => Err(StateError::NoContext(ContextType::Default))?, + }; self.update_current_index(|i| i.page += 1); - return Ok(LoadNext::PageUrl(next.page_url)); + return Ok(LoadNext::PageUrl(next_page_url)); } self.fill_context_from_page(next)?; diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index d3788b22..b1effb68 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -1,4 +1,4 @@ -use librespot_protocol::player::{ContextTrack, ProvidedTrack}; +use librespot_protocol::{context_track::ContextTrack, player::ProvidedTrack}; use std::collections::HashMap; const CONTEXT_URI: &str = "context_uri"; diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index b6bc331c..b9c2c576 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -41,7 +41,7 @@ impl ConnectState { .first() { Err(StateError::CurrentlyDisallowed { - action: "shuffle".to_string(), + action: "shuffle", reason: reason.clone(), })? } diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index c310e0b9..53d420a1 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -1,9 +1,13 @@ -use crate::state::context::ContextType; -use crate::state::metadata::Metadata; -use crate::state::provider::{IsProvider, Provider}; -use crate::state::{ConnectState, StateError}; -use librespot_core::Error; -use librespot_protocol::player::{ProvidedTrack, TransferState}; +use crate::{ + core::Error, + protocol::{player::ProvidedTrack, transfer_state::TransferState}, + state::{ + context::ContextType, + metadata::Metadata, + provider::{IsProvider, Provider}, + {ConnectState, StateError}, + }, +}; use protobuf::MessageField; impl ConnectState { @@ -11,7 +15,7 @@ impl ConnectState { &self, transfer: &TransferState, ) -> Result { - let track = if transfer.queue.is_playing_queue { + let track = if transfer.queue.is_playing_queue.unwrap_or_default() { transfer.queue.tracks.first() } else { transfer.playback.current_track.as_ref() @@ -20,8 +24,11 @@ impl ConnectState { self.context_to_provided_track( track, - Some(&transfer.current_session.context.uri), - transfer.queue.is_playing_queue.then_some(Provider::Queue), + transfer.current_session.context.uri.as_deref(), + transfer + .queue + .is_playing_queue + .and_then(|b| b.then_some(Provider::Queue)), ) } @@ -33,23 +40,22 @@ impl ConnectState { player.is_buffering = false; if let Some(options) = transfer.options.take() { - player.options = MessageField::some(options); + player.options = MessageField::some(options.into()); } - player.is_paused = transfer.playback.is_paused; - player.is_playing = !transfer.playback.is_paused; + player.is_paused = transfer.playback.is_paused.unwrap_or_default(); + player.is_playing = !player.is_paused; - if transfer.playback.playback_speed != 0. { - player.playback_speed = transfer.playback.playback_speed - } else { - player.playback_speed = 1.; + match transfer.playback.playback_speed { + Some(speed) if speed != 0. => player.playback_speed = speed, + _ => player.playback_speed = 1., } if let Some(session) = transfer.current_session.as_mut() { - player.play_origin = session.play_origin.take().into(); - player.suppressions = session.suppressions.take().into(); + player.play_origin = session.play_origin.take().map(Into::into).into(); + player.suppressions = session.suppressions.take().map(Into::into).into(); if let Some(mut ctx) = session.context.take() { - player.restrictions = ctx.restrictions.take().into(); + player.restrictions = ctx.restrictions.take().map(Into::into).into(); for (key, value) in ctx.metadata { player.context_metadata.insert(key, value); } @@ -87,11 +93,10 @@ impl ConnectState { let ctx = self.get_context(&self.active_context).ok(); - let current_index = if track.is_queue() { - Self::find_index_in_context(ctx, |c| c.uid == transfer.current_session.current_uid) - .map(|i| if i > 0 { i - 1 } else { i }) - } else { - Self::find_index_in_context(ctx, |c| c.uri == track.uri || c.uid == track.uid) + let current_index = match transfer.current_session.current_uid.as_ref() { + Some(uid) if track.is_queue() => Self::find_index_in_context(ctx, |c| &c.uid == uid) + .map(|i| if i > 0 { i - 1 } else { i }), + _ => Self::find_index_in_context(ctx, |c| c.uri == track.uri || c.uid == track.uid), }; debug!( @@ -116,7 +121,7 @@ impl ConnectState { ); for (i, track) in transfer.queue.tracks.iter().enumerate() { - if transfer.queue.is_playing_queue && i == 0 { + if transfer.queue.is_playing_queue.unwrap_or_default() && i == 0 { // if we are currently playing from the queue, // don't add the first queued item, because we are currently playing that item continue; diff --git a/core/src/dealer/protocol/request.rs b/core/src/dealer/protocol/request.rs index 67992437..86c44cf1 100644 --- a/core/src/dealer/protocol/request.rs +++ b/core/src/dealer/protocol/request.rs @@ -1,6 +1,11 @@ -use crate::deserialize_with::*; -use librespot_protocol::player::{ - Context, ContextPlayerOptionOverrides, PlayOrigin, ProvidedTrack, TransferState, +use crate::{ + deserialize_with::*, + protocol::{ + context::Context, + context_player_options::ContextPlayerOptionOverrides, + player::{PlayOrigin, ProvidedTrack}, + transfer_state::TransferState, + }, }; use serde::Deserialize; use serde_json::Value; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index c818570a..42213a57 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -10,12 +10,13 @@ use crate::{ config::SessionConfig, error::ErrorKind, protocol::{ - canvaz::EntityCanvazRequest, + autoplay_context_request::AutoplayContextRequest, clienttoken_http::{ ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse, ClientTokenResponseType, }, connect::PutStateRequest, + context::Context, extended_metadata::BatchedEntityRequest, }, token::Token, @@ -32,7 +33,6 @@ use hyper::{ HeaderMap, Method, Request, }; use hyper_util::client::legacy::ResponseFuture; -use librespot_protocol::{autoplay_context_request::AutoplayContextRequest, player::Context}; use protobuf::{Enum, Message, MessageFull}; use rand::RngCore; use sysinfo::System; @@ -716,13 +716,6 @@ impl SpClient { // TODO: Seen-in-the-wild but unimplemented endpoints // - /presence-view/v1/buddylist - // 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(&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(&Method::POST, endpoint, None, &request) diff --git a/core/src/version.rs b/core/src/version.rs index 4fce65ad..0e48a47a 100644 --- a/core/src/version.rs +++ b/core/src/version.rs @@ -17,19 +17,26 @@ pub const SEMVER: &str = env!("CARGO_PKG_VERSION"); pub const BUILD_ID: &str = env!("LIBRESPOT_BUILD_ID"); /// The protocol version of the Spotify desktop client. -pub const SPOTIFY_VERSION: u64 = 117300517; +pub const SPOTIFY_VERSION: u64 = 124200290; /// The semantic version of the Spotify desktop client. -pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.31.1205.g4d59ad7c"; +pub const SPOTIFY_SEMANTIC_VERSION: &str = "1.2.52.442"; + +/// `property_set_id` related to desktop version 1.2.52.442 +pub const SPOTIFY_PROPERTY_SET_ID: &str = "b4c7e4b5835079ed94391b2e65fca0fdba65eb50"; /// The protocol version of the Spotify mobile app. -pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84"; +pub const SPOTIFY_MOBILE_VERSION: &str = "8.9.82.620"; + +/// `property_set_id` related to mobile version 8.9.82.620 +pub const SPOTIFY_MOBILE_PROPERTY_SET_ID: &str = + "5ec87c2cc32e7c509703582cfaaa3c7ad253129d5701127c1f5eab5c9531736c"; /// The general spirc version pub const SPOTIFY_SPIRC_VERSION: &str = "3.2.6"; /// 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)"; +pub const FALLBACK_USER_AGENT: &str = "Spotify/124200290 Linux/0 (librespot)"; pub fn spotify_version() -> String { match crate::config::OS { diff --git a/metadata/src/album.rs b/metadata/src/album.rs index ede9cc5b..6663b9c3 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -32,7 +32,6 @@ pub struct Album { pub label: String, pub date: Date, pub popularity: i32, - pub genres: Vec, pub covers: Images, pub external_ids: ExternalIds, pub discs: Discs, @@ -95,7 +94,6 @@ impl TryFrom<&::Message> for Album { label: album.label().to_owned(), date: album.date.get_or_default().try_into()?, popularity: album.popularity(), - genres: album.genre.to_vec(), covers: album.cover_group.get_or_default().into(), external_ids: album.external_id.as_slice().into(), discs: album.disc.as_slice().try_into()?, diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 927846a3..6a3a63fc 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -37,7 +37,6 @@ pub struct Artist { pub singles: AlbumGroups, pub compilations: AlbumGroups, pub appears_on_albums: AlbumGroups, - pub genre: Vec, pub external_ids: ExternalIds, pub portraits: Images, pub biographies: Biographies, @@ -193,7 +192,6 @@ impl TryFrom<&::Message> for Artist { singles: artist.single_group.as_slice().try_into()?, compilations: artist.compilation_group.as_slice().try_into()?, appears_on_albums: artist.appears_on_group.as_slice().try_into()?, - genre: artist.genre.to_vec(), external_ids: artist.external_id.as_slice().into(), portraits: artist.portrait.as_slice().into(), biographies: artist.biography.as_slice().into(), diff --git a/playback/src/player.rs b/playback/src/player.rs index 6a4170f0..e9663a70 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -907,27 +907,24 @@ impl PlayerTrackLoader { fn stream_data_rate(&self, format: AudioFileFormat) -> Option { 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::AAC_160 => 20, - AudioFileFormat::AAC_320 => 40, - AudioFileFormat::MP4_128 => 16, - AudioFileFormat::OTHER5 => 40, - AudioFileFormat::FLAC_FLAC => 112, // assume 900 kbit/s on average - AudioFileFormat::UNKNOWN_FORMAT => { - error!("Unknown stream data rate"); - return None; - } + 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 + AudioFileFormat::XHE_AAC_12 => 1.5, + AudioFileFormat::XHE_AAC_16 => 2., + AudioFileFormat::XHE_AAC_24 => 3., + AudioFileFormat::FLAC_FLAC_24BIT => 3., }; - Some(kbps * 1024) + let data_rate: f32 = kbps * 1024.; + Some(data_rate.ceil() as usize) } async fn load_track( diff --git a/protocol/build.rs b/protocol/build.rs index 43971bc8..a20ea22d 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("media.proto"), proto_dir.join("connectivity.proto"), proto_dir.join("devices.proto"), proto_dir.join("entity_extension_data.proto"), @@ -27,6 +28,8 @@ fn compile() { proto_dir.join("playlist_annotate3.proto"), proto_dir.join("playlist_permission.proto"), proto_dir.join("playlist4_external.proto"), + proto_dir.join("lens-model.proto"), + proto_dir.join("signal-model.proto"), proto_dir.join("spotify/clienttoken/v0/clienttoken_http.proto"), proto_dir.join("spotify/login5/v3/challenges/code.proto"), proto_dir.join("spotify/login5/v3/challenges/hashcash.proto"), @@ -39,6 +42,19 @@ fn compile() { proto_dir.join("user_attributes.proto"), proto_dir.join("autoplay_context_request.proto"), proto_dir.join("social_connect_v2.proto"), + proto_dir.join("transfer_state.proto"), + proto_dir.join("context_player_options.proto"), + proto_dir.join("playback.proto"), + proto_dir.join("play_history.proto"), + proto_dir.join("session.proto"), + proto_dir.join("queue.proto"), + proto_dir.join("context_track.proto"), + proto_dir.join("context.proto"), + proto_dir.join("restrictions.proto"), + proto_dir.join("context_page.proto"), + proto_dir.join("play_origin.proto"), + proto_dir.join("suppressions.proto"), + proto_dir.join("instrumentation_params.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/apiv1.proto b/protocol/proto/apiv1.proto index 2d8b9c28..e8a10b4e 100644 --- a/protocol/proto/apiv1.proto +++ b/protocol/proto/apiv1.proto @@ -1,4 +1,4 @@ -// No longer present in Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -19,7 +19,7 @@ message ListDevicesResponse { message PutDeviceRequest { string user_id = 1; - + Body body = 2; message Body { Device device = 1; @@ -39,8 +39,27 @@ message RemoveDeviceRequest { bool is_force_remove = 2; } +message OfflineEnableDeviceRequest { + message Body { + bool auto_opc = 1; + } + + DeviceKey key = 1; + Body body = 2; + string name = 9; + int32 platform = 7; + string client_id = 8; +} + message OfflineEnableDeviceResponse { + enum StatusCode { + UNKNOWN = 0; + OK = 1; + DEVICE_LIMIT_REACHED = 2; + } + Restrictions restrictions = 1; + StatusCode status_code = 2; } message ListResourcesResponse { @@ -50,7 +69,7 @@ message ListResourcesResponse { message WriteResourcesRequest { DeviceKey key = 1; - + Body body = 2; message Body { repeated ResourceOperation operations = 1; @@ -66,7 +85,7 @@ message ResourcesUpdate { message DeltaResourcesRequest { DeviceKey key = 1; - + Body body = 2; message Body { google.protobuf.Timestamp last_known_server_time = 1; @@ -90,7 +109,7 @@ message GetResourceResponse { message WriteResourcesDetailsRequest { DeviceKey key = 1; - + Body body = 2; message Body { repeated Resource resources = 1; @@ -106,3 +125,39 @@ message GetResourceForDevicesResponse { repeated Device devices = 1; repeated ResourceForDevice resources = 2; } + +message ListDevicesWithResourceRequest { + message Body { + string uri = 1; + } + + string user_id = 1; + string username = 2; + Body body = 3; +} + +message ListDevicesWithResourceResponse { + message DeviceWithResource { + Device device = 1; + bool is_supported = 2; + optional Resource resource = 3; + } + + repeated DeviceWithResource deviceWithResource = 1; + FetchStrategy fetch_strategy = 2; +} + +message FetchStrategy { + oneof fetch_strategy { + PollStrategy poll_strategy = 1; + SubStrategy sub_strategy = 2; + } +} + +message PollStrategy { + int32 interval_ms = 1; +} + +message SubStrategy { +} + diff --git a/protocol/proto/audio_files_extension.proto b/protocol/proto/audio_files_extension.proto index 32efd995..7c0a8b62 100644 --- a/protocol/proto/audio_files_extension.proto +++ b/protocol/proto/audio_files_extension.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -15,13 +15,15 @@ message NormalizationParams { } message ExtendedAudioFile { + reserved 2; + reserved 3; metadata.AudioFile file = 1; - NormalizationParams file_normalization_params = 2; - NormalizationParams album_normalization_params = 3; + int32 average_bitrate = 4; } message AudioFilesExtensionResponse { repeated ExtendedAudioFile files = 1; NormalizationParams default_file_normalization_params = 2; NormalizationParams default_album_normalization_params = 3; + bytes audio_id = 4; } diff --git a/protocol/proto/audio_format.proto b/protocol/proto/audio_format.proto new file mode 100644 index 00000000..1985c37b --- /dev/null +++ b/protocol/proto/audio_format.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +option java_package = "com.spotify.stream_reporting_esperanto.proto"; +option objc_class_prefix = "ESP"; + +enum AudioFormat { + 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; + FORMAT_MP4_Unknown = 19; + FORMAT_MP3_Unknown = 20; + FORMAT_XHE_AAC_12 = 21; + FORMAT_XHE_AAC_16 = 22; + FORMAT_XHE_AAC_24 = 23; + FORMAT_FLAC_FLAC_24 = 24; +} + diff --git a/protocol/proto/autodownload_backend_service.proto b/protocol/proto/autodownload_backend_service.proto index fa088feb..69ee6dfe 100644 --- a/protocol/proto/autodownload_backend_service.proto +++ b/protocol/proto/autodownload_backend_service.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/automix_mode.proto b/protocol/proto/automix_mode.proto index d0d7f938..50f730ca 100644 --- a/protocol/proto/automix_mode.proto +++ b/protocol/proto/automix_mode.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -32,10 +32,14 @@ enum AutomixStyle { SLEEP = 5; MIXED = 6; CUSTOM = 7; + HEURISTIC = 8; + BACKEND = 9; } enum TransitionType { CUEPOINTS = 0; CROSSFADE = 1; GAPLESS = 2; + HEURISTIC_TRANSITION = 3; + BACKEND_TRANSITION = 4; } diff --git a/protocol/proto/autoplay_context_request.proto b/protocol/proto/autoplay_context_request.proto index 4fa4b0bc..35b12b07 100644 --- a/protocol/proto/autoplay_context_request.proto +++ b/protocol/proto/autoplay_context_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -9,4 +9,5 @@ option optimize_for = CODE_SIZE; message AutoplayContextRequest { required string context_uri = 1; repeated string recent_track_uri = 2; + optional bool is_video = 3; } diff --git a/protocol/proto/autoplay_node.proto b/protocol/proto/autoplay_node.proto index 18709f12..9d8012a6 100644 --- a/protocol/proto/autoplay_node.proto +++ b/protocol/proto/autoplay_node.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -12,4 +12,5 @@ message AutoplayNode { map filler_node = 1; required bool is_playing_filler = 2; required LoggingParams logging_params = 3; + optional bool called_play_on_filler = 4; } diff --git a/protocol/proto/canvaz.proto b/protocol/proto/canvaz.proto index 2493da95..936a5332 100644 --- a/protocol/proto/canvaz.proto +++ b/protocol/proto/canvaz.proto @@ -1,11 +1,9 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.canvaz.cache; -import "canvaz-meta.proto"; - option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.canvazcache.proto"; @@ -17,12 +15,11 @@ message Artist { } message EntityCanvazResponse { - repeated Canvaz canvases = 1; message Canvaz { string id = 1; string url = 2; string file_id = 3; - spotify.canvaz.Type type = 4; + Type type = 4; string entity_uri = 5; Artist artist = 6; bool explicit = 7; @@ -31,14 +28,14 @@ message EntityCanvazResponse { string canvas_uri = 11; string storylines_id = 12; } - - int64 ttl_in_seconds = 2; + } -message EntityCanvazRequest { - repeated Entity entities = 1; - message Entity { - string entity_uri = 1; - string etag = 2; - } +enum Type { + IMAGE = 0; + VIDEO = 1; + VIDEO_LOOPING = 2; + VIDEO_LOOPING_RANDOM = 3; + GIF = 4; } + diff --git a/protocol/proto/client-tts.proto b/protocol/proto/client-tts.proto index 0968f515..fe40b025 100644 --- a/protocol/proto/client-tts.proto +++ b/protocol/proto/client-tts.proto @@ -1,8 +1,8 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; -package spotify.narration_injection.proto; +package spotify.narration.proto; import "tts-resolve.proto"; @@ -18,7 +18,6 @@ message TtsRequest { ResolveRequest.TtsVoice tts_voice = 5; ResolveRequest.TtsProvider tts_provider = 6; int32 sample_rate_hz = 7; - oneof prompt { string text = 1; string ssml = 2; diff --git a/protocol/proto/client_config.proto b/protocol/proto/client_config.proto index b838873e..17734462 100644 --- a/protocol/proto/client_config.proto +++ b/protocol/proto/client_config.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/client_update.proto b/protocol/proto/client_update.proto index fb93c9bd..c7aa5ecb 100644 --- a/protocol/proto/client_update.proto +++ b/protocol/proto/client_update.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/collection/album_collection_state.proto b/protocol/proto/collection/album_collection_state.proto index 1258961d..c4cfbfed 100644 --- a/protocol/proto/collection/album_collection_state.proto +++ b/protocol/proto/collection/album_collection_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/collection/artist_collection_state.proto b/protocol/proto/collection/artist_collection_state.proto index 33ade56a..cdc001a7 100644 --- a/protocol/proto/collection/artist_collection_state.proto +++ b/protocol/proto/collection/artist_collection_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -15,4 +15,5 @@ message ArtistCollectionState { optional uint32 num_albums_in_collection = 4; optional bool is_banned = 5; optional bool can_ban = 6; + optional uint32 num_explicitly_liked_tracks = 7; } diff --git a/protocol/proto/collection/episode_collection_state.proto b/protocol/proto/collection/episode_collection_state.proto index 56fcc533..c900c17d 100644 --- a/protocol/proto/collection/episode_collection_state.proto +++ b/protocol/proto/collection/episode_collection_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/collection/show_collection_state.proto b/protocol/proto/collection/show_collection_state.proto index d3904b51..6b9e4101 100644 --- a/protocol/proto/collection/show_collection_state.proto +++ b/protocol/proto/collection/show_collection_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/collection/track_collection_state.proto b/protocol/proto/collection/track_collection_state.proto index 68e42ed2..6782cb17 100644 --- a/protocol/proto/collection/track_collection_state.proto +++ b/protocol/proto/collection/track_collection_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/collection2v2.proto b/protocol/proto/collection2v2.proto index 19530fe8..71d4b75c 100644 --- a/protocol/proto/collection2v2.proto +++ b/protocol/proto/collection2v2.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -18,6 +18,7 @@ message CollectionItem { string uri = 1; int32 added_at = 2; bool is_removed = 3; + optional string context_uri = 4; } message PageResponse { @@ -45,13 +46,6 @@ message WriteRequest { 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; diff --git a/protocol/proto/collection_add_remove_items_request.proto b/protocol/proto/collection_add_remove_items_request.proto index 4dac680e..6be1eb2d 100644 --- a/protocol/proto/collection_add_remove_items_request.proto +++ b/protocol/proto/collection_add_remove_items_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,12 +6,15 @@ package spotify.collection_cosmos.proto; import "status.proto"; +option java_package = "spotify.collection.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionAddRemoveItemsRequest { - repeated string item = 1; + repeated string uri = 1; } message CollectionAddRemoveItemsResponse { - Status status = 1; + Status status = 1; } diff --git a/protocol/proto/collection_ban_request.proto b/protocol/proto/collection_ban_request.proto index e64220df..78cb0c55 100644 --- a/protocol/proto/collection_ban_request.proto +++ b/protocol/proto/collection_ban_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,6 +6,9 @@ package spotify.collection_cosmos.proto; import "status.proto"; +option java_package = "spotify.collection.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionBanRequest { diff --git a/protocol/proto/collection_decoration_policy.proto b/protocol/proto/collection_decoration_policy.proto index 79b4b8cf..e673b86f 100644 --- a/protocol/proto/collection_decoration_policy.proto +++ b/protocol/proto/collection_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,7 +7,12 @@ package spotify.collection_cosmos.proto; import "policy/artist_decoration_policy.proto"; import "policy/album_decoration_policy.proto"; import "policy/track_decoration_policy.proto"; +import "policy/show_decoration_policy.proto"; +import "policy/episode_decoration_policy.proto"; +option java_package = "spotify.collection.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionArtistDecorationPolicy { @@ -35,4 +40,22 @@ message CollectionTrackDecorationPolicy { CollectionAlbumDecorationPolicy album_policy = 5; cosmos_util.proto.ArtistDecorationPolicy artist_policy = 6; bool decorated = 7; + cosmos_util.proto.ArtistCollectionDecorationPolicy artist_collection_policy = 8; } + +message CollectionShowDecorationPolicy { + cosmos_util.proto.ShowDecorationPolicy show_policy = 1; + cosmos_util.proto.ShowPlayedStateDecorationPolicy played_state_policy = 2; + cosmos_util.proto.ShowCollectionDecorationPolicy collection_policy = 3; + bool decorated = 4; +} + +message CollectionEpisodeDecorationPolicy { + cosmos_util.proto.EpisodeDecorationPolicy episode_policy = 1; + cosmos_util.proto.EpisodeCollectionDecorationPolicy collection_policy = 2; + cosmos_util.proto.EpisodeSyncDecorationPolicy sync_policy = 3; + cosmos_util.proto.EpisodePlayedStateDecorationPolicy played_state_policy = 4; + CollectionShowDecorationPolicy show_policy = 5; + bool decorated = 6; +} + diff --git a/protocol/proto/collection_get_bans_request.proto b/protocol/proto/collection_get_bans_request.proto index a67574ae..bb4c44f2 100644 --- a/protocol/proto/collection_get_bans_request.proto +++ b/protocol/proto/collection_get_bans_request.proto @@ -1,21 +1,21 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) 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 "collection_decoration_policy.proto"; +import "collection_item.proto"; import "status.proto"; +option java_multiple_files = true; +option java_package = "spotify.collection.esperanto.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; + CollectionTrackDecorationPolicy track_policy = 1; + CollectionArtistDecorationPolicy artist_policy = 2; string sort = 3; bool timestamp = 4; uint32 update_throttling = 5; @@ -23,8 +23,8 @@ message CollectionGetBansRequest { message Item { uint32 add_time = 1; - cosmos_util.proto.TrackMetadata track_metadata = 2; - cosmos_util.proto.ArtistMetadata artist_metadata = 3; + CollectionTrack track_metadata = 2; + CollectionArtist artist_metadata = 3; } message CollectionGetBansResponse { diff --git a/protocol/proto/collection_index.proto b/protocol/proto/collection_index.proto index ee6b3efc..359e8eb3 100644 --- a/protocol/proto/collection_index.proto +++ b/protocol/proto/collection_index.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,6 +11,10 @@ message IndexRepairerState { int64 last_full_check_finished_at = 2; } +message AddTime { + int64 timestamp = 1; +} + message CollectionTrackEntry { string uri = 1; string track_name = 2; @@ -23,16 +27,52 @@ message CollectionTrackEntry { int64 add_time = 9; } -message CollectionAlbumLikeEntry { +message CollectionAlbumEntry { string uri = 1; string album_name = 2; - string creator_uri = 4; + string artist_uri = 4; + string artist_name = 5; + int64 add_time = 6; + int64 last_played = 8; + int64 release_date = 9; +} + +message CollectionShowEntry { + string uri = 1; + string show_name = 2; string creator_name = 5; int64 add_time = 6; + int64 publish_date = 7; + int64 last_played = 8; +} + +message CollectionBookEntry { + string uri = 1; + string book_name = 2; + string author_name = 5; + int64 add_time = 6; + int64 last_played = 8; } message CollectionArtistEntry { string uri = 1; string artist_name = 2; int64 add_time = 4; + int64 last_played = 8; } + +message CollectionAuthorEntry { + string uri = 1; + string author_name = 2; + int64 add_time = 4; +} + +message CollectionEpisodeEntry { + string uri = 1; + string episode_name = 2; + string show_uri = 3; + string show_name = 4; + int64 add_time = 5; + int64 publish_time = 6; +} + diff --git a/protocol/proto/collection_item.proto b/protocol/proto/collection_item.proto index 4a98e9d0..514de22f 100644 --- a/protocol/proto/collection_item.proto +++ b/protocol/proto/collection_item.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,14 +7,24 @@ package spotify.collection_cosmos.proto; import "metadata/album_metadata.proto"; import "metadata/artist_metadata.proto"; import "metadata/track_metadata.proto"; +import "metadata/show_metadata.proto"; +import "metadata/episode_metadata.proto"; import "collection/artist_collection_state.proto"; import "collection/album_collection_state.proto"; import "collection/track_collection_state.proto"; +import "collection/show_collection_state.proto"; +import "collection/episode_collection_state.proto"; import "sync/artist_sync_state.proto"; import "sync/album_sync_state.proto"; import "sync/track_sync_state.proto"; +import "sync/episode_sync_state.proto"; import "played_state/track_played_state.proto"; +import "played_state/show_played_state.proto"; +import "played_state/episode_played_state.proto"; +option java_package = "spotify.collection.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionTrack { @@ -27,6 +37,8 @@ message CollectionTrack { bool decorated = 7; CollectionAlbum album = 8; string cover = 9; + string link = 10; + repeated cosmos_util.proto.ArtistCollectionState artist_collection_state = 11; } message CollectionAlbum { @@ -37,6 +49,7 @@ message CollectionAlbum { bool decorated = 5; string album_type = 6; repeated CollectionTrack track = 7; + string link = 11; } message CollectionArtist { @@ -45,4 +58,23 @@ message CollectionArtist { cosmos_util.proto.ArtistSyncState artist_sync_state = 3; bool decorated = 4; repeated CollectionAlbum album = 5; + string link = 6; } + +message CollectionShow { + cosmos_util.proto.ShowMetadata show_metadata = 1; + cosmos_util.proto.ShowCollectionState show_collection_state = 2; + cosmos_util.proto.ShowPlayState show_play_state = 3; + uint32 add_time = 4; + string link = 5; +} + +message CollectionEpisode { + cosmos_util.proto.EpisodeMetadata episode_metadata = 1; + cosmos_util.proto.EpisodeCollectionState episode_collection_state = 2; + cosmos_util.proto.EpisodeSyncState episode_offline_state = 3; + cosmos_util.proto.EpisodePlayState episode_play_state = 4; + CollectionShow show = 5; + string link = 6; +} + diff --git a/protocol/proto/collection_platform_items.proto b/protocol/proto/collection_platform_items.proto new file mode 100644 index 00000000..fde06a05 --- /dev/null +++ b/protocol/proto/collection_platform_items.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package spotify.collection_platform.proto; + +option java_package = "com.spotify.collection_platform.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; + +message CollectionPlatformItem { + string uri = 1; + int64 add_time = 2; +} + +message CollectionPlatformContextItem { + string uri = 1; + int64 add_time = 2; + string context_uri = 3; +} + diff --git a/protocol/proto/collection_platform_requests.proto b/protocol/proto/collection_platform_requests.proto index a855c217..01bbd24d 100644 --- a/protocol/proto/collection_platform_requests.proto +++ b/protocol/proto/collection_platform_requests.proto @@ -1,9 +1,14 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.collection_platform.proto; +import "collection_platform_items.proto"; + +option java_package = "com.spotify.collection_platform.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionPlatformItemsRequest { @@ -11,11 +16,21 @@ message CollectionPlatformItemsRequest { repeated string items = 2; } +message CollectionPlatformContextItemsRequest { + CollectionSet set = 1; + repeated CollectionPlatformContextItem items = 2; +} + enum CollectionSet { UNKNOWN = 0; - SHOW = 1; - BAN = 2; - LISTENLATER = 3; IGNOREINRECS = 4; ENHANCED = 5; + BANNED_ARTISTS = 8; + CONCERTS = 10; + TAGS = 11; + PRERELEASE = 12; + MARKED_AS_FINISHED = 13; + NOT_INTERESTED = 14; + LOCAL_BANS = 15; } + diff --git a/protocol/proto/collection_platform_responses.proto b/protocol/proto/collection_platform_responses.proto index 6b7716d8..98e2a091 100644 --- a/protocol/proto/collection_platform_responses.proto +++ b/protocol/proto/collection_platform_responses.proto @@ -1,20 +1,20 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.collection_platform.proto; +import "collection_platform_items.proto"; + +option java_package = "com.spotify.collection_platform.esperanto.proto"; +option java_multiple_files = true; +option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; message CollectionPlatformSimpleResponse { string error_msg = 1; } -message CollectionPlatformItem { - string uri = 1; - int64 add_time = 2; -} - message CollectionPlatformItemsResponse { repeated CollectionPlatformItem items = 1; } @@ -22,3 +22,29 @@ message CollectionPlatformItemsResponse { message CollectionPlatformContainsResponse { repeated bool found = 1; } + +message Status { + int32 code = 1; + string reason = 2; +} + +message CollectionPlatformEsperantoContainsResponse { + Status status = 1; + CollectionPlatformContainsResponse contains = 2; +} + +message CollectionPlatformEsperantoItemsResponse { + Status status = 1; + repeated CollectionPlatformItem items = 2; +} + +message CollectionPlatformContextItemsResponse { + Status status = 1; + repeated CollectionPlatformContextItem items = 2; +} + +message CollectionPlatformContainsContextItemsResponse { + Status status = 1; + repeated bool found = 2; +} + diff --git a/protocol/proto/connect.proto b/protocol/proto/connect.proto index d6485252..9e10c796 100644 --- a/protocol/proto/connect.proto +++ b/protocol/proto/connect.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,9 +6,10 @@ package spotify.connectstate; import "player.proto"; import "devices.proto"; +import "media.proto"; -option optimize_for = CODE_SIZE; option java_package = "com.spotify.connectstate.model"; +option optimize_for = CODE_SIZE; message ClusterUpdate { Cluster cluster = 1; @@ -17,10 +18,6 @@ message ClusterUpdate { repeated string devices_that_changed = 4; } -message PostCommandResponse { - string ack_id = 1; -} - message Device { DeviceInfo device_info = 1; PlayerState player_state = 2; @@ -29,15 +26,18 @@ message Device { } message Cluster { + reserved 7; + 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; + optional bool needs_state_updates = 10; + optional uint64 started_playing_at_timestamp = 11; } message PutStateRequest { @@ -59,18 +59,15 @@ message PrivateDeviceInfo { string platform = 1; } -message SubscribeRequest { - string callback_url = 1; -} - message DeviceInfo { + reserved 5; + bool can_play = 1; uint32 volume = 2; string name = 3; Capabilities capabilities = 4; - repeated DeviceMetadata metadata = 5; string device_software_version = 6; - spotify.connectstate.devices.DeviceType device_type = 7; + devices.DeviceType device_type = 7; string spirc_version = 9; string device_id = 10; bool is_private_session = 11; @@ -82,7 +79,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; @@ -90,29 +87,20 @@ message DeviceInfo { 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; - } + optional 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; + optional AudioOutputDeviceType audio_output_device_type = 1; + optional string device_name = 2; } message Capabilities { + reserved "supported_contexts"; + reserved "supports_lossless_audio"; + reserved 1; + reserved 4; + reserved 24; bool can_be_player = 2; bool restrict_to_local = 3; bool gaia_eq_connect_id = 5; @@ -137,8 +125,9 @@ message Capabilities { bool supports_set_options_command = 25; CapabilitySupportDetails supports_hifi = 26; string connect_capabilities = 27; - - //reserved 1, 4, 24, "supported_contexts", "supports_lossless_audio"; + bool supports_rooms = 28; + bool supports_dj = 29; + common.media.AudioQuality supported_audio_quality = 30; } message CapabilitySupportDetails { @@ -152,6 +141,11 @@ message ConnectCommandOptions { uint32 target_alias_id = 3; } +message ConnectLoggingParams { + repeated string interaction_ids = 1; + repeated string page_instance_ids = 2; +} + message LogoutCommand { ConnectCommandOptions command_options = 1; } @@ -159,6 +153,8 @@ message LogoutCommand { message SetVolumeCommand { int32 volume = 1; ConnectCommandOptions command_options = 2; + ConnectLoggingParams logging_params = 3; + string connection_type = 4; } message RenameCommand { @@ -166,35 +162,18 @@ message RenameCommand { 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; + AUTOMOTIVE = 5; + CAR_PROJECTED = 6; } enum PutStateReason { @@ -207,6 +186,11 @@ enum PutStateReason { PICKER_OPENED = 6; BECAME_INACTIVE = 7; ALIAS_CHANGED = 8; + NEW_CONNECTION = 9; + PULL_PLAYBACK = 10; + AUDIO_DRIVER_INFO_CHANGED = 11; + PUT_STATE_RATE_LIMITED = 12; + BACKEND_METADATA_APPLIED = 13; } enum MemberType { @@ -225,15 +209,6 @@ enum ClusterUpdateReason { NEW_DEVICE_APPEARED = 3; DEVICE_VOLUME_CHANGED = 4; DEVICE_ALIAS_CHANGED = 5; + DEVICE_NEW_CONNECTION = 6; } -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/contains_request.proto b/protocol/proto/contains_request.proto index cf59c5f5..52cc7382 100644 --- a/protocol/proto/contains_request.proto +++ b/protocol/proto/contains_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context.proto b/protocol/proto/context.proto index eb022415..bc1d4722 100644 --- a/protocol/proto/context.proto +++ b/protocol/proto/context.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_application_desktop.proto b/protocol/proto/context_application_desktop.proto index 04f443b2..a754673c 100644 --- a/protocol/proto/context_application_desktop.proto +++ b/protocol/proto/context_application_desktop.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,4 +9,5 @@ option optimize_for = CODE_SIZE; message ApplicationDesktop { string version_string = 1; int64 version_code = 2; + bytes session_id = 3; } diff --git a/protocol/proto/context_client_id.proto b/protocol/proto/context_client_id.proto index bab3b6b8..7e116532 100644 --- a/protocol/proto/context_client_id.proto +++ b/protocol/proto/context_client_id.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_device_desktop.proto b/protocol/proto/context_device_desktop.proto index a6b38372..8ff3b874 100644 --- a/protocol/proto/context_device_desktop.proto +++ b/protocol/proto/context_device_desktop.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_index.proto b/protocol/proto/context_index.proto index c7049eac..40edd57d 100644 --- a/protocol/proto/context_index.proto +++ b/protocol/proto/context_index.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_installation_id.proto b/protocol/proto/context_installation_id.proto index 08fe2580..b690db11 100644 --- a/protocol/proto/context_installation_id.proto +++ b/protocol/proto/context_installation_id.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_monotonic_clock.proto b/protocol/proto/context_monotonic_clock.proto index 3ec525ff..c2e807af 100644 --- a/protocol/proto/context_monotonic_clock.proto +++ b/protocol/proto/context_monotonic_clock.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_node.proto b/protocol/proto/context_node.proto index 82dd9d62..398db427 100644 --- a/protocol/proto/context_node.proto +++ b/protocol/proto/context_node.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -9,6 +9,7 @@ import "play_origin.proto"; import "prepare_play_options.proto"; import "track_instance.proto"; import "track_instantiator.proto"; +import "context_track.proto"; option optimize_for = CODE_SIZE; @@ -21,4 +22,5 @@ message ContextNode { optional string session_id = 7; optional sint32 iteration = 8; optional bool pending_pause = 9; + optional ContextTrack injected_connect_transfer_track = 10; } diff --git a/protocol/proto/context_page.proto b/protocol/proto/context_page.proto index b6e8ecdc..c3c7f9a4 100644 --- a/protocol/proto/context_page.proto +++ b/protocol/proto/context_page.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_player_options.proto b/protocol/proto/context_player_options.proto index 57e069b5..1dca4360 100644 --- a/protocol/proto/context_player_options.proto +++ b/protocol/proto/context_player_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -10,10 +10,15 @@ message ContextPlayerOptions { optional bool shuffling_context = 1; optional bool repeating_context = 2; optional bool repeating_track = 3; + optional float playback_speed = 4; + map modes = 5; } message ContextPlayerOptionOverrides { optional bool shuffling_context = 1; optional bool repeating_context = 2; optional bool repeating_track = 3; + optional float playback_speed = 4; + map modes = 5; } + diff --git a/protocol/proto/context_processor.proto b/protocol/proto/context_processor.proto index 2d931b0b..fbfa1c30 100644 --- a/protocol/proto/context_processor.proto +++ b/protocol/proto/context_processor.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_sdk.proto b/protocol/proto/context_sdk.proto index 419f7aa5..fa3b0f71 100644 --- a/protocol/proto/context_sdk.proto +++ b/protocol/proto/context_sdk.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_time.proto b/protocol/proto/context_time.proto index 93749b41..009cad10 100644 --- a/protocol/proto/context_time.proto +++ b/protocol/proto/context_time.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/context_track.proto b/protocol/proto/context_track.proto index e9d06f21..12fe06f5 100644 --- a/protocol/proto/context_track.proto +++ b/protocol/proto/context_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_view.proto b/protocol/proto/context_view.proto index 0b78991a..63efd004 100644 --- a/protocol/proto/context_view.proto +++ b/protocol/proto/context_view.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -13,6 +13,5 @@ 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 index 76cde3ed..56ace3e2 100644 --- a/protocol/proto/context_view_cyclic_list.proto +++ b/protocol/proto/context_view_cyclic_list.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/context_view_entry.proto b/protocol/proto/context_view_entry.proto index 8451f481..23c9ce2e 100644 --- a/protocol/proto/context_view_entry.proto +++ b/protocol/proto/context_view_entry.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -10,14 +10,14 @@ 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 Type type = 1; optional player.proto.ContextTrack track = 2; optional player.proto.ContextIndex index = 3; optional int32 page_index = 4; diff --git a/protocol/proto/context_view_entry_key.proto b/protocol/proto/context_view_entry_key.proto index 6c8a019f..7cb7a1e6 100644 --- a/protocol/proto/context_view_entry_key.proto +++ b/protocol/proto/context_view_entry_key.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/cosmos_changes_request.proto b/protocol/proto/cosmos_changes_request.proto index 2e4b7040..81c81894 100644 --- a/protocol/proto/cosmos_changes_request.proto +++ b/protocol/proto/cosmos_changes_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -8,5 +8,4 @@ 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 9e586021..c20c561a 100644 --- a/protocol/proto/cosmos_decorate_request.proto +++ b/protocol/proto/cosmos_decorate_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/cosmos_get_album_list_request.proto b/protocol/proto/cosmos_get_album_list_request.proto index 448dcd46..83f2ad92 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -34,5 +34,4 @@ message Response { 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 index 437a621f..72bd7c42 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/cosmos_get_tags_info_request.proto b/protocol/proto/cosmos_get_tags_info_request.proto index 5480c7bc..d1a34956 100644 --- a/protocol/proto/cosmos_get_tags_info_request.proto +++ b/protocol/proto/cosmos_get_tags_info_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,6 +7,9 @@ package spotify.collection_cosmos.tags_info_request.proto; option objc_class_prefix = "SPTCollectionCosmosTagsInfo"; option optimize_for = CODE_SIZE; +message Request { +} + 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 index a4586249..2399656c 100644 --- a/protocol/proto/cosmos_get_track_list_metadata_request.proto +++ b/protocol/proto/cosmos_get_track_list_metadata_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/cosmos_get_track_list_request.proto b/protocol/proto/cosmos_get_track_list_request.proto index 95c83410..21eea1b1 100644 --- a/protocol/proto/cosmos_get_track_list_request.proto +++ b/protocol/proto/cosmos_get_track_list_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.collection_cosmos.track_list_request.proto; +import "collection/artist_collection_state.proto"; import "collection/track_collection_state.proto"; import "played_state/track_played_state.proto"; import "sync/track_sync_state.proto"; @@ -21,6 +22,7 @@ message Item { optional cosmos_util.proto.TrackPlayState track_play_state = 6; optional cosmos_util.proto.TrackCollectionState track_collection_state = 7; optional string group_label = 8; + repeated cosmos_util.proto.ArtistCollectionState artist_collection_state = 9; } message GroupHeader { @@ -36,5 +38,4 @@ message Response { 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 index 09339c78..691761b3 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/cuepoints.proto b/protocol/proto/cuepoints.proto index 16bfd6a9..77a01a67 100644 --- a/protocol/proto/cuepoints.proto +++ b/protocol/proto/cuepoints.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/decorate_request.proto b/protocol/proto/decorate_request.proto index ff1fa0ed..4d56f65b 100644 --- a/protocol/proto/decorate_request.proto +++ b/protocol/proto/decorate_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -7,45 +7,38 @@ 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 "played_state/show_played_state.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 "podcast_ratings.proto"; -import "transcripts.proto"; -import "clips_cover.proto"; +import "show_offline_state.proto"; option objc_class_prefix = "SPTShowCosmosDecorate"; option optimize_for = CODE_SIZE; message Show { + reserved 5; + reserved 6; 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 cosmos_util.proto.ShowPlayState show_play_state = 3; optional string link = 4; - optional podcast_paywalls.ShowAccess access_info = 5; - optional ratings.PodcastRating podcast_rating = 6; + optional show_cosmos.proto.ShowOfflineState show_offline_state = 7; } message Episode { + reserved 6; + reserved 7; + reserved 8; + reserved 9; + reserved 10; + reserved 11; + reserved 12; + reserved 13; 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 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; - 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; - optional clips.ClipsCover clips = 13; - - reserved 8; } message Response { diff --git a/protocol/proto/devices.proto b/protocol/proto/devices.proto index ebfadc1b..00deaa4d 100644 --- a/protocol/proto/devices.proto +++ b/protocol/proto/devices.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/display_segments_extension.proto b/protocol/proto/display_segments_extension.proto index 04714446..55914bd7 100644 --- a/protocol/proto/display_segments_extension.proto +++ b/protocol/proto/display_segments_extension.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.displaysegments.v1; +option objc_class_prefix = "ESP"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_outer_classname = "DisplaySegmentsExtensionProto"; @@ -13,9 +14,9 @@ message DisplaySegmentsExtension { string episode_uri = 1; repeated DisplaySegment segments = 2; int32 duration_ms = 3; - oneof decoration { MusicAndTalkDecoration music_and_talk_decoration = 4; + PodcastChaptersDecoration podcast_chapters_decoration = 5; } } @@ -25,28 +26,20 @@ message DisplaySegment { 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; - } + optional string title = 6; + optional string subtitle = 7; + optional string image_url = 8; + optional bool is_preview = 9; } message MusicAndTalkDecoration { bool can_upsell = 1; } +message PodcastChaptersDecoration { + repeated string tags = 1; +} + enum SegmentType { SEGMENT_TYPE_UNSPECIFIED = 0; SEGMENT_TYPE_TALK = 1; diff --git a/protocol/proto/entity_extension_data.proto b/protocol/proto/entity_extension_data.proto index e26d735e..589ee687 100644 --- a/protocol/proto/entity_extension_data.proto +++ b/protocol/proto/entity_extension_data.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -30,7 +30,6 @@ message PlainListAssoc { } message AssocHeader { - } message Assoc { diff --git a/protocol/proto/es_add_to_queue_request.proto b/protocol/proto/es_add_to_queue_request.proto index 34997731..dd0e009b 100644 --- a/protocol/proto/es_add_to_queue_request.proto +++ b/protocol/proto/es_add_to_queue_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_command_options.proto b/protocol/proto/es_command_options.proto index 0a37e801..8f6c5bc1 100644 --- a/protocol/proto/es_command_options.proto +++ b/protocol/proto/es_command_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_context.proto b/protocol/proto/es_context.proto index 05962fa7..19e1f885 100644 --- a/protocol/proto/es_context.proto +++ b/protocol/proto/es_context.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -16,6 +16,6 @@ message Context { map metadata = 2; string uri = 3; string url = 4; - bool is_loaded = 5; + bool is_loading = 5; Restrictions restrictions = 6; } diff --git a/protocol/proto/es_context_page.proto b/protocol/proto/es_context_page.proto index f4cc6930..28776cfd 100644 --- a/protocol/proto/es_context_page.proto +++ b/protocol/proto/es_context_page.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -15,5 +15,5 @@ message ContextPage { map metadata = 2; string page_url = 3; string next_page_url = 4; - bool is_loaded = 5; + bool is_loading = 5; } diff --git a/protocol/proto/es_context_player_error.proto b/protocol/proto/es_context_player_error.proto index f332fe8a..bd6ded16 100644 --- a/protocol/proto/es_context_player_error.proto +++ b/protocol/proto/es_context_player_error.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,7 +9,6 @@ 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; @@ -48,8 +47,15 @@ message ContextPlayerError { TIMEOUT = 34; PLAYBACK_REPORTING_ERROR = 35; UNKNOWN = 36; + ADD_TO_QUEUE_RESTRICTED = 37; + PICK_AND_SHUFFLE_CAPPED = 38; + PICK_AND_SHUFFLE_CONNECT_RESTRICTED = 39; + CONTEXT_LOADING_FAILED = 40; + AUDIOBOOK_NOT_PLAYABLE = 41; + SIGNAL_NOT_AVAILABLE = 42; } - + + ErrorCode code = 1; string message = 2; map data = 3; } diff --git a/protocol/proto/es_context_player_options.proto b/protocol/proto/es_context_player_options.proto index 372b53c0..337f8596 100644 --- a/protocol/proto/es_context_player_options.proto +++ b/protocol/proto/es_context_player_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -14,10 +14,14 @@ message ContextPlayerOptions { bool shuffling_context = 1; bool repeating_context = 2; bool repeating_track = 3; + map modes = 5; + optional float playback_speed = 4; } message ContextPlayerOptionOverrides { OptionalBoolean shuffling_context = 1; OptionalBoolean repeating_context = 2; OptionalBoolean repeating_track = 3; + map modes = 5; + optional float playback_speed = 4; } diff --git a/protocol/proto/es_context_player_state.proto b/protocol/proto/es_context_player_state.proto index f1626572..c0023411 100644 --- a/protocol/proto/es_context_player_state.proto +++ b/protocol/proto/es_context_player_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -21,7 +21,6 @@ message ContextIndex { } message PlaybackQuality { - BitrateLevel bitrate_level = 1; enum BitrateLevel { UNKNOWN = 0; LOW = 1; @@ -29,9 +28,9 @@ message PlaybackQuality { HIGH = 3; VERY_HIGH = 4; HIFI = 5; + HIFI24 = 6; } - - BitrateStrategy strategy = 2; + enum BitrateStrategy { UNKNOWN_STRATEGY = 0; BEST_MATCHING = 1; @@ -40,16 +39,18 @@ message PlaybackQuality { 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; } + + BitrateLevel bitrate_level = 1; + BitrateStrategy strategy = 2; + BitrateLevel target_bitrate_level = 3; + bool target_bitrate_available = 4; + HiFiStatus hifi_status = 5; } message ContextPlayerState { @@ -79,4 +80,6 @@ message ContextPlayerState { string session_id = 24; uint64 queue_revision = 25; PreparePlayOptions.AudioStream audio_stream = 26; + repeated string signals = 27; + string session_command_id = 28; } diff --git a/protocol/proto/es_context_track.proto b/protocol/proto/es_context_track.proto index cdcbd7c2..c80e29ac 100644 --- a/protocol/proto/es_context_track.proto +++ b/protocol/proto/es_context_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_delete_session.proto b/protocol/proto/es_delete_session.proto index e45893c4..140f62a4 100644 --- a/protocol/proto/es_delete_session.proto +++ b/protocol/proto/es_delete_session.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,5 +13,4 @@ message DeleteSessionRequest { } message DeleteSessionResponse { - } diff --git a/protocol/proto/es_get_error_request.proto b/protocol/proto/es_get_error_request.proto index 3119beaa..f62feff8 100644 --- a/protocol/proto/es_get_error_request.proto +++ b/protocol/proto/es_get_error_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,5 +9,4 @@ 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 index 08bb053c..e887b3c4 100644 --- a/protocol/proto/es_get_play_history.proto +++ b/protocol/proto/es_get_play_history.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,7 +11,6 @@ option optimize_for = CODE_SIZE; option java_package = "com.spotify.player.esperanto.proto"; message GetPlayHistoryRequest { - } message GetPlayHistoryResponse { diff --git a/protocol/proto/es_get_position_state.proto b/protocol/proto/es_get_position_state.proto index 6147f0c5..4018d996 100644 --- a/protocol/proto/es_get_position_state.proto +++ b/protocol/proto/es_get_position_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,16 +9,15 @@ 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; } - + + Error error = 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 index 68b6830a..65a2a24a 100644 --- a/protocol/proto/es_get_queue_request.proto +++ b/protocol/proto/es_get_queue_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,5 +9,4 @@ 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 index d8cd5335..b4d29dce 100644 --- a/protocol/proto/es_get_state_request.proto +++ b/protocol/proto/es_get_state_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_optional.proto b/protocol/proto/es_optional.proto index 2ca0b01f..2f75c54c 100644 --- a/protocol/proto/es_optional.proto +++ b/protocol/proto/es_optional.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_pause.proto b/protocol/proto/es_pause.proto index 56378fb5..7e8c4955 100644 --- a/protocol/proto/es_pause.proto +++ b/protocol/proto/es_pause.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,6 +6,7 @@ package spotify.player.esperanto.proto; import "es_command_options.proto"; import "es_logging_params.proto"; +import "es_pauseresume_origin.proto"; option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; @@ -14,4 +15,5 @@ option java_package = "com.spotify.player.esperanto.proto"; message PauseRequest { CommandOptions options = 1; LoggingParams logging_params = 2; + PauseResumeOrigin pause_origin = 3; } diff --git a/protocol/proto/es_pauseresume_origin.proto b/protocol/proto/es_pauseresume_origin.proto new file mode 100644 index 00000000..20446a20 --- /dev/null +++ b/protocol/proto/es_pauseresume_origin.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package spotify.player.esperanto.proto; + +option java_package = "com.spotify.player.esperanto.proto"; +option objc_class_prefix = "ESP"; + +message PauseResumeOrigin { + string feature_identifier = 1; +} + diff --git a/protocol/proto/es_play.proto b/protocol/proto/es_play.proto index 34dca48a..ed7196ca 100644 --- a/protocol/proto/es_play.proto +++ b/protocol/proto/es_play.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_play_origin.proto b/protocol/proto/es_play_origin.proto index 62cff8b7..c9d8004a 100644 --- a/protocol/proto/es_play_origin.proto +++ b/protocol/proto/es_play_origin.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -16,4 +16,5 @@ message PlayOrigin { string referrer_identifier = 5; string device_identifier = 6; repeated string feature_classes = 7; + string restriction_identifier = 8; } diff --git a/protocol/proto/es_prefs.proto b/protocol/proto/es_prefs.proto index f81916ca..220d8942 100644 --- a/protocol/proto/es_prefs.proto +++ b/protocol/proto/es_prefs.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -25,11 +25,9 @@ message SubParams { } message GetAllParams { - } message SubAllParams { - } message Value { diff --git a/protocol/proto/es_prepare_play.proto b/protocol/proto/es_prepare_play.proto index 6662eb65..bee7d510 100644 --- a/protocol/proto/es_prepare_play.proto +++ b/protocol/proto/es_prepare_play.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_prepare_play_options.proto b/protocol/proto/es_prepare_play_options.proto index b4a4449c..de179814 100644 --- a/protocol/proto/es_prepare_play_options.proto +++ b/protocol/proto/es_prepare_play_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -21,19 +21,19 @@ message PreparePlayOptions { 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 index 6dcffc0d..76e86c4c 100644 --- a/protocol/proto/es_provided_track.proto +++ b/protocol/proto/es_provided_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_queue.proto b/protocol/proto/es_queue.proto index 625b184d..2f7a7b67 100644 --- a/protocol/proto/es_queue.proto +++ b/protocol/proto/es_queue.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_remote_config.proto b/protocol/proto/es_remote_config.proto index fca7f0f9..adf31a39 100644 --- a/protocol/proto/es_remote_config.proto +++ b/protocol/proto/es_remote_config.proto @@ -1,21 +1,33 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.remote_config.esperanto.proto; +import "esperanto_options.proto"; + option objc_class_prefix = "ESP"; option java_package = "com.spotify.remoteconfig.esperanto.proto"; service RemoteConfig { - rpc lookupBool(LookupRequest) returns (BoolResponse); + rpc lookupBool(LookupRequest) returns (.spotify.remote_config.esperanto.proto.BoolResponse) {} + rpc lookupInt(LookupRequest) returns (.spotify.remote_config.esperanto.proto.IntResponse) {} + rpc lookupEnum(LookupRequest) returns (.spotify.remote_config.esperanto.proto.EnumResponse) {} } message LookupRequest { - string component_id = 1; - string key = 2; + string scope = 1; + string name = 2; } message BoolResponse { - bool value = 1; + optional bool value = 1; +} + +message IntResponse { + optional int32 value = 1; +} + +message EnumResponse { + optional string value = 1; } diff --git a/protocol/proto/es_request_info.proto b/protocol/proto/es_request_info.proto index 95b5cb81..91a659c9 100644 --- a/protocol/proto/es_request_info.proto +++ b/protocol/proto/es_request_info.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -24,4 +24,6 @@ message RequestInfo { int64 event_first_byte_received = 11; int64 event_last_byte_received = 12; int64 event_ended = 13; + string protocol = 14; + int64 event_redirects_done = 15; } diff --git a/protocol/proto/es_response_with_reasons.proto b/protocol/proto/es_response_with_reasons.proto index 6570a177..d12fcd25 100644 --- a/protocol/proto/es_response_with_reasons.proto +++ b/protocol/proto/es_response_with_reasons.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,13 +9,13 @@ 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; } - + + Error error = 1; string reasons = 2; } diff --git a/protocol/proto/es_restrictions.proto b/protocol/proto/es_restrictions.proto index 3a5c3a0a..225cc9c2 100644 --- a/protocol/proto/es_restrictions.proto +++ b/protocol/proto/es_restrictions.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,6 +8,14 @@ option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; option java_package = "com.spotify.player.esperanto.proto"; +message ModeRestrictions { + map values = 1; +} + +message RestrictionReasons { + repeated string reasons = 1; +} + message Restrictions { repeated string disallow_pausing_reasons = 1; repeated string disallow_resuming_reasons = 2; @@ -30,4 +38,8 @@ message Restrictions { 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_add_to_queue_reasons = 22; + repeated string disallow_setting_playback_speed_reasons = 23; + map disallow_setting_modes = 25; + map disallow_signals = 26; } diff --git a/protocol/proto/es_resume.proto b/protocol/proto/es_resume.proto index 1af5980f..257a5b9f 100644 --- a/protocol/proto/es_resume.proto +++ b/protocol/proto/es_resume.proto @@ -1,17 +1,19 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (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"; +import "es_command_options.proto"; +import "es_logging_params.proto"; +import "es_pauseresume_origin.proto"; + message ResumeRequest { CommandOptions options = 1; LoggingParams logging_params = 2; + PauseResumeOrigin resume_origin = 3; } diff --git a/protocol/proto/es_seek_to.proto b/protocol/proto/es_seek_to.proto index 59073cf9..a185a7d9 100644 --- a/protocol/proto/es_seek_to.proto +++ b/protocol/proto/es_seek_to.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -12,14 +12,15 @@ 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; - - Relative relative = 4; enum Relative { BEGINNING = 0; END = 1; CURRENT = 2; } + + CommandOptions options = 1; + LoggingParams logging_params = 2; + int64 position = 3; + Relative relative = 4; } + diff --git a/protocol/proto/es_session_response.proto b/protocol/proto/es_session_response.proto index 692ae30f..26f599b4 100644 --- a/protocol/proto/es_session_response.proto +++ b/protocol/proto/es_session_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_set_options.proto b/protocol/proto/es_set_options.proto index 33faf5f8..f4c2f438 100644 --- a/protocol/proto/es_set_options.proto +++ b/protocol/proto/es_set_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -18,4 +18,6 @@ message SetOptionsRequest { OptionalBoolean shuffling_context = 3; CommandOptions options = 4; LoggingParams logging_params = 5; + map modes = 7; + optional float playback_speed = 6; } diff --git a/protocol/proto/es_set_queue_request.proto b/protocol/proto/es_set_queue_request.proto index 83715232..7cc6dd17 100644 --- a/protocol/proto/es_set_queue_request.proto +++ b/protocol/proto/es_set_queue_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_set_repeating_context.proto b/protocol/proto/es_set_repeating_context.proto index 25667c81..2f2d6571 100644 --- a/protocol/proto/es_set_repeating_context.proto +++ b/protocol/proto/es_set_repeating_context.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_set_repeating_track.proto b/protocol/proto/es_set_repeating_track.proto index 01ae3b56..dd0588be 100644 --- a/protocol/proto/es_set_repeating_track.proto +++ b/protocol/proto/es_set_repeating_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_set_shuffling_context.proto b/protocol/proto/es_set_shuffling_context.proto index 6eb779e6..9a1a470c 100644 --- a/protocol/proto/es_set_shuffling_context.proto +++ b/protocol/proto/es_set_shuffling_context.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_skip_next.proto b/protocol/proto/es_skip_next.proto index d6b0dc83..2761b0b9 100644 --- a/protocol/proto/es_skip_next.proto +++ b/protocol/proto/es_skip_next.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_skip_prev.proto b/protocol/proto/es_skip_prev.proto index 2a6b9a71..da354be2 100644 --- a/protocol/proto/es_skip_prev.proto +++ b/protocol/proto/es_skip_prev.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_skip_to_track.proto b/protocol/proto/es_skip_to_track.proto index ecf0d03f..ba588811 100644 --- a/protocol/proto/es_skip_to_track.proto +++ b/protocol/proto/es_skip_to_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/es_stop.proto b/protocol/proto/es_stop.proto index 068490e0..cb3760fc 100644 --- a/protocol/proto/es_stop.proto +++ b/protocol/proto/es_stop.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,13 +13,13 @@ option java_package = "com.spotify.player.esperanto.proto"; message StopRequest { CommandOptions options = 1; - - Reason reason = 2; + + StopRequest.Reason reason = 2; enum Reason { INTERACTIVE = 0; REMOTE_TRANSFER = 1; SHUTDOWN = 2; } - + LoggingParams logging_params = 3; } diff --git a/protocol/proto/es_storage.proto b/protocol/proto/es_storage.proto index c20b3be7..c7d35f71 100644 --- a/protocol/proto/es_storage.proto +++ b/protocol/proto/es_storage.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,8 +6,8 @@ package spotify.storage.esperanto.proto; import "google/protobuf/empty.proto"; -option objc_class_prefix = "ESP"; option java_package = "com.spotify.storage.esperanto.proto"; +option objc_class_prefix = "ESP"; service Storage { rpc GetCacheSizeLimit(GetCacheSizeLimitParams) returns (CacheSizeLimit); @@ -23,7 +23,6 @@ message CacheSizeLimit { } message GetCacheSizeLimitParams { - } message SetCacheSizeLimitParams { @@ -31,11 +30,9 @@ message SetCacheSizeLimitParams { } message DeleteExpiredItemsParams { - } message DeleteUnlockedItemsParams { - } message RealmStats { @@ -58,18 +55,17 @@ message Stats { } 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; } + + bool byte_size_known = 1; + uint64 byte_size = 2; + repeated Range ranges = 3; } message GetFileRangesParams { diff --git a/protocol/proto/es_update.proto b/protocol/proto/es_update.proto index 90734b5d..498a0d26 100644 --- a/protocol/proto/es_update.proto +++ b/protocol/proto/es_update.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/esperanto_options.proto b/protocol/proto/esperanto_options.proto new file mode 100644 index 00000000..5c914f06 --- /dev/null +++ b/protocol/proto/esperanto_options.proto @@ -0,0 +1,6 @@ +syntax = "proto3"; + +package spotify.esperanto; + +import "google/protobuf/descriptor.proto"; + diff --git a/protocol/proto/event_entity.proto b/protocol/proto/event_entity.proto index 06239d59..b926ba6b 100644 --- a/protocol/proto/event_entity.proto +++ b/protocol/proto/event_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/explicit_content_pubsub.proto b/protocol/proto/explicit_content_pubsub.proto index 1bb45f91..14679488 100644 --- a/protocol/proto/explicit_content_pubsub.proto +++ b/protocol/proto/explicit_content_pubsub.proto @@ -6,11 +6,6 @@ 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; + map pairs = 1; } diff --git a/protocol/proto/extended_metadata.proto b/protocol/proto/extended_metadata.proto index 2e38d28d..b48b34eb 100644 --- a/protocol/proto/extended_metadata.proto +++ b/protocol/proto/extended_metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -47,7 +47,6 @@ message EntityExtensionDataArray { } message BatchedExtensionResponseHeader { - } message BatchedExtensionResponse { diff --git a/protocol/proto/extension_kind.proto b/protocol/proto/extension_kind.proto index 02444dea..6bb8182b 100644 --- a/protocol/proto/extension_kind.proto +++ b/protocol/proto/extension_kind.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -18,6 +18,7 @@ enum ExtensionKind { PODCAST_SEGMENTS = 4; AUDIO_FILES = 5; TRACK_DESCRIPTOR = 6; + PODCAST_COUNTER = 7; ARTIST_V4 = 8; ALBUM_V4 = 9; TRACK_V4 = 10; @@ -44,11 +45,166 @@ enum ExtensionKind { SHOW_ACCESS = 31; PODCAST_QNA = 32; CLIPS = 33; + SHOW_V5 = 34; + EPISODE_V5 = 35; PODCAST_CTA_CARDS = 36; PODCAST_RATING = 37; DISPLAY_SEGMENTS = 38; GREENROOM = 39; USER_CREATED = 40; + SHOW_DESCRIPTION = 41; + SHOW_HTML_DESCRIPTION = 42; + SHOW_PLAYABILITY = 43; + EPISODE_DESCRIPTION = 44; + EPISODE_HTML_DESCRIPTION = 45; + EPISODE_PLAYABILITY = 46; + SHOW_EPISODES_ASSOC = 47; CLIENT_CONFIG = 48; + PLAYLISTABILITY = 49; + AUDIOBOOK_V5 = 50; + CHAPTER_V5 = 51; AUDIOBOOK_SPECIFICS = 52; + EPISODE_RANKING = 53; + HTML_DESCRIPTION = 54; + CREATOR_CHANNEL = 55; + AUDIOBOOK_PROVIDERS = 56; + PLAY_TRAIT = 57; + CONTENT_WARNING = 58; + IMAGE_CUE = 59; + STREAM_COUNT = 60; + AUDIO_ATTRIBUTES = 61; + NAVIGABLE_TRAIT = 62; + NEXT_BEST_EPISODE = 63; + AUDIOBOOK_PRICE = 64; + EXPRESSIVE_PLAYLISTS = 65; + DYNAMIC_SHOW_EPISODE = 66; + LIVE = 67; + SKIP_PLAYED = 68; + AD_BREAK_FREE_PODCASTS = 69; + ASSOCIATIONS = 70; + PLAYLIST_EVALUATION = 71; + CACHE_INVALIDATIONS = 72; + LIVESTREAM_ENTITY = 73; + SINGLE_TAP_REACTIONS = 74; + USER_COMMENTS = 75; + CLIENT_RESTRICTIONS = 76; + PODCAST_GUEST = 77; + PLAYABILITY = 78; + COVER_IMAGE = 79; + SHARE_TRAIT = 80; + INSTANCE_SHARING = 81; + ARTIST_TOUR = 82; + AUDIOBOOK_GENRE = 83; + CONCEPT = 84; + ORIGINAL_VIDEO = 85; + SMART_SHUFFLE = 86; + LIVE_EVENTS = 87; + AUDIOBOOK_RELATIONS = 88; + HOME_POC_BASECARD = 89; + AUDIOBOOK_SUPPLEMENTS = 90; + PAID_PODCAST_BANNER = 91; + FEWER_ADS = 92; + WATCH_FEED_SHOW_EXPLORER = 93; + TRACK_EXTRA_DESCRIPTORS = 94; + TRACK_EXTRA_AUDIO_ATTRIBUTES = 95; + TRACK_EXTENDED_CREDITS = 96; + SIMPLE_TRAIT = 97; + AUDIO_ASSOCIATIONS = 98; + VIDEO_ASSOCIATIONS = 99; + PLAYLIST_TUNER = 100; + ARTIST_VIDEOS_ENTRYPOINT = 101; + ALBUM_PRERELEASE = 102; + CONTENT_ALTERNATIVES = 103; + SNAPSHOT_SHARING = 105; + DISPLAY_SEGMENTS_COUNT = 106; + PODCAST_FEATURED_EPISODE = 107; + PODCAST_SPONSORED_CONTENT = 108; + PODCAST_EPISODE_TOPICS_LLM = 109; + PODCAST_EPISODE_TOPICS_KG = 110; + EPISODE_RANKING_POPULARITY = 111; + MERCH = 112; + COMPANION_CONTENT = 113; + WATCH_FEED_ENTITY_EXPLORER = 114; + ANCHOR_CARD_TRAIT = 115; + AUDIO_PREVIEW_PLAYBACK_TRAIT = 116; + VIDEO_PREVIEW_STILL_TRAIT = 117; + PREVIEW_CARD_TRAIT = 118; + SHORTCUTS_CARD_TRAIT = 119; + VIDEO_PREVIEW_PLAYBACK_TRAIT = 120; + COURSE_SPECIFICS = 121; + CONCERT = 122; + CONCERT_LOCATION = 123; + CONCERT_MARKETING = 124; + CONCERT_PERFORMERS = 125; + TRACK_PAIR_TRANSITION = 126; + CONTENT_TYPE_TRAIT = 127; + NAME_TRAIT = 128; + ARTWORK_TRAIT = 129; + RELEASE_DATE_TRAIT = 130; + CREDITS_TRAIT = 131; + RELEASE_URI_TRAIT = 132; + ENTITY_CAPPING = 133; + LESSON_SPECIFICS = 134; + CONCERT_OFFERS = 135; + TRANSITION_MAPS = 136; + ARTIST_HAS_CONCERTS = 137; + PRERELEASE = 138; + PLAYLIST_ATTRIBUTES_V2 = 139; + LIST_ATTRIBUTES_V2 = 140; + LIST_METADATA = 141; + LIST_TUNER_AUDIO_ANALYSIS = 142; + LIST_TUNER_CUEPOINTS = 143; + CONTENT_RATING_TRAIT = 144; + COPYRIGHT_TRAIT = 145; + SUPPORTED_BADGES = 146; + BADGES = 147; + PREVIEW_TRAIT = 148; + ROOTLISTABILITY_TRAIT = 149; + LOCAL_CONCERTS = 150; + RECOMMENDED_PLAYLISTS = 151; + POPULAR_RELEASES = 152; + RELATED_RELEASES = 153; + SHARE_RESTRICTIONS = 154; + CONCERT_OFFER = 155; + CONCERT_OFFER_PROVIDER = 156; + ENTITY_BOOKMARKS = 157; + PRIVACY_TRAIT = 158; + DUPLICATE_ITEMS_TRAIT = 159; + REORDERING_TRAIT = 160; + PODCAST_RESUMPTION_SEGMENTS = 161; + ARTIST_EXPRESSION_VIDEO = 162; + PRERELEASE_VIDEO = 163; + GATED_ENTITY_RELATIONS = 164; + RELATED_CREATORS_SECTION = 165; + CREATORS_APPEARS_ON_SECTION = 166; + PROMO_V1_TRAIT = 167; + SPEECHLESS_SHARE_CARD = 168; + TOP_PLAYABLES_SECTION = 169; + AUTO_LENS = 170; + PROMO_V3_TRAIT = 171; + TRACK_CONTENT_FILTER = 172; + HIGHLIGHTABILITY = 173; + LINK_CARD_WITH_IMAGE_TRAIT = 174; + TRACK_CLOUD_SECTION = 175; + EPISODE_TOPICS = 176; + VIDEO_THUMBNAIL = 177; + IDENTITY_TRAIT = 178; + VISUAL_IDENTITY_TRAIT = 179; + CONTENT_TYPE_V2_TRAIT = 180; + PREVIEW_PLAYBACK_TRAIT = 181; + CONSUMPTION_EXPERIENCE_TRAIT = 182; + PUBLISHING_METADATA_TRAIT = 183; + DETAILED_EVALUATION_TRAIT = 184; + ON_PLATFORM_REPUTATION_TRAIT = 185; + CREDITS_V2_TRAIT = 186; + HIGHLIGHT_PLAYABILITY_TRAIT = 187; + SHOW_EPISODE_LIST = 188; + AVAILABLE_RELEASES = 189; + PLAYLIST_DESCRIPTORS = 190; + LINK_CARD_WITH_ANIMATIONS_TRAIT = 191; + RECAP = 192; + AUDIOBOOK_COMPANION_CONTENT = 193; + THREE_OH_THREE_PLAY_TRAIT = 194; + ARTIST_WRAPPED_2024_VIDEO = 195; } + diff --git a/protocol/proto/extracted_colors.proto b/protocol/proto/extracted_colors.proto index 999a27ea..cf8b8ca5 100644 --- a/protocol/proto/extracted_colors.proto +++ b/protocol/proto/extracted_colors.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/follow_request.proto b/protocol/proto/follow_request.proto index 5a026895..913573ee 100644 --- a/protocol/proto/follow_request.proto +++ b/protocol/proto/follow_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -16,6 +16,11 @@ message FollowRequest { bool follow = 2; } +message FollowRequestV4 { + 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 index afb71f43..a0d2dfc0 100644 --- a/protocol/proto/followed_users_request.proto +++ b/protocol/proto/followed_users_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/frecency.proto b/protocol/proto/frecency.proto index 89c6c7f6..3f875aa1 100644 --- a/protocol/proto/frecency.proto +++ b/protocol/proto/frecency.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/frecency_storage.proto b/protocol/proto/frecency_storage.proto index 9e32269f..f1b50487 100644 --- a/protocol/proto/frecency_storage.proto +++ b/protocol/proto/frecency_storage.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/gabito.proto b/protocol/proto/gabito.proto index b47f4fdd..3256727e 100644 --- a/protocol/proto/gabito.proto +++ b/protocol/proto/gabito.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,16 +8,16 @@ 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; } @@ -27,10 +27,11 @@ message PublishEventsRequest { } message PublishEventsResponse { - repeated EventError error = 1; message EventError { int32 index = 1; bool transient = 2; int32 reason = 3; } + + repeated EventError error = 1; } diff --git a/protocol/proto/global_node.proto b/protocol/proto/global_node.proto index cd6f1b6c..f54604a5 100644 --- a/protocol/proto/global_node.proto +++ b/protocol/proto/global_node.proto @@ -1,10 +1,11 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.player.proto; import "context_player_options.proto"; +import "pause_resume_origin.proto"; import "player_license.proto"; option optimize_for = CODE_SIZE; @@ -13,4 +14,6 @@ message GlobalNode { optional ContextPlayerOptions options = 1; optional PlayerLicense license = 2; map configuration = 3; + optional PauseResumeOrigin pause_resume_origin = 4; + optional bool is_paused = 5; } diff --git a/protocol/proto/google/protobuf/any.proto b/protocol/proto/google/protobuf/any.proto index bb7f136c..7a7e77fb 100644 --- a/protocol/proto/google/protobuf/any.proto +++ b/protocol/proto/google/protobuf/any.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/google/protobuf/descriptor.proto b/protocol/proto/google/protobuf/descriptor.proto index 884a5151..49ccd18b 100644 --- a/protocol/proto/google/protobuf/descriptor.proto +++ b/protocol/proto/google/protobuf/descriptor.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -29,6 +29,7 @@ message FileDescriptorProto { optional FileOptions options = 8; optional SourceCodeInfo source_code_info = 9; optional string syntax = 12; + optional Edition edition = 14; } message DescriptorProto { @@ -37,43 +38,58 @@ message DescriptorProto { 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 { + message Declaration { + reserved 4; + optional int32 number = 1; + optional string full_name = 2; + optional string type = 3; + optional bool reserved = 5; + optional bool repeated = 6; + } + + enum VerificationState { + DECLARATION = 0; + UNVERIFIED = 1; + } + repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; + repeated Declaration declaration = 2; + optional FeatureSet features = 50; + optional VerificationState verification = 3 [default = UNVERIFIED]; } 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; + LABEL_REQUIRED = 2; } - + optional Type type = 5; enum Type { TYPE_DOUBLE = 1; @@ -95,7 +111,7 @@ message FieldDescriptorProto { TYPE_SINT32 = 17; TYPE_SINT64 = 18; } - + optional string type_name = 6; optional string extendee = 2; optional string default_value = 7; @@ -111,16 +127,16 @@ message OneofDescriptorProto { } message EnumDescriptorProto { - optional string name = 1; + 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; } @@ -149,16 +165,16 @@ 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_generate_equals_and_hash = 20; 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]; @@ -173,10 +189,9 @@ message FileOptions { optional string php_namespace = 41; optional string php_metadata_namespace = 44; optional string ruby_package = 45; + optional FeatureSet features = 50; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; - + reserved 38; } @@ -185,10 +200,10 @@ message MessageOptions { optional bool no_standard_descriptor_accessor = 2 [default = false]; optional bool deprecated = 3 [default = false]; optional bool map_entry = 7; + optional bool deprecated_legacy_json_field_conflicts = 11; + optional FeatureSet features = 12; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; - + reserved 4, 5, 6, 8, 9; } @@ -199,78 +214,105 @@ message FieldOptions { 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 unverified_lazy = 15 [default = false]; optional bool deprecated = 3 [default = false]; optional bool weak = 10 [default = false]; + optional bool debug_redact = 16 [default = false]; + + optional OptionRetention retention = 17; + enum OptionRetention { + RETENTION_UNKNOWN = 0; + RETENTION_RUNTIME = 1; + RETENTION_SOURCE = 2; + } + + repeated OptionTargetType targets = 19; + enum OptionTargetType { + TARGET_TYPE_UNKNOWN = 0; + TARGET_TYPE_FILE = 1; + TARGET_TYPE_EXTENSION_RANGE = 2; + TARGET_TYPE_MESSAGE = 3; + TARGET_TYPE_FIELD = 4; + TARGET_TYPE_ONEOF = 5; + TARGET_TYPE_ENUM = 6; + TARGET_TYPE_ENUM_ENTRY = 7; + TARGET_TYPE_SERVICE = 8; + TARGET_TYPE_METHOD = 9; + } + + + repeated EditionDefault edition_defaults = 20; + message EditionDefault { + optional Edition edition = 3; + optional string value = 2; + } + + optional FeatureSet features = 21; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; - - reserved 4; + + reserved 4, 18; } message OneofOptions { + optional FeatureSet features = 1; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; } message EnumOptions { optional bool allow_alias = 2; optional bool deprecated = 3 [default = false]; + optional bool deprecated_legacy_json_field_conflicts = 6; + optional FeatureSet features = 7; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; - + reserved 5; } message EnumValueOptions { optional bool deprecated = 1 [default = false]; + optional FeatureSet features = 2; + optional bool debug_redact = 3 [default = false]; repeated UninterpretedOption uninterpreted_option = 999; - - extensions 1000 to max; } message ServiceOptions { + optional FeatureSet features = 34; 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; } - + + optional FeatureSet features = 35; 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; } - + + repeated UninterpretedOption.NamePart name = 2; optional string identifier_value = 3; optional uint64 positive_int_value = 4; optional int64 negative_int_value = 5; @@ -279,23 +321,103 @@ message UninterpretedOption { optional string aggregate_value = 8; } +message FeatureSet { + reserved 999; + enum FieldPresence { + FIELD_PRESENCE_UNKNOWN = 0; + EXPLICIT = 1; + IMPLICIT = 2; + LEGACY_REQUIRED = 3; + } + + enum EnumType { + ENUM_TYPE_UNKNOWN = 0; + OPEN = 1; + CLOSED = 2; + } + + enum RepeatedFieldEncoding { + REPEATED_FIELD_ENCODING_UNKNOWN = 0; + PACKED = 1; + EXPANDED = 2; + } + + enum Utf8Validation { + UTF8_VALIDATION_UNKNOWN = 0; + NONE = 1; + VERIFY = 2; + } + + enum MessageEncoding { + MESSAGE_ENCODING_UNKNOWN = 0; + LENGTH_PREFIXED = 1; + DELIMITED = 2; + } + + enum JsonFormat { + JSON_FORMAT_UNKNOWN = 0; + ALLOW = 1; + LEGACY_BEST_EFFORT = 2; + } + + optional FieldPresence field_presence = 1; + optional EnumType enum_type = 2; + optional RepeatedFieldEncoding repeated_field_encoding = 3; + optional Utf8Validation utf8_validation = 4; + optional MessageEncoding message_encoding = 5; + optional JsonFormat json_format = 6; +} + +message FeatureSetDefaults { + message FeatureSetEditionDefault { + optional Edition edition = 3; + optional FeatureSet features = 2; + } + + repeated FeatureSetDefaults.FeatureSetEditionDefault defaults = 1; + optional Edition minimum_edition = 4; + optional Edition maximum_edition = 5; +} + message SourceCodeInfo { - repeated Location location = 1; message Location { - repeated int32 path = 1 [packed = true]; - repeated int32 span = 2 [packed = true]; + repeated int32 path = 1; + repeated int32 span = 2; optional string leading_comments = 3; optional string trailing_comments = 4; repeated string leading_detached_comments = 6; } + + repeated Location location = 1; } message GeneratedCodeInfo { - repeated Annotation annotation = 1; message Annotation { - repeated int32 path = 1 [packed = true]; + enum Semantic { + NONE = 0; + SET = 1; + ALIAS = 2; + } + + repeated int32 path = 1; optional string source_file = 2; optional int32 begin = 3; optional int32 end = 4; + optional Annotation.Semantic semantic = 5; } + + repeated Annotation annotation = 1; } + +enum Edition { + EDITION_UNKNOWN = 0; + EDITION_PROTO2 = 998; + EDITION_PROTO3 = 999; + EDITION_2023 = 1000; + EDITION_1_TEST_ONLY = 1; + EDITION_2_TEST_ONLY = 2; + EDITION_99997_TEST_ONLY = 99997; + EDITION_99998_TEST_ONLY = 99998; + EDITION_99999_TEST_ONLY = 99999; +} + diff --git a/protocol/proto/google/protobuf/duration.proto b/protocol/proto/google/protobuf/duration.proto index f7d01301..dc76d4eb 100644 --- a/protocol/proto/google/protobuf/duration.proto +++ b/protocol/proto/google/protobuf/duration.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/google/protobuf/empty.proto b/protocol/proto/google/protobuf/empty.proto index 28c4d77b..25f74377 100644 --- a/protocol/proto/google/protobuf/empty.proto +++ b/protocol/proto/google/protobuf/empty.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,5 +13,5 @@ option java_outer_classname = "EmptyProto"; option java_package = "com.google.protobuf"; message Empty { - + } diff --git a/protocol/proto/google/protobuf/field_mask.proto b/protocol/proto/google/protobuf/field_mask.proto index 3ae48712..860a8709 100644 --- a/protocol/proto/google/protobuf/field_mask.proto +++ b/protocol/proto/google/protobuf/field_mask.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/google/protobuf/source_context.proto b/protocol/proto/google/protobuf/source_context.proto index 8449fd4b..e19c07cc 100644 --- a/protocol/proto/google/protobuf/source_context.proto +++ b/protocol/proto/google/protobuf/source_context.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/google/protobuf/timestamp.proto b/protocol/proto/google/protobuf/timestamp.proto index e402c47a..084f311a 100644 --- a/protocol/proto/google/protobuf/timestamp.proto +++ b/protocol/proto/google/protobuf/timestamp.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/google/protobuf/type.proto b/protocol/proto/google/protobuf/type.proto index 38d7f2d1..d7b79651 100644 --- a/protocol/proto/google/protobuf/type.proto +++ b/protocol/proto/google/protobuf/type.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -22,10 +22,10 @@ message Type { repeated Option options = 4; SourceContext source_context = 5; Syntax syntax = 6; + string edition = 7; } message Field { - Kind kind = 1; enum Kind { TYPE_UNKNOWN = 0; TYPE_DOUBLE = 1; @@ -47,15 +47,16 @@ message Field { TYPE_SINT32 = 17; TYPE_SINT64 = 18; } - - Cardinality cardinality = 2; + enum Cardinality { CARDINALITY_UNKNOWN = 0; CARDINALITY_OPTIONAL = 1; CARDINALITY_REQUIRED = 2; CARDINALITY_REPEATED = 3; } - + + Kind kind = 1; + Cardinality cardinality = 2; int32 number = 3; string name = 4; string type_url = 6; @@ -72,6 +73,7 @@ message Enum { repeated Option options = 3; SourceContext source_context = 4; Syntax syntax = 5; + string edition = 6; } message EnumValue { @@ -88,4 +90,5 @@ message Option { enum Syntax { SYNTAX_PROTO2 = 0; SYNTAX_PROTO3 = 1; + SYNTAX_EDITIONS = 2; } diff --git a/protocol/proto/google/protobuf/wrappers.proto b/protocol/proto/google/protobuf/wrappers.proto index 10e94ee0..647641a3 100644 --- a/protocol/proto/google/protobuf/wrappers.proto +++ b/protocol/proto/google/protobuf/wrappers.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/identity.proto b/protocol/proto/identity.proto index efd8b9e1..c6963862 100644 --- a/protocol/proto/identity.proto +++ b/protocol/proto/identity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -29,9 +29,25 @@ message UserProfile { google.protobuf.BoolValue has_spotify_name = 9; google.protobuf.BoolValue has_spotify_image = 10; google.protobuf.Int32Value color = 11; + google.protobuf.BoolValue is_private = 12; + google.protobuf.StringValue pronouns = 13; + google.protobuf.StringValue location = 14; + google.protobuf.StringValue bio = 15; + google.protobuf.BoolValue abuse_reported_bio = 17; + google.protobuf.BoolValue edit_name_disabled = 18; + google.protobuf.BoolValue edit_image_disabled = 19; + google.protobuf.BoolValue edit_bio_disabled = 20; + google.protobuf.BoolValue is_kid = 21; } message UserProfileUpdateRequest { google.protobuf.FieldMask mask = 1; UserProfile user_profile = 2; + bool skip_emit_events = 3; } + +message UserProfileChangedEvent { + string userid = 1; + UserProfile user_profile = 2; +} + diff --git a/protocol/proto/installation_data.proto b/protocol/proto/installation_data.proto index 083fe466..f0451981 100644 --- a/protocol/proto/installation_data.proto +++ b/protocol/proto/installation_data.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -12,7 +12,7 @@ message InstallationEntity { } message InstallationData { - string installation_id = 1; - string last_seen_device_id = 2; + bytes installation_id = 1; + bytes last_seen_device_id = 2; int64 monotonic_clock_id = 3; } diff --git a/protocol/proto/instrumentation_params.proto b/protocol/proto/instrumentation_params.proto index b8e44f4a..c317f16d 100644 --- a/protocol/proto/instrumentation_params.proto +++ b/protocol/proto/instrumentation_params.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/lens-model.proto b/protocol/proto/lens-model.proto new file mode 100644 index 00000000..aa85defc --- /dev/null +++ b/protocol/proto/lens-model.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto3"; + +package spotify.lens.model.proto; + +option java_package = "com.spotify.lens.model.proto"; +option java_outer_classname = "LensModelProto"; +option optimize_for = CODE_SIZE; + +message Lens { + string identifier = 1; +} + +message LensState { + string identifier = 1; + bytes revision = 2; +} + diff --git a/protocol/proto/lfs_secret_provider.proto b/protocol/proto/lfs_secret_provider.proto index 0862181e..782d19d0 100644 --- a/protocol/proto/lfs_secret_provider.proto +++ b/protocol/proto/lfs_secret_provider.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/liked_songs_tags_sync_state.proto b/protocol/proto/liked_songs_tags_sync_state.proto index 634f9d03..60bea86a 100644 --- a/protocol/proto/liked_songs_tags_sync_state.proto +++ b/protocol/proto/liked_songs_tags_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/listen_later_cosmos_response.proto b/protocol/proto/listen_later_cosmos_response.proto index f71c577c..c35615f3 100644 --- a/protocol/proto/listen_later_cosmos_response.proto +++ b/protocol/proto/listen_later_cosmos_response.proto @@ -1,14 +1,17 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (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 java_package = "spotify.listen_later_cosmos.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; message Episode { @@ -25,4 +28,6 @@ message EpisodesResponse { repeated Episode episode = 3; optional string offline_availability = 5; optional uint32 offline_progress = 6; + optional uint32 status_code = 98; + optional string error = 99; } diff --git a/protocol/proto/local_bans_storage.proto b/protocol/proto/local_bans_storage.proto index 388f05b5..d40dca71 100644 --- a/protocol/proto/local_bans_storage.proto +++ b/protocol/proto/local_bans_storage.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -12,6 +12,10 @@ message BanItem { required int64 timestamp = 3; } +message LocalBansTimestamp { + required int64 timestamp = 1; +} + message Bans { repeated BanItem items = 1; } diff --git a/protocol/proto/local_sync_state.proto b/protocol/proto/local_sync_state.proto index 630f1843..6c096926 100644 --- a/protocol/proto/local_sync_state.proto +++ b/protocol/proto/local_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/logging_params.proto b/protocol/proto/logging_params.proto index 1f11809d..545d7b64 100644 --- a/protocol/proto/logging_params.proto +++ b/protocol/proto/logging_params.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -11,4 +11,6 @@ message LoggingParams { optional int64 command_received_time = 2; repeated string page_instance_ids = 3; repeated string interaction_ids = 4; + optional string device_identifier = 5; + optional string command_id = 6; } diff --git a/protocol/proto/mdata.proto b/protocol/proto/mdata.proto index 29faad9c..5045d868 100644 --- a/protocol/proto/mdata.proto +++ b/protocol/proto/mdata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -20,12 +20,6 @@ message LocalBatchedEntityRequest { } 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; @@ -33,11 +27,20 @@ message LocalBatchedExtensionResponse { bool is_empty = 4; int64 cache_expiry_timestamp = 5; int64 offline_expiry_timestamp = 6; + string etag = 7; } - + message EntityExtension { string entity_uri = 1; ExtensionHeader header = 2; google.protobuf.Any extension_data = 3; } + + message Extension { + extendedmetadata.ExtensionKind extension_kind = 1; + repeated EntityExtension entity_extension = 2; + } + + repeated Extension extension = 1; } + diff --git a/protocol/proto/mdata_cosmos.proto b/protocol/proto/mdata_cosmos.proto index 3c67357c..2639ae9f 100644 --- a/protocol/proto/mdata_cosmos.proto +++ b/protocol/proto/mdata_cosmos.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -17,5 +17,4 @@ message InvalidateCacheRequest { } message InvalidateCacheResponse { - } diff --git a/protocol/proto/mdata_storage.proto b/protocol/proto/mdata_storage.proto index 8703fe54..d293ff7b 100644 --- a/protocol/proto/mdata_storage.proto +++ b/protocol/proto/mdata_storage.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -23,6 +23,7 @@ message CacheInfo { string etag = 5; fixed64 cache_checksum_lo = 6; fixed64 cache_checksum_hi = 7; + uint64 last_modified = 8; } message OfflineLock { diff --git a/protocol/proto/media.proto b/protocol/proto/media.proto new file mode 100644 index 00000000..5be102bf --- /dev/null +++ b/protocol/proto/media.proto @@ -0,0 +1,19 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto3"; + +package spotify.common.media; + +option java_package = "com.spotify.common.proto"; +option optimize_for = CODE_SIZE; + +enum AudioQuality { + DEFAULT = 0; + LOW = 1; + NORMAL = 2; + HIGH = 3; + VERY_HIGH = 4; + HIFI = 5; + HIFI_24 = 6; +} + diff --git a/protocol/proto/media_manifest.proto b/protocol/proto/media_manifest.proto index e9fe45fd..b6f32e77 100644 --- a/protocol/proto/media_manifest.proto +++ b/protocol/proto/media_manifest.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -18,38 +18,39 @@ message AudioFile { MP3_160_ENC = 7; AAC_24 = 8; AAC_48 = 9; - AAC_160 = 10; - AAC_320 = 11; - MP4_128 = 12; - OTHER5 = 13; 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; bytes body = 4; - oneof endpoint { string url = 2; string service = 3; } + optional bool disable_range_requests = 5; } - + message FileIdFile { string file_id_hex = 1; AudioFile.Format download_format = 2; EncryptionType encryption = 3; } + + message NormalizationParams { + float loudness_db = 1; + float true_peak_db = 2; + } + + int32 bitrate = 3; + string mime_type = 4; + oneof file { + ExternalFile external_file = 1; + FileIdFile file_id_file = 2; + } + optional NormalizationParams normalization_params = 5; } message Files { diff --git a/protocol/proto/media_type.proto b/protocol/proto/media_type.proto index 2d8def46..a2f9a41b 100644 --- a/protocol/proto/media_type.proto +++ b/protocol/proto/media_type.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -10,4 +10,7 @@ option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum MediaType { AUDIO = 0; VIDEO = 1; + MEDIA_TYPE_AUDIO = 0; + MEDIA_TYPE_VIDEO = 1; + MEDIA_TYPE_UNKNOWN = 2; } diff --git a/protocol/proto/members_request.proto b/protocol/proto/members_request.proto index 931f91d3..486eff7c 100644 --- a/protocol/proto/members_request.proto +++ b/protocol/proto/members_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/members_response.proto b/protocol/proto/members_response.proto index f341a8d2..386228ab 100644 --- a/protocol/proto/members_response.proto +++ b/protocol/proto/members_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata.proto b/protocol/proto/metadata.proto index 4c52e0ed..fa7aec95 100644 --- a/protocol/proto/metadata.proto +++ b/protocol/proto/metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -9,6 +9,7 @@ option java_outer_classname = "Metadata"; option java_package = "com.spotify.metadata.proto"; message Artist { + reserved 9; optional bytes gid = 1; optional string name = 2; optional sint32 popularity = 3; @@ -17,7 +18,6 @@ message Artist { 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; @@ -31,10 +31,12 @@ message Artist { } message Album { + reserved 8; + optional bytes gid = 1; optional string name = 2; repeated Artist artist = 3; - + optional Type type = 4; enum Type { ALBUM = 1; @@ -44,11 +46,10 @@ message Album { AUDIOBOOK = 5; PODCAST = 6; } - + 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; @@ -86,17 +87,16 @@ message Track { repeated Availability availability = 19; optional Licensor licensor = 21; repeated string language_of_performance = 22; + optional Audio original_audio = 24; repeated ContentRating content_rating = 25; optional string original_title = 27; optional string version_title = 28; repeated ArtistWithRole artist_with_role = 32; + optional string canonical_uri = 36; + repeated Video original_video = 38; } 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; @@ -107,6 +107,10 @@ message ArtistWithRole { ARTIST_ROLE_CONDUCTOR = 6; ARTIST_ROLE_ORCHESTRA = 7; } + + optional bytes artist_gid = 1; + optional string artist_name = 2; + optional ArtistRole role = 3; } message Show { @@ -122,25 +126,26 @@ message Show { 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; optional bool is_audiobook = 89; + optional bool is_creator_channel = 90; } message Episode { @@ -165,23 +170,29 @@ message Episode { optional bool allow_background_playback = 81; repeated Availability availability = 82; optional string external_url = 83; - - optional EpisodeType type = 87; + optional Audio original_audio = 84; + + optional Episode.EpisodeType type = 87; enum EpisodeType { FULL = 0; TRAILER = 1; BONUS = 2; } - + optional bool music_and_talk = 91; repeated ContentRating content_rating = 95; optional bool is_audiobook_chapter = 96; + optional bool is_podcast_short = 97; } message Licensor { optional bytes uuid = 1; } +message Audio { + optional bytes uuid = 1; +} + message TopTracks { optional string country = 1; repeated Track track = 2; @@ -206,16 +217,15 @@ message Date { } message Image { - optional bytes file_id = 1; - - optional Size size = 2; enum Size { DEFAULT = 0; SMALL = 1; LARGE = 2; XLARGE = 3; } - + + optional bytes file_id = 1; + optional Size size = 2; optional sint32 width = 3; optional sint32 height = 4; } @@ -237,17 +247,16 @@ message Disc { } message Copyright { - optional Type type = 1; enum Type { P = 0; C = 1; } - + + optional Type type = 1; optional string text = 2; } message Restriction { - repeated Catalogue catalogue = 1; enum Catalogue { AD = 0; SUBSCRIPTION = 1; @@ -255,14 +264,14 @@ message Restriction { SHUFFLE = 3; COMMERCIAL = 4; } - - optional Type type = 4; + enum Type { STREAMING = 0; } - + + repeated Catalogue catalogue = 1; + optional Type type = 4; repeated string catalogue_str = 5; - oneof country_restriction { string countries_allowed = 2; string countries_forbidden = 3; @@ -286,9 +295,6 @@ message ExternalId { } message AudioFile { - optional bytes file_id = 1; - - optional Format format = 2 [default = UNKNOWN_FORMAT]; enum Format { OGG_VORBIS_96 = 0; OGG_VORBIS_160 = 1; @@ -300,13 +306,19 @@ message AudioFile { MP3_160_ENC = 7; AAC_24 = 8; AAC_48 = 9; - AAC_160 = 10; - AAC_320 = 11; - MP4_128 = 12; - OTHER5 = 13; FLAC_FLAC = 16; - UNKNOWN_FORMAT = 255; + XHE_AAC_24 = 18; + XHE_AAC_16 = 19; + XHE_AAC_12 = 20; + FLAC_FLAC_24BIT = 22; } + + optional bytes file_id = 1; + optional Format format = 2; +} + +message Video { + optional bytes gid = 1; } message VideoFile { diff --git a/protocol/proto/metadata/album_metadata.proto b/protocol/proto/metadata/album_metadata.proto index 5a7de5f9..8df63e7d 100644 --- a/protocol/proto/metadata/album_metadata.proto +++ b/protocol/proto/metadata/album_metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata/artist_metadata.proto b/protocol/proto/metadata/artist_metadata.proto index 4e5e9bfe..8521fdde 100644 --- a/protocol/proto/metadata/artist_metadata.proto +++ b/protocol/proto/metadata/artist_metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata/episode_metadata.proto b/protocol/proto/metadata/episode_metadata.proto index 5d4a0b25..fa77b59d 100644 --- a/protocol/proto/metadata/episode_metadata.proto +++ b/protocol/proto/metadata/episode_metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -6,8 +6,6 @@ package spotify.cosmos_util.proto; import "metadata/extension.proto"; import "metadata/image_group.proto"; -import "podcast_segments.proto"; -import "podcast_subscription.proto"; option java_multiple_files = true; option optimize_for = CODE_SIZE; @@ -21,6 +19,9 @@ message EpisodeShowMetadata { } message EpisodeMetadata { + reserved 20; + reserved 21; + optional EpisodeShowMetadata show = 1; optional string link = 2; optional string name = 3; @@ -32,20 +33,20 @@ message EpisodeMetadata { 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; @@ -53,11 +54,12 @@ message EpisodeMetadata { 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; repeated Extension extension = 22; optional bool is_19_plus_only = 23; optional bool is_book_chapter = 24; + optional bool is_podcast_short = 25; + optional bool is_curated = 26; } + diff --git a/protocol/proto/metadata/extension.proto b/protocol/proto/metadata/extension.proto index b10a0f08..7a4b4679 100644 --- a/protocol/proto/metadata/extension.proto +++ b/protocol/proto/metadata/extension.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata/image_group.proto b/protocol/proto/metadata/image_group.proto index 310a408b..77924904 100644 --- a/protocol/proto/metadata/image_group.proto +++ b/protocol/proto/metadata/image_group.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata/show_metadata.proto b/protocol/proto/metadata/show_metadata.proto index 9b9891d3..76a4bb5f 100644 --- a/protocol/proto/metadata/show_metadata.proto +++ b/protocol/proto/metadata/show_metadata.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -28,4 +28,5 @@ message ShowMetadata { optional bool is_music_and_talk = 14; repeated Extension extension = 15; optional bool is_book = 16; + optional bool is_creator_channel = 17; } diff --git a/protocol/proto/metadata/track_metadata.proto b/protocol/proto/metadata/track_metadata.proto index 08bff401..9916b797 100644 --- a/protocol/proto/metadata/track_metadata.proto +++ b/protocol/proto/metadata/track_metadata.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.cosmos_util.proto; +import "metadata/extension.proto"; import "metadata/image_group.proto"; option java_multiple_files = true; @@ -30,6 +31,7 @@ message TrackArtistMetadata { message TrackDescriptor { optional string name = 1; + optional float weight = 2; } message TrackMetadata { @@ -52,4 +54,7 @@ message TrackMetadata { optional uint32 popularity = 17; optional bool is_19_plus_only = 18; repeated TrackDescriptor track_descriptors = 19; + repeated Extension extension = 20; + optional bool is_curated = 21; + optional bool to_be_obfuscated = 22; } diff --git a/protocol/proto/metadata_cosmos.proto b/protocol/proto/metadata_cosmos.proto index f04e5957..bc2b2a42 100644 --- a/protocol/proto/metadata_cosmos.proto +++ b/protocol/proto/metadata_cosmos.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/metadata_esperanto.proto b/protocol/proto/metadata_esperanto.proto index 601290a1..26ec73cf 100644 --- a/protocol/proto/metadata_esperanto.proto +++ b/protocol/proto/metadata_esperanto.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/modification_request.proto b/protocol/proto/modification_request.proto index d35b613c..f8da2506 100644 --- a/protocol/proto/modification_request.proto +++ b/protocol/proto/modification_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -14,7 +14,7 @@ message ModificationRequest { optional string after = 3; optional string name = 4; optional bool playlist = 5; - + optional Attributes attributes = 6; message Attributes { optional bool published = 1; @@ -23,15 +23,48 @@ message ModificationRequest { optional string description = 4; optional string imageUri = 5; optional string picture = 6; + optional string ai_curation_reference_id = 7; + optional PublishedState published_state = 8; } - + repeated string uris = 7; repeated string rows = 8; optional bool contents = 9; optional string item_id = 10; + repeated ListAttributeKind attributes_to_clear = 11; + optional CreateItemKind create_item_kind = 12; } message ModificationResponse { optional bool success = 1; optional string uri = 2; } + +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; + LIST_SEQUENCE_CONTEXT_TEMPLATE = 14; + LIST_AI_CURATION_REFERENCE_ID = 15; +} + +enum PublishedState { + PUBLISHED_STATE_UNSPECIFIED = 0; + PUBLISHED_STATE_NOT_PUBLISHED = 1; + PUBLISHED_STATE_PUBLISHED = 2; +} + +enum CreateItemKind { + CREATE_ITEM_KIND_UNSPECIFIED = 0; + CREATE_ITEM_KIND_PLAYLIST = 1; + CREATE_ITEM_KIND_FOLDER = 2; +} + diff --git a/protocol/proto/net-fortune.proto b/protocol/proto/net-fortune.proto index dbf476b2..62ba5e09 100644 --- a/protocol/proto/net-fortune.proto +++ b/protocol/proto/net-fortune.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,4 +13,6 @@ message NetFortuneResponse { message NetFortuneV2Response { string predict_id = 1; int32 estimated_max_bitrate = 2; + optional int32 advised_prefetch_bitrate_metered = 3; + optional int32 advised_prefetch_bitrate_non_metered = 4; } diff --git a/protocol/proto/offline.proto b/protocol/proto/offline.proto index b3d12491..f84a73c9 100644 --- a/protocol/proto/offline.proto +++ b/protocol/proto/offline.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -17,6 +17,10 @@ message Capacity { uint64 episode_count = 5; } +message Capabilities { + bool remote_downloads_enabled = 1; +} + message Device { string device_id = 1; string cache_id = 2; @@ -25,9 +29,11 @@ message Device { int32 platform = 5; bool offline_enabled = 6; Capacity capacity = 7; + Capabilities capabilities = 8; google.protobuf.Timestamp updated_at = 9; google.protobuf.Timestamp last_seen_at = 10; bool removal_pending = 11; + string client_id = 12; } message Restrictions { @@ -35,6 +41,7 @@ message Restrictions { uint64 max_tracks = 2; google.protobuf.Duration allowed_duration_episodes = 3; uint64 max_episodes = 4; + google.protobuf.Duration allowed_duration_abp_chapters = 5; } message Resource { @@ -58,13 +65,13 @@ message ResourceForDevice { } message ResourceOperation { - Operation operation = 2; enum Operation { INVALID = 0; ADD = 1; REMOVE = 2; } - + + Operation operation = 2; string uri = 3; } diff --git a/protocol/proto/offline_playlists_containing.proto b/protocol/proto/offline_playlists_containing.proto index 3d75865f..7573f493 100644 --- a/protocol/proto/offline_playlists_containing.proto +++ b/protocol/proto/offline_playlists_containing.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/on_demand_in_free_reason.proto b/protocol/proto/on_demand_in_free_reason.proto index bf3a820e..164d22aa 100644 --- a/protocol/proto/on_demand_in_free_reason.proto +++ b/protocol/proto/on_demand_in_free_reason.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -11,4 +11,5 @@ enum OnDemandInFreeReason { NOT_ON_DEMAND = 1; ON_DEMAND = 2; ON_DEMAND_EPISODES_ONLY = 3; + ON_DEMAND_NON_MUSIC_ONLY = 4; } diff --git a/protocol/proto/on_demand_set_cosmos_request.proto b/protocol/proto/on_demand_set_cosmos_request.proto index 72d4d3d9..ebbd0f53 100644 --- a/protocol/proto/on_demand_set_cosmos_request.proto +++ b/protocol/proto/on_demand_set_cosmos_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/on_demand_set_cosmos_response.proto b/protocol/proto/on_demand_set_cosmos_response.proto index 8ca68cbe..9a17849b 100644 --- a/protocol/proto/on_demand_set_cosmos_response.proto +++ b/protocol/proto/on_demand_set_cosmos_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/on_demand_set_response.proto b/protocol/proto/on_demand_set_response.proto index 9d914dd7..9633bb26 100644 --- a/protocol/proto/on_demand_set_response.proto +++ b/protocol/proto/on_demand_set_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/pause_resume_origin.proto b/protocol/proto/pause_resume_origin.proto new file mode 100644 index 00000000..b65d3db2 --- /dev/null +++ b/protocol/proto/pause_resume_origin.proto @@ -0,0 +1,12 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto2"; + +package spotify.player.proto; + +option optimize_for = CODE_SIZE; + +message PauseResumeOrigin { + optional string feature_identifier = 1; +} + diff --git a/protocol/proto/pending_event_entity.proto b/protocol/proto/pending_event_entity.proto index 0dd5c099..965b8cab 100644 --- a/protocol/proto/pending_event_entity.proto +++ b/protocol/proto/pending_event_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/pin_request.proto b/protocol/proto/pin_request.proto index a5337320..bbc388d7 100644 --- a/protocol/proto/pin_request.proto +++ b/protocol/proto/pin_request.proto @@ -1,25 +1,42 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; +option java_package = "spotify.your_library.esperanto.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; message PinRequest { string uri = 1; + oneof position { + string after_uri = 2; + string before_uri = 3; + bool first = 4; + } +} + +message MovePinRequest { + string move_uri = 1; + oneof position { + string after_uri = 2; + string before_uri = 3; + bool first = 4; + } } message PinResponse { - PinStatus status = 1; enum PinStatus { UNKNOWN = 0; PINNED = 1; NOT_PINNED = 2; } - + + PinStatus status = 1; bool has_maximum_pinned_items = 2; int32 maximum_pinned_items = 3; + uint32 status_code = 98; string error = 99; } @@ -28,7 +45,3 @@ message PinItem { bool in_library = 2; } -message PinList { - repeated PinItem item = 1; - string error = 99; -} diff --git a/protocol/proto/play_history.proto b/protocol/proto/play_history.proto new file mode 100644 index 00000000..f2a4a789 --- /dev/null +++ b/protocol/proto/play_history.proto @@ -0,0 +1,20 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto2"; + +package spotify.player.proto.transfer; + +option optimize_for = CODE_SIZE; + +message PlayHistory { + message Item { + optional string context_id = 1; + optional string uid = 2; + optional bool disliked = 3; + repeated transfer.PlayHistory.Item children = 4; + } + + repeated transfer.PlayHistory.Item backward_items = 1; + repeated transfer.PlayHistory.Item forward_items = 2; +} + diff --git a/protocol/proto/play_origin.proto b/protocol/proto/play_origin.proto index 53bb0706..02903cec 100644 --- a/protocol/proto/play_origin.proto +++ b/protocol/proto/play_origin.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -14,4 +14,5 @@ message PlayOrigin { optional string referrer_identifier = 5; optional string device_identifier = 6; repeated string feature_classes = 7; + optional string restriction_identifier = 8; } diff --git a/protocol/proto/play_queue_node.proto b/protocol/proto/play_queue_node.proto index d79a9825..bf763dfd 100644 --- a/protocol/proto/play_queue_node.proto +++ b/protocol/proto/play_queue_node.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -16,4 +16,5 @@ message PlayQueueNode { optional TrackInstantiator instantiator = 3; optional uint32 next_uid = 4; optional sint32 iteration = 5; + optional bool delay_enqueued_tracks = 6; } diff --git a/protocol/proto/play_reason.proto b/protocol/proto/play_reason.proto index 04bba83f..c124ebae 100644 --- a/protocol/proto/play_reason.proto +++ b/protocol/proto/play_reason.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -29,4 +29,6 @@ enum PlayReason { PLAY_REASON_OFFLINE = 18; PLAY_REASON_UNEXPECTED_EXIT = 19; PLAY_REASON_UNEXPECTED_EXIT_WHILE_PAUSED = 20; + PLAY_REASON_SWITCHED_TO_AUDIO = 21; + PLAY_REASON_SWITCHED_TO_VIDEO = 22; } diff --git a/protocol/proto/playback.proto b/protocol/proto/playback.proto index 94d8ae7e..06dcfcc9 100644 --- a/protocol/proto/playback.proto +++ b/protocol/proto/playback.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -14,4 +14,6 @@ message Playback { optional double playback_speed = 3; optional bool is_paused = 4; optional ContextTrack current_track = 5; + optional ContextTrack associated_current_track = 6; + optional int32 associated_position_as_of_timestamp = 7; } diff --git a/protocol/proto/playback_esperanto.proto b/protocol/proto/playback_esperanto.proto index 3c57325a..80ae1a7a 100644 --- a/protocol/proto/playback_esperanto.proto +++ b/protocol/proto/playback_esperanto.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,30 +8,55 @@ option objc_class_prefix = "ESP"; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playback_esperanto.proto"; +message ConnectLoggingParams { + repeated string interaction_ids = 1; + repeated string page_instance_ids = 2; +} + message GetVolumeResponse { Status status = 1; double volume = 2; } +message GetRawVolumeResponse { + Status status = 1; + int32 volume = 2; +} + message SubVolumeResponse { Status status = 1; double volume = 2; VolumeChangeSource source = 3; } +message SubRawVolumeResponse { + Status status = 1; + int32 volume = 2; + VolumeChangeSource source = 3; +} + message SetVolumeRequest { VolumeChangeSource source = 1; double volume = 2; + ConnectLoggingParams connect_logging_params = 3; +} + +message SetRawVolumeRequest { + VolumeChangeSource source = 1; + int32 volume = 2; + ConnectLoggingParams connect_logging_params = 3; } message NudgeVolumeRequest { VolumeChangeSource source = 1; + ConnectLoggingParams connect_logging_params = 2; } message PlaybackInfoResponse { + reserved 3; + reserved 16; Status status = 1; uint64 length_ms = 2; - uint64 position_ms = 3; bool playing = 4; bool buffering = 5; int32 error = 6; @@ -48,12 +73,10 @@ message PlaybackInfoResponse { int32 target_bitrate = 18; int32 advised_bitrate = 19; bool target_file_available = 20; - - reserved 16; + string audio_id = 21; } message GetFormatsResponse { - repeated Format formats = 1; message Format { string enum_key = 1; uint32 enum_value = 2; @@ -61,6 +84,8 @@ message GetFormatsResponse { uint32 bitrate = 4; string mime_type = 5; } + + repeated GetFormatsResponse.Format formats = 1; } message SubPositionRequest { @@ -77,24 +102,24 @@ message GetFilesRequest { } 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; } + + GetFilesStatus status = 1; + repeated File files = 2; } message DuckRequest { - Action action = 2; enum Action { START = 0; STOP = 1; } - + + Action action = 2; double volume = 3; uint32 fade_duration_ms = 4; } @@ -119,4 +144,5 @@ enum GetFilesStatus { enum VolumeChangeSource { USER = 0; SYSTEM = 1; + CONNECT = 2; } diff --git a/protocol/proto/playback_stack.proto b/protocol/proto/playback_stack.proto new file mode 100644 index 00000000..81ebf752 --- /dev/null +++ b/protocol/proto/playback_stack.proto @@ -0,0 +1,13 @@ +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +option java_package = "com.spotify.stream_reporting_esperanto.proto"; +option objc_class_prefix = "ESP"; + +enum PlaybackStack { + BOOMBOX = 0; + BETAMAX = 1; + UNKNOWN = 2; +} + diff --git a/protocol/proto/playback_stack_v2.proto b/protocol/proto/playback_stack_v2.proto new file mode 100644 index 00000000..302b1bb4 --- /dev/null +++ b/protocol/proto/playback_stack_v2.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +option java_package = "com.spotify.stream_reporting_esperanto.proto"; +option objc_class_prefix = "ESP"; + +enum PlaybackStackV2 { + PLAYBACK_STACK_UNKNOWN = 0; + PLAYBACK_STACK_BOOMBOX = 1; + PLAYBACK_STACK_BETAMAX = 2; + PLAYBACK_STACK_KUBRICK = 3; +} + diff --git a/protocol/proto/playback_state.proto b/protocol/proto/playback_state.proto new file mode 100644 index 00000000..aaf34a77 --- /dev/null +++ b/protocol/proto/playback_state.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package spotify.stream_reporting_esperanto.proto; + +option java_package = "com.spotify.stream_reporting_esperanto.proto"; +option objc_class_prefix = "ESP"; + +enum PlaybackState { + ACTIVE = 0; + PAUSED = 1; + SUSPENDED = 2; + INVALID_PLAYBACK_STATE = 3; +} + diff --git a/protocol/proto/played_state.proto b/protocol/proto/played_state.proto index 58990254..f1371578 100644 --- a/protocol/proto/played_state.proto +++ b/protocol/proto/played_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/played_state/episode_played_state.proto b/protocol/proto/played_state/episode_played_state.proto index 696b2e7a..6a90905d 100644 --- a/protocol/proto/played_state/episode_played_state.proto +++ b/protocol/proto/played_state/episode_played_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/played_state/playability_restriction.proto b/protocol/proto/played_state/playability_restriction.proto index d6de6f4e..b1471fc0 100644 --- a/protocol/proto/played_state/playability_restriction.proto +++ b/protocol/proto/played_state/playability_restriction.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -15,4 +15,5 @@ enum PlayabilityRestriction { AGE_RESTRICTED = 3; NOT_IN_CATALOGUE = 4; NOT_AVAILABLE_OFFLINE = 5; + PREMIUM_ONLY = 6; } diff --git a/protocol/proto/played_state/show_played_state.proto b/protocol/proto/played_state/show_played_state.proto index 47f13ec7..912c7dbf 100644 --- a/protocol/proto/played_state/show_played_state.proto +++ b/protocol/proto/played_state/show_played_state.proto @@ -1,14 +1,29 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.cosmos_util.proto; +import "played_state/playability_restriction.proto"; + option objc_class_prefix = "SPTCosmosUtil"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.proto"; message ShowPlayState { + enum Label { + UNKNOWN_LABEL = 0; + NOT_STARTED = 1; + IN_PROGRESS = 2; + COMPLETED = 3; + } + optional string latest_played_episode_link = 1; + optional uint64 played_time = 2; + optional bool is_playable = 3; + optional PlayabilityRestriction playability_restriction = 4 [default = UNKNOWN]; + optional Label label = 5; + optional uint32 played_percentage = 6; + optional string resume_episode_link = 7; } diff --git a/protocol/proto/played_state/track_played_state.proto b/protocol/proto/played_state/track_played_state.proto index 3a8e4c86..2f26c774 100644 --- a/protocol/proto/played_state/track_played_state.proto +++ b/protocol/proto/played_state/track_played_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/player.proto b/protocol/proto/player.proto index dfe5e5ab..3b5716c3 100644 --- a/protocol/proto/player.proto +++ b/protocol/proto/player.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,6 +8,14 @@ option optimize_for = CODE_SIZE; option java_package = "com.spotify.connectstate.model"; message PlayerState { + reserved 26; + reserved 27; + reserved 28; + reserved 29; + reserved 30; + reserved 31; + reserved 34; + int64 timestamp = 1; string context_uri = 2; string context_url = 3; @@ -33,16 +41,14 @@ message PlayerState { 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; + repeated string signals = 33; + string session_command_id = 35; } message ProvidedTrack { + reserved 11; + string uri = 1; string uid = 2; map metadata = 3; @@ -53,7 +59,6 @@ message ProvidedTrack { string album_uri = 8; repeated string disallow_reasons = 9; string artist_uri = 10; - repeated string disallow_undecided = 11; } message ContextIndex { @@ -61,7 +66,18 @@ message ContextIndex { uint32 track = 2; } +message ModeRestrictions { + map values = 1; +} + +message RestrictionReasons { + repeated string reasons = 1; +} + message Restrictions { + reserved 26; + reserved 27; + repeated string disallow_pausing_reasons = 1; repeated string disallow_resuming_reasons = 2; repeated string disallow_seeking_reasons = 3; @@ -85,6 +101,10 @@ message Restrictions { repeated string disallow_updating_context_reasons = 21; repeated string disallow_playing_reasons = 22; repeated string disallow_stopping_reasons = 23; + repeated string disallow_add_to_queue_reasons = 24; + repeated string disallow_setting_playback_speed_reasons = 25; + map disallow_setting_modes = 28; + map disallow_signals = 29; } message PlayOrigin { @@ -95,89 +115,21 @@ message PlayOrigin { string referrer_identifier = 5; string device_identifier = 6; repeated string feature_classes = 7; + string restriction_identifier = 8; } message ContextPlayerOptions { bool shuffling_context = 1; bool repeating_context = 2; bool repeating_track = 3; + map modes = 5; + optional float playback_speed = 4; } 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; @@ -193,6 +145,7 @@ enum BitrateLevel { high = 3; very_high = 4; hifi = 5; + hifi24 = 6; } enum BitrateStrategy { diff --git a/protocol/proto/player_license.proto b/protocol/proto/player_license.proto index 3d0e905d..106bb356 100644 --- a/protocol/proto/player_license.proto +++ b/protocol/proto/player_license.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/playlist4_external.proto b/protocol/proto/playlist4_external.proto index 2a7b44b9..1e4f4e7c 100644 --- a/protocol/proto/playlist4_external.proto +++ b/protocol/proto/playlist4_external.proto @@ -1,10 +1,12 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.playlist4.proto; +import "lens-model.proto"; import "playlist_permission.proto"; +import "signal-model.proto"; option optimize_for = CODE_SIZE; option java_outer_classname = "Playlist4ApiProto"; @@ -22,7 +24,9 @@ message MetaItem { optional int64 timestamp = 4; optional string owner_username = 5; optional bool abuse_reporting_enabled = 6; - optional spotify.playlist_permission.proto.Capabilities capabilities = 7; + optional playlist_permission.proto.Capabilities capabilities = 7; + repeated GeoblockBlockingType geoblock = 8; + optional sint32 status_code = 9; } message ListItems { @@ -30,6 +34,25 @@ message ListItems { required bool truncated = 2; repeated Item items = 3; repeated MetaItem meta_items = 4; + repeated playlist.signal.proto.Signal available_signals = 5; + optional string continuation_token = 6; +} + +message PaginatedUnfollowedListItems { + optional int32 limit = 1; + optional int32 offset = 2; + optional int32 nextPageIndex = 3; + optional int32 previousPageIndex = 4; + optional int32 totalPages = 5; + repeated UnfollowedListItem items = 6; +} + +message UnfollowedListItem { + optional string uri = 1; + optional bool recoverable = 2; + optional string name = 3; + optional int64 deleted_at = 4; + optional int32 length = 5; } message FormatListAttribute { @@ -42,6 +65,10 @@ message PictureSize { optional string url = 2; } +message RecommendationInfo { + optional bool is_recommendation = 1; +} + message ListAttributes { optional string name = 1; optional string description = 2; @@ -53,6 +80,8 @@ message ListAttributes { optional string format = 11; repeated FormatListAttribute format_attributes = 12; repeated PictureSize picture_size = 13; + optional bytes sequence_context_template = 14; + optional bytes ai_curation_reference_id = 15; } message ItemAttributes { @@ -62,6 +91,10 @@ message ItemAttributes { optional bool public = 10; repeated FormatListAttribute format_attributes = 11; optional bytes item_id = 12; + optional lens.model.proto.Lens source_lens = 13; + repeated playlist.signal.proto.Signal available_signals = 14; + optional RecommendationInfo recommendation_info = 15; + optional bytes sequence_child_template = 16; } message Add { @@ -69,6 +102,8 @@ message Add { repeated Item items = 2; optional bool add_last = 4; optional bool add_first = 5; + optional Item add_before_item = 6; + optional Item add_after_item = 7; } message Rem { @@ -79,9 +114,14 @@ message Rem { } message Mov { - required int32 from_index = 1; - required int32 length = 2; - required int32 to_index = 3; + optional int32 from_index = 1; + optional int32 length = 2; + optional int32 to_index = 3; + repeated Item items = 4; + optional Item add_before_item = 5; + optional Item add_after_item = 6; + optional bool add_first = 7; + optional bool add_last = 8; } message ItemAttributesPartialState { @@ -95,9 +135,10 @@ message ListAttributesPartialState { } message UpdateItemAttributes { - required int32 index = 1; + optional int32 index = 1; required ItemAttributesPartialState new_attributes = 2; optional ItemAttributesPartialState old_attributes = 3; + optional Item item = 4; } message UpdateListAttributes { @@ -105,8 +146,17 @@ message UpdateListAttributes { optional ListAttributesPartialState old_attributes = 2; } +message UpdateItemUris { + repeated UriReplacement uri_replacements = 1; +} + +message UriReplacement { + optional int32 index = 1; + optional Item item = 2; + optional string new_uri = 3; +} + message Op { - required Kind kind = 1; enum Kind { KIND_UNKNOWN = 0; ADD = 2; @@ -114,13 +164,16 @@ message Op { MOV = 4; UPDATE_ITEM_ATTRIBUTES = 5; UPDATE_LIST_ATTRIBUTES = 6; + UPDATE_ITEM_URIS = 7; } - + + required Kind kind = 1; optional Add add = 2; optional Rem rem = 3; optional Mov mov = 4; optional UpdateItemAttributes update_item_attributes = 5; optional UpdateListAttributes update_list_attributes = 6; + optional UpdateItemUris update_item_uris = 7; } message OpList { @@ -141,7 +194,6 @@ message ChangeInfo { } message SourceInfo { - optional Client client = 1; enum Client { CLIENT_UNKNOWN = 0; NATIVE_HERMES = 1; @@ -151,10 +203,12 @@ message SourceInfo { WEBPLAYER = 5; LIBSPOTIFY = 6; } - + + optional Client client = 1; optional string app = 3; optional string source = 4; optional string version = 5; + optional string server_domain = 6; } message Delta { @@ -177,6 +231,11 @@ message ListChanges { repeated int64 nonces = 6; } +message ListSignals { + optional bytes base_revision = 1; + repeated playlist.signal.proto.Signal emitted_signals = 2; +} + message SelectedListContent { optional bytes revision = 1; optional int32 length = 2; @@ -193,6 +252,13 @@ message SelectedListContent { optional bool abuse_reporting_enabled = 17; optional spotify.playlist_permission.proto.Capabilities capabilities = 18; repeated GeoblockBlockingType geoblock = 19; + optional bool changes_require_resync = 20; + optional int64 created_at = 21; + optional AppliedLenses applied_lenses = 22; +} + +message AppliedLenses { + repeated lens.model.proto.LensState states = 1; } message CreateListReply { @@ -272,6 +338,8 @@ enum ListAttributeKind { LIST_FORMAT = 11; LIST_FORMAT_ATTRIBUTES = 12; LIST_PICTURE_SIZE = 13; + LIST_SEQUENCE_CONTEXT_TEMPLATE = 14; + LIST_AI_CURATION_REFERENCE_ID = 15; } enum ItemAttributeKind { @@ -282,6 +350,10 @@ enum ItemAttributeKind { ITEM_PUBLIC = 10; ITEM_FORMAT_ATTRIBUTES = 11; ITEM_ID = 12; + ITEM_SOURCE_LENS = 13; + ITEM_AVAILABLE_SIGNALS = 14; + ITEM_RECOMMENDATION_INFO = 15; + ITEM_SEQUENCE_CHILD_TEMPLATE = 16; } enum GeoblockBlockingType { @@ -290,3 +362,4 @@ enum GeoblockBlockingType { 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 index 072d5379..c18e4849 100644 --- a/protocol/proto/playlist_contains_request.proto +++ b/protocol/proto/playlist_contains_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_folder_state.proto b/protocol/proto/playlist_folder_state.proto index a2d32d71..c78e3e78 100644 --- a/protocol/proto/playlist_folder_state.proto +++ b/protocol/proto/playlist_folder_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/playlist_get_request.proto b/protocol/proto/playlist_get_request.proto index 7e6dd3f0..5ed15ee3 100644 --- a/protocol/proto/playlist_get_request.proto +++ b/protocol/proto/playlist_get_request.proto @@ -1,9 +1,10 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.playlist_esperanto.proto; +import "google/protobuf/duration.proto"; import "policy/playlist_request_decoration_policy.proto"; import "playlist_query.proto"; import "playlist_request.proto"; @@ -20,7 +21,30 @@ message PlaylistGetRequest { playlist.cosmos.proto.PlaylistRequestDecorationPolicy policy = 3; } +message PlaylistMultiGetSingleRequest { + string id = 1; + PlaylistGetRequest request = 2; +} + +message PlaylistMultiGetRequest { + repeated PlaylistMultiGetSingleRequest requests = 1; + google.protobuf.Duration timeout = 2; +} + message PlaylistGetResponse { ResponseStatus status = 1; playlist.cosmos.playlist_request.proto.Response data = 2; + PlaylistQuery query = 3; } + +message PlaylistMultiGetSingleResponse { + string id = 1; + string uri = 2; + PlaylistGetResponse response = 3; +} + +message PlaylistMultiGetResponse { + ResponseStatus status = 1; + repeated PlaylistMultiGetSingleResponse responses = 2; +} + diff --git a/protocol/proto/playlist_members_request.proto b/protocol/proto/playlist_members_request.proto index d5bd9b98..d7304257 100644 --- a/protocol/proto/playlist_members_request.proto +++ b/protocol/proto/playlist_members_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_modification_request.proto b/protocol/proto/playlist_modification_request.proto index 2bdb0146..c5fea0b4 100644 --- a/protocol/proto/playlist_modification_request.proto +++ b/protocol/proto/playlist_modification_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_offline_request.proto b/protocol/proto/playlist_offline_request.proto index e0ab6312..30c7ebbb 100644 --- a/protocol/proto/playlist_offline_request.proto +++ b/protocol/proto/playlist_offline_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_permission.proto b/protocol/proto/playlist_permission.proto index 96e9c06d..f155f6e0 100644 --- a/protocol/proto/playlist_permission.proto +++ b/protocol/proto/playlist_permission.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -13,6 +13,24 @@ message Permission { optional PermissionLevel permission_level = 2; } +message GrantableLevels { + repeated PermissionLevel base = 1; + repeated PermissionLevel member = 2; +} + +message AttributeCapabilities { + optional bool can_edit = 1; +} + +message ListAttributeCapabilities { + optional AttributeCapabilities name = 1; + optional AttributeCapabilities description = 2; + optional AttributeCapabilities picture = 3; + optional AttributeCapabilities collaborative = 4; + optional AttributeCapabilities deleted_by_owner = 6; + optional AttributeCapabilities ai_curation_reference_id = 15; +} + message Capabilities { optional bool can_view = 1; optional bool can_administrate_permissions = 2; @@ -20,6 +38,8 @@ message Capabilities { optional bool can_edit_metadata = 4; optional bool can_edit_items = 5; optional bool can_cancel_membership = 6; + optional GrantableLevels grantable_levels = 7; + optional ListAttributeCapabilities list_attribute_capabilities = 8; } message CapabilitiesMultiRequest { @@ -29,11 +49,17 @@ message CapabilitiesMultiRequest { optional string fallback_uri = 4; } +message CapabilitiesRequestOptions { + optional bool can_view_only = 1; +} + message CapabilitiesRequest { optional string username = 1; optional string user_id = 2; optional string uri = 3; optional bool user_is_owner = 4; + optional string permission_grant_token = 5; + optional CapabilitiesRequestOptions request_options = 6; } message CapabilitiesMultiResponse { @@ -82,9 +108,29 @@ message PermissionGrant { optional PermissionGrantOptions permission_grant_options = 2; } +message PermissionGrantDetails { + optional bool permission_level_downgraded = 1; +} + +message PermissionGrantDescription { + enum ClaimFailReason { + CLAIM_FAIL_REASON_UNSPECIFIED = 0; + CLAIM_FAIL_REASON_ANONYMOUS = 1; + CLAIM_FAIL_REASON_NO_GRANT_FOUND = 2; + CLAIM_FAIL_REASON_GRANT_EXPIRED = 3; + } + + optional PermissionGrantOptions permission_grant_options = 1; + optional ClaimFailReason claim_fail_reason = 2; + optional bool is_effective = 3; + optional Capabilities capabilities = 4; + repeated PermissionGrantDetails details = 5; +} + message ClaimPermissionGrantResponse { optional Permission user_permission = 1; optional Capabilities capabilities = 2; + repeated PermissionGrantDetails details = 3; } message ResponseStatus { @@ -92,9 +138,42 @@ message ResponseStatus { optional string status_message = 2; } +message PermissionIdentifier { + required PermissionIdentifierKind kind = 1; + optional string user_id = 2; +} + +message PermissionEntry { + optional PermissionIdentifier identifier = 1; + optional Permission permission = 2; +} + +message CreateInitialPermissions { + repeated PermissionEntry permission_entry = 1; +} + +message CreateInitialPermissionsResponse { + repeated PermissionEntry permission_entry = 1; +} + +message DefaultOwnerCapabilitiesResponse { + optional Capabilities capabilities = 1; +} + enum PermissionLevel { UNKNOWN = 0; BLOCKED = 1; VIEWER = 2; CONTRIBUTOR = 3; + MADE_FOR = 4; } + +enum PermissionIdentifierKind { + PERMISSION_IDENTIFIER_KIND_UNSPECIFIED = 0; + PERMISSION_IDENTIFIER_KIND_BASE = 1; + PERMISSION_IDENTIFIER_KIND_MEMBER = 2; + PERMISSION_IDENTIFIER_KIND_ABUSE = 3; + PERMISSION_IDENTIFIER_KIND_PROFILE = 4; + PERMISSION_IDENTIFIER_KIND_AUTHORIZED = 5; +} + diff --git a/protocol/proto/playlist_play_request.proto b/protocol/proto/playlist_play_request.proto index 032b2b2a..eb166756 100644 --- a/protocol/proto/playlist_play_request.proto +++ b/protocol/proto/playlist_play_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_playback_request.proto b/protocol/proto/playlist_playback_request.proto index 8cd20257..83435a31 100644 --- a/protocol/proto/playlist_playback_request.proto +++ b/protocol/proto/playlist_playback_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_playlist_state.proto b/protocol/proto/playlist_playlist_state.proto index 5663252c..28e78377 100644 --- a/protocol/proto/playlist_playlist_state.proto +++ b/protocol/proto/playlist_playlist_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -44,6 +44,8 @@ message PlaylistMetadata { optional string load_state = 19; optional User made_for = 20; repeated cosmos_util.proto.Extension extension = 21; + optional uint32 length_ignoring_text_filter = 22; + optional string ai_curation_reference_id = 23; } message PlaylistOfflineState { diff --git a/protocol/proto/playlist_query.proto b/protocol/proto/playlist_query.proto index afd97614..1aedb0cf 100644 --- a/protocol/proto/playlist_query.proto +++ b/protocol/proto/playlist_query.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.playlist_esperanto.proto; +import "policy/supported_link_types_in_playlists.proto"; + option objc_class_prefix = "ESP"; option java_multiple_files = true; option optimize_for = CODE_SIZE; @@ -24,10 +26,14 @@ message PlaylistQuery { NOT_BANNED = 4; NOT_EXPLICIT = 5; NOT_EPISODE = 6; + NOT_RECOMMENDATION = 7; + UNPLAYED = 8; + IN_PROGRESS = 9; + NOT_FULLY_PLAYED = 10; } - + string text_filter = 2; - + SortBy sort_by = 3; enum SortBy { NO_SORT = 0; @@ -45,8 +51,16 @@ message PlaylistQuery { NAME_DESC = 12; ADD_TIME_ASC = 13; ADD_TIME_DESC = 14; + ADDED_BY_ASC = 15; + ADDED_BY_DESC = 16; + DURATION_ASC = 17; + DURATION_DESC = 18; + SHOW_NAME_ASC = 19; + SHOW_NAME_DESC = 20; + PUBLISH_DATE_ASC = 21; + PUBLISH_DATE_DESC = 22; } - + PlaylistRange range = 4; int32 update_throttling_ms = 5; bool group = 6; @@ -54,6 +68,16 @@ message PlaylistQuery { bool show_unavailable = 8; bool always_show_windowed = 9; bool load_recommendations = 10; + repeated playlist.cosmos.proto.LinkType supported_placeholder_types = 11; + repeated string descriptor_filter = 12; + string item_id_filter = 13; + + repeated AttributeFilter attribute_filter = 14; + message AttributeFilter { + repeated string contains_one_of = 1; + } + + bool include_all_placeholders = 15; } enum PlaylistSourceRestriction { diff --git a/protocol/proto/playlist_request.proto b/protocol/proto/playlist_request.proto index 52befb1f..5ad7803d 100644 --- a/protocol/proto/playlist_request.proto +++ b/protocol/proto/playlist_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -15,12 +15,40 @@ import "playlist_permission.proto"; import "playlist_playlist_state.proto"; import "playlist_track_state.proto"; import "playlist_user_state.proto"; +import "policy/supported_link_types_in_playlists.proto"; import "metadata/track_metadata.proto"; +import "metadata/extension.proto"; option objc_class_prefix = "SPTPlaylistCosmosPlaylist"; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.proto"; +message AvailableSignal { + optional string name = 1; + optional .spotify.playlist.cosmos.playlist_request.proto.SignalState state = 2; +} + +message ItemOfflineState { + optional string offline = 1; + optional uint32 sync_progress = 2; + optional bool locally_playable = 3; +} + +message ItemCollectionState { + optional bool is_in_collection = 1; + optional bool is_banned = 2; +} + +message ItemMetadata { + optional string name = 1; + optional string image = 2; + optional bool is_explicit = 3; +} + +message ItemCurationState { + optional bool is_curated = 1; +} + message Item { optional string header_field = 1; optional uint32 add_time = 2; @@ -36,11 +64,30 @@ message Item { 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; + repeated AvailableSignal available_signals = 15; + optional bool is_recommendation = 16; + repeated cosmos_util.proto.Extension extension = 17; + optional string uri = 18; + optional ItemOfflineState offline_state = 19; + optional ItemCollectionState collection_state = 20; + optional ItemMetadata metadata = 21; + optional ItemCurationState curation_state = 22; + optional bool should_be_obfuscated = 23; +} + +message Lens { + optional string name = 1; +} + +message LensState { + repeated Lens requested_lenses = 1; + repeated Lens applied_lenses = 2; } message Playlist { optional cosmos.proto.PlaylistMetadata playlist_metadata = 1; optional cosmos.proto.PlaylistOfflineState playlist_offline_state = 2; + optional LensState lenses = 3; } message RecommendationItem { @@ -63,6 +110,11 @@ message Collaborators { repeated Collaborator collaborator = 2; } +message NumberOfItemsForLinkType { + optional cosmos.proto.LinkType link_type = 1; + optional int32 num_items = 2; +} + message Response { repeated Item item = 1; optional Playlist playlist = 2; @@ -88,4 +140,12 @@ message Response { optional Collaborators collaborators = 22; optional playlist_permission.proto.Permission base_permission = 23; optional playlist_permission.proto.Capabilities user_capabilities = 24; + repeated NumberOfItemsForLinkType number_of_items_per_link_type = 25; + repeated AvailableSignal available_signals = 26; } + +enum SignalState { + READY = 0; + PENDING = 1; +} + diff --git a/protocol/proto/playlist_set_base_permission_request.proto b/protocol/proto/playlist_set_base_permission_request.proto index 3e8e1838..45d7bb5a 100644 --- a/protocol/proto/playlist_set_base_permission_request.proto +++ b/protocol/proto/playlist_set_base_permission_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_set_member_permission_request.proto b/protocol/proto/playlist_set_member_permission_request.proto index d3d687a4..1fe6ba69 100644 --- a/protocol/proto/playlist_set_member_permission_request.proto +++ b/protocol/proto/playlist_set_member_permission_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/playlist_set_permission_request.proto b/protocol/proto/playlist_set_permission_request.proto index a410cc23..60e95c76 100644 --- a/protocol/proto/playlist_set_permission_request.proto +++ b/protocol/proto/playlist_set_permission_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/playlist_track_state.proto b/protocol/proto/playlist_track_state.proto index cd55947f..b6fa87ef 100644 --- a/protocol/proto/playlist_track_state.proto +++ b/protocol/proto/playlist_track_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/playlist_user_state.proto b/protocol/proto/playlist_user_state.proto index 86c07dee..4f8565af 100644 --- a/protocol/proto/playlist_user_state.proto +++ b/protocol/proto/playlist_user_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/plugin.proto b/protocol/proto/plugin.proto index c0e912ce..a1f61fff 100644 --- a/protocol/proto/plugin.proto +++ b/protocol/proto/plugin.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,19 +11,20 @@ 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; } + + repeated Entry plugins = 1; } message PluginInit { @@ -35,63 +36,74 @@ message TargetFormat { } message Metadata { - Header header = 1; message Header { int32 status_code = 1; bool is_empty = 2; } - + + Header header = 1; google.protobuf.Any extension_data = 2; } message IdentifyCommand { - Header header = 3; message Header { TargetFormat target_format = 1; } - - repeated Query query = 4; + message Query { + message MetadataEntry { + int32 key = 1; + Metadata value = 2; + } + string link = 1; - map metadata = 2; + repeated MetadataEntry metadata = 2; } + + Header header = 3; + repeated Query query = 4; } message IdentifyResponse { - map results = 1; - message Result { - Status status = 1; enum Status { UNKNOWN = 0; MISSING = 1; COMPLETE = 2; NOT_APPLICABLE = 3; } - + + Status status = 1; int64 estimated_file_size = 2; } + + map results = 1; } message DownloadCommand { + message MetadataEntry { + int32 key = 1; + Metadata value = 2; + } + string link = 1; TargetFormat target_format = 2; - map metadata = 3; + repeated MetadataEntry 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; } + + string link = 1; + bool complete = 2; + int64 file_size = 3; + int64 bytes_downloaded = 4; + Error error = 5; } message StopDownloadCommand { @@ -99,28 +111,25 @@ message StopDownloadCommand { } message StopDownloadResponse { - } message RemoveCommand { - Header header = 2; message Header { - } - - repeated Query query = 3; + message Query { string link = 1; } + + Header header = 2; + repeated Query query = 3; } message RemoveResponse { - } message PluginCommand { string id = 1; - oneof command { IdentifyCommand identify = 2; DownloadCommand download = 3; @@ -131,7 +140,6 @@ message PluginCommand { message PluginResponse { string id = 1; - oneof response { IdentifyResponse identify = 2; DownloadResponse download = 3; diff --git a/protocol/proto/podcast_ad_segments.proto b/protocol/proto/podcast_ad_segments.proto index ebff7385..d24b697b 100644 --- a/protocol/proto/podcast_ad_segments.proto +++ b/protocol/proto/podcast_ad_segments.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -14,6 +14,7 @@ message PodcastAds { repeated string file_ids = 1; repeated string manifest_ids = 2; repeated Segment segments = 3; + string request_id = 4; } message Segment { diff --git a/protocol/proto/podcast_cta_cards.proto b/protocol/proto/podcast_cta_cards.proto index 9cd4dfc6..a516fb40 100644 --- a/protocol/proto/podcast_cta_cards.proto +++ b/protocol/proto/podcast_cta_cards.proto @@ -1,8 +1,8 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; -package spotify.context_track_exts.podcastctacards; +package spotify.context_mdata.podcastctacards; message Card { bool has_cards = 1; diff --git a/protocol/proto/podcast_poll.proto b/protocol/proto/podcast_poll.proto index 60dc04c6..7374c175 100644 --- a/protocol/proto/podcast_poll.proto +++ b/protocol/proto/podcast_poll.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/podcast_qna.proto b/protocol/proto/podcast_qna.proto index fca3ba55..340d21f0 100644 --- a/protocol/proto/podcast_qna.proto +++ b/protocol/proto/podcast_qna.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/podcast_subscription.proto b/protocol/proto/podcast_subscription.proto index 52b7f8f3..36107386 100644 --- a/protocol/proto/podcast_subscription.proto +++ b/protocol/proto/podcast_subscription.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,7 +13,7 @@ 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; diff --git a/protocol/proto/podcastextensions.proto b/protocol/proto/podcastextensions.proto index 4c85e396..7a5c9363 100644 --- a/protocol/proto/podcastextensions.proto +++ b/protocol/proto/podcastextensions.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -22,8 +22,7 @@ message PodcastTopic { 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 index 359347d4..11a38c63 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/policy/artist_decoration_policy.proto b/protocol/proto/policy/artist_decoration_policy.proto index 0419dc31..1e60743b 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -22,6 +22,7 @@ message ArtistCollectionDecorationPolicy { bool num_albums_in_collection = 4; bool is_banned = 5; bool can_ban = 6; + bool num_explicitly_liked_tracks = 8; } message ArtistSyncDecorationPolicy { diff --git a/protocol/proto/policy/episode_decoration_policy.proto b/protocol/proto/policy/episode_decoration_policy.proto index 467426bd..a194ddaf 100644 --- a/protocol/proto/policy/episode_decoration_policy.proto +++ b/protocol/proto/policy/episode_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,6 +11,8 @@ option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.policy.proto"; message EpisodeDecorationPolicy { + reserved 19; + reserved 20; bool link = 1; bool length = 2; bool name = 3; @@ -29,11 +31,11 @@ message EpisodeDecorationPolicy { bool is_explicit = 16; bool type = 17; 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; + bool is_podcast_short = 24; + bool is_curated = 25; } message EpisodeCollectionDecorationPolicy { @@ -55,10 +57,3 @@ message EpisodePlayedStateDecorationPolicy { bool last_played_at = 5; } -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 index 0d47e4d6..16e3eb11 100644 --- a/protocol/proto/policy/folder_decoration_policy.proto +++ b/protocol/proto/policy/folder_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/policy/playlist_album_decoration_policy.proto b/protocol/proto/policy/playlist_album_decoration_policy.proto index 01537c78..97bbf72b 100644 --- a/protocol/proto/policy/playlist_album_decoration_policy.proto +++ b/protocol/proto/policy/playlist_album_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/policy/playlist_decoration_policy.proto b/protocol/proto/policy/playlist_decoration_policy.proto index a6aef1b7..e2376c22 100644 --- a/protocol/proto/policy/playlist_decoration_policy.proto +++ b/protocol/proto/policy/playlist_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -60,4 +60,11 @@ message PlaylistDecorationPolicy { bool base_permission = 41; bool user_capabilities = 42; repeated extendedmetadata.ExtensionKind extension = 43; + bool lenses = 44; + bool length_ignoring_text_filter = 45; + bool number_of_items_per_link_type = 46; + bool available_signals = 47; + bool ai_curation_reference_id = 48; + bool unranged_length = 49; + bool unfiltered_length = 50; } diff --git a/protocol/proto/policy/playlist_episode_decoration_policy.proto b/protocol/proto/policy/playlist_episode_decoration_policy.proto index 4e038944..04a63b0b 100644 --- a/protocol/proto/policy/playlist_episode_decoration_policy.proto +++ b/protocol/proto/policy/playlist_episode_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -22,4 +22,6 @@ message PlaylistEpisodeDecorationPolicy { cosmos_util.proto.EpisodePlayedStateDecorationPolicy played_state = 7; UserDecorationPolicy added_by = 8; cosmos_util.proto.ShowDecorationPolicy show = 9; + bool signals = 10; + bool is_recommendation = 11; } diff --git a/protocol/proto/policy/playlist_request_decoration_policy.proto b/protocol/proto/policy/playlist_request_decoration_policy.proto index a1663d28..66fdea32 100644 --- a/protocol/proto/policy/playlist_request_decoration_policy.proto +++ b/protocol/proto/policy/playlist_request_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,13 +7,47 @@ 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"; +import "policy/supported_link_types_in_playlists.proto"; +import "extension_kind.proto"; option java_multiple_files = true; option optimize_for = CODE_SIZE; option java_package = "com.spotify.playlist.policy.proto"; +message ItemExtensionPolicy { + LinkType link_type = 1; + extendedmetadata.ExtensionKind extension = 2; +} + +message ItemOfflineStateDecorationPolicy { + bool offline_state = 1; + bool sync_progress = 2; + bool locally_playable = 3; +} + +message ItemMetadataPolicy { + bool name = 1; + bool image = 2; + bool is_explicit = 3; +} + +message ItemCurationStatePolicy { + bool is_curated = 1; +} + +message PlaylistItemDecorationPolicy { + bool uri = 1; + repeated ItemExtensionPolicy extension_policy = 2; + ItemOfflineStateDecorationPolicy offline_state = 3; + bool collection_state = 4; + ItemMetadataPolicy metadata = 5; + ItemCurationStatePolicy curation_state = 6; + bool obfuscation_state = 7; +} + message PlaylistRequestDecorationPolicy { PlaylistDecorationPolicy playlist = 1; PlaylistTrackDecorationPolicy track = 2; PlaylistEpisodeDecorationPolicy episode = 3; + PlaylistItemDecorationPolicy item = 4; } diff --git a/protocol/proto/policy/playlist_track_decoration_policy.proto b/protocol/proto/policy/playlist_track_decoration_policy.proto index 97eb0187..cad855ab 100644 --- a/protocol/proto/policy/playlist_track_decoration_policy.proto +++ b/protocol/proto/policy/playlist_track_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -28,4 +28,6 @@ message PlaylistTrackDecorationPolicy { UserDecorationPolicy added_by = 12; PlaylistAlbumDecorationPolicy album = 13; cosmos_util.proto.ArtistDecorationPolicy artist = 14; + bool signals = 15; + bool is_recommendation = 16; } diff --git a/protocol/proto/policy/rootlist_folder_decoration_policy.proto b/protocol/proto/policy/rootlist_folder_decoration_policy.proto index f93888b4..5c58a3f1 100644 --- a/protocol/proto/policy/rootlist_folder_decoration_policy.proto +++ b/protocol/proto/policy/rootlist_folder_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/policy/rootlist_playlist_decoration_policy.proto b/protocol/proto/policy/rootlist_playlist_decoration_policy.proto index 9e8446ab..2c052c37 100644 --- a/protocol/proto/policy/rootlist_playlist_decoration_policy.proto +++ b/protocol/proto/policy/rootlist_playlist_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/policy/rootlist_request_decoration_policy.proto b/protocol/proto/policy/rootlist_request_decoration_policy.proto index ebad00ca..cf7b5ab9 100644 --- a/protocol/proto/policy/rootlist_request_decoration_policy.proto +++ b/protocol/proto/policy/rootlist_request_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/policy/show_decoration_policy.proto b/protocol/proto/policy/show_decoration_policy.proto index 2e5e2020..fbb1617a 100644 --- a/protocol/proto/policy/show_decoration_policy.proto +++ b/protocol/proto/policy/show_decoration_policy.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,6 +11,7 @@ option optimize_for = CODE_SIZE; option java_package = "com.spotify.cosmos.util.policy.proto"; message ShowDecorationPolicy { + reserved 15; bool link = 1; bool name = 2; bool description = 3; @@ -25,11 +26,26 @@ message ShowDecorationPolicy { bool copyrights = 12; bool trailer_uri = 13; bool is_music_and_talk = 14; - bool access_info = 15; repeated extendedmetadata.ExtensionKind extension = 16; bool is_book = 17; + bool is_creator_channel = 18; } message ShowPlayedStateDecorationPolicy { bool latest_played_episode_link = 1; + bool played_time = 2; + bool is_playable = 3; + bool playability_restriction = 4; + bool label = 5; + bool resume_episode_link = 7; } + +message ShowCollectionDecorationPolicy { + bool is_in_collection = 1; +} + +message ShowOfflineStateDecorationPolicy { + bool offline = 1; + bool sync_progress = 2; +} + diff --git a/protocol/proto/policy/supported_link_types_in_playlists.proto b/protocol/proto/policy/supported_link_types_in_playlists.proto new file mode 100644 index 00000000..4c1897a5 --- /dev/null +++ b/protocol/proto/policy/supported_link_types_in_playlists.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package spotify.playlist.cosmos.proto; + +option java_package = "com.spotify.playlist.policy.proto"; +option java_multiple_files = true; + +enum LinkType { + EMPTY = 0; + ARTIST = 1; + ALBUM = 2; + TRACK = 4; + LOCAL_TRACK = 9; + SHOW = 62; + EPISODE = 63; +} + diff --git a/protocol/proto/policy/track_decoration_policy.proto b/protocol/proto/policy/track_decoration_policy.proto index aa71f497..e68922c9 100644 --- a/protocol/proto/policy/track_decoration_policy.proto +++ b/protocol/proto/policy/track_decoration_policy.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) 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"; @@ -27,6 +29,9 @@ message TrackDecorationPolicy { bool popularity = 16; bool is_19_plus_only = 17; bool track_descriptors = 18; + repeated extendedmetadata.ExtensionKind extension = 19; + bool is_curated = 20; + bool to_be_obfuscated = 22; } message TrackPlayedStateDecorationPolicy { diff --git a/protocol/proto/policy/user_decoration_policy.proto b/protocol/proto/policy/user_decoration_policy.proto index f2c342eb..c284df3e 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.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/popcount2_external.proto b/protocol/proto/popcount2_external.proto index 069cdcfd..8c69a4b4 100644 --- a/protocol/proto/popcount2_external.proto +++ b/protocol/proto/popcount2_external.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -7,24 +7,29 @@ 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; + repeated string userid = 6; + optional int64 raw_count = 7; + optional bool count_hidden_from_users = 8; } message PopcountUserUpdate { optional string user = 1; optional sint64 timestamp = 2; optional bool added = 3; + optional string userid = 4; } -message PopcountUpdate { - repeated PopcountUserUpdate updates = 1; - optional sint64 common_timestamp = 2; - optional sint64 remove_older_than_timestamp = 3; - optional bool verify_counter = 4; +message PopcountFollowerResult { + optional bool is_truncated = 1; + repeated bytes user_id = 2; +} + +message PopcountSetFollowerCounterValueRequest { + optional int64 count = 1; } diff --git a/protocol/proto/prepare_play_options.proto b/protocol/proto/prepare_play_options.proto index cb27650d..9c545824 100644 --- a/protocol/proto/prepare_play_options.proto +++ b/protocol/proto/prepare_play_options.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -27,11 +27,11 @@ message PreparePlayOptions { } enum PrefetchLevel { - kNone = 0; - kMedia = 1; + NONE = 0; + MEDIA = 1; } enum AudioStream { - kDefault = 0; - kAlarm = 1; + DEFAULT = 0; + ALARM = 1; } diff --git a/protocol/proto/profile_cosmos.proto b/protocol/proto/profile_cosmos.proto index c6c945db..a643e6f3 100644 --- a/protocol/proto/profile_cosmos.proto +++ b/protocol/proto/profile_cosmos.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/profile_service.proto b/protocol/proto/profile_service.proto index 194e5fea..84425e49 100644 --- a/protocol/proto/profile_service.proto +++ b/protocol/proto/profile_service.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,6 +6,8 @@ package spotify.profile_esperanto.proto.v1; import "identity.proto"; +option java_package = "spotify.profile_esperanto.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; service ProfileService { diff --git a/protocol/proto/property_definition.proto b/protocol/proto/property_definition.proto index 9df7caa7..277f73f4 100644 --- a/protocol/proto/property_definition.proto +++ b/protocol/proto/property_definition.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,38 +7,39 @@ 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"; - + reserved "hash"; + reserved 2; + 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; } + + 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; + } } diff --git a/protocol/proto/protobuf_delta.proto b/protocol/proto/protobuf_delta.proto index c0a89fec..95ac7e14 100644 --- a/protocol/proto/protobuf_delta.proto +++ b/protocol/proto/protobuf_delta.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -14,7 +14,7 @@ message Delta { DELETE = 0; INSERT = 1; } - + required uint32 index = 2; required uint32 length = 3; } diff --git a/protocol/proto/queue.proto b/protocol/proto/queue.proto index 24b45b7c..64d10414 100644 --- a/protocol/proto/queue.proto +++ b/protocol/proto/queue.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/rate_limited_events.proto b/protocol/proto/rate_limited_events.proto index c9116b6d..6d080705 100644 --- a/protocol/proto/rate_limited_events.proto +++ b/protocol/proto/rate_limited_events.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/rcs.proto b/protocol/proto/rcs.proto index 00e86103..8225eb27 100644 --- a/protocol/proto/rcs.proto +++ b/protocol/proto/rcs.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,35 +7,35 @@ 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; } } - + + repeated AssignedPropertyValue properties = 1; int64 rcs_fetch_time = 2; string configuration_assignment_id = 3; + string etag = 10; } message PolicyGroupId { @@ -44,58 +44,56 @@ message PolicyGroupId { } message ClientPropertySet { - string client_id = 1; - string version = 2; - repeated PropertyDefinition properties = 5; - - repeated ComponentInfo component_infos = 6; message ComponentInfo { + reserved "owner"; + reserved "tags"; + reserved 1; + reserved 2; 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; } + + string client_id = 1; + string version = 2; + repeated PropertyDefinition properties = 5; + repeated ComponentInfo component_infos = 6; + string property_set_key = 7; + PublisherInfo publisherInfo = 8; } 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; } + + 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; + } } enum Platform { diff --git a/protocol/proto/recently_played.proto b/protocol/proto/recently_played.proto index fd22fdd9..95b8f2cd 100644 --- a/protocol/proto/recently_played.proto +++ b/protocol/proto/recently_played.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,7 +7,7 @@ package spotify.recently_played.proto; option optimize_for = CODE_SIZE; message Item { - string link = 1; + string uri = 1; int64 timestamp = 2; bool hidden = 3; } diff --git a/protocol/proto/recently_played_backend.proto b/protocol/proto/recently_played_backend.proto index fa137288..5533f3d3 100644 --- a/protocol/proto/recently_played_backend.proto +++ b/protocol/proto/recently_played_backend.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/record_id.proto b/protocol/proto/record_id.proto index 167c0ecd..5839058a 100644 --- a/protocol/proto/record_id.proto +++ b/protocol/proto/record_id.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/remote.proto b/protocol/proto/remote.proto index a81c1c0f..06a1d322 100644 --- a/protocol/proto/remote.proto +++ b/protocol/proto/remote.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/request_failure.proto b/protocol/proto/request_failure.proto index 10deb1be..50445542 100644 --- a/protocol/proto/request_failure.proto +++ b/protocol/proto/request_failure.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/resolve.proto b/protocol/proto/resolve.proto index 793b8c5a..e0af636e 100644 --- a/protocol/proto/resolve.proto +++ b/protocol/proto/resolve.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,15 +9,16 @@ import "property_definition.proto"; option optimize_for = CODE_SIZE; message ResolveRequest { + reserved "custom_context"; + reserved "projection"; + reserved 4; + reserved 5; string property_set_id = 1; Fetch fetch_type = 2; Context context = 11; - oneof resolution_context { - BackendContext backend_context = 12 [deprecated = true]; + BackendContext backend_context = 12; } - - //reserved 4, 5, "custom_context", "projection"; } message ResolveResponse { @@ -25,42 +26,40 @@ message ResolveResponse { } 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; } - + + message BoolValue { + bool value = 1; + } + + message IntValue { + int32 value = 1; + } + + message EnumValue { + string value = 1; + } + + PropertyDefinition.Identifier property_id = 1; + Metadata metadata = 2; 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; - } } + + string configuration_assignment_id = 1; + int64 fetch_time_millis = 2; + repeated AssignedValue assigned_values = 3; } message Fetch { - Type type = 1; enum Type { BLOCKING = 0; BACKGROUND_SYNC = 1; @@ -68,49 +67,52 @@ message Fetch { PUSH_INITIATED = 3; RECONNECT = 4; } + + Type type = 1; } message Context { - repeated ContextEntry context = 1; message ContextEntry { string value = 10; - oneof context { DynamicContext.KnownContext known_context = 1; + string policy_input_name = 2; } } + + repeated ContextEntry 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; } + + string system = 1; + string service_name = 2; + StaticContext static_context = 3; + DynamicContext dynamic_context = 4; + SurfaceMetadata surface_metadata = 10; } 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; } + + repeated ContextDefinition context_definition = 1; } diff --git a/protocol/proto/resource_type.proto b/protocol/proto/resource_type.proto index ccea6920..0803390c 100644 --- a/protocol/proto/resource_type.proto +++ b/protocol/proto/resource_type.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/response_status.proto b/protocol/proto/response_status.proto index 5709571f..af2717d9 100644 --- a/protocol/proto/response_status.proto +++ b/protocol/proto/response_status.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/restrictions.proto b/protocol/proto/restrictions.proto index 0661858c..7cf3521d 100644 --- a/protocol/proto/restrictions.proto +++ b/protocol/proto/restrictions.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -6,7 +6,17 @@ package spotify.player.proto; option optimize_for = CODE_SIZE; +message ModeRestrictions { + map values = 1; +} + +message RestrictionReasons { + repeated string reasons = 1; +} + message Restrictions { + reserved 24; + repeated string disallow_pausing_reasons = 1; repeated string disallow_resuming_reasons = 2; repeated string disallow_seeking_reasons = 3; @@ -28,4 +38,9 @@ message Restrictions { 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_add_to_queue_reasons = 22; + repeated string disallow_setting_playback_speed = 23; + map disallow_setting_modes = 25; + map disallow_signals = 26; } + diff --git a/protocol/proto/rootlist_request.proto b/protocol/proto/rootlist_request.proto index ae055475..b21e41e0 100644 --- a/protocol/proto/rootlist_request.proto +++ b/protocol/proto/rootlist_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/seek_to_position.proto b/protocol/proto/seek_to_position.proto index 6f426842..ec5cc67c 100644 --- a/protocol/proto/seek_to_position.proto +++ b/protocol/proto/seek_to_position.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sequence_number_entity.proto b/protocol/proto/sequence_number_entity.proto index a3b88c81..7f78d699 100644 --- a/protocol/proto/sequence_number_entity.proto +++ b/protocol/proto/sequence_number_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/session.proto b/protocol/proto/session.proto index 7c4589f3..6038cdf6 100644 --- a/protocol/proto/session.proto +++ b/protocol/proto/session.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -19,4 +19,7 @@ message Session { optional ContextPlayerOptionOverrides option_overrides = 4; optional Suppressions suppressions = 5; optional InstrumentationParams instrumentation_params = 6; + optional string shuffle_seed = 7; + optional Context main_context = 8; + optional string original_session_id = 9; } diff --git a/protocol/proto/set_member_permission_request.proto b/protocol/proto/set_member_permission_request.proto index 160eaf92..f35be29e 100644 --- a/protocol/proto/set_member_permission_request.proto +++ b/protocol/proto/set_member_permission_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/show_access.proto b/protocol/proto/show_access.proto index eddc0342..54a9cee6 100644 --- a/protocol/proto/show_access.proto +++ b/protocol/proto/show_access.proto @@ -1,9 +1,11 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.podcast_paywalls; +import "spotify/audiobookcashier/v1/audiobook_price.proto"; + option objc_class_prefix = "SPT"; option java_multiple_files = true; option optimize_for = CODE_SIZE; @@ -11,16 +13,30 @@ option java_outer_classname = "ShowAccessProto"; option java_package = "com.spotify.podcast.access.proto"; message ShowAccess { + reserved 7; AccountLinkPrompt prompt = 5; - + bool is_user_member_of_at_least_one_group = 8; + repeated UnlockingMethod unlocked_by = 10; + repeated UnlockingMethod unlocking_methods = 14; + Signifier signifier = 15; + Disclaimer disclaimer = 16; oneof explanation { NoExplanation none = 1; LegacyExplanation legacy = 2; BasicExplanation basic = 3; UpsellLinkExplanation upsellLink = 4; + EngagementExplanation engagement = 6; + MultiPassExplanation multiPass = 9; + CheckoutOnWebOverlayExplanation checkoutOnWebOverlay = 11; + FreeCheckoutExplanation freeCheckout = 12; + ConsumptionCappedExplanation consumptionCapped = 13; } } +message Signifier { + string text = 1; +} + message BasicExplanation { string title = 1; string body = 2; @@ -28,11 +44,9 @@ message BasicExplanation { } message LegacyExplanation { - } message NoExplanation { - } message UpsellLinkExplanation { @@ -42,9 +56,66 @@ message UpsellLinkExplanation { string url = 4; } +message EngagementExplanation { + string header = 1; + string title = 2; + string body = 3; + string cta = 4; + string dismiss = 5; + string action_type = 6; + string body_secondary = 7; +} + +message CheckoutOnWebOverlayExplanation { + string cta = 1; + string snackbar_success = 2; + string snackbar_error = 3; + string snackbar_fulfilment_complete = 4; + audiobookcashier.v1.AudiobookPrice price = 5; + bool is_price_displayed = 6; +} + +message FreeCheckoutExplanation { + string snackbar_awaiting_fulfilment = 1; +} + +message ConsumptionCappedExplanation { + string title = 1; + string body = 2; + string cta = 3; +} + +message MultiPassExplanation { + string title = 1; + string soa_description = 2; + repeated .spotify.podcast_paywalls.SOAPartner soa_partner = 3; +} + +message SOAPartner { + string display_name = 1; + string link_url = 2; + string logo_url = 3; +} + message AccountLinkPrompt { string title = 1; string body = 2; string cta = 3; string url = 4; } + +message Disclaimer { + string title = 1; + string body = 2; +} + +enum UnlockingMethod { + UNKNOWN = 0; + ANCHOR_PAYWALL = 1; + OAP_OTP = 2; + OAP_LINKING = 3; + AUDIOBOOK_DIRECT_SALES = 4; + ABP = 5; + AUDIOBOOK_PROMOTION = 6; +} + diff --git a/protocol/proto/show_episode_state.proto b/protocol/proto/show_episode_state.proto index b780dbb6..d9c52879 100644 --- a/protocol/proto/show_episode_state.proto +++ b/protocol/proto/show_episode_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/show_offline_state.proto b/protocol/proto/show_offline_state.proto new file mode 100644 index 00000000..a3cd8ae9 --- /dev/null +++ b/protocol/proto/show_offline_state.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package spotify.show_cosmos.proto; + +message ShowOfflineState { + optional string offline_state = 1; + optional uint32 sync_progress = 2; +} + diff --git a/protocol/proto/show_request.proto b/protocol/proto/show_request.proto index 3624fa04..75c3a69e 100644 --- a/protocol/proto/show_request.proto +++ b/protocol/proto/show_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -7,39 +7,37 @@ package spotify.show_cosmos.proto; import "metadata/episode_metadata.proto"; import "metadata/show_metadata.proto"; import "played_state/episode_played_state.proto"; +import "played_state/show_played_state.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"; -import "podcast_ratings.proto"; -import "greenroom_extension.proto"; +import "show_offline_state.proto"; option objc_class_prefix = "SPTShowCosmos"; option optimize_for = CODE_SIZE; message Item { + reserved 6; + reserved 7; + reserved 8; + reserved 9; 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 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; } message Header { optional cosmos_util.proto.ShowMetadata show_metadata = 1; optional ShowCollectionState show_collection_state = 2; - optional ShowPlayState show_play_state = 3; + optional cosmos_util.proto.ShowPlayState show_play_state = 3; + optional ShowOfflineState show_offline_state = 4; } message Response { + reserved "online_data"; + reserved 3; + reserved 9; repeated Item item = 1; optional Header header = 2; optional uint32 unfiltered_length = 4; @@ -47,23 +45,21 @@ message Response { optional bool loading_contents = 6; 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"; } 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; - optional ratings.PodcastRating rating_section = 7; - optional greenroom.api.extendedmetadata.v1.GreenroomSection greenroom_section = 8; - optional LatestUnplayedEpisodeSection latest_unplayed_episode_section = 9; - + reserved 2; reserved 4; + reserved 5; + reserved 6; + reserved 7; + reserved 8; + optional ContinueListeningSection continue_listening = 1; + optional TrailerSection trailer_section = 3; + optional LatestUnplayedEpisodeSection latest_unplayed_episode_section = 9; + optional NextBestEpisodeSection next_best_episode_section = 10; + optional SavedEpisodesSection saved_episodes_section = 11; } message ContinueListeningSection { @@ -77,3 +73,23 @@ message TrailerSection { message LatestUnplayedEpisodeSection { optional Item item = 1; } + +message NextBestEpisodeSection { + enum Label { + UNKNOWN = 0; + TRAILER = 1; + CONTINUE_LISTENING = 2; + LATEST_PUBLISHED = 3; + UP_NEXT = 4; + FIRST_PUBLISHED = 5; + } + + optional Label label = 1; + optional Item item = 2; +} + +message SavedEpisodesSection { + optional uint32 saved_episodes_count = 1; + optional uint32 downloaded_episodes_count = 2; +} + diff --git a/protocol/proto/show_show_state.proto b/protocol/proto/show_show_state.proto index c9c3548a..126b19e3 100644 --- a/protocol/proto/show_show_state.proto +++ b/protocol/proto/show_show_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -11,6 +11,3 @@ message ShowCollectionState { optional bool is_in_collection = 1; } -message ShowPlayState { - optional string latest_played_episode_link = 1; -} diff --git a/protocol/proto/signal-model.proto b/protocol/proto/signal-model.proto new file mode 100644 index 00000000..d3824455 --- /dev/null +++ b/protocol/proto/signal-model.proto @@ -0,0 +1,16 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + +syntax = "proto3"; + +package spotify.playlist.signal.proto; + +option java_package = "com.spotify.playlist_signal.model.proto"; +option java_outer_classname = "SignalModelProto"; +option optimize_for = CODE_SIZE; + +message Signal { + string identifier = 1; + bytes data = 2; + bytes client_payload = 3; +} + diff --git a/protocol/proto/skip_to_track.proto b/protocol/proto/skip_to_track.proto index 67b5f717..2d7830ef 100644 --- a/protocol/proto/skip_to_track.proto +++ b/protocol/proto/skip_to_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/social_connect_v2.proto b/protocol/proto/social_connect_v2.proto index f4d084c8..86b3a38e 100644 --- a/protocol/proto/social_connect_v2.proto +++ b/protocol/proto/social_connect_v2.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,6 +7,7 @@ package socialconnect; option optimize_for = CODE_SIZE; message Session { + reserved 8; int64 timestamp = 1; string session_id = 2; string join_session_token = 3; @@ -19,12 +20,7 @@ message Session { 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; + optional string host_active_device_id = 14; } message SessionMember { diff --git a/protocol/proto/social_service.proto b/protocol/proto/social_service.proto index d5c108a8..05875dc7 100644 --- a/protocol/proto/social_service.proto +++ b/protocol/proto/social_service.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -19,15 +19,12 @@ message SetAccessTokenRequest { } message SetAccessTokenResponse { - } message SubscribeToEventsRequest { - } message SubscribeToEventsResponse { - Error status = 1; enum Error { NONE = 0; FAILED_TO_CONNECT = 1; @@ -36,12 +33,12 @@ message SubscribeToEventsResponse { SERVICE_CONNECT_NOT_PERMITTED = 4; USER_UNAUTHORIZED = 5; } - + + Error status = 1; string description = 2; } message SubscribeToStateRequest { - } message SubscribeToStateResponse { @@ -50,3 +47,4 @@ message SubscribeToStateResponse { repeated string missingPermissions = 3; string accessToken = 4; } + diff --git a/protocol/proto/socialgraph_response_status.proto b/protocol/proto/socialgraph_response_status.proto index 1518daf1..91acaddf 100644 --- a/protocol/proto/socialgraph_response_status.proto +++ b/protocol/proto/socialgraph_response_status.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/socialgraphv2.proto b/protocol/proto/socialgraphv2.proto index ace70589..f365f2f9 100644 --- a/protocol/proto/socialgraphv2.proto +++ b/protocol/proto/socialgraphv2.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/spotify/audiobookcashier/v1/audiobook_price.proto b/protocol/proto/spotify/audiobookcashier/v1/audiobook_price.proto new file mode 100755 index 00000000..40352742 --- /dev/null +++ b/protocol/proto/spotify/audiobookcashier/v1/audiobook_price.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; + +package spotify.audiobookcashier.v1; + +option java_package = "com.spotify.audiobookcashier.v1"; +option java_multiple_files = true; + +message Price { + double amount = 1; + string currency = 2; + string formatted_price = 3; +} + +message AudiobookPrice { + .spotify.audiobookcashier.v1.Price final_price = 1; + .spotify.audiobookcashier.v1.Price final_list_price = 2; +} + diff --git a/protocol/proto/state_restore/ads_rules_inject_tracks.proto b/protocol/proto/state_restore/ads_rules_inject_tracks.proto index 569c8cdf..8dfaa6f3 100644 --- a/protocol/proto/state_restore/ads_rules_inject_tracks.proto +++ b/protocol/proto/state_restore/ads_rules_inject_tracks.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/automix_rules.proto b/protocol/proto/state_restore/automix_rules.proto new file mode 100644 index 00000000..8421c7c8 --- /dev/null +++ b/protocol/proto/state_restore/automix_rules.proto @@ -0,0 +1,9 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +message AutomixRules { + required bool automix = 1; + required string current_track_uri = 2; +} + diff --git a/protocol/proto/state_restore/automix_talk_rules.proto b/protocol/proto/state_restore/automix_talk_rules.proto new file mode 100644 index 00000000..8fb15713 --- /dev/null +++ b/protocol/proto/state_restore/automix_talk_rules.proto @@ -0,0 +1,11 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/provided_track.proto"; + +message AutomixTalkRules { + optional ProvidedTrack current_track = 1; + optional int64 narration_duration = 2; +} + diff --git a/protocol/proto/state_restore/behavior_metadata_rules.proto b/protocol/proto/state_restore/behavior_metadata_rules.proto index 4bb65cd4..94ced855 100644 --- a/protocol/proto/state_restore/behavior_metadata_rules.proto +++ b/protocol/proto/state_restore/behavior_metadata_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/circuit_breaker_rules.proto b/protocol/proto/state_restore/circuit_breaker_rules.proto index e81eaf57..c628122f 100644 --- a/protocol/proto/state_restore/circuit_breaker_rules.proto +++ b/protocol/proto/state_restore/circuit_breaker_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/context_loader.proto b/protocol/proto/state_restore/context_loader.proto new file mode 100644 index 00000000..ba016bb3 --- /dev/null +++ b/protocol/proto/state_restore/context_loader.proto @@ -0,0 +1,10 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context.proto"; + +message ContextLoader { + required Context context = 1; +} + diff --git a/protocol/proto/state_restore/context_player_restorable.proto b/protocol/proto/state_restore/context_player_restorable.proto new file mode 100644 index 00000000..2f278da8 --- /dev/null +++ b/protocol/proto/state_restore/context_player_restorable.proto @@ -0,0 +1,23 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/play_history.proto"; +import "state_restore/player_model.proto"; +import "state_restore/mft_state.proto"; +import "state_restore/mft_context_history.proto"; +import "state_restore/mft_fallback_page_history.proto"; +import "state_restore/pns_capper.proto"; + +message ContextPlayerRestorable { + reserved 8; + required int32 version = 1; + optional string version_suffix = 2; + required PlayerModel player_model = 3; + required PlayHistory play_history = 4; + required MftState mft_can_play_checker = 5; + required MftContextHistory mft_context_history = 6; + required MftFallbackPageHistory mft_fallback_page_history = 7; + optional PnsCapper pns_capper = 9; +} + diff --git a/protocol/proto/state_restore/context_player_rules.proto b/protocol/proto/state_restore/context_player_rules.proto index b06bf8e8..e4a406b0 100644 --- a/protocol/proto/state_restore/context_player_rules.proto +++ b/protocol/proto/state_restore/context_player_rules.proto @@ -1,16 +1,72 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; package spotify.player.proto.state_restore; -import "state_restore/context_player_rules_base.proto"; -import "state_restore/mft_rules.proto"; +import "state_restore/ads_rules_inject_tracks.proto"; +import "state_restore/automix_rules.proto"; +import "state_restore/automix_talk_rules.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/kitteh_box_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 ContextPlayerRules { - optional ContextPlayerRulesBase base = 1; - optional MftRules mft_rules = 2; - map sub_rules = 3; +message PlayEvents { + required uint64 max_consecutive = 1; + required uint64 max_occurrences_in_period = 2; + required int64 period = 3; } + +message SkipEvents { + required uint64 max_occurrences_in_period = 1; + required int64 period = 2; +} + +message Context { + required uint64 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; + optional PlayEvents social_track = 6; +} + +message MftRules { + required bool locked = 1; + optional MftConfiguration config = 2; + map old_forward_rules = 3; + optional ContextPlayerRules forward_rules = 4; +} + +message ContextPlayerRules { + optional BehaviorMetadataRules behavior_metadata_rules = 1; + optional CircuitBreakerRules circuit_breaker_rules = 2; + optional ExplicitContentRules explicit_content_rules = 3; + 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; + optional KittehBoxRules kitteh_box_rules = 12; + optional AutomixRules automix_rules = 13; + optional AutomixTalkRules automix_talk_rules = 14; + optional MftRules mft_rules = 15; + map sub_rules = 16; + optional bool is_adaptor_only = 17; +} + diff --git a/protocol/proto/state_restore/context_player_state.proto b/protocol/proto/state_restore/context_player_state.proto new file mode 100644 index 00000000..74b66a65 --- /dev/null +++ b/protocol/proto/state_restore/context_player_state.proto @@ -0,0 +1,59 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_index.proto"; +import "restrictions.proto"; +import "play_origin.proto"; +import "state_restore/provided_track.proto"; +import "context_player_options.proto"; +import "prepare_play_options.proto"; +import "state_restore/playback_quality.proto"; + +message ContextPlayerState { + message ContextMetadataEntry { + optional string key = 1; + optional string value = 2; + } + + message PageMetadataEntry { + optional string key = 1; + optional string value = 2; + } + + message ModesEntry { + optional string key = 1; + optional string value = 2; + } + + optional uint64 timestamp = 1; + optional string context_uri = 2; + optional string context_url = 3; + optional Restrictions context_restrictions = 4; + optional PlayOrigin play_origin = 5; + optional ContextIndex index = 6; + optional ProvidedTrack track = 7; + optional bytes playback_id = 8; + optional PlaybackQuality playback_quality = 9; + optional double playback_speed = 10; + optional uint64 position_as_of_timestamp = 11; + optional uint64 duration = 12; + optional bool is_playing = 13; + optional bool is_paused = 14; + optional bool is_buffering = 15; + optional bool is_system_initiated = 16; + optional ContextPlayerOptions options = 17; + optional Restrictions restrictions = 18; + repeated string suppressions = 19; + repeated ProvidedTrack prev_tracks = 20; + repeated ProvidedTrack next_tracks = 21; + repeated ContextPlayerState.ContextMetadataEntry context_metadata = 22; + repeated ContextPlayerState.PageMetadataEntry page_metadata = 23; + optional string session_id = 24; + optional uint64 queue_revision = 25; + optional AudioStream audio_stream = 26; + repeated string signals = 27; + repeated ContextPlayerState.ModesEntry modes = 28; + optional string session_command_id = 29; +} + diff --git a/protocol/proto/state_restore/explicit_content_rules.proto b/protocol/proto/state_restore/explicit_content_rules.proto index 271ad6ea..8dafae43 100644 --- a/protocol/proto/state_restore/explicit_content_rules.proto +++ b/protocol/proto/state_restore/explicit_content_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/kitteh_box_rules.proto b/protocol/proto/state_restore/kitteh_box_rules.proto new file mode 100644 index 00000000..08de7d30 --- /dev/null +++ b/protocol/proto/state_restore/kitteh_box_rules.proto @@ -0,0 +1,30 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +message KittehBoxRules { + message NodeAspectsEntry { + optional string key = 1; + optional bytes value = 2; + } + + enum Transition { + ADVANCE = 0; + SKIP_NEXT = 1; + SKIP_PREV = 2; + } + + enum Position { + BETWEEN_TRACKS = 0; + ON_DELIMITER = 1; + ON_NODE_TRACK = 2; + } + + repeated KittehBoxRules.NodeAspectsEntry node_aspects = 1; + required KittehBoxRules.Position pos = 2; + required KittehBoxRules.Transition last_transition = 3; + required int32 context_iteration = 4; + required bool pending_skip_to = 5; + optional int64 page_index = 6; +} + diff --git a/protocol/proto/state_restore/mft_context_history.proto b/protocol/proto/state_restore/mft_context_history.proto index 48e77205..8b9d7a30 100644 --- a/protocol/proto/state_restore/mft_context_history.proto +++ b/protocol/proto/state_restore/mft_context_history.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -17,3 +17,4 @@ message MftContextHistoryEntry { 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 index d01e9298..876d65c8 100644 --- a/protocol/proto/state_restore/mft_context_switch_rules.proto +++ b/protocol/proto/state_restore/mft_context_switch_rules.proto @@ -1,3 +1,5 @@ +// Extracted from: Spotify 1.2.52.442 (windows) + syntax = "proto2"; package spotify.player.proto.state_restore; diff --git a/protocol/proto/state_restore/mft_fallback_page_history.proto b/protocol/proto/state_restore/mft_fallback_page_history.proto index 54d15e8d..7daca14a 100644 --- a/protocol/proto/state_restore/mft_fallback_page_history.proto +++ b/protocol/proto/state_restore/mft_fallback_page_history.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/mft_rules_core.proto b/protocol/proto/state_restore/mft_rules_core.proto index 05549624..8845778e 100644 --- a/protocol/proto/state_restore/mft_rules_core.proto +++ b/protocol/proto/state_restore/mft_rules_core.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto b/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto index b5b8c657..41eafd48 100644 --- a/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto +++ b/protocol/proto/state_restore/mft_rules_inject_filler_tracks.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/mft_state.proto b/protocol/proto/state_restore/mft_state.proto index 8f5f9561..4494ae12 100644 --- a/protocol/proto/state_restore/mft_state.proto +++ b/protocol/proto/state_restore/mft_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -7,12 +7,12 @@ package spotify.player.proto.state_restore; option optimize_for = CODE_SIZE; message EventList { - repeated int64 event_times = 1; + repeated uint64 event_times = 1; } message LastEvent { required string uri = 1; - required int32 when = 2; + required uint64 when = 2; } message History { @@ -25,7 +25,7 @@ message MftState { required History social_track = 2; required History album = 3; required History artist = 4; - required EventList skip = 5; - required int32 time = 6; + optional EventList skip = 5; + required uint64 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 index e09ffe13..31ca96d8 100644 --- a/protocol/proto/state_restore/mod_interruption_state.proto +++ b/protocol/proto/state_restore/mod_interruption_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/mod_rules_interruptions.proto b/protocol/proto/state_restore/mod_rules_interruptions.proto index 1b965ccd..6faa2a57 100644 --- a/protocol/proto/state_restore/mod_rules_interruptions.proto +++ b/protocol/proto/state_restore/mod_rules_interruptions.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -10,6 +10,12 @@ import "state_restore/provided_track.proto"; option optimize_for = CODE_SIZE; message ModRulesInterruptions { + enum InterruptionSource { + CONTEXT = 1; + SAS = 2; + NO_INTERRUPTIONS = 3; + } + optional ProvidedTrack seek_repeat_track = 1; required uint32 prng_seed = 2; required bool support_video = 3; @@ -20,8 +26,3 @@ message ModRulesInterruptions { 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 index 5ae18bce..5670b521 100644 --- a/protocol/proto/state_restore/music_injection_rules.proto +++ b/protocol/proto/state_restore/music_injection_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/playback_state.proto b/protocol/proto/state_restore/playback_state.proto new file mode 100644 index 00000000..1b995d1a --- /dev/null +++ b/protocol/proto/state_restore/playback_state.proto @@ -0,0 +1,15 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "state_restore/playback_quality.proto"; + +message PlaybackState { + optional int64 timestamp = 1; + optional int32 position_as_of_timestamp = 2; + optional int32 duration = 3; + optional bool is_buffering = 4; + optional PlaybackQuality playback_quality = 5; + optional double playback_speed = 6; +} + diff --git a/protocol/proto/state_restore/player_model.proto b/protocol/proto/state_restore/player_model.proto new file mode 100644 index 00000000..aa08495c --- /dev/null +++ b/protocol/proto/state_restore/player_model.proto @@ -0,0 +1,38 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_player_options.proto"; +import "state_restore/player_session_queue.proto"; + +message PlayerModel { + message ConfigurationEntry { + optional string key = 1; + optional string value = 2; + } + + enum AdvanceReason { + SKIP_TO_PREV_TRACK = 1; + SKIP_TO_NEXT_TRACK = 2; + EXTERNAL_ADVANCE = 3; + INTERRUPTED = 4; + SWITCHED_TO_VIDEO = 5; + SWITCHED_TO_AUDIO = 6; + } + + enum StartReason { + PLAY_CONTEXT = 1; + PLAY_CONTEXT_TRACK = 2; + STATE_RESTORE = 3; + REMOTE_TRANSFER = 4; + } + + required ContextPlayerOptions options = 1; + repeated PlayerModel.ConfigurationEntry configuration = 2; + required PlayerSessionQueue session_queue = 3; + required PlayerModel.AdvanceReason last_advance_reason = 4; + optional PlayerModel.StartReason last_start_reason = 5; + required string prev_state_id = 6; + optional string override_state_id = 7; +} + diff --git a/protocol/proto/state_restore/player_session.proto b/protocol/proto/state_restore/player_session.proto new file mode 100644 index 00000000..ee5d525f --- /dev/null +++ b/protocol/proto/state_restore/player_session.proto @@ -0,0 +1,39 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context_track.proto"; +import "context_player_options.proto"; +import "logging_params.proto"; +import "play_origin.proto"; +import "player_license.proto"; +import "prepare_play_options.proto"; +import "state_restore/context_loader.proto"; +import "state_restore/context_player_rules.proto"; +import "state_restore/playback_state.proto"; +import "state_restore/player_session_fake.proto"; +import "state_restore/provided_track.proto"; + +message PlayerSession { + required PreparePlayOptions prepare_play_options = 1; + optional PlaybackState playback_state = 2; + optional ProvidedTrack track = 3; + optional ContextTrack track_to_skip_to = 4; + optional bytes given_playback_id = 5; + required LoggingParams next_command_logging_params = 6; + required LoggingParams curr_command_logging_params = 7; + required PlayOrigin play_origin = 8; + required bool is_playing = 9; + required bool is_paused = 10; + required bool is_system_initiated = 11; + required bool is_finished = 12; + required ContextPlayerOptions options = 13; + required uint64 playback_seed = 14; + required int32 num_advances = 15; + required bool did_skip_prev = 16; + required PlayerLicense license = 17; + required ContextPlayerRules rules = 18; + required ContextLoader loader = 19; + optional PlayerSessionFake fake = 100; +} + diff --git a/protocol/proto/state_restore/player_session_fake.proto b/protocol/proto/state_restore/player_session_fake.proto new file mode 100644 index 00000000..7f405127 --- /dev/null +++ b/protocol/proto/state_restore/player_session_fake.proto @@ -0,0 +1,13 @@ +syntax = "proto2"; + +package spotify.player.proto.state_restore; + +import "context.proto"; +import "state_restore/context_player_state.proto"; + +message PlayerSessionFake { + required ContextPlayerState player_state = 1; + required Context player_context = 2; + required bool is_finished = 3; +} + diff --git a/protocol/proto/state_restore/player_session_queue.proto b/protocol/proto/state_restore/player_session_queue.proto index 22ee7941..036c277e 100644 --- a/protocol/proto/state_restore/player_session_queue.proto +++ b/protocol/proto/state_restore/player_session_queue.proto @@ -1,27 +1,26 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; +import "state_restore/player_session.proto"; + 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; + enum Trigger { + DID_GO_PAST_TRACK = 1; + DID_GO_PAST_CONTEXT = 2; + } + + optional QueuedSession.Trigger trigger = 1; + optional PlayerSession session = 2; } message PlayerSessionQueue { - optional SessionJson active = 1; - repeated SessionJson pushed = 2; + optional PlayerSession active = 1; + repeated PlayerSession 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 index a61010e5..7332cb57 100644 --- a/protocol/proto/state_restore/provided_track.proto +++ b/protocol/proto/state_restore/provided_track.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/random_source.proto b/protocol/proto/state_restore/random_source.proto index f1ad1019..f2739c57 100644 --- a/protocol/proto/state_restore/random_source.proto +++ b/protocol/proto/state_restore/random_source.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/remove_banned_tracks_rules.proto b/protocol/proto/state_restore/remove_banned_tracks_rules.proto index 9db5c70c..99de63aa 100644 --- a/protocol/proto/state_restore/remove_banned_tracks_rules.proto +++ b/protocol/proto/state_restore/remove_banned_tracks_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/resume_points_rules.proto b/protocol/proto/state_restore/resume_points_rules.proto index 6f2618a9..31d35a6d 100644 --- a/protocol/proto/state_restore/resume_points_rules.proto +++ b/protocol/proto/state_restore/resume_points_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/state_restore/track_error_rules.proto b/protocol/proto/state_restore/track_error_rules.proto index e13b8562..a241d88c 100644 --- a/protocol/proto/state_restore/track_error_rules.proto +++ b/protocol/proto/state_restore/track_error_rules.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/status.proto b/protocol/proto/status.proto index 1293af57..017cc826 100644 --- a/protocol/proto/status.proto +++ b/protocol/proto/status.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/status_code.proto b/protocol/proto/status_code.proto index abc8bd49..08153b1e 100644 --- a/protocol/proto/status_code.proto +++ b/protocol/proto/status_code.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,7 +8,10 @@ option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; enum StatusCode { - SUCCESS = 0; - NO_PLAYBACK_ID = 1; + INVALID_STATUS_CODE = 0; + SUCCESS = 1; EVENT_SENDER_ERROR = 2; + INVALID_STREAM_HANDLE = 3; + PENDING_EVENTS_ERROR = 4; + IGNORED = 5; } diff --git a/protocol/proto/status_response.proto b/protocol/proto/status_response.proto index 78d15c9a..27173957 100644 --- a/protocol/proto/status_response.proto +++ b/protocol/proto/status_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -11,5 +11,4 @@ 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 index 1cb3b673..8ccd73a8 100644 --- a/protocol/proto/storage-resolve.proto +++ b/protocol/proto/storage-resolve.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,7 +13,7 @@ message StorageResolveResponse { STORAGE = 1; RESTRICTED = 3; } - + repeated string cdnurl = 2; bytes fileid = 4; } diff --git a/protocol/proto/storylines.proto b/protocol/proto/storylines.proto index c9361966..11509454 100644 --- a/protocol/proto/storylines.proto +++ b/protocol/proto/storylines.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/stream_end_request.proto b/protocol/proto/stream_end_request.proto index ed72fd51..762d0941 100644 --- a/protocol/proto/stream_end_request.proto +++ b/protocol/proto/stream_end_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -15,5 +15,6 @@ message StreamEndRequest { StreamHandle stream_handle = 1; string source_end = 2; PlayReason reason_end = 3; - MediaFormat format = 4; + google.protobuf.Timestamp client_timestamp = 5; + optional AudioFormat format = 4; } diff --git a/protocol/proto/stream_handle.proto b/protocol/proto/stream_handle.proto index b66ed4ce..b293fa18 100644 --- a/protocol/proto/stream_handle.proto +++ b/protocol/proto/stream_handle.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -8,5 +8,6 @@ option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamHandle { - string playback_id = 1; + reserved 1; + uint32 raw_handle = 2; } diff --git a/protocol/proto/stream_progress_request.proto b/protocol/proto/stream_progress_request.proto index 63fe9d80..4c68e690 100644 --- a/protocol/proto/stream_progress_request.proto +++ b/protocol/proto/stream_progress_request.proto @@ -1,10 +1,13 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.stream_reporting_esperanto.proto; +import "google/protobuf/timestamp.proto"; +import "audio_format.proto"; import "stream_handle.proto"; +import "playback_state.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; @@ -19,4 +22,12 @@ message StreamProgressRequest { bool is_fullscreen = 7; bool is_external = 8; double playback_speed = 9; + google.protobuf.Timestamp client_timestamp = 14; + PlaybackState playback_state = 15; + optional string media_id = 10; + optional bool content_is_downloaded = 11; + optional AudioFormat audio_format = 12; + optional string content_uri = 13; + optional bool is_audio_on = 16; + optional string video_surface = 17; } diff --git a/protocol/proto/stream_seek_request.proto b/protocol/proto/stream_seek_request.proto index 7d99169e..32c6ac6b 100644 --- a/protocol/proto/stream_seek_request.proto +++ b/protocol/proto/stream_seek_request.proto @@ -1,16 +1,20 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.stream_reporting_esperanto.proto; +import "google/protobuf/timestamp.proto"; import "stream_handle.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamSeekRequest { + reserved 2; StreamHandle stream_handle = 1; uint64 from_position = 3; uint64 to_position = 4; + google.protobuf.Timestamp client_timestamp = 5; + optional bool is_system_initiated = 6; } diff --git a/protocol/proto/stream_start_request.proto b/protocol/proto/stream_start_request.proto index 656016a6..3f762dd0 100644 --- a/protocol/proto/stream_start_request.proto +++ b/protocol/proto/stream_start_request.proto @@ -1,31 +1,37 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.stream_reporting_esperanto.proto; +import "google/protobuf/timestamp.proto"; import "media_type.proto"; import "play_reason.proto"; +import "playback_stack.proto"; +import "playback_stack_v2.proto"; import "streaming_rule.proto"; option objc_class_prefix = "ESP"; option java_package = "com.spotify.stream_reporting_esperanto.proto"; message StreamStartRequest { - string playback_id = 1; - string parent_playback_id = 2; + reserved 9; + reserved 10; + reserved 13; + reserved 14; + reserved 27; + reserved 25; + reserved 35; + bytes playback_id = 1; + bytes 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 content_uri = 6; + string displayed_content_uri = 7; + PlaybackStack playback_stack = 8; 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; @@ -33,12 +39,25 @@ message StreamStartRequest { 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 content_is_downloaded = 33; bool client_offline = 34; + string feature_uuid = 36; + string decision_id = 37; + string custom_reporting_attribution = 38; + string play_context_decision_id = 39; + google.protobuf.Timestamp client_timestamp = 40; + bool is_video_on = 44; + string player_session_id = 47; + optional bool is_repeating_track = 41; + optional bool is_repeating_context = 42; + optional bool is_audio_on = 43; + optional string video_surface = 45; + optional PlaybackStackV2 playback_stack_v2 = 46; + optional string preview_impression_uri = 48; } + diff --git a/protocol/proto/stream_start_response.proto b/protocol/proto/stream_start_response.proto index 98af2976..fd1f710c 100644 --- a/protocol/proto/stream_start_response.proto +++ b/protocol/proto/stream_start_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/streaming_rule.proto b/protocol/proto/streaming_rule.proto index 9593fdef..fbf57382 100644 --- a/protocol/proto/streaming_rule.proto +++ b/protocol/proto/streaming_rule.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -13,4 +13,5 @@ enum StreamingRule { STREAMING_RULE_PREVIEW = 2; STREAMING_RULE_WIFI = 3; STREAMING_RULE_SHUFFLE_MODE = 4; + STREAMING_RULE_TABLET_FREE = 5; } diff --git a/protocol/proto/suppressions.proto b/protocol/proto/suppressions.proto index 4ddfaefb..f514b03f 100644 --- a/protocol/proto/suppressions.proto +++ b/protocol/proto/suppressions.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sync/album_sync_state.proto b/protocol/proto/sync/album_sync_state.proto index 7ea90276..b06a35da 100644 --- a/protocol/proto/sync/album_sync_state.proto +++ b/protocol/proto/sync/album_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sync/artist_sync_state.proto b/protocol/proto/sync/artist_sync_state.proto index 03ba32f3..93f7495f 100644 --- a/protocol/proto/sync/artist_sync_state.proto +++ b/protocol/proto/sync/artist_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sync/episode_sync_state.proto b/protocol/proto/sync/episode_sync_state.proto index 7dce8424..fa1511e6 100644 --- a/protocol/proto/sync/episode_sync_state.proto +++ b/protocol/proto/sync/episode_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sync/track_sync_state.proto b/protocol/proto/sync/track_sync_state.proto index 8873fad5..f9f4be01 100644 --- a/protocol/proto/sync/track_sync_state.proto +++ b/protocol/proto/sync/track_sync_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/sync_request.proto b/protocol/proto/sync_request.proto index b2d77625..18d5b650 100644 --- a/protocol/proto/sync_request.proto +++ b/protocol/proto/sync_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/track_instance.proto b/protocol/proto/track_instance.proto index 952f28c8..fd501bfe 100644 --- a/protocol/proto/track_instance.proto +++ b/protocol/proto/track_instance.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -11,12 +11,11 @@ import "seek_to_position.proto"; option optimize_for = CODE_SIZE; message TrackInstance { + reserved 3; 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 index 3b8b8baf..47ee739a 100644 --- a/protocol/proto/track_instantiator.proto +++ b/protocol/proto/track_instantiator.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; diff --git a/protocol/proto/transfer_state.proto b/protocol/proto/transfer_state.proto index 200547c0..a90b048e 100644 --- a/protocol/proto/transfer_state.proto +++ b/protocol/proto/transfer_state.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -6,6 +6,7 @@ package spotify.player.proto.transfer; import "context_player_options.proto"; import "playback.proto"; +import "play_history.proto"; import "session.proto"; import "queue.proto"; @@ -16,4 +17,5 @@ message TransferState { optional Playback playback = 2; optional Session current_session = 3; optional Queue queue = 4; + optional PlayHistory play_history = 5; } diff --git a/protocol/proto/tts-resolve.proto b/protocol/proto/tts-resolve.proto index adb50854..0d0bcf02 100644 --- a/protocol/proto/tts-resolve.proto +++ b/protocol/proto/tts-resolve.proto @@ -1,15 +1,11 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; -package spotify.narration_injection.proto; +package spotify.narration.proto; option optimize_for = CODE_SIZE; -service TtsResolveService { - rpc Resolve(ResolveRequest) returns (ResolveResponse); -} - message ResolveRequest { AudioFormat audio_format = 3; enum AudioFormat { @@ -20,17 +16,54 @@ message ResolveRequest { VORBIS = 4; MP3 = 5; } - + string language = 4; - + TtsVoice tts_voice = 5; enum TtsVoice { UNSET_TTS_VOICE = 0; VOICE1 = 1; VOICE2 = 2; VOICE3 = 3; + VOICE4 = 4; + VOICE5 = 5; + VOICE6 = 6; + VOICE7 = 7; + VOICE8 = 8; + VOICE9 = 9; + VOICE10 = 10; + VOICE11 = 11; + VOICE12 = 12; + VOICE13 = 13; + VOICE14 = 14; + VOICE15 = 15; + VOICE16 = 16; + VOICE17 = 17; + VOICE18 = 18; + VOICE19 = 19; + VOICE20 = 20; + VOICE21 = 21; + VOICE22 = 22; + VOICE23 = 23; + VOICE24 = 24; + VOICE25 = 25; + VOICE26 = 26; + VOICE27 = 27; + VOICE28 = 28; + VOICE29 = 29; + VOICE30 = 30; + VOICE31 = 31; + VOICE32 = 32; + VOICE33 = 33; + VOICE34 = 34; + VOICE35 = 35; + VOICE36 = 36; + VOICE37 = 37; + VOICE38 = 38; + VOICE39 = 39; + VOICE40 = 40; } - + TtsProvider tts_provider = 6; enum TtsProvider { UNSET_TTS_PROVIDER = 0; @@ -38,10 +71,11 @@ message ResolveRequest { READSPEAKER = 2; POLLY = 3; WELL_SAID = 4; + SONANTIC_DEPRECATED = 5; + SONANTIC_FAST = 6; } - + int32 sample_rate_hz = 7; - oneof prompt { string text = 1; string ssml = 2; @@ -50,4 +84,5 @@ message ResolveRequest { message ResolveResponse { string url = 1; + int64 expiry = 2; } diff --git a/protocol/proto/ucs.proto b/protocol/proto/ucs.proto index c5048f8c..d3c82997 100644 --- a/protocol/proto/ucs.proto +++ b/protocol/proto/ucs.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -16,12 +16,11 @@ message UcsRequest { string request_orgin_version = 2; string reason = 3; } - + ResolveRequest resolve_request = 2; - + AccountAttributesRequest account_attributes_request = 3; message AccountAttributesRequest { - } } @@ -30,25 +29,23 @@ message UcsResponseWrapper { 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 index 68e5f903..ffeb703b 100644 --- a/protocol/proto/unfinished_episodes_request.proto +++ b/protocol/proto/unfinished_episodes_request.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto2"; @@ -21,6 +21,6 @@ message Episode { message Response { repeated Episode episode = 2; - + reserved 1; } diff --git a/protocol/proto/useraccount.proto b/protocol/proto/useraccount.proto index ca8fea90..fd73fe02 100644 --- a/protocol/proto/useraccount.proto +++ b/protocol/proto/useraccount.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; diff --git a/protocol/proto/your_library_config.proto b/protocol/proto/your_library_config.proto new file mode 100644 index 00000000..3030f34b --- /dev/null +++ b/protocol/proto/your_library_config.proto @@ -0,0 +1,57 @@ +syntax = "proto3"; + +package spotify.your_library.proto; + +message YourLibraryLabelAndImage { + string label = 1; + string image = 2; + bool include_empty = 3; +} + +message YourLibraryPseudoPlaylistConfig { + .spotify.your_library.proto.YourLibraryLabelAndImage liked_songs = 1; + .spotify.your_library.proto.YourLibraryLabelAndImage your_episodes = 2; + .spotify.your_library.proto.YourLibraryLabelAndImage new_episodes = 3; + .spotify.your_library.proto.YourLibraryLabelAndImage local_files = 4; + .spotify.your_library.proto.YourLibraryLabelAndImage cached_files = 5; + bool your_highlights = 6; + bool all_available_configs_provided = 99; +} + +message YourLibraryFilters { + enum Filter { + ALBUM = 0; + ARTIST = 1; + PLAYLIST = 2; + SHOW = 3; + BOOK = 4; + EVENT = 5; + AUTHOR = 7; + DOWNLOADED = 100; + WRITABLE = 101; + BY_YOU = 102; + BY_SPOTIFY = 103; + UNPLAYED = 104; + IN_PROGRESS = 105; + FINISHED = 106; + } + + repeated .spotify.your_library.proto.YourLibraryFilters.Filter filter = 1; +} + +message YourLibrarySortOrder { + enum SortOrder { + NAME = 0; + RECENTLY_ADDED = 1; + CREATOR = 2; + CUSTOM = 4; + RECENTLY_UPDATED = 5; + RECENTLY_PLAYED_OR_ADDED = 6; + RELEVANCE = 7; + EVENT_START_TIME = 8; + RELEASE_DATE = 9; + } + + .spotify.your_library.proto.YourLibrarySortOrder.SortOrder sort_order = 1; +} + diff --git a/protocol/proto/your_library_contains_request.proto b/protocol/proto/your_library_contains_request.proto index bbb43c20..ea1f746d 100644 --- a/protocol/proto/your_library_contains_request.proto +++ b/protocol/proto/your_library_contains_request.proto @@ -1,14 +1,17 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; -import "your_library_pseudo_playlist_config.proto"; +import "your_library_config.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "spotify.your_library.esperanto.proto"; message YourLibraryContainsRequest { repeated string requested_uri = 3; YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 4; + int32 update_throttling = 5; } diff --git a/protocol/proto/your_library_contains_response.proto b/protocol/proto/your_library_contains_response.proto index 641d71a5..232f9939 100644 --- a/protocol/proto/your_library_contains_response.proto +++ b/protocol/proto/your_library_contains_response.proto @@ -1,10 +1,12 @@ -// Extracted from: Spotify 1.1.61.583 (Windows) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "spotify.your_library.esperanto.proto"; message YourLibraryContainsResponseHeader { bool is_loading = 2; @@ -18,5 +20,6 @@ message YourLibraryContainsResponseEntity { message YourLibraryContainsResponse { YourLibraryContainsResponseHeader header = 1; repeated YourLibraryContainsResponseEntity entity = 2; + uint32 status_code = 98; string error = 99; } diff --git a/protocol/proto/your_library_decorate_request.proto b/protocol/proto/your_library_decorate_request.proto index 6b77a976..9c35b1df 100644 --- a/protocol/proto/your_library_decorate_request.proto +++ b/protocol/proto/your_library_decorate_request.proto @@ -1,14 +1,17 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; -import "your_library_pseudo_playlist_config.proto"; +import "your_library_config.proto"; +option java_package = "spotify.your_library.esperanto.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; message YourLibraryDecorateRequest { repeated string requested_uri = 3; YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 6; + int32 update_throttling = 7; } diff --git a/protocol/proto/your_library_decorate_response.proto b/protocol/proto/your_library_decorate_response.proto index 125d5c33..b6896df2 100644 --- a/protocol/proto/your_library_decorate_response.proto +++ b/protocol/proto/your_library_decorate_response.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -6,7 +6,9 @@ package spotify.your_library.proto; import "your_library_decorated_entity.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "spotify.your_library.esperanto.proto"; message YourLibraryDecorateResponseHeader { bool is_loading = 2; @@ -15,5 +17,6 @@ message YourLibraryDecorateResponseHeader { message YourLibraryDecorateResponse { YourLibraryDecorateResponseHeader header = 1; repeated YourLibraryDecoratedEntity entity = 2; + uint32 status_code = 98; string error = 99; } diff --git a/protocol/proto/your_library_decorated_entity.proto b/protocol/proto/your_library_decorated_entity.proto index c31b45eb..848ab09e 100644 --- a/protocol/proto/your_library_decorated_entity.proto +++ b/protocol/proto/your_library_decorated_entity.proto @@ -1,26 +1,29 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; +import "policy/supported_link_types_in_playlists.proto"; + option optimize_for = CODE_SIZE; message YourLibraryEntityInfo { - string key = 1; + enum Pinnable { + YES = 0; + NO_IN_FOLDER = 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; + int64 add_time = 11; + int64 last_played = 12; + bool has_curated_items = 13; } message Offline { @@ -31,20 +34,51 @@ message Offline { DOWNLOADING = 3; WAITING = 4; } + } message YourLibraryAlbumExtraInfo { + enum Type { + ALBUM = 0; + SINGLE = 1; + COMPILATION = 2; + EP = 3; + } + string artist_name = 1; + string artist_uri = 2; + Type type = 3; + bool is_premium_only = 4; + bool new_release = 5; } message YourLibraryArtistExtraInfo { - + bool has_liked_tracks_or_albums = 1; +} + +message NumberOfItemsForLinkType { + playlist.cosmos.proto.LinkType link_type = 1; + int32 num_items = 2; +} + +message YourLibraryPlaylistFolderInfo { + string uri = 1; + string name = 2; } message YourLibraryPlaylistExtraInfo { string creator_name = 1; + string creator_uri = 8; bool is_loading = 5; bool can_view = 6; + bool can_add = 9; + string row_id = 7; + string made_for_name = 10; + string made_for_uri = 11; + repeated NumberOfItemsForLinkType number_of_items_per_link_type = 12; + bool owned_by_self = 13; + YourLibraryPlaylistFolderInfo from_folder = 14; + string name_prefix = 15; } message YourLibraryShowExtraInfo { @@ -57,6 +91,8 @@ message YourLibraryShowExtraInfo { message YourLibraryFolderExtraInfo { int32 number_of_playlists = 2; int32 number_of_folders = 3; + string row_id = 4; + repeated YourLibraryDecoratedEntity entity = 5; } message YourLibraryLikedSongsExtraInfo { @@ -76,12 +112,53 @@ message YourLibraryLocalFilesExtraInfo { } message YourLibraryBookExtraInfo { + enum Access { + OPEN = 0; + LOCKED = 1; + CAPPED = 2; + } + + enum State { + NOT_STARTED = 0; + IN_PROGRESS = 1; + FINISHED = 2; + } + string author_name = 1; + Access access = 2; + int64 milliseconds_left = 3; + int32 percent_done = 4; + State state = 5; +} + +message YourLibraryCachedFilesExtraInfo { + int32 number_of_items = 1; + int32 duration_in_seconds = 2; +} + +message YourLibraryPreReleaseExtraInfo { + enum Type { + ALBUM = 0; + BOOK = 1; + } + + string artist_name = 1; + string artist_uri = 2; + Type type = 3; + YourLibraryAlbumExtraInfo.Type album_type = 4; +} + +message YourLibraryEventExtraInfo { + string location_name = 1; + int64 start_time = 2; + string city_name = 3; +} + +message YourLibraryAuthorExtraInfo { } message YourLibraryDecoratedEntity { YourLibraryEntityInfo entity_info = 1; - oneof entity { YourLibraryAlbumExtraInfo album = 2; YourLibraryArtistExtraInfo artist = 3; @@ -93,13 +170,10 @@ message YourLibraryDecoratedEntity { YourLibraryNewEpisodesExtraInfo new_episodes = 10; YourLibraryLocalFilesExtraInfo local_files = 11; YourLibraryBookExtraInfo book = 12; + YourLibraryCachedFilesExtraInfo cached_files = 13; + YourLibraryPreReleaseExtraInfo prerelease = 15; + YourLibraryEventExtraInfo event = 16; + YourLibraryAuthorExtraInfo author = 17; } } -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 897fc6c1..174136ec 100644 --- a/protocol/proto/your_library_entity.proto +++ b/protocol/proto/your_library_entity.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -9,24 +9,15 @@ 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.CollectionAlbumLikeEntry album = 2; - collection.proto.CollectionArtistEntry artist = 3; - YourLibraryRootlistEntity rootlist_entity = 4; - YourLibraryShowWrapper show = 7; - YourLibraryBookWrapper book = 8; + collection.proto.CollectionAlbumEntry album = 1; + collection.proto.CollectionArtistEntry artist = 2; + YourLibraryRootlistEntity rootlist_entity = 3; + collection.proto.CollectionShowEntry show = 4; + collection.proto.CollectionBookEntry book = 5; + YourLibraryPreReleaseEntity prerelease = 6; + YourLibraryEventEntity event = 7; + collection.proto.CollectionAuthorEntry author = 9; } } diff --git a/protocol/proto/your_library_index.proto b/protocol/proto/your_library_index.proto index 835c0fa2..68ada3d2 100644 --- a/protocol/proto/your_library_index.proto +++ b/protocol/proto/your_library_index.proto @@ -1,4 +1,4 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; @@ -7,26 +7,50 @@ package spotify.your_library.proto; option optimize_for = CODE_SIZE; message YourLibraryRootlistPlaylist { - string image_uri = 1; - bool is_loading = 3; - int32 rootlist_index = 4; - bool can_view = 5; + string prefix = 1; + string image_uri = 2; + string creator_uri = 3; + string made_for_name = 4; + string made_for_uri = 5; + bool is_loading = 6; + int32 rootlist_index = 7; + string row_id = 8; + bool can_view = 9; + bool can_add = 10; + bool owned_by_self = 11; +} + +message YourLibraryPredefinedPlaylist { + string prefix = 1; + string image_uri = 2; + string creator_uri = 3; + string made_for_name = 4; + string made_for_uri = 5; + bool is_loading = 6; + bool can_view = 7; + bool can_add = 8; + bool owned_by_self = 9; } message YourLibraryRootlistFolder { int32 number_of_playlists = 1; int32 number_of_folders = 2; int32 rootlist_index = 3; + string row_id = 4; } -message YourLibraryRootlistCollection { - Kind kind = 1; +message YourLibraryRootlistPseudoPlaylist { enum Kind { LIKED_SONGS = 0; YOUR_EPISODES = 1; NEW_EPISODES = 2; LOCAL_FILES = 3; + CACHED_FILES = 4; + CONTENT_FEED = 5; + YOUR_HIGHLIGHTS = 6; } + + Kind kind = 1; } message YourLibraryRootlistEntity { @@ -34,10 +58,41 @@ message YourLibraryRootlistEntity { string name = 2; string creator_name = 3; int64 add_time = 4; - + int64 last_played = 5; oneof entity { - YourLibraryRootlistPlaylist playlist = 5; - YourLibraryRootlistFolder folder = 6; - YourLibraryRootlistCollection collection = 7; + YourLibraryRootlistPlaylist playlist = 6; + YourLibraryRootlistFolder folder = 7; + YourLibraryRootlistPseudoPlaylist pseudo_playlist = 8; + YourLibraryPredefinedPlaylist predefined_playlist = 9; } } + +message YourLibraryPreReleaseEntity { + enum Type { + ALBUM = 0; + BOOK = 1; + } + + string entity_name = 1; + string uri = 2; + string creator_name = 3; + string creator_uri = 4; + string image_uri = 5; + int64 add_time = 6; + int64 release_time = 9; + Type type = 7; + string type_str = 8; +} + +message YourLibraryEventEntity { + string uri = 1; + string event_name = 2; + repeated string artist_names = 3; + string location_name = 4; + string image_uri = 5; + int64 add_time = 6; + int64 event_time = 7; + int64 utc_event_time = 8; + string city_name = 9; +} + diff --git a/protocol/proto/your_library_request.proto b/protocol/proto/your_library_request.proto index 917a1add..bbbdd5d8 100644 --- a/protocol/proto/your_library_request.proto +++ b/protocol/proto/your_library_request.proto @@ -1,18 +1,59 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; -import "your_library_pseudo_playlist_config.proto"; +import "your_library_config.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "spotify.your_library.esperanto.proto"; + +message YourLibraryTagFilter { + string tag_uri = 1; +} + +message CuratedItems { + enum CuratedItemsFilter { + NONE = 0; + GROUP_BY = 1; + ONLY_CURATED = 2; + ONLY_NOT_CURATED = 3; + } + + repeated string items = 1; + CuratedItemsFilter filter = 2; +} message YourLibraryRequestHeader { bool remaining_entities = 9; + bool total_count = 18; + string lower_bound = 10; + int32 skip = 11; + int32 length = 12; + string text_filter = 13; + YourLibraryFilters filters = 14; + YourLibrarySortOrder sort_order = 15; + bool all_playlists = 17; + repeated int64 fill_folders = 34; + bool separate_pinned_items = 22; + bool num_link_types_in_playlists = 25; + bool ignore_pinning = 26; + CuratedItems curated_items = 29; + bool include_events = 30; + bool include_prereleases = 31; + bool include_authors = 33; + oneof maybe_folder_id { + int64 folder_id = 16; + } + oneof maybe_tag_filter { + .spotify.your_library.proto.YourLibraryTagFilter tag_filter = 24; + } } message YourLibraryRequest { YourLibraryRequestHeader header = 1; YourLibraryPseudoPlaylistConfig pseudo_playlist_config = 4; + int32 update_throttling = 5; } diff --git a/protocol/proto/your_library_response.proto b/protocol/proto/your_library_response.proto index c354ff5b..3297d7d4 100644 --- a/protocol/proto/your_library_response.proto +++ b/protocol/proto/your_library_response.proto @@ -1,23 +1,48 @@ -// Extracted from: Spotify 1.1.73.517 (macOS) +// Extracted from: Spotify 1.2.52.442 (windows) syntax = "proto3"; package spotify.your_library.proto; import "your_library_decorated_entity.proto"; +import "your_library_config.proto"; +option java_multiple_files = true; option optimize_for = CODE_SIZE; +option java_package = "spotify.your_library.esperanto.proto"; + +message YourLibraryTagPlaylist { + string name = 1; + string uri = 2; + string description = 3; + string image_uri = 4; + proto.Offline.Availability offline_availability = 5; + bool is_curated = 6; + bool is_loading = 7; +} + +message YourLibraryTagInfo { + string tag_name = 1; + bool is_added = 5; + YourLibraryTagPlaylist tag_playlist_info = 7; +} message YourLibraryResponseHeader { int32 remaining_entities = 9; + int32 total_count = 17; + int32 pin_count = 18; + int32 maximum_pinned_items = 19; bool is_loading = 12; - YourLibraryAvailableEntityTypes has = 13; - YourLibraryAvailableEntityTypes has_downloaded = 14; string folder_name = 15; + string parent_folder_uri = 20; + YourLibraryFilters available_filters = 16; + YourLibraryTagInfo tag_info = 21; } message YourLibraryResponse { YourLibraryResponseHeader header = 1; repeated YourLibraryDecoratedEntity entity = 2; + repeated YourLibraryDecoratedEntity pinned_entity = 3; + int32 status_code = 98; string error = 99; } diff --git a/protocol/src/conversion.rs b/protocol/src/conversion.rs new file mode 100644 index 00000000..13286e7f --- /dev/null +++ b/protocol/src/conversion.rs @@ -0,0 +1,173 @@ +use crate::{ + context_player_options::ContextPlayerOptions, + play_origin::PlayOrigin, + player::{ + ContextPlayerOptions as PlayerContextPlayerOptions, + ModeRestrictions as PlayerModeRestrictions, PlayOrigin as PlayerPlayOrigin, + RestrictionReasons as PlayerRestrictionReasons, Restrictions as PlayerRestrictions, + Suppressions as PlayerSuppressions, + }, + restrictions::{ModeRestrictions, RestrictionReasons, Restrictions}, + suppressions::Suppressions, +}; +use std::collections::HashMap; + +fn hashmap_into, V>(map: HashMap) -> HashMap { + map.into_iter().map(|(k, v)| (k, v.into())).collect() +} + +impl From for PlayerContextPlayerOptions { + fn from(value: ContextPlayerOptions) -> Self { + PlayerContextPlayerOptions { + shuffling_context: value.shuffling_context.unwrap_or_default(), + repeating_context: value.repeating_context.unwrap_or_default(), + repeating_track: value.repeating_track.unwrap_or_default(), + modes: value.modes, + playback_speed: value.playback_speed, + special_fields: value.special_fields, + } + } +} + +impl From for Restrictions { + fn from(value: PlayerRestrictions) -> Self { + Restrictions { + disallow_pausing_reasons: value.disallow_pausing_reasons, + disallow_resuming_reasons: value.disallow_resuming_reasons, + disallow_seeking_reasons: value.disallow_seeking_reasons, + disallow_peeking_prev_reasons: value.disallow_peeking_prev_reasons, + disallow_peeking_next_reasons: value.disallow_peeking_next_reasons, + disallow_skipping_prev_reasons: value.disallow_skipping_prev_reasons, + disallow_skipping_next_reasons: value.disallow_skipping_next_reasons, + disallow_toggling_repeat_context_reasons: value + .disallow_toggling_repeat_context_reasons, + disallow_toggling_repeat_track_reasons: value.disallow_toggling_repeat_track_reasons, + disallow_toggling_shuffle_reasons: value.disallow_toggling_shuffle_reasons, + disallow_set_queue_reasons: value.disallow_set_queue_reasons, + disallow_interrupting_playback_reasons: value.disallow_interrupting_playback_reasons, + disallow_transferring_playback_reasons: value.disallow_transferring_playback_reasons, + disallow_remote_control_reasons: value.disallow_remote_control_reasons, + disallow_inserting_into_next_tracks_reasons: value + .disallow_inserting_into_next_tracks_reasons, + disallow_inserting_into_context_tracks_reasons: value + .disallow_inserting_into_context_tracks_reasons, + disallow_reordering_in_next_tracks_reasons: value + .disallow_reordering_in_next_tracks_reasons, + disallow_reordering_in_context_tracks_reasons: value + .disallow_reordering_in_context_tracks_reasons, + disallow_removing_from_next_tracks_reasons: value + .disallow_removing_from_next_tracks_reasons, + disallow_removing_from_context_tracks_reasons: value + .disallow_removing_from_context_tracks_reasons, + disallow_updating_context_reasons: value.disallow_updating_context_reasons, + disallow_add_to_queue_reasons: value.disallow_add_to_queue_reasons, + disallow_setting_playback_speed: value.disallow_setting_playback_speed_reasons, + disallow_setting_modes: hashmap_into(value.disallow_setting_modes), + disallow_signals: hashmap_into(value.disallow_signals), + special_fields: value.special_fields, + } + } +} + +impl From for PlayerRestrictions { + fn from(value: Restrictions) -> Self { + PlayerRestrictions { + disallow_pausing_reasons: value.disallow_pausing_reasons, + disallow_resuming_reasons: value.disallow_resuming_reasons, + disallow_seeking_reasons: value.disallow_seeking_reasons, + disallow_peeking_prev_reasons: value.disallow_peeking_prev_reasons, + disallow_peeking_next_reasons: value.disallow_peeking_next_reasons, + disallow_skipping_prev_reasons: value.disallow_skipping_prev_reasons, + disallow_skipping_next_reasons: value.disallow_skipping_next_reasons, + disallow_toggling_repeat_context_reasons: value + .disallow_toggling_repeat_context_reasons, + disallow_toggling_repeat_track_reasons: value.disallow_toggling_repeat_track_reasons, + disallow_toggling_shuffle_reasons: value.disallow_toggling_shuffle_reasons, + disallow_set_queue_reasons: value.disallow_set_queue_reasons, + disallow_interrupting_playback_reasons: value.disallow_interrupting_playback_reasons, + disallow_transferring_playback_reasons: value.disallow_transferring_playback_reasons, + disallow_remote_control_reasons: value.disallow_remote_control_reasons, + disallow_inserting_into_next_tracks_reasons: value + .disallow_inserting_into_next_tracks_reasons, + disallow_inserting_into_context_tracks_reasons: value + .disallow_inserting_into_context_tracks_reasons, + disallow_reordering_in_next_tracks_reasons: value + .disallow_reordering_in_next_tracks_reasons, + disallow_reordering_in_context_tracks_reasons: value + .disallow_reordering_in_context_tracks_reasons, + disallow_removing_from_next_tracks_reasons: value + .disallow_removing_from_next_tracks_reasons, + disallow_removing_from_context_tracks_reasons: value + .disallow_removing_from_context_tracks_reasons, + disallow_updating_context_reasons: value.disallow_updating_context_reasons, + disallow_add_to_queue_reasons: value.disallow_add_to_queue_reasons, + disallow_setting_playback_speed_reasons: value.disallow_setting_playback_speed, + disallow_setting_modes: hashmap_into(value.disallow_setting_modes), + disallow_signals: hashmap_into(value.disallow_signals), + disallow_playing_reasons: vec![], + disallow_stopping_reasons: vec![], + special_fields: value.special_fields, + } + } +} + +impl From for ModeRestrictions { + fn from(value: PlayerModeRestrictions) -> Self { + ModeRestrictions { + values: hashmap_into(value.values), + special_fields: value.special_fields, + } + } +} + +impl From for PlayerModeRestrictions { + fn from(value: ModeRestrictions) -> Self { + PlayerModeRestrictions { + values: hashmap_into(value.values), + special_fields: value.special_fields, + } + } +} + +impl From for RestrictionReasons { + fn from(value: PlayerRestrictionReasons) -> Self { + RestrictionReasons { + reasons: value.reasons, + special_fields: value.special_fields, + } + } +} + +impl From for PlayerRestrictionReasons { + fn from(value: RestrictionReasons) -> Self { + PlayerRestrictionReasons { + reasons: value.reasons, + special_fields: value.special_fields, + } + } +} + +impl From for PlayerPlayOrigin { + fn from(value: PlayOrigin) -> Self { + PlayerPlayOrigin { + feature_identifier: value.feature_identifier.unwrap_or_default(), + feature_version: value.feature_version.unwrap_or_default(), + view_uri: value.view_uri.unwrap_or_default(), + external_referrer: value.external_referrer.unwrap_or_default(), + referrer_identifier: value.referrer_identifier.unwrap_or_default(), + device_identifier: value.device_identifier.unwrap_or_default(), + feature_classes: value.feature_classes, + restriction_identifier: value.restriction_identifier.unwrap_or_default(), + special_fields: value.special_fields, + } + } +} + +impl From for PlayerSuppressions { + fn from(value: Suppressions) -> Self { + PlayerSuppressions { + providers: value.providers, + special_fields: value.special_fields, + } + } +} diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 224043e7..05aef10f 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,4 +1,6 @@ // This file is parsed by build.rs // Each included module will be compiled from the matching .proto definition. +mod conversion; + include!(concat!(env!("OUT_DIR"), "/mod.rs")); From 7003e98c1b671755bd7b6c2f6ef1668c0223da52 Mon Sep 17 00:00:00 2001 From: eladyn <59307989+eladyn@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:27:34 +0100 Subject: [PATCH 485/561] metadata: handle empty trailer_uri for shows (#1432) * metadata: handle empty trailer_uri for shows * chore: update changelog --- CHANGELOG.md | 1 + metadata/src/show.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 146b6fe1..1edce464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 client ID on Android platform. - [connect] Fix "play" command not handled if missing "offset" property - [discovery] Fix libmdns zerconf setup errors not propagating to the main task. +- [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking) ### Removed diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 62faa107..8f57b18c 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -27,7 +27,7 @@ pub struct Show { pub media_type: ShowMediaType, pub consumption_order: ShowConsumptionOrder, pub availability: Availabilities, - pub trailer_uri: SpotifyId, + pub trailer_uri: Option, pub has_music_and_talk: bool, pub is_audiobook: bool, } @@ -63,7 +63,12 @@ impl TryFrom<&::Message> for Show { media_type: show.media_type(), consumption_order: show.consumption_order(), availability: show.availability.as_slice().try_into()?, - trailer_uri: SpotifyId::from_uri(show.trailer_uri())?, + trailer_uri: show + .trailer_uri + .as_deref() + .filter(|s| !s.is_empty()) + .map(SpotifyId::from_uri) + .transpose()?, has_music_and_talk: show.music_and_talk(), is_audiobook: show.is_audiobook(), }) From 4cecb2289a6da84c7742041765d74a903e50e4ca Mon Sep 17 00:00:00 2001 From: Fabio Waljaard Date: Sun, 12 Jan 2025 20:39:45 +0100 Subject: [PATCH 486/561] Handle 'None' uri or empty string uri in transfer command (#1439) --- CHANGELOG.md | 1 + connect/src/state/context.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1edce464..674e45bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Fix "play" command not handled if missing "offset" property - [discovery] Fix libmdns zerconf setup errors not propagating to the main task. - [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking) +- [connect] Handle transfer of playback with empty "uri" field ### Removed diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 11827cc5..5abd52fe 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -342,7 +342,7 @@ impl ConnectState { Err(StateError::InvalidTrackUri(Some(uri.clone())))? } (Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?, - (None, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?, + (_, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?, _ => Err(StateError::InvalidTrackUri(None))?, }; From 3a570fc5f12575a29f1573c7943f2c0c8d13238e Mon Sep 17 00:00:00 2001 From: Fabio Waljaard Date: Sun, 12 Jan 2025 22:48:38 +0100 Subject: [PATCH 487/561] Fix pause state when transferring (#1444) --- CHANGELOG.md | 1 + connect/src/spirc.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 674e45bf..4ce1a3fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [discovery] Fix libmdns zerconf setup errors not propagating to the main task. - [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking) - [connect] Handle transfer of playback with empty "uri" field +- [connect] Correctly apply playing/paused state when transferring playback ### Removed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e8286523..f5681fd1 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1140,7 +1140,7 @@ impl SpircTask { _ => 0, }; - let is_playing = matches!(transfer.playback.is_paused, Some(is_playing) if is_playing); + let is_playing = !transfer.playback.is_paused(); if self.connect_state.current_track(|t| t.is_autoplay()) || autoplay { debug!("currently in autoplay context, async resolving autoplay for {ctx_uri}"); From 14e3965ea3768b7529c039ad84bd58f3839aa426 Mon Sep 17 00:00:00 2001 From: Mateusz Mojsiejuk Date: Sun, 12 Jan 2025 23:03:56 +0100 Subject: [PATCH 488/561] Shrink/Optimize Dockerfile (#1430) Changes Shrink the resulting image with 70mb by removing un-used dependencies and merging several runs to a single layer. --- contrib/Dockerfile | 57 +++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 23 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index a450d6ca..64d306c0 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -8,38 +8,49 @@ # The compiled binaries will be located in /tmp/librespot-build # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features alsa-backend # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend FROM debian:bookworm -RUN echo "deb http://deb.debian.org/debian bookworm main" > /etc/apt/sources.list -RUN echo "deb http://deb.debian.org/debian bookworm-updates main" >> /etc/apt/sources.list -RUN echo "deb http://deb.debian.org/debian-security bookworm-security main" >> /etc/apt/sources.list +RUN echo "deb http://deb.debian.org/debian bookworm main" > /etc/apt/sources.list && \ + echo "deb http://deb.debian.org/debian bookworm-updates main" >> /etc/apt/sources.list && \ + echo "deb http://deb.debian.org/debian-security bookworm-security main" >> /etc/apt/sources.list -RUN dpkg --add-architecture arm64 -RUN dpkg --add-architecture armhf -RUN dpkg --add-architecture armel -RUN apt-get update +RUN dpkg --add-architecture arm64 && \ + dpkg --add-architecture armhf && \ + dpkg --add-architecture armel && \ + apt-get update && \ + apt-get install -y \ + build-essential \ + cmake \ + crossbuild-essential-arm64 \ + crossbuild-essential-armel \ + crossbuild-essential-armhf \ + curl \ + libasound2-dev \ + libasound2-dev:arm64 \ + libasound2-dev:armel \ + libasound2-dev:armhf \ + libclang-dev \ + libpulse0 \ + libpulse0:arm64 \ + libpulse0:armel \ + libpulse0:armhf \ + pkg-config -RUN apt-get install -y cmake libclang-dev -RUN apt-get install -y curl git build-essential crossbuild-essential-arm64 crossbuild-essential-armel crossbuild-essential-armhf pkg-config -RUN apt-get install -y libasound2-dev libasound2-dev:arm64 libasound2-dev:armel libasound2-dev:armhf -RUN apt-get install -y libpulse0 libpulse0:arm64 libpulse0:armel libpulse0:armhf - -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y ENV PATH="/root/.cargo/bin/:${PATH}" -RUN rustup target add aarch64-unknown-linux-gnu -RUN rustup target add arm-unknown-linux-gnueabi -RUN rustup target add arm-unknown-linux-gnueabihf -RUN cargo install bindgen-cli - -RUN mkdir /.cargo && \ - echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ - echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ - echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y && \ + rustup target add aarch64-unknown-linux-gnu && \ + rustup target add arm-unknown-linux-gnueabi && \ + rustup target add arm-unknown-linux-gnueabihf && \ + cargo install bindgen-cli && \ + mkdir /.cargo && \ + echo '[target.aarch64-unknown-linux-gnu]\nlinker = "aarch64-linux-gnu-gcc"' > /.cargo/config && \ + echo '[target.arm-unknown-linux-gnueabihf]\nlinker = "arm-linux-gnueabihf-gcc"' >> /.cargo/config && \ + echo '[target.arm-unknown-linux-gnueabi]\nlinker = "arm-linux-gnueabi-gcc"' >> /.cargo/config ENV CARGO_TARGET_DIR=/build ENV CARGO_HOME=/build/cache From c288cf710680574309441937ecc9b0c36cd331d4 Mon Sep 17 00:00:00 2001 From: catango Date: Sat, 18 Jan 2025 13:59:44 +0100 Subject: [PATCH 489/561] Fix testcases for cross compilation (#1443) Crossbuild breaks due to missing bindgen. Additionally install bindgen-cli as it is not part of the default github runnter any more. --- .github/workflows/test.yml | 8 +++++++- CHANGELOG.md | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ff877e12..b58e61c2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -241,7 +241,13 @@ jobs: - name: Install the cross compiler rust targets run: rustup target add ${{ matrix.target }} - + + - name: Update and Install dependencies + run: sudo apt-get update && sudo apt-get install -y build-essential cmake libclang1 + + - name: Install bindgen-cli + run: cargo install --force --locked bindgen-cli + - name: Install cross compiler run: | if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ce1a3fb..ad5c9432 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- [test] Missing bindgen breaks crossbuild on recent runners. Now installing latest bindgen in addition. - [core] Fix "no native root CA certificates found" on platforms unsupported by `rustls-native-certs`. - [core] Fix all APs rejecting with "TryAnotherAP" when connecting session From f3bb38085171da4cdb135ef2dc8f22871bf56727 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sat, 18 Jan 2025 16:45:33 +0100 Subject: [PATCH 490/561] Dealer: Rework context retrieval (#1414) * connect: simplify `handle_command` for SpircCommand * connect: simplify `handle_player_event` * connect: `handle_player_event` update log entries * connect: set `playback_speed` according to player state * connect: reduce/group state updates by delaying them slightly * connect: load entire context at once * connect: use is_playing from connect_state * connect: move `ResolveContext` in own file * connect: handle metadata correct * connect: resolve context rework - resolved contexts independently, by that we don't block our main loop - move resolve logic into own file - polish handling for play and transfer * connect: rework aftermath * general logging and comment fixups * connect: fix incorrect stopping * connect: always handle player seek event * connect: adjust behavior - rename `handle_context` to `handle_next_context` - disconnect should only pause the playback - find_next should not exceed queue length * fix typo and `find_next` * connect: fixes for context and transfer - fix context_metadata and restriction incorrect reset - do some state updates earlier - add more logging * revert removal of state setup * `clear_next_tracks` should never clear queued items just mimics official client behavior * connect: make `playing_track` optional and handle it correctly * connect: adjust finish of context resolving * connect: set track position when shuffling * example: adjust to model change * connect: remove duplicate track * connect: provide all recently played tracks to autoplay request - removes previously added workaround * connect: apply review suggestions - use drain instead of for with pop - use for instead of loop - use or_else instead of match - use Self::Error instead of the value - free memory for metadata and restrictions * connect: impl trait for player context * connect: fix incorrect playing and paused * connect: apply options as official clients * protocol: move trait impls into impl_trait mod --- connect/src/context_resolver.rs | 347 ++++++++ connect/src/lib.rs | 1 + connect/src/model.rs | 155 +--- connect/src/spirc.rs | 780 ++++++++---------- connect/src/state.rs | 41 +- connect/src/state/context.rs | 280 ++++--- connect/src/state/handle.rs | 13 +- connect/src/state/options.rs | 16 +- connect/src/state/restrictions.rs | 8 +- connect/src/state/tracks.rs | 60 +- connect/src/state/transfer.rs | 12 +- core/src/dealer/protocol.rs | 4 +- core/src/spclient.rs | 2 +- examples/play_connect.rs | 2 +- protocol/src/impl_trait.rs | 2 + protocol/src/impl_trait/context.rs | 13 + .../{conversion.rs => impl_trait/player.rs} | 0 protocol/src/lib.rs | 2 +- 18 files changed, 1004 insertions(+), 734 deletions(-) create mode 100644 connect/src/context_resolver.rs create mode 100644 protocol/src/impl_trait.rs create mode 100644 protocol/src/impl_trait/context.rs rename protocol/src/{conversion.rs => impl_trait/player.rs} (100%) diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs new file mode 100644 index 00000000..278fc089 --- /dev/null +++ b/connect/src/context_resolver.rs @@ -0,0 +1,347 @@ +use crate::{ + core::{Error, Session}, + protocol::{ + autoplay_context_request::AutoplayContextRequest, context::Context, + transfer_state::TransferState, + }, + state::{ + context::{ContextType, UpdateContext}, + ConnectState, + }, +}; +use std::cmp::PartialEq; +use std::{ + collections::{HashMap, VecDeque}, + fmt::{Display, Formatter}, + hash::Hash, + time::Duration, +}; +use thiserror::Error as ThisError; +use tokio::time::Instant; + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +enum Resolve { + Uri(String), + Context(Context), +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(super) enum ContextAction { + Append, + Replace, +} + +#[derive(Debug, Clone, Hash, PartialEq, Eq)] +pub(super) struct ResolveContext { + resolve: Resolve, + fallback: Option, + update: UpdateContext, + action: ContextAction, +} + +impl ResolveContext { + fn append_context(uri: impl Into) -> Self { + Self { + resolve: Resolve::Uri(uri.into()), + fallback: None, + update: UpdateContext::Default, + action: ContextAction::Append, + } + } + + pub fn from_uri( + uri: impl Into, + fallback: impl Into, + update: UpdateContext, + action: ContextAction, + ) -> Self { + let fallback_uri = fallback.into(); + Self { + resolve: Resolve::Uri(uri.into()), + fallback: (!fallback_uri.is_empty()).then_some(fallback_uri), + update, + action, + } + } + + pub fn from_context(context: Context, update: UpdateContext, action: ContextAction) -> Self { + Self { + resolve: Resolve::Context(context), + fallback: None, + update, + action, + } + } + + /// the uri which should be used to resolve the context, might not be the context uri + fn resolve_uri(&self) -> Option<&str> { + // it's important to call this always, or at least for every ResolveContext + // otherwise we might not even check if we need to fallback and just use the fallback uri + match self.resolve { + Resolve::Uri(ref uri) => ConnectState::valid_resolve_uri(uri), + Resolve::Context(ref ctx) => ConnectState::get_context_uri_from_context(ctx), + } + .or(self.fallback.as_deref()) + } + + /// the actual context uri + fn context_uri(&self) -> &str { + match self.resolve { + Resolve::Uri(ref uri) => uri, + Resolve::Context(ref ctx) => ctx.uri.as_deref().unwrap_or_default(), + } + } +} + +impl Display for ResolveContext { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "resolve_uri: <{:?}>, context_uri: <{}>, update: <{:?}>", + self.resolve_uri(), + self.context_uri(), + self.update, + ) + } +} + +#[derive(Debug, ThisError)] +enum ContextResolverError { + #[error("no next context to resolve")] + NoNext, + #[error("tried appending context with {0} pages")] + UnexpectedPagesSize(usize), + #[error("tried resolving not allowed context: {0:?}")] + NotAllowedContext(String), +} + +impl From for Error { + fn from(value: ContextResolverError) -> Self { + Error::failed_precondition(value) + } +} + +pub struct ContextResolver { + session: Session, + queue: VecDeque, + unavailable_contexts: HashMap, +} + +// time after which an unavailable context is retried +const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600); + +impl ContextResolver { + pub fn new(session: Session) -> Self { + Self { + session, + queue: VecDeque::new(), + unavailable_contexts: HashMap::new(), + } + } + + pub fn add(&mut self, resolve: ResolveContext) { + let last_try = self + .unavailable_contexts + .get(&resolve) + .map(|i| i.duration_since(Instant::now())); + + let last_try = if matches!(last_try, Some(last_try) if last_try > RETRY_UNAVAILABLE) { + let _ = self.unavailable_contexts.remove(&resolve); + debug!( + "context was requested {}s ago, trying again to resolve the requested context", + last_try.expect("checked by condition").as_secs() + ); + None + } else { + last_try + }; + + if last_try.is_some() { + debug!("tried loading unavailable context: {resolve}"); + return; + } else if self.queue.contains(&resolve) { + debug!("update for {resolve} is already added"); + return; + } else { + trace!( + "added {} to resolver queue", + resolve.resolve_uri().unwrap_or(resolve.context_uri()) + ) + } + + self.queue.push_back(resolve) + } + + pub fn add_list(&mut self, resolve: Vec) { + for resolve in resolve { + self.add(resolve) + } + } + + pub fn remove_used_and_invalid(&mut self) { + if let Some((_, _, remove)) = self.find_next() { + let _ = self.queue.drain(0..remove); // remove invalid + } + self.queue.pop_front(); // remove used + } + + pub fn clear(&mut self) { + self.queue = VecDeque::new() + } + + fn find_next(&self) -> Option<(&ResolveContext, &str, usize)> { + for idx in 0..self.queue.len() { + let next = self.queue.get(idx)?; + match next.resolve_uri() { + None => { + warn!("skipped {idx} because of invalid resolve_uri: {next}"); + continue; + } + Some(uri) => return Some((next, uri, idx)), + } + } + None + } + + pub fn has_next(&self) -> bool { + self.find_next().is_some() + } + + pub async fn get_next_context( + &self, + recent_track_uri: impl Fn() -> Vec, + ) -> Result { + let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?; + + match next.update { + UpdateContext::Default => { + let mut ctx = self.session.spclient().get_context(resolve_uri).await; + if let Ok(ctx) = ctx.as_mut() { + ctx.uri = Some(next.context_uri().to_string()); + ctx.url = ctx.uri.as_ref().map(|s| format!("context://{s}")); + } + + ctx + } + UpdateContext::Autoplay => { + if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") + { + // autoplay is not supported for podcasts + Err(ContextResolverError::NotAllowedContext( + resolve_uri.to_string(), + ))? + } + + let request = AutoplayContextRequest { + context_uri: Some(resolve_uri.to_string()), + recent_track_uri: recent_track_uri(), + ..Default::default() + }; + self.session.spclient().get_autoplay_context(&request).await + } + } + } + + pub fn mark_next_unavailable(&mut self) { + if let Some((next, _, _)) = self.find_next() { + self.unavailable_contexts + .insert(next.clone(), Instant::now()); + } + } + + pub fn apply_next_context( + &self, + state: &mut ConnectState, + mut context: Context, + ) -> Result>, Error> { + let (next, _, _) = self.find_next().ok_or(ContextResolverError::NoNext)?; + + let remaining = match next.action { + ContextAction::Append if context.pages.len() == 1 => state + .fill_context_from_page(context.pages.remove(0)) + .map(|_| None), + ContextAction::Replace => { + let remaining = state.update_context(context, next.update); + if let Resolve::Context(ref ctx) = next.resolve { + state.merge_context(Some(ctx.clone())); + } + + remaining + } + ContextAction::Append => { + warn!("unexpected page size: {context:#?}"); + Err(ContextResolverError::UnexpectedPagesSize(context.pages.len()).into()) + } + }?; + + Ok(remaining.map(|remaining| { + remaining + .into_iter() + .map(ResolveContext::append_context) + .collect::>() + })) + } + + pub fn try_finish( + &self, + state: &mut ConnectState, + transfer_state: &mut Option, + ) -> bool { + let (next, _, _) = match self.find_next() { + None => return false, + Some(next) => next, + }; + + // when there is only one update type, we are the last of our kind, so we should update the state + if self + .queue + .iter() + .filter(|resolve| resolve.update == next.update) + .count() + != 1 + { + return false; + } + + match (next.update, state.active_context) { + (UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => { + debug!( + "last item of type <{:?}>, finishing state setup", + next.update + ); + } + (UpdateContext::Default, _) => { + debug!("skipped finishing default, because it isn't the active context"); + return false; + } + } + + let active_ctx = state.get_context(state.active_context); + let res = if let Some(transfer_state) = transfer_state.take() { + state.finish_transfer(transfer_state) + } else if state.shuffling_context() { + state.shuffle() + } else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) { + // has context, and context is not touched + // when the index is not zero, the next index was already evaluated elsewhere + let ctx = active_ctx.expect("checked by precondition"); + let idx = ConnectState::find_index_in_context(ctx, |t| { + state.current_track(|c| t.uri == c.uri) + }) + .ok(); + + state.reset_playback_to_position(idx) + } else { + state.fill_up_next_tracks() + }; + + if let Err(why) = res { + error!("setup of state failed: {why}, last used resolve {next:#?}") + } + + state.update_restrictions(); + state.update_queue_revision(); + + true + } +} diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 3cfbbca1..11a65186 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -5,6 +5,7 @@ use librespot_core as core; use librespot_playback as playback; use librespot_protocol as protocol; +mod context_resolver; mod model; pub mod spirc; pub mod state; diff --git a/connect/src/model.rs b/connect/src/model.rs index a080f968..8315ee29 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -1,8 +1,4 @@ -use crate::state::ConnectState; use librespot_core::dealer::protocol::SkipTo; -use librespot_protocol::context::Context; -use std::fmt::{Display, Formatter}; -use std::hash::{Hash, Hasher}; #[derive(Debug)] pub struct SpircLoadCommand { @@ -13,7 +9,11 @@ pub struct SpircLoadCommand { pub shuffle: bool, pub repeat: bool, pub repeat_track: bool, - pub playing_track: PlayingTrack, + /// Decides the starting position in the given context + /// + /// ## Remarks: + /// If none is provided and shuffle true, a random track is played, otherwise the first + pub playing_track: Option, } #[derive(Debug)] @@ -23,19 +23,20 @@ pub enum PlayingTrack { Uid(String), } -impl From for PlayingTrack { - fn from(value: SkipTo) -> Self { +impl TryFrom for PlayingTrack { + type Error = (); + + fn try_from(value: SkipTo) -> Result { // order of checks is important, as the index can be 0, but still has an uid or uri provided, // so we only use the index as last resort if let Some(uri) = value.track_uri { - PlayingTrack::Uri(uri) + Ok(PlayingTrack::Uri(uri)) } else if let Some(uid) = value.track_uid { - PlayingTrack::Uid(uid) + Ok(PlayingTrack::Uid(uid)) + } else if let Some(index) = value.track_index { + Ok(PlayingTrack::Index(index)) } else { - PlayingTrack::Index(value.track_index.unwrap_or_else(|| { - warn!("SkipTo didn't provided any point to skip to, falling back to index 0"); - 0 - })) + Err(()) } } } @@ -58,131 +59,3 @@ pub(super) enum SpircPlayStatus { preloading_of_next_track_triggered: bool, }, } - -#[derive(Debug, Clone)] -pub(super) struct ResolveContext { - context: Context, - fallback: Option, - autoplay: bool, - /// if `true` updates the entire context, otherwise only fills the context from the next - /// retrieve page, it is usually used when loading the next page of an already established context - /// - /// like for example: - /// - playing an artists profile - update: bool, -} - -impl ResolveContext { - pub fn from_uri(uri: impl Into, fallback: impl Into, autoplay: bool) -> Self { - let fallback_uri = fallback.into(); - Self { - context: Context { - uri: Some(uri.into()), - ..Default::default() - }, - fallback: (!fallback_uri.is_empty()).then_some(fallback_uri), - autoplay, - update: true, - } - } - - pub fn from_context(context: Context, autoplay: bool) -> Self { - Self { - context, - fallback: None, - autoplay, - update: true, - } - } - - // expected page_url: hm://artistplaycontext/v1/page/spotify/album/5LFzwirfFwBKXJQGfwmiMY/km_artist - pub fn from_page_url(page_url: String) -> Self { - let split = if let Some(rest) = page_url.strip_prefix("hm://") { - rest.split('/') - } else { - warn!("page_url didn't started with hm://. got page_url: {page_url}"); - page_url.split('/') - }; - - let uri = split - .skip_while(|s| s != &"spotify") - .take(3) - .collect::>() - .join(":"); - - trace!("created an ResolveContext from page_url <{page_url}> as uri <{uri}>"); - - Self { - context: Context { - uri: Some(uri), - ..Default::default() - }, - fallback: None, - update: false, - autoplay: false, - } - } - - /// the uri which should be used to resolve the context, might not be the context uri - pub fn resolve_uri(&self) -> Option<&String> { - // it's important to call this always, or at least for every ResolveContext - // otherwise we might not even check if we need to fallback and just use the fallback uri - ConnectState::get_context_uri_from_context(&self.context) - .and_then(|s| (!s.is_empty()).then_some(s)) - .or(self.fallback.as_ref()) - } - - /// the actual context uri - pub fn context_uri(&self) -> &str { - self.context.uri.as_deref().unwrap_or_default() - } - - pub fn autoplay(&self) -> bool { - self.autoplay - } - - pub fn update(&self) -> bool { - self.update - } -} - -impl Display for ResolveContext { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "resolve_uri: <{:?}>, context_uri: <{:?}>, autoplay: <{}>, update: <{}>", - self.resolve_uri(), - self.context.uri, - self.autoplay, - self.update - ) - } -} - -impl PartialEq for ResolveContext { - fn eq(&self, other: &Self) -> bool { - let eq_context = self.context_uri() == other.context_uri(); - let eq_resolve = self.resolve_uri() == other.resolve_uri(); - let eq_autoplay = self.autoplay == other.autoplay; - let eq_update = self.update == other.update; - - eq_context && eq_resolve && eq_autoplay && eq_update - } -} - -impl Eq for ResolveContext {} - -impl Hash for ResolveContext { - fn hash(&self, state: &mut H) { - self.context_uri().hash(state); - self.resolve_uri().hash(state); - self.autoplay.hash(state); - self.update.hash(state); - } -} - -impl From for Context { - fn from(value: ResolveContext) -> Self { - value.context - } -} diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index f5681fd1..8546296c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,6 +1,6 @@ pub use crate::model::{PlayingTrack, SpircLoadCommand}; -use crate::state::{context::ResetContext, metadata::Metadata}; use crate::{ + context_resolver::{ContextAction, ContextResolver, ResolveContext}, core::{ authentication::Credentials, dealer::{ @@ -10,12 +10,12 @@ use crate::{ session::UserAttributes, Error, Session, SpotifyId, }, + model::SpircPlayStatus, playback::{ mixer::Mixer, player::{Player, PlayerEvent, PlayerEventChannel}, }, protocol::{ - autoplay_context_request::AutoplayContextRequest, connect::{Cluster, ClusterUpdate, LogoutCommand, SetVolumeCommand}, context::Context, explicit_content_pubsub::UserAttributesUpdate, @@ -24,19 +24,17 @@ use crate::{ transfer_state::TransferState, user_attributes::UserAttributesMutation, }, -}; -use crate::{ - model::{ResolveContext, SpircPlayStatus}, state::{ - context::{ContextType, LoadNext, UpdateContext}, + context::{ + ResetContext, {ContextType, UpdateContext}, + }, + metadata::Metadata, provider::IsProvider, {ConnectState, ConnectStateConfig}, }, }; use futures_util::StreamExt; use protobuf::MessageField; -use std::collections::HashMap; -use std::time::Instant; use std::{ future::Future, sync::atomic::{AtomicUsize, Ordering}, @@ -96,17 +94,11 @@ struct SpircTask { commands: Option>, player_events: Option, + context_resolver: ContextResolver, + shutdown: bool, session: Session, - /// the list of contexts to resolve - resolve_context: Vec, - - /// contexts may not be resolvable at the moment so we should ignore any further request - /// - /// an unavailable context is retried after [RETRY_UNAVAILABLE] - unavailable_contexts: HashMap, - /// is set when transferring, and used after resolving the contexts to finish the transfer pub transfer_state: Option, @@ -114,6 +106,10 @@ struct SpircTask { /// when no other future resolves, otherwise resets the delay update_volume: bool, + /// when set to true, it will update the volume after [UPDATE_STATE_DELAY], + /// when no other future resolves, otherwise resets the delay + update_state: bool, + spirc_id: usize, } @@ -143,12 +139,10 @@ const CONTEXT_FETCH_THRESHOLD: usize = 2; const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS -// delay to resolve a bundle of context updates, delaying the update prevents duplicate context updates of the same type -const RESOLVE_CONTEXT_DELAY: Duration = Duration::from_millis(500); -// time after which an unavailable context is retried -const RETRY_UNAVAILABLE: Duration = Duration::from_secs(3600); // delay to update volume after a certain amount of time, instead on each update request const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2); +// to reduce updates to remote, we group some request by waiting for a set amount of time +const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200); pub struct Spirc { commands: mpsc::UnboundedSender, @@ -246,13 +240,14 @@ impl Spirc { commands: Some(cmd_rx), player_events: Some(player_events), + context_resolver: ContextResolver::new(session.clone()), + shutdown: false, session, - resolve_context: Vec::new(), - unavailable_contexts: HashMap::new(), transfer_state: None, update_volume: false, + update_state: false, spirc_id, }; @@ -355,6 +350,10 @@ impl SpircTask { let commands = self.commands.as_mut(); let player_events = self.player_events.as_mut(); + // when state and volume update have a higher priority than context resolving + // because of that the context resolving has to wait, so that the other tasks can finish + let allow_context_resolving = !self.update_state && !self.update_volume; + tokio::select! { // startup of the dealer requires a connection_id, which is retrieved at the very beginning connection_id_update = self.connection_id_update.next() => unwrap! { @@ -417,13 +416,15 @@ impl SpircTask { } }, 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).await { + if let Err(e) = self.handle_player_event(event) { error!("could not dispatch player event: {}", e); } }, - _ = async { sleep(RESOLVE_CONTEXT_DELAY).await }, if !self.resolve_context.is_empty() => { - if let Err(why) = self.handle_resolve_context().await { - error!("ContextError: {why}") + _ = async { sleep(UPDATE_STATE_DELAY).await }, if self.update_state => { + self.update_state = false; + + if let Err(why) = self.notify().await { + error!("state update: {why}") } }, _ = async { sleep(VOLUME_UPDATE_DELAY).await }, if self.update_volume => { @@ -441,7 +442,27 @@ impl SpircTask { error!("error updating connect state for volume update: {why}") } }, - else => break, + // context resolver handling, the idea/reason behind it the following: + // + // when we request a context that has multiple pages (for example an artist) + // resolving all pages at once can take around ~1-30sec, when we resolve + // everything at once that would block our main loop for that time + // + // to circumvent this behavior, we request each context separately here and + // finish after we received our last item of a type + next_context = async { + self.context_resolver.get_next_context(|| { + self.connect_state.recent_track_uris() + }).await + }, if allow_context_resolving && self.context_resolver.has_next() => { + let update_state = self.handle_next_context(next_context); + if update_state { + if let Err(why) = self.notify().await { + error!("update after context resolving failed: {why}") + } + } + }, + else => break } } @@ -455,154 +476,48 @@ impl SpircTask { self.session.dealer().close().await; } - async fn handle_resolve_context(&mut self) -> Result<(), Error> { - let mut last_resolve = None::; - while let Some(resolve) = self.resolve_context.pop() { - if matches!(last_resolve, Some(ref last_resolve) if last_resolve == &resolve) { - debug!("did already update the context for {resolve}"); - continue; - } else { - last_resolve = Some(resolve.clone()); + fn handle_next_context(&mut self, next_context: Result) -> bool { + let next_context = match next_context { + Err(why) => { + self.context_resolver.mark_next_unavailable(); + self.context_resolver.remove_used_and_invalid(); + error!("{why}"); + return false; + } + Ok(ctx) => ctx, + }; - let resolve_uri = match resolve.resolve_uri() { - Some(resolve) => resolve, - None => { - warn!("tried to resolve context without resolve_uri: {resolve}"); - return Ok(()); - } - }; + debug!("handling next context {:?}", next_context.uri); - debug!("resolving: {resolve}"); - // the autoplay endpoint can return a 404, when it tries to retrieve an - // autoplay context for an empty playlist as it seems - if let Err(why) = self - .resolve_context( - resolve_uri, - resolve.context_uri(), - resolve.autoplay(), - resolve.update(), - ) - .await - { - error!("failed resolving context <{resolve}>: {why}"); - self.unavailable_contexts.insert(resolve, Instant::now()); - continue; + match self + .context_resolver + .apply_next_context(&mut self.connect_state, next_context) + { + Ok(remaining) => { + if let Some(remaining) = remaining { + self.context_resolver.add_list(remaining) } - - self.connect_state.merge_context(Some(resolve.into())); + } + Err(why) => { + error!("{why}") } } - if let Some(transfer_state) = self.transfer_state.take() { - self.connect_state.finish_transfer(transfer_state)? - } - - if matches!(self.connect_state.active_context, ContextType::Default) { - let ctx = self.connect_state.context.as_ref(); - if matches!(ctx, Some(ctx) if ctx.tracks.is_empty()) { - self.connect_state.clear_next_tracks(true); - self.handle_next(None)?; - } - } - - self.connect_state.fill_up_next_tracks()?; - self.connect_state.update_restrictions(); - self.connect_state.update_queue_revision(); - - self.preload_autoplay_when_required(); - - self.notify().await - } - - async fn resolve_context( - &mut self, - resolve_uri: &str, - context_uri: &str, - autoplay: bool, - update: bool, - ) -> Result<(), Error> { - if !autoplay { - let mut ctx = self.session.spclient().get_context(resolve_uri).await?; - - if update { - ctx.uri = Some(context_uri.to_string()); - ctx.url = Some(format!("context://{context_uri}")); - - self.connect_state - .update_context(ctx, UpdateContext::Default)? - } else if matches!(ctx.pages.first(), Some(p) if !p.tracks.is_empty()) { - debug!( - "update context from single page, context {:?} had {} pages", - ctx.uri, - ctx.pages.len() - ); - self.connect_state - .fill_context_from_page(ctx.pages.remove(0))?; - } else { - error!("resolving context should only update the tracks, but had no page, or track. {ctx:#?}"); - }; - - if let Err(why) = self.notify().await { - error!("failed to update connect state, after updating the context: {why}") - } - - return Ok(()); - } - - if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") { - // autoplay is not supported for podcasts - Err(SpircError::NotAllowedContext(resolve_uri.to_string()))? - } - - let previous_tracks = self.connect_state.prev_autoplay_track_uris(); - - debug!( - "requesting autoplay context <{resolve_uri}> with {} previous tracks", - previous_tracks.len() - ); - - let ctx_request = AutoplayContextRequest { - context_uri: Some(resolve_uri.to_string()), - recent_track_uri: previous_tracks, - ..Default::default() + let update_state = if self + .context_resolver + .try_finish(&mut self.connect_state, &mut self.transfer_state) + { + self.add_autoplay_resolving_when_required(); + true + } else { + false }; - let context = self - .session - .spclient() - .get_autoplay_context(&ctx_request) - .await?; - - self.connect_state - .update_context(context, UpdateContext::Autoplay) + self.context_resolver.remove_used_and_invalid(); + update_state } - fn add_resolve_context(&mut self, resolve: ResolveContext) { - let last_try = self - .unavailable_contexts - .get(&resolve) - .map(|i| i.duration_since(Instant::now())); - - let last_try = if matches!(last_try, Some(last_try) if last_try > RETRY_UNAVAILABLE) { - let _ = self.unavailable_contexts.remove(&resolve); - debug!( - "context was requested {}s ago, trying again to resolve the requested context", - last_try.expect("checked by condition").as_secs() - ); - None - } else { - last_try - }; - - if last_try.is_none() { - debug!("add resolve request: {resolve}"); - self.resolve_context.push(resolve); - } else { - debug!("tried loading unavailable context: {resolve}") - } - } - - // todo: time_delta still necessary? + // todo: is the time_delta still necessary? fn now_ms(&self) -> i64 { let dur = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -612,96 +527,56 @@ impl SpircTask { } async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { - if matches!(cmd, SpircCommand::Shutdown) { - trace!("Received SpircCommand::Shutdown"); - self.handle_disconnect().await?; - self.shutdown = true; - if let Some(rx) = self.commands.as_mut() { - rx.close() - } - Ok(()) - } else if self.connect_state.is_active() { - trace!("Received SpircCommand::{:?}", cmd); - match cmd { - SpircCommand::Play => { - self.handle_play(); - self.notify().await - } - SpircCommand::PlayPause => { - self.handle_play_pause(); - self.notify().await - } - SpircCommand::Pause => { - self.handle_pause(); - self.notify().await - } - SpircCommand::Prev => { - self.handle_prev()?; - self.notify().await - } - SpircCommand::Next => { - self.handle_next(None)?; - self.notify().await - } - SpircCommand::VolumeUp => { - self.handle_volume_up(); - self.notify().await - } - SpircCommand::VolumeDown => { - self.handle_volume_down(); - self.notify().await - } - SpircCommand::Disconnect { pause } => { - if pause { - self.handle_pause() - } - self.handle_disconnect().await?; - self.notify().await - } - SpircCommand::Shuffle(shuffle) => { - self.connect_state.handle_shuffle(shuffle)?; - self.notify().await - } - SpircCommand::Repeat(repeat) => { - self.connect_state.set_repeat_context(repeat); - self.notify().await - } - SpircCommand::RepeatTrack(repeat) => { - self.connect_state.set_repeat_track(repeat); - self.notify().await - } - SpircCommand::SetPosition(position) => { - self.handle_seek(position); - self.notify().await - } - SpircCommand::SetVolume(volume) => { - self.set_volume(volume); - self.notify().await - } - SpircCommand::Load(command) => { - self.handle_load(command, None).await?; - self.notify().await - } - _ => Ok(()), - } - } else { - match cmd { - SpircCommand::Activate => { - trace!("Received SpircCommand::{:?}", cmd); - self.handle_activate(); - self.notify().await - } - _ => { - warn!("SpircCommand::{:?} will be ignored while Not Active", cmd); - Ok(()) + trace!("Received SpircCommand::{:?}", cmd); + match cmd { + SpircCommand::Shutdown => { + trace!("Received SpircCommand::Shutdown"); + self.handle_disconnect().await?; + self.shutdown = true; + if let Some(rx) = self.commands.as_mut() { + rx.close() } } - } + SpircCommand::Activate if !self.connect_state.is_active() => { + trace!("Received SpircCommand::{:?}", cmd); + self.handle_activate(); + return self.notify().await; + } + SpircCommand::Activate => warn!( + "SpircCommand::{:?} will be ignored while already active", + cmd + ), + _ if !self.connect_state.is_active() => { + warn!("SpircCommand::{:?} will be ignored while Not Active", cmd) + } + SpircCommand::Disconnect { pause } => { + if pause { + self.handle_pause() + } + return self.handle_disconnect().await; + } + SpircCommand::Play => self.handle_play(), + SpircCommand::PlayPause => self.handle_play_pause(), + SpircCommand::Pause => self.handle_pause(), + SpircCommand::Prev => self.handle_prev()?, + SpircCommand::Next => self.handle_next(None)?, + SpircCommand::VolumeUp => self.handle_volume_up(), + SpircCommand::VolumeDown => self.handle_volume_down(), + SpircCommand::Shuffle(shuffle) => self.connect_state.handle_shuffle(shuffle)?, + SpircCommand::Repeat(repeat) => self.connect_state.set_repeat_context(repeat), + SpircCommand::RepeatTrack(repeat) => self.connect_state.set_repeat_track(repeat), + SpircCommand::SetPosition(position) => self.handle_seek(position), + SpircCommand::SetVolume(volume) => self.set_volume(volume), + SpircCommand::Load(command) => self.handle_load(command, None).await?, + }; + + self.notify().await } - async fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { + fn handle_player_event(&mut self, event: PlayerEvent) -> Result<(), Error> { if let PlayerEvent::TrackChanged { audio_item } = event { self.connect_state.update_duration(audio_item.duration_ms); + self.update_state = true; return Ok(()); } @@ -710,122 +585,125 @@ impl SpircTask { self.play_request_id = Some(play_request_id); return Ok(()); } + + let is_current_track = matches! { + (event.get_play_request_id(), self.play_request_id), + (Some(event_id), Some(current_id)) if event_id == current_id + }; + // 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 // mess with it. - if let Some(play_request_id) = event.get_play_request_id() { - if Some(play_request_id) == self.play_request_id { - match event { - PlayerEvent::EndOfTrack { .. } => self.handle_end_of_track().await, - PlayerEvent::Loading { .. } => { - match self.play_status { - SpircPlayStatus::LoadingPlay { position_ms } => { - self.connect_state - .update_position(position_ms, self.now_ms()); - trace!("==> kPlayStatusPlay"); - } - SpircPlayStatus::LoadingPause { position_ms } => { - self.connect_state - .update_position(position_ms, self.now_ms()); - trace!("==> kPlayStatusPause"); - } - _ => { - self.connect_state.update_position(0, self.now_ms()); - trace!("==> kPlayStatusLoading"); - } - } - self.notify().await - } - PlayerEvent::Playing { position_ms, .. } - | PlayerEvent::PositionCorrection { position_ms, .. } - | PlayerEvent::Seeked { position_ms, .. } => { - trace!("==> kPlayStatusPlay"); - let new_nominal_start_time = self.now_ms() - position_ms as i64; - match self.play_status { - SpircPlayStatus::Playing { - ref mut nominal_start_time, - .. - } => { - if (*nominal_start_time - new_nominal_start_time).abs() > 100 { - *nominal_start_time = new_nominal_start_time; - self.connect_state - .update_position(position_ms, self.now_ms()); - self.notify().await - } else { - Ok(()) - } - } - SpircPlayStatus::LoadingPlay { .. } - | SpircPlayStatus::LoadingPause { .. } => { - self.connect_state - .update_position(position_ms, self.now_ms()); - self.play_status = SpircPlayStatus::Playing { - nominal_start_time: new_nominal_start_time, - preloading_of_next_track_triggered: false, - }; - self.notify().await - } - _ => Ok(()), - } - } - PlayerEvent::Paused { - position_ms: new_position_ms, + if !is_current_track { + return Ok(()); + } + + match event { + PlayerEvent::EndOfTrack { .. } => { + let next_track = self + .connect_state + .repeat_track() + .then(|| self.connect_state.current_track(|t| t.uri.clone())); + + self.handle_next(next_track)? + } + PlayerEvent::Loading { .. } => match self.play_status { + SpircPlayStatus::LoadingPlay { position_ms } => { + self.connect_state + .update_position(position_ms, self.now_ms()); + trace!("==> LoadingPlay"); + } + SpircPlayStatus::LoadingPause { position_ms } => { + self.connect_state + .update_position(position_ms, self.now_ms()); + trace!("==> LoadingPause"); + } + _ => { + self.connect_state.update_position(0, self.now_ms()); + trace!("==> Loading"); + } + }, + PlayerEvent::Seeked { position_ms, .. } => { + trace!("==> Seeked"); + self.connect_state + .update_position(position_ms, self.now_ms()) + } + PlayerEvent::Playing { position_ms, .. } + | PlayerEvent::PositionCorrection { position_ms, .. } => { + trace!("==> Playing"); + let new_nominal_start_time = self.now_ms() - position_ms as i64; + match self.play_status { + SpircPlayStatus::Playing { + ref mut nominal_start_time, .. } => { - trace!("==> kPlayStatusPause"); - match self.play_status { - SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => { - self.connect_state - .update_position(new_position_ms, self.now_ms()); - self.play_status = SpircPlayStatus::Paused { - position_ms: new_position_ms, - preloading_of_next_track_triggered: false, - }; - self.notify().await - } - SpircPlayStatus::LoadingPlay { .. } - | SpircPlayStatus::LoadingPause { .. } => { - self.connect_state - .update_position(new_position_ms, self.now_ms()); - self.play_status = SpircPlayStatus::Paused { - position_ms: new_position_ms, - preloading_of_next_track_triggered: false, - }; - self.notify().await - } - _ => Ok(()), + if (*nominal_start_time - new_nominal_start_time).abs() > 100 { + *nominal_start_time = new_nominal_start_time; + self.connect_state + .update_position(position_ms, self.now_ms()); + } else { + return Ok(()); } } - PlayerEvent::Stopped { .. } => { - trace!("==> kPlayStatusStop"); - match self.play_status { - SpircPlayStatus::Stopped => Ok(()), - _ => { - self.play_status = SpircPlayStatus::Stopped; - self.notify().await - } - } + SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { + self.connect_state + .update_position(position_ms, self.now_ms()); + self.play_status = SpircPlayStatus::Playing { + nominal_start_time: new_nominal_start_time, + preloading_of_next_track_triggered: false, + }; } - PlayerEvent::TimeToPreloadNextTrack { .. } => { - self.handle_preload_next_track(); - Ok(()) - } - PlayerEvent::Unavailable { track_id, .. } => { - self.handle_unavailable(track_id)?; - if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? { - self.handle_next(None)?; - } - self.notify().await - } - _ => Ok(()), + _ => return Ok(()), } - } else { - Ok(()) } - } else { - Ok(()) + PlayerEvent::Paused { + position_ms: new_position_ms, + .. + } => { + trace!("==> Paused"); + match self.play_status { + SpircPlayStatus::Paused { .. } | SpircPlayStatus::Playing { .. } => { + self.connect_state + .update_position(new_position_ms, self.now_ms()); + self.play_status = SpircPlayStatus::Paused { + position_ms: new_position_ms, + preloading_of_next_track_triggered: false, + }; + } + SpircPlayStatus::LoadingPlay { .. } | SpircPlayStatus::LoadingPause { .. } => { + self.connect_state + .update_position(new_position_ms, self.now_ms()); + self.play_status = SpircPlayStatus::Paused { + position_ms: new_position_ms, + preloading_of_next_track_triggered: false, + }; + } + _ => return Ok(()), + } + } + PlayerEvent::Stopped { .. } => { + trace!("==> Stopped"); + match self.play_status { + SpircPlayStatus::Stopped => return Ok(()), + _ => self.play_status = SpircPlayStatus::Stopped, + } + } + PlayerEvent::TimeToPreloadNextTrack { .. } => { + self.handle_preload_next_track(); + return Ok(()); + } + PlayerEvent::Unavailable { track_id, .. } => { + self.handle_unavailable(track_id)?; + if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? { + self.handle_next(None)? + } + } + _ => return Ok(()), } + + self.update_state = true; + Ok(()) } async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> { @@ -924,7 +802,7 @@ impl SpircTask { self.player .emit_auto_play_changed_event(matches!(new_value, "1")); - self.preload_autoplay_when_required() + self.add_autoplay_resolving_when_required() } } else { trace!( @@ -958,7 +836,7 @@ impl SpircTask { // fixme: workaround fix, because of missing information why it behaves like it does // background: when another device sends a connect-state update, some player's position de-syncs // tried: providing session_id, playback_id, track-metadata "track_player" - self.notify().await?; + self.update_state = true; } } else if self.connect_state.is_active() { self.connect_state.became_inactive(&self.session).await?; @@ -1008,16 +886,18 @@ impl SpircTask { update_context.context.uri, self.connect_state.context_uri() ) } else { - self.add_resolve_context(ResolveContext::from_context( + self.context_resolver.add(ResolveContext::from_context( update_context.context, - false, + super::state::context::UpdateContext::Default, + ContextAction::Replace, )) } return Ok(()); } // modification and update of the connect_state Transfer(transfer) => { - self.handle_transfer(transfer.data.expect("by condition checked"))? + self.handle_transfer(transfer.data.expect("by condition checked"))?; + return self.notify().await; } Play(play) => { let shuffle = play @@ -1025,19 +905,19 @@ impl SpircTask { .player_options_override .as_ref() .map(|o| o.shuffling_context.unwrap_or_default()) - .unwrap_or_else(|| self.connect_state.shuffling_context()); + .unwrap_or_default(); let repeat = play .options .player_options_override .as_ref() .map(|o| o.repeating_context.unwrap_or_default()) - .unwrap_or_else(|| self.connect_state.repeat_context()); + .unwrap_or_default(); let repeat_track = play .options .player_options_override .as_ref() .map(|o| o.repeating_track.unwrap_or_default()) - .unwrap_or_else(|| self.connect_state.repeat_track()); + .unwrap_or_default(); let context_uri = play .context @@ -1050,7 +930,7 @@ impl SpircTask { context_uri, start_playing: true, seek_to: play.options.seek_to.unwrap_or_default(), - playing_track: play.options.skip_to.unwrap_or_default().into(), + playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()), shuffle, repeat, repeat_track, @@ -1094,7 +974,8 @@ impl SpircTask { Resume(_) => self.handle_play(), } - self.notify().await + self.update_state = true; + Ok(()) } fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> { @@ -1121,7 +1002,12 @@ impl SpircTask { let fallback = self.connect_state.current_track(|t| &t.uri).clone(); - self.add_resolve_context(ResolveContext::from_uri(ctx_uri.clone(), &fallback, false)); + self.context_resolver.add(ResolveContext::from_uri( + ctx_uri.clone(), + &fallback, + UpdateContext::Default, + ContextAction::Replace, + )); let timestamp = self.now_ms(); let state = &mut self.connect_state; @@ -1129,6 +1015,14 @@ impl SpircTask { state.set_active(true); state.handle_initial_transfer(&mut transfer); + // adjust active context, so resolve knows for which context it should set up the state + state.active_context = if autoplay { + ContextType::Autoplay + } else { + ContextType::Default + }; + + // update position if the track continued playing let transfer_timestamp = transfer.playback.timestamp.unwrap_or_default(); let position = match transfer.playback.position_as_of_timestamp { Some(position) if transfer.playback.is_paused.unwrap_or_default() => position.into(), @@ -1145,7 +1039,12 @@ impl SpircTask { if self.connect_state.current_track(|t| t.is_autoplay()) || autoplay { debug!("currently in autoplay context, async resolving autoplay for {ctx_uri}"); - self.add_resolve_context(ResolveContext::from_uri(ctx_uri, fallback, true)) + self.context_resolver.add(ResolveContext::from_uri( + ctx_uri, + fallback, + UpdateContext::Autoplay, + ContextAction::Replace, + )) } self.transfer_state = Some(transfer); @@ -1154,6 +1053,9 @@ impl SpircTask { } async fn handle_disconnect(&mut self) -> Result<(), Error> { + self.context_resolver.clear(); + + self.play_status = SpircPlayStatus::Stopped {}; self.connect_state .update_position_in_relation(self.now_ms()); self.notify().await?; @@ -1175,9 +1077,9 @@ impl SpircTask { fn handle_stop(&mut self) { self.player.stop(); self.connect_state.update_position(0, self.now_ms()); - self.connect_state.clear_next_tracks(true); + self.connect_state.clear_next_tracks(); - if let Err(why) = self.connect_state.fill_up_next_tracks() { + if let Err(why) = self.connect_state.reset_playback_to_position(None) { warn!("failed filling up next_track during stopping: {why}") } } @@ -1219,6 +1121,8 @@ impl SpircTask { self.connect_state .reset_context(ResetContext::WhenDifferent(&cmd.context_uri)); + self.connect_state.reset_options(); + if !self.connect_state.is_active() { self.handle_activate(); } @@ -1231,35 +1135,46 @@ impl SpircTask { } } else { &cmd.context_uri - } - .clone(); + }; if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { debug!("context <{current_context_uri}> didn't change, no resolving required") } else { debug!("resolving context for load command"); - self.resolve_context(&fallback, &cmd.context_uri, false, true) - .await?; + self.context_resolver.clear(); + self.context_resolver.add(ResolveContext::from_uri( + &cmd.context_uri, + fallback, + UpdateContext::Default, + ContextAction::Replace, + )); + let context = self.context_resolver.get_next_context(Vec::new).await; + self.handle_next_context(context); } // for play commands with skip by uid, the context of the command contains // tracks with uri and uid, so we merge the new context with the resolved/existing context self.connect_state.merge_context(context); - self.connect_state.clear_next_tracks(false); + + // load here, so that we clear the queue only after we definitely retrieved a new context + self.connect_state.clear_next_tracks(); self.connect_state.clear_restrictions(); debug!("play track <{:?}>", cmd.playing_track); let index = match cmd.playing_track { - PlayingTrack::Index(i) => i as usize, - PlayingTrack::Uri(uri) => { - let ctx = self.connect_state.context.as_ref(); - ConnectState::find_index_in_context(ctx, |t| t.uri == uri)? - } - PlayingTrack::Uid(uid) => { - let ctx = self.connect_state.context.as_ref(); - ConnectState::find_index_in_context(ctx, |t| t.uid == uid)? - } + None => None, + Some(playing_track) => Some(match playing_track { + PlayingTrack::Index(i) => i as usize, + PlayingTrack::Uri(uri) => { + let ctx = self.connect_state.get_context(ContextType::Default)?; + ConnectState::find_index_in_context(ctx, |t| t.uri == uri)? + } + PlayingTrack::Uid(uid) => { + let ctx = self.connect_state.get_context(ContextType::Default)?; + ConnectState::find_index_in_context(ctx, |t| t.uid == uid)? + } + }), }; debug!( @@ -1269,17 +1184,27 @@ impl SpircTask { self.connect_state.set_shuffle(cmd.shuffle); self.connect_state.set_repeat_context(cmd.repeat); + self.connect_state.set_repeat_track(cmd.repeat_track); if cmd.shuffle { - self.connect_state.set_current_track(index)?; - self.connect_state.shuffle()?; - } else { - // manually overwrite a possible current queued track - self.connect_state.set_current_track(index)?; - self.connect_state.reset_playback_to_position(Some(index))?; - } + if let Some(index) = index { + self.connect_state.set_current_track(index)?; + } else { + self.connect_state.set_current_track_random()?; + } - self.connect_state.set_repeat_track(cmd.repeat_track); + if self.context_resolver.has_next() { + self.connect_state.update_queue_revision() + } else { + self.connect_state.shuffle()?; + self.add_autoplay_resolving_when_required(); + } + } else { + self.connect_state + .set_current_track(index.unwrap_or_default())?; + self.connect_state.reset_playback_to_position(index)?; + self.add_autoplay_resolving_when_required(); + } if self.connect_state.current_track(MessageField::is_some) { self.load_track(cmd.start_playing, cmd.seek_to)?; @@ -1288,8 +1213,6 @@ impl SpircTask { self.handle_stop() } - self.preload_autoplay_when_required(); - Ok(()) } @@ -1408,47 +1331,41 @@ impl SpircTask { Ok(()) } - fn preload_autoplay_when_required(&mut self) { + fn add_autoplay_resolving_when_required(&mut self) { let require_load_new = !self .connect_state - .has_next_tracks(Some(CONTEXT_FETCH_THRESHOLD)); + .has_next_tracks(Some(CONTEXT_FETCH_THRESHOLD)) + && self.session.autoplay(); if !require_load_new { return; } - match self.connect_state.try_load_next_context() { - Err(why) => error!("failed loading next context: {why}"), - Ok(next) => { - match next { - LoadNext::Done => info!("loaded next context"), - LoadNext::PageUrl(page_url) => { - self.add_resolve_context(ResolveContext::from_page_url(page_url)) - } - LoadNext::Empty if self.session.autoplay() => { - let current_context = self.connect_state.context_uri(); - let fallback = self.connect_state.current_track(|t| &t.uri); - let resolve = ResolveContext::from_uri(current_context, fallback, true); + let current_context = self.connect_state.context_uri(); + let fallback = self.connect_state.current_track(|t| &t.uri); - self.add_resolve_context(resolve) - } - LoadNext::Empty => { - debug!("next context is empty and autoplay isn't enabled, no preloading required") - } - } - } - } - } + let has_tracks = self + .connect_state + .get_context(ContextType::Autoplay) + .map(|c| !c.tracks.is_empty()) + .unwrap_or_default(); - fn is_playing(&self) -> bool { - matches!( - self.play_status, - SpircPlayStatus::Playing { .. } | SpircPlayStatus::LoadingPlay { .. } - ) + let resolve = ResolveContext::from_uri( + current_context, + fallback, + UpdateContext::Autoplay, + if has_tracks { + ContextAction::Append + } else { + ContextAction::Replace + }, + ); + + self.context_resolver.add(resolve); } fn handle_next(&mut self, track_uri: Option) -> Result<(), Error> { - let continue_playing = self.is_playing(); + let continue_playing = self.connect_state.is_playing(); let current_uri = self.connect_state.current_track(|t| &t.uri); let mut has_next_track = @@ -1467,13 +1384,11 @@ impl SpircTask { }; }; - self.preload_autoplay_when_required(); - if has_next_track { + self.add_autoplay_resolving_when_required(); self.load_track(continue_playing, 0) } else { info!("Not playing next track because there are no more tracks left in queue."); - self.connect_state.reset_playback_to_position(None)?; self.handle_stop(); Ok(()) } @@ -1491,7 +1406,7 @@ impl SpircTask { self.connect_state.reset_playback_to_position(None)?; self.handle_stop() } - Some(_) => self.load_track(self.is_playing(), 0)?, + Some(_) => self.load_track(self.connect_state.is_playing(), 0)?, } } else { self.handle_seek(0); @@ -1512,16 +1427,6 @@ impl SpircTask { self.set_volume(volume); } - async fn handle_end_of_track(&mut self) -> Result<(), Error> { - let next_track = self - .connect_state - .repeat_track() - .then(|| self.connect_state.current_track(|t| t.uri.clone())); - - self.handle_next(next_track)?; - self.notify().await - } - fn handle_playlist_modification( &mut self, playlist_modification_info: PlaylistModificationInfo, @@ -1537,10 +1442,11 @@ impl SpircTask { } debug!("playlist modification for current context: {uri}"); - self.add_resolve_context(ResolveContext::from_uri( + self.context_resolver.add(ResolveContext::from_uri( uri, self.connect_state.current_track(|t| &t.uri), - false, + UpdateContext::Default, + ContextAction::Replace, )); Ok(()) @@ -1619,7 +1525,7 @@ impl SpircTask { async fn notify(&mut self) -> Result<(), Error> { self.connect_state.set_status(&self.play_status); - if self.is_playing() { + if self.connect_state.is_playing() { self.connect_state .update_position_in_relation(self.now_ms()); } diff --git a/connect/src/state.rs b/connect/src/state.rs index eff83bac..c1fb73ab 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -15,7 +15,6 @@ use crate::{ }, protocol::{ connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, - context_page::ContextPage, player::{ ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, Suppressions, @@ -105,19 +104,17 @@ pub struct ConnectState { unavailable_uri: Vec, - pub active_since: Option, + active_since: Option, queue_count: u64, // separation is necessary because we could have already loaded // the autoplay context but are still playing from the default context /// to update the active context use [switch_active_context](ConnectState::set_active_context) pub active_context: ContextType, - pub fill_up_context: ContextType, + fill_up_context: ContextType, /// the context from which we play, is used to top up prev and next tracks - pub context: Option, - /// upcoming contexts, directly provided by the context-resolver - next_contexts: Vec, + context: Option, /// a context to keep track of our shuffled context, /// should be only available when `player.option.shuffling_context` is true @@ -240,6 +237,22 @@ impl ConnectState { self.request.is_active } + /// Returns the `is_playing` value as perceived by other connect devices + /// + /// see [ConnectState::set_status] + pub fn is_playing(&self) -> bool { + let player = self.player(); + player.is_playing && !player.is_paused + } + + /// Returns the `is_paused` state value as perceived by other connect devices + /// + /// see [ConnectState::set_status] + pub fn is_pause(&self) -> bool { + let player = self.player(); + player.is_playing && player.is_paused && player.is_buffering + } + pub fn set_volume(&mut self, volume: u32) { self.device_mut() .device_info @@ -297,6 +310,12 @@ impl ConnectState { | SpircPlayStatus::Stopped ); + if player.is_paused { + player.playback_speed = 0.; + } else { + player.playback_speed = 1.; + } + // desktop and mobile require all 'states' set to true, when we are paused, // otherwise the play button (desktop) is grayed out or the preview (mobile) can't be opened player.is_buffering = player.is_paused @@ -349,9 +368,15 @@ impl ConnectState { } pub fn reset_playback_to_position(&mut self, new_index: Option) -> Result<(), Error> { + debug!( + "reset_playback with active ctx <{:?}> fill_up ctx <{:?}>", + self.active_context, self.fill_up_context + ); + let new_index = new_index.unwrap_or(0); self.update_current_index(|i| i.track = new_index as u32); self.update_context_index(self.active_context, new_index + 1)?; + self.fill_up_context = self.active_context; if !self.current_track(|t| t.is_queue()) { self.set_current_track(new_index)?; @@ -360,7 +385,7 @@ impl ConnectState { self.clear_prev_track(); if new_index > 0 { - let context = self.get_context(&self.active_context)?; + let context = self.get_context(self.active_context)?; let before_new_track = context.tracks.len() - new_index; self.player_mut().prev_tracks = context @@ -375,7 +400,7 @@ impl ConnectState { debug!("has {} prev tracks", self.prev_tracks().len()) } - self.clear_next_tracks(true); + self.clear_next_tracks(); self.fill_up_next_tracks()?; self.update_restrictions(); diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 5abd52fe..fa78180a 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -7,10 +7,15 @@ use crate::{ player::{ContextIndex, ProvidedTrack}, restrictions::Restrictions, }, - state::{metadata::Metadata, provider::Provider, ConnectState, StateError}, + state::{ + metadata::Metadata, + provider::{IsProvider, Provider}, + ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, + }, }; use protobuf::MessageField; use std::collections::HashMap; +use std::ops::Deref; use uuid::Uuid; const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files"; @@ -25,7 +30,7 @@ pub struct StateContext { pub index: ContextIndex, } -#[derive(Default, Debug, Copy, Clone)] +#[derive(Default, Debug, Copy, Clone, PartialEq)] pub enum ContextType { #[default] Default, @@ -33,57 +38,81 @@ pub enum ContextType { Autoplay, } -pub enum LoadNext { - Done, - PageUrl(String), - Empty, -} - -#[derive(Debug)] +#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)] pub enum UpdateContext { Default, Autoplay, } +impl Deref for UpdateContext { + type Target = ContextType; + + fn deref(&self) -> &Self::Target { + match self { + UpdateContext::Default => &ContextType::Default, + UpdateContext::Autoplay => &ContextType::Autoplay, + } + } +} + pub enum ResetContext<'s> { Completely, DefaultIndex, WhenDifferent(&'s str), } +/// Extracts the spotify uri from a given page_url +/// +/// Just extracts "spotify/album/5LFzwirfFwBKXJQGfwmiMY" and replaces the slash's with colon's +/// +/// Expected `page_url` should look something like the following: +/// `hm://artistplaycontext/v1/page/spotify/album/5LFzwirfFwBKXJQGfwmiMY/km_artist` +fn page_url_to_uri(page_url: &str) -> String { + let split = if let Some(rest) = page_url.strip_prefix("hm://") { + rest.split('/') + } else { + warn!("page_url didn't start with hm://. got page_url: {page_url}"); + page_url.split('/') + }; + + split + .skip_while(|s| s != &"spotify") + .take(3) + .collect::>() + .join(":") +} + impl ConnectState { pub fn find_index_in_context bool>( - context: Option<&StateContext>, + ctx: &StateContext, f: F, ) -> Result { - let ctx = context - .as_ref() - .ok_or(StateError::NoContext(ContextType::Default))?; - ctx.tracks .iter() .position(f) .ok_or(StateError::CanNotFindTrackInContext(None, ctx.tracks.len())) } - pub(super) fn get_context(&self, ty: &ContextType) -> Result<&StateContext, StateError> { + pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, StateError> { match ty { ContextType::Default => self.context.as_ref(), ContextType::Shuffle => self.shuffle_context.as_ref(), ContextType::Autoplay => self.autoplay_context.as_ref(), } - .ok_or(StateError::NoContext(*ty)) + .ok_or(StateError::NoContext(ty)) } pub fn context_uri(&self) -> &String { &self.player().context_uri } - pub fn reset_context(&mut self, mut reset_as: ResetContext) { - self.set_active_context(ContextType::Default); - self.fill_up_context = ContextType::Default; + fn different_context_uri(&self, uri: &str) -> bool { + // search identifier is always different + self.context_uri() != uri || uri.starts_with(SEARCH_IDENTIFIER) + } - if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.context_uri() != ctx) { + pub fn reset_context(&mut self, mut reset_as: ResetContext) { + if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.different_context_uri(ctx)) { reset_as = ResetContext::Completely } self.shuffle_context = None; @@ -92,7 +121,6 @@ impl ConnectState { ResetContext::Completely => { self.context = None; self.autoplay_context = None; - self.next_contexts.clear(); } ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::DefaultIndex => { @@ -106,28 +134,40 @@ impl ConnectState { } } + self.fill_up_context = ContextType::Default; + self.set_active_context(ContextType::Default); self.update_restrictions() } - pub fn get_context_uri_from_context(context: &Context) -> Option<&String> { - let context_uri = context.uri.as_ref()?; - - if !context_uri.starts_with(SEARCH_IDENTIFIER) { - return Some(context_uri); + pub fn valid_resolve_uri(uri: &str) -> Option<&str> { + if uri.is_empty() || uri.starts_with(SEARCH_IDENTIFIER) { + None + } else { + Some(uri) } + } - context - .pages - .first() - .and_then(|p| p.tracks.first().and_then(|t| t.uri.as_ref())) + pub fn get_context_uri_from_context(context: &Context) -> Option<&str> { + let uri = context.uri.as_deref().unwrap_or_default(); + Self::valid_resolve_uri(uri).or_else(|| { + context + .pages + .first() + .and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref())) + }) } pub fn set_active_context(&mut self, new_context: ContextType) { self.active_context = new_context; - let ctx = match self.get_context(&new_context) { + let player = self.player_mut(); + + player.context_metadata = Default::default(); + player.restrictions = Some(Default::default()).into(); + + let ctx = match self.get_context(new_context) { Err(why) => { - debug!("couldn't load context info because: {why}"); + warn!("couldn't load context info because: {why}"); return; } Ok(ctx) => ctx, @@ -138,9 +178,6 @@ impl ConnectState { let player = self.player_mut(); - player.context_metadata.clear(); - player.restrictions.clear(); - if let Some(restrictions) = restrictions.take() { player.restrictions = MessageField::some(restrictions.into()); } @@ -150,24 +187,25 @@ impl ConnectState { } } - pub fn update_context(&mut self, mut context: Context, ty: UpdateContext) -> Result<(), Error> { + pub fn update_context( + &mut self, + mut context: Context, + ty: UpdateContext, + ) -> Result>, Error> { if context.pages.iter().all(|p| p.tracks.is_empty()) { error!("context didn't have any tracks: {context:#?}"); - return Err(StateError::ContextHasNoTracks.into()); + Err(StateError::ContextHasNoTracks)?; } else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) { - return Err(StateError::UnsupportedLocalPlayBack.into()); - } - - if matches!(ty, UpdateContext::Default) { - self.next_contexts.clear(); + Err(StateError::UnsupportedLocalPlayBack)?; } + let mut next_contexts = Vec::new(); let mut first_page = None; for page in context.pages { if first_page.is_none() && !page.tracks.is_empty() { first_page = Some(page); } else { - self.next_contexts.push(page) + next_contexts.push(page) } } @@ -176,17 +214,8 @@ impl ConnectState { Some(p) => p, }; - let prev_context = match ty { - UpdateContext::Default => self.context.as_ref(), - UpdateContext::Autoplay => self.autoplay_context.as_ref(), - }; - debug!( - "updated context {ty:?} from <{:?}> ({} tracks) to <{:?}> ({} tracks)", - self.context_uri(), - prev_context - .map(|c| c.tracks.len().to_string()) - .unwrap_or_else(|| "-".to_string()), + "updated context {ty:?} to <{:?}> ({} tracks)", context.uri, page.tracks.len() ); @@ -195,32 +224,32 @@ impl ConnectState { UpdateContext::Default => { let mut new_context = self.state_context_from_page( page, + context.metadata, context.restrictions.take(), context.uri.as_deref(), None, ); // when we update the same context, we should try to preserve the previous position - // otherwise we might load the entire context twice - if !self.context_uri().contains(SEARCH_IDENTIFIER) + // otherwise we might load the entire context twice, unless it's the search context + if !self.context_uri().starts_with(SEARCH_IDENTIFIER) && matches!(context.uri, Some(ref uri) if uri == self.context_uri()) { - match Self::find_index_in_context(Some(&new_context), |t| { - self.current_track(|t| &t.uri) == &t.uri - }) { - Ok(new_pos) => { - debug!("found new index of current track, updating new_context index to {new_pos}"); - new_context.index.track = (new_pos + 1) as u32; + if let Some(new_index) = self.find_last_index_in_new_context(&new_context) { + new_context.index.track = match new_index { + Ok(i) => i, + Err(i) => { + self.player_mut().index = MessageField::none(); + i + } + }; + + // enforce reloading the context + if let Some(autoplay_ctx) = self.autoplay_context.as_mut() { + autoplay_ctx.index.track = 0 } - // the track isn't anymore in the context - Err(_) if matches!(self.active_context, ContextType::Default) => { - warn!("current track was removed, setting pos to last known index"); - new_context.index.track = self.player().index.track - } - Err(_) => {} + self.clear_next_tracks(); } - // enforce reloading the context - self.clear_next_tracks(true); } self.context = Some(new_context); @@ -235,6 +264,7 @@ impl ConnectState { UpdateContext::Autoplay => { self.autoplay_context = Some(self.state_context_from_page( page, + context.metadata, context.restrictions.take(), context.uri.as_deref(), Some(Provider::Autoplay), @@ -242,12 +272,81 @@ impl ConnectState { } } - Ok(()) + if next_contexts.is_empty() { + return Ok(None); + } + + // load remaining contexts + let next_contexts = next_contexts + .into_iter() + .flat_map(|page| { + if !page.tracks.is_empty() { + self.fill_context_from_page(page).ok()?; + None + } else if matches!(page.page_url, Some(ref url) if !url.is_empty()) { + Some(page_url_to_uri( + &page.page_url.expect("checked by precondition"), + )) + } else { + warn!("unhandled context page: {page:#?}"); + None + } + }) + .collect(); + + Ok(Some(next_contexts)) + } + + fn find_first_prev_track_index(&self, ctx: &StateContext) -> Option { + let prev_tracks = self.prev_tracks(); + for i in (0..prev_tracks.len()).rev() { + let prev_track = prev_tracks.get(i)?; + if let Ok(idx) = Self::find_index_in_context(ctx, |t| prev_track.uri == t.uri) { + return Some(idx); + } + } + None + } + + fn find_last_index_in_new_context( + &self, + new_context: &StateContext, + ) -> Option> { + let ctx = self.context.as_ref()?; + + let is_queued_item = self.current_track(|t| t.is_queue() || t.is_from_queue()); + + let new_index = if ctx.index.track as usize >= SPOTIFY_MAX_NEXT_TRACKS_SIZE { + Some(ctx.index.track as usize - SPOTIFY_MAX_NEXT_TRACKS_SIZE) + } else if is_queued_item { + self.find_first_prev_track_index(new_context) + } else { + Self::find_index_in_context(new_context, |current| { + self.current_track(|t| t.uri == current.uri) + }) + .ok() + } + .map(|i| i as u32 + 1); + + Some(new_index.ok_or_else(|| { + info!( + "couldn't distinguish index from current or previous tracks in the updated context" + ); + let fallback_index = self + .player() + .index + .as_ref() + .map(|i| i.track) + .unwrap_or_default(); + info!("falling back to index {fallback_index}"); + fallback_index + })) } fn state_context_from_page( &mut self, page: ContextPage, + metadata: HashMap, restrictions: Option, new_context_uri: Option<&str>, provider: Option, @@ -258,8 +357,12 @@ impl ConnectState { .tracks .iter() .flat_map(|track| { - match self.context_to_provided_track(track, Some(new_context_uri), provider.clone()) - { + match self.context_to_provided_track( + track, + Some(new_context_uri), + Some(&page.metadata), + provider.clone(), + ) { Ok(t) => Some(t), Err(why) => { error!("couldn't convert {track:#?} into ProvidedTrack: {why}"); @@ -272,7 +375,7 @@ impl ConnectState { StateContext { tracks, restrictions, - metadata: page.metadata, + metadata, index: ContextIndex::new(), } } @@ -293,12 +396,11 @@ impl ConnectState { let new_track_uri = new_track.uri.unwrap_or_default(); if let Ok(position) = - Self::find_index_in_context(Some(current_context), |t| t.uri == new_track_uri) + Self::find_index_in_context(current_context, |t| t.uri == new_track_uri) { let context_track = current_context.tracks.get_mut(position)?; for (key, value) in new_track.metadata { - warn!("merging metadata {key} {value}"); context_track.metadata.insert(key, value); } @@ -334,10 +436,10 @@ impl ConnectState { &self, ctx_track: &ContextTrack, context_uri: Option<&str>, + page_metadata: Option<&HashMap>, provider: Option, ) -> Result { let id = match (ctx_track.uri.as_ref(), ctx_track.gid.as_ref()) { - (None, None) => Err(StateError::InvalidTrackUri(None))?, (Some(uri), _) if uri.contains(['?', '%']) => { Err(StateError::InvalidTrackUri(Some(uri.clone())))? } @@ -363,7 +465,7 @@ impl ConnectState { _ => Uuid::new_v4().as_simple().to_string(), }; - let mut metadata = HashMap::new(); + let mut metadata = page_metadata.cloned().unwrap_or_default(); for (k, v) in &ctx_track.metadata { metadata.insert(k.to_string(), v.to_string()); } @@ -389,7 +491,7 @@ impl ConnectState { } pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<(), Error> { - let context = self.state_context_from_page(page, None, None, None); + let context = self.state_context_from_page(page, HashMap::new(), None, None, None); let ctx = self .context .as_mut() @@ -401,26 +503,4 @@ impl ConnectState { Ok(()) } - - pub fn try_load_next_context(&mut self) -> Result { - let next = match self.next_contexts.first() { - None => return Ok(LoadNext::Empty), - Some(_) => self.next_contexts.remove(0), - }; - - if next.tracks.is_empty() { - let next_page_url = match next.page_url { - Some(page_url) if !page_url.is_empty() => page_url, - _ => Err(StateError::NoContext(ContextType::Default))?, - }; - - self.update_current_index(|i| i.page += 1); - return Ok(LoadNext::PageUrl(next_page_url)); - } - - self.fill_context_from_page(next)?; - self.fill_up_next_tracks()?; - - Ok(LoadNext::Done) - } } diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index a69e1ebe..1c1a4b32 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -1,5 +1,10 @@ -use crate::state::{context::ResetContext, ConnectState}; -use librespot_core::{dealer::protocol::SetQueueCommand, Error}; +use crate::{ + core::{dealer::protocol::SetQueueCommand, Error}, + state::{ + context::{ContextType, ResetContext}, + ConnectState, + }, +}; use protobuf::MessageField; impl ConnectState { @@ -16,7 +21,7 @@ impl ConnectState { return Ok(()); } - let ctx = self.context.as_ref(); + let ctx = self.get_context(ContextType::Default)?; let current_index = ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; @@ -52,7 +57,7 @@ impl ConnectState { self.set_shuffle(false); self.reset_context(ResetContext::DefaultIndex); - let ctx = self.context.as_ref(); + let ctx = self.get_context(ContextType::Default)?; let current_track = ConnectState::find_index_in_context(ctx, |t| { self.current_track(|t| &t.uri) == &t.uri })?; diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index b9c2c576..97484f67 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -33,6 +33,12 @@ impl ConnectState { } } + pub fn reset_options(&mut self) { + self.set_shuffle(false); + self.set_repeat_track(false); + self.set_repeat_context(false); + } + pub fn shuffle(&mut self) -> Result<(), Error> { if let Some(reason) = self .player() @@ -47,16 +53,12 @@ impl ConnectState { } self.clear_prev_track(); - self.clear_next_tracks(true); + self.clear_next_tracks(); let current_uri = self.current_track(|t| &t.uri); - let ctx = self - .context - .as_ref() - .ok_or(StateError::NoContext(ContextType::Default))?; - - let current_track = Self::find_index_in_context(Some(ctx), |t| &t.uri == current_uri)?; + let ctx = self.get_context(ContextType::Default)?; + let current_track = Self::find_index_in_context(ctx, |t| &t.uri == current_uri)?; let mut shuffle_context = ctx.clone(); // we don't need to include the current track, because it is already being played diff --git a/connect/src/state/restrictions.rs b/connect/src/state/restrictions.rs index a0f26933..03495c68 100644 --- a/connect/src/state/restrictions.rs +++ b/connect/src/state/restrictions.rs @@ -17,14 +17,18 @@ impl ConnectState { const ENDLESS_CONTEXT: &str = "endless_context"; let prev_tracks_is_empty = self.prev_tracks().is_empty(); + + let is_paused = self.is_pause(); + let is_playing = self.is_playing(); + let player = self.player_mut(); if let Some(restrictions) = player.restrictions.as_mut() { - if player.is_playing { + if is_playing { restrictions.disallow_pausing_reasons.clear(); restrictions.disallow_resuming_reasons = vec!["not_paused".to_string()] } - if player.is_paused { + if is_paused { restrictions.disallow_resuming_reasons.clear(); restrictions.disallow_pausing_reasons = vec!["not_playing".to_string()] } diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 2dc1b9af..14e3abcc 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -1,12 +1,15 @@ -use crate::state::{ - context::ContextType, - metadata::Metadata, - provider::{IsProvider, Provider}, - ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, +use crate::{ + core::{Error, SpotifyId}, + protocol::player::ProvidedTrack, + state::{ + context::ContextType, + metadata::Metadata, + provider::{IsProvider, Provider}, + ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, + }, }; -use librespot_core::{Error, SpotifyId}; -use librespot_protocol::player::ProvidedTrack; use protobuf::MessageField; +use rand::Rng; // identifier used as part of the uid pub const IDENTIFIER_DELIMITER: &str = "delimiter"; @@ -64,8 +67,14 @@ impl<'ct> ConnectState { &self.player().next_tracks } + pub fn set_current_track_random(&mut self) -> Result<(), Error> { + let max_tracks = self.get_context(self.active_context)?.tracks.len(); + let rng_track = rand::thread_rng().gen_range(0..max_tracks); + self.set_current_track(rng_track) + } + pub fn set_current_track(&mut self, index: usize) -> Result<(), Error> { - let context = self.get_context(&self.active_context)?; + let context = self.get_context(self.active_context)?; let new_track = context .tracks @@ -77,8 +86,8 @@ impl<'ct> ConnectState { debug!( "set track to: {} at {} of {} tracks", - index, new_track.uri, + index, context.tracks.len() ); @@ -132,7 +141,7 @@ impl<'ct> ConnectState { self.set_active_context(ContextType::Autoplay); None } else { - let ctx = self.context.as_ref(); + let ctx = self.get_context(ContextType::Default)?; let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); match new_index { Ok(new_index) => Some(new_index as u32), @@ -251,12 +260,7 @@ impl<'ct> ConnectState { self.prev_tracks_mut().clear() } - pub fn clear_next_tracks(&mut self, keep_queued: bool) { - if !keep_queued { - self.next_tracks_mut().clear(); - return; - } - + pub fn clear_next_tracks(&mut self) { // respect queued track and don't throw them out of our next played tracks let first_non_queued_track = self .next_tracks() @@ -271,13 +275,13 @@ impl<'ct> ConnectState { } } - pub fn fill_up_next_tracks(&mut self) -> Result<(), StateError> { - let ctx = self.get_context(&self.fill_up_context)?; + pub fn fill_up_next_tracks(&mut self) -> Result<(), Error> { + let ctx = self.get_context(self.fill_up_context)?; let mut new_index = ctx.index.track as usize; let mut iteration = ctx.index.page; while self.next_tracks().len() < SPOTIFY_MAX_NEXT_TRACKS_SIZE { - let ctx = self.get_context(&self.fill_up_context)?; + let ctx = self.get_context(self.fill_up_context)?; let track = match ctx.tracks.get(new_index) { None if self.repeat_context() => { let delimiter = Self::new_delimiter(iteration.into()); @@ -292,14 +296,14 @@ impl<'ct> ConnectState { // transition to autoplay as fill up context self.fill_up_context = ContextType::Autoplay; - new_index = self.get_context(&ContextType::Autoplay)?.index.track as usize; + new_index = self.get_context(ContextType::Autoplay)?.index.track as usize; // add delimiter to only display the current context Self::new_delimiter(iteration.into()) } None if self.autoplay_context.is_some() => { match self - .get_context(&ContextType::Autoplay)? + .get_context(ContextType::Autoplay)? .tracks .get(new_index) { @@ -324,6 +328,11 @@ impl<'ct> ConnectState { self.next_tracks_mut().push(track); } + debug!( + "finished filling up next_tracks ({})", + self.next_tracks().len() + ); + self.update_context_index(self.fill_up_context, new_index)?; // the web-player needs a revision update, otherwise the queue isn't updated in the ui @@ -350,17 +359,14 @@ impl<'ct> ConnectState { } } - pub fn prev_autoplay_track_uris(&self) -> Vec { + pub fn recent_track_uris(&self) -> Vec { let mut prev = self .prev_tracks() .iter() - .flat_map(|t| t.is_autoplay().then_some(t.uri.clone())) + .map(|t| t.uri.clone()) .collect::>(); - if self.current_track(|t| t.is_autoplay()) { - prev.push(self.current_track(|t| t.uri.clone())); - } - + prev.push(self.current_track(|t| t.uri.clone())); prev } diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index 53d420a1..7404bf55 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -25,10 +25,12 @@ impl ConnectState { self.context_to_provided_track( track, transfer.current_session.context.uri.as_deref(), + None, transfer .queue .is_playing_queue - .and_then(|b| b.then_some(Provider::Queue)), + .unwrap_or_default() + .then_some(Provider::Queue), ) } @@ -72,7 +74,8 @@ impl ConnectState { } self.clear_prev_track(); - self.clear_next_tracks(false); + self.clear_next_tracks(); + self.update_queue_revision() } /// completes the transfer, loading the queue and updating metadata @@ -91,7 +94,7 @@ impl ConnectState { self.set_active_context(context_ty); self.fill_up_context = context_ty; - let ctx = self.get_context(&self.active_context).ok(); + let ctx = self.get_context(self.active_context)?; let current_index = match transfer.current_session.current_uid.as_ref() { Some(uid) if track.is_queue() => Self::find_index_in_context(ctx, |c| &c.uid == uid) @@ -103,7 +106,7 @@ impl ConnectState { "active track is <{}> with index {current_index:?} in {:?} context, has {} tracks", track.uri, self.active_context, - ctx.map(|c| c.tracks.len()).unwrap_or_default() + ctx.tracks.len() ); if self.player().track.is_none() { @@ -130,6 +133,7 @@ impl ConnectState { if let Ok(queued_track) = self.context_to_provided_track( track, Some(self.context_uri()), + None, Some(Provider::Queue), ) { self.add_to_queue(queued_track, false); diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs index e6b7f2dc..c774a119 100644 --- a/core/src/dealer/protocol.rs +++ b/core/src/dealer/protocol.rs @@ -174,7 +174,9 @@ fn handle_transfer_encoding( ) -> Result, Error> { let encoding = headers.get("Transfer-Encoding").map(String::as_str); if let Some(encoding) = encoding { - trace!("message was send with {encoding} encoding "); + trace!("message was sent with {encoding} encoding "); + } else { + trace!("message was sent with no encoding "); } if !matches!(encoding, Some("gzip")) { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 42213a57..a6680463 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -813,7 +813,7 @@ impl SpClient { /// **will** contain the query /// - artists /// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album - /// - remaining pages are albums of the artists and are only provided as page_url + /// - remaining pages are artist albums sorted by popularity (only provided as page_url) /// - search /// - is massively influenced by the provided query /// - the query result shown by the search expects no query at all diff --git a/examples/play_connect.rs b/examples/play_connect.rs index 9a033da2..60cf631f 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -84,7 +84,7 @@ async fn main() { repeat: false, repeat_track: false, // the index specifies which track in the context starts playing, in this case the first in the album - playing_track: PlayingTrack::Index(0), + playing_track: PlayingTrack::Index(0).into(), }) .unwrap(); }); diff --git a/protocol/src/impl_trait.rs b/protocol/src/impl_trait.rs new file mode 100644 index 00000000..c936a5f0 --- /dev/null +++ b/protocol/src/impl_trait.rs @@ -0,0 +1,2 @@ +mod context; +mod player; diff --git a/protocol/src/impl_trait/context.rs b/protocol/src/impl_trait/context.rs new file mode 100644 index 00000000..875ef9ad --- /dev/null +++ b/protocol/src/impl_trait/context.rs @@ -0,0 +1,13 @@ +use crate::context::Context; +use protobuf::Message; +use std::hash::{Hash, Hasher}; + +impl Hash for Context { + fn hash(&self, state: &mut H) { + if let Ok(ctx) = self.write_to_bytes() { + ctx.hash(state) + } + } +} + +impl Eq for Context {} diff --git a/protocol/src/conversion.rs b/protocol/src/impl_trait/player.rs similarity index 100% rename from protocol/src/conversion.rs rename to protocol/src/impl_trait/player.rs diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs index 05aef10f..c905ceb8 100644 --- a/protocol/src/lib.rs +++ b/protocol/src/lib.rs @@ -1,6 +1,6 @@ // This file is parsed by build.rs // Each included module will be compiled from the matching .proto definition. -mod conversion; +mod impl_trait; include!(concat!(env!("OUT_DIR"), "/mod.rs")); From b54f3e30db99cb665ac054e9860a2b29c21bca43 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Mon, 20 Jan 2025 16:59:04 +0100 Subject: [PATCH 491/561] connect: expose disable_volume (#1451) --- connect/src/state.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/connect/src/state.rs b/connect/src/state.rs index c1fb73ab..c06618ae 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -15,6 +15,7 @@ use crate::{ }, protocol::{ connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, + media::AudioQuality, player::{ ContextIndex, ContextPlayerOptions, PlayOrigin, PlayerState, ProvidedTrack, Suppressions, @@ -82,6 +83,7 @@ pub struct ConnectStateConfig { pub device_type: DeviceType, pub volume_steps: i32, pub is_group: bool, + pub disable_volume: bool, } impl Default for ConnectStateConfig { @@ -93,6 +95,7 @@ impl Default for ConnectStateConfig { device_type: DeviceType::Speaker, volume_steps: 64, is_group: false, + disable_volume: false, } } } @@ -137,14 +140,14 @@ impl ConnectState { is_group: cfg.is_group, capabilities: MessageField::some(Capabilities { volume_steps: cfg.volume_steps, - hidden: false, // could be exposed later to only observe the playback + disable_volume: cfg.disable_volume, + gaia_eq_connect_id: true, can_be_player: true, - needs_full_player_state: true, - is_observable: true, is_controllable: true, + hidden: false, supports_gzip_pushes: true, // todo: enable after logout handling is implemented, see spirc logout_request @@ -157,14 +160,19 @@ impl ConnectState { is_voice_enabled: false, restrict_to_local: false, - disable_volume: false, connect_disabled: false, supports_rename: false, supports_external_episodes: false, supports_set_backend_metadata: false, supports_hifi: MessageField::none(), + // that "AI" dj thingy only available to specific regions/users + supports_dj: false, + supports_rooms: false, + // AudioQuality::HIFI is available, further investigation necessary + supported_audio_quality: EnumOrUnknown::new(AudioQuality::VERY_HIGH), command_acks: true, + ..Default::default() }), ..Default::default() From 0e9a3def83b4adb216b353eef3348c29de4edf69 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Wed, 22 Jan 2025 22:49:16 +0100 Subject: [PATCH 492/561] update Cargo.lock (#1440) Fixes #1426 --- Cargo.lock | 990 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 616 insertions(+), 374 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d253687..51bf7fd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "libc", ] @@ -76,9 +76,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.17" +version = "0.6.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a1e53f0f5d86382dafe1cf314783b2044280f406e7e1506368220ad11b1338" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" dependencies = [ "anstyle", "anstyle-parse", @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8365de52b16c035ff4fcafe0092ba9390540e3e352870ac09933bebcaa2c8c56" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" [[package]] name = "anstyle-parse" @@ -125,9 +125,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.91" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" +checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" [[package]] name = "arrayvec" @@ -137,9 +137,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "async-broadcast" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cd0e2e25ea8e5f7e9df04578dc6cf5c83577fd09b1a46aaf5c85e1c33f2a7e" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", @@ -155,18 +155,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "async-trait" -version = "0.1.83" +version = "0.1.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -189,28 +189,26 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" +checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" dependencies = [ "aws-lc-sys", - "mirai-annotations", "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" +checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" dependencies = [ "bindgen 0.69.5", "cc", "cmake", "dunce", "fs_extra", - "libc", "paste", ] @@ -259,7 +257,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -272,7 +270,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.90", + "syn 2.0.96", "which", ] @@ -282,7 +280,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -291,7 +289,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -302,9 +300,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" [[package]] name = "block-buffer" @@ -323,9 +321,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.19.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8334215b81e418a0a7bdb8ef0849474f40bb10c8b71f1c4ed315cff49f32494d" +checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" [[package]] name = "byteorder" @@ -335,15 +333,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" +checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" [[package]] name = "cc" -version = "1.1.31" +version = "1.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" dependencies = [ "jobserver", "libc", @@ -367,9 +365,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.17.0" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0890061c4d3223e7267f3bad2ec40b997d64faac1c2815a4a9d95018e2b9e9c" +checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" dependencies = [ "smallvec", "target-lexicon", @@ -389,9 +387,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.38" +version = "0.4.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", @@ -420,14 +418,14 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.5", + "libloading 0.8.6", ] [[package]] name = "cmake" -version = "0.1.51" +version = "0.1.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" dependencies = [ "cc", ] @@ -473,6 +471,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -525,9 +533,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.14" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" dependencies = [ "libc", ] @@ -543,9 +551,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.20" +version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "crypto-common" @@ -587,7 +595,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -598,7 +606,7 @@ checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -651,7 +659,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -661,7 +669,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -676,6 +684,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "dns-sd" version = "0.1.3" @@ -731,23 +750,23 @@ checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "env_filter" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" dependencies = [ "log", ] [[package]] name = "env_logger" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" dependencies = [ "anstream", "anstyle", @@ -764,19 +783,19 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "event-listener" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" dependencies = [ "concurrent-queue", "parking", @@ -785,9 +804,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" dependencies = [ "event-listener", "pin-project-lite", @@ -795,9 +814,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.1" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fixedbitset" @@ -807,9 +826,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" dependencies = [ "crc32fast", "miniz_oxide", @@ -892,7 +911,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -971,24 +990,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.20.5" +version = "0.20.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217f464cad5946ae4369c355155e2d16b488c08920601083cb4891e352ae777b" +checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "glib" -version = "0.20.5" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "358431b0e0eb15b9d02db52e1f19c805b953c5c168099deb3de88beab761768c" +checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "futures-channel", "futures-core", "futures-executor", @@ -1005,22 +1024,22 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.5" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7d21ca27acfc3e91da70456edde144b4ac7c36f78ee77b10189b3eb4901c156" +checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "glib-sys" -version = "0.20.5" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5911863ab7ecd4a6f8d5976f12eeba076b23669c49b066d877e742544aa389" +checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" dependencies = [ "libc", "system-deps", @@ -1028,15 +1047,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "gobject-sys" -version = "0.20.4" +version = "0.20.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c674d2ff8478cf0ec29d2be730ed779fef54415a2fb4b565c52def62696462" +checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" dependencies = [ "glib-sys", "libc", @@ -1064,9 +1083,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ecf3bcfc2ceb82ce02437f53ff2fcaee5e7d45ae697ab64a018408749779b9" +checksum = "700cb1b2e86dda424f85eb728102a111602317e40b4dd71cf1c0dc04e0cc5d95" dependencies = [ "cfg-if", "futures-channel", @@ -1084,14 +1103,14 @@ dependencies = [ "paste", "pin-project-lite", "smallvec", - "thiserror 1.0.65", + "thiserror 2.0.11", ] [[package]] name = "gstreamer-app" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a4ec9f0d2037349c82f589c1cbfe788a62f4941851924bb7c3929a6a790007" +checksum = "41b7bda01190cf5000869083afbdd5acbe1ab86fbc523825898ba9ce777846c0" dependencies = [ "futures-core", "futures-sink", @@ -1104,9 +1123,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d5cac633c1ab7030c777c8c58c682a0c763bbc4127bccc370dabe39c01a12d" +checksum = "6b0a5c2b149c629a46f21671118f491f61daab4469979105172fb2f8536b4e56" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1117,9 +1136,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36d39b07213f83055fc705a384fa32ad581776b8e5b04c86f3a419ec5dfc0f81" +checksum = "52a6009b5c9c942cab1089956a501bd63778e65a3e69310949d173e90e2cdda2" dependencies = [ "cfg-if", "glib", @@ -1133,9 +1152,9 @@ dependencies = [ [[package]] name = "gstreamer-audio-sys" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d84744e7ac8f8bc0cf76b7be40f2d5be12e6cf197e4c6ca9d3438109c21e2f51" +checksum = "ef70a3d80e51ef9a45749a844cb8579d4cabe5ff59cb43a65d6f3a377943262f" dependencies = [ "glib-sys", "gobject-sys", @@ -1147,9 +1166,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46ce7330d2995138a77192ea20961422ddee1578e1a47480acb820c43ceb0e2d" +checksum = "d152db7983f98d5950cf64e53805286548063475fb61a5e5450fba4cec05899b" dependencies = [ "atomic_refcell", "cfg-if", @@ -1161,9 +1180,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7796e694c21c215447811c9cff694dce1fc6e02b0bbafb75cd8583b6aefe9e5f" +checksum = "d47cc2d15f2a3d5eb129e5dacbbeec9600432b706805c15dff57b6aa11b2791c" dependencies = [ "glib-sys", "gobject-sys", @@ -1174,9 +1193,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.23.2" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb3859929db32f26a35818d0d9ed82f0887c9221ca402ddefaea2bb99833d535" +checksum = "16cf1ae0a869aa7066ce3c685b76053b4b4f48f364a5b18c4b1f36ef57469719" dependencies = [ "glib-sys", "gobject-sys", @@ -1205,16 +1224,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.1.0", + "http 1.2.0", "indexmap", "slab", "tokio", @@ -1237,7 +1256,7 @@ dependencies = [ "base64 0.21.7", "bytes", "headers-core", - "http 1.1.0", + "http 1.2.0", "httpdate", "mime", "sha1", @@ -1249,7 +1268,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1258,12 +1277,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "hex" version = "0.4.3" @@ -1281,11 +1294,11 @@ dependencies = [ [[package]] name = "home" -version = "0.5.9" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1312,9 +1325,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -1339,7 +1352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.1.0", + "http 1.2.0", ] [[package]] @@ -1350,7 +1363,7 @@ checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", "pin-project-lite", ] @@ -1375,9 +1388,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.31" +version = "0.14.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" dependencies = [ "bytes", "futures-channel", @@ -1399,15 +1412,15 @@ dependencies = [ [[package]] name = "hyper" -version = "1.5.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" +checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.6", - "http 1.1.0", + "h2 0.4.7", + "http 1.2.0", "http-body 1.0.1", "httparse", "httpdate", @@ -1427,8 +1440,8 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.1.0", - "hyper 1.5.0", + "http 1.2.0", + "hyper 1.5.2", "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", @@ -1448,7 +1461,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.31", + "hyper 0.14.32", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -1461,8 +1474,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.5.0", + "http 1.2.0", + "hyper 1.5.2", "hyper-util", "log", "rustls 0.22.4", @@ -1476,20 +1489,20 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.3" +version = "0.27.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" dependencies = [ "futures-util", - "http 1.1.0", - "hyper 1.5.0", + "http 1.2.0", + "hyper 1.5.2", "hyper-util", "log", - "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls 0.23.21", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tower-service", "webpki-roots 0.26.7", ] @@ -1503,9 +1516,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "http 1.1.0", + "http 1.2.0", "http-body 1.0.1", - "hyper 1.5.0", + "hyper 1.5.2", "pin-project-lite", "socket2", "tokio", @@ -1536,6 +1549,124 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -1544,12 +1675,23 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -1564,9 +1706,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", @@ -1613,9 +1755,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.11" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jack" @@ -1636,7 +1778,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78a4ae24f4ee29676aef8330fed1104e72f314cab16643dbeb61bfd99b4a8273" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "jack-sys", "lazy_static", "libc", @@ -1668,7 +1810,7 @@ dependencies = [ "combine", "jni-sys", "log", - "thiserror 1.0.65", + "thiserror 1.0.69", "walkdir", "windows-sys 0.45.0", ] @@ -1690,10 +1832,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -1714,9 +1857,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.168" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libloading" @@ -1730,9 +1873,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" dependencies = [ "cfg-if", "windows-targets 0.52.6", @@ -1758,15 +1901,15 @@ dependencies = [ "multimap", "rand", "socket2", - "thiserror 1.0.65", + "thiserror 1.0.69", "tokio", ] [[package]] name = "libpulse-binding" -version = "2.28.1" +version = "2.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3557a2dfc380c8f061189a01c6ae7348354e0c9886038dc6c171219c08eaff" +checksum = "b6b1040a6c4c4d1e9e852000f6202df1a02a4f074320de336ab21e4fd317b538" dependencies = [ "bitflags 1.3.2", "libc", @@ -1829,7 +1972,7 @@ dependencies = [ "log", "sha1", "sysinfo", - "thiserror 2.0.7", + "thiserror 2.0.11", "tokio", "url", ] @@ -1843,13 +1986,13 @@ dependencies = [ "ctr", "futures-util", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.2", "hyper-util", "librespot-core", "log", "parking_lot", "tempfile", - "thiserror 2.0.7", + "thiserror 2.0.11", "tokio", ] @@ -1865,7 +2008,7 @@ dependencies = [ "protobuf", "rand", "serde_json", - "thiserror 2.0.7", + "thiserror 2.0.11", "tokio", "tokio-stream", "uuid", @@ -1886,12 +2029,12 @@ dependencies = [ "futures-util", "governor", "hmac", - "http 1.1.0", + "http 1.2.0", "http-body-util", "httparse", - "hyper 1.5.0", + "hyper 1.5.2", "hyper-proxy2", - "hyper-rustls 0.27.3", + "hyper-rustls 0.27.5", "hyper-util", "librespot-oauth", "librespot-protocol", @@ -1916,7 +2059,7 @@ dependencies = [ "sha1", "shannon", "sysinfo", - "thiserror 2.0.7", + "thiserror 2.0.11", "time", "tokio", "tokio-stream", @@ -1943,7 +2086,7 @@ dependencies = [ "hex", "hmac", "http-body-util", - "hyper 1.5.0", + "hyper 1.5.2", "hyper-util", "libmdns", "librespot-core", @@ -1953,7 +2096,7 @@ dependencies = [ "serde_json", "serde_repr", "sha1", - "thiserror 2.0.7", + "thiserror 2.0.11", "tokio", "zbus", ] @@ -1970,7 +2113,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 2.0.7", + "thiserror 2.0.11", "uuid", ] @@ -1981,7 +2124,7 @@ dependencies = [ "env_logger", "log", "oauth2", - "thiserror 2.0.7", + "thiserror 2.0.11", "url", ] @@ -2012,9 +2155,9 @@ dependencies = [ "sdl2", "shell-words", "symphonia", - "thiserror 2.0.7", + "thiserror 2.0.11", "tokio", - "zerocopy 0.8.13", + "zerocopy 0.8.14", ] [[package]] @@ -2027,9 +2170,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" [[package]] name = "lock_api" @@ -2085,31 +2234,24 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.0" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" dependencies = [ - "hermit-abi", "libc", "wasi", "windows-sys 0.52.0", ] -[[package]] -name = "mirai-annotations" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" - [[package]] name = "muldiv" version = "1.0.1" @@ -2131,12 +2273,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "jni-sys", "log", "ndk-sys", "num_enum", - "thiserror 1.0.65", + "thiserror 1.0.69", ] [[package]] @@ -2160,7 +2302,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "cfg-if", "cfg_aliases", "libc", @@ -2251,7 +2393,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2312,7 +2454,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2340,15 +2482,15 @@ dependencies = [ "serde_json", "serde_path_to_error", "sha2", - "thiserror 1.0.65", + "thiserror 1.0.69", "url", ] [[package]] name = "object" -version = "0.36.5" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" dependencies = [ "memchr", ] @@ -2491,9 +2633,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" @@ -2530,9 +2672,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "portable-atomic" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" [[package]] name = "portaudio-rs" @@ -2572,12 +2714,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.25" +version = "0.2.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d1ec885c64d0457d564db4ec299b2dae3f9c02808b8ad9c3a089c591b18033" +checksum = "924b9a625d6df5b74b0b3cfbb5669b3f62ddf3d46a677ce12b1945471b4ae5c3" dependencies = [ "proc-macro2", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -2602,9 +2744,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -2617,7 +2759,7 @@ checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" dependencies = [ "once_cell", "protobuf-support", - "thiserror 1.0.65", + "thiserror 1.0.69", ] [[package]] @@ -2632,7 +2774,7 @@ dependencies = [ "protobuf-parse", "regex", "tempfile", - "thiserror 1.0.65", + "thiserror 1.0.69", ] [[package]] @@ -2643,7 +2785,7 @@ checksum = "9b445cf83c9303695e6c423d269759e139b6182d2f1171e18afda7078a764336" dependencies = [ "protobuf", "protobuf-support", - "thiserror 1.0.65", + "thiserror 1.0.69", ] [[package]] @@ -2658,7 +2800,7 @@ dependencies = [ "protobuf", "protobuf-support", "tempfile", - "thiserror 1.0.65", + "thiserror 1.0.69", "which", ] @@ -2668,14 +2810,14 @@ version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" dependencies = [ - "thiserror 1.0.65", + "thiserror 1.0.69", ] [[package]] name = "quick-xml" -version = "0.37.1" +version = "0.37.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22f29bdff3987b4d8632ef95fd6424ec7e4e0a57e2f4fc63e489e75357f6a03" +checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" dependencies = [ "memchr", "serde", @@ -2683,9 +2825,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.37" +version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] @@ -2732,11 +2874,11 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", ] [[package]] @@ -2753,9 +2895,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", @@ -2782,7 +2924,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.31", + "hyper 0.14.32", "hyper-rustls 0.24.2", "ipnet", "js-sys", @@ -2835,9 +2977,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" dependencies = [ "const-oid", "digest", @@ -2867,15 +3009,15 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.38" +version = "0.38.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa260229e6538e52293eeb577aabd09945a09d6d9cc0fc550ed7529056c2e32a" +checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.7.0", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2906,9 +3048,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.16" +version = "0.23.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eee87ff5d9b36712a58574e12e9f0ea80f915a5b0ac518d322b24a465617925e" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" dependencies = [ "aws-lc-rs", "log", @@ -2929,20 +3071,19 @@ dependencies = [ "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.2.0", ] [[package]] @@ -2965,9 +3106,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" [[package]] name = "rustls-webpki" @@ -2993,9 +3134,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" [[package]] name = "ryu" @@ -3014,9 +3155,9 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.26" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ "windows-sys 0.59.0", ] @@ -3066,8 +3207,21 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.6.0", - "core-foundation", + "bitflags 2.7.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.7.0", + "core-foundation 0.10.0", "core-foundation-sys", "libc", "security-framework-sys", @@ -3075,9 +3229,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.12.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -3085,29 +3239,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.214" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "serde_json" -version = "1.0.132" +version = "1.0.135" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" dependencies = [ "itoa", "memchr", @@ -3133,7 +3287,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3236,9 +3390,9 @@ checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" dependencies = [ "libc", "windows-sys 0.52.0", @@ -3269,6 +3423,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -3384,9 +3544,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.90" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3400,10 +3560,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" [[package]] -name = "sysinfo" -version = "0.33.0" +name = "synstructure" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "948512566b1895f93b1592c7574baeb2de842f224f2aab158799ecadb8ebbb46" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" dependencies = [ "core-foundation-sys", "libc", @@ -3419,7 +3590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3454,12 +3625,13 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" [[package]] name = "tempfile" -version = "3.13.0" +version = "3.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" dependencies = [ "cfg-if", "fastrand", + "getrandom", "once_cell", "rustix", "windows-sys 0.59.0", @@ -3467,42 +3639,42 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl 1.0.65", + "thiserror-impl 1.0.69", ] [[package]] name = "thiserror" -version = "2.0.7" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ - "thiserror-impl 2.0.7", + "thiserror-impl 2.0.11", ] [[package]] name = "thiserror-impl" -version = "1.0.65" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "thiserror-impl" -version = "2.0.7" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3517,9 +3689,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -3540,34 +3712,29 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", ] [[package]] -name = "tinyvec" -version = "1.8.0" +name = "tinystr" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" dependencies = [ - "tinyvec_macros", + "displaydoc", + "zerovec", ] -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" -version = "1.41.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", @@ -3584,13 +3751,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -3616,20 +3783,19 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls 0.23.16", - "rustls-pki-types", + "rustls 0.23.21", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", @@ -3644,20 +3810,20 @@ checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" dependencies = [ "futures-util", "log", - "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls 0.23.21", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.0", + "tokio-rustls 0.26.1", "tungstenite", "webpki-roots 0.26.7", ] [[package]] name = "tokio-util" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" dependencies = [ "bytes", "futures-core", @@ -3708,9 +3874,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3719,20 +3885,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", ] @@ -3752,14 +3918,14 @@ dependencies = [ "byteorder", "bytes", "data-encoding", - "http 1.1.0", + "http 1.2.0", "httparse", "log", "rand", - "rustls 0.23.16", + "rustls 0.23.21", "rustls-pki-types", "sha1", - "thiserror 1.0.65", + "thiserror 1.0.69", "utf-8", ] @@ -3780,26 +3946,11 @@ dependencies = [ "winapi", ] -[[package]] -name = "unicode-bidi" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" - [[package]] name = "unicode-ident" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" - -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] +checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "unicode-width" @@ -3815,9 +3966,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -3831,6 +3982,18 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "utf8parse" version = "0.2.2" @@ -3839,9 +4002,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" +checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" dependencies = [ "getrandom", "rand", @@ -3849,9 +4012,9 @@ dependencies = [ [[package]] name = "vergen" -version = "9.0.1" +version = "9.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349ed9e45296a581f455bc18039878f409992999bc1d5da12a6800eb18c8752f" +checksum = "e0d2f179f8075b805a43a2a21728a46f0cc2921b3c58695b28fa8817e103cd9a" dependencies = [ "anyhow", "derive_builder", @@ -3862,9 +4025,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.1" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3a7f91caabecefc3c249fd864b11d4abe315c166fbdb568964421bccfd2b7a" +checksum = "b2f89d70a58a4506a6079cedf575c64cf51649ccbb4e02a63dac539b264b7711" dependencies = [ "anyhow", "derive_builder", @@ -3876,9 +4039,9 @@ dependencies = [ [[package]] name = "vergen-lib" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229eaddb0050920816cf051e619affaf18caa3dd512de8de5839ccbc8e53abb0" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" dependencies = [ "anyhow", "derive_builder", @@ -3930,9 +4093,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -3941,36 +4104,36 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.45" +version = "0.4.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" dependencies = [ "cfg-if", "js-sys", + "once_cell", "wasm-bindgen", "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3978,28 +4141,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4142,7 +4305,7 @@ checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4153,7 +4316,7 @@ checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] @@ -4381,9 +4544,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" dependencies = [ "memchr", ] @@ -4398,6 +4561,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + [[package]] name = "xdg-home" version = "1.3.0" @@ -4409,10 +4584,34 @@ dependencies = [ ] [[package]] -name = "zbus" -version = "5.2.0" +name = "yoke" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb67eadba43784b6fb14857eba0d8fc518686d3ee537066eb6086dc318e2c8a1" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "192a0d989036cd60a1e91a54c9851fb9ad5bd96125d41803eed79d2e2ef74bd7" dependencies = [ "async-broadcast", "async-recursion", @@ -4440,14 +4639,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d49ebc960ceb660f2abe40a5904da975de6986f2af0d7884b39eec6528c57" +checksum = "3685b5c81fce630efc3e143a4ded235b107f1b1cdf186c3f115529e5e5ae4265" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "zbus_names", "zvariant", "zvariant_utils", @@ -4477,11 +4676,11 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67914ab451f3bfd2e69e5e9d2ef3858484e7074d63f204fd166ec391b54de21d" +checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" dependencies = [ - "zerocopy-derive 0.8.13", + "zerocopy-derive 0.8.14", ] [[package]] @@ -4492,18 +4691,39 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", ] [[package]] name = "zerocopy-derive" -version = "0.8.13" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7988d73a4303ca289df03316bc490e934accf371af6bc745393cf3c2c5c4f25d" +checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", +] + +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "synstructure", ] [[package]] @@ -4513,10 +4733,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] -name = "zvariant" -version = "5.1.0" +name = "zerovec" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1200ee6ac32f1e5a312e455a949a4794855515d34f9909f4a3e082d14e1a56f" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "zvariant" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9" dependencies = [ "endi", "enumflags2", @@ -4529,27 +4771,27 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.1.0" +version = "5.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687e3b97fae6c9104fbbd36c73d27d149abf04fb874e2efbd84838763daa8916" +checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.90", + "syn 2.0.96", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.0.2" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d1d011a38f12360e5fcccceeff5e2c42a8eb7f27f0dcba97a0862ede05c9c6" +checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", - "syn 2.0.90", + "syn 2.0.96", "winnow", ] From 5f7cfdc76cbd2271084c3377e1fe29a9b720595f Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Thu, 23 Jan 2025 19:13:30 +0100 Subject: [PATCH 493/561] Expose `autoplay` option to `SpircLoadCommand` (#1446) * connect: add autoplay option to SpircLoadCommand * update CHANGELOG.md * actually ignore options when starting autoplay --- CHANGELOG.md | 1 + connect/src/model.rs | 7 ++++++- connect/src/spirc.rs | 23 ++++++++++++++++++----- examples/play_connect.rs | 1 + 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad5c9432..305bf5d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Add `seek_to` field to `SpircLoadCommand` (breaking) - [connect] Add `repeat_track` field to `SpircLoadCommand` (breaking) +- [connect] Add `autoplay` field to `SpircLoadCommand` (breaking) - [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking) - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` diff --git a/connect/src/model.rs b/connect/src/model.rs index 8315ee29..73f86999 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -9,10 +9,15 @@ pub struct SpircLoadCommand { pub shuffle: bool, pub repeat: bool, pub repeat_track: bool, + /// Decides if the context or the autoplay of the context is played + /// + /// ## Remarks: + /// If `true` is provided, the option values (`shuffle`, `repeat` and `repeat_track`) are ignored + pub autoplay: bool, /// Decides the starting position in the given context /// /// ## Remarks: - /// If none is provided and shuffle true, a random track is played, otherwise the first + /// If `None` is provided and `shuffle` is `true`, a random track is played, otherwise the first pub playing_track: Option, } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 8546296c..d4773fc0 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -37,6 +37,7 @@ use futures_util::StreamExt; use protobuf::MessageField; use std::{ future::Future, + ops::Deref, sync::atomic::{AtomicUsize, Ordering}, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -934,6 +935,7 @@ impl SpircTask { shuffle, repeat, repeat_track, + autoplay: false, }, Some(play.context), ) @@ -1127,7 +1129,6 @@ impl SpircTask { self.handle_activate(); } - let current_context_uri = self.connect_state.context_uri(); let fallback = if let Some(ref ctx) = context { match ConnectState::get_context_uri_from_context(ctx) { Some(ctx_uri) => ctx_uri, @@ -1137,6 +1138,16 @@ impl SpircTask { &cmd.context_uri }; + let update_context = if cmd.autoplay { + UpdateContext::Autoplay + } else { + UpdateContext::Default + }; + + self.connect_state + .set_active_context(*update_context.deref()); + + let current_context_uri = self.connect_state.context_uri(); if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { debug!("context <{current_context_uri}> didn't change, no resolving required") } else { @@ -1145,7 +1156,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( &cmd.context_uri, fallback, - UpdateContext::Default, + update_context, ContextAction::Replace, )); let context = self.context_resolver.get_next_context(Vec::new).await; @@ -1182,9 +1193,11 @@ impl SpircTask { cmd.shuffle, cmd.repeat, cmd.repeat_track ); - self.connect_state.set_shuffle(cmd.shuffle); - self.connect_state.set_repeat_context(cmd.repeat); - self.connect_state.set_repeat_track(cmd.repeat_track); + self.connect_state.set_shuffle(!cmd.autoplay && cmd.shuffle); + self.connect_state + .set_repeat_context(!cmd.autoplay && cmd.repeat); + self.connect_state + .set_repeat_track(!cmd.autoplay && cmd.repeat_track); if cmd.shuffle { if let Some(index) = index { diff --git a/examples/play_connect.rs b/examples/play_connect.rs index 60cf631f..8ea7eaca 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -83,6 +83,7 @@ async fn main() { shuffle: false, repeat: false, repeat_track: false, + autoplay: false, // the index specifies which track in the context starts playing, in this case the first in the album playing_track: PlayingTrack::Index(0).into(), }) From 98e9703edbeb2665c9e8e21196d382a7c81e12cd Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Thu, 23 Jan 2025 19:14:48 +0100 Subject: [PATCH 494/561] connect: handle dnd of current track (#1449) --- connect/src/state/tracks.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 14e3abcc..05468575 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -249,6 +249,14 @@ impl<'ct> ConnectState { self.queue_count += 1; }); + // when you drag 'n drop the current track in the queue view into the "Next from: ..." + // section, it is only send as an empty item with just the provider and metadata, so we have + // to provide set the uri from the current track manually + tracks + .iter_mut() + .filter(|t| t.uri.is_empty()) + .for_each(|t| t.uri = self.current_track(|ct| ct.uri.clone())); + self.player_mut().next_tracks = tracks; } From c1ae8608aaa8eff0de0411d443b7d574d9835a10 Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Tue, 28 Jan 2025 16:04:58 +0000 Subject: [PATCH 495/561] core: include AP handshake in 5s timeout (#1458) * core: include AP handshake in 5s timeout * Update CHANGELOG.md --- CHANGELOG.md | 1 + core/src/connection/mod.rs | 13 ++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 305bf5d5..e8db0b46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [core] MSRV is now 1.81 (breaking) +- [core] AP connect and handshake have a combined 5 second timeout. - [connect] Replaced `ConnectConfig` with `ConnectStateConfig` (breaking) - [connect] Replaced `playing_track_index` field of `SpircLoadCommand` with `playing_track` (breaking) - [connect] Replaced Mercury usage in `Spirc` with Dealer diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index 6f2c7d44..ca89e87b 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -63,10 +63,13 @@ impl From for AuthenticationError { } pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result { - const TIMEOUT: Duration = Duration::from_secs(3); - let socket = tokio::time::timeout(TIMEOUT, crate::socket::connect(host, port, proxy)).await??; - - handshake(socket).await + const TIMEOUT: Duration = Duration::from_secs(5); + tokio::time::timeout(TIMEOUT, { + let socket = crate::socket::connect(host, port, proxy).await?; + debug!("Connection to AP established."); + handshake(socket) + }) + .await? } pub async fn connect_with_retry( @@ -80,7 +83,7 @@ pub async fn connect_with_retry( match connect(host, port, proxy).await { Ok(f) => return Ok(f), Err(e) => { - debug!("Connection failed: {e}"); + debug!("Connection to \"{host}:{port}\" failed: {e}"); if num_retries < max_retries { num_retries += 1; debug!("Retry access point..."); From 471735aa5a27a1a1ebed16740624366ce8c3caa9 Mon Sep 17 00:00:00 2001 From: humaita-github <68893003+humaita-github@users.noreply.github.com> Date: Fri, 31 Jan 2025 23:29:55 +0100 Subject: [PATCH 496/561] Update softmixer.rs to use AtomicU64 from atomic_shim, so that librespot works with MIPS and MIPSEL (#1461) * Update softmixer.rs Use AtomicU64 from atomic_shim, so that librespot works with MIPS and MIPSEL * Update Cargo.toml with atomic-shim dependency Added atomic-shim dependency * Update Cargo.lock with atomic-shim package Added atomic-shim package --- Cargo.lock | 10 ++++++++++ playback/Cargo.toml | 1 + playback/src/mixer/softmixer.rs | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 51bf7fd1..9402e6c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,6 +169,15 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "atomic-shim" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2133,6 +2142,7 @@ name = "librespot-playback" version = "0.6.0-dev" dependencies = [ "alsa", + "atomic-shim", "cpal", "futures-util", "glib", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index e24f9868..ba1b5ad8 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -21,6 +21,7 @@ path = "../metadata" version = "0.6.0-dev" [dependencies] +atomic-shim = "0.2.0" futures-util = "0.3" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 061f39b9..14adac4d 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,4 +1,5 @@ -use std::sync::atomic::{AtomicU64, Ordering}; +use atomic_shim::AtomicU64; +use std::sync::atomic::Ordering; use std::sync::Arc; use super::VolumeGetter; From 34762f227465281a636e962362f7982ca54f5764 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 2 Feb 2025 22:58:30 +0100 Subject: [PATCH 497/561] Shuffle tracks in place (#1445) * connect: add shuffle_vec.rs * connect: shuffle in place * add shuffle with seed option * reduce complexity to add new metadata fields * add context index to metadata * use seed for shuffle When losing the connection and restarting the dealer, the seed is now stored in the context metadata. So on transfer we can pickup the seed again and shuffle the context as it was previously * add log for shuffle seed * connect: use small_rng, derive Default * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- connect/Cargo.toml | 2 +- connect/src/context_resolver.rs | 25 +++---- connect/src/lib.rs | 1 + connect/src/shuffle_vec.rs | 117 ++++++++++++++++++++++++++++++++ connect/src/spirc.rs | 27 +++----- connect/src/state.rs | 8 +-- connect/src/state/context.rs | 93 +++++++++++++------------ connect/src/state/handle.rs | 18 +++-- connect/src/state/metadata.rs | 114 +++++++++++++++---------------- connect/src/state/options.rs | 39 ++++++----- connect/src/state/tracks.rs | 17 +++-- connect/src/state/transfer.rs | 23 ++++++- 12 files changed, 314 insertions(+), 170 deletions(-) create mode 100644 connect/src/shuffle_vec.rs diff --git a/connect/Cargo.toml b/connect/Cargo.toml index ee076c3e..74b21b7d 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -12,7 +12,7 @@ edition = "2021" futures-util = "0.3" log = "0.4" protobuf = "3.5" -rand = "0.8" +rand = { version = "0.8", default-features = false, features = ["small_rng"] } serde_json = "1.0" thiserror = "2.0" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs index 278fc089..79d48973 100644 --- a/connect/src/context_resolver.rs +++ b/connect/src/context_resolver.rs @@ -4,13 +4,10 @@ use crate::{ autoplay_context_request::AutoplayContextRequest, context::Context, transfer_state::TransferState, }, - state::{ - context::{ContextType, UpdateContext}, - ConnectState, - }, + state::{context::ContextType, ConnectState}, }; -use std::cmp::PartialEq; use std::{ + cmp::PartialEq, collections::{HashMap, VecDeque}, fmt::{Display, Formatter}, hash::Hash, @@ -35,7 +32,7 @@ pub(super) enum ContextAction { pub(super) struct ResolveContext { resolve: Resolve, fallback: Option, - update: UpdateContext, + update: ContextType, action: ContextAction, } @@ -44,7 +41,7 @@ impl ResolveContext { Self { resolve: Resolve::Uri(uri.into()), fallback: None, - update: UpdateContext::Default, + update: ContextType::Default, action: ContextAction::Append, } } @@ -52,7 +49,7 @@ impl ResolveContext { pub fn from_uri( uri: impl Into, fallback: impl Into, - update: UpdateContext, + update: ContextType, action: ContextAction, ) -> Self { let fallback_uri = fallback.into(); @@ -64,7 +61,7 @@ impl ResolveContext { } } - pub fn from_context(context: Context, update: UpdateContext, action: ContextAction) -> Self { + pub fn from_context(context: Context, update: ContextType, action: ContextAction) -> Self { Self { resolve: Resolve::Context(context), fallback: None, @@ -214,7 +211,7 @@ impl ContextResolver { let (next, resolve_uri, _) = self.find_next().ok_or(ContextResolverError::NoNext)?; match next.update { - UpdateContext::Default => { + ContextType::Default => { let mut ctx = self.session.spclient().get_context(resolve_uri).await; if let Ok(ctx) = ctx.as_mut() { ctx.uri = Some(next.context_uri().to_string()); @@ -223,7 +220,7 @@ impl ContextResolver { ctx } - UpdateContext::Autoplay => { + ContextType::Autoplay => { if resolve_uri.contains("spotify:show:") || resolve_uri.contains("spotify:episode:") { // autoplay is not supported for podcasts @@ -304,13 +301,13 @@ impl ContextResolver { } match (next.update, state.active_context) { - (UpdateContext::Default, ContextType::Default) | (UpdateContext::Autoplay, _) => { + (ContextType::Default, ContextType::Default) | (ContextType::Autoplay, _) => { debug!( "last item of type <{:?}>, finishing state setup", next.update ); } - (UpdateContext::Default, _) => { + (ContextType::Default, _) => { debug!("skipped finishing default, because it isn't the active context"); return false; } @@ -320,7 +317,7 @@ impl ContextResolver { let res = if let Some(transfer_state) = transfer_state.take() { state.finish_transfer(transfer_state) } else if state.shuffling_context() { - state.shuffle() + state.shuffle(None) } else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) { // has context, and context is not touched // when the index is not zero, the next index was already evaluated elsewhere diff --git a/connect/src/lib.rs b/connect/src/lib.rs index 11a65186..ebceaaac 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -7,5 +7,6 @@ use librespot_protocol as protocol; mod context_resolver; mod model; +pub mod shuffle_vec; pub mod spirc; pub mod state; diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs new file mode 100644 index 00000000..b7bb5f3d --- /dev/null +++ b/connect/src/shuffle_vec.rs @@ -0,0 +1,117 @@ +use rand::{rngs::SmallRng, Rng, SeedableRng}; +use std::{ + ops::{Deref, DerefMut}, + vec::IntoIter, +}; + +#[derive(Debug, Clone, Default)] +pub struct ShuffleVec { + vec: Vec, + indices: Option>, +} + +impl PartialEq for ShuffleVec { + fn eq(&self, other: &Self) -> bool { + self.vec == other.vec + } +} + +impl Deref for ShuffleVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.vec + } +} + +impl DerefMut for ShuffleVec { + fn deref_mut(&mut self) -> &mut Self::Target { + self.vec.as_mut() + } +} + +impl IntoIterator for ShuffleVec { + type Item = T; + type IntoIter = IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.vec.into_iter() + } +} + +impl From> for ShuffleVec { + fn from(vec: Vec) -> Self { + Self { vec, indices: None } + } +} + +impl ShuffleVec { + pub fn new() -> Self { + Self { + vec: Vec::new(), + indices: None, + } + } + + pub fn shuffle_with_seed(&mut self, seed: u64) { + self.shuffle_with_rng(SmallRng::seed_from_u64(seed)) + } + + pub fn shuffle_with_rng(&mut self, mut rng: impl Rng) { + if self.indices.is_some() { + self.unshuffle() + } + + let indices = { + (1..self.vec.len()) + .rev() + .map(|i| rng.gen_range(0..i + 1)) + .collect() + }; + + for (i, &rnd_ind) in (1..self.vec.len()).rev().zip(&indices) { + self.vec.swap(i, rnd_ind); + } + + self.indices = Some(indices) + } + + pub fn unshuffle(&mut self) { + let indices = match self.indices.take() { + Some(indices) => indices, + None => return, + }; + + for i in 1..self.vec.len() { + let n = indices[self.vec.len() - i - 1]; + self.vec.swap(n, i); + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::Rng; + + #[test] + fn test_shuffle_with_seed() { + let seed = rand::thread_rng().gen_range(0..10000000000000); + + let vec = (0..100).collect::>(); + let base_vec: ShuffleVec = vec.into(); + + let mut shuffled_vec = base_vec.clone(); + shuffled_vec.shuffle_with_seed(seed); + + let mut different_shuffled_vec = base_vec.clone(); + different_shuffled_vec.shuffle_with_seed(seed); + + assert_eq!(shuffled_vec, different_shuffled_vec); + + let mut unshuffled_vec = shuffled_vec.clone(); + unshuffled_vec.unshuffle(); + + assert_eq!(base_vec, unshuffled_vec); + } +} diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d4773fc0..d1cf9e5b 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -25,9 +25,7 @@ use crate::{ user_attributes::UserAttributesMutation, }, state::{ - context::{ - ResetContext, {ContextType, UpdateContext}, - }, + context::{ContextType, ResetContext}, metadata::Metadata, provider::IsProvider, {ConnectState, ConnectStateConfig}, @@ -37,7 +35,6 @@ use futures_util::StreamExt; use protobuf::MessageField; use std::{ future::Future, - ops::Deref, sync::atomic::{AtomicUsize, Ordering}, sync::Arc, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -749,9 +746,6 @@ impl SpircTask { use protobuf::Message; - // todo: handle received pages from transfer, important to not always shuffle the first 10 tracks - // also important when the dealer is restarted, currently we just shuffle again, but at least - // the 10 tracks provided should be used and after that the new shuffle context match TransferState::parse_from_bytes(&cluster.transfer_data) { Ok(transfer_state) => self.handle_transfer(transfer_state)?, Err(why) => error!("failed to take over control: {why}"), @@ -889,7 +883,7 @@ impl SpircTask { } else { self.context_resolver.add(ResolveContext::from_context( update_context.context, - super::state::context::UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )) } @@ -1007,7 +1001,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( ctx_uri.clone(), &fallback, - UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )); @@ -1044,7 +1038,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( ctx_uri, fallback, - UpdateContext::Autoplay, + ContextType::Autoplay, ContextAction::Replace, )) } @@ -1139,13 +1133,12 @@ impl SpircTask { }; let update_context = if cmd.autoplay { - UpdateContext::Autoplay + ContextType::Autoplay } else { - UpdateContext::Default + ContextType::Default }; - self.connect_state - .set_active_context(*update_context.deref()); + self.connect_state.set_active_context(update_context); let current_context_uri = self.connect_state.context_uri(); if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { @@ -1209,7 +1202,7 @@ impl SpircTask { if self.context_resolver.has_next() { self.connect_state.update_queue_revision() } else { - self.connect_state.shuffle()?; + self.connect_state.shuffle(None)?; self.add_autoplay_resolving_when_required(); } } else { @@ -1366,7 +1359,7 @@ impl SpircTask { let resolve = ResolveContext::from_uri( current_context, fallback, - UpdateContext::Autoplay, + ContextType::Autoplay, if has_tracks { ContextAction::Append } else { @@ -1458,7 +1451,7 @@ impl SpircTask { self.context_resolver.add(ResolveContext::from_uri( uri, self.connect_state.current_track(|t| &t.uri), - UpdateContext::Default, + ContextType::Default, ContextAction::Replace, )); diff --git a/connect/src/state.rs b/connect/src/state.rs index c06618ae..73010b25 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -7,12 +7,12 @@ mod restrictions; mod tracks; mod transfer; -use crate::model::SpircPlayStatus; use crate::{ core::{ config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, version, Error, Session, }, + model::SpircPlayStatus, protocol::{ connect::{Capabilities, Device, DeviceInfo, MemberType, PutStateReason, PutStateRequest}, media::AudioQuality, @@ -26,7 +26,6 @@ use crate::{ provider::{IsProvider, Provider}, }, }; - use log::LevelFilter; use protobuf::{EnumOrUnknown, MessageField}; use std::{ @@ -118,10 +117,9 @@ pub struct ConnectState { /// the context from which we play, is used to top up prev and next tracks context: Option, + /// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer] + transfer_shuffle_seed: Option, - /// a context to keep track of our shuffled context, - /// should be only available when `player.option.shuffling_context` is true - shuffle_context: Option, /// a context to keep track of the autoplay context autoplay_context: Option, } diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index fa78180a..5233795e 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -7,6 +7,7 @@ use crate::{ player::{ContextIndex, ProvidedTrack}, restrictions::Restrictions, }, + shuffle_vec::ShuffleVec, state::{ metadata::Metadata, provider::{IsProvider, Provider}, @@ -15,46 +16,28 @@ use crate::{ }; use protobuf::MessageField; use std::collections::HashMap; -use std::ops::Deref; use uuid::Uuid; const LOCAL_FILES_IDENTIFIER: &str = "spotify:local-files"; const SEARCH_IDENTIFIER: &str = "spotify:search"; -#[derive(Debug, Clone)] +#[derive(Debug)] pub struct StateContext { - pub tracks: Vec, + pub tracks: ShuffleVec, + pub skip_track: Option, pub metadata: HashMap, pub restrictions: Option, /// is used to keep track which tracks are already loaded into the next_tracks pub index: ContextIndex, } -#[derive(Default, Debug, Copy, Clone, PartialEq)] +#[derive(Default, Debug, Copy, Clone, PartialEq, Hash, Eq)] pub enum ContextType { #[default] Default, - Shuffle, Autoplay, } -#[derive(Debug, Hash, Copy, Clone, PartialEq, Eq)] -pub enum UpdateContext { - Default, - Autoplay, -} - -impl Deref for UpdateContext { - type Target = ContextType; - - fn deref(&self) -> &Self::Target { - match self { - UpdateContext::Default => &ContextType::Default, - UpdateContext::Autoplay => &ContextType::Autoplay, - } - } -} - pub enum ResetContext<'s> { Completely, DefaultIndex, @@ -96,12 +79,19 @@ impl ConnectState { pub fn get_context(&self, ty: ContextType) -> Result<&StateContext, StateError> { match ty { ContextType::Default => self.context.as_ref(), - ContextType::Shuffle => self.shuffle_context.as_ref(), ContextType::Autoplay => self.autoplay_context.as_ref(), } .ok_or(StateError::NoContext(ty)) } + pub fn get_context_mut(&mut self, ty: ContextType) -> Result<&mut StateContext, StateError> { + match ty { + ContextType::Default => self.context.as_mut(), + ContextType::Autoplay => self.autoplay_context.as_mut(), + } + .ok_or(StateError::NoContext(ty)) + } + pub fn context_uri(&self) -> &String { &self.player().context_uri } @@ -115,14 +105,18 @@ impl ConnectState { if matches!(reset_as, ResetContext::WhenDifferent(ctx) if self.different_context_uri(ctx)) { reset_as = ResetContext::Completely } - self.shuffle_context = None; + + if let Ok(ctx) = self.get_context_mut(ContextType::Default) { + ctx.remove_shuffle_seed(); + ctx.tracks.unshuffle() + } match reset_as { + ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::Completely => { self.context = None; self.autoplay_context = None; } - ResetContext::WhenDifferent(_) => debug!("context didn't change, no reset"), ResetContext::DefaultIndex => { for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()] .into_iter() @@ -190,7 +184,7 @@ impl ConnectState { pub fn update_context( &mut self, mut context: Context, - ty: UpdateContext, + ty: ContextType, ) -> Result>, Error> { if context.pages.iter().all(|p| p.tracks.is_empty()) { error!("context didn't have any tracks: {context:#?}"); @@ -221,12 +215,13 @@ impl ConnectState { ); match ty { - UpdateContext::Default => { + ContextType::Default => { let mut new_context = self.state_context_from_page( page, context.metadata, context.restrictions.take(), context.uri.as_deref(), + Some(0), None, ); @@ -245,7 +240,7 @@ impl ConnectState { }; // enforce reloading the context - if let Some(autoplay_ctx) = self.autoplay_context.as_mut() { + if let Ok(autoplay_ctx) = self.get_context_mut(ContextType::Autoplay) { autoplay_ctx.index.track = 0 } self.clear_next_tracks(); @@ -261,12 +256,13 @@ impl ConnectState { } self.player_mut().context_uri = context.uri.take().unwrap_or_default(); } - UpdateContext::Autoplay => { + ContextType::Autoplay => { self.autoplay_context = Some(self.state_context_from_page( page, context.metadata, context.restrictions.take(), context.uri.as_deref(), + None, Some(Provider::Autoplay), )) } @@ -349,6 +345,7 @@ impl ConnectState { metadata: HashMap, restrictions: Option, new_context_uri: Option<&str>, + context_length: Option, provider: Option, ) -> StateContext { let new_context_uri = new_context_uri.unwrap_or(self.context_uri()); @@ -356,10 +353,12 @@ impl ConnectState { let tracks = page .tracks .iter() - .flat_map(|track| { + .enumerate() + .flat_map(|(i, track)| { match self.context_to_provided_track( track, Some(new_context_uri), + context_length.map(|l| l + i), Some(&page.metadata), provider.clone(), ) { @@ -373,20 +372,28 @@ impl ConnectState { .collect::>(); StateContext { - tracks, + tracks: tracks.into(), + skip_track: None, restrictions, metadata, index: ContextIndex::new(), } } + pub fn is_skip_track(&self, track: &ProvidedTrack) -> bool { + self.get_context(self.active_context) + .ok() + .and_then(|t| t.skip_track.as_ref().map(|t| t.uri == track.uri)) + .unwrap_or(false) + } + pub fn merge_context(&mut self, context: Option) -> Option<()> { let mut context = context?; if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) { return None; } - let current_context = self.context.as_mut()?; + let current_context = self.get_context_mut(ContextType::Default).ok()?; let new_page = context.pages.pop()?; for new_track in new_page.tracks { @@ -421,12 +428,7 @@ impl ConnectState { ty: ContextType, new_index: usize, ) -> Result<(), StateError> { - let context = match ty { - ContextType::Default => self.context.as_mut(), - ContextType::Shuffle => self.shuffle_context.as_mut(), - ContextType::Autoplay => self.autoplay_context.as_mut(), - } - .ok_or(StateError::NoContext(ty))?; + let context = self.get_context_mut(ty)?; context.index.track = new_index as u32; Ok(()) @@ -436,6 +438,7 @@ impl ConnectState { &self, ctx_track: &ContextTrack, context_uri: Option<&str>, + context_index: Option, page_metadata: Option<&HashMap>, provider: Option, ) -> Result { @@ -479,19 +482,25 @@ impl ConnectState { }; if let Some(context_uri) = context_uri { - track.set_context_uri(context_uri.to_string()); - track.set_entity_uri(context_uri.to_string()); + track.set_entity_uri(context_uri); + track.set_context_uri(context_uri); + } + + if let Some(index) = context_index { + track.set_context_index(index); } if matches!(provider, Provider::Autoplay) { - track.set_autoplay(true) + track.set_from_autoplay(true) } Ok(track) } pub fn fill_context_from_page(&mut self, page: ContextPage) -> Result<(), Error> { - let context = self.state_context_from_page(page, HashMap::new(), None, None, None); + let ctx_len = self.context.as_ref().map(|c| c.tracks.len()); + let context = self.state_context_from_page(page, HashMap::new(), None, None, ctx_len, None); + let ctx = self .context .as_mut() diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index 1c1a4b32..659ed92c 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -2,6 +2,7 @@ use crate::{ core::{dealer::protocol::SetQueueCommand, Error}, state::{ context::{ContextType, ResetContext}, + metadata::Metadata, ConnectState, }, }; @@ -12,7 +13,7 @@ impl ConnectState { self.set_shuffle(shuffle); if shuffle { - return self.shuffle(); + return self.shuffle(None); } self.reset_context(ResetContext::DefaultIndex); @@ -21,11 +22,16 @@ impl ConnectState { return Ok(()); } - let ctx = self.get_context(ContextType::Default)?; - let current_index = - ConnectState::find_index_in_context(ctx, |c| self.current_track(|t| c.uri == t.uri))?; - - self.reset_playback_to_position(Some(current_index)) + match self.current_track(|t| t.get_context_index()) { + Some(current_index) => self.reset_playback_to_position(Some(current_index)), + None => { + let ctx = self.get_context(ContextType::Default)?; + let current_index = ConnectState::find_index_in_context(ctx, |c| { + self.current_track(|t| c.uri == t.uri) + })?; + self.reset_playback_to_position(Some(current_index)) + } + } } pub fn handle_set_queue(&mut self, set_queue: SetQueueCommand) { diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index b1effb68..763244b7 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -1,84 +1,82 @@ -use librespot_protocol::{context_track::ContextTrack, player::ProvidedTrack}; +use crate::{ + protocol::{context::Context, context_track::ContextTrack, player::ProvidedTrack}, + state::context::StateContext, +}; use std::collections::HashMap; +use std::fmt::Display; const CONTEXT_URI: &str = "context_uri"; const ENTITY_URI: &str = "entity_uri"; const IS_QUEUED: &str = "is_queued"; const IS_AUTOPLAY: &str = "autoplay.is_autoplay"; - const HIDDEN: &str = "hidden"; const ITERATION: &str = "iteration"; +const CUSTOM_CONTEXT_INDEX: &str = "context_index"; +const CUSTOM_SHUFFLE_SEED: &str = "shuffle_seed"; + +macro_rules! metadata_entry { + ( $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident)) => { + metadata_entry!( $get use get, $set, $clear ($key: $entry) -> Option<&String> ); + }; + ( $get_key:ident use $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident) -> $ty:ty ) => { + fn $get_key (&self) -> $ty { + self.$get($entry) + } + + fn $set (&mut self, $key: impl Display) { + self.metadata_mut().insert($entry.to_string(), $key.to_string()); + } + + fn $clear(&mut self) { + self.metadata_mut().remove($entry); + } + }; +} + #[allow(dead_code)] pub trait Metadata { fn metadata(&self) -> &HashMap; fn metadata_mut(&mut self) -> &mut HashMap; - fn is_from_queue(&self) -> bool { - matches!(self.metadata().get(IS_QUEUED), Some(is_queued) if is_queued.eq("true")) + fn get_bool(&self, entry: &str) -> bool { + matches!(self.metadata().get(entry), Some(entry) if entry.eq("true")) } - fn is_from_autoplay(&self) -> bool { - matches!(self.metadata().get(IS_AUTOPLAY), Some(is_autoplay) if is_autoplay.eq("true")) + fn get_usize(&self, entry: &str) -> Option { + self.metadata().get(entry)?.parse().ok() } - fn is_hidden(&self) -> bool { - matches!(self.metadata().get(HIDDEN), Some(is_hidden) if is_hidden.eq("true")) + fn get(&self, entry: &str) -> Option<&String> { + self.metadata().get(entry) } - fn get_context_uri(&self) -> Option<&String> { - self.metadata().get(CONTEXT_URI) - } + metadata_entry!(is_from_queue use get_bool, set_from_queue, remove_from_queue (is_queued: IS_QUEUED) -> bool); + metadata_entry!(is_from_autoplay use get_bool, set_from_autoplay, remove_from_autoplay (is_autoplay: IS_AUTOPLAY) -> bool); + metadata_entry!(is_hidden use get_bool, set_hidden, remove_hidden (is_hidden: HIDDEN) -> bool); - fn get_iteration(&self) -> Option<&String> { - self.metadata().get(ITERATION) - } - - fn set_queued(&mut self, queued: bool) { - self.metadata_mut() - .insert(IS_QUEUED.to_string(), queued.to_string()); - } - - fn set_autoplay(&mut self, autoplay: bool) { - self.metadata_mut() - .insert(IS_AUTOPLAY.to_string(), autoplay.to_string()); - } - - fn set_hidden(&mut self, hidden: bool) { - self.metadata_mut() - .insert(HIDDEN.to_string(), hidden.to_string()); - } - - fn set_context_uri(&mut self, uri: String) { - self.metadata_mut().insert(CONTEXT_URI.to_string(), uri); - } - - fn set_entity_uri(&mut self, uri: String) { - self.metadata_mut().insert(ENTITY_URI.to_string(), uri); - } - - fn add_iteration(&mut self, iter: i64) { - self.metadata_mut() - .insert(ITERATION.to_string(), iter.to_string()); - } + metadata_entry!(get_context_index use get_usize, set_context_index, remove_context_index (context_index: CUSTOM_CONTEXT_INDEX) -> Option); + metadata_entry!(get_context_uri, set_context_uri, remove_context_uri (context_uri: CONTEXT_URI)); + metadata_entry!(get_entity_uri, set_entity_uri, remove_entity_uri (entity_uri: ENTITY_URI)); + metadata_entry!(get_iteration, set_iteration, remove_iteration (iteration: ITERATION)); + metadata_entry!(get_shuffle_seed, set_shuffle_seed, remove_shuffle_seed (shuffle_seed: CUSTOM_SHUFFLE_SEED)); } -impl Metadata for ContextTrack { - fn metadata(&self) -> &HashMap { - &self.metadata - } +macro_rules! impl_metadata { + ($impl_for:ident) => { + impl Metadata for $impl_for { + fn metadata(&self) -> &HashMap { + &self.metadata + } - fn metadata_mut(&mut self) -> &mut HashMap { - &mut self.metadata - } + fn metadata_mut(&mut self) -> &mut HashMap { + &mut self.metadata + } + } + }; } -impl Metadata for ProvidedTrack { - fn metadata(&self) -> &HashMap { - &self.metadata - } - - fn metadata_mut(&mut self) -> &mut HashMap { - &mut self.metadata - } -} +impl_metadata!(ContextTrack); +impl_metadata!(ProvidedTrack); +impl_metadata!(Context); +impl_metadata!(StateContext); diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index 97484f67..6f384810 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -1,9 +1,14 @@ -use crate::state::context::ContextType; -use crate::state::{ConnectState, StateError}; -use librespot_core::Error; -use librespot_protocol::player::{ContextIndex, ContextPlayerOptions}; +use crate::{ + core::Error, + protocol::player::ContextPlayerOptions, + state::{ + context::{ContextType, ResetContext}, + metadata::Metadata, + ConnectState, StateError, + }, +}; use protobuf::MessageField; -use rand::prelude::SliceRandom; +use rand::Rng; impl ConnectState { fn add_options_if_empty(&mut self) { @@ -39,7 +44,7 @@ impl ConnectState { self.set_repeat_context(false); } - pub fn shuffle(&mut self) -> Result<(), Error> { + pub fn shuffle(&mut self, seed: Option) -> Result<(), Error> { if let Some(reason) = self .player() .restrictions @@ -55,22 +60,22 @@ impl ConnectState { self.clear_prev_track(); self.clear_next_tracks(); - let current_uri = self.current_track(|t| &t.uri); + let current_track = self.current_track(|t| t.clone().take()); - let ctx = self.get_context(ContextType::Default)?; - let current_track = Self::find_index_in_context(ctx, |t| &t.uri == current_uri)?; + self.reset_context(ResetContext::DefaultIndex); + let ctx = self.get_context_mut(ContextType::Default)?; - let mut shuffle_context = ctx.clone(); // we don't need to include the current track, because it is already being played - shuffle_context.tracks.remove(current_track); + ctx.skip_track = current_track; - let mut rng = rand::thread_rng(); - shuffle_context.tracks.shuffle(&mut rng); - shuffle_context.index = ContextIndex::new(); + let seed = seed + .unwrap_or_else(|| rand::thread_rng().gen_range(100_000_000_000..1_000_000_000_000)); - self.shuffle_context = Some(shuffle_context); - self.set_active_context(ContextType::Shuffle); - self.fill_up_context = ContextType::Shuffle; + ctx.tracks.shuffle_with_seed(seed); + ctx.set_shuffle_seed(seed); + + self.set_active_context(ContextType::Default); + self.fill_up_context = ContextType::Default; self.fill_up_next_tracks()?; Ok(()) diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 05468575..07d03991 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -23,7 +23,7 @@ impl<'ct> ConnectState { ..Default::default() }; delimiter.set_hidden(true); - delimiter.add_iteration(iteration); + delimiter.set_iteration(iteration); delimiter } @@ -124,6 +124,7 @@ impl<'ct> ConnectState { continue; } Some(next) if next.is_unavailable() => continue, + Some(next) if self.is_skip_track(&next) => continue, other => break other, }; }; @@ -141,12 +142,10 @@ impl<'ct> ConnectState { self.set_active_context(ContextType::Autoplay); None } else { - let ctx = self.get_context(ContextType::Default)?; - let new_index = Self::find_index_in_context(ctx, |c| c.uri == new_track.uri); - match new_index { - Ok(new_index) => Some(new_index as u32), - Err(why) => { - error!("didn't find the track in the current context: {why}"); + match new_track.get_context_index() { + Some(new_index) => Some(new_index as u32), + None => { + error!("the given context track had no set context_index"); None } } @@ -323,7 +322,7 @@ impl<'ct> ConnectState { } } None => break, - Some(ct) if ct.is_unavailable() => { + Some(ct) if ct.is_unavailable() || self.is_skip_track(ct) => { new_index += 1; continue; } @@ -414,7 +413,7 @@ impl<'ct> ConnectState { track.set_provider(Provider::Queue); if !track.is_from_queue() { - track.set_queued(true); + track.set_from_queue(true); } let next_tracks = self.next_tracks_mut(); diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index 7404bf55..1e2f40cf 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -26,6 +26,7 @@ impl ConnectState { track, transfer.current_session.context.uri.as_deref(), None, + None, transfer .queue .is_playing_queue @@ -52,10 +53,25 @@ impl ConnectState { _ => player.playback_speed = 1., } + let mut shuffle_seed = None; if let Some(session) = transfer.current_session.as_mut() { player.play_origin = session.play_origin.take().map(Into::into).into(); player.suppressions = session.suppressions.take().map(Into::into).into(); + // maybe at some point we can use the shuffle seed provided by spotify, + // but I doubt it, as spotify doesn't use true randomness but rather an algorithm + // based shuffle + trace!( + "shuffle_seed: <{:?}> (spotify), <{:?}> (own)", + session.shuffle_seed, + session.context.get_shuffle_seed() + ); + + shuffle_seed = session + .context + .get_shuffle_seed() + .and_then(|seed| seed.parse().ok()); + if let Some(mut ctx) = session.context.take() { player.restrictions = ctx.restrictions.take().map(Into::into).into(); for (key, value) in ctx.metadata { @@ -73,6 +89,8 @@ impl ConnectState { } } + self.transfer_shuffle_seed = shuffle_seed; + self.clear_prev_track(); self.clear_next_tracks(); self.update_queue_revision() @@ -134,6 +152,7 @@ impl ConnectState { track, Some(self.context_uri()), None, + None, Some(Provider::Queue), ) { self.add_to_queue(queued_track, false); @@ -143,7 +162,9 @@ impl ConnectState { if self.shuffling_context() { self.set_current_track(current_index.unwrap_or_default())?; self.set_shuffle(true); - self.shuffle()?; + + let previous_seed = self.transfer_shuffle_seed.take(); + self.shuffle(previous_seed)?; } else { self.reset_playback_to_position(current_index)?; } From cf61ede6c694556b0587a47e1630e7521bc9d8e9 Mon Sep 17 00:00:00 2001 From: Dariusz Olszewski <8277636+starypatyk@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:50:47 +0100 Subject: [PATCH 498/561] Fix cross compilation for armv6hf (Raspberry Pi 1) (#1457) * Use ring instead of aws-lc as the default backend for hyper-rustls Signed-off-by: Dariusz Olszewski * Cross-compile with libmdns Signed-off-by: Dariusz Olszewski * Simplify Docker image to cross-compile for armv6hf (RPi 1) Signed-off-by: Dariusz Olszewski * Revert "Use ring instead of aws-lc as the default backend for hyper-rustls" This reverts commit faeaf506d64ab988d6cc950b58dbe22c0eed9386. * Fix bindgen issues (aws-lc-rs) when cross-compiling for armv6hf Signed-off-by: Dariusz Olszewski * Add git to the Docker image for cross-compiling Signed-off-by: Dariusz Olszewski --------- Signed-off-by: Dariusz Olszewski Co-authored-by: Dariusz Olszewski --- contrib/Dockerfile | 9 +++++---- contrib/cross-compile-armv6hf/Dockerfile | 15 +++++---------- contrib/cross-compile-armv6hf/docker-build.sh | 13 ++++++++----- contrib/docker-build.sh | 8 ++++---- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 64d306c0..644ab029 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -8,10 +8,10 @@ # The compiled binaries will be located in /tmp/librespot-build # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features alsa-backend -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" FROM debian:bookworm @@ -30,6 +30,7 @@ RUN dpkg --add-architecture arm64 && \ crossbuild-essential-armel \ crossbuild-essential-armhf \ curl \ + git \ libasound2-dev \ libasound2-dev:arm64 \ libasound2-dev:armel \ diff --git a/contrib/cross-compile-armv6hf/Dockerfile b/contrib/cross-compile-armv6hf/Dockerfile index c7f73f4c..16694afa 100644 --- a/contrib/cross-compile-armv6hf/Dockerfile +++ b/contrib/cross-compile-armv6hf/Dockerfile @@ -11,20 +11,18 @@ FROM --platform=linux/amd64 ubuntu:18.04 # Install common packages. RUN apt-get update -RUN apt-get install -y -qq git curl build-essential libasound2-dev libssl-dev libpulse-dev libdbus-1-dev +RUN apt-get install -y -qq git curl build-essential cmake clang libclang-dev libasound2-dev libpulse-dev # Install armhf packages. RUN echo "deb [arch=armhf] http://ports.ubuntu.com/ubuntu-ports/ bionic main" | tee -a /etc/apt/sources.list RUN apt-get update -RUN apt-get download libasound2:armhf libasound2-dev:armhf libssl-dev:armhf libssl1.1:armhf +RUN apt-get download libasound2:armhf libasound2-dev:armhf RUN mkdir /sysroot && \ dpkg -x libasound2_*.deb /sysroot/ && \ - dpkg -x libssl-dev*.deb /sysroot/ && \ - dpkg -x libssl1.1*.deb /sysroot/ && \ dpkg -x libasound2-dev*.deb /sysroot/ # Install rust. -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add arm-unknown-linux-gnueabihf RUN mkdir /.cargo && \ @@ -35,14 +33,11 @@ RUN mkdir /pi && \ git -C /pi clone --depth=1 https://github.com/raspberrypi/tools.git # Build env variables. -ENV CARGO_TARGET_DIR /build -ENV CARGO_HOME /build/cache +ENV CARGO_TARGET_DIR=/build +ENV CARGO_HOME=/build/cache ENV PATH="/pi/tools/arm-bcm2708/arm-linux-gnueabihf/bin:${PATH}" ENV PKG_CONFIG_ALLOW_CROSS=1 ENV PKG_CONFIG_PATH_arm-unknown-linux-gnueabihf=/usr/lib/arm-linux-gnueabihf/pkgconfig/ -ENV C_INCLUDE_PATH=/sysroot/usr/include -ENV OPENSSL_LIB_DIR=/sysroot/usr/lib/arm-linux-gnueabihf -ENV OPENSSL_INCLUDE_DIR=/sysroot/usr/include/arm-linux-gnueabihf ADD . /src WORKDIR /src diff --git a/contrib/cross-compile-armv6hf/docker-build.sh b/contrib/cross-compile-armv6hf/docker-build.sh index ab84175c..76158e44 100755 --- a/contrib/cross-compile-armv6hf/docker-build.sh +++ b/contrib/cross-compile-armv6hf/docker-build.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -eux -PI1_TOOLS_DIR="/pi/tools/arm-bcm2708/arm-linux-gnueabihf" +cargo install --force --locked bindgen-cli + +PI1_TOOLS_DIR=/pi/tools/arm-bcm2708/arm-linux-gnueabihf +PI1_TOOLS_SYSROOT_DIR=$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot PI1_LIB_DIRS=( - "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/lib" - "$PI1_TOOLS_DIR/arm-linux-gnueabihf/sysroot/usr/lib" + "$PI1_TOOLS_SYSROOT_DIR/lib" + "$PI1_TOOLS_SYSROOT_DIR/usr/lib" "/sysroot/usr/lib/arm-linux-gnueabihf" - "/sysroot/lib/arm-linux-gnueabihf" ) export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}" +export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$PI1_TOOLS_SYSROOT_DIR -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" diff --git a/contrib/docker-build.sh b/contrib/docker-build.sh index 0d59e341..50b6b3e1 100755 --- a/contrib/docker-build.sh +++ b/contrib/docker-build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eux -cargo build --release --no-default-features --features alsa-backend -cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend -cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend +cargo build --release --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" From 581c8d61eac252b23375555417708a3ba90e15e2 Mon Sep 17 00:00:00 2001 From: humaita-github <68893003+humaita-github@users.noreply.github.com> Date: Fri, 14 Feb 2025 20:16:29 +0100 Subject: [PATCH 499/561] Replacing atomic_shim with portable-atomic, since atomic_shim worsk well for MIPS and MIPSEL, but has problem with some ARM flavours. (#1466) --- Cargo.lock | 11 +---------- playback/Cargo.toml | 2 +- playback/src/mixer/softmixer.rs | 2 +- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9402e6c6..4bf54243 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,15 +169,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "atomic-shim" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cd4b51d303cf3501c301e8125df442128d3c6d7c69f71b27833d253de47e77" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -2142,7 +2133,6 @@ name = "librespot-playback" version = "0.6.0-dev" dependencies = [ "alsa", - "atomic-shim", "cpal", "futures-util", "glib", @@ -2158,6 +2148,7 @@ dependencies = [ "log", "ogg", "parking_lot", + "portable-atomic", "portaudio-rs", "rand", "rand_distr", diff --git a/playback/Cargo.toml b/playback/Cargo.toml index ba1b5ad8..bd53c35b 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -21,7 +21,7 @@ path = "../metadata" version = "0.6.0-dev" [dependencies] -atomic-shim = "0.2.0" +portable-atomic = "1" futures-util = "0.3" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 14adac4d..6d32edc4 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,4 +1,4 @@ -use atomic_shim::AtomicU64; +use portable_atomic::AtomicU64; use std::sync::atomic::Ordering; use std::sync::Arc; From f497806fb1cc85c81ee9feb95f225221bf7a6b53 Mon Sep 17 00:00:00 2001 From: Carlos Tocino Date: Tue, 18 Feb 2025 16:39:31 +0100 Subject: [PATCH 500/561] OAuth process made by a struct, allowing customization options. (#1462) * get refresh token. Optional auth url browser opening * changelog * access token accepts custom message * docs updated * CustomParams renamed * OAuthToken can be cloned * builder pattern on token management * changelog * docs and format issues * split methods and finish documentation * new example and minor adjustments * typo * remove unnecessary dependency * requested changes * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz * Update CHANGELOG.md Co-authored-by: Felix Prillwitz * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz * Update oauth/src/lib.rs Co-authored-by: Nick Steel * Update oauth/src/lib.rs Co-authored-by: Nick Steel * remove veil. Oauth flow fix * debug trait instead of veil * Update main.rs Co-authored-by: Nick Steel --------- Co-authored-by: Felix Prillwitz Co-authored-by: Nick Steel --- CHANGELOG.md | 23 ++- Cargo.lock | 38 +++++ oauth/Cargo.toml | 2 + oauth/examples/oauth.rs | 32 ---- oauth/examples/oauth_async.rs | 65 ++++++++ oauth/examples/oauth_sync.rs | 64 ++++++++ oauth/src/lib.rs | 277 ++++++++++++++++++++++++++++++++-- src/main.rs | 23 +-- 8 files changed, 471 insertions(+), 53 deletions(-) delete mode 100644 oauth/examples/oauth.rs create mode 100644 oauth/examples/oauth_async.rs create mode 100644 oauth/examples/oauth_sync.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index e8db0b46..878999be 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking) - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` +- [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process ### Fixed @@ -39,6 +40,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Handle transfer of playback with empty "uri" field - [connect] Correctly apply playing/paused state when transferring playback +### Deprecated + +- [oauth] `get_access_token()` function marked for deprecation + ### Removed - [core] Removed `get_canvases` from SpClient (breaking) @@ -76,7 +81,7 @@ backend for Spotify Connect discovery. ## [0.5.0] - 2024-10-15 This version is be a major departure from the architecture up until now. It -focuses on implementing the "new Spotify API". This means moving large parts +focuses on implementing the "new Spotify API". This means moving large parts of the Spotify protocol from Mercury to HTTP. A lot of this was reverse engineered before by @devgianlu of librespot-java. It was long overdue that we started implementing it too, not in the least because new features like the @@ -219,14 +224,17 @@ to offer. But, unless anything big comes up, it is also intended as the last release to be based on the old API. Happy listening. ### Changed + - [playback] `pipe`: Better error handling - [playback] `subprocess`: Better error handling ### Added + - [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors - [playback] `pipe`: Implement stop ### Fixed + - [main] fix `--opt=value` line argument logging - [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa` @@ -235,9 +243,11 @@ release to be based on the old API. Happy listening. This release fixes dependency issues when installing from crates. ### Changed + - [chore] The MSRV is now 1.56 ### Fixed + - [playback] Fixed dependency issues when installing from crate ## [0.4.0] - 2022-05-21 @@ -253,6 +263,7 @@ Targeting that major effort for a v0.5 release sometime, we intend to maintain v0.4.x as a stable branch until then. ### Changed + - [chore] The MSRV is now 1.53 - [contrib] Hardened security of the `systemd` service units - [core] `Session`: `connect()` now returns the long-term credentials @@ -265,6 +276,7 @@ v0.4.x as a stable branch until then. - [playback] `Sink`: `write()` now receives ownership of the packet (breaking) ### Added + - [main] Enforce reasonable ranges for option values (breaking) - [main] Add the ability to parse environment variables - [main] Log now emits warning when trying to use options that would otherwise have no effect @@ -277,6 +289,7 @@ v0.4.x as a stable branch until then. - [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 + - [connect] Don't panic when activating shuffle without previous interaction - [core] Removed unsafe code (breaking) - [main] Fix crash when built with Avahi support but Avahi is locally unavailable @@ -287,20 +300,24 @@ v0.4.x as a stable branch until then. - [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls ### Removed + - [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been removed ## [0.3.1] - 2021-10-24 ### Changed + - Include build profile in the displayed version information - [playback] Improve dithering CPU usage by about 33% ### Fixed + - [connect] Partly fix behavior after last track of an album/playlist ## [0.3.0] - 2021-10-13 ### Added + - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. - [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 @@ -309,6 +326,7 @@ v0.4.x as a stable branch until then. - [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically ### Changed + - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) - [audio, playback] Use `Duration` for time constants and functions (breaking) - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate @@ -325,17 +343,20 @@ v0.4.x as a stable branch until then. - [playback] `player`: default normalisation type is now `auto` ### Deprecated + - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate - [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device` - [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control` - [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index` ### Removed + - [connect] Removed no-op mixer started/stopped logic (breaking) - [playback] Removed `with-vorbis` and `with-tremor` features - [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa ### Fixed + - [connect] Fix step size on volume up/down events - [connect] Fix looping back to the first track after the last track of an album or playlist - [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream diff --git a/Cargo.lock b/Cargo.lock index 4bf54243..4f861b91 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1729,6 +1729,25 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -2124,7 +2143,9 @@ dependencies = [ "env_logger", "log", "oauth2", + "open", "thiserror 2.0.11", + "tokio", "url", ] @@ -2534,6 +2555,17 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "open" +version = "5.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl-probe" version = "0.1.5" @@ -2597,6 +2629,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pbkdf2" version = "0.12.2" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index c3d4a81b..d95a5431 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -11,8 +11,10 @@ edition = "2021" [dependencies] log = "0.4" oauth2 = "4.4" +open = "5.3" thiserror = "2.0" url = "2.2" [dev-dependencies] env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } +tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] } diff --git a/oauth/examples/oauth.rs b/oauth/examples/oauth.rs deleted file mode 100644 index 76ff088e..00000000 --- a/oauth/examples/oauth.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::env; - -use librespot_oauth::get_access_token; - -const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; -const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login"; - -fn main() { - let mut builder = env_logger::Builder::new(); - builder.parse_filters("librespot=trace"); - builder.init(); - - let args: Vec<_> = env::args().collect(); - let (client_id, redirect_uri, scopes) = if args.len() == 4 { - // You can use your own client ID, along with it's associated redirect URI. - ( - args[1].as_str(), - args[2].as_str(), - args[3].split(',').collect::>(), - ) - } else if args.len() == 1 { - (SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"]) - } else { - eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]); - return; - }; - - match get_access_token(client_id, redirect_uri, scopes) { - Ok(token) => println!("Success: {token:#?}"), - Err(e) => println!("Failed: {e}"), - }; -} diff --git a/oauth/examples/oauth_async.rs b/oauth/examples/oauth_async.rs new file mode 100644 index 00000000..a8b26799 --- /dev/null +++ b/oauth/examples/oauth_async.rs @@ -0,0 +1,65 @@ +use std::env; + +use librespot_oauth::OAuthClientBuilder; + +const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login"; + +const RESPONSE: &str = r#" + + + +

Return to your app!

+ + +"#; + +#[tokio::main] +async fn main() { + let mut builder = env_logger::Builder::new(); + builder.parse_filters("librespot=trace"); + builder.init(); + + let args: Vec<_> = env::args().collect(); + let (client_id, redirect_uri, scopes) = if args.len() == 4 { + // You can use your own client ID, along with it's associated redirect URI. + ( + args[1].as_str(), + args[2].as_str(), + args[3].split(',').collect::>(), + ) + } else if args.len() == 1 { + (SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"]) + } else { + eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]); + return; + }; + + let client = match OAuthClientBuilder::new(client_id, redirect_uri, scopes) + .open_in_browser() + .with_custom_message(RESPONSE) + .build() + { + Ok(client) => client, + Err(err) => { + eprintln!("Unable to build an OAuth client: {}", err); + return; + } + }; + + let refresh_token = match client.get_access_token_async().await { + Ok(token) => { + println!("OAuth Token: {token:#?}"); + token.refresh_token + } + Err(err) => { + println!("Unable to get OAuth Token: {err}"); + return; + } + }; + + match client.refresh_token_async(&refresh_token).await { + Ok(token) => println!("New refreshed OAuth Token: {token:#?}"), + Err(err) => println!("Unable to get refreshed OAuth Token: {err}"), + } +} diff --git a/oauth/examples/oauth_sync.rs b/oauth/examples/oauth_sync.rs new file mode 100644 index 00000000..af773d20 --- /dev/null +++ b/oauth/examples/oauth_sync.rs @@ -0,0 +1,64 @@ +use std::env; + +use librespot_oauth::OAuthClientBuilder; + +const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd"; +const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login"; + +const RESPONSE: &str = r#" + + + +

Return to your app!

+ + +"#; + +fn main() { + let mut builder = env_logger::Builder::new(); + builder.parse_filters("librespot=trace"); + builder.init(); + + let args: Vec<_> = env::args().collect(); + let (client_id, redirect_uri, scopes) = if args.len() == 4 { + // You can use your own client ID, along with it's associated redirect URI. + ( + args[1].as_str(), + args[2].as_str(), + args[3].split(',').collect::>(), + ) + } else if args.len() == 1 { + (SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"]) + } else { + eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]); + return; + }; + + let client = match OAuthClientBuilder::new(client_id, redirect_uri, scopes) + .open_in_browser() + .with_custom_message(RESPONSE) + .build() + { + Ok(client) => client, + Err(err) => { + eprintln!("Unable to build an OAuth client: {}", err); + return; + } + }; + + let refresh_token = match client.get_access_token() { + Ok(token) => { + println!("OAuth Token: {token:#?}"); + token.refresh_token + } + Err(err) => { + println!("Unable to get OAuth Token: {err}"); + return; + } + }; + + match client.refresh_token(&refresh_token) { + Ok(token) => println!("New refreshed OAuth Token: {token:#?}"), + Err(err) => println!("Unable to get refreshed OAuth Token: {err}"), + } +} diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 591e6559..284f08d7 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -1,3 +1,4 @@ +#![warn(missing_docs)] //! Provides a Spotify access token using the OAuth authorization code flow //! with PKCE. //! @@ -11,66 +12,108 @@ //! is appropriate for headless systems. use log::{error, info, trace}; -use oauth2::reqwest::http_client; +use oauth2::basic::BasicTokenType; +use oauth2::reqwest::{async_http_client, http_client}; use oauth2::{ basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, }; +use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; use std::io; +use std::sync::mpsc; use std::time::{Duration, Instant}; use std::{ io::{BufRead, BufReader, Write}, net::{SocketAddr, TcpListener}, - sync::mpsc, }; use thiserror::Error; use url::Url; +/// Possible errors encountered during the OAuth authentication flow. #[derive(Debug, Error)] pub enum OAuthError { + /// The redirect URI cannot be parsed as a valid URL. #[error("Unable to parse redirect URI {uri} ({e})")] - AuthCodeBadUri { uri: String, e: url::ParseError }, + AuthCodeBadUri { + /// Auth URI. + uri: String, + /// Inner error code. + e: url::ParseError, + }, + /// The authorization code parameter is missing in the redirect URI. #[error("Auth code param not found in URI {uri}")] - AuthCodeNotFound { uri: String }, + AuthCodeNotFound { + /// Auth URI. + uri: String, + }, + /// Failed to read input from standard input when manually collecting auth code. #[error("Failed to read redirect URI from stdin")] AuthCodeStdinRead, + /// Could not bind TCP listener to the specified socket address for OAuth callback. #[error("Failed to bind server to {addr} ({e})")] - AuthCodeListenerBind { addr: SocketAddr, e: io::Error }, + AuthCodeListenerBind { + /// Callback address. + addr: SocketAddr, + /// Inner error code. + e: io::Error, + }, + /// Listener terminated before receiving an OAuth callback connection. #[error("Listener terminated without accepting a connection")] AuthCodeListenerTerminated, + /// Failed to read incoming HTTP request containing OAuth callback. #[error("Failed to read redirect URI from HTTP request")] AuthCodeListenerRead, + /// Received malformed HTTP request for OAuth callback. #[error("Failed to parse redirect URI from HTTP request")] AuthCodeListenerParse, + /// Could not send HTTP response after handling OAuth callback. #[error("Failed to write HTTP response")] AuthCodeListenerWrite, + /// Invalid Spotify authorization endpoint URL. #[error("Invalid Spotify OAuth URI")] InvalidSpotifyUri, + /// Redirect URI failed validation. #[error("Invalid Redirect URI {uri} ({e})")] - InvalidRedirectUri { uri: String, e: url::ParseError }, + InvalidRedirectUri { + /// Auth URI. + uri: String, + /// Inner error code + e: url::ParseError, + }, + /// Channel communication failure. #[error("Failed to receive code")] Recv, + /// Token exchange failure with Spotify's authorization server. #[error("Failed to exchange code for access token ({e})")] - ExchangeCode { e: String }, + ExchangeCode { + /// Inner error description + e: String, + }, } -#[derive(Debug)] +/// Represents an OAuth token used for accessing Spotify's Web API and sessions. +#[derive(Debug, Clone)] pub struct OAuthToken { + /// Bearer token used for authenticated Spotify API requests pub access_token: String, + /// Long-lived token used to obtain new access tokens pub refresh_token: String, + /// Instant when the access token becomes invalid pub expires_at: Instant, + /// Type of token pub token_type: String, + /// Permission scopes granted by this token pub scopes: Vec, } @@ -104,7 +147,10 @@ fn get_authcode_stdin() -> Result { } /// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code. -fn get_authcode_listener(socket_address: SocketAddr) -> Result { +fn get_authcode_listener( + socket_address: SocketAddr, + message: String, +) -> Result { let listener = TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind { addr: socket_address, @@ -130,7 +176,6 @@ fn get_authcode_listener(socket_address: SocketAddr) -> Result Result Option { + #![warn(missing_docs)] let url = match Url::parse(redirect_uri) { Ok(u) if u.scheme() == "http" && u.port().is_some() => u, _ => return None, @@ -162,6 +208,215 @@ fn get_socket_address(redirect_uri: &str) -> Option { None } +/// Struct that handle obtaining and refreshing access tokens. +pub struct OAuthClient { + scopes: Vec, + redirect_uri: String, + should_open_url: bool, + message: String, + client: BasicClient, +} + +impl OAuthClient { + /// Generates and opens/shows the authorization URL to obtain an access token. + /// + /// Returns a verifier that must be included in the final request for validation. + fn set_auth_url(&self) -> PkceCodeVerifier { + let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); + // Generate the full authorization URL. + // Some of these scopes are unavailable for custom client IDs. Which? + let request_scopes: Vec = + self.scopes.iter().map(|s| Scope::new(s.into())).collect(); + let (auth_url, _) = self + .client + .authorize_url(CsrfToken::new_random) + .add_scopes(request_scopes) + .set_pkce_challenge(pkce_challenge) + .url(); + + if self.should_open_url { + open::that_in_background(auth_url.as_str()); + } + println!("Browse to: {}", auth_url); + + pkce_verifier + } + + fn build_token( + &self, + resp: StandardTokenResponse, + ) -> Result { + trace!("Obtained new access token: {resp:?}"); + + let token_scopes: Vec = match resp.scopes() { + Some(s) => s.iter().map(|s| s.to_string()).collect(), + _ => self.scopes.clone(), + }; + let refresh_token = match resp.refresh_token() { + Some(t) => t.secret().to_string(), + _ => "".to_string(), // Spotify always provides a refresh token. + }; + Ok(OAuthToken { + access_token: resp.access_token().secret().to_string(), + refresh_token, + expires_at: Instant::now() + + resp + .expires_in() + .unwrap_or_else(|| Duration::from_secs(3600)), + token_type: format!("{:?}", resp.token_type()), + scopes: token_scopes, + }) + } + + /// Syncronously obtain a Spotify access token using the authorization code with PKCE OAuth flow. + pub fn get_access_token(&self) -> Result { + let pkce_verifier = self.set_auth_url(); + + let code = match get_socket_address(&self.redirect_uri) { + Some(addr) => get_authcode_listener(addr, self.message.clone()), + _ => get_authcode_stdin(), + }?; + trace!("Exchange {code:?} for access token"); + + let (tx, rx) = mpsc::channel(); + let client = self.client.clone(); + std::thread::spawn(move || { + let resp = client + .exchange_code(code) + .set_pkce_verifier(pkce_verifier) + .request(http_client); + if let Err(e) = tx.send(resp) { + error!("OAuth channel send error: {e}"); + } + }); + let channel_response = rx.recv().map_err(|_| OAuthError::Recv)?; + let token_response = + channel_response.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + + self.build_token(token_response) + } + + /// Synchronously obtain a new valid OAuth token from `refresh_token` + pub fn refresh_token(&self, refresh_token: &str) -> Result { + let refresh_token = RefreshToken::new(refresh_token.to_string()); + let resp = self + .client + .exchange_refresh_token(&refresh_token) + .request(http_client); + + let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + self.build_token(resp) + } + + /// Asyncronously obtain a Spotify access token using the authorization code with PKCE OAuth flow. + pub async fn get_access_token_async(&self) -> Result { + let pkce_verifier = self.set_auth_url(); + + let code = match get_socket_address(&self.redirect_uri) { + Some(addr) => get_authcode_listener(addr, self.message.clone()), + _ => get_authcode_stdin(), + }?; + trace!("Exchange {code:?} for access token"); + + let resp = self + .client + .exchange_code(code) + .set_pkce_verifier(pkce_verifier) + .request_async(async_http_client) + .await; + + let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + self.build_token(resp) + } + + /// Asynchronously obtain a new valid OAuth token from `refresh_token` + pub async fn refresh_token_async(&self, refresh_token: &str) -> Result { + let refresh_token = RefreshToken::new(refresh_token.to_string()); + let resp = self + .client + .exchange_refresh_token(&refresh_token) + .request_async(async_http_client) + .await; + + let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; + self.build_token(resp) + } +} + +/// Builder struct through which structures of type OAuthClient are instantiated. +pub struct OAuthClientBuilder { + client_id: String, + redirect_uri: String, + scopes: Vec, + should_open_url: bool, + message: String, +} + +impl OAuthClientBuilder { + /// Create a new OAuthClientBuilder with provided params and default config. + /// + /// `redirect_uri` must match to the registered Uris of `client_id` + pub fn new(client_id: &str, redirect_uri: &str, scopes: Vec<&str>) -> Self { + Self { + client_id: client_id.to_string(), + redirect_uri: redirect_uri.to_string(), + scopes: scopes.into_iter().map(Into::into).collect(), + should_open_url: false, + message: String::from("Go back to your terminal :)"), + } + } + + /// When this function is added to the building process pipeline, the auth url will be + /// opened with the default web browser. Otherwise, it will be printed to standard output. + pub fn open_in_browser(mut self) -> Self { + self.should_open_url = true; + self + } + + /// When this function is added to the building process pipeline, the body of the response to + /// the callback request will be `message`. This is useful to load custom HTMLs to that &str. + pub fn with_custom_message(mut self, message: &str) -> Self { + self.message = message.to_string(); + self + } + + /// End of the building process pipeline. If Ok, a OAuthClient instance will be returned. + pub fn build(self) -> Result { + let auth_url = AuthUrl::new("https://accounts.spotify.com/authorize".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let token_url = TokenUrl::new("https://accounts.spotify.com/api/token".to_string()) + .map_err(|_| OAuthError::InvalidSpotifyUri)?; + let redirect_url = RedirectUrl::new(self.redirect_uri.clone()).map_err(|e| { + OAuthError::InvalidRedirectUri { + uri: self.redirect_uri.clone(), + e, + } + })?; + + let client = BasicClient::new( + ClientId::new(self.client_id.to_string()), + None, + auth_url, + Some(token_url), + ) + .set_redirect_uri(redirect_url); + + Ok(OAuthClient { + scopes: self.scopes, + should_open_url: self.should_open_url, + message: self.message, + redirect_uri: self.redirect_uri, + client, + }) + } +} + +/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow. +/// The `redirect_uri` must match what is registered to the client ID. +#[deprecated( + since = "0.7.0", + note = "please use builder pattern with `OAuthClientBuilder` instead" +)] /// Obtain a Spotify access token using the authorization code with PKCE OAuth flow. /// The redirect_uri must match what is registered to the client ID. pub fn get_access_token( @@ -204,7 +459,7 @@ pub fn get_access_token( println!("Browse to: {}", auth_url); let code = match get_socket_address(redirect_uri) { - Some(addr) => get_authcode_listener(addr), + Some(addr) => get_authcode_listener(addr, String::from("Go back to your terminal :)")), _ => get_authcode_stdin(), }?; trace!("Exchange {code:?} for access token"); diff --git a/src/main.rs b/src/main.rs index 2d6c9614..f1dd0271 100644 --- a/src/main.rs +++ b/src/main.rs @@ -30,6 +30,7 @@ use librespot::{ player::{coefficient_to_duration, duration_to_coefficient, Player}, }, }; +use librespot_oauth::OAuthClientBuilder; use log::{debug, error, info, trace, warn}; use sha1::{Digest, Sha1}; use sysinfo::{ProcessesToUpdate, System}; @@ -1895,18 +1896,22 @@ async fn main() { Some(port) => format!(":{port}"), _ => String::new(), }; - let access_token = match librespot::oauth::get_access_token( + let client = OAuthClientBuilder::new( &setup.session_config.client_id, &format!("http://127.0.0.1{port_str}/login"), OAUTH_SCOPES.to_vec(), - ) { - Ok(token) => token.access_token, - Err(e) => { - error!("Failed to get Spotify access token: {e}"); - exit(1); - } - }; - last_credentials = Some(Credentials::with_access_token(access_token)); + ) + .open_in_browser() + .build() + .unwrap_or_else(|e| { + error!("Failed to create OAuth client: {e}"); + exit(1); + }); + let oauth_token = client.get_access_token().unwrap_or_else(|e| { + error!("Failed to get Spotify access token: {e}"); + exit(1); + }); + last_credentials = Some(Credentials::with_access_token(oauth_token.access_token)); connecting = true; } else if discovery.is_none() { error!( From 09b4aa41e5da73281408491cefb29ed2d8eb35a0 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sat, 22 Feb 2025 23:39:16 +0100 Subject: [PATCH 501/561] Add rustdoc to connect crate (#1455) * restructure connect and add initial docs * replace inline crate rustdoc with README.md * connect: make metadata trait less visible * connect: add some more docs * chore: remove clippy warning * update CHANGELOG.md * revert unrelated changes * enforce separation of autoplay and options * hide and improve docs of uid * remove/rename remarks * update connect example and link in docs * fixup rebase and clippy warnings --- .gitignore | 1 + CHANGELOG.md | 12 +- connect/README.md | 63 +++++++++ connect/src/lib.rs | 13 +- connect/src/model.rs | 105 +++++++++++++-- connect/src/shuffle_vec.rs | 7 - connect/src/spirc.rs | 245 ++++++++++++++++++++++------------ connect/src/state.rs | 35 ++--- connect/src/state/metadata.rs | 5 +- core/src/authentication.rs | 2 +- core/src/spclient.rs | 19 ++- examples/play_connect.rs | 115 +++++++--------- playback/src/player.rs | 4 +- src/main.rs | 12 +- 14 files changed, 429 insertions(+), 209 deletions(-) create mode 100644 connect/README.md diff --git a/.gitignore b/.gitignore index eebf401d..c9a8b06b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ spotify_appkey.key .vagrant/ .project .history +.cache *.save *.*~ diff --git a/CHANGELOG.md b/CHANGELOG.md index 878999be..3e8682f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] MSRV is now 1.81 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. -- [connect] Replaced `ConnectConfig` with `ConnectStateConfig` (breaking) -- [connect] Replaced `playing_track_index` field of `SpircLoadCommand` with `playing_track` (breaking) +- [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) +- [connect] Changed `initial_volume` from `Option` to `u16` in `ConnectConfig` (breaking) +- [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) +- [connect] Moved all public items to the highest level (breaking) - [connect] Replaced Mercury usage in `Spirc` with Dealer ### Added -- [connect] Add `seek_to` field to `SpircLoadCommand` (breaking) -- [connect] Add `repeat_track` field to `SpircLoadCommand` (breaking) -- [connect] Add `autoplay` field to `SpircLoadCommand` (breaking) +- [connect] Add support for `seek_to`, `repeat_track` and `autoplay` for `Spirc` loading - [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking) +- [connect] Add `volume_steps` to `ConnectConfig` (breaking) +- [connect] Add and enforce rustdoc - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` - [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process diff --git a/connect/README.md b/connect/README.md new file mode 100644 index 00000000..127474d7 --- /dev/null +++ b/connect/README.md @@ -0,0 +1,63 @@ +[//]: # (This readme is optimized for inline rustdoc, if some links don't work, they will when included in lib.rs) + +# Connect + +The connect module of librespot. Provides the option to create your own connect device +and stream to it like any other official spotify client. + +The [`Spirc`] is the entrypoint to creating your own connect device. It can be +configured with the given [`ConnectConfig`] options and requires some additional data +to start up the device. + +When creating a new [`Spirc`] it returns two items. The [`Spirc`] itself, which is can +be used as to control the local connect device. And a [`Future`](std::future::Future), +lets name it `SpircTask`, that starts and executes the event loop of the connect device +when awaited. + +A basic example in which the `Spirc` and `SpircTask` is used can be found here: +[`examples/play_connect.rs`](../examples/play_connect.rs). + +# Example + +```rust +use std::{future::Future, thread}; + +use librespot_connect::{ConnectConfig, Spirc}; +use librespot_core::{authentication::Credentials, Error, Session, SessionConfig}; +use librespot_playback::{ + audio_backend, mixer, + config::{AudioFormat, PlayerConfig}, + mixer::{MixerConfig, NoOpVolume}, + player::Player +}; + +async fn create_basic_spirc() -> Result<(), Error> { + let credentials = Credentials::with_access_token("access-token-here"); + let session = Session::new(SessionConfig::default(), None); + + let backend = audio_backend::find(None).expect("will default to rodio"); + + let player = Player::new( + PlayerConfig::default(), + session.clone(), + Box::new(NoOpVolume), + move || { + let format = AudioFormat::default(); + let device = None; + backend(device, format) + }, + ); + + let mixer = mixer::find(None).expect("will default to SoftMixer"); + + let (spirc, spirc_task): (Spirc, _) = Spirc::new( + ConnectConfig::default(), + session, + credentials, + player, + mixer(MixerConfig::default()) + ).await?; + + Ok(()) +} +``` \ No newline at end of file diff --git a/connect/src/lib.rs b/connect/src/lib.rs index ebceaaac..ba00aa4c 100644 --- a/connect/src/lib.rs +++ b/connect/src/lib.rs @@ -1,3 +1,6 @@ +#![warn(missing_docs)] +#![doc=include_str!("../README.md")] + #[macro_use] extern crate log; @@ -7,6 +10,10 @@ use librespot_protocol as protocol; mod context_resolver; mod model; -pub mod shuffle_vec; -pub mod spirc; -pub mod state; +mod shuffle_vec; +mod spirc; +mod state; + +pub use model::*; +pub use spirc::*; +pub use state::*; diff --git a/connect/src/model.rs b/connect/src/model.rs index 73f86999..5e15b01a 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -1,30 +1,111 @@ -use librespot_core::dealer::protocol::SkipTo; +use crate::{ + core::dealer::protocol::SkipTo, protocol::context_player_options::ContextPlayerOptionOverrides, +}; +use std::ops::Deref; + +/// Request for loading playback #[derive(Debug)] -pub struct SpircLoadCommand { - pub context_uri: String, +pub struct LoadRequest { + pub(super) context_uri: String, + pub(super) options: LoadRequestOptions, +} + +impl Deref for LoadRequest { + type Target = LoadRequestOptions; + + fn deref(&self) -> &Self::Target { + &self.options + } +} + +/// The parameters for creating a load request +#[derive(Debug, Default)] +pub struct LoadRequestOptions { /// Whether the given tracks should immediately start playing, or just be initially loaded. pub start_playing: bool, + /// Start the playback at a specific point of the track. + /// + /// The provided value is used as milliseconds. Providing a value greater + /// than the track duration will start the track at the beginning. pub seek_to: u32, - pub shuffle: bool, - pub repeat: bool, - pub repeat_track: bool, - /// Decides if the context or the autoplay of the context is played + /// Options that decide how the context starts playing + pub context_options: Option, + /// Decides the starting position in the given context. /// - /// ## Remarks: - /// If `true` is provided, the option values (`shuffle`, `repeat` and `repeat_track`) are ignored - pub autoplay: bool, - /// Decides the starting position in the given context + /// If the provided item doesn't exist or is out of range, + /// the playback starts at the beginning of the context. /// - /// ## Remarks: /// If `None` is provided and `shuffle` is `true`, a random track is played, otherwise the first pub playing_track: Option, } +/// The options which decide how the playback is started +/// +/// Separated into an `enum` to exclude the other variants from being used +/// simultaneously, as they are not compatible. +#[derive(Debug)] +pub enum LoadContextOptions { + /// Starts the context with options + Options(Options), + /// Starts the playback as the autoplay variant of the context + /// + /// This is the same as finishing a context and + /// automatically continuing playback of similar tracks + Autoplay, +} + +/// The available options that indicate how to start the context +#[derive(Debug, Default)] +pub struct Options { + /// Start the context in shuffle mode + pub shuffle: bool, + /// Start the context in repeat mode + pub repeat: bool, + /// Start the context, repeating the first track until skipped or manually disabled + pub repeat_track: bool, +} + +impl From for Options { + fn from(value: ContextPlayerOptionOverrides) -> Self { + Self { + shuffle: value.shuffling_context.unwrap_or_default(), + repeat: value.repeating_context.unwrap_or_default(), + repeat_track: value.repeating_track.unwrap_or_default(), + } + } +} + +impl LoadRequest { + /// Create a load request from a `context_uri` + /// + /// For supported `context_uri` see [`SpClient::get_context`](librespot_core::spclient::SpClient::get_context) + pub fn from_context_uri(context_uri: String, options: LoadRequestOptions) -> Self { + Self { + context_uri, + options, + } + } +} + +/// An item that represent a track to play #[derive(Debug)] pub enum PlayingTrack { + /// Represent the track at a given index. Index(u32), + /// Represent the uri of a track. Uri(String), + #[doc(hidden)] + /// Represent an internal identifier from spotify. + /// + /// The internal identifier is not the id contained in the uri. And rather + /// an unrelated id probably unique in spotify's internal database. But that's + /// just speculation. + /// + /// This identifier is not available by any public api. It's used for varies in + /// any spotify client, like sorting, displaying which track is currently played + /// and skipping to a track. Mobile uses it pretty intensively but also web and + /// desktop seem to make use of it. Uid(String), } diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs index b7bb5f3d..84fb6b15 100644 --- a/connect/src/shuffle_vec.rs +++ b/connect/src/shuffle_vec.rs @@ -46,13 +46,6 @@ impl From> for ShuffleVec { } impl ShuffleVec { - pub fn new() -> Self { - Self { - vec: Vec::new(), - indices: None, - } - } - pub fn shuffle_with_seed(&mut self, seed: u64) { self.shuffle_with_rng(SmallRng::seed_from_u64(seed)) } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d1cf9e5b..bcd094ec 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,4 +1,3 @@ -pub use crate::model::{PlayingTrack, SpircLoadCommand}; use crate::{ context_resolver::{ContextAction, ContextResolver, ResolveContext}, core::{ @@ -10,7 +9,7 @@ use crate::{ session::UserAttributes, Error, Session, SpotifyId, }, - model::SpircPlayStatus, + model::{LoadRequest, PlayingTrack, SpircPlayStatus}, playback::{ mixer::Mixer, player::{Player, PlayerEvent, PlayerEventChannel}, @@ -26,10 +25,10 @@ use crate::{ }, state::{ context::{ContextType, ResetContext}, - metadata::Metadata, provider::IsProvider, - {ConnectState, ConnectStateConfig}, + {ConnectConfig, ConnectState}, }, + LoadContextOptions, LoadRequestOptions, }; use futures_util::StreamExt; use protobuf::MessageField; @@ -43,15 +42,13 @@ use thiserror::Error; use tokio::{sync::mpsc, time::sleep}; #[derive(Debug, Error)] -pub enum SpircError { +enum SpircError { #[error("response payload empty")] NoData, #[error("{0} had no uri")] NoUri(&'static str), #[error("message pushed for another URI")] InvalidUri(String), - #[error("tried resolving not allowed context: {0:?}")] - NotAllowedContext(String), #[error("failed to put connect state for new device")] FailedDealerSetup, #[error("unknown endpoint: {0:#?}")] @@ -62,7 +59,7 @@ impl From for Error { fn from(err: SpircError) -> Self { use SpircError::*; match err { - NoData | NoUri(_) | NotAllowedContext(_) => Error::unavailable(err), + NoData | NoUri(_) => Error::unavailable(err), InvalidUri(_) | FailedDealerSetup => Error::aborted(err), UnknownEndpoint(_) => Error::unimplemented(err), } @@ -130,25 +127,30 @@ enum SpircCommand { SetPosition(u32), SetVolume(u16), Activate, - Load(SpircLoadCommand), + Load(LoadRequest), } const CONTEXT_FETCH_THRESHOLD: usize = 2; -const VOLUME_STEP_SIZE: u16 = 1024; // (u16::MAX + 1) / VOLUME_STEPS - // delay to update volume after a certain amount of time, instead on each update request const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2); // to reduce updates to remote, we group some request by waiting for a set amount of time const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200); +/// The spotify connect handle pub struct Spirc { commands: mpsc::UnboundedSender, } impl Spirc { + /// Initializes a new spotify connect device + /// + /// The returned tuple consists out of a handle to the [`Spirc`] that + /// can control the local connect device when active. And a [`Future`] + /// which represents the [`Spirc`] event loop that processes the whole + /// connect device logic. pub async fn new( - config: ConnectStateConfig, + config: ConnectConfig, session: Session, credentials: Credentials, player: Arc, @@ -268,54 +270,132 @@ impl Spirc { Ok((spirc, task.run())) } - pub fn play(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Play)?) - } - pub fn play_pause(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::PlayPause)?) - } - pub fn pause(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Pause)?) - } - pub fn prev(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Prev)?) - } - pub fn next(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Next)?) - } - pub fn volume_up(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::VolumeUp)?) - } - pub fn volume_down(&self) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::VolumeDown)?) - } + /// Safely shutdowns the spirc. + /// + /// This pauses the playback, disconnects the connect device and + /// bring the future initially returned to an end. pub fn shutdown(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Shutdown)?) } + + /// Resumes the playback + /// + /// Does nothing if we are not the active device, or it isn't paused. + pub fn play(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Play)?) + } + + /// Resumes or pauses the playback + /// + /// Does nothing if we are not the active device. + pub fn play_pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::PlayPause)?) + } + + /// Pauses the playback + /// + /// Does nothing if we are not the active device, or if it isn't playing. + pub fn pause(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Pause)?) + } + + /// Seeks to the beginning or skips to the previous track. + /// + /// Seeks to the beginning when the current track position + /// is greater than 3 seconds. + /// + /// Does nothing if we are not the active device. + pub fn prev(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Prev)?) + } + + /// Skips to the next track. + /// + /// Does nothing if we are not the active device. + pub fn next(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Next)?) + } + + /// Increases the volume by configured steps of [ConnectConfig]. + /// + /// Does nothing if we are not the active device. + pub fn volume_up(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeUp)?) + } + + /// Decreases the volume by configured steps of [ConnectConfig]. + /// + /// Does nothing if we are not the active device. + pub fn volume_down(&self) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::VolumeDown)?) + } + + /// Shuffles the playback according to the value. + /// + /// If true shuffles/reshuffles the playback. Otherwise, does + /// nothing (if not shuffled) or unshuffles the playback while + /// resuming at the position of the current track. + /// + /// Does nothing if we are not the active device. pub fn shuffle(&self, shuffle: bool) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Shuffle(shuffle))?) } + + /// Repeats the playback context according to the value. + /// + /// Does nothing if we are not the active device. pub fn repeat(&self, repeat: bool) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Repeat(repeat))?) } + + /// Repeats the current track if true. + /// + /// Does nothing if we are not the active device. + /// + /// Skipping to the next track disables the repeating. pub fn repeat_track(&self, repeat: bool) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::RepeatTrack(repeat))?) } + + /// Update the volume to the given value. + /// + /// Does nothing if we are not the active device. pub fn set_volume(&self, volume: u16) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::SetVolume(volume))?) } + + /// Updates the position to the given value. + /// + /// Does nothing if we are not the active device. + /// + /// If value is greater than the track duration, + /// the update is ignored. pub fn set_position_ms(&self, position_ms: u32) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::SetPosition(position_ms))?) } + + /// Load a new context and replace the current. + /// + /// Does nothing if we are not the active device. + /// + /// Does not overwrite the queue. + pub fn load(&self, command: LoadRequest) -> Result<(), Error> { + Ok(self.commands.send(SpircCommand::Load(command))?) + } + + /// Disconnects the current device and pauses the playback according the value. + /// + /// Does nothing if we are not the active device. pub fn disconnect(&self, pause: bool) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Disconnect { pause })?) } + + /// Acquires the control as active connect device. + /// + /// Does nothing if we are not the active device. pub fn activate(&self) -> Result<(), Error> { Ok(self.commands.send(SpircCommand::Activate)?) } - pub fn load(&self, command: SpircLoadCommand) -> Result<(), Error> { - Ok(self.commands.send(SpircCommand::Load(command))?) - } } impl SpircTask { @@ -529,6 +609,7 @@ impl SpircTask { match cmd { SpircCommand::Shutdown => { trace!("Received SpircCommand::Shutdown"); + self.handle_pause(); self.handle_disconnect().await?; self.shutdown = true; if let Some(rx) = self.commands.as_mut() { @@ -895,42 +976,28 @@ impl SpircTask { return self.notify().await; } Play(play) => { - let shuffle = play - .options - .player_options_override - .as_ref() - .map(|o| o.shuffling_context.unwrap_or_default()) - .unwrap_or_default(); - let repeat = play - .options - .player_options_override - .as_ref() - .map(|o| o.repeating_context.unwrap_or_default()) - .unwrap_or_default(); - let repeat_track = play - .options - .player_options_override - .as_ref() - .map(|o| o.repeating_track.unwrap_or_default()) - .unwrap_or_default(); - let context_uri = play .context .uri .clone() .ok_or(SpircError::NoUri("context"))?; + let context_options = play + .options + .player_options_override + .map(Into::into) + .map(LoadContextOptions::Options); + self.handle_load( - SpircLoadCommand { + LoadRequest::from_context_uri( context_uri, - start_playing: true, - seek_to: play.options.seek_to.unwrap_or_default(), - playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()), - shuffle, - repeat, - repeat_track, - autoplay: false, - }, + LoadRequestOptions { + start_playing: true, + seek_to: play.options.seek_to.unwrap_or_default(), + playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()), + context_options, + }, + ), Some(play.context), ) .await?; @@ -991,7 +1058,7 @@ impl SpircTask { } }; - let autoplay = self.connect_state.current_track(|t| t.is_from_autoplay()); + let autoplay = self.connect_state.current_track(|t| t.is_autoplay()); if autoplay { ctx_uri = ctx_uri.replace("station:", ""); } @@ -1111,7 +1178,7 @@ impl SpircTask { async fn handle_load( &mut self, - cmd: SpircLoadCommand, + cmd: LoadRequest, context: Option, ) -> Result<(), Error> { self.connect_state @@ -1132,7 +1199,7 @@ impl SpircTask { &cmd.context_uri }; - let update_context = if cmd.autoplay { + let update_context = if matches!(cmd.context_options, Some(LoadContextOptions::Autoplay)) { ContextType::Autoplay } else { ContextType::Default @@ -1168,31 +1235,31 @@ impl SpircTask { let index = match cmd.playing_track { None => None, - Some(playing_track) => Some(match playing_track { - PlayingTrack::Index(i) => i as usize, + Some(ref playing_track) => Some(match playing_track { + PlayingTrack::Index(i) => *i as usize, PlayingTrack::Uri(uri) => { let ctx = self.connect_state.get_context(ContextType::Default)?; - ConnectState::find_index_in_context(ctx, |t| t.uri == uri)? + ConnectState::find_index_in_context(ctx, |t| &t.uri == uri)? } PlayingTrack::Uid(uid) => { let ctx = self.connect_state.get_context(ContextType::Default)?; - ConnectState::find_index_in_context(ctx, |t| t.uid == uid)? + ConnectState::find_index_in_context(ctx, |t| &t.uid == uid)? } }), }; - debug!( - "loading with shuffle: <{}>, repeat track: <{}> context: <{}>", - cmd.shuffle, cmd.repeat, cmd.repeat_track - ); + if let Some(LoadContextOptions::Options(ref options)) = cmd.context_options { + debug!( + "loading with shuffle: <{}>, repeat track: <{}> context: <{}>", + options.shuffle, options.repeat, options.repeat_track + ); - self.connect_state.set_shuffle(!cmd.autoplay && cmd.shuffle); - self.connect_state - .set_repeat_context(!cmd.autoplay && cmd.repeat); - self.connect_state - .set_repeat_track(!cmd.autoplay && cmd.repeat_track); + self.connect_state.set_shuffle(options.shuffle); + self.connect_state.set_repeat_context(options.repeat); + self.connect_state.set_repeat_track(options.repeat_track); + } - if cmd.shuffle { + if matches!(cmd.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle) { if let Some(index) = index { self.connect_state.set_current_track(index)?; } else { @@ -1285,6 +1352,12 @@ impl SpircTask { } fn handle_seek(&mut self, position_ms: u32) { + let duration = self.connect_state.player().duration; + if i64::from(position_ms) > duration { + warn!("tried to seek to {position_ms}ms of {duration}ms"); + return; + } + self.connect_state .update_position(position_ms, self.now_ms()); self.player.seek(position_ms); @@ -1422,14 +1495,16 @@ impl SpircTask { } fn handle_volume_up(&mut self) { - let volume = - (self.connect_state.device_info().volume as u16).saturating_add(VOLUME_STEP_SIZE); + let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16; + + let volume = (self.connect_state.device_info().volume as u16).saturating_add(volume_steps); self.set_volume(volume); } fn handle_volume_down(&mut self) { - let volume = - (self.connect_state.device_info().volume as u16).saturating_sub(VOLUME_STEP_SIZE); + let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16; + + let volume = (self.connect_state.device_info().volume as u16).saturating_sub(volume_steps); self.set_volume(volume); } diff --git a/connect/src/state.rs b/connect/src/state.rs index 73010b25..047f3c8c 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -1,6 +1,6 @@ pub(super) mod context; mod handle; -pub mod metadata; +mod metadata; mod options; pub(super) mod provider; mod restrictions; @@ -40,7 +40,7 @@ const SPOTIFY_MAX_PREV_TRACKS_SIZE: usize = 10; const SPOTIFY_MAX_NEXT_TRACKS_SIZE: usize = 80; #[derive(Debug, Error)] -pub enum StateError { +pub(super) enum StateError { #[error("the current track couldn't be resolved from the transfer state")] CouldNotResolveTrackFromTransfer, #[error("context is not available. type: {0:?}")] @@ -74,33 +74,38 @@ impl From for Error { } } +/// Configuration of the connect device #[derive(Debug, Clone)] -pub struct ConnectStateConfig { - pub session_id: String, - pub initial_volume: u32, +pub struct ConnectConfig { + /// The name of the connect device (default: librespot) pub name: String, + /// The icon type of the connect device (default: [DeviceType::Speaker]) pub device_type: DeviceType, - pub volume_steps: i32, + /// Displays the [DeviceType] twice in the ui to show up as a group (default: false) pub is_group: bool, + /// The volume with which the connect device will be initialized (default: 50%) + pub initial_volume: u16, + /// Disables the option to control the volume remotely (default: false) pub disable_volume: bool, + /// The steps in which the volume is incremented (default: 1024) + pub volume_steps: u16, } -impl Default for ConnectStateConfig { +impl Default for ConnectConfig { fn default() -> Self { Self { - session_id: String::new(), - initial_volume: u32::from(u16::MAX) / 2, name: "librespot".to_string(), device_type: DeviceType::Speaker, - volume_steps: 64, is_group: false, + initial_volume: u16::MAX / 2, disable_volume: false, + volume_steps: 1024, } } } #[derive(Default, Debug)] -pub struct ConnectState { +pub(super) struct ConnectState { /// the entire state that is updated to the remote server request: PutStateRequest, @@ -125,10 +130,10 @@ pub struct ConnectState { } impl ConnectState { - pub fn new(cfg: ConnectStateConfig, session: &Session) -> Self { + pub fn new(cfg: ConnectConfig, session: &Session) -> Self { let device_info = DeviceInfo { can_play: true, - volume: cfg.initial_volume, + volume: cfg.initial_volume.into(), name: cfg.name, device_id: session.device_id().to_string(), device_type: EnumOrUnknown::new(cfg.device_type.into()), @@ -137,7 +142,7 @@ impl ConnectState { client_id: session.client_id(), is_group: cfg.is_group, capabilities: MessageField::some(Capabilities { - volume_steps: cfg.volume_steps, + volume_steps: cfg.volume_steps.into(), disable_volume: cfg.disable_volume, gaia_eq_connect_id: true, @@ -183,7 +188,7 @@ impl ConnectState { device: MessageField::some(Device { device_info: MessageField::some(device_info), player_state: MessageField::some(PlayerState { - session_id: cfg.session_id, + session_id: session.session_id(), ..Default::default() }), ..Default::default() diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index 763244b7..8212c276 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -24,6 +24,7 @@ macro_rules! metadata_entry { self.$get($entry) } + fn $set (&mut self, $key: impl Display) { self.metadata_mut().insert($entry.to_string(), $key.to_string()); } @@ -34,9 +35,11 @@ macro_rules! metadata_entry { }; } +/// Allows easy access of known metadata fields #[allow(dead_code)] -pub trait Metadata { +pub(super) trait Metadata { fn metadata(&self) -> &HashMap; + fn metadata_mut(&mut self) -> &mut HashMap; fn get_bool(&self, entry: &str) -> bool { diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 230661ef..8dd68540 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -84,7 +84,7 @@ impl Credentials { } let hi = read_u8(stream)? as u32; - Ok(lo & 0x7f | hi << 7) + Ok(lo & 0x7f | (hi << 7)) } fn read_bytes(stream: &mut R) -> io::Result> { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index a6680463..4377d406 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -804,20 +804,27 @@ impl SpClient { /// Request the context for an uri /// - /// ## Query entry found in the wild: + /// All [SpotifyId] uris are supported in addition to the following special uris: + /// - liked songs: + /// - all: `spotify:user::collection` + /// - of artist: `spotify:user::collection:artist:` + /// - search: `spotify:search:` (whitespaces are replaced with `+`) + /// + /// ## Query params found in the wild: /// - include_video=true - /// ## Remarks: - /// - track + /// + /// ## Known results of uri types: + /// - uris of type `track` /// - returns a single page with a single track /// - when requesting a single track with a query in the request, the returned track uri /// **will** contain the query - /// - artists + /// - uris of type `artist` /// - returns 2 pages with tracks: 10 most popular tracks and latest/popular album /// - remaining pages are artist albums sorted by popularity (only provided as page_url) - /// - search + /// - uris of type `search` /// - is massively influenced by the provided query /// - the query result shown by the search expects no query at all - /// - uri looks like "spotify:search:never+gonna" + /// - uri looks like `spotify:search:never+gonna` pub async fn get_context(&self, uri: &str) -> Result { let uri = format!("/context-resolve/v1/{uri}"); diff --git a/examples/play_connect.rs b/examples/play_connect.rs index 8ea7eaca..26e52022 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -1,92 +1,77 @@ use librespot::{ + connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc}, core::{ - authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, Error, }, + playback::mixer::MixerConfig, playback::{ audio_backend, config::{AudioFormat, PlayerConfig}, - mixer::NoOpVolume, + mixer, player::Player, }, }; -use librespot_connect::spirc::PlayingTrack; -use librespot_connect::{ - spirc::{Spirc, SpircLoadCommand}, - state::ConnectStateConfig, -}; -use librespot_metadata::{Album, Metadata}; -use librespot_playback::mixer::{softmixer::SoftMixer, Mixer, MixerConfig}; -use std::env; -use std::sync::Arc; -use tokio::join; + +use log::LevelFilter; + +const CACHE: &str = ".cache"; +const CACHE_FILES: &str = ".cache/files"; #[tokio::main] -async fn main() { +async fn main() -> Result<(), Error> { + env_logger::builder() + .filter_module("librespot", LevelFilter::Debug) + .init(); + let session_config = SessionConfig::default(); let player_config = PlayerConfig::default(); let audio_format = AudioFormat::default(); - let connect_config = ConnectStateConfig::default(); + let connect_config = ConnectConfig::default(); + let mixer_config = MixerConfig::default(); + let request_options = LoadRequestOptions::default(); - let mut args: Vec<_> = env::args().collect(); - let context_uri = if args.len() == 3 { - args.pop().unwrap() - } else if args.len() == 2 { - String::from("spotify:album:79dL7FLiJFOO0EoehUHQBv") - } else { - eprintln!("Usage: {} ACCESS_TOKEN (ALBUM URI)", args[0]); - return; - }; + let sink_builder = audio_backend::find(None).unwrap(); + let mixer_builder = mixer::find(None).unwrap(); - let credentials = Credentials::with_access_token(&args[1]); - let backend = audio_backend::find(None).unwrap(); + let cache = Cache::new(Some(CACHE), Some(CACHE), Some(CACHE_FILES), None)?; + let credentials = cache + .credentials() + .ok_or(Error::unavailable("credentials not cached")) + .or_else(|_| { + librespot_oauth::OAuthClientBuilder::new( + &session_config.client_id, + "http://127.0.0.1:8898/login", + vec!["streaming"], + ) + .open_in_browser() + .build()? + .get_access_token() + .map(|t| Credentials::with_access_token(t.access_token)) + })?; - println!("Connecting..."); - let session = Session::new(session_config, None); + let session = Session::new(session_config, Some(cache)); + let mixer = mixer_builder(mixer_config); let player = Player::new( player_config, session.clone(), - Box::new(NoOpVolume), - move || backend(None, audio_format), + mixer.get_soft_volume(), + move || sink_builder(None, audio_format), ); - let (spirc, spirc_task) = Spirc::new( - connect_config, - session.clone(), - credentials, - player, - Arc::new(SoftMixer::open(MixerConfig::default())), - ) - .await - .unwrap(); + let (spirc, spirc_task) = + Spirc::new(connect_config, session.clone(), credentials, player, mixer).await?; - join!(spirc_task, async { - let album = Album::get(&session, &SpotifyId::from_uri(&context_uri).unwrap()) - .await - .unwrap(); + // these calls can be seen as "queued" + spirc.activate()?; + spirc.load(LoadRequest::from_context_uri( + format!("spotify:user:{}:collection", session.username()), + request_options, + ))?; + spirc.play()?; - println!( - "Playing album: {} by {}", - &album.name, - album - .artists - .first() - .map_or("unknown", |artist| &artist.name) - ); + // starting the connect device and processing the previously "queued" calls + spirc_task.await; - spirc.activate().unwrap(); - spirc - .load(SpircLoadCommand { - context_uri, - start_playing: true, - seek_to: 0, - shuffle: false, - repeat: false, - repeat_track: false, - autoplay: false, - // the index specifies which track in the context starts playing, in this case the first in the album - playing_track: PlayingTrack::Index(0).into(), - }) - .unwrap(); - }); + Ok(()) } diff --git a/playback/src/player.rs b/playback/src/player.rs index e9663a70..a42bda14 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -2238,9 +2238,7 @@ impl PlayerInternal { let wait_for_data_length = (read_ahead_during_playback.as_secs_f32() * bytes_per_second as f32) as usize; - stream_loader_controller - .fetch_next_and_wait(request_data_length, wait_for_data_length) - .map_err(Into::into) + stream_loader_controller.fetch_next_and_wait(request_data_length, wait_for_data_length) } else { Ok(()) } diff --git a/src/main.rs b/src/main.rs index f1dd0271..0447bda7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,7 +14,7 @@ use futures_util::StreamExt; #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::{ - connect::{spirc::Spirc, state::ConnectStateConfig}, + connect::{ConnectConfig, Spirc}, core::{ authentication::Credentials, cache::Cache, config::DeviceType, version, Session, SessionConfig, @@ -208,7 +208,7 @@ struct Setup { cache: Option, player_config: PlayerConfig, session_config: SessionConfig, - connect_config: ConnectStateConfig, + connect_config: ConnectConfig, mixer_config: MixerConfig, credentials: Option, enable_oauth: bool, @@ -1371,7 +1371,7 @@ fn get_setup() -> Setup { }); let connect_config = { - let connect_default_config = ConnectStateConfig::default(); + let connect_default_config = ConnectConfig::default(); let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); @@ -1483,15 +1483,15 @@ fn get_setup() -> Setup { let is_group = opt_present(DEVICE_IS_GROUP); if let Some(initial_volume) = initial_volume { - ConnectStateConfig { + ConnectConfig { name, device_type, is_group, - initial_volume: initial_volume.into(), + initial_volume, ..Default::default() } } else { - ConnectStateConfig { + ConnectConfig { name, device_type, is_group, From 11c3df8eb1ab2c6a8f31a02cb8833caf825f415b Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 3 Mar 2025 15:06:48 +0100 Subject: [PATCH 502/561] removes homething devicetype (#1471) * removes homething devicetype fixes #1470 From the comments about "homething" support in spotify: "Did they ever release their homething product? I assumed not and guess they removed all support for it. We could just remove this footgun." * mention breaking homething change in Changelog --- CHANGELOG.md | 1 + core/src/config.rs | 4 ---- src/main.rs | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e8682f9..face1c8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Removed - [core] Removed `get_canvases` from SpClient (breaking) +- [core] DeviceType `homething` removed due to crashes on Android (breaking) - [metadata] Removed `genres` from Album (breaking) - [metadata] Removed `genre` from Artists (breaking) diff --git a/core/src/config.rs b/core/src/config.rs index 0b17690e..1b81123c 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -79,7 +79,6 @@ pub enum DeviceType { UnknownSpotify = 100, CarThing = 101, Observer = 102, - HomeThing = 103, } impl FromStr for DeviceType { @@ -102,7 +101,6 @@ impl FromStr for DeviceType { "smartwatch" => Ok(Smartwatch), "chromebook" => Ok(Chromebook), "carthing" => Ok(CarThing), - "homething" => Ok(HomeThing), _ => Err(()), } } @@ -130,7 +128,6 @@ impl From<&DeviceType> for &str { UnknownSpotify => "UnknownSpotify", CarThing => "CarThing", Observer => "Observer", - HomeThing => "HomeThing", } } } @@ -169,7 +166,6 @@ impl From for ProtoDeviceType { DeviceType::UnknownSpotify => ProtoDeviceType::UNKNOWN_SPOTIFY, DeviceType::CarThing => ProtoDeviceType::CAR_THING, DeviceType::Observer => ProtoDeviceType::OBSERVER, - DeviceType::HomeThing => ProtoDeviceType::HOME_THING, } } } diff --git a/src/main.rs b/src/main.rs index 0447bda7..b14a57a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1471,7 +1471,7 @@ fn get_setup() -> Setup { speaker, tv, avr, stb, audiodongle, \ gameconsole, castaudio, castvideo, \ automobile, smartwatch, chromebook, \ - carthing, homething", + carthing", DeviceType::default().into(), ); From 837b3e6d1a248cdac881d8538f76c1d297a80701 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sat, 22 Mar 2025 20:17:46 +0100 Subject: [PATCH 503/561] Emit shuffle and repeat events again (#1469) * emit shuffle and repeat events again * connect: split context/track repeat handling --- connect/src/spirc.rs | 47 ++++++++++++++++++++++++++----------- connect/src/state/handle.rs | 22 +++-------------- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index bcd094ec..ee192aaf 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -641,9 +641,9 @@ impl SpircTask { SpircCommand::Next => self.handle_next(None)?, SpircCommand::VolumeUp => self.handle_volume_up(), SpircCommand::VolumeDown => self.handle_volume_down(), - SpircCommand::Shuffle(shuffle) => self.connect_state.handle_shuffle(shuffle)?, - SpircCommand::Repeat(repeat) => self.connect_state.set_repeat_context(repeat), - SpircCommand::RepeatTrack(repeat) => self.connect_state.set_repeat_track(repeat), + SpircCommand::Shuffle(shuffle) => self.handle_shuffle(shuffle)?, + SpircCommand::Repeat(repeat) => self.handle_repeat_context(repeat)?, + SpircCommand::RepeatTrack(repeat) => self.handle_repeat_track(repeat), SpircCommand::SetPosition(position) => self.handle_seek(position), SpircCommand::SetVolume(volume) => self.set_volume(volume), SpircCommand::Load(command) => self.handle_load(command, None).await?, @@ -1010,23 +1010,25 @@ impl SpircTask { trace!("seek to {seek_to:?}"); self.handle_seek(seek_to.value) } - SetShufflingContext(shuffle) => self.connect_state.handle_shuffle(shuffle.value)?, - SetRepeatingContext(repeat_context) => self - .connect_state - .handle_set_repeat(Some(repeat_context.value), None)?, - SetRepeatingTrack(repeat_track) => self - .connect_state - .handle_set_repeat(None, Some(repeat_track.value))?, + SetShufflingContext(shuffle) => self.handle_shuffle(shuffle.value)?, + SetRepeatingContext(repeat_context) => { + self.handle_repeat_context(repeat_context.value)? + } + SetRepeatingTrack(repeat_track) => self.handle_repeat_track(repeat_track.value), AddToQueue(add_to_queue) => self.connect_state.add_to_queue(add_to_queue.track, true), SetQueue(set_queue) => self.connect_state.handle_set_queue(set_queue), SetOptions(set_options) => { - let context = set_options.repeating_context; - let track = set_options.repeating_track; - self.connect_state.handle_set_repeat(context, track)?; + if let Some(repeat_context) = set_options.repeating_context { + self.handle_repeat_context(repeat_context)? + } + + if let Some(repeat_track) = set_options.repeating_track { + self.handle_repeat_track(repeat_track) + } let shuffle = set_options.shuffling_context; if let Some(shuffle) = shuffle { - self.connect_state.handle_shuffle(shuffle)?; + self.handle_shuffle(shuffle)?; } } SkipNext(skip_next) => self.handle_next(skip_next.track.map(|t| t.uri))?, @@ -1381,6 +1383,23 @@ impl SpircTask { }; } + fn handle_shuffle(&mut self, shuffle: bool) -> Result<(), Error> { + self.player.emit_shuffle_changed_event(shuffle); + self.connect_state.handle_shuffle(shuffle) + } + + fn handle_repeat_context(&mut self, repeat: bool) -> Result<(), Error> { + self.player + .emit_repeat_changed_event(repeat, self.connect_state.repeat_track()); + self.connect_state.handle_set_repeat_context(repeat) + } + + fn handle_repeat_track(&mut self, repeat: bool) { + self.player + .emit_repeat_changed_event(self.connect_state.repeat_context(), repeat); + self.connect_state.set_repeat_track(repeat); + } + fn handle_preload_next_track(&mut self) { // Requests the player thread to preload the next track match self.play_status { diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index 659ed92c..99840800 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -40,26 +40,10 @@ impl ConnectState { self.update_queue_revision(); } - pub fn handle_set_repeat( - &mut self, - context: Option, - track: Option, - ) -> Result<(), Error> { - // doesn't need any state updates, because it should only change how the current song is played - if let Some(track) = track { - self.set_repeat_track(track); - } + pub fn handle_set_repeat_context(&mut self, repeat: bool) -> Result<(), Error> { + self.set_repeat_context(repeat); - if matches!(context, Some(context) if self.repeat_context() == context) || context.is_none() - { - return Ok(()); - } - - if let Some(context) = context { - self.set_repeat_context(context); - } - - if self.repeat_context() { + if repeat { self.set_shuffle(false); self.reset_context(ResetContext::DefaultIndex); From 5981b88ac50876d86c882c2cf817dfe1d0427208 Mon Sep 17 00:00:00 2001 From: Rudi Heitbaum Date: Tue, 1 Apr 2025 02:24:03 +1100 Subject: [PATCH 504/561] Update Cargo.lock (#1480) Fixes build with CMake 4.0.0 --- Cargo.lock | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f861b91..fee06376 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,27 +189,25 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-lc-rs" -version = "1.12.0" +version = "1.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f409eb70b561706bf8abba8ca9c112729c481595893fd06a2dd9af8ed8441148" +checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" dependencies = [ "aws-lc-sys", - "paste", "zeroize", ] [[package]] name = "aws-lc-sys" -version = "0.24.1" +version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "923ded50f602b3007e5e63e3f094c479d9c8a9b42d7f4034e4afe456aa48bfd2" +checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" dependencies = [ "bindgen 0.69.5", "cc", "cmake", "dunce", "fs_extra", - "paste", ] [[package]] From cb958a07f22494294ee856be57d5e248f9c4383c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 6 Apr 2025 20:37:19 +0200 Subject: [PATCH 505/561] Saturate seek position to track duration (#1483) The fix prevents seeking past the end of a track by capping the requested seek position to the actual duration. --- CHANGELOG.md | 1 + playback/src/decoder/symphonia_decoder.rs | 29 +++++++++++++---------- playback/src/player.rs | 2 +- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index face1c8c..25a0d8d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking) - [connect] Handle transfer of playback with empty "uri" field - [connect] Correctly apply playing/paused state when transferring playback +- [player] Saturate invalid seek positions to track duration ### Deprecated diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index b6a8023e..4df6882d 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -1,4 +1,4 @@ -use std::io; +use std::{io, time::Duration}; use symphonia::{ core::{ @@ -8,7 +8,6 @@ use symphonia::{ formats::{FormatOptions, FormatReader, SeekMode, SeekTo}, io::{MediaSource, MediaSourceStream, MediaSourceStreamOptions}, meta::{StandardTagKey, Value}, - units::Time, }, default::{ codecs::{MpaDecoder, VorbisDecoder}, @@ -133,30 +132,34 @@ 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 { + match self.decoder.codec_params().time_base { Some(time_base) => { - let time = time_base.calc_time(ts); - (time.seconds as f64 + time.frac) * 1000. + let time = Duration::from(time_base.calc_time(ts)); + time.as_millis() as u32 } // 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 + None => (ts as f64 * PAGES_PER_MS) as u32, + } } } impl AudioDecoder for SymphoniaDecoder { 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); + // "Saturate" the position_ms to the duration of the track if it exceeds it. + let mut target = Duration::from_millis(position_ms.into()); + let codec_params = self.decoder.codec_params(); + if let (Some(time_base), Some(n_frames)) = (codec_params.time_base, codec_params.n_frames) { + let duration = Duration::from(time_base.calc_time(n_frames)); + if target > duration { + target = duration; + } + } // `track_id: None` implies the default track ID (of the container, not of Spotify). let seeked_to_ts = self.format.seek( SeekMode::Accurate, SeekTo::Time { - time, + time: target.into(), track_id: None, }, )?; diff --git a/playback/src/player.rs b/playback/src/player.rs index a42bda14..2c81ca18 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1780,7 +1780,7 @@ impl PlayerInternal { self.ensure_sink_stopped(play); } - if matches!(self.state, PlayerState::Invalid { .. }) { + if matches!(self.state, PlayerState::Invalid) { return Err(Error::internal(format!( "Player::handle_command_load called from invalid state: {:?}", self.state From 59381ccad38ed39037392f3d2d30bf0d9593ff56 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 13 Apr 2025 08:37:12 +0200 Subject: [PATCH 506/561] Handle/Ignore unknown audio formats (#1467) * ignore unknown enum values * update CHANGELOG.md * metadata: introduce own AudioFileFormat * metadata: adjust handling for better readability --- CHANGELOG.md | 2 + metadata/src/audio/file.rs | 85 ++++++++++++++++++++++++++++++++++---- playback/src/player.rs | 4 ++ 3 files changed, 84 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25a0d8d8..bf3b3057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) - [connect] Moved all public items to the highest level (breaking) - [connect] Replaced Mercury usage in `Spirc` with Dealer +- [metadata] Replaced `AudioFileFormat` with own enum. (breaking) ### Added @@ -39,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Fix "play" command not handled if missing "offset" property - [discovery] Fix libmdns zerconf setup errors not propagating to the main task. - [metadata] `Show::trailer_uri` is now optional since it isn't always present (breaking) +- [metadata] Fix incorrect parsing of audio format - [connect] Handle transfer of playback with empty "uri" field - [connect] Correctly apply playing/paused state when transferring playback - [player] Saturate invalid seek positions to track duration diff --git a/metadata/src/audio/file.rs b/metadata/src/audio/file.rs index e8fe822b..cf70a88d 100644 --- a/metadata/src/audio/file.rs +++ b/metadata/src/audio/file.rs @@ -6,11 +6,73 @@ use std::{ use librespot_core::FileId; +use crate::util::impl_deref_wrapped; use librespot_protocol as protocol; -pub use protocol::metadata::audio_file::Format as AudioFileFormat; use protocol::metadata::AudioFile as AudioFileMessage; -use crate::util::impl_deref_wrapped; +use librespot_protocol::metadata::audio_file::Format; +use protobuf::Enum; + +#[allow(non_camel_case_types)] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub enum AudioFileFormat { + 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 + XHE_AAC_24, // 18 + XHE_AAC_16, // 19 + XHE_AAC_12, // 20 + FLAC_FLAC_24BIT, // 22 + // not defined in protobuf, but sometimes send + AAC_160, // 10 + AAC_320, // 11 + MP4_128, // 12 + OTHER5, // 13 +} + +impl TryFrom for AudioFileFormat { + type Error = i32; + + fn try_from(value: i32) -> Result { + Ok(match value { + 10 => AudioFileFormat::AAC_160, + 11 => AudioFileFormat::AAC_320, + 12 => AudioFileFormat::MP4_128, + 13 => AudioFileFormat::OTHER5, + _ => Format::from_i32(value).ok_or(value)?.into(), + }) + } +} + +impl From for AudioFileFormat { + fn from(value: Format) -> Self { + match value { + Format::OGG_VORBIS_96 => AudioFileFormat::OGG_VORBIS_96, + Format::OGG_VORBIS_160 => AudioFileFormat::OGG_VORBIS_160, + Format::OGG_VORBIS_320 => AudioFileFormat::OGG_VORBIS_320, + Format::MP3_256 => AudioFileFormat::MP3_256, + Format::MP3_320 => AudioFileFormat::MP3_320, + Format::MP3_160 => AudioFileFormat::MP3_160, + Format::MP3_96 => AudioFileFormat::MP3_96, + Format::MP3_160_ENC => AudioFileFormat::MP3_160_ENC, + Format::AAC_24 => AudioFileFormat::AAC_24, + Format::AAC_48 => AudioFileFormat::AAC_48, + Format::FLAC_FLAC => AudioFileFormat::FLAC_FLAC, + Format::XHE_AAC_24 => AudioFileFormat::XHE_AAC_24, + Format::XHE_AAC_16 => AudioFileFormat::XHE_AAC_16, + Format::XHE_AAC_12 => AudioFileFormat::XHE_AAC_12, + Format::FLAC_FLAC_24BIT => AudioFileFormat::FLAC_FLAC_24BIT, + } + } +} #[derive(Debug, Clone, Default)] pub struct AudioFiles(pub HashMap); @@ -49,12 +111,21 @@ impl From<&[AudioFileMessage]> for AudioFiles { .iter() .filter_map(|file| { let file_id = FileId::from(file.file_id()); - if file.has_format() { - Some((file.format(), file_id)) - } else { - trace!("Ignoring file <{}> with unspecified format", file_id); - None + let format = file + .format + .ok_or(format!("Ignoring file <{file_id}> with unspecified format",)) + .and_then(|format| match format.enum_value() { + Ok(f) => Ok((f.into(), file_id)), + Err(unknown) => Err(format!( + "Ignoring file <{file_id}> with unknown format {unknown}", + )), + }); + + if let Err(ref why) = format { + trace!("{why}"); } + + format.ok() }) .collect(); diff --git a/playback/src/player.rs b/playback/src/player.rs index 2c81ca18..296c26e5 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -917,6 +917,10 @@ impl PlayerTrackLoader { AudioFileFormat::MP3_160_ENC => 20., AudioFileFormat::AAC_24 => 3., AudioFileFormat::AAC_48 => 6., + AudioFileFormat::AAC_160 => 20., + AudioFileFormat::AAC_320 => 40., + AudioFileFormat::MP4_128 => 16., + AudioFileFormat::OTHER5 => 40., AudioFileFormat::FLAC_FLAC => 112., // assume 900 kbit/s on average AudioFileFormat::XHE_AAC_12 => 1.5, AudioFileFormat::XHE_AAC_16 => 2., From 6bdc0eb3128d7e86eb2a11c74f70d70e6cfefbff Mon Sep 17 00:00:00 2001 From: "Scott S. McCoy" Date: Thu, 1 May 2025 22:19:47 +0100 Subject: [PATCH 507/561] spirc: Configurable volume control steps (#1498) * spirc: Configurable volume control steps Allow the volume control steps to be configured via the `--volume-steps` command line parameter. The author personally found the default volume steps of `1024` to be completely unusable, and is presently using `128` as his configuration. Perhaps consider this as a more reasonable default. Additionally, reduce the delay in volume update from a wopping two seconds to 500ms, again for usability. Also clean up the seemingly unnecessary use of a pattern match on whether or not `--initial-volume` was supplied. * fixup! spirc: Configurable volume control steps * fixup! spirc: Configurable volume control steps * fixup! spirc: Configurable volume control steps * fixup! spirc: Configurable volume control steps * fixup! spirc: Configurable volume control steps * fixup! spirc: Configurable volume control steps --------- Co-authored-by: Scott S. McCoy --- CHANGELOG.md | 1 + connect/src/spirc.rs | 12 ++++++---- connect/src/state.rs | 10 ++++++-- src/main.rs | 54 +++++++++++++++++++++++++++++++------------- 4 files changed, 54 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bf3b3057..84b34578 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- [connect] Add command line parameter for setting volume steps. - [connect] Add support for `seek_to`, `repeat_track` and `autoplay` for `Spirc` loading - [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking) - [connect] Add `volume_steps` to `ConnectConfig` (breaking) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index ee192aaf..95b48a8c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -133,7 +133,7 @@ enum SpircCommand { const CONTEXT_FETCH_THRESHOLD: usize = 2; // delay to update volume after a certain amount of time, instead on each update request -const VOLUME_UPDATE_DELAY: Duration = Duration::from_secs(2); +const VOLUME_UPDATE_DELAY: Duration = Duration::from_millis(500); // to reduce updates to remote, we group some request by waiting for a set amount of time const UPDATE_STATE_DELAY: Duration = Duration::from_millis(200); @@ -1514,16 +1514,16 @@ impl SpircTask { } fn handle_volume_up(&mut self) { - let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16; + let volume = (self.connect_state.device_info().volume as u16) + .saturating_add(self.connect_state.volume_step_size); - let volume = (self.connect_state.device_info().volume as u16).saturating_add(volume_steps); self.set_volume(volume); } fn handle_volume_down(&mut self) { - let volume_steps = self.connect_state.device_info().capabilities.volume_steps as u16; + let volume = (self.connect_state.device_info().volume as u16) + .saturating_sub(self.connect_state.volume_step_size); - let volume = (self.connect_state.device_info().volume as u16).saturating_sub(volume_steps); self.set_volume(volume); } @@ -1639,6 +1639,8 @@ impl SpircTask { } fn set_volume(&mut self, volume: u16) { + debug!("SpircTask::set_volume({})", volume); + let old_volume = self.connect_state.device_info().volume; let new_volume = volume as u32; if old_volume != new_volume || self.mixer.volume() != volume { diff --git a/connect/src/state.rs b/connect/src/state.rs index 047f3c8c..64a1e14a 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -87,7 +87,7 @@ pub struct ConnectConfig { pub initial_volume: u16, /// Disables the option to control the volume remotely (default: false) pub disable_volume: bool, - /// The steps in which the volume is incremented (default: 1024) + /// Number of incremental steps (default: 64) pub volume_steps: u16, } @@ -99,7 +99,7 @@ impl Default for ConnectConfig { is_group: false, initial_volume: u16::MAX / 2, disable_volume: false, - volume_steps: 1024, + volume_steps: 64, } } } @@ -127,10 +127,15 @@ pub(super) struct ConnectState { /// a context to keep track of the autoplay context autoplay_context: Option, + + /// The volume adjustment per step when handling individual volume adjustments. + pub volume_step_size: u16, } impl ConnectState { pub fn new(cfg: ConnectConfig, session: &Session) -> Self { + let volume_step_size = u16::MAX.checked_div(cfg.volume_steps).unwrap_or(1024); + let device_info = DeviceInfo { can_play: true, volume: cfg.initial_volume.into(), @@ -195,6 +200,7 @@ impl ConnectState { }), ..Default::default() }, + volume_step_size, ..Default::default() }; state.reset(); diff --git a/src/main.rs b/src/main.rs index b14a57a4..6169fac5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -276,6 +276,7 @@ fn get_setup() -> Setup { const VERSION: &str = "version"; const VOLUME_CTRL: &str = "volume-ctrl"; const VOLUME_RANGE: &str = "volume-range"; + const VOLUME_STEPS: &str = "volume-steps"; const ZEROCONF_PORT: &str = "zeroconf-port"; const ZEROCONF_INTERFACE: &str = "zeroconf-interface"; const ZEROCONF_BACKEND: &str = "zeroconf-backend"; @@ -291,6 +292,7 @@ fn get_setup() -> Setup { const DEVICE_SHORT: &str = "d"; const VOLUME_CTRL_SHORT: &str = "E"; const VOLUME_RANGE_SHORT: &str = "e"; + const VOLUME_STEPS_SHORT: &str = ""; // no short flag const DEVICE_TYPE_SHORT: &str = "F"; const FORMAT_SHORT: &str = "f"; const DISABLE_AUDIO_CACHE_SHORT: &str = "G"; @@ -371,6 +373,8 @@ fn get_setup() -> Setup { #[cfg(not(feature = "alsa-backend"))] const VOLUME_RANGE_DESC: &str = "Range of the volume control (dB) from 0.0 to 100.0. Defaults to 60.0."; + const VOLUME_STEPS_DESC: &str = + "Number of incremental steps when responding to volume control updates. Defaults to 64."; let mut opts = getopts::Options::new(); opts.optflag( @@ -570,6 +574,12 @@ fn get_setup() -> Setup { VOLUME_RANGE_DESC, "RANGE", ) + .optopt( + VOLUME_STEPS_SHORT, + VOLUME_STEPS, + VOLUME_STEPS_DESC, + "STEPS", + ) .optopt( NORMALISATION_METHOD_SHORT, NORMALISATION_METHOD, @@ -1457,7 +1467,8 @@ fn get_setup() -> Setup { } else { cache.as_ref().and_then(Cache::volume) } - }); + }) + .unwrap_or_default(); let device_type = opt_str(DEVICE_TYPE) .as_deref() @@ -1480,23 +1491,34 @@ fn get_setup() -> Setup { }) .unwrap_or_default(); + let volume_steps = opt_str(VOLUME_STEPS) + .map(|steps| match steps.parse::() { + Ok(value) => value, + _ => { + let default_value = &connect_default_config.volume_steps.to_string(); + + invalid_error_msg( + VOLUME_STEPS, + VOLUME_STEPS_SHORT, + &steps, + "a positive whole number <= 65535", + default_value, + ); + + exit(1); + } + }) + .unwrap_or_else(|| connect_default_config.volume_steps); + let is_group = opt_present(DEVICE_IS_GROUP); - if let Some(initial_volume) = initial_volume { - ConnectConfig { - name, - device_type, - is_group, - initial_volume, - ..Default::default() - } - } else { - ConnectConfig { - name, - device_type, - is_group, - ..Default::default() - } + ConnectConfig { + name, + device_type, + is_group, + initial_volume, + volume_steps, + ..connect_default_config } }; From d12e1b8549b9f30ac6ad24fa22e11cfd08e837d7 Mon Sep 17 00:00:00 2001 From: Thomas Schlage Date: Sat, 3 May 2025 23:37:17 +0200 Subject: [PATCH 508/561] Send playback position as player event (#1495) * Send play progress as PlayerEvent::PositionChanged * Replaced PlayerEvent::PositionChanged with set_progress_callback() method * Revert "Replaced PlayerEvent::PositionChanged with set_progress_callback() method" This reverts commit f26e3de07b667fd764416a79ae682882063e0688. * Added opt-in config in PlayerConfig for progress event * Added doc comments and set default position interval to 1sec for standalone * Remove handling of PositionChanged in standalone binary * Fixed wrong event handling --- CHANGELOG.md | 1 + playback/src/config.rs | 4 ++++ playback/src/player.rs | 26 ++++++++++++++++++++++++++ src/main.rs | 1 + src/player_event_handler.rs | 2 ++ 5 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84b34578..cca28afe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Add `volume_steps` to `ConnectConfig` (breaking) - [connect] Add and enforce rustdoc - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) +- [playback] Add `PlayerEvent::PositionChanged` event to notify about the current playback position - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` - [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process diff --git a/playback/src/config.rs b/playback/src/config.rs index cdb455ce..b2ece190 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -139,6 +139,9 @@ pub struct PlayerConfig { // 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, + /// Setting this will enable periodically sending events during playback informing about the playback position + /// To consume the PlayerEvent::PositionChanged event, listen to events via `Player::get_player_event_channel()`` + pub position_update_interval: Option, } impl Default for PlayerConfig { @@ -156,6 +159,7 @@ impl Default for PlayerConfig { normalisation_knee_db: 5.0, passthrough: false, ditherer: Some(mk_ditherer::), + position_update_interval: None, } } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 296c26e5..8e0281e6 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -85,6 +85,7 @@ struct PlayerInternal { player_id: usize, play_request_id_generator: SeqGenerator, + last_progress_update: Instant, } static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); @@ -195,6 +196,14 @@ pub enum PlayerEvent { track_id: SpotifyId, position_ms: u32, }, + /// Requires `PlayerConfig::position_update_interval` to be set to Some. + /// Once set this event will be sent periodically while playing the track to inform about the + /// current playback position + PositionChanged { + play_request_id: u64, + track_id: SpotifyId, + position_ms: u32, + }, Seeked { play_request_id: u64, track_id: SpotifyId, @@ -481,6 +490,7 @@ impl Player { player_id, play_request_id_generator: SeqGenerator::new(0), + last_progress_update: Instant::now(), }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -1340,6 +1350,22 @@ impl Future for PlayerInternal { position_ms: new_stream_position_ms, }); } + + if let Some(interval) = + self.config.position_update_interval + { + let last_progress_update_since_ms = + now.duration_since(self.last_progress_update); + + if last_progress_update_since_ms > interval { + self.last_progress_update = now; + self.send_event(PlayerEvent::PositionChanged { + play_request_id, + track_id, + position_ms: new_stream_position_ms, + }); + } + } } Err(e) => { error!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); diff --git a/src/main.rs b/src/main.rs index 6169fac5..1c4c4f2f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1818,6 +1818,7 @@ fn get_setup() -> Setup { normalisation_release_cf, normalisation_knee_db, ditherer, + position_update_interval: None, } }; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 21cfe01c..11c3c1d8 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -243,6 +243,8 @@ impl EventHandler { ); env_vars.insert("FILTER", filter.to_string()); } + // Ignore event irrelevant for standalone binary like PositionChanged + _ => {} } if !env_vars.is_empty() { From e2c3ac31466548e75d6d3017dcf7d8e6ae3035e0 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sat, 3 May 2025 23:39:07 +0200 Subject: [PATCH 509/561] Mark unrelated parsing error as warning (#1491) * mark known parsing error as warning * add copilot suggestion * adjust unknown enum handling for json --- connect/src/spirc.rs | 25 +++++++++++++++++++++---- core/src/dealer/mod.rs | 2 +- core/src/dealer/protocol.rs | 25 +++++++++++++------------ core/src/deserialize_with.rs | 6 +----- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 95b48a8c..911c58ed 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -4,7 +4,7 @@ use crate::{ authentication::Credentials, dealer::{ manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply}, - protocol::{Command, Message, Request}, + protocol::{Command, FallbackWrapper, Message, Request}, }, session::UserAttributes, Error, Session, SpotifyId, @@ -81,7 +81,7 @@ struct SpircTask { connect_state_volume_update: BoxedStreamResult, connect_state_logout_request: BoxedStreamResult, playlist_update: BoxedStreamResult, - session_update: BoxedStreamResult, + session_update: BoxedStreamResult>, connect_state_command: BoxedStream, user_attributes_update: BoxedStreamResult, user_attributes_mutation: BoxedStreamResult, @@ -191,7 +191,7 @@ impl Spirc { let session_update = session .dealer() - .listen_for("social-connect/v2/session_update", Message::from_json)?; + .listen_for("social-connect/v2/session_update", Message::try_from_json)?; let user_attributes_update = session .dealer() @@ -1552,7 +1552,24 @@ impl SpircTask { Ok(()) } - fn handle_session_update(&mut self, mut session_update: SessionUpdate) { + fn handle_session_update(&mut self, session_update: FallbackWrapper) { + // we know that this enum value isn't present in our current proto definitions, by that + // the json parsing fails because the enum isn't known as proto representation + const WBC: &str = "WIFI_BROADCAST_CHANGED"; + + let mut session_update = match session_update { + FallbackWrapper::Inner(update) => update, + FallbackWrapper::Fallback(value) => { + let fallback_inner = value.to_string(); + if fallback_inner.contains(WBC) { + log::debug!("Received SessionUpdate::{WBC}"); + } else { + log::warn!("SessionUpdate couldn't be parse correctly: {value:?}"); + } + return; + } + }; + let reason = session_update.reason.enum_value(); let mut session = match session_update.session.take() { diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index d5bff5b1..4d79f67c 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -357,7 +357,7 @@ impl DealerShared { } } - warn!("No subscriber for msg.uri: {}", msg.uri); + debug!("No subscriber for msg.uri: {}", msg.uri); } fn dispatch_request( diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs index c774a119..f8fdc8e9 100644 --- a/core/src/dealer/protocol.rs +++ b/core/src/dealer/protocol.rs @@ -5,9 +5,8 @@ pub use request::*; use std::collections::HashMap; use std::io::{Error as IoError, Read}; -use crate::Error; -use base64::prelude::BASE64_STANDARD; -use base64::{DecodeError, Engine}; +use crate::{deserialize_with::json_proto, Error}; +use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; use flate2::read::GzDecoder; use log::LevelFilter; use serde::Deserialize; @@ -99,17 +98,19 @@ pub struct Message { pub uri: String, } +#[derive(Deserialize)] +#[serde(untagged)] +pub enum FallbackWrapper { + Inner(#[serde(deserialize_with = "json_proto")] T), + Fallback(JsonValue), +} + impl Message { - pub fn from_json(value: Self) -> Result { - use protobuf_json_mapping::*; + pub fn try_from_json( + value: Self, + ) -> Result, Error> { match value.payload { - PayloadValue::Json(json) => match parse_from_str::(&json) { - Ok(message) => Ok(message), - Err(_) => match parse_from_str_with_options(&json, &IGNORE_UNKNOWN) { - Ok(message) => Ok(message), - Err(why) => Err(Error::failed_precondition(why)), - }, - }, + PayloadValue::Json(json) => Ok(serde_json::from_str(&json)?), other => Err(ProtocolError::UnexpectedData(other).into()), } } diff --git a/core/src/deserialize_with.rs b/core/src/deserialize_with.rs index 11687f9b..0e735cbb 100644 --- a/core/src/deserialize_with.rs +++ b/core/src/deserialize_with.rs @@ -35,11 +35,7 @@ where D: Deserializer<'de>, { let v: Value = Deserialize::deserialize(de)?; - parse_value_to_msg(&v).map_err(|why| { - warn!("deserialize_json_proto: {v}"); - error!("deserialize_json_proto: {why}"); - Error::custom(why) - }) + parse_value_to_msg(&v).map_err(Error::custom) } pub fn option_json_proto<'de, T, D>(de: D) -> Result, D::Error> From 8b729540f4ad1e7f8c94ff3bba33095878b24d02 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 4 May 2025 20:29:54 +0200 Subject: [PATCH 510/561] Re-Add ability to handle/play tracks (#1468) * re-add support to play a set of tracks * connect: reduce some cloning * connect: derive clone for LoadRequest * apply review, improve function naming * clippy fix --- connect/src/context_resolver.rs | 6 +- connect/src/model.rs | 34 +++-- connect/src/spirc.rs | 196 ++++++++++++++++++++--------- connect/src/state/context.rs | 32 +++-- connect/src/state/restrictions.rs | 4 +- protocol/src/impl_trait/context.rs | 26 +++- 6 files changed, 213 insertions(+), 85 deletions(-) diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs index 79d48973..b566e8d7 100644 --- a/connect/src/context_resolver.rs +++ b/connect/src/context_resolver.rs @@ -76,7 +76,9 @@ impl ResolveContext { // otherwise we might not even check if we need to fallback and just use the fallback uri match self.resolve { Resolve::Uri(ref uri) => ConnectState::valid_resolve_uri(uri), - Resolve::Context(ref ctx) => ConnectState::get_context_uri_from_context(ctx), + Resolve::Context(ref ctx) => { + ConnectState::find_valid_uri(ctx.uri.as_deref(), ctx.pages.first()) + } } .or(self.fallback.as_deref()) } @@ -260,7 +262,7 @@ impl ContextResolver { ContextAction::Replace => { let remaining = state.update_context(context, next.update); if let Resolve::Context(ref ctx) = next.resolve { - state.merge_context(Some(ctx.clone())); + state.merge_context(ctx.pages.clone().pop()); } remaining diff --git a/connect/src/model.rs b/connect/src/model.rs index 5e15b01a..10f25f1b 100644 --- a/connect/src/model.rs +++ b/connect/src/model.rs @@ -5,9 +5,9 @@ use crate::{ use std::ops::Deref; /// Request for loading playback -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct LoadRequest { - pub(super) context_uri: String, + pub(super) context: PlayContext, pub(super) options: LoadRequestOptions, } @@ -19,8 +19,14 @@ impl Deref for LoadRequest { } } +#[derive(Debug, Clone)] +pub(super) enum PlayContext { + Uri(String), + Tracks(Vec), +} + /// The parameters for creating a load request -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct LoadRequestOptions { /// Whether the given tracks should immediately start playing, or just be initially loaded. pub start_playing: bool, @@ -44,7 +50,7 @@ pub struct LoadRequestOptions { /// /// Separated into an `enum` to exclude the other variants from being used /// simultaneously, as they are not compatible. -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum LoadContextOptions { /// Starts the context with options Options(Options), @@ -56,7 +62,7 @@ pub enum LoadContextOptions { } /// The available options that indicate how to start the context -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct Options { /// Start the context in shuffle mode pub shuffle: bool, @@ -80,16 +86,30 @@ impl LoadRequest { /// Create a load request from a `context_uri` /// /// For supported `context_uri` see [`SpClient::get_context`](librespot_core::spclient::SpClient::get_context) + /// + /// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback) + /// and providing `context_uri` pub fn from_context_uri(context_uri: String, options: LoadRequestOptions) -> Self { Self { - context_uri, + context: PlayContext::Uri(context_uri), + options, + } + } + + /// Create a load request from a set of `tracks` + /// + /// Equivalent to using [`/me/player/play`](https://developer.spotify.com/documentation/web-api/reference/start-a-users-playback) + /// and providing `uris` + pub fn from_tracks(tracks: Vec, options: LoadRequestOptions) -> Self { + Self { + context: PlayContext::Tracks(tracks), options, } } } /// An item that represent a track to play -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum PlayingTrack { /// Represent the track at a given index. Index(u32), diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 911c58ed..46a3bde2 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -28,9 +28,10 @@ use crate::{ provider::IsProvider, {ConnectConfig, ConnectState}, }, - LoadContextOptions, LoadRequestOptions, + LoadContextOptions, LoadRequestOptions, PlayContext, }; use futures_util::StreamExt; +use librespot_protocol::context_page::ContextPage; use protobuf::MessageField; use std::{ future::Future, @@ -975,12 +976,21 @@ impl SpircTask { self.handle_transfer(transfer.data.expect("by condition checked"))?; return self.notify().await; } - Play(play) => { - let context_uri = play - .context - .uri - .clone() - .ok_or(SpircError::NoUri("context"))?; + Play(mut play) => { + let first_page = play.context.pages.pop(); + let context = match play.context.uri { + Some(s) => PlayContext::Uri(s), + None if !play.context.pages.is_empty() => PlayContext::Tracks( + play.context + .pages + .iter() + .cloned() + .flat_map(|p| p.tracks) + .flat_map(|t| t.uri) + .collect(), + ), + None => Err(SpircError::NoUri("context"))?, + }; let context_options = play .options @@ -989,16 +999,16 @@ impl SpircTask { .map(LoadContextOptions::Options); self.handle_load( - LoadRequest::from_context_uri( - context_uri, - LoadRequestOptions { + LoadRequest { + context, + options: LoadRequestOptions { start_playing: true, seek_to: play.options.seek_to.unwrap_or_default(), playing_track: play.options.skip_to.and_then(|s| s.try_into().ok()), context_options, }, - ), - Some(play.context), + }, + first_page, ) .await?; @@ -1046,6 +1056,8 @@ impl SpircTask { fn handle_transfer(&mut self, mut transfer: TransferState) -> Result<(), Error> { let mut ctx_uri = match transfer.current_session.context.uri { None => Err(SpircError::NoUri("transfer context"))?, + // can apparently happen when a state is transferred stared with "uris" via the api + Some(ref uri) if uri == "-" => String::new(), Some(ref uri) => uri.clone(), }; @@ -1066,6 +1078,27 @@ impl SpircTask { } let fallback = self.connect_state.current_track(|t| &t.uri).clone(); + let load_from_context_uri = !ctx_uri.is_empty(); + + if load_from_context_uri { + self.context_resolver.add(ResolveContext::from_uri( + ctx_uri.clone(), + &fallback, + ContextType::Default, + ContextAction::Replace, + )); + } else { + self.load_context_from_tracks( + transfer + .current_session + .context + .pages + .iter() + .cloned() + .flat_map(|p| p.tracks) + .collect::>(), + )? + } self.context_resolver.add(ResolveContext::from_uri( ctx_uri.clone(), @@ -1112,7 +1145,15 @@ impl SpircTask { )) } - self.transfer_state = Some(transfer); + if load_from_context_uri { + self.transfer_state = Some(transfer); + } else { + let ctx = self.connect_state.get_context(ContextType::Default)?; + let idx = ConnectState::find_index_in_context(ctx, |pt| { + self.connect_state.current_track(|t| pt.uri == t.uri) + })?; + self.connect_state.reset_playback_to_position(Some(idx))?; + } self.load_track(is_playing, position.try_into()?) } @@ -1181,61 +1222,41 @@ impl SpircTask { async fn handle_load( &mut self, cmd: LoadRequest, - context: Option, + page: Option, ) -> Result<(), Error> { self.connect_state - .reset_context(ResetContext::WhenDifferent(&cmd.context_uri)); + .reset_context(if let PlayContext::Uri(ref uri) = cmd.context { + ResetContext::WhenDifferent(uri) + } else { + ResetContext::Completely + }); self.connect_state.reset_options(); - if !self.connect_state.is_active() { - self.handle_activate(); - } - - let fallback = if let Some(ref ctx) = context { - match ConnectState::get_context_uri_from_context(ctx) { - Some(ctx_uri) => ctx_uri, - None => Err(SpircError::InvalidUri(cmd.context_uri.clone()))?, + let autoplay = matches!(cmd.context_options, Some(LoadContextOptions::Autoplay)); + match cmd.context { + PlayContext::Uri(uri) => { + self.load_context_from_uri(uri, page.as_ref(), autoplay) + .await? } - } else { - &cmd.context_uri - }; - - let update_context = if matches!(cmd.context_options, Some(LoadContextOptions::Autoplay)) { - ContextType::Autoplay - } else { - ContextType::Default - }; - - self.connect_state.set_active_context(update_context); - - let current_context_uri = self.connect_state.context_uri(); - if current_context_uri == &cmd.context_uri && fallback == cmd.context_uri { - debug!("context <{current_context_uri}> didn't change, no resolving required") - } else { - debug!("resolving context for load command"); - self.context_resolver.clear(); - self.context_resolver.add(ResolveContext::from_uri( - &cmd.context_uri, - fallback, - update_context, - ContextAction::Replace, - )); - let context = self.context_resolver.get_next_context(Vec::new).await; - self.handle_next_context(context); + PlayContext::Tracks(tracks) => self.load_context_from_tracks(tracks)?, } + let cmd_options = cmd.options; + + self.connect_state.set_active_context(ContextType::Default); + // for play commands with skip by uid, the context of the command contains // tracks with uri and uid, so we merge the new context with the resolved/existing context - self.connect_state.merge_context(context); + self.connect_state.merge_context(page); // load here, so that we clear the queue only after we definitely retrieved a new context self.connect_state.clear_next_tracks(); self.connect_state.clear_restrictions(); - debug!("play track <{:?}>", cmd.playing_track); + debug!("play track <{:?}>", cmd_options.playing_track); - let index = match cmd.playing_track { + let index = match cmd_options.playing_track { None => None, Some(ref playing_track) => Some(match playing_track { PlayingTrack::Index(i) => *i as usize, @@ -1250,7 +1271,7 @@ impl SpircTask { }), }; - if let Some(LoadContextOptions::Options(ref options)) = cmd.context_options { + if let Some(LoadContextOptions::Options(ref options)) = cmd_options.context_options { debug!( "loading with shuffle: <{}>, repeat track: <{}> context: <{}>", options.shuffle, options.repeat, options.repeat_track @@ -1261,7 +1282,8 @@ impl SpircTask { self.connect_state.set_repeat_track(options.repeat_track); } - if matches!(cmd.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle) { + if matches!(cmd_options.context_options, Some(LoadContextOptions::Options(ref o)) if o.shuffle) + { if let Some(index) = index { self.connect_state.set_current_track(index)?; } else { @@ -1282,7 +1304,7 @@ impl SpircTask { } if self.connect_state.current_track(MessageField::is_some) { - self.load_track(cmd.start_playing, cmd.seek_to)?; + self.load_track(cmd_options.start_playing, cmd_options.seek_to)?; } else { info!("No active track, stopping"); self.handle_stop() @@ -1291,6 +1313,67 @@ impl SpircTask { Ok(()) } + async fn load_context_from_uri( + &mut self, + context_uri: String, + page: Option<&ContextPage>, + autoplay: bool, + ) -> Result<(), Error> { + if !self.connect_state.is_active() { + self.handle_activate(); + } + + let update_context = if autoplay { + ContextType::Autoplay + } else { + ContextType::Default + }; + + self.connect_state.set_active_context(update_context); + + let fallback = match page { + // check that the uri is valid or the page has a valid uri that can be used + Some(page) => match ConnectState::find_valid_uri(Some(&context_uri), Some(page)) { + Some(ctx_uri) => ctx_uri, + None => return Err(SpircError::InvalidUri(context_uri).into()), + }, + // when there is no page, the uri should be valid + None => &context_uri, + }; + + let current_context_uri = self.connect_state.context_uri(); + + if current_context_uri == &context_uri && fallback == context_uri { + debug!("context <{current_context_uri}> didn't change, no resolving required") + } else { + debug!("resolving context for load command"); + self.context_resolver.clear(); + self.context_resolver.add(ResolveContext::from_uri( + &context_uri, + fallback, + update_context, + ContextAction::Replace, + )); + let context = self.context_resolver.get_next_context(Vec::new).await; + self.handle_next_context(context); + } + + Ok(()) + } + + fn load_context_from_tracks(&mut self, tracks: impl Into) -> Result<(), Error> { + let ctx = Context { + pages: vec![tracks.into()], + ..Default::default() + }; + + let _ = self + .connect_state + .update_context(ctx, ContextType::Default)?; + + Ok(()) + } + fn handle_play(&mut self) { match self.play_status { SpircPlayStatus::Paused { @@ -1433,7 +1516,8 @@ impl SpircTask { let require_load_new = !self .connect_state .has_next_tracks(Some(CONTEXT_FETCH_THRESHOLD)) - && self.session.autoplay(); + && self.session.autoplay() + && !self.connect_state.context_uri().is_empty(); if !require_load_new { return; diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 5233795e..c9afbb64 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -116,6 +116,10 @@ impl ConnectState { ResetContext::Completely => { self.context = None; self.autoplay_context = None; + + let player = self.player_mut(); + player.context_uri.clear(); + player.context_url.clear(); } ResetContext::DefaultIndex => { for ctx in [self.context.as_mut(), self.autoplay_context.as_mut()] @@ -141,14 +145,13 @@ impl ConnectState { } } - pub fn get_context_uri_from_context(context: &Context) -> Option<&str> { - let uri = context.uri.as_deref().unwrap_or_default(); - Self::valid_resolve_uri(uri).or_else(|| { - context - .pages - .first() - .and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref())) - }) + pub fn find_valid_uri<'s>( + context_uri: Option<&'s str>, + first_page: Option<&'s ContextPage>, + ) -> Option<&'s str> { + context_uri + .and_then(Self::valid_resolve_uri) + .or_else(|| first_page.and_then(|p| p.tracks.first().and_then(|t| t.uri.as_deref()))) } pub fn set_active_context(&mut self, new_context: ContextType) { @@ -157,7 +160,8 @@ impl ConnectState { let player = self.player_mut(); player.context_metadata = Default::default(); - player.restrictions = Some(Default::default()).into(); + player.context_restrictions = MessageField::some(Default::default()); + player.restrictions = MessageField::some(Default::default()); let ctx = match self.get_context(new_context) { Err(why) => { @@ -387,16 +391,10 @@ impl ConnectState { .unwrap_or(false) } - pub fn merge_context(&mut self, context: Option) -> Option<()> { - let mut context = context?; - if matches!(context.uri, Some(ref uri) if uri != self.context_uri()) { - return None; - } - + pub fn merge_context(&mut self, new_page: Option) -> Option<()> { let current_context = self.get_context_mut(ContextType::Default).ok()?; - let new_page = context.pages.pop()?; - for new_track in new_page.tracks { + for new_track in new_page?.tracks { if new_track.uri.is_none() || matches!(new_track.uri, Some(ref uri) if uri.is_empty()) { continue; } diff --git a/connect/src/state/restrictions.rs b/connect/src/state/restrictions.rs index 03495c68..e4604c54 100644 --- a/connect/src/state/restrictions.rs +++ b/connect/src/state/restrictions.rs @@ -7,8 +7,8 @@ impl ConnectState { pub fn clear_restrictions(&mut self) { let player = self.player_mut(); - player.restrictions.clear(); - player.context_restrictions.clear(); + player.context_restrictions = Some(Default::default()).into(); + player.restrictions = Some(Default::default()).into(); } pub fn update_restrictions(&mut self) { diff --git a/protocol/src/impl_trait/context.rs b/protocol/src/impl_trait/context.rs index 875ef9ad..782a41dc 100644 --- a/protocol/src/impl_trait/context.rs +++ b/protocol/src/impl_trait/context.rs @@ -1,4 +1,4 @@ -use crate::context::Context; +use crate::{context::Context, context_page::ContextPage, context_track::ContextTrack}; use protobuf::Message; use std::hash::{Hash, Hasher}; @@ -11,3 +11,27 @@ impl Hash for Context { } impl Eq for Context {} + +impl From> for ContextPage { + fn from(value: Vec) -> Self { + ContextPage { + tracks: value + .into_iter() + .map(|uri| ContextTrack { + uri: Some(uri), + ..Default::default() + }) + .collect(), + ..Default::default() + } + } +} + +impl From> for ContextPage { + fn from(tracks: Vec) -> Self { + ContextPage { + tracks, + ..Default::default() + } + } +} From 3686718ea2413a98ce600a1533272a5f3bedecb0 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Mon, 9 Jun 2025 12:13:17 +0200 Subject: [PATCH 511/561] Chore: fix clippy warnings for stable (1.87) and nightly (1.89) (#1504) * chore: stable - fix clippy warnings * chore: nightly - fix clippy warnings --- core/src/mercury/types.rs | 2 +- core/src/proxytunnel.rs | 11 ++++------- discovery/src/lib.rs | 10 ++++------ oauth/src/lib.rs | 4 ++-- playback/src/mixer/mappings.rs | 2 +- src/main.rs | 3 +-- 6 files changed, 13 insertions(+), 19 deletions(-) diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 70fb3f86..8154d4f5 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -57,7 +57,7 @@ impl std::fmt::Display for MercuryMethod { MercuryMethod::Unsub => "UNSUB", MercuryMethod::Send => "SEND", }; - write!(f, "{}", s) + write!(f, "{s}") } } diff --git a/core/src/proxytunnel.rs b/core/src/proxytunnel.rs index af51bbb7..9e8cd3b7 100644 --- a/core/src/proxytunnel.rs +++ b/core/src/proxytunnel.rs @@ -22,7 +22,7 @@ pub async fn proxy_connect( loop { let bytes_read = proxy_connection.read(&mut buffer[offset..]).await?; if bytes_read == 0 { - return Err(io::Error::new(io::ErrorKind::Other, "Early EOF from proxy")); + return Err(io::Error::other("Early EOF from proxy")); } offset += bytes_read; @@ -31,7 +31,7 @@ pub async fn proxy_connect( let status = response .parse(&buffer[..offset]) - .map_err(|err| io::Error::new(io::ErrorKind::Other, err))?; + .map_err(io::Error::other)?; if status.is_complete() { return match response.code { @@ -39,12 +39,9 @@ pub async fn proxy_connect( Some(code) => { let reason = response.reason.unwrap_or("no reason"); let msg = format!("Proxy responded with {code}: {reason}"); - Err(io::Error::new(io::ErrorKind::Other, msg)) + Err(io::Error::other(msg)) } - None => Err(io::Error::new( - io::ErrorKind::Other, - "Malformed response from proxy", - )), + None => Err(io::Error::other("Malformed response from proxy")), }; } diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index c6d88a2e..36fd2509 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -96,12 +96,10 @@ pub fn find(name: Option<&str>) -> Result { match BACKENDS.iter().find(|(id, _)| name == id) { Some((_id, Some(launch_svc))) => Ok(*launch_svc), Some((_id, None)) => Err(Error::unavailable(format!( - "librespot built without '{}' support", - name + "librespot built without '{name}' support" ))), None => Err(Error::not_found(format!( - "unknown zeroconf backend '{}'", - name + "unknown zeroconf backend '{name}'" ))), } } else { @@ -286,14 +284,14 @@ async fn avahi_task( // // EntryGroup has been withdrawn at this point already! log::error!("zeroconf collision for name '{}'", &name); - return Err(zbus::Error::Failure(format!("zeroconf collision for name: {}", name)).into()); + return Err(zbus::Error::Failure(format!("zeroconf collision for name: {name}")).into()); } EntryGroupState::Failure => { // TODO: Back off/treat as fatal? // EntryGroup has been withdrawn at this point already! // There seems to be no code in Avahi that actually sets this state. log::error!("zeroconf failure: {}", error); - return Err(zbus::Error::Failure(format!("zeroconf failure: {}", error)).into()); + return Err(zbus::Error::Failure(format!("zeroconf failure: {error}")).into()); } } } diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 284f08d7..2706ac73 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -237,7 +237,7 @@ impl OAuthClient { if self.should_open_url { open::that_in_background(auth_url.as_str()); } - println!("Browse to: {}", auth_url); + println!("Browse to: {auth_url}"); pkce_verifier } @@ -456,7 +456,7 @@ pub fn get_access_token( .set_pkce_challenge(pkce_challenge) .url(); - println!("Browse to: {}", auth_url); + println!("Browse to: {auth_url}"); let code = match get_socket_address(redirect_uri) { Some(addr) => get_authcode_listener(addr, String::from("Go back to your terminal :)")), diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index 736b3c3f..bcb3fa45 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -158,6 +158,6 @@ 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) + f64::powf(10.0, -db_range / 60.0) } } diff --git a/src/main.rs b/src/main.rs index 1c4c4f2f..85755658 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1227,8 +1227,7 @@ fn get_setup() -> Setup { Some("librespot compiled without zeroconf backend".to_owned()) } else if opt_present(DISABLE_DISCOVERY) { Some(format!( - "the `--{}` / `-{}` flag set", - DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, + "the `--{DISABLE_DISCOVERY}` / `-{DISABLE_DISCOVERY_SHORT}` flag set", )) } else { None From 2c425ebd0685820a59eac1a4206728acb8a24a51 Mon Sep 17 00:00:00 2001 From: Mads Marquart Date: Mon, 9 Jun 2025 12:34:54 +0200 Subject: [PATCH 512/561] Fix compiler error when `objc2` is in dependency tree (#1503) --- connect/src/shuffle_vec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs index 84fb6b15..c5367598 100644 --- a/connect/src/shuffle_vec.rs +++ b/connect/src/shuffle_vec.rs @@ -55,7 +55,7 @@ impl ShuffleVec { self.unshuffle() } - let indices = { + let indices: Vec<_> = { (1..self.vec.len()) .rev() .map(|i| rng.gen_range(0..i + 1)) From b2915ee2bf2df5dae0ff3d39c7618533c3d09988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Richard=20Pe=C3=B1a=20B=2E?= <138641726+richardhapb@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:27:59 -0400 Subject: [PATCH 513/561] fix(cdn_url): add support for `verify` query parameter (#1513) - Updated `MaybeExpiringUrls` to handle `verify` query parameter. - Extracted expiry timestamp from `verify` parameter if present. - Adjusted test cases to include URLs with `verify` parameter. - Updated assertions to account for the new URL format. This change ensures compatibility with URLs containing the `verify` query parameter, improving the flexibility of the CDN URL handling. Solves #1512 --- core/src/cdn_url.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 5a80528e..b6b7ecfc 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -119,6 +119,15 @@ impl TryFrom for MaybeExpiringUrls { if is_expiring { let mut expiry_str: Option = None; if let Some(token) = url + .query_pairs() + .into_iter() + .find(|(key, _value)| key == "verify") + { + // https://audio-cf.spotifycdn.com/audio/844ecdb297a87ebfee4399f28892ef85d9ba725f?verify=1750549951-4R3I2w2q7OfNkR%2FGH8qH7xtIKUPlDxywBuADY%2BsvMeU%3D + if let Some((expiry_str_candidate, _)) = token.1.split_once('-') { + expiry_str = Some(expiry_str_candidate.to_string()); + } + } else if let Some(token) = url .query_pairs() .into_iter() .find(|(key, _value)| key == "__token__") @@ -187,6 +196,7 @@ mod test { let mut msg = CdnUrlMessage::new(); msg.result = StorageResolveResponse_Result::CDN.into(); msg.cdnurl = vec![ + format!("https://audio-cf.spotifycdn.com/audio/844ecdb297a87ebfee4399f28892ef85d9ba725f?verify={timestamp}-4R3I2w2q7OfNkR%2FGH8qH7xtIKUPlDxywBuADY%2BsvMeU%3D"), format!("https://audio-ak-spotify-com.akamaized.net/audio/foo?__token__=exp={timestamp}~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41"), format!("https://audio-gm-off.spotifycdn.com/audio/foo?Expires={timestamp}~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM="), format!("https://audio4-fa.scdn.co/audio/foo?{timestamp}_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4="), @@ -195,11 +205,12 @@ mod test { msg.fileid = vec![0]; let urls = MaybeExpiringUrls::try_from(msg).expect("valid urls"); - assert_eq!(urls.len(), 4); + assert_eq!(urls.len(), 5); assert!(urls[0].1.is_some()); assert!(urls[1].1.is_some()); assert!(urls[2].1.is_some()); - assert!(urls[3].1.is_none()); + assert!(urls[3].1.is_some()); + assert!(urls[4].1.is_none()); let timestamp_margin = Duration::seconds(timestamp) - CDN_URL_EXPIRY_MARGIN; assert_eq!( urls[0].1.unwrap().as_timestamp_ms() as i128, From 80c27ec476666b40aba98327b3ba52d620dd6d06 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Thu, 26 Jun 2025 17:39:49 +0200 Subject: [PATCH 514/561] fix: playback of uris by api request (#1509) - pop page after usage - become active when play is requested --- connect/src/spirc.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 46a3bde2..14c0f015 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -977,7 +977,10 @@ impl SpircTask { return self.notify().await; } Play(mut play) => { - let first_page = play.context.pages.pop(); + if !self.connect_state.is_active() { + self.handle_activate() + } + let context = match play.context.uri { Some(s) => PlayContext::Uri(s), None if !play.context.pages.is_empty() => PlayContext::Tracks( @@ -1008,7 +1011,7 @@ impl SpircTask { context_options, }, }, - first_page, + play.context.pages.pop(), ) .await?; From be37402421167ec3721aa7f6fd1103851475517b Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Mon, 14 Jul 2025 17:39:33 +0200 Subject: [PATCH 515/561] Expose possible mixer opening errors (#1488) * playback: handle errors when opening mixer * chore: update CHANGELOG.md * fix tests and typo --- CHANGELOG.md | 2 ++ connect/README.md | 2 +- examples/play_connect.rs | 2 +- playback/src/mixer/alsamixer.rs | 47 +++++++++++++++++++++++++-------- playback/src/mixer/mod.rs | 14 +++++----- playback/src/mixer/softmixer.rs | 14 +++++----- src/main.rs | 29 +++++++++++--------- 7 files changed, 71 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cca28afe..fdac644d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Moved all public items to the highest level (breaking) - [connect] Replaced Mercury usage in `Spirc` with Dealer - [metadata] Replaced `AudioFileFormat` with own enum. (breaking) +- [playback] Changed trait `Mixer::open` to return `Result` instead of `Self` (breaking) +- [playback] Changed type alias `MixerFn` to return `Result, Error>` instead of `Arc` (breaking) ### Added diff --git a/connect/README.md b/connect/README.md index 127474d7..015cce3d 100644 --- a/connect/README.md +++ b/connect/README.md @@ -55,7 +55,7 @@ async fn create_basic_spirc() -> Result<(), Error> { session, credentials, player, - mixer(MixerConfig::default()) + mixer(MixerConfig::default())? ).await?; Ok(()) diff --git a/examples/play_connect.rs b/examples/play_connect.rs index 26e52022..bd57df7d 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -50,7 +50,7 @@ async fn main() -> Result<(), Error> { })?; let session = Session::new(session_config, Some(cache)); - let mixer = mixer_builder(mixer_config); + let mixer = mixer_builder(mixer_config)?; let player = Player::new( player_config, diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 52be1085..33ad64c4 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -5,9 +5,12 @@ use super::{Mixer, MixerConfig, VolumeCtrl}; use alsa::ctl::{ElemId, ElemIface}; use alsa::mixer::{MilliBel, SelemChannelId, SelemId}; +use alsa::Error as AlsaError; use alsa::{Ctl, Round}; -use std::ffi::CString; +use librespot_core::Error; +use std::ffi::{CString, NulError}; +use thiserror::Error; #[derive(Clone)] #[allow(dead_code)] @@ -29,8 +32,30 @@ pub struct AlsaMixer { const SND_CTL_TLV_DB_GAIN_MUTE: MilliBel = MilliBel(-9999999); const ZERO_DB: MilliBel = MilliBel(0); +#[derive(Debug, Error)] +enum AlsaMixerError { + #[error("Could not open Alsa mixer. {0}")] + CouldNotOpen(AlsaError), + #[error("Could not find Alsa mixer control")] + CouldNotFindController, + #[error("Could not open Alsa softvol with that device. {0}")] + CouldNotOpenWithDevice(AlsaError), + #[error("Could not open Alsa softvol with that name. {0}")] + CouldNotOpenWithName(NulError), + #[error("Could not get Alsa softvol dB range. {0}")] + NoDbRange(AlsaError), + #[error("Could not convert Alsa raw volume to dB volume. {0}")] + CouldNotConvertRaw(AlsaError), +} + +impl From for Error { + fn from(value: AlsaMixerError) -> Self { + Error::failed_precondition(value) + } +} + impl Mixer for AlsaMixer { - fn open(config: MixerConfig) -> Self { + fn open(config: MixerConfig) -> Result { info!( "Mixing with Alsa and volume control: {:?} for device: {} with mixer control: {},{}", config.volume_ctrl, config.device, config.control, config.index, @@ -39,10 +64,10 @@ impl Mixer for AlsaMixer { let mut config = config; // clone let mixer = - alsa::mixer::Mixer::new(&config.device, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&config.device, false).map_err(AlsaMixerError::CouldNotOpen)?; let simple_element = mixer .find_selem(&SelemId::new(&config.control, config.index)) - .expect("Could not find Alsa mixer control"); + .ok_or(AlsaMixerError::CouldNotFindController)?; // Query capabilities let has_switch = simple_element.has_playback_switch(); @@ -57,17 +82,17 @@ impl Mixer for AlsaMixer { // Query dB volume range -- note that Alsa exposes a different // API for hardware and software mixers let (min_millibel, max_millibel) = if is_softvol { - let control = Ctl::new(&config.device, false) - .expect("Could not open Alsa softvol with that device"); + let control = + Ctl::new(&config.device, false).map_err(AlsaMixerError::CouldNotOpenWithDevice)?; 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"), + .map_err(AlsaMixerError::CouldNotOpenWithName)?, ); 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"); + .map_err(AlsaMixerError::NoDbRange)?; // Alsa can report incorrect maximum volumes due to rounding // errors. e.g. Alsa rounds [-60.0..0.0] in range [0..255] to @@ -97,7 +122,7 @@ impl Mixer for AlsaMixer { 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"); + .map_err(AlsaMixerError::CouldNotConvertRaw)?; } (min_millibel, max_millibel) }; @@ -150,7 +175,7 @@ impl Mixer for AlsaMixer { ); debug!("Alsa forcing linear dB mapping: {}", use_linear_in_db); - Self { + Ok(Self { config, min, max, @@ -161,7 +186,7 @@ impl Mixer for AlsaMixer { has_switch, is_softvol, use_linear_in_db, - } + }) } fn volume(&self) -> u16 { diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 83d00853..86252217 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -1,6 +1,6 @@ -use std::sync::Arc; - use crate::config::VolumeCtrl; +use librespot_core::Error; +use std::sync::Arc; pub mod mappings; use self::mappings::MappedCtrl; @@ -8,12 +8,12 @@ use self::mappings::MappedCtrl; pub struct NoOpVolume; pub trait Mixer: Send + Sync { - fn open(config: MixerConfig) -> Self + fn open(config: MixerConfig) -> Result where Self: Sized; - fn set_volume(&self, volume: u16); fn volume(&self) -> u16; + fn set_volume(&self, volume: u16); fn get_soft_volume(&self) -> Box { Box::new(NoOpVolume) @@ -57,10 +57,10 @@ impl Default for MixerConfig { } } -pub type MixerFn = fn(MixerConfig) -> Arc; +pub type MixerFn = fn(MixerConfig) -> Result, Error>; -fn mk_sink(config: MixerConfig) -> Arc { - Arc::new(M::open(config)) +fn mk_sink(config: MixerConfig) -> Result, Error> { + Ok(Arc::new(M::open(config)?)) } pub const MIXERS: &[(&str, MixerFn)] = &[ diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 6d32edc4..bf5e26ed 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -1,10 +1,10 @@ -use portable_atomic::AtomicU64; -use std::sync::atomic::Ordering; -use std::sync::Arc; - use super::VolumeGetter; use super::{MappedCtrl, VolumeCtrl}; use super::{Mixer, MixerConfig}; +use librespot_core::Error; +use portable_atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::sync::Arc; #[derive(Clone)] pub struct SoftMixer { @@ -15,14 +15,14 @@ pub struct SoftMixer { } impl Mixer for SoftMixer { - fn open(config: MixerConfig) -> Self { + fn open(config: MixerConfig) -> Result { let volume_ctrl = config.volume_ctrl; info!("Mixing with softvol and volume control: {:?}", volume_ctrl); - Self { + Ok(Self { volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), volume_ctrl, - } + }) } fn volume(&self) -> u16 { diff --git a/src/main.rs b/src/main.rs index 85755658..38f8391a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,3 @@ -use std::{ - env, - fs::create_dir_all, - ops::RangeInclusive, - path::{Path, PathBuf}, - pin::Pin, - process::exit, - str::FromStr, - time::{Duration, Instant}, -}; - use data_encoding::HEXLOWER; use futures_util::StreamExt; #[cfg(feature = "alsa-backend")] @@ -33,6 +22,16 @@ use librespot::{ use librespot_oauth::OAuthClientBuilder; use log::{debug, error, info, trace, warn}; use sha1::{Digest, Sha1}; +use std::{ + env, + fs::create_dir_all, + ops::RangeInclusive, + path::{Path, PathBuf}, + pin::Pin, + process::exit, + str::FromStr, + time::{Duration, Instant}, +}; use sysinfo::{ProcessesToUpdate, System}; use thiserror::Error; use url::Url; @@ -1943,7 +1942,13 @@ async fn main() { } let mixer_config = setup.mixer_config.clone(); - let mixer = (setup.mixer)(mixer_config); + let mixer = match (setup.mixer)(mixer_config) { + Ok(mixer) => mixer, + Err(why) => { + error!("{why}"); + exit(1) + } + }; let player_config = setup.player_config.clone(); let soft_volume = mixer.get_soft_volume(); From 3a700f0020afca40b80cfe97f32e583f7ac9c1cf Mon Sep 17 00:00:00 2001 From: "./lemon.sh" Date: Fri, 8 Aug 2025 16:32:20 +0200 Subject: [PATCH 516/561] fix: add fallback logic for CDN urls (#1524) --- CHANGELOG.md | 4 +++ audio/src/fetch/mod.rs | 57 +++++++++++++++++++++++++++++------------- core/src/cdn_url.rs | 29 +++++++++++++++++++++ core/src/spclient.rs | 16 ++++++------ 4 files changed, 81 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdac644d..560de2b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] MSRV is now 1.81 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. +- [core] `stream_from_cdn` now accepts the URL as a `&str` instead of `CdnUrl` (breaking) - [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) - [connect] Changed `initial_volume` from `Option` to `u16` in `ConnectConfig` (breaking) - [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) @@ -30,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking) - [playback] Add `PlayerEvent::PositionChanged` event to notify about the current playback position - [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient` +- [core] Add `try_get_urls` to `CdnUrl` - [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process ### Fixed @@ -48,10 +50,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Handle transfer of playback with empty "uri" field - [connect] Correctly apply playing/paused state when transferring playback - [player] Saturate invalid seek positions to track duration +- [audio] Fall back to other URLs in case of a failure when downloading from CDN ### Deprecated - [oauth] `get_access_token()` function marked for deprecation +- [core] `try_get_url()` function marked for deprecation ### Removed diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index d1617106..414ce3cf 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -306,7 +306,7 @@ struct AudioFileDownloadStatus { } struct AudioFileShared { - cdn_url: CdnUrl, + cdn_url: String, file_size: usize, bytes_per_second: usize, cond: Condvar, @@ -426,25 +426,46 @@ impl AudioFileStreaming { ) -> Result { 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 minimum_download_size = AudioFetchParams::get().minimum_download_size; - // 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, minimum_download_size)?; + let mut response_streamer_url = None; + let urls = cdn_url.try_get_urls()?; + for url in &urls { + // 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(*url, 0, minimum_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 response = streamer.next().await.ok_or(AudioFileError::NoData)??; + // 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 streamer_result = tokio::time::timeout(Duration::from_secs(10), streamer.next()) + .await + .map_err(|_| AudioFileError::WaitTimeout.into()) + .and_then(|x| x.ok_or_else(|| AudioFileError::NoData.into())) + .and_then(|x| x.map_err(Error::from)); + + match streamer_result { + Ok(r) => { + response_streamer_url = Some((r, streamer, url)); + break; + } + Err(e) => warn!("Fetching {url} failed with error {e:?}, trying next"), + } + } + + let Some((response, streamer, url)) = response_streamer_url else { + return Err(Error::unavailable(format!( + "{} URLs failed, none left to try", + urls.len() + ))); + }; + + trace!("Streaming from {}", url); let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { @@ -473,7 +494,7 @@ impl AudioFileStreaming { }; let shared = Arc::new(AudioFileShared { - cdn_url, + cdn_url: url.to_string(), file_size, bytes_per_second, cond: Condvar::new(), diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index b6b7ecfc..44f4488f 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -78,6 +78,7 @@ impl CdnUrl { Ok(cdn_url) } + #[deprecated = "This function only returns the first valid URL. Use try_get_urls instead, which allows for fallback logic."] pub fn try_get_url(&self) -> Result<&str, Error> { if self.urls.is_empty() { return Err(CdnUrlError::Unresolved.into()); @@ -95,6 +96,34 @@ impl CdnUrl { Err(CdnUrlError::Expired.into()) } } + + pub fn try_get_urls(&self) -> Result, Error> { + if self.urls.is_empty() { + return Err(CdnUrlError::Unresolved.into()); + } + + let now = Date::now_utc(); + let urls: Vec<&str> = self + .urls + .iter() + .filter_map(|MaybeExpiringUrl(url, expiry)| match *expiry { + Some(expiry) => { + if now < expiry { + Some(url.as_str()) + } else { + None + } + } + None => Some(url.as_str()), + }) + .collect(); + + if urls.is_empty() { + Err(CdnUrlError::Expired.into()) + } else { + Ok(urls) + } + } } impl TryFrom for MaybeExpiringUrls { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 4377d406..87a6098c 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -6,7 +6,6 @@ use std::{ use crate::config::{os_version, OS}; use crate::{ apresolve::SocketAddress, - cdn_url::CdnUrl, config::SessionConfig, error::ErrorKind, protocol::{ @@ -27,7 +26,7 @@ use crate::{ use bytes::Bytes; use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; -use http::header::HeaderValue; +use http::{header::HeaderValue, Uri}; use hyper::{ header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, HeaderMap, Method, Request, @@ -730,16 +729,19 @@ impl SpClient { self.request(&Method::GET, &endpoint, None, None).await } - pub fn stream_from_cdn( + pub fn stream_from_cdn( &self, - cdn_url: &CdnUrl, + cdn_url: U, offset: usize, length: usize, - ) -> Result, Error> { - let url = cdn_url.try_get_url()?; + ) -> Result, Error> + where + U: TryInto, + >::Error: Into, + { let req = Request::builder() .method(&Method::GET) - .uri(url) + .uri(cdn_url) .header( RANGE, HeaderValue::from_str(&format!("bytes={}-{}", offset, offset + length - 1))?, From ba3d501b08345aadf207d09b3a0713853228ba64 Mon Sep 17 00:00:00 2001 From: Timon de Groot Date: Mon, 11 Aug 2025 13:31:36 +0200 Subject: [PATCH 517/561] spclient: Specify base url for metadata requests (#1528) Fixes #1527 --- CHANGELOG.md | 1 + core/src/spclient.rs | 26 ++++++++++++++++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 560de2b9..b62e9f85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Correctly apply playing/paused state when transferring playback - [player] Saturate invalid seek positions to track duration - [audio] Fall back to other URLs in case of a failure when downloading from CDN +- [core] Metadata requests failing with 500 Internal Server Error ### Deprecated diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 87a6098c..272975d1 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -55,6 +55,13 @@ const CONNECTION_ID: HeaderName = HeaderName::from_static("x-spotify-connection- const NO_METRICS_AND_SALT: RequestOptions = RequestOptions { metrics: false, salt: false, + base_url: None, +}; + +const SPCLIENT_FALLBACK_ENDPOINT: RequestOptions = RequestOptions { + metrics: true, + salt: true, + base_url: Some("https://spclient.wg.spotify.com"), }; #[derive(Debug, Error)] @@ -86,6 +93,7 @@ impl Default for RequestStrategy { pub struct RequestOptions { metrics: bool, salt: bool, + base_url: Option<&'static str>, } impl Default for RequestOptions { @@ -93,6 +101,7 @@ impl Default for RequestOptions { Self { metrics: true, salt: true, + base_url: None, } } } @@ -449,7 +458,10 @@ 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 = match options.base_url { + Some(base_url) => base_url.to_string(), + None => self.base_url().await?, + }; url.push_str(endpoint); // Add metrics. There is also an optional `partner` key with a value like @@ -566,7 +578,17 @@ impl SpClient { pub async fn get_metadata(&self, scope: &str, id: &SpotifyId) -> SpClientResult { let endpoint = format!("/metadata/4/{}/{}", scope, id.to_base16()?); - self.request(&Method::GET, &endpoint, None, None).await + // For unknown reasons, metadata requests must now be sent through spclient.wg.spotify.com. + // Otherwise, the API will respond with 500 Internal Server Error responses. + // Context: https://github.com/librespot-org/librespot/issues/1527 + self.request_with_options( + &Method::GET, + &endpoint, + None, + None, + &SPCLIENT_FALLBACK_ENDPOINT, + ) + .await } pub async fn get_track_metadata(&self, track_id: &SpotifyId) -> SpClientResult { From 1d5c0d845108a5a92046d634ccc7264e7e9de3a0 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 12 Aug 2025 19:24:55 +0200 Subject: [PATCH 518/561] docs: correct changelog entry for stream_from_cdn --- CHANGELOG.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62e9f85..4f6550ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,13 @@ All notable changes to this project will be documented in this file. 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] - YYYY-MM-DD +## [0.7.0] - Unreleased ### Changed - [core] MSRV is now 1.81 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. -- [core] `stream_from_cdn` now accepts the URL as a `&str` instead of `CdnUrl` (breaking) +- [core] `stream_from_cdn` now accepts the URL as `TryInto` instead of `CdnUrl` (breaking) - [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) - [connect] Changed `initial_volume` from `Option` to `u16` in `ConnectConfig` (breaking) - [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) @@ -52,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [player] Saturate invalid seek positions to track duration - [audio] Fall back to other URLs in case of a failure when downloading from CDN - [core] Metadata requests failing with 500 Internal Server Error +- [player] Rodio backend did not honor audio output format request ### Deprecated @@ -398,7 +399,8 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.6.0...HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.7.0...HEAD +[0.7.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.7.0 [0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2 From ce1ab8ff3fb9d4bcac461307c0233ef732463084 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 13:19:48 +0200 Subject: [PATCH 519/561] refactor: update dependencies and code for latest ecosystem changes - Update many dependencies to latest versions across all crates - Switch from `once_cell::OnceCell` to `std::sync::OnceLock` where appropriate - Update OAuth to use stateful `reqwest` for HTTP requests - Fix Rodio backend to honor the requested sample format --- Cargo.lock | 2378 +++++++++++++++------------ Cargo.toml | 37 +- audio/Cargo.toml | 6 +- connect/Cargo.toml | 8 +- connect/src/shuffle_vec.rs | 4 +- connect/src/state/options.rs | 4 +- connect/src/state/tracks.rs | 2 +- core/Cargo.toml | 100 +- core/build.rs | 5 +- core/src/connection/handshake.rs | 6 +- core/src/dealer/manager.rs | 6 +- core/src/dealer/mod.rs | 7 +- core/src/diffie_hellman.rs | 13 +- core/src/http_client.rs | 12 +- core/src/session.rs | 50 +- core/src/spclient.rs | 2 +- discovery/Cargo.toml | 26 +- discovery/src/server.rs | 2 +- metadata/Cargo.toml | 4 +- oauth/Cargo.toml | 15 +- oauth/src/lib.rs | 45 +- playback/Cargo.toml | 45 +- playback/src/audio_backend/rodio.rs | 82 +- playback/src/dither.rs | 6 +- protocol/Cargo.toml | 2 +- 25 files changed, 1662 insertions(+), 1205 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fee06376..3f164aa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -37,6 +37,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alsa" version = "0.9.1" @@ -44,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.7.0", + "bitflags 2.9.1", "cfg-if", "libc", ] @@ -76,9 +82,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -91,43 +97,44 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.6" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", - "windows-sys 0.59.0", + "once_cell_polyfill", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.95" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "arrayvec" @@ -155,18 +162,18 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "async-trait" -version = "0.1.85" +version = "0.1.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -183,15 +190,15 @@ checksum = "41e67cd8309bbd06cd603a9e693a784ac2e5d1e955f11286e355089fcab3047c" [[package]] name = "autocfg" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-lc-rs" -version = "1.12.6" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabb68eb3a7aa08b46fddfd59a3d55c978243557a90ab804769f7e20e67d2b01" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" dependencies = [ "aws-lc-sys", "zeroize", @@ -199,11 +206,11 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.27.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77926887776171ced7d662120a75998e444d3750c951abfe07f90da130514b1f" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" dependencies = [ - "bindgen 0.69.5", + "bindgen", "cc", "cmake", "dunce", @@ -212,9 +219,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -225,18 +232,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" @@ -245,9 +240,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "base64ct" -version = "1.6.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" [[package]] name = "bindgen" @@ -255,7 +250,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools 0.12.1", @@ -266,30 +261,12 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 1.1.0", "shlex", - "syn 2.0.96", + "syn", "which", ] -[[package]] -name = "bindgen" -version = "0.70.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" -dependencies = [ - "bitflags 2.7.0", - "cexpr", - "clang-sys", - "itertools 0.13.0", - "proc-macro2", - "quote", - "regex", - "rustc-hash", - "shlex", - "syn 2.0.96", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -298,9 +275,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.7.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1be3f42a67d6d345ecd59f675f3f012d6974981560836e938c22b424b85ce1be" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-buffer" @@ -313,15 +290,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "bytemuck" -version = "1.21.0" +version = "1.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef657dfab802224e671f5818e9a4935f9b1957ed18e58292690cc39e7a4092a3" +checksum = "3995eaeebcdf32f91f980d360f78732ddc061097ab4e39991ae7a6ace9194677" [[package]] name = "byteorder" @@ -331,15 +308,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.9" +version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ "jobserver", "libc", @@ -363,9 +340,9 @@ dependencies = [ [[package]] name = "cfg-expr" -version = "0.17.2" +version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d4ba6e40bd1184518716a6e1a781bf9160e286d219ccdb8ab2612e74cfe4789" +checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" dependencies = [ "smallvec", "target-lexicon", @@ -373,9 +350,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cfg_aliases" @@ -385,9 +362,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", @@ -395,7 +372,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -416,23 +393,23 @@ checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" dependencies = [ "glob", "libc", - "libloading 0.8.6", + "libloading 0.8.8", ] [[package]] name = "cmake" -version = "0.1.52" +version = "0.1.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c682c223677e0e5b6b7f63a64b9351844c3f1b1678a68b7ee617e30fb082620e" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" dependencies = [ "cc", ] [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "combine" @@ -471,9 +448,9 @@ dependencies = [ [[package]] name = "core-foundation" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" dependencies = [ "core-foundation-sys", "libc", @@ -487,42 +464,39 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "coreaudio-rs" -version = "0.11.3" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" dependencies = [ "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ce857aa0b77d77287acc1ac3e37a05a8c95a2af3647d23b15f263bdaeb7562b" -dependencies = [ - "bindgen 0.70.1", + "libc", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", ] [[package]] name = "cpal" -version = "0.15.3" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" dependencies = [ "alsa", - "core-foundation-sys", "coreaudio-rs", "dasp_sample", - "jack 0.11.4", + "jack", "jni", "js-sys", "libc", "mach2", "ndk", "ndk-context", - "oboe", + "num-derive", + "num-traits", + "objc2-audio-toolbox", + "objc2-core-audio", + "objc2-core-audio-types", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -531,18 +505,18 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -574,9 +548,9 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" dependencies = [ "darling_core", "darling_macro", @@ -584,27 +558,27 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 2.0.96", + "syn", ] [[package]] name = "darling_macro" -version = "0.20.10" +version = "0.20.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -615,15 +589,15 @@ checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" [[package]] name = "data-encoding" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" [[package]] name = "der" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ "const-oid", "pem-rfc7468", @@ -632,9 +606,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -657,7 +631,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -667,7 +641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core", - "syn 2.0.96", + "syn", ] [[package]] @@ -682,6 +656,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -690,7 +674,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -711,9 +695,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "encoding_rs" @@ -732,9 +716,9 @@ checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" [[package]] name = "enumflags2" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", @@ -742,13 +726,13 @@ dependencies = [ [[package]] name = "enumflags2_derive" -version = "0.7.10" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -762,38 +746,38 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcaee3d8e3cfc3fd92428d477bc97fc29ec8716d180c0d74c643bb26166660e0" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" dependencies = [ "anstream", "anstyle", "env_filter", - "humantime", + "jiff", "log", ] [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "event-listener" -version = "5.4.0" +version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", @@ -802,9 +786,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -824,9 +808,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.35" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "miniz_oxide", @@ -838,6 +822,27 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -901,6 +906,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -909,7 +927,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -960,23 +978,37 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ "unicode-width", ] [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", "wasm-bindgen", ] @@ -988,24 +1020,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.20.8" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8446d9b475730ebef81802c1738d972db42fde1c5a36a627ebc4d665fc87db04" +checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "glib" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f969edf089188d821a30cde713b6f9eb08b20c63fc2e584aba2892a7984a8cc0" +checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "futures-channel", "futures-core", "futures-executor", @@ -1022,22 +1054,22 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.7" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "715601f8f02e71baef9c1f94a657a9a77c192aea6097cf9ae7e5e177cd8cde68" +checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" dependencies = [ "heck", "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "glib-sys" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b360ff0f90d71de99095f79c526a5888c9c92fc9ee1b19da06c6f5e75f0c2a53" +checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" dependencies = [ "libc", "system-deps", @@ -1045,15 +1077,15 @@ dependencies = [ [[package]] name = "glob" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" [[package]] name = "gobject-sys" -version = "0.20.7" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a56235e971a63bfd75abb13ef70064e1346388723422a68580d8a6fbac6423" +checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" dependencies = [ "glib-sys", "libc", @@ -1062,28 +1094,30 @@ dependencies = [ [[package]] name = "governor" -version = "0.8.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842dc78579ce01e6a1576ad896edc92fca002dd60c9c3746b7fc2bec6fb429d0" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", "futures-sink", "futures-timer", "futures-util", - "no-std-compat", + "getrandom 0.3.3", + "hashbrown", "nonzero_ext", "parking_lot", "portable-atomic", - "rand", + "rand 0.9.2", "smallvec", "spinning_top", + "web-time", ] [[package]] name = "gstreamer" -version = "0.23.4" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "700cb1b2e86dda424f85eb728102a111602317e40b4dd71cf1c0dc04e0cc5d95" +checksum = "32f5db514ad5ccf70ad35485058aa8b894bb81cfcf76bb994af135d9789427c6" dependencies = [ "cfg-if", "futures-channel", @@ -1091,24 +1125,24 @@ dependencies = [ "futures-util", "glib", "gstreamer-sys", - "itertools 0.13.0", + "itertools 0.14.0", + "kstring", "libc", "muldiv", "num-integer", "num-rational", - "once_cell", "option-operations", "paste", "pin-project-lite", "smallvec", - "thiserror 2.0.11", + "thiserror 2.0.14", ] [[package]] name = "gstreamer-app" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41b7bda01190cf5000869083afbdd5acbe1ab86fbc523825898ba9ce777846c0" +checksum = "fad8ae64a7af6d1aa04e96db085a0cbd64a6b838d85c115c99fa053ab8902d98" dependencies = [ "futures-core", "futures-sink", @@ -1121,9 +1155,9 @@ dependencies = [ [[package]] name = "gstreamer-app-sys" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b0a5c2b149c629a46f21671118f491f61daab4469979105172fb2f8536b4e56" +checksum = "aaf1a3af017f9493c34ccc8439cbce5c48f6ddff6ec0514c23996b374ff25f9a" dependencies = [ "glib-sys", "gstreamer-base-sys", @@ -1134,9 +1168,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a6009b5c9c942cab1089956a501bd63778e65a3e69310949d173e90e2cdda2" +checksum = "7404c5d0cbb2189e6a10d05801e93f47fe60b195e4d73dd1c540d055f7b340b8" dependencies = [ "cfg-if", "glib", @@ -1144,15 +1178,14 @@ dependencies = [ "gstreamer-audio-sys", "gstreamer-base", "libc", - "once_cell", "smallvec", ] [[package]] name = "gstreamer-audio-sys" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef70a3d80e51ef9a45749a844cb8579d4cabe5ff59cb43a65d6f3a377943262f" +checksum = "626cd3130bc155a8b6d4ac48cfddc15774b5a6cc76fcb191aab09a2655bad8f5" dependencies = [ "glib-sys", "gobject-sys", @@ -1164,9 +1197,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d152db7983f98d5950cf64e53805286548063475fb61a5e5450fba4cec05899b" +checksum = "34745d3726a080e0d57e402a314e37073d0b341f3a5754258550311ca45e4754" dependencies = [ "atomic_refcell", "cfg-if", @@ -1178,9 +1211,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d47cc2d15f2a3d5eb129e5dacbbeec9600432b706805c15dff57b6aa11b2791c" +checksum = "dfad00fa63ddd8132306feef9d5095a3636192f09d925adfd0a9be0d82b9ea91" dependencies = [ "glib-sys", "gobject-sys", @@ -1191,10 +1224,11 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.23.4" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16cf1ae0a869aa7066ce3c685b76053b4b4f48f364a5b18c4b1f36ef57469719" +checksum = "36f46b35f9dc4b5a0dca3f19d2118bb5355c3112f228a99a84ed555f48ce5cf9" dependencies = [ + "cfg-if", "glib-sys", "gobject-sys", "libc", @@ -1203,35 +1237,16 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.26" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", "fnv", "futures-core", "futures-sink", - "http 1.2.0", + "http", "indexmap", "slab", "tokio", @@ -1241,20 +1256,25 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "headers" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "headers-core", - "http 1.2.0", + "http", "httpdate", "mime", "sha1", @@ -1266,7 +1286,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" dependencies = [ - "http 1.2.0", + "http", ] [[package]] @@ -1301,48 +1321,26 @@ dependencies = [ [[package]] name = "hostname" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows 0.52.0", + "windows-link", ] [[package]] name = "http" -version = "0.2.12" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ "bytes", "fnv", "itoa", ] -[[package]] -name = "http" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -1350,27 +1348,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.2.0", + "http", ] [[package]] name = "http-body-util" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", - "futures-util", - "http 1.2.0", - "http-body 1.0.1", + "futures-core", + "http", + "http-body", "pin-project-lite", ] [[package]] name = "httparse" -version = "1.9.5" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "httpdate" @@ -1378,48 +1376,18 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" -version = "0.14.32" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.7", - "http 1.2.0", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1438,8 +1406,8 @@ dependencies = [ "bytes", "futures-util", "headers", - "http 1.2.0", - "hyper 1.5.2", + "http", + "hyper", "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", @@ -1448,21 +1416,7 @@ dependencies = [ "tokio-rustls 0.25.0", "tower-service", "webpki", - "webpki-roots 0.26.7", -] - -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "rustls 0.21.12", - "tokio", - "tokio-rustls 0.24.1", + "webpki-roots 0.26.11", ] [[package]] @@ -1472,8 +1426,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 1.2.0", - "hyper 1.5.2", + "http", + "hyper", "hyper-util", "log", "rustls 0.22.4", @@ -1482,60 +1436,83 @@ dependencies = [ "tokio", "tokio-rustls 0.25.0", "tower-service", - "webpki-roots 0.26.7", + "webpki-roots 0.26.11", ] [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "futures-util", - "http 1.2.0", - "hyper 1.5.2", + "http", + "hyper", "hyper-util", "log", - "rustls 0.23.21", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.2", + "tower-service", + "webpki-roots 1.0.2", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", "tower-service", - "webpki-roots 0.26.7", ] [[package]] name = "hyper-util" -version = "0.1.10" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", - "http 1.2.0", - "http-body 1.0.1", - "hyper 1.5.2", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", - "socket2", + "socket2 0.6.0", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core 0.61.2", ] [[package]] @@ -1549,21 +1526,22 @@ dependencies = [ [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1572,31 +1550,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1604,67 +1562,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1684,9 +1629,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1704,9 +1649,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown", @@ -1714,18 +1659,39 @@ dependencies = [ [[package]] name = "inout" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "generic-array", ] [[package]] -name = "ipnet" -version = "2.10.1" +name = "io-uring" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] [[package]] name = "is-docker" @@ -1763,39 +1729,26 @@ dependencies = [ [[package]] name = "itertools" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" dependencies = [ "either", ] [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jack" -version = "0.11.4" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e5a18a3c2aefb354fb77111ade228b20267bdc779de84e7a4ccf7ea96b9a6cd" +checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 1.3.2", - "jack-sys", - "lazy_static", - "libc", - "log", -] - -[[package]] -name = "jack" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78a4ae24f4ee29676aef8330fed1104e72f314cab16643dbeb61bfd99b4a8273" -dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "jack-sys", "lazy_static", "libc", @@ -1816,6 +1769,30 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "jni" version = "0.21.1" @@ -1840,23 +1817,33 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ + "getrandom 0.3.3", "libc", ] [[package]] name = "js-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" dependencies = [ "once_cell", "wasm-bindgen", ] +[[package]] +name = "kstring" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558bf9508a558512042d3095138b1f7b8fe90c5467d94f9f1da28b3731c5dbd1" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1874,9 +1861,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "libloading" @@ -1890,19 +1877,19 @@ dependencies = [ [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] name = "libm" -version = "0.2.11" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmdns" @@ -1916,31 +1903,31 @@ dependencies = [ "if-addrs", "log", "multimap", - "rand", - "socket2", + "rand 0.8.5", + "socket2 0.5.10", "thiserror 1.0.69", "tokio", ] [[package]] name = "libpulse-binding" -version = "2.28.2" +version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1040a6c4c4d1e9e852000f6202df1a02a4f074320de336ab21e4fd317b538" +checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "libc", "libpulse-sys", - "num-derive 0.3.3", + "num-derive", "num-traits", "winapi", ] [[package]] name = "libpulse-simple-binding" -version = "2.28.1" +version = "2.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05fd6b68f33f6a251265e6ed1212dc3107caad7c5c6fdcd847b2e65ef58c308d" +checksum = "b7bebef0381c8e3e4b23cc24aaf36fab37472bece128de96f6a111efa464cfef" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1949,9 +1936,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.21.1" +version = "1.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6613b4199d8b9f0edcfb623e020cb17bbd0bee8dd21f3c7cc938de561c4152" +checksum = "3bd96888fe37ad270d16abf5e82cccca1424871cf6afa2861824d2a52758eebc" dependencies = [ "libpulse-sys", "pkg-config", @@ -1959,12 +1946,12 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.21.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc19e110fbf42c17260d30f6d3dc545f58491c7830d38ecb9aaca96e26067a9b" +checksum = "d74371848b22e989f829cc1621d2ebd74960711557d8b45cfe740f60d0a05e61" dependencies = [ "libc", - "num-derive 0.3.3", + "num-derive", "num-traits", "pkg-config", "winapi", @@ -1989,7 +1976,7 @@ dependencies = [ "log", "sha1", "sysinfo", - "thiserror 2.0.11", + "thiserror 2.0.14", "tokio", "url", ] @@ -2003,13 +1990,13 @@ dependencies = [ "ctr", "futures-util", "http-body-util", - "hyper 1.5.2", + "hyper", "hyper-util", "librespot-core", "log", "parking_lot", "tempfile", - "thiserror 2.0.11", + "thiserror 2.0.14", "tokio", ] @@ -2023,9 +2010,9 @@ dependencies = [ "librespot-protocol", "log", "protobuf", - "rand", + "rand 0.9.2", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.14", "tokio", "tokio-stream", "uuid", @@ -2036,7 +2023,7 @@ name = "librespot-core" version = "0.6.0-dev" dependencies = [ "aes", - "base64 0.22.1", + "base64", "byteorder", "bytes", "data-encoding", @@ -2046,22 +2033,21 @@ dependencies = [ "futures-util", "governor", "hmac", - "http 1.2.0", + "http", "http-body-util", "httparse", - "hyper 1.5.2", + "hyper", "hyper-proxy2", - "hyper-rustls 0.27.5", + "hyper-rustls 0.27.7", "hyper-util", "librespot-oauth", "librespot-protocol", "log", "nonzero_ext", "num-bigint", - "num-derive 0.4.2", + "num-derive", "num-integer", "num-traits", - "once_cell", "parking_lot", "pbkdf2", "pin-project-lite", @@ -2069,14 +2055,15 @@ dependencies = [ "protobuf", "protobuf-json-mapping", "quick-xml", - "rand", + "rand 0.9.2", + "rand_distr", "rsa", "serde", "serde_json", "sha1", "shannon", "sysinfo", - "thiserror 2.0.11", + "thiserror 2.0.14", "time", "tokio", "tokio-stream", @@ -2092,7 +2079,7 @@ name = "librespot-discovery" version = "0.6.0-dev" dependencies = [ "aes", - "base64 0.22.1", + "base64", "bytes", "ctr", "dns-sd", @@ -2103,17 +2090,17 @@ dependencies = [ "hex", "hmac", "http-body-util", - "hyper 1.5.2", + "hyper", "hyper-util", "libmdns", "librespot-core", "log", - "rand", + "rand 0.9.2", "serde", "serde_json", "serde_repr", "sha1", - "thiserror 2.0.11", + "thiserror 2.0.14", "tokio", "zbus", ] @@ -2130,7 +2117,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 2.0.11", + "thiserror 2.0.14", "uuid", ] @@ -2142,7 +2129,8 @@ dependencies = [ "log", "oauth2", "open", - "thiserror 2.0.11", + "reqwest", + "thiserror 2.0.14", "tokio", "url", ] @@ -2158,7 +2146,7 @@ dependencies = [ "gstreamer", "gstreamer-app", "gstreamer-audio", - "jack 0.13.0", + "jack", "libpulse-binding", "libpulse-simple-binding", "librespot-audio", @@ -2169,15 +2157,15 @@ dependencies = [ "parking_lot", "portable-atomic", "portaudio-rs", - "rand", + "rand 0.9.2", "rand_distr", "rodio", "sdl2", "shell-words", "symphonia", - "thiserror 2.0.11", + "thiserror 2.0.14", "tokio", - "zerocopy 0.8.14", + "zerocopy", ] [[package]] @@ -2195,16 +2183,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] -name = "litemap" -version = "0.7.4" +name = "linux-raw-sys" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -2212,24 +2206,30 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] name = "mach2" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" dependencies = [ "libc", ] [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "memoffset" @@ -2254,22 +2254,22 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.2" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", - "windows-sys 0.52.0", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] @@ -2280,20 +2280,37 @@ checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" [[package]] name = "multimap" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" dependencies = [ "serde", ] [[package]] -name = "ndk" -version = "0.8.0" +name = "native-tls" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ - "bitflags 2.7.0", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.9.1", "jni-sys", "log", "ndk-sys", @@ -2309,32 +2326,26 @@ checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" [[package]] name = "ndk-sys" -version = "0.5.0+25.2.9519653" +version = "0.6.0+11769913" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" dependencies = [ "jni-sys", ] [[package]] name = "nix" -version = "0.29.0" +version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "cfg-if", "cfg_aliases", "libc", "memoffset", ] -[[package]] -name = "no-std-compat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" - [[package]] name = "nom" version = "7.1.3" @@ -2368,7 +2379,6 @@ checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" dependencies = [ "num-integer", "num-traits", - "rand", ] [[package]] @@ -2383,7 +2393,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -2394,17 +2404,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-derive" version = "0.4.2" @@ -2413,7 +2412,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -2442,6 +2441,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" dependencies = [ + "num-bigint", "num-integer", "num-traits", ] @@ -2458,23 +2458,24 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" dependencies = [ "num_enum_derive", + "rustversion", ] [[package]] name = "num_enum_derive" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -2488,15 +2489,15 @@ dependencies = [ [[package]] name = "oauth2" -version = "4.4.2" +version = "5.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c38841cdd844847e3e7c8d29cef9dcfed8877f8f56f9071f77843ecf3baf937f" +checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d" dependencies = [ - "base64 0.13.1", + "base64", "chrono", - "getrandom", - "http 0.2.12", - "rand", + "getrandom 0.2.16", + "http", + "rand 0.8.5", "reqwest", "serde", "serde_json", @@ -2506,6 +2507,88 @@ dependencies = [ "url", ] +[[package]] +name = "objc2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-audio-toolbox" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" +dependencies = [ + "bitflags 2.9.1", + "libc", + "objc2", + "objc2-core-audio", + "objc2-core-audio-types", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-audio" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82" +dependencies = [ + "dispatch2", + "objc2", + "objc2-core-audio-types", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-core-audio-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" +dependencies = [ + "bitflags 2.9.1", + "objc2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" +dependencies = [ + "bitflags 2.9.1", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c" +dependencies = [ + "objc2", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c1c64d6120e51cd86033f67176b1cb66780c2efe34dec55176f77befd93c0a" +dependencies = [ + "libc", + "objc2-core-foundation", +] + [[package]] name = "object" version = "0.36.7" @@ -2515,43 +2598,26 @@ dependencies = [ "memchr", ] -[[package]] -name = "oboe" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" -dependencies = [ - "jni", - "ndk", - "ndk-context", - "num-derive 0.4.2", - "num-traits", - "oboe-sys", -] - -[[package]] -name = "oboe-sys" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" -dependencies = [ - "cc", -] - [[package]] name = "ogg" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5477016638150530ba21dec7caac835b29ef69b20865751d2973fce6be386cf1" +checksum = "fdab8dcd8d4052eaacaf8fb07a3ccd9a6e26efadb42878a413c68fc4af1dee2b" dependencies = [ "byteorder", ] [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] name = "open" @@ -2565,10 +2631,48 @@ dependencies = [ ] [[package]] -name = "openssl-probe" -version = "0.1.5" +name = "openssl" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] [[package]] name = "option-operations" @@ -2597,9 +2701,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2607,9 +2711,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "backtrace", "cfg-if", @@ -2703,15 +2807,24 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] [[package]] name = "portaudio-rs" @@ -2734,6 +2847,15 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -2742,28 +2864,28 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy 0.7.35", + "zerocopy", ] [[package]] name = "prettyplease" -version = "0.2.28" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924b9a625d6df5b74b0b3cfbb5669b3f62ddf3d46a677ce12b1945471b4ae5c3" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" dependencies = [ "proc-macro2", - "syn 2.0.96", + "syn", ] [[package]] name = "priority-queue" -version = "2.1.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714c75db297bc88a63783ffc6ab9f830698a6705aa0201416931759ef4c8183d" +checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" dependencies = [ "autocfg", "equivalent", @@ -2772,27 +2894,27 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" dependencies = [ "unicode-ident", ] [[package]] name = "protobuf" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a7c64d9bf75b1b8d981124c14c179074e8caa7dfe7b6a12e6222ddcd0c8f72" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" dependencies = [ "once_cell", "protobuf-support", @@ -2801,9 +2923,9 @@ dependencies = [ [[package]] name = "protobuf-codegen" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e26b833f144769a30e04b1db0146b2aaa53fd2fd83acf10a6b5f996606c18144" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" dependencies = [ "anyhow", "once_cell", @@ -2816,9 +2938,9 @@ dependencies = [ [[package]] name = "protobuf-json-mapping" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b445cf83c9303695e6c423d269759e139b6182d2f1171e18afda7078a764336" +checksum = "e0d6e4be637b310d8a5c02fa195243328e2d97fa7df1127a27281ef1187fcb1d" dependencies = [ "protobuf", "protobuf-support", @@ -2827,9 +2949,9 @@ dependencies = [ [[package]] name = "protobuf-parse" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322330e133eab455718444b4e033ebfac7c6528972c784fcde28d2cc783c6257" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" dependencies = [ "anyhow", "indexmap", @@ -2843,32 +2965,93 @@ dependencies = [ [[package]] name = "protobuf-support" -version = "3.7.1" +version = "3.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b088fd20b938a875ea00843b6faf48579462630015c3788d397ad6a786663252" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" dependencies = [ "thiserror 1.0.69", ] [[package]] name = "quick-xml" -version = "0.37.2" +version = "0.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003" +checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" dependencies = [ "memchr", "serde", ] [[package]] -name = "quote" -version = "1.0.38" +name = "quinn" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.1", + "rustls 0.23.31", + "socket2 0.5.10", + "thiserror 2.0.14", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +dependencies = [ + "bytes", + "getrandom 0.3.3", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash 2.1.1", + "rustls 0.23.31", + "rustls-pki-types", + "slab", + "thiserror 2.0.14", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.5.10", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -2876,8 +3059,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -2887,7 +3080,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -2896,26 +3099,35 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", ] [[package]] name = "rand_distr" -version = "0.4.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" dependencies = [ "num-traits", - "rand", + "rand 0.9.2", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", ] [[package]] @@ -2949,74 +3161,80 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ - "base64 0.21.7", + "base64", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.3.26", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "ipnet", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls 0.27.7", + "hyper-tls", + "hyper-util", "js-sys", "log", "mime", - "once_cell", + "native-tls", "percent-encoding", "pin-project-lite", - "rustls 0.21.12", - "rustls-pemfile 1.0.4", + "quinn", + "rustls 0.23.31", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", - "tokio-rustls 0.24.1", + "tokio-native-tls", + "tokio-rustls 0.26.2", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 0.25.4", - "winreg", + "webpki-roots 1.0.2", ] [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rodio" -version = "0.20.1" +version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183" dependencies = [ "cpal", + "dasp_sample", + "num-rational", ] [[package]] name = "rsa" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519" +checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b" dependencies = [ "const-oid", "digest", @@ -3025,7 +3243,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -3034,9 +3252,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -3045,28 +3263,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] -name = "rustix" -version = "0.38.43" +name = "rustc-hash" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a78891ee6bf2340288408954ac787aa063d8e8817e9f53abb37c695c6d834ef6" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.15", "windows-sys 0.59.0", ] [[package]] -name = "rustls" -version = "0.21.12" +name = "rustix" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", ] [[package]] @@ -3085,15 +3310,16 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", - "rustls-webpki 0.102.8", + "rustls-webpki 0.103.4", "subtle", "zeroize", ] @@ -3105,7 +3331,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", + "rustls-pemfile", "rustls-pki-types", "schannel", "security-framework 2.11.1", @@ -3120,16 +3346,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.2.0", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework 3.3.0", ] [[package]] @@ -3143,18 +3360,12 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" - -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "ring", - "untrusted", + "web-time", + "zeroize", ] [[package]] @@ -3162,6 +3373,17 @@ name = "rustls-webpki" version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "aws-lc-rs", "ring", @@ -3171,15 +3393,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "same-file" @@ -3205,21 +3427,11 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "sdl2" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" dependencies = [ "bitflags 1.3.2", "lazy_static", @@ -3229,9 +3441,9 @@ dependencies = [ [[package]] name = "sdl2-sys" -version = "0.37.0" +version = "0.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" dependencies = [ "cfg-if", "libc", @@ -3244,7 +3456,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.7.0", + "bitflags 2.9.1", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3253,12 +3465,12 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.2.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.7.0", - "core-foundation 0.10.0", + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3276,29 +3488,29 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ "itoa", "memchr", @@ -3308,9 +3520,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" dependencies = [ "itoa", "serde", @@ -3318,20 +3530,20 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -3361,9 +3573,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", @@ -3393,9 +3605,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.2" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] @@ -3407,34 +3619,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] name = "slab" -version = "0.4.9" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "spin" version = "0.9.8" @@ -3570,20 +3789,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.109" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.96" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -3592,50 +3800,54 @@ dependencies = [ [[package]] name = "sync_wrapper" -version = "0.1.2" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "sysinfo" -version = "0.33.1" +version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" dependencies = [ - "core-foundation-sys", "libc", "memchr", "ntapi", - "windows 0.57.0", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", ] [[package]] name = "system-configuration" -version = "0.5.1" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.9.1", "core-foundation 0.9.4", "system-configuration-sys", ] [[package]] name = "system-configuration-sys" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" dependencies = [ "core-foundation-sys", "libc", @@ -3643,9 +3855,9 @@ dependencies = [ [[package]] name = "system-deps" -version = "7.0.3" +version = "7.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d23aaf9f331227789a99e8de4c91bf46703add012bdfd45fdecdfb2975a005" +checksum = "e4be53aa0cba896d2dc615bd42bbc130acdcffa239e0a2d965ea5b3b2a86ffdb" dependencies = [ "cfg-expr", "heck", @@ -3656,21 +3868,20 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.16" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.15.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8a559c81686f576e8cd0290cd2a24a2a9ad80c98b3478856500fcbd7acd704" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ - "cfg-if", "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -3685,11 +3896,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.11" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" dependencies = [ - "thiserror-impl 2.0.11", + "thiserror-impl 2.0.14", ] [[package]] @@ -3700,18 +3911,18 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "thiserror-impl" -version = "2.0.11" +version = "2.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] @@ -3726,9 +3937,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -3743,15 +3954,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -3759,31 +3970,48 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", ] [[package]] -name = "tokio" -version = "1.43.0" +name = "tinyvec" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", + "io-uring", "libc", "mio", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "slab", + "socket2 0.6.0", "tokio-macros", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3794,16 +4022,16 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] -name = "tokio-rustls" -version = "0.24.1" +name = "tokio-native-tls" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ - "rustls 0.21.12", + "native-tls", "tokio", ] @@ -3820,11 +4048,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.21", + "rustls 0.23.31", "tokio", ] @@ -3841,26 +4069,26 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", - "rustls 0.23.21", + "rustls 0.23.31", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.1", + "tokio-rustls 0.26.2", "tungstenite", - "webpki-roots 0.26.7", + "webpki-roots 0.26.11", ] [[package]] name = "tokio-util" -version = "0.7.13" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -3871,9 +4099,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.19" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -3883,18 +4111,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.8" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.22" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -3903,6 +4131,45 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + [[package]] name = "tower-service" version = "0.3.3" @@ -3922,20 +4189,20 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.28" +version = "0.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -3948,29 +4215,28 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.24.0" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ - "byteorder", "bytes", "data-encoding", - "http 1.2.0", + "http", "httparse", "log", - "rand", - "rustls 0.23.21", + "rand 0.9.2", + "rustls 0.23.31", "rustls-pki-types", "sha1", - "thiserror 1.0.69", + "thiserror 2.0.14", "utf-8", ] [[package]] name = "typenum" -version = "1.17.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" [[package]] name = "uds_windows" @@ -3985,15 +4251,15 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-width" -version = "0.1.14" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "untrusted" @@ -4019,12 +4285,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -4039,19 +4299,26 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.11.1" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b913a3b5fe84142e269d63cc62b64319ccaf89b748fc31fe025177f767a756c4" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" dependencies = [ - "getrandom", - "rand", + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] -name = "vergen" -version = "9.0.4" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d2f179f8075b805a43a2a21728a46f0cc2921b3c58695b28fa8817e103cd9a" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "9.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" dependencies = [ "anyhow", "derive_builder", @@ -4062,9 +4329,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f89d70a58a4506a6079cedf575c64cf51649ccbb4e02a63dac539b264b7711" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" dependencies = [ "anyhow", "derive_builder", @@ -4124,40 +4391,50 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] [[package]] name = "wasm-bindgen" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", "once_cell", + "rustversion", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", "proc-macro2", "quote", - "syn 2.0.96", + "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.49" +version = "0.4.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" dependencies = [ "cfg-if", "js-sys", @@ -4168,9 +4445,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4178,28 +4455,41 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.99" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "web-sys" -version = "0.3.76" +version = "0.3.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" dependencies = [ "js-sys", "wasm-bindgen", @@ -4217,15 +4507,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.2", +] [[package]] name = "webpki-roots" -version = "0.26.7" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -4239,7 +4532,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.44", ] [[package]] @@ -4273,16 +4566,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.54.0" @@ -4295,21 +4578,24 @@ dependencies = [ [[package]] name = "windows" -version = "0.57.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.61.2", ] [[package]] @@ -4318,42 +4604,81 @@ version = "0.54.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" dependencies = [ - "windows-result", + "windows-result 0.1.2", "windows-targets 0.52.6", ] [[package]] name = "windows-core" -version = "0.57.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-result", - "windows-targets 0.52.6", + "windows-link", + "windows-result 0.3.4", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.57.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "windows-interface" -version = "0.57.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link", +] + +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result 0.3.4", + "windows-strings", ] [[package]] @@ -4365,6 +4690,24 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -4374,15 +4717,6 @@ dependencies = [ "windows-targets 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -4401,6 +4735,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4416,21 +4759,6 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -4440,139 +4768,165 @@ dependencies = [ "windows_aarch64_gnullvm 0.52.6", "windows_aarch64_msvc 0.52.6", "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", + "windows_i686_gnullvm 0.52.6", "windows_i686_msvc 0.52.6", "windows_x86_64_gnu 0.52.6", "windows_x86_64_gnullvm 0.52.6", "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + [[package]] name = "windows_i686_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + [[package]] name = "windows_i686_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -4580,51 +4934,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "winnow" -version = "0.6.24" +name = "windows_x86_64_msvc" +version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d71a593cc5c42ad7876e2c1fda56f314f3754c084128833e64f1345ff8a03a" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] [[package]] -name = "winreg" -version = "0.50.0" +name = "wit-bindgen-rt" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "cfg-if", - "windows-sys 0.48.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "xdg-home" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -4634,21 +4977,21 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", "synstructure", ] [[package]] name = "zbus" -version = "5.3.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a0d989036cd60a1e91a54c9851fb9ad5bd96125d41803eed79d2e2ef74bd7" +checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" dependencies = [ "async-broadcast", "async-recursion", @@ -4656,19 +4999,17 @@ dependencies = [ "enumflags2", "event-listener", "futures-core", - "futures-util", + "futures-lite", "hex", "nix", "ordered-stream", "serde", "serde_repr", - "static_assertions", "tokio", "tracing", "uds_windows", "windows-sys 0.59.0", "winnow", - "xdg-home", "zbus_macros", "zbus_names", "zvariant", @@ -4676,14 +5017,14 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.3.0" +version = "5.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3685b5c81fce630efc3e143a4ded235b107f1b1cdf186c3f115529e5e5ae4265" +checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn", "zbus_names", "zvariant", "zvariant_utils", @@ -4691,9 +5032,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "4.1.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "856b7a38811f71846fd47856ceee8bccaec8399ff53fb370247e66081ace647b" +checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", @@ -4703,63 +5044,42 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" dependencies = [ - "byteorder", - "zerocopy-derive 0.7.35", -] - -[[package]] -name = "zerocopy" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a367f292d93d4eab890745e75a778da40909cab4d6ff8173693812f79c4a2468" -dependencies = [ - "zerocopy-derive 0.8.14", + "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3931cb58c62c13adec22e38686b559c86a30565e16ad6e8510a337cedc611e1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "zerofrom" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", "synstructure", ] @@ -4770,10 +5090,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" [[package]] -name = "zerovec" -version = "0.10.4" +name = "zerotrie" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", @@ -4782,25 +5113,24 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.96", + "syn", ] [[package]] name = "zvariant" -version = "5.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55e6b9b5f1361de2d5e7d9fd1ee5f6f7fcb6060618a1f82f3472f58f2b8d4be9" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", - "static_assertions", "winnow", "zvariant_derive", "zvariant_utils", @@ -4808,27 +5138,27 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.2.0" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573a8dd76961957108b10f7a45bac6ab1ea3e9b7fe01aff88325dc57bb8f5c8b" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.96", + "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" -version = "3.1.0" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd46446ea2a1f353bfda53e35f17633afa79f4fe290a611c94645c69fe96a50" +checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" dependencies = [ "proc-macro2", "quote", "serde", "static_assertions", - "syn 2.0.96", + "syn", "winnow", ] diff --git a/Cargo.toml b/Cargo.toml index ae89da48..831b4f5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,14 +56,25 @@ version = "0.6.0-dev" [dependencies] data-encoding = "2.5" -env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } +env_logger = { version = "0.11.2", default-features = false, features = [ + "color", + "humantime", + "auto-color", +] } futures-util = { version = "0.3", default-features = false } getopts = "0.2" log = "0.4" sha1 = "0.10" -sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } -thiserror = "2.0" -tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } +sysinfo = { version = "0.37", default-features = false, features = ["system"] } +thiserror = "2" +tokio = { version = "1", features = [ + "rt", + "macros", + "signal", + "sync", + "parking_lot", + "process", +] } url = "2.2" [features] @@ -97,9 +108,21 @@ available in the official library.""" 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"] + [ + "target/release/librespot", + "usr/bin/", + "755", + ], + [ + "contrib/librespot.service", + "lib/systemd/system/", + "644", + ], + [ + "contrib/librespot.user.service", + "lib/systemd/user/", + "644", + ], ] [workspace.package] diff --git a/audio/Cargo.toml b/audio/Cargo.toml index b6da63b5..e1d24352 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -17,11 +17,11 @@ aes = "0.8" bytes = "1" ctr = "0.9" futures-util = "0.3" -hyper = { version = "1.3", features = [] } +hyper = "1.6" hyper-util = { version = "0.1", features = ["client"] } -http-body-util = "0.1.1" +http-body-util = "0.1" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" -thiserror = "2.0" +thiserror = "2" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 74b21b7d..216842aa 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -11,13 +11,13 @@ edition = "2021" [dependencies] futures-util = "0.3" log = "0.4" -protobuf = "3.5" -rand = { version = "0.8", default-features = false, features = ["small_rng"] } +protobuf = "3.7" +rand = { version = "0.9", default-features = false, features = ["small_rng"] } serde_json = "1.0" -thiserror = "2.0" +thiserror = "2" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio-stream = "0.1" -uuid = { version = "1.11.0", features = ["v4"] } +uuid = { version = "1.18", features = ["v4"] } [dependencies.librespot-core] path = "../core" diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs index c5367598..01000cde 100644 --- a/connect/src/shuffle_vec.rs +++ b/connect/src/shuffle_vec.rs @@ -58,7 +58,7 @@ impl ShuffleVec { let indices: Vec<_> = { (1..self.vec.len()) .rev() - .map(|i| rng.gen_range(0..i + 1)) + .map(|i| rng.random_range(0..i + 1)) .collect() }; @@ -89,7 +89,7 @@ mod test { #[test] fn test_shuffle_with_seed() { - let seed = rand::thread_rng().gen_range(0..10000000000000); + let seed = rand::rng().random_range(0..10000000000000); let vec = (0..100).collect::>(); let base_vec: ShuffleVec = vec.into(); diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index 6f384810..80c62e6b 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -68,8 +68,8 @@ impl ConnectState { // we don't need to include the current track, because it is already being played ctx.skip_track = current_track; - let seed = seed - .unwrap_or_else(|| rand::thread_rng().gen_range(100_000_000_000..1_000_000_000_000)); + let seed = + seed.unwrap_or_else(|| rand::rng().random_range(100_000_000_000..1_000_000_000_000)); ctx.tracks.shuffle_with_seed(seed); ctx.set_shuffle_seed(seed); diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 07d03991..c5da0351 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -69,7 +69,7 @@ impl<'ct> ConnectState { pub fn set_current_track_random(&mut self) -> Result<(), Error> { let max_tracks = self.get_context(self.active_context)?.tracks.len(); - let rng_track = rand::thread_rng().gen_range(0..max_tracks); + let rng_track = rand::rng().random_range(0..max_tracks); self.set_current_track(rng_track) } diff --git a/core/Cargo.toml b/core/Cargo.toml index 62355325..aedaf064 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -20,64 +20,104 @@ version = "0.6.0-dev" [dependencies] aes = "0.8" base64 = "0.22" -byteorder = "1.4" +byteorder = "1.5" bytes = "1" -form_urlencoded = "1.0" +form_urlencoded = "1.2" futures-core = "0.3" -futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } -governor = { version = "0.8", default-features = false, features = ["std", "jitter"] } +futures-util = { version = "0.3", features = [ + "alloc", + "bilock", + "sink", + "unstable", +] } +governor = { version = "0.10", default-features = false, features = [ + "std", + "jitter", +] } hmac = "0.12" -httparse = "1.7" -http = "1.0" -hyper = { version = "1.3", features = ["http1", "http2"] } +httparse = "1.10" +http = "1.3" +hyper = { version = "1.6", features = ["http1", "http2"] } hyper-util = { version = "0.1", features = ["client"] } -http-body-util = "0.1.1" +http-body-util = "0.1" log = "0.4" nonzero_ext = "0.3" -num-bigint = { version = "0.4", features = ["rand"] } +num-bigint = "0.4" num-derive = "0.4" num-integer = "0.1" num-traits = "0.2" -once_cell = "1" parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pin-project-lite = "0.2" -priority-queue = "2.0" -protobuf = "3.5" -quick-xml = { version = "0.37.1", features = ["serialize"] } -rand = "0.8" -rsa = "0.9.2" +priority-queue = "2.5" +protobuf = "3.7" +quick-xml = { version = "0.38", features = ["serialize"] } +rand = "0.9" +rsa = "0.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } -thiserror = "2.0" +sysinfo = { version = "0.37", default-features = false, features = ["system"] } +thiserror = "2" time = { version = "0.3", features = ["formatting", "parsing"] } -tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] } +tokio = { version = "1", features = [ + "io-util", + "macros", + "net", + "parking_lot", + "rt", + "sync", + "time", +] } tokio-stream = "0.1" tokio-util = { version = "0.7", features = ["codec"] } url = "2" -uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } -data-encoding = "2.5" -flate2 = "1.0.33" -protobuf-json-mapping = "3.5" +uuid = { version = "1", default-features = false, features = ["v4"] } +data-encoding = "2.9" +flate2 = "1.1" +protobuf-json-mapping = "3.7" # Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies # but currently, hyper-proxy2 and tokio-tungstenite do not support it. [target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies] -hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } -hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "native-tokio", "http2"] } -tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } +hyper-proxy2 = { version = "0.1", default-features = false, features = [ + "rustls", +] } +hyper-rustls = { version = "0.27", default-features = false, features = [ + "aws-lc-rs", + "http1", + "logging", + "tls12", + "native-tokio", + "http2", +] } +tokio-tungstenite = { version = "0.27", default-features = false, features = [ + "rustls-tls-native-roots", +] } [target.'cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))'.dependencies] -hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls-webpki"] } -hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "webpki-tokio", "http2"] } -tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-webpki-roots"] } +hyper-proxy2 = { version = "0.1", default-features = false, features = [ + "rustls-webpki", +] } +hyper-rustls = { version = "0.27", default-features = false, features = [ + "aws-lc-rs", + "http1", + "logging", + "tls12", + "webpki-tokio", + "http2", +] } +tokio-tungstenite = { version = "0.27", default-features = false, features = [ + "rustls-tls-webpki-roots", +] } [build-dependencies] -rand = "0.8" -vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } +rand = "0.9" +rand_distr = "0.5" +vergen-gitcl = { version = "1.0", default-features = false, features = [ + "build", +] } [dev-dependencies] tokio = { version = "1", features = ["macros", "parking_lot"] } diff --git a/core/build.rs b/core/build.rs index 008ec1d2..4413d2f3 100644 --- a/core/build.rs +++ b/core/build.rs @@ -1,4 +1,5 @@ -use rand::{distributions::Alphanumeric, Rng}; +use rand::Rng; +use rand_distr::Alphanumeric; use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; fn main() -> Result<(), Box> { @@ -18,7 +19,7 @@ fn main() -> Result<(), Box> { .expect("Unable to generate the cargo keys!"); let build_id = match std::env::var("SOURCE_DATE_EPOCH") { Ok(val) => val, - Err(_) => rand::thread_rng() + Err(_) => rand::rng() .sample_iter(Alphanumeric) .take(8) .map(char::from) diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 0d1da46c..1dea18ac 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -3,7 +3,7 @@ use std::{env::consts::ARCH, io}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac}; use protobuf::Message; -use rand::{thread_rng, RngCore}; +use rand::RngCore; use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use sha1::{Digest, Sha1}; use thiserror::Error; @@ -49,7 +49,7 @@ pub enum HandshakeError { pub async fn handshake( mut connection: T, ) -> io::Result> { - let local_keys = DhLocalKeys::random(&mut thread_rng()); + let local_keys = DhLocalKeys::random(&mut rand::rng()); let gc = local_keys.public_key(); let mut accumulator = client_hello(&mut connection, gc).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; @@ -108,7 +108,7 @@ where T: AsyncWrite + Unpin, { let mut client_nonce = vec![0; 0x10]; - thread_rng().fill_bytes(&mut client_nonce); + rand::rng().fill_bytes(&mut client_nonce); let platform = match crate::config::OS { "freebsd" | "netbsd" | "openbsd" => match ARCH { diff --git a/core/src/dealer/manager.rs b/core/src/dealer/manager.rs index 792deca3..ebe5ddc5 100644 --- a/core/src/dealer/manager.rs +++ b/core/src/dealer/manager.rs @@ -1,6 +1,6 @@ use futures_core::Stream; use futures_util::StreamExt; -use std::{cell::OnceCell, pin::Pin, str::FromStr}; +use std::{pin::Pin, str::FromStr, sync::OnceLock}; use thiserror::Error; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; @@ -14,8 +14,8 @@ use crate::{Error, Session}; component! { DealerManager: DealerManagerInner { - builder: OnceCell = OnceCell::from(Builder::new()), - dealer: OnceCell = OnceCell::new(), + builder: OnceLock = OnceLock::from(Builder::new()), + dealer: OnceLock = OnceLock::new(), } } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 4d79f67c..8934daab 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -88,7 +88,7 @@ impl Responder { }) .to_string(); - if let Err(e) = self.tx.send(WsMessage::Text(response)) { + if let Err(e) = self.tx.send(WsMessage::Text(response.into())) { warn!("Wasn't able to reply to dealer request: {}", e); } } @@ -586,7 +586,10 @@ async fn connect( timer.tick().await; pong_received.store(false, atomic::Ordering::Relaxed); - if send_tx.send(WsMessage::Ping(vec![])).is_err() { + if send_tx + .send(WsMessage::Ping(bytes::Bytes::default())) + .is_err() + { // The sender is closed. break; } diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index 57caa029..283aa026 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -1,11 +1,12 @@ -use num_bigint::{BigUint, RandBigInt}; +use std::sync::LazyLock; + +use num_bigint::BigUint; use num_integer::Integer; use num_traits::{One, Zero}; -use once_cell::sync::Lazy; use rand::{CryptoRng, Rng}; -static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); -static DH_PRIME: Lazy = Lazy::new(|| { +static DH_GENERATOR: LazyLock = LazyLock::new(|| BigUint::from_bytes_be(&[0x02])); +static DH_PRIME: LazyLock = LazyLock::new(|| { BigUint::from_bytes_be(&[ 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67, @@ -40,7 +41,9 @@ pub struct DhLocalKeys { impl DhLocalKeys { pub fn random(rng: &mut R) -> DhLocalKeys { - let private_key = rng.gen_biguint(95 * 8); + let mut bytes = [0u8; 95]; + rng.fill_bytes(&mut bytes); + let private_key = BigUint::from_bytes_le(&bytes); let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME); DhLocalKeys { diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 0b932ad9..1bb464d8 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + sync::OnceLock, time::{Duration, Instant}, }; @@ -18,7 +19,6 @@ use hyper_util::{ rt::TokioExecutor, }; use nonzero_ext::nonzero; -use once_cell::sync::OnceCell; use parking_lot::Mutex; use thiserror::Error; use url::Url; @@ -94,7 +94,7 @@ type HyperClient = Client>, Full, - hyper_client: OnceCell, + hyper_client: OnceLock, // while the DashMap variant is more performant, our level of concurrency // is pretty low so we can save pulling in that extra dependency @@ -138,7 +138,7 @@ impl HttpClient { Self { user_agent, proxy_url: proxy_url.cloned(), - hyper_client: OnceCell::new(), + hyper_client: OnceLock::new(), rate_limiter, } } @@ -170,9 +170,9 @@ impl HttpClient { Ok(client) } - fn hyper_client(&self) -> Result<&HyperClient, Error> { + fn hyper_client(&self) -> &HyperClient { self.hyper_client - .get_or_try_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref())) + .get_or_init(|| Self::try_create_hyper_client(self.proxy_url.as_ref()).unwrap()) } pub async fn request(&self, req: Request) -> Result, Error> { @@ -253,7 +253,7 @@ impl HttpClient { )) })?; - Ok(self.hyper_client()?.request(req.map(Full::new))) + Ok(self.hyper_client().request(req.map(Full::new))) } pub fn get_retry_after(headers: &HeaderMap) -> Option { diff --git a/core/src/session.rs b/core/src/session.rs index 92933e35..3d39b8c7 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -4,6 +4,7 @@ use std::{ io, pin::Pin, process::exit, + sync::OnceLock, sync::{Arc, Weak}, task::{Context, Poll}, time::{Duration, SystemTime, UNIX_EPOCH}, @@ -33,7 +34,6 @@ use futures_core::TryStream; use futures_util::StreamExt; use librespot_protocol::authentication::AuthenticationType; use num_traits::FromPrimitive; -use once_cell::sync::OnceCell; use parking_lot::RwLock; use pin_project_lite::pin_project; use quick_xml::events::Event; @@ -68,6 +68,12 @@ impl From for Error { } } +impl From for Error { + fn from(err: quick_xml::encoding::EncodingError) -> Self { + Error::invalid_argument(err) + } +} + pub type UserAttributes = HashMap; #[derive(Debug, Clone, Default)] @@ -96,16 +102,16 @@ struct SessionInternal { data: RwLock, http_client: HttpClient, - tx_connection: OnceCell)>>, + tx_connection: OnceLock)>>, - apresolver: OnceCell, - audio_key: OnceCell, - channel: OnceCell, - mercury: OnceCell, - dealer: OnceCell, - spclient: OnceCell, - token_provider: OnceCell, - login5: OnceCell, + apresolver: OnceLock, + audio_key: OnceLock, + channel: OnceLock, + mercury: OnceLock, + dealer: OnceLock, + spclient: OnceLock, + token_provider: OnceLock, + login5: OnceLock, cache: Option>, handle: tokio::runtime::Handle, @@ -140,16 +146,16 @@ impl Session { config, data: RwLock::new(session_data), http_client, - tx_connection: OnceCell::new(), + tx_connection: OnceLock::new(), cache: cache.map(Arc::new), - apresolver: OnceCell::new(), - audio_key: OnceCell::new(), - channel: OnceCell::new(), - mercury: OnceCell::new(), - dealer: OnceCell::new(), - spclient: OnceCell::new(), - token_provider: OnceCell::new(), - login5: OnceCell::new(), + apresolver: OnceLock::new(), + audio_key: OnceLock::new(), + channel: OnceLock::new(), + mercury: OnceLock::new(), + dealer: OnceLock::new(), + spclient: OnceLock::new(), + token_provider: OnceLock::new(), + login5: OnceLock::new(), handle: tokio::runtime::Handle::current(), })) } @@ -688,8 +694,10 @@ where } Ok(Event::Text(ref value)) => { if !current_element.is_empty() { - let _ = user_attributes - .insert(current_element.clone(), value.unescape()?.to_string()); + let _ = user_attributes.insert( + current_element.clone(), + value.xml_content()?.to_string(), + ); } } Ok(Event::Eof) => break, diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 272975d1..3b3c679f 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -483,7 +483,7 @@ impl SpClient { url, "{}salt={}", util::get_next_query_separator(&url), - rand::thread_rng().next_u32() + rand::rng().next_u32() ); } diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 8eac51a5..8cfbfa3a 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -13,24 +13,32 @@ aes = "0.8" base64 = "0.22" bytes = "1" ctr = "0.9" -dns-sd = { version = "0.1.3", optional = true } -form_urlencoded = "1.0" +dns-sd = { version = "0.1", optional = true } +form_urlencoded = "1.2" futures-core = "0.3" futures-util = "0.3" hmac = "0.12" -hyper = { version = "1.3", features = ["http1"] } -hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } -http-body-util = "0.1.1" +hyper = { version = "1.6", features = ["http1"] } +hyper-util = { version = "0.1", features = [ + "server-auto", + "server-graceful", + "service", +] } +http-body-util = "0.1" libmdns = { version = "0.9", optional = true } log = "0.4" -rand = "0.8" -serde = { version = "1", default-features = false, features = ["derive"], optional = true } +rand = "0.9" +serde = { version = "1", default-features = false, features = [ + "derive", +], optional = true } serde_repr = "0.1" serde_json = "1.0" sha1 = "0.10" -thiserror = "2.0" +thiserror = "2" tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } -zbus = { version = "5", default-features = false, features = ["tokio"], optional = true } +zbus = { version = "5", default-features = false, features = [ + "tokio", +], optional = true } [dependencies.librespot-core] path = "../core" diff --git a/discovery/src/server.rs b/discovery/src/server.rs index aa66fcb2..bdf18045 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -51,7 +51,7 @@ impl RequestHandler { Self { config, username: Mutex::new(None), - keys: DhLocalKeys::random(&mut rand::thread_rng()), + keys: DhLocalKeys::random(&mut rand::rng()), event_tx, } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 8be98fee..bbb2cd0a 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -12,8 +12,8 @@ edition = "2021" async-trait = "0.1" bytes = "1" log = "0.4" -protobuf = "3.5" -thiserror = "2.0" +protobuf = "3.7" +thiserror = "2" uuid = { version = "1", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index d95a5431..9a469487 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -10,11 +10,16 @@ edition = "2021" [dependencies] log = "0.4" -oauth2 = "4.4" +oauth2 = { version = "5.0", features = ["reqwest", "reqwest-blocking"] } +reqwest = { version = "0.12", features = ["blocking"] } open = "5.3" -thiserror = "2.0" -url = "2.2" +thiserror = "2" +url = "2.5" [dev-dependencies] -env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } -tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] } +env_logger = { version = "0.11", default-features = false, features = [ + "color", + "humantime", + "auto-color", +] } +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 2706ac73..7b08140f 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -13,12 +13,12 @@ use log::{error, info, trace}; use oauth2::basic::BasicTokenType; -use oauth2::reqwest::{async_http_client, http_client}; use oauth2::{ - basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, - RedirectUrl, Scope, TokenResponse, TokenUrl, + basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, EndpointNotSet, + EndpointSet, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; +use reqwest; use std::io; use std::sync::mpsc; use std::time::{Duration, Instant}; @@ -214,7 +214,7 @@ pub struct OAuthClient { redirect_uri: String, should_open_url: bool, message: String, - client: BasicClient, + client: BasicClient, } impl OAuthClient { @@ -281,10 +281,11 @@ impl OAuthClient { let (tx, rx) = mpsc::channel(); let client = self.client.clone(); std::thread::spawn(move || { + let http_client = reqwest::blocking::Client::new(); let resp = client .exchange_code(code) .set_pkce_verifier(pkce_verifier) - .request(http_client); + .request(&http_client); if let Err(e) = tx.send(resp) { error!("OAuth channel send error: {e}"); } @@ -299,10 +300,11 @@ impl OAuthClient { /// Synchronously obtain a new valid OAuth token from `refresh_token` pub fn refresh_token(&self, refresh_token: &str) -> Result { let refresh_token = RefreshToken::new(refresh_token.to_string()); + let http_client = reqwest::blocking::Client::new(); let resp = self .client .exchange_refresh_token(&refresh_token) - .request(http_client); + .request(&http_client); let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; self.build_token(resp) @@ -318,11 +320,12 @@ impl OAuthClient { }?; trace!("Exchange {code:?} for access token"); + let http_client = reqwest::Client::new(); let resp = self .client .exchange_code(code) .set_pkce_verifier(pkce_verifier) - .request_async(async_http_client) + .request_async(&http_client) .await; let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; @@ -332,10 +335,11 @@ impl OAuthClient { /// Asynchronously obtain a new valid OAuth token from `refresh_token` pub async fn refresh_token_async(&self, refresh_token: &str) -> Result { let refresh_token = RefreshToken::new(refresh_token.to_string()); + let http_client = reqwest::Client::new(); let resp = self .client .exchange_refresh_token(&refresh_token) - .request_async(async_http_client) + .request_async(&http_client) .await; let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; @@ -393,13 +397,10 @@ impl OAuthClientBuilder { } })?; - let client = BasicClient::new( - ClientId::new(self.client_id.to_string()), - None, - auth_url, - Some(token_url), - ) - .set_redirect_uri(redirect_url); + let client = BasicClient::new(ClientId::new(self.client_id.to_string())) + .set_auth_uri(auth_url) + .set_token_uri(token_url) + .set_redirect_uri(redirect_url); Ok(OAuthClient { scopes: self.scopes, @@ -433,13 +434,10 @@ pub fn get_access_token( uri: redirect_uri.to_string(), e, })?; - let client = BasicClient::new( - ClientId::new(client_id.to_string()), - None, - auth_url, - Some(token_url), - ) - .set_redirect_uri(redirect_url); + let client = BasicClient::new(ClientId::new(client_id.to_string())) + .set_auth_uri(auth_url) + .set_token_uri(token_url) + .set_redirect_uri(redirect_url); let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); @@ -467,10 +465,11 @@ pub fn get_access_token( // Do this sync in another thread because I am too stupid to make the async version work. let (tx, rx) = mpsc::channel(); std::thread::spawn(move || { + let http_client = reqwest::blocking::Client::new(); let resp = client .exchange_code(code) .set_pkce_verifier(pkce_verifier) - .request(http_client); + .request(&http_client); if let Err(e) = tx.send(resp) { error!("OAuth channel send error: {e}"); } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index bd53c35b..c87d7f9d 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -26,35 +26,46 @@ futures-util = "0.3" log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } shell-words = "1.1" -thiserror = "2.0" -tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } -zerocopy = { version = "0.8.13", features = ["derive"] } +thiserror = "2" +tokio = { version = "1", features = [ + "parking_lot", + "rt", + "rt-multi-thread", + "sync", +] } +zerocopy = { version = "0.8", features = ["derive"] } # Backends -alsa = { version = "0.9.0", optional = true } -portaudio-rs = { version = "0.3", optional = true } -libpulse-binding = { version = "2", optional = true, default-features = false } +alsa = { version = "0.9", 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.13", optional = true } -sdl2 = { version = "0.37", optional = true } -gstreamer = { version = "0.23.1", optional = true } -gstreamer-app = { version = "0.23.0", optional = true } -gstreamer-audio = { version = "0.23.0", optional = true } -glib = { version = "0.20.3", optional = true } +jack = { version = "0.13", optional = true } +sdl2 = { version = "0.38", optional = true } +gstreamer = { version = "0.24", optional = true } +gstreamer-app = { version = "0.24", optional = true } +gstreamer-audio = { version = "0.24", optional = true } +glib = { version = "0.21", optional = true } # Rodio dependencies -rodio = { version = "0.20.1", optional = true, default-features = false } -cpal = { version = "0.15.1", optional = true } +rodio = { version = "0.21", optional = true, default-features = false, features = [ + "playback", +] } +cpal = { version = "0.16", optional = true } # Container and audio decoder -symphonia = { version = "0.5", 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.9", optional = true } # Dithering -rand = { version = "0.8", features = ["small_rng"] } -rand_distr = "0.4" +rand = { version = "0.9", features = ["small_rng"] } +rand_distr = "0.5" [features] alsa-backend = ["alsa"] diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index f63fdef0..646b3dd9 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -59,13 +59,24 @@ impl From for SinkError { } } +impl From for RodioError { + fn from(_: cpal::DefaultStreamConfigError) -> RodioError { + RodioError::NoDeviceAvailable + } +} + +impl From for RodioError { + fn from(_: cpal::SupportedStreamConfigsError) -> RodioError { + RodioError::NoDeviceAvailable + } +} + pub struct RodioSink { rodio_sink: rodio::Sink, - format: AudioFormat, _stream: rodio::OutputStream, } -fn list_formats(device: &rodio::Device) { +fn list_formats(device: &cpal::Device) { match device.default_output_config() { Ok(cfg) => { debug!(" Default config:"); @@ -134,8 +145,9 @@ fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> { fn create_sink( host: &cpal::Host, device: Option, + format: AudioFormat, ) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { - let rodio_device = match device.as_deref() { + let cpal_device = match device.as_deref() { Some("?") => match list_outputs(host) { Ok(()) => exit(0), Err(e) => { @@ -144,6 +156,7 @@ fn create_sink( } }, Some(device_name) => { + // Ignore devices for which getting name fails, or format doesn't match host.output_devices()? .find(|d| d.name().ok().is_some_and(|name| name == device_name)) // Ignore devices for which getting name fails .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? @@ -153,14 +166,40 @@ fn create_sink( .ok_or(RodioError::NoDeviceAvailable)?, }; - let name = rodio_device.name().ok(); + let name = cpal_device.name().ok(); info!( "Using audio device: {}", name.as_deref().unwrap_or("[unknown name]") ); - let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?; - let sink = rodio::Sink::try_new(&handle)?; + // First try native stereo 44.1 kHz playback, then fall back to the device default sample rate + // (some devices only support 48 kHz and Rodio will resample linearly), then fall back to + // whatever the default device config is (like mono). + let default_config = cpal_device.default_output_config()?; + let config = cpal_device + .supported_output_configs()? + .find(|c| c.channels() == NUM_CHANNELS as cpal::ChannelCount) + .and_then(|c| { + c.try_with_sample_rate(cpal::SampleRate(SAMPLE_RATE)) + .or_else(|| c.try_with_sample_rate(default_config.sample_rate())) + }) + .unwrap_or(default_config); + + let sample_format = match format { + AudioFormat::F64 => cpal::SampleFormat::F64, + AudioFormat::F32 => cpal::SampleFormat::F32, + AudioFormat::S32 => cpal::SampleFormat::I32, + AudioFormat::S24 | AudioFormat::S24_3 => cpal::SampleFormat::I24, + AudioFormat::S16 => cpal::SampleFormat::I16, + }; + + let stream = rodio::OutputStreamBuilder::default() + .with_device(cpal_device) + .with_config(&config.config()) + .with_sample_format(sample_format) + .open_stream_or_fallback()?; + + let sink = rodio::Sink::connect_new(stream.mixer()); Ok((sink, stream)) } @@ -174,12 +213,11 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro unimplemented!("Rodio currently only supports F32 and S16 formats"); } - let (sink, stream) = create_sink(&host, device).unwrap(); + let (sink, stream) = create_sink(&host, device, format).unwrap(); debug!("Rodio sink was created"); RodioSink { rodio_sink: sink, - format, _stream: stream, } } @@ -200,27 +238,13 @@ impl Sink for RodioSink { let samples = packet .samples() .map_err(|e| RodioError::Samples(e.to_string()))?; - 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, - ); - self.rodio_sink.append(source); - } - AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); - let source = rodio::buffer::SamplesBuffer::new( - NUM_CHANNELS as u16, - SAMPLE_RATE, - samples_s16, - ); - self.rodio_sink.append(source); - } - _ => unreachable!(), - }; + let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let source = rodio::buffer::SamplesBuffer::new( + NUM_CHANNELS as cpal::ChannelCount, + SAMPLE_RATE, + samples_f32, + ); + self.rodio_sink.append(source); // Chunk sizes seem to be about 256 to 3000 ish items long. // Assuming they're on average 1628 then a half second buffer is: diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 4b8a427c..9145e045 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -43,7 +43,7 @@ impl fmt::Display for dyn Ditherer { } fn create_rng() -> SmallRng { - SmallRng::from_entropy() + SmallRng::from_os_rng() } pub struct TriangularDitherer { @@ -113,7 +113,9 @@ impl Ditherer for HighPassDitherer { active_channel: 0, 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 + // 1 LSB +/- 1 LSB (previous) = 2 LSB + distribution: Uniform::new_inclusive(-0.5, 0.5) + .expect("Failed to create uniform distribution"), } } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index bfaf96bc..e2143782 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/librespot-org/librespot" edition = "2021" [dependencies] -protobuf = "3.5" +protobuf = "3.7" [build-dependencies] protobuf-codegen = "3" From fdd4a16fdc2abe1437825e5c7839cf0b73c0328a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 13:27:16 +0200 Subject: [PATCH 520/561] feat: fallback to S16 format if unsupported in Rodio backend --- playback/src/audio_backend/rodio.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 646b3dd9..ff3dce96 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -209,8 +209,10 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro host.id().name() ); + let mut format = format; if format != AudioFormat::S16 && format != AudioFormat::F32 { - unimplemented!("Rodio currently only supports F32 and S16 formats"); + error!("Rodio currently only supports F32 and S16 formats, falling back to S16"); + format = AudioFormat::S16; } let (sink, stream) = create_sink(&host, device, format).unwrap(); From 0aec38b07a975d717f3f94bc64dd2a84f74d3a6f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 15:44:58 +0200 Subject: [PATCH 521/561] refactor: use Rust 2021 format strings for error and debug messages --- examples/play.rs | 2 +- examples/playlist_tracks.rs | 4 ++-- src/main.rs | 29 ++++++++++++----------------- src/player_event_handler.rs | 36 +++++++++++++++++------------------- 4 files changed, 32 insertions(+), 39 deletions(-) diff --git a/examples/play.rs b/examples/play.rs index 46079632..fa751cbb 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -36,7 +36,7 @@ async fn main() { println!("Connecting..."); let session = Session::new(session_config, None); if let Err(e) = session.connect(credentials, false).await { - println!("Error connecting: {}", e); + println!("Error connecting: {e}"); exit(1); } diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 18fc2e37..1d6a4266 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -29,12 +29,12 @@ async fn main() { let session = Session::new(session_config, None); if let Err(e) = session.connect(credentials, false).await { - println!("Error connecting: {}", e); + println!("Error connecting: {e}"); exit(1); } let plist = Playlist::get(&session, &plist_uri).await.unwrap(); - println!("{:?}", plist); + println!("{plist:?}"); for track_id in plist.tracks() { let plist_track = Track::get(&session, track_id).await.unwrap(); println!("track: {} ", plist_track.name); diff --git a/src/main.rs b/src/main.rs index 38f8391a..bfe158f7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1114,7 +1114,7 @@ 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); + error!("could not create or access specified tmp directory: {e}"); exit(1); } tmp_dir @@ -1164,15 +1164,14 @@ fn get_setup() -> Setup { if audio_dir.is_none() && opt_present(CACHE_SIZE_LIMIT) { warn!( - "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", - CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT + "Without a `--{CACHE}` / `-{CACHE_SHORT}` path, and/or if the `--{DISABLE_AUDIO_CACHE}` / `-{DISABLE_AUDIO_CACHE_SHORT}` flag is set, `--{CACHE_SIZE_LIMIT}` / `-{CACHE_SIZE_LIMIT_SHORT}` has no effect." ); } let cache = match Cache::new(cred_dir.clone(), volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { - warn!("Cannot create cache: {}", e); + warn!("Cannot create cache: {e}"); None } }; @@ -1240,8 +1239,7 @@ fn get_setup() -> Setup { let oauth_port = if opt_present(OAUTH_PORT) { if !enable_oauth { warn!( - "Without the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", - ENABLE_OAUTH, ENABLE_OAUTH_SHORT, OAUTH_PORT, OAUTH_PORT_SHORT + "Without the `--{ENABLE_OAUTH}` / `-{ENABLE_OAUTH_SHORT}` flag set `--{OAUTH_PORT}` / `-{OAUTH_PORT_SHORT}` has no effect." ); } opt_str(OAUTH_PORT) @@ -1268,8 +1266,7 @@ fn get_setup() -> Setup { if let Some(reason) = no_discovery_reason.as_deref() { if opt_present(ZEROCONF_PORT) { warn!( - "With {} `--{}` / `-{}` has no effect.", - reason, ZEROCONF_PORT, ZEROCONF_PORT_SHORT + "With {reason} `--{ZEROCONF_PORT}` / `-{ZEROCONF_PORT_SHORT}` has no effect." ); } } @@ -1348,8 +1345,7 @@ fn get_setup() -> Setup { if let Some(reason) = no_discovery_reason.as_deref() { if opt_present(ZEROCONF_BACKEND) { warn!( - "With {} `--{}` / `-{}` has no effect.", - reason, ZEROCONF_BACKEND, ZEROCONF_BACKEND_SHORT + "With {reason} `--{ZEROCONF_BACKEND}` / `-{ZEROCONF_BACKEND_SHORT}` has no effect." ); } } @@ -1534,7 +1530,7 @@ fn get_setup() -> Setup { url }, Err(e) => { - error!("Invalid proxy URL: \"{}\", only URLs in the format \"http(s)://host:port\" are allowed", e); + error!("Invalid proxy URL: \"{e}\", only URLs in the format \"http(s)://host:port\" are allowed"); exit(1); } } @@ -1591,8 +1587,7 @@ fn get_setup() -> Setup { ] { if opt_present(a) { warn!( - "Without the `--{}` / `-{}` flag normalisation options have no effect.", - ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, + "Without the `--{ENABLE_VOLUME_NORMALISATION}` / `-{ENABLE_VOLUME_NORMALISATION_SHORT}` flag normalisation options have no effect.", ); break; } @@ -1774,7 +1769,7 @@ fn get_setup() -> Setup { "none" => None, _ => match format { AudioFormat::F64 | AudioFormat::F32 => { - error!("Dithering is not available with format: {:?}.", format); + error!("Dithering is not available with format: {format:?}."); exit(1); } _ => Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { @@ -1987,7 +1982,7 @@ async fn main() { if let Some(spirc) = spirc.take() { if let Err(e) = spirc.shutdown() { - error!("error sending spirc shutdown message: {}", e); + error!("error sending spirc shutdown message: {e}"); } } if let Some(spirc_task) = spirc_task.take() { @@ -2021,7 +2016,7 @@ async fn main() { mixer.clone()).await { Ok((spirc_, spirc_task_)) => (spirc_, spirc_task_), Err(e) => { - error!("could not initialize spirc: {}", e); + error!("could not initialize spirc: {e}"); exit(1); } }; @@ -2073,7 +2068,7 @@ async fn main() { // Shutdown spirc if necessary if let Some(spirc) = spirc { if let Err(e) = spirc.shutdown() { - error!("error sending spirc shutdown message: {}", e); + error!("error sending spirc shutdown message: {e}"); } if let Some(spirc_task) = spirc_task { diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 11c3c1d8..c4814572 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -28,7 +28,7 @@ impl EventHandler { PlayerEvent::TrackChanged { audio_item } => { match audio_item.track_id.to_base62() { Err(e) => { - warn!("PlayerEvent::TrackChanged: Invalid track id: {}", e) + warn!("PlayerEvent::TrackChanged: Invalid track id: {e}") } Ok(id) => { env_vars.insert("PLAYER_EVENT", "track_changed".to_string()); @@ -94,7 +94,7 @@ impl EventHandler { } } PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "stopped".to_string()); env_vars.insert("TRACK_ID", id); @@ -105,7 +105,7 @@ impl EventHandler { position_ms, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "playing".to_string()); env_vars.insert("TRACK_ID", id); @@ -117,7 +117,7 @@ impl EventHandler { position_ms, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "paused".to_string()); env_vars.insert("TRACK_ID", id); @@ -125,14 +125,14 @@ impl EventHandler { } }, PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "loading".to_string()); env_vars.insert("TRACK_ID", id); } }, PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "preloading".to_string()); env_vars.insert("TRACK_ID", id); @@ -141,8 +141,7 @@ impl EventHandler { PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { match track_id.to_base62() { Err(e) => warn!( - "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {}", - e + "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}" ), Ok(id) => { env_vars.insert("PLAYER_EVENT", "preload_next".to_string()); @@ -151,14 +150,14 @@ impl EventHandler { } } PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); env_vars.insert("TRACK_ID", id); } }, PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); env_vars.insert("TRACK_ID", id); @@ -173,7 +172,7 @@ impl EventHandler { position_ms, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {}", e), + Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "seeked".to_string()); env_vars.insert("TRACK_ID", id); @@ -186,7 +185,7 @@ impl EventHandler { .. } => match track_id.to_base62() { Err(e) => { - warn!("PlayerEvent::PositionCorrection: Invalid track id: {}", e) + warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}") } Ok(id) => { env_vars.insert("PLAYER_EVENT", "position_correction".to_string()); @@ -263,7 +262,7 @@ impl Drop for EventHandler { debug!("Shutting down EventHandler thread ..."); if let Some(handle) = self.thread_handle.take() { if let Err(e) = handle.join() { - error!("EventHandler thread Error: {:?}", e); + error!("EventHandler thread Error: {e:?}"); } } } @@ -289,8 +288,7 @@ fn run_program(env_vars: HashMap<&str, String>, onevent: &str) { let mut v: Vec<&str> = onevent.split_whitespace().collect(); debug!( - "Running {} with environment variables:\n{:#?}", - onevent, env_vars + "Running {onevent} with environment variables:\n{env_vars:#?}" ); match Command::new(v.remove(0)) @@ -298,15 +296,15 @@ fn run_program(env_vars: HashMap<&str, String>, onevent: &str) { .envs(env_vars.iter()) .spawn() { - Err(e) => warn!("On event program {} failed to start: {}", onevent, e), + Err(e) => warn!("On event program {onevent} failed to start: {e}"), Ok(mut child) => match child.wait() { - Err(e) => warn!("On event program {} failed: {}", onevent, e), + Err(e) => warn!("On event program {onevent} failed: {e}"), Ok(e) if e.success() => (), Ok(e) => { if let Some(code) = e.code() { - warn!("On event program {} returned exit code {}", onevent, code); + warn!("On event program {onevent} returned exit code {code}"); } else { - warn!("On event program {} returned failure: {}", onevent, e); + warn!("On event program {onevent} returned failure: {e}"); } } }, From 6288e7e03cb1a4d63a42e47f62c641fcea9a181c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 16:19:39 +0200 Subject: [PATCH 522/561] refactor: update to Rust 1.85 and edition 2024, use inline log args - Update MSRV to 1.85 and Rust edition to 2024. - Refactor all logging macros to use inline argument formatting. - Fix import order in main.rs and examples. - Add async environment variable setter to main.rs as safe facade. --- CHANGELOG.md | 2 +- Cargo.toml | 6 +- audio/src/fetch/mod.rs | 17 +- connect/src/spirc.rs | 41 +- core/src/apresolve.rs | 2 +- core/src/audio_key.rs | 6 +- core/src/cache.rs | 24 +- core/src/cdn_url.rs | 2 +- core/src/connection/mod.rs | 6 +- core/src/dealer/mod.rs | 16 +- core/src/http_client.rs | 4 +- core/src/login5.rs | 2 +- core/src/mercury/mod.rs | 10 +- core/src/session.rs | 16 +- core/src/socket.rs | 2 +- core/src/spclient.rs | 16 +- core/src/token.rs | 5 +- discovery/src/lib.rs | 2 +- discovery/src/server.rs | 4 +- examples/play_connect.rs | 2 +- metadata/src/playlist/list.rs | 5 +- oauth/src/lib.rs | 3 +- playback/src/audio_backend/alsa.rs | 55 +-- playback/src/audio_backend/pipe.rs | 2 +- playback/src/audio_backend/subprocess.rs | 2 +- playback/src/mixer/mappings.rs | 14 +- playback/src/mixer/softmixer.rs | 2 +- playback/src/player.rs | 75 ++-- src/main.rs | 65 ++-- src/player_event_handler.rs | 459 ++++++++++++----------- 30 files changed, 419 insertions(+), 448 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f6550ef..986487b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- [core] MSRV is now 1.81 (breaking) +- [core] MSRV is now 1.85 with Rust edition 2024 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. - [core] `stream_from_cdn` now accepts the URL as `TryInto` instead of `CdnUrl` (breaking) - [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) diff --git a/Cargo.toml b/Cargo.toml index 831b4f5e..a930b39e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "librespot" version = "0.6.0-dev" -rust-version = "1.81" +rust-version = "1.85" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" keywords = ["spotify"] repository = "https://github.com/librespot-org/librespot" readme = "README.md" -edition = "2021" +edition = "2024" [workspace] @@ -126,4 +126,4 @@ assets = [ ] [workspace.package] -rust-version = "1.81" +rust-version = "1.85" diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 414ce3cf..96a6eb96 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -366,11 +366,11 @@ impl AudioFile { bytes_per_second: usize, ) -> Result { if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) { - debug!("File {} already in cache", file_id); + debug!("File {file_id} already in cache"); return Ok(AudioFile::Cached(file)); } - debug!("Downloading file {}", file_id); + debug!("Downloading file {file_id}"); let (complete_tx, complete_rx) = oneshot::channel(); @@ -379,14 +379,14 @@ impl AudioFile { let session_ = session.clone(); session.spawn(complete_rx.map_ok(move |mut file| { - debug!("Downloading file {} complete", file_id); + debug!("Downloading file {file_id} complete"); if let Some(cache) = session_.cache() { 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); + error!("Error caching file {file_id} to {cache_id:?}: {e}"); } else { - debug!("File {} cached to {:?}", file_id, cache_id); + debug!("File {file_id} cached to {cache_id:?}"); } } } @@ -465,14 +465,11 @@ impl AudioFileStreaming { ))); }; - trace!("Streaming from {}", url); + trace!("Streaming from {url}"); let code = response.status(); if code != StatusCode::PARTIAL_CONTENT { - debug!( - "Opening audio file 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/connect/src/spirc.rs b/connect/src/spirc.rs index 14c0f015..57f8b978 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -166,7 +166,7 @@ impl Spirc { } let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel); - debug!("new Spirc[{}]", spirc_id); + debug!("new Spirc[{spirc_id}]"); let connect_state = ConnectState::new(config, &session); @@ -446,14 +446,14 @@ impl SpircTask { cluster_update = self.connect_state_update.next() => unwrap! { cluster_update, match |cluster_update| if let Err(e) = self.handle_cluster_update(cluster_update).await { - error!("could not dispatch connect state update: {}", e); + error!("could not dispatch connect state update: {e}"); } }, // main dealer request handling (dealer expects an answer) request = self.connect_state_command.next() => unwrap! { request, |request| if let Err(e) = self.handle_connect_state_request(request).await { - error!("couldn't handle connect state command: {}", e); + error!("couldn't handle connect state command: {e}"); } }, // volume request handling is send separately (it's more like a fire forget) @@ -491,12 +491,12 @@ impl SpircTask { }, cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd { if let Err(e) = self.handle_command(cmd).await { - debug!("could not dispatch command: {}", e); + debug!("could not dispatch command: {e}"); } }, 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); + error!("could not dispatch player event: {e}"); } }, _ = async { sleep(UPDATE_STATE_DELAY).await }, if self.update_state => { @@ -606,7 +606,7 @@ impl SpircTask { } async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> { - trace!("Received SpircCommand::{:?}", cmd); + trace!("Received SpircCommand::{cmd:?}"); match cmd { SpircCommand::Shutdown => { trace!("Received SpircCommand::Shutdown"); @@ -618,16 +618,15 @@ impl SpircTask { } } SpircCommand::Activate if !self.connect_state.is_active() => { - trace!("Received SpircCommand::{:?}", cmd); + trace!("Received SpircCommand::{cmd:?}"); self.handle_activate(); return self.notify().await; } - SpircCommand::Activate => warn!( - "SpircCommand::{:?} will be ignored while already active", - cmd - ), + SpircCommand::Activate => { + warn!("SpircCommand::{cmd:?} will be ignored while already active") + } _ if !self.connect_state.is_active() => { - warn!("SpircCommand::{:?} will be ignored while Not Active", cmd) + warn!("SpircCommand::{cmd:?} will be ignored while Not Active") } SpircCommand::Disconnect { pause } => { if pause { @@ -787,7 +786,7 @@ impl SpircTask { } async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> { - trace!("Received connection ID update: {:?}", connection_id); + trace!("Received connection ID update: {connection_id:?}"); self.session.set_connection_id(&connection_id); let cluster = match self @@ -837,7 +836,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 .pairs .iter() @@ -863,12 +862,7 @@ impl SpircTask { }; self.session.set_user_attribute(key, new_value); - trace!( - "Received attribute mutation, {} was {} is now {}", - key, - old_value, - new_value - ); + trace!("Received attribute mutation, {key} was {old_value} is now {new_value}"); if key == "filter-explicit-content" && new_value == "1" { self.player @@ -882,10 +876,7 @@ impl SpircTask { self.add_autoplay_resolving_when_required() } } else { - trace!( - "Received attribute mutation for {} but key was not found!", - key - ); + trace!("Received attribute mutation for {key} but key was not found!"); } } } @@ -1743,7 +1734,7 @@ impl SpircTask { } fn set_volume(&mut self, volume: u16) { - debug!("SpircTask::set_volume({})", volume); + debug!("SpircTask::set_volume({volume})"); let old_volume = self.connect_state.device_info().volume; let new_volume = volume as u32; diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 37e4ba3d..82d37e73 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -108,7 +108,7 @@ impl ApResolver { if inner.data.is_any_empty() { warn!("Failed to resolve all access points, using fallbacks"); if let Some(error) = error { - warn!("Resolve access points error: {}", error); + warn!("Resolve access points error: {error}"); } let fallback = self.parse_resolve_to_access_points(ApResolveData::fallback()); diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 0ffa8383..79710ec5 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -70,11 +70,7 @@ impl AudioKeyManager { .map_err(|_| AudioKeyError::Channel)? } _ => { - trace!( - "Did not expect {:?} AES key packet with data {:#?}", - cmd, - data - ); + trace!("Did not expect {cmd:?} AES key packet with data {data:#?}"); return Err(AudioKeyError::Packet(cmd as u8).into()); } } diff --git a/core/src/cache.rs b/core/src/cache.rs index af373ff1..fe38b681 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -141,7 +141,7 @@ impl FsSizeLimiter { let list_dir = match fs::read_dir(path) { Ok(list_dir) => list_dir, Err(e) => { - warn!("Could not read directory {:?} in cache dir: {}", path, e); + warn!("Could not read directory {path:?} in cache dir: {e}"); return; } }; @@ -150,7 +150,7 @@ impl FsSizeLimiter { let entry = match entry { Ok(entry) => entry, Err(e) => { - warn!("Could not directory {:?} in cache dir: {}", path, e); + warn!("Could not directory {path:?} in cache dir: {e}"); return; } }; @@ -166,7 +166,7 @@ impl FsSizeLimiter { limiter.add(&path, size, access_time); } Err(e) => { - warn!("Could not read file {:?} in cache dir: {}", path, e) + warn!("Could not read file {path:?} in cache dir: {e}") } } } @@ -213,7 +213,7 @@ impl FsSizeLimiter { let res = fs::remove_file(&file); if let Err(e) = res { - warn!("Could not remove file {:?} from cache dir: {}", file, e); + warn!("Could not remove file {file:?} from cache dir: {e}"); last_error = Some(e); } else { count += 1; @@ -221,7 +221,7 @@ impl FsSizeLimiter { } if count > 0 { - info!("Removed {} cache files.", count); + info!("Removed {count} cache files."); } if let Some(err) = last_error { @@ -317,7 +317,7 @@ impl Cache { // If the file did not exist, the file was probably not written // before. Otherwise, log the error. if e.kind != ErrorKind::NotFound { - warn!("Error reading credentials from cache: {}", e); + warn!("Error reading credentials from cache: {e}"); } None } @@ -332,7 +332,7 @@ impl Cache { }); if let Err(e) = result { - warn!("Cannot save credentials to cache: {}", e) + warn!("Cannot save credentials to cache: {e}") } } } @@ -351,7 +351,7 @@ impl Cache { Ok(v) => Some(v), Err(e) => { if e.kind != ErrorKind::NotFound { - warn!("Error reading volume from cache: {}", e); + warn!("Error reading volume from cache: {e}"); } None } @@ -362,7 +362,7 @@ impl Cache { if let Some(ref location) = self.volume_location { let result = File::create(location).and_then(|mut file| write!(file, "{volume}")); if let Err(e) = result { - warn!("Cannot save volume to cache: {}", e); + warn!("Cannot save volume to cache: {e}"); } } } @@ -375,7 +375,7 @@ impl Cache { path }), Err(e) => { - warn!("Invalid FileId: {}", e); + warn!("Invalid FileId: {e}"); None } } @@ -387,14 +387,14 @@ impl Cache { Ok(file) => { if let Some(limiter) = self.size_limiter.as_deref() { if !limiter.touch(&path) { - error!("limiter could not touch {:?}", path); + error!("limiter could not touch {path:?}"); } } Some(file) } Err(e) => { if e.kind() != io::ErrorKind::NotFound { - warn!("Error reading file from cache: {}", e) + warn!("Error reading file from cache: {e}") } None } diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 44f4488f..49e4757c 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -73,7 +73,7 @@ impl CdnUrl { let cdn_url = Self { file_id, urls }; - trace!("Resolved CDN storage: {:#?}", cdn_url); + trace!("Resolved CDN storage: {cdn_url:#?}"); Ok(cdn_url) } diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index ca89e87b..f83827c0 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -186,11 +186,7 @@ pub async fn authenticate( Err(error_data.into()) } _ => { - trace!( - "Did not expect {:?} AES key packet with data {:#?}", - cmd, - data - ); + trace!("Did not expect {cmd:?} AES key packet with data {data:#?}"); Err(AuthenticationError::Packet(cmd)) } }; diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 8934daab..c5158c84 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -89,7 +89,7 @@ impl Responder { .to_string(); if let Err(e) = self.tx.send(WsMessage::Text(response.into())) { - warn!("Wasn't able to reply to dealer request: {}", e); + warn!("Wasn't able to reply to dealer request: {e}"); } } @@ -452,7 +452,7 @@ impl Dealer { if let Some(handle) = self.handle.take() { if let Err(e) = CancelOnDrop(handle).await { - error!("error aborting dealer operations: {}", e); + error!("error aborting dealer operations: {e}"); } } } @@ -524,13 +524,13 @@ async fn connect( 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); + 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); + warn!("Error while closing websocket: {e}"); } debug!("Dropping send task"); @@ -565,7 +565,7 @@ async fn connect( _ => (), // tungstenite handles Close and Ping automatically }, Some(Err(e)) => { - warn!("Websocket connection failed: {}", e); + warn!("Websocket connection failed: {e}"); break; } None => { @@ -648,13 +648,13 @@ where () = shared.closed() => break, r = t0 => { if let Err(e) = r { - error!("timeout on task 0: {}", e); + error!("timeout on task 0: {e}"); } tasks.0.take(); }, r = t1 => { if let Err(e) = r { - error!("timeout on task 1: {}", e); + error!("timeout on task 1: {e}"); } tasks.1.take(); } @@ -671,7 +671,7 @@ where match connect(&url, proxy.as_ref(), &shared).await { Ok((s, r)) => tasks = (init_task(s), init_task(r)), Err(e) => { - error!("Error while connecting: {}", e); + error!("Error while connecting: {e}"); tokio::time::sleep(RECONNECT_INTERVAL).await; } } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 1bb464d8..728857f9 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -124,7 +124,7 @@ impl HttpClient { ); let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| { - error!("Invalid user agent <{}>: {}", user_agent_str, err); + error!("Invalid user agent <{user_agent_str}>: {err}"); HeaderValue::from_static(FALLBACK_USER_AGENT) }); @@ -176,7 +176,7 @@ impl HttpClient { } pub async fn request(&self, req: Request) -> Result, Error> { - debug!("Requesting {}", req.uri().to_string()); + debug!("Requesting {}", req.uri()); // `Request` does not implement `Clone` because its `Body` may be a single-shot stream. // As correct as that may be technically, we now need all this boilerplate to clone it diff --git a/core/src/login5.rs b/core/src/login5.rs index 75f739a1..e94978f6 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -199,7 +199,7 @@ impl Login5Manager { inner.auth_token.clone() }); - trace!("Got auth token: {:?}", auth_token); + trace!("Got auth token: {auth_token:?}"); token.ok_or(Login5Error::NoStoredCredentials.into()) } diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index 76b060a3..db8f1c0a 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -131,12 +131,12 @@ impl MercuryManager { 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())); } Err(e) => { - error!("could not subscribe to {}: {}", uri, e); + error!("could not subscribe to {uri}: {e}"); } } } @@ -163,7 +163,7 @@ impl MercuryManager { manager.lock(move |inner| { if !inner.invalid { - debug!("listening to uri={}", uri); + debug!("listening to uri={uri}"); inner.subscriptions.push((uri, tx)); } }); @@ -283,14 +283,14 @@ impl MercuryManager { Ok(()) } else { debug!("unknown subscription uri={}", &response.uri); - trace!("response pushed over Mercury: {:?}", response); + trace!("response pushed over Mercury: {response:?}"); Err(MercuryError::Response(response).into()) } } else if let Some(cb) = pending.callback { cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?; Ok(()) } else { - error!("can't handle Mercury response: {:?}", response); + error!("can't handle Mercury response: {response:?}"); Err(MercuryError::Response(response).into()) } } diff --git a/core/src/session.rs b/core/src/session.rs index 3d39b8c7..2d0b5987 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -273,7 +273,7 @@ impl Session { let session_weak = self.weak(); tokio::spawn(async move { if let Err(e) = sender_task.await { - error!("{}", e); + error!("{e}"); if let Some(session) = session_weak.try_upgrade() { if !session.is_invalid() { session.shutdown(); @@ -360,7 +360,7 @@ 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); + error!("librespot does not support {account_type:?} accounts."); info!("Please support Spotify and your artists and sign up for a premium account."); // TODO: logout instead of exiting @@ -566,7 +566,7 @@ impl KeepAliveState { .map(|t| t.as_secs_f64()) .unwrap_or(f64::INFINITY); - trace!("keep-alive state: {:?}, timeout in {:.1}", self, delay); + trace!("keep-alive state: {self:?}, timeout in {delay:.1}"); } } @@ -619,7 +619,7 @@ where let cmd = match packet_type { Some(cmd) => cmd, None => { - trace!("Ignoring unknown packet {:x}", cmd); + trace!("Ignoring unknown packet {cmd:x}"); return Err(SessionError::Packet(cmd).into()); } }; @@ -667,7 +667,7 @@ where } Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned())?; - info!("Country: {:?}", country); + info!("Country: {country:?}"); session.0.data.write().user_data.country = country; Ok(()) } @@ -710,7 +710,7 @@ where } } - trace!("Received product info: {:#?}", user_attributes); + trace!("Received product info: {user_attributes:#?}"); Session::check_catalogue(&user_attributes); session.0.data.write().user_data.attributes = user_attributes; @@ -721,7 +721,7 @@ where | Some(UnknownDataAllZeros) | Some(LicenseVersion) => Ok(()), _ => { - trace!("Ignoring {:?} packet with data {:#?}", cmd, data); + trace!("Ignoring {cmd:?} packet with data {data:#?}"); Err(SessionError::Packet(cmd as u8).into()) } } @@ -749,7 +749,7 @@ where Poll::Ready(Some(Ok((cmd, data)))) => { let result = self.as_mut().dispatch(&session, cmd, data); if let Err(e) = result { - debug!("could not dispatch command: {}", e); + debug!("could not dispatch command: {e}"); } } Poll::Ready(None) => { diff --git a/core/src/socket.rs b/core/src/socket.rs index 84ac6024..71b0d6b9 100644 --- a/core/src/socket.rs +++ b/core/src/socket.rs @@ -7,7 +7,7 @@ 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); + info!("Using proxy \"{proxy_url}\""); let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| { addrs.into_iter().next().ok_or_else(|| { diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 3b3c679f..22df5006 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -311,20 +311,12 @@ impl SpClient { continue; } Err(e) => { - trace!( - "Answer not accepted {}/{}: {}", - count, - MAX_TRIES, - e - ); + trace!("Answer not accepted {count}/{MAX_TRIES}: {e}"); } } } Err(e) => trace!( - "Unable to solve hash cash challenge {}/{}: {}", - count, - MAX_TRIES, - e + "Unable to solve hash cash challenge {count}/{MAX_TRIES}: {e}" ), } @@ -373,7 +365,7 @@ impl SpClient { inner.client_token = Some(client_token); }); - trace!("Got client token: {:?}", granted_token); + trace!("Got client token: {granted_token:?}"); Ok(access_token) } @@ -542,7 +534,7 @@ impl SpClient { } } - debug!("Error was: {:?}", last_response); + debug!("Error was: {last_response:?}"); } last_response diff --git a/core/src/token.rs b/core/src/token.rs index 445b8aeb..7e604797 100644 --- a/core/src/token.rs +++ b/core/src/token.rs @@ -86,8 +86,7 @@ impl TokenProvider { } trace!( - "Requested token in scopes {:?} unavailable or expired, requesting new token.", - scopes + "Requested token in scopes {scopes:?} unavailable or expired, requesting new token." ); let query_uri = format!( @@ -100,7 +99,7 @@ impl TokenProvider { let response = request.await?; let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec(); let token = Token::from_json(String::from_utf8(data)?)?; - trace!("Got token: {:#?}", token); + trace!("Got token: {token:#?}"); self.lock(|inner| inner.tokens.push(token.clone())); Ok(token) } diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 36fd2509..68dfb0b8 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -419,7 +419,7 @@ fn launch_libmdns( }; if let Err(e) = inner() { - log::error!("libmdns error: {}", e); + log::error!("libmdns error: {e}"); let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e)); } }); diff --git a/discovery/src/server.rs b/discovery/src/server.rs index bdf18045..6e4b0135 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -170,7 +170,7 @@ impl RequestHandler { .map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?; h.update(encrypted); if h.verify_slice(cksum).is_err() { - warn!("Login error for user {:?}: MAC mismatch", username); + warn!("Login error for user {username:?}: MAC mismatch"); let result = json!({ "status": 102, "spotifyError": 1, @@ -314,7 +314,7 @@ impl DiscoveryServer { discovery .clone() .handle(request) - .inspect_err(|e| error!("could not handle discovery request: {}", e)) + .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 }); diff --git a/examples/play_connect.rs b/examples/play_connect.rs index bd57df7d..1be6345b 100644 --- a/examples/play_connect.rs +++ b/examples/play_connect.rs @@ -1,7 +1,7 @@ use librespot::{ connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc}, core::{ - authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, Error, + Error, authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, }, playback::mixer::MixerConfig, playback::{ diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index b81e61e4..7fa0b311 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -78,10 +78,7 @@ impl Playlist { 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, - ); + warn!("Got {length} tracks, but the list should contain {expected_length} tracks.",); } tracks diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 7b08140f..c97b2bf0 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -18,7 +18,6 @@ use oauth2::{ EndpointSet, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, }; use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; -use reqwest; use std::io; use std::sync::mpsc; use std::time::{Duration, Instant}; @@ -156,7 +155,7 @@ fn get_authcode_listener( addr: socket_address, e, })?; - info!("OAuth server listening on {:?}", socket_address); + info!("OAuth server listening on {socket_address:?}"); // The server will terminate itself after collecting the first code. let mut stream = listener diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index dc0b87c0..afbf3195 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -226,7 +226,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> let buffer_size = { let max = match hwp.get_buffer_size_max() { Err(e) => { - trace!("Error getting the device's max Buffer size: {}", e); + trace!("Error getting the device's max Buffer size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -234,7 +234,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> let min = match hwp.get_buffer_size_min() { Err(e) => { - trace!("Error getting the device's min Buffer size: {}", e); + trace!("Error getting the device's min Buffer size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -246,11 +246,11 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> .find(|f| (min..=max).contains(f)) { Some(size) => { - trace!("Desired Frames per Buffer: {:?}", size); + trace!("Desired Frames per Buffer: {size:?}"); match hwp.set_buffer_size_near(size) { Err(e) => { - trace!("Error setting the device's Buffer size: {}", e); + trace!("Error setting the device's Buffer size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -267,17 +267,9 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> }; if buffer_size == ZERO_FRAMES { - trace!( - "Desired Buffer Frame range: {:?} - {:?}", - MIN_BUFFER, - MAX_BUFFER - ); + trace!("Desired Buffer Frame range: {MIN_BUFFER:?} - {MAX_BUFFER:?}",); - trace!( - "Actual Buffer Frame range as reported by the device: {:?} - {:?}", - min, - max - ); + trace!("Actual Buffer Frame range as reported by the device: {min:?} - {max:?}",); } buffer_size @@ -289,7 +281,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> } else { let max = match hwp.get_period_size_max() { Err(e) => { - trace!("Error getting the device's max Period size: {}", e); + trace!("Error getting the device's max Period size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -297,7 +289,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> let min = match hwp.get_period_size_min() { Err(e) => { - trace!("Error getting the device's min Period size: {}", e); + trace!("Error getting the device's min Period size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -312,11 +304,11 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> .find(|f| (min..=max).contains(f)) { Some(size) => { - trace!("Desired Frames per Period: {:?}", size); + trace!("Desired Frames per Period: {size:?}"); match hwp.set_period_size_near(size, ValueOr::Nearest) { Err(e) => { - trace!("Error setting the device's Period size: {}", e); + trace!("Error setting the device's Period size: {e}"); ZERO_FRAMES } Ok(s) => s, @@ -334,20 +326,14 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> }; if period_size == ZERO_FRAMES { - trace!("Buffer size: {:?}", buffer_size); + trace!("Buffer size: {buffer_size:?}"); trace!( - "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", - min_period, - MIN_PERIOD_DIVISOR, - max_period, - MAX_PERIOD_DIVISOR, + "Desired Period Frame range: {min_period:?} (Buffer size / {MIN_PERIOD_DIVISOR:?}) - {max_period:?} (Buffer size / {MAX_PERIOD_DIVISOR:?})", ); trace!( - "Actual Period Frame range as reported by the device: {:?} - {:?}", - min, - max + "Actual Period Frame range as reported by the device: {min:?} - {max:?}", ); } @@ -381,14 +367,14 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); - trace!("Actual Frames per Period: {:?}", frames_per_period); + trace!("Actual Frames per Buffer: {frames_per_buffer:?}"); + trace!("Actual Frames per Period: {frames_per_period:?}"); // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize }; - trace!("Period Buffer size in bytes: {:?}", bytes_per_period); + trace!("Period Buffer size in bytes: {bytes_per_period:?}"); Ok((pcm, bytes_per_period)) } @@ -401,7 +387,7 @@ impl Open for AlsaSink { exit(0); } Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } }, @@ -410,7 +396,7 @@ impl Open for AlsaSink { } .to_string(); - info!("Using AlsaSink with format: {:?}", format); + info!("Using AlsaSink with format: {format:?}"); Self { pcm: None, @@ -500,10 +486,7 @@ impl AlsaSink { Err(e) => { // 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, {}", - e - ); + warn!("Error writing from AlsaSink buffer to PCM, trying to recover, {e}"); pcm.try_recover(e, false).map_err(AlsaError::OnWrite) } diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index e680256d..59690e25 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -48,7 +48,7 @@ impl Open for StdoutSink { exit(0); } - info!("Using StdoutSink (pipe) with format: {:?}", format); + info!("Using StdoutSink (pipe) with format: {format:?}"); Self { output: None, diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 6ce545da..0c2cd116 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -72,7 +72,7 @@ impl Open for SubprocessSink { exit(0); } - info!("Using SubprocessSink with format: {:?}", format); + info!("Using SubprocessSink with format: {format:?}"); Self { shell_command, diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index bcb3fa45..bb740997 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -33,10 +33,7 @@ impl MappedCtrl for VolumeCtrl { } } 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 - ); + error!("{self:?} does not work with 0 dB range, using linear mapping instead"); normalized_volume }; @@ -67,10 +64,7 @@ impl MappedCtrl for VolumeCtrl { } } 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 - ); + error!("{self:?} does not work with 0 dB range, using linear mapping instead"); mapped_volume }; @@ -88,10 +82,10 @@ impl MappedCtrl for VolumeCtrl { 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), + _ => error!("Invalid to set dB range for volume control type {self:?}"), } - debug!("Volume control is now {:?}", self) + debug!("Volume control is now {self:?}") } fn range_ok(&self) -> bool { diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index bf5e26ed..c8946a9b 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -17,7 +17,7 @@ pub struct SoftMixer { impl Mixer for SoftMixer { fn open(config: MixerConfig) -> Result { let volume_ctrl = config.volume_ctrl; - info!("Mixing with softvol and volume control: {:?}", volume_ctrl); + info!("Mixing with softvol and volume control: {volume_ctrl:?}"); Ok(Self { volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))), diff --git a/playback/src/player.rs b/playback/src/player.rs index 8e0281e6..13790449 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -324,8 +324,7 @@ impl NormalisationData { 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 + "NormalisationData::parse_from_file seeking to {SPOTIFY_NORMALIZATION_HEADER_START_OFFSET} but position is now {newpos}" ); error!("Falling back to default (non-track and non-album) normalisation data."); @@ -396,8 +395,7 @@ impl NormalisationData { let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs(); warn!( - "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at its peak.", - factor_db, limiting_db + "This track may exceed dBFS by {factor_db:.2} dB and be subject to {limiting_db:.2} dB of dynamic limiting at its peak." ); } else if factor > threshold_ratio { let limiting_db = gain_db @@ -405,15 +403,14 @@ impl NormalisationData { + config.normalisation_threshold_dbfs.abs(); info!( - "This track may be subject to {:.2} dB of dynamic limiting at its peak.", - limiting_db + "This track may be subject to {limiting_db:.2} dB of dynamic limiting at its peak." ); } factor }; - debug!("Normalisation Data: {:?}", data); + debug!("Normalisation Data: {data:?}"); debug!( "Calculated Normalisation Factor for {:?}: {:.2}%", config.normalisation_type, @@ -464,7 +461,7 @@ impl Player { let handle = thread::spawn(move || { let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel); - debug!("new Player [{}]", player_id); + debug!("new Player [{player_id}]"); let converter = Converter::new(config.ditherer); @@ -517,7 +514,7 @@ impl Player { fn command(&self, cmd: PlayerCommand) { if let Some(commands) = self.commands.as_ref() { if let Err(e) = commands.send(cmd) { - error!("Player Commands Error: {}", e); + error!("Player Commands Error: {e}"); } } } @@ -636,7 +633,7 @@ impl Drop for Player { self.commands = None; if let Some(handle) = self.thread_handle.take() { if let Err(e) = handle.join() { - error!("Player thread Error: {:?}", e); + error!("Player thread Error: {e:?}"); } } } @@ -787,10 +784,7 @@ impl PlayerState { }; } _ => { - error!( - "Called playing_to_end_of_track in non-playing state: {:?}", - new_state - ); + error!("Called playing_to_end_of_track in non-playing state: {new_state:?}"); exit(1); } } @@ -832,10 +826,7 @@ impl PlayerState { }; } _ => { - error!( - "PlayerState::paused_to_playing in invalid state: {:?}", - new_state - ); + error!("PlayerState::paused_to_playing in invalid state: {new_state:?}"); exit(1); } } @@ -876,10 +867,7 @@ impl PlayerState { }; } _ => { - error!( - "PlayerState::playing_to_paused in invalid state: {:?}", - new_state - ); + error!("PlayerState::playing_to_paused in invalid state: {new_state:?}"); exit(1); } } @@ -894,7 +882,7 @@ struct PlayerTrackLoader { impl PlayerTrackLoader { async fn find_available_alternative(&self, audio_item: AudioItem) -> Option { if let Err(e) = audio_item.availability { - error!("Track is unavailable: {}", e); + error!("Track is unavailable: {e}"); None } else if !audio_item.files.is_empty() { Some(audio_item) @@ -958,7 +946,7 @@ impl PlayerTrackLoader { } }, Err(e) => { - error!("Unable to load audio item: {:?}", e); + error!("Unable to load audio item: {e:?}"); return None; } }; @@ -1026,7 +1014,7 @@ impl PlayerTrackLoader { let encrypted_file = match encrypted_file.await { Ok(encrypted_file) => encrypted_file, Err(e) => { - error!("Unable to load encrypted file: {:?}", e); + error!("Unable to load encrypted file: {e:?}"); return None; } }; @@ -1041,7 +1029,7 @@ impl PlayerTrackLoader { 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); + warn!("Unable to load key, continuing without decryption: {e}"); None } }; @@ -1064,7 +1052,7 @@ impl PlayerTrackLoader { ) { Ok(audio_file) => audio_file, Err(e) => { - error!("PlayerTrackLoader::load_track error opening subfile: {}", e); + error!("PlayerTrackLoader::load_track error opening subfile: {e}"); return None; } }; @@ -1098,10 +1086,7 @@ impl PlayerTrackLoader { let mut decoder = match decoder_type { Ok(decoder) => decoder, Err(e) if is_cached => { - warn!( - "Unable to read cached audio file: {}. Trying to download it.", - e - ); + warn!("Unable to read cached audio file: {e}. Trying to download it."); match self.session.cache() { Some(cache) => { @@ -1120,7 +1105,7 @@ impl PlayerTrackLoader { continue; } Err(e) => { - error!("Unable to read audio file: {}", e); + error!("Unable to read audio file: {e}"); return None; } }; @@ -1130,7 +1115,7 @@ impl PlayerTrackLoader { // If the position is invalid just start from // the beginning of the track. let position_ms = if position_ms > duration_ms { - warn!("Invalid start position of {} ms exceeds track's duration of {} ms, starting track from the beginning", position_ms, duration_ms); + warn!("Invalid start position of {position_ms} ms exceeds track's duration of {duration_ms} ms, starting track from the beginning"); 0 } else { position_ms @@ -1144,8 +1129,7 @@ impl PlayerTrackLoader { Ok(new_position_ms) => new_position_ms, Err(e) => { error!( - "PlayerTrackLoader::load_track error seeking to starting position {}: {}", - position_ms, e + "PlayerTrackLoader::load_track error seeking to starting position {position_ms}: {e}" ); return None; } @@ -1195,7 +1179,7 @@ impl Future for PlayerInternal { if let Some(cmd) = cmd { if let Err(e) = self.handle_command(cmd) { - error!("Error handling command: {}", e); + error!("Error handling command: {e}"); } } @@ -1225,8 +1209,7 @@ impl Future for PlayerInternal { } Poll::Ready(Err(e)) => { error!( - "Skipping to next track, unable to load track <{:?}>: {:?}", - track_id, e + "Skipping to next track, unable to load track <{track_id:?}>: {e:?}" ); self.send_event(PlayerEvent::Unavailable { track_id, @@ -1253,7 +1236,7 @@ impl Future for PlayerInternal { }; } Poll::Ready(Err(_)) => { - debug!("Unable to preload {:?}", track_id); + debug!("Unable to preload {track_id:?}"); self.preload = PlayerPreload::None; // Let Spirc know that the track was unavailable. if let PlayerState::Playing { @@ -1368,7 +1351,7 @@ impl Future for PlayerInternal { } } Err(e) => { - error!("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, @@ -1381,7 +1364,7 @@ impl Future for PlayerInternal { self.handle_packet(result, normalisation_factor); } Err(e) => { - error!("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, @@ -1443,7 +1426,7 @@ impl PlayerInternal { match self.sink.start() { Ok(()) => self.sink_status = SinkStatus::Running, Err(e) => { - error!("{}", e); + error!("{e}"); self.handle_pause(); } } @@ -1466,7 +1449,7 @@ impl PlayerInternal { } } Err(e) => { - error!("{}", e); + error!("{e}"); exit(1); } } @@ -1694,7 +1677,7 @@ impl PlayerInternal { } if let Err(e) = self.sink.write(packet, &mut self.converter) { - error!("{}", e); + error!("{e}"); self.handle_pause(); } } @@ -2085,7 +2068,7 @@ impl PlayerInternal { }); } } - Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e), + Err(e) => error!("PlayerInternal::handle_command_seek error: {e}"), } } else { error!("Player::seek called from invalid state: {:?}", self.state); @@ -2107,7 +2090,7 @@ impl PlayerInternal { } fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult { - debug!("command={:?}", cmd); + debug!("command={cmd:?}"); match cmd { PlayerCommand::Load { track_id, diff --git a/src/main.rs b/src/main.rs index bfe158f7..0c6c3956 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,18 +5,18 @@ use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::{ connect::{ConnectConfig, Spirc}, core::{ - authentication::Credentials, cache::Cache, config::DeviceType, version, Session, - SessionConfig, + Session, SessionConfig, authentication::Credentials, cache::Cache, config::DeviceType, + version, }, discovery::DnsSdServiceBuilder, playback::{ - audio_backend::{self, SinkBuilder, BACKENDS}, + audio_backend::{self, BACKENDS, SinkBuilder}, config::{ AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl, }, dither, mixer::{self, MixerConfig, MixerFn}, - player::{coefficient_to_duration, duration_to_coefficient, Player}, + player::{Player, coefficient_to_duration, duration_to_coefficient}, }, }; use librespot_oauth::OAuthClientBuilder; @@ -24,6 +24,7 @@ use log::{debug, error, info, trace, warn}; use sha1::{Digest, Sha1}; use std::{ env, + ffi::OsStr, fs::create_dir_all, ops::RangeInclusive, path::{Path, PathBuf}, @@ -34,10 +35,11 @@ use std::{ }; use sysinfo::{ProcessesToUpdate, System}; use thiserror::Error; +use tokio::sync::Semaphore; use url::Url; mod player_event_handler; -use player_event_handler::{run_program_on_sink_events, EventHandler}; +use player_event_handler::{EventHandler, run_program_on_sink_events}; fn device_id(name: &str) -> String { HEXLOWER.encode(&Sha1::digest(name.as_bytes())) @@ -75,7 +77,9 @@ fn setup_logging(quiet: bool, verbose: bool) { builder.init(); if verbose && quiet { - warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode."); + warn!( + "`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode." + ); } } } @@ -219,7 +223,7 @@ struct Setup { zeroconf_backend: Option, } -fn get_setup() -> Setup { +async 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; @@ -810,7 +814,9 @@ fn get_setup() -> Setup { ALSA_MIXER_CONTROL, ] { if opt_present(a) { - warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); + warn!( + "Alsa specific options have no effect if the alsa backend is not enabled at build time." + ); break; } } @@ -1196,7 +1202,9 @@ fn get_setup() -> Setup { empty_string_error_msg(USERNAME, USERNAME_SHORT); } if opt_present(PASSWORD) { - error!("Invalid `--{PASSWORD}` / `-{PASSWORD_SHORT}`: Password authentication no longer supported, use OAuth"); + error!( + "Invalid `--{PASSWORD}` / `-{PASSWORD_SHORT}`: Password authentication no longer supported, use OAuth" + ); exit(1); } match cached_creds { @@ -1265,9 +1273,7 @@ fn get_setup() -> Setup { if let Some(reason) = no_discovery_reason.as_deref() { if opt_present(ZEROCONF_PORT) { - warn!( - "With {reason} `--{ZEROCONF_PORT}` / `-{ZEROCONF_PORT_SHORT}` has no effect." - ); + warn!("With {reason} `--{ZEROCONF_PORT}` / `-{ZEROCONF_PORT_SHORT}` has no effect."); } } @@ -1393,31 +1399,31 @@ fn get_setup() -> Setup { name.clone() }; - env::set_var("PULSE_PROP_application.name", pulseaudio_name); + set_env_var("PULSE_PROP_application.name", pulseaudio_name).await; } if env::var("PULSE_PROP_application.version").is_err() { - env::set_var("PULSE_PROP_application.version", version::SEMVER); + set_env_var("PULSE_PROP_application.version", version::SEMVER).await; } if env::var("PULSE_PROP_application.icon_name").is_err() { - env::set_var("PULSE_PROP_application.icon_name", "audio-x-generic"); + set_env_var("PULSE_PROP_application.icon_name", "audio-x-generic").await; } if env::var("PULSE_PROP_application.process.binary").is_err() { - env::set_var("PULSE_PROP_application.process.binary", "librespot"); + set_env_var("PULSE_PROP_application.process.binary", "librespot").await; } if env::var("PULSE_PROP_stream.description").is_err() { - env::set_var("PULSE_PROP_stream.description", "Spotify Connect endpoint"); + set_env_var("PULSE_PROP_stream.description", "Spotify Connect endpoint").await; } if env::var("PULSE_PROP_media.software").is_err() { - env::set_var("PULSE_PROP_media.software", "Spotify"); + set_env_var("PULSE_PROP_media.software", "Spotify").await; } if env::var("PULSE_PROP_media.role").is_err() { - env::set_var("PULSE_PROP_media.role", "music"); + set_env_var("PULSE_PROP_media.role", "music").await; } } @@ -1839,6 +1845,23 @@ fn get_setup() -> Setup { } } +// Initialize a static semaphore with only one permit, which is used to +// prevent setting environment variables from running in parallel. +static PERMIT: Semaphore = Semaphore::const_new(1); +async fn set_env_var, V: AsRef>(key: K, value: V) { + let permit = PERMIT + .acquire() + .await + .expect("Failed to acquire semaphore permit"); + + // SAFETY: This is safe because setting the environment variable will wait if the permit is + // already acquired by other callers. + unsafe { env::set_var(key, value) } + + // Drop the permit manually, so the compiler doesn't optimize it away as unused variable. + drop(permit); +} + #[tokio::main(flavor = "current_thread")] async fn main() { const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; @@ -1847,10 +1870,10 @@ async fn main() { const RECONNECT_RATE_LIMIT: usize = 5; if env::var(RUST_BACKTRACE).is_err() { - env::set_var(RUST_BACKTRACE, "full") + set_env_var(RUST_BACKTRACE, "full").await; } - let setup = get_setup(); + let setup = get_setup().await; let mut last_credentials = None; let mut spirc: Option = None; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index c4814572..36695c99 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -14,240 +14,263 @@ pub struct EventHandler { impl EventHandler { pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self { let on_event = onevent.to_string(); - let thread_handle = Some(thread::spawn(move || loop { - match player_events.blocking_recv() { - None => break, - Some(event) => { - let mut env_vars = HashMap::new(); + let thread_handle = Some(thread::spawn(move || { + loop { + match player_events.blocking_recv() { + None => break, + Some(event) => { + let mut env_vars = HashMap::new(); - match event { - PlayerEvent::PlayRequestIdChanged { play_request_id } => { - env_vars.insert("PLAYER_EVENT", "play_request_id_changed".to_string()); - env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); - } - PlayerEvent::TrackChanged { audio_item } => { - match audio_item.track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::TrackChanged: Invalid track id: {e}") - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "track_changed".to_string()); - env_vars.insert("TRACK_ID", id); - env_vars.insert("URI", audio_item.uri); - env_vars.insert("NAME", audio_item.name); - env_vars.insert( - "COVERS", - audio_item - .covers - .into_iter() - .map(|c| c.url) - .collect::>() - .join("\n"), - ); - env_vars.insert("LANGUAGE", audio_item.language.join("\n")); - env_vars - .insert("DURATION_MS", audio_item.duration_ms.to_string()); - env_vars - .insert("IS_EXPLICIT", audio_item.is_explicit.to_string()); + match event { + PlayerEvent::PlayRequestIdChanged { play_request_id } => { + env_vars + .insert("PLAYER_EVENT", "play_request_id_changed".to_string()); + env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); + } + PlayerEvent::TrackChanged { audio_item } => { + match audio_item.track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::TrackChanged: Invalid track id: {e}") + } + Ok(id) => { + env_vars + .insert("PLAYER_EVENT", "track_changed".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("URI", audio_item.uri); + env_vars.insert("NAME", audio_item.name); + env_vars.insert( + "COVERS", + audio_item + .covers + .into_iter() + .map(|c| c.url) + .collect::>() + .join("\n"), + ); + env_vars.insert("LANGUAGE", audio_item.language.join("\n")); + env_vars.insert( + "DURATION_MS", + audio_item.duration_ms.to_string(), + ); + env_vars.insert( + "IS_EXPLICIT", + audio_item.is_explicit.to_string(), + ); - match audio_item.unique_fields { - UniqueFields::Track { - artists, - album, - album_artists, - popularity, - number, - disc_number, - } => { - env_vars.insert("ITEM_TYPE", "Track".to_string()); - env_vars.insert( - "ARTISTS", - artists - .0 - .into_iter() - .map(|a| a.name) - .collect::>() - .join("\n"), - ); - env_vars - .insert("ALBUM_ARTISTS", album_artists.join("\n")); - env_vars.insert("ALBUM", album); - env_vars.insert("POPULARITY", popularity.to_string()); - env_vars.insert("NUMBER", number.to_string()); - env_vars.insert("DISC_NUMBER", disc_number.to_string()); - } - UniqueFields::Episode { - description, - publish_time, - show_name, - } => { - env_vars.insert("ITEM_TYPE", "Episode".to_string()); - env_vars.insert("DESCRIPTION", description); - env_vars.insert( - "PUBLISH_TIME", - publish_time.unix_timestamp().to_string(), - ); - env_vars.insert("SHOW_NAME", show_name); + match audio_item.unique_fields { + UniqueFields::Track { + artists, + album, + album_artists, + popularity, + number, + disc_number, + } => { + env_vars.insert("ITEM_TYPE", "Track".to_string()); + env_vars.insert( + "ARTISTS", + artists + .0 + .into_iter() + .map(|a| a.name) + .collect::>() + .join("\n"), + ); + env_vars.insert( + "ALBUM_ARTISTS", + album_artists.join("\n"), + ); + env_vars.insert("ALBUM", album); + env_vars + .insert("POPULARITY", popularity.to_string()); + env_vars.insert("NUMBER", number.to_string()); + env_vars + .insert("DISC_NUMBER", disc_number.to_string()); + } + UniqueFields::Episode { + description, + publish_time, + show_name, + } => { + env_vars.insert("ITEM_TYPE", "Episode".to_string()); + env_vars.insert("DESCRIPTION", description); + env_vars.insert( + "PUBLISH_TIME", + publish_time.unix_timestamp().to_string(), + ); + env_vars.insert("SHOW_NAME", show_name); + } } } } } - } - PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "stopped".to_string()); - env_vars.insert("TRACK_ID", id); - } - }, - PlayerEvent::Playing { - track_id, - position_ms, - .. - } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "playing".to_string()); - env_vars.insert("TRACK_ID", id); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } - }, - PlayerEvent::Paused { - track_id, - position_ms, - .. - } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "paused".to_string()); - env_vars.insert("TRACK_ID", id); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } - }, - PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "loading".to_string()); - env_vars.insert("TRACK_ID", id); - } - }, - PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "preloading".to_string()); - env_vars.insert("TRACK_ID", id); - } - }, - PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { - match track_id.to_base62() { - Err(e) => warn!( - "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}" - ), + PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"), Ok(id) => { - env_vars.insert("PLAYER_EVENT", "preload_next".to_string()); + env_vars.insert("PLAYER_EVENT", "stopped".to_string()); env_vars.insert("TRACK_ID", id); } + }, + PlayerEvent::Playing { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "playing".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Paused { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "paused".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "loading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Preloading { track_id, .. } => { + match track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::Preloading: Invalid track id: {e}") + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + } } - } - PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); - env_vars.insert("TRACK_ID", id); + PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { + match track_id.to_base62() { + Err(e) => warn!( + "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}" + ), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preload_next".to_string()); + env_vars.insert("TRACK_ID", id); + } + } } - }, - PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); - env_vars.insert("TRACK_ID", id); + PlayerEvent::EndOfTrack { track_id, .. } => { + match track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}") + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); + env_vars.insert("TRACK_ID", id); + } + } } - }, - PlayerEvent::VolumeChanged { volume } => { - env_vars.insert("PLAYER_EVENT", "volume_changed".to_string()); - env_vars.insert("VOLUME", volume.to_string()); - } - PlayerEvent::Seeked { - track_id, - position_ms, - .. - } => match track_id.to_base62() { - Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"), - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "seeked".to_string()); - env_vars.insert("TRACK_ID", id); - env_vars.insert("POSITION_MS", position_ms.to_string()); + PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() + { + Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::VolumeChanged { volume } => { + env_vars.insert("PLAYER_EVENT", "volume_changed".to_string()); + env_vars.insert("VOLUME", volume.to_string()); } - }, - PlayerEvent::PositionCorrection { - track_id, - position_ms, - .. - } => match track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}") + PlayerEvent::Seeked { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"), + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "seeked".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::PositionCorrection { + track_id, + position_ms, + .. + } => match track_id.to_base62() { + Err(e) => { + warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}") + } + Ok(id) => { + env_vars + .insert("PLAYER_EVENT", "position_correction".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::SessionConnected { + connection_id, + user_name, + } => { + env_vars.insert("PLAYER_EVENT", "session_connected".to_string()); + env_vars.insert("CONNECTION_ID", connection_id); + env_vars.insert("USER_NAME", user_name); } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "position_correction".to_string()); - env_vars.insert("TRACK_ID", id); - env_vars.insert("POSITION_MS", position_ms.to_string()); + PlayerEvent::SessionDisconnected { + connection_id, + user_name, + } => { + env_vars.insert("PLAYER_EVENT", "session_disconnected".to_string()); + env_vars.insert("CONNECTION_ID", connection_id); + env_vars.insert("USER_NAME", user_name); } - }, - PlayerEvent::SessionConnected { - connection_id, - user_name, - } => { - env_vars.insert("PLAYER_EVENT", "session_connected".to_string()); - env_vars.insert("CONNECTION_ID", connection_id); - env_vars.insert("USER_NAME", user_name); - } - PlayerEvent::SessionDisconnected { - connection_id, - user_name, - } => { - env_vars.insert("PLAYER_EVENT", "session_disconnected".to_string()); - env_vars.insert("CONNECTION_ID", connection_id); - env_vars.insert("USER_NAME", user_name); - } - PlayerEvent::SessionClientChanged { - client_id, - client_name, - client_brand_name, - client_model_name, - } => { - env_vars.insert("PLAYER_EVENT", "session_client_changed".to_string()); - env_vars.insert("CLIENT_ID", client_id); - env_vars.insert("CLIENT_NAME", client_name); - env_vars.insert("CLIENT_BRAND_NAME", client_brand_name); - env_vars.insert("CLIENT_MODEL_NAME", client_model_name); - } - PlayerEvent::ShuffleChanged { shuffle } => { - env_vars.insert("PLAYER_EVENT", "shuffle_changed".to_string()); - env_vars.insert("SHUFFLE", shuffle.to_string()); - } - PlayerEvent::RepeatChanged { context, track } => { - env_vars.insert("PLAYER_EVENT", "repeat_changed".to_string()); - env_vars.insert("REPEAT", context.to_string()); - env_vars.insert("REPEAT_TRACK", track.to_string()); - } - PlayerEvent::AutoPlayChanged { auto_play } => { - env_vars.insert("PLAYER_EVENT", "auto_play_changed".to_string()); - env_vars.insert("AUTO_PLAY", auto_play.to_string()); + PlayerEvent::SessionClientChanged { + client_id, + client_name, + client_brand_name, + client_model_name, + } => { + env_vars + .insert("PLAYER_EVENT", "session_client_changed".to_string()); + env_vars.insert("CLIENT_ID", client_id); + env_vars.insert("CLIENT_NAME", client_name); + env_vars.insert("CLIENT_BRAND_NAME", client_brand_name); + env_vars.insert("CLIENT_MODEL_NAME", client_model_name); + } + PlayerEvent::ShuffleChanged { shuffle } => { + env_vars.insert("PLAYER_EVENT", "shuffle_changed".to_string()); + env_vars.insert("SHUFFLE", shuffle.to_string()); + } + PlayerEvent::RepeatChanged { context, track } => { + env_vars.insert("PLAYER_EVENT", "repeat_changed".to_string()); + env_vars.insert("REPEAT", context.to_string()); + env_vars.insert("REPEAT_TRACK", track.to_string()); + } + PlayerEvent::AutoPlayChanged { auto_play } => { + env_vars.insert("PLAYER_EVENT", "auto_play_changed".to_string()); + env_vars.insert("AUTO_PLAY", auto_play.to_string()); + } + + PlayerEvent::FilterExplicitContentChanged { filter } => { + env_vars.insert( + "PLAYER_EVENT", + "filter_explicit_content_changed".to_string(), + ); + env_vars.insert("FILTER", filter.to_string()); + } + // Ignore event irrelevant for standalone binary like PositionChanged + _ => {} } - PlayerEvent::FilterExplicitContentChanged { filter } => { - env_vars.insert( - "PLAYER_EVENT", - "filter_explicit_content_changed".to_string(), - ); - env_vars.insert("FILTER", filter.to_string()); + if !env_vars.is_empty() { + run_program(env_vars, &on_event); } - // Ignore event irrelevant for standalone binary like PositionChanged - _ => {} - } - - if !env_vars.is_empty() { - run_program(env_vars, &on_event); } } } @@ -287,9 +310,7 @@ pub fn run_program_on_sink_events(sink_status: SinkStatus, onevent: &str) { fn run_program(env_vars: HashMap<&str, String>, onevent: &str) { let mut v: Vec<&str> = onevent.split_whitespace().collect(); - debug!( - "Running {onevent} with environment variables:\n{env_vars:#?}" - ); + debug!("Running {onevent} with environment variables:\n{env_vars:#?}"); match Command::new(v.remove(0)) .args(&v) From 1dcd041070e17b0af72ff03513fb02791d2a096b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 16:44:22 +0200 Subject: [PATCH 523/561] fix: install default crypto provider --- Cargo.lock | 3 ++- core/Cargo.toml | 1 + core/src/http_client.rs | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3f164aa0..a9e06301 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -2058,6 +2058,7 @@ dependencies = [ "rand 0.9.2", "rand_distr", "rsa", + "rustls 0.23.31", "serde", "serde_json", "sha1", diff --git a/core/Cargo.toml b/core/Cargo.toml index aedaf064..1ed9a396 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -77,6 +77,7 @@ uuid = { version = "1", default-features = false, features = ["v4"] } data-encoding = "2.9" flate2 = "1.1" protobuf-json-mapping = "3.7" +rustls = { version = "0.23", features = ["aws-lc-rs"] } # Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies # but currently, hyper-proxy2 and tokio-tungstenite do not support it. diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 728857f9..21ac8fa5 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -145,6 +145,11 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process + let _ = rustls::crypto::aws_lc_rs::default_provider() + .install_default() + .map_err(|e| { + Error::internal(format!("unable to install default crypto provider: {e:?}")) + }); // On supported platforms, use native roots #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] From 648c9e30eabb3dbdd4792c0be9d767a9b8a3f8e7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 17:06:14 +0200 Subject: [PATCH 524/561] ci: bump MSRV to 1.85 and update actions --- .devcontainer/Dockerfile | 2 +- .devcontainer/Dockerfile.alpine | 2 +- .github/workflows/test.yml | 78 ++++++++++++------------ contrib/Dockerfile | 4 +- contrib/cross-compile-armv6hf/Dockerfile | 2 +- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 233ac838..d7594a55 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG debian_version=slim-bookworm -ARG rust_version=1.81.0 +ARG rust_version=1.85.0 FROM rust:${rust_version}-${debian_version} ARG DEBIAN_FRONTEND=noninteractive diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index 1a908955..bec4bb71 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 ARG alpine_version=alpine3.19 -ARG rust_version=1.81.0 +ARG rust_version=1.85.0 FROM rust:${rust_version}-${alpine_version} ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b58e61c2..5e84f9c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -51,7 +51,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - run: cargo fmt --all -- --check @@ -68,7 +68,7 @@ jobs: toolchain: [stable] steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v4 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y @@ -79,7 +79,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.2.0 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index @@ -109,7 +109,7 @@ jobs: matrix: os: [ubuntu-latest] toolchain: - - "1.81" # MSRV (Minimum supported rust version) + - "1.85" # MSRV (Minimum supported rust version) - stable experimental: [false] # Ignore failures in beta @@ -119,7 +119,7 @@ jobs: experimental: true steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -130,7 +130,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.2.0 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index @@ -164,12 +164,12 @@ jobs: matrix: os: [windows-latest] toolchain: - - "1.81" # MSRV (Minimum supported rust version) + - "1.85" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.2 - + uses: actions/checkout@v5 + # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build - name: Install NASM uses: ilammy/setup-nasm@v1.5.1 @@ -183,7 +183,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.2.0 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index @@ -210,16 +210,16 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest] - target: + target: - armv7-unknown-linux-gnueabihf - aarch64-unknown-linux-gnu - riscv64gc-unknown-linux-gnu toolchain: - - "1.81" # MSRV (Minimum supported rust version) + - "1.85" # MSRV (Minimum supported rust version) - stable steps: - name: Checkout code - uses: actions/checkout@v4.2.2 + uses: actions/checkout@v5 - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y @@ -230,7 +230,7 @@ jobs: shell: bash - name: Cache Rust dependencies - uses: actions/cache@v4.2.0 + uses: actions/cache@v4 with: path: | ~/.cargo/registry/index @@ -250,36 +250,36 @@ jobs: - name: Install cross compiler run: | - if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then - sudo apt-get install -y gcc-arm-linux-gnueabihf - fi - if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then - sudo apt-get install -y gcc-aarch64-linux-gnu - fi - if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then - sudo apt-get install -y gcc-riscv64-linux-gnu - fi - + if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then + sudo apt-get install -y gcc-arm-linux-gnueabihf + fi + if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then + sudo apt-get install -y gcc-aarch64-linux-gnu + fi + if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then + sudo apt-get install -y gcc-riscv64-linux-gnu + fi + - name: Set target link compiler run: | - # Convert target to uppercase and replace - with _ - target=${{ matrix.target }} - target=${target^^} - target=${target//-/_} - if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then - echo "CARGO_TARGET_${target}_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV - fi - if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then - echo "CARGO_TARGET_${target}_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - fi - if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then - echo "CARGO_TARGET_${target}_LINKER=riscv64-linux-gnu-gcc" >> $GITHUB_ENV - fi - + # Convert target to uppercase and replace - with _ + target=${{ matrix.target }} + target=${target^^} + target=${target//-/_} + if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then + echo "CARGO_TARGET_${target}_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV + fi + if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then + echo "CARGO_TARGET_${target}_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV + fi + if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then + echo "CARGO_TARGET_${target}_LINKER=riscv64-linux-gnu-gcc" >> $GITHUB_ENV + fi + - name: Fetch run: cargo fetch --locked - name: Build run: cargo build --frozen --verbose --target ${{ matrix.target }} --no-default-features - + - name: Check binary run: file target/${{ matrix.target }}/debug/librespot diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 644ab029..cae7a6d7 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -40,10 +40,10 @@ RUN dpkg --add-architecture arm64 && \ libpulse0:arm64 \ libpulse0:armel \ libpulse0:armhf \ - pkg-config + pkg-config ENV PATH="/root/.cargo/bin/:${PATH}" -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y && \ +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.85 -y && \ rustup target add aarch64-unknown-linux-gnu && \ rustup target add arm-unknown-linux-gnueabi && \ rustup target add arm-unknown-linux-gnueabihf && \ diff --git a/contrib/cross-compile-armv6hf/Dockerfile b/contrib/cross-compile-armv6hf/Dockerfile index 16694afa..86ac2644 100644 --- a/contrib/cross-compile-armv6hf/Dockerfile +++ b/contrib/cross-compile-armv6hf/Dockerfile @@ -22,7 +22,7 @@ RUN mkdir /sysroot && \ dpkg -x libasound2-dev*.deb /sysroot/ # Install rust. -RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.81 -y +RUN curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain 1.85 -y ENV PATH="/root/.cargo/bin/:${PATH}" RUN rustup target add arm-unknown-linux-gnueabihf RUN mkdir /.cargo && \ From 445f8b10a2cf0d445091ca666b51a3ecb4b6b811 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 17:11:43 +0200 Subject: [PATCH 525/561] refactor: clean up Rodio fallback handling --- playback/src/audio_backend/rodio.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index ff3dce96..a17ea5f3 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -193,11 +193,18 @@ fn create_sink( AudioFormat::S16 => cpal::SampleFormat::I16, }; - let stream = rodio::OutputStreamBuilder::default() - .with_device(cpal_device) + let stream = match rodio::OutputStreamBuilder::default() + .with_device(cpal_device.clone()) .with_config(&config.config()) .with_sample_format(sample_format) - .open_stream_or_fallback()?; + .open_stream() + { + Ok(exact_stream) => exact_stream, + Err(e) => { + warn!("unable to create Rodio output, falling back to default: {e}"); + rodio::OutputStreamBuilder::from_device(cpal_device)?.open_stream_or_fallback()? + } + }; let sink = rodio::Sink::connect_new(stream.mixer()); Ok((sink, stream)) @@ -209,12 +216,6 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro host.id().name() ); - let mut format = format; - if format != AudioFormat::S16 && format != AudioFormat::F32 { - error!("Rodio currently only supports F32 and S16 formats, falling back to S16"); - format = AudioFormat::S16; - } - let (sink, stream) = create_sink(&host, device, format).unwrap(); debug!("Rodio sink was created"); From c4766ceb996f805ce85c710ddac5fd1533bac3f5 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 17:22:37 +0200 Subject: [PATCH 526/561] fix: downgrade sysinfo for MSRV constraints --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- core/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a9e06301..a6066645 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3821,9 +3821,9 @@ dependencies = [ [[package]] name = "sysinfo" -version = "0.37.0" +version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07cec4dc2d2e357ca1e610cfb07de2fa7a10fc3e9fe89f72545f3d244ea87753" +checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d" dependencies = [ "libc", "memchr", diff --git a/Cargo.toml b/Cargo.toml index a930b39e..2bdcbb9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,7 +65,7 @@ futures-util = { version = "0.3", default-features = false } getopts = "0.2" log = "0.4" sha1 = "0.10" -sysinfo = { version = "0.37", default-features = false, features = ["system"] } +sysinfo = { version = "0.36", default-features = false, features = ["system"] } thiserror = "2" tokio = { version = "1", features = [ "rt", diff --git a/core/Cargo.toml b/core/Cargo.toml index 1ed9a396..5381352b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -58,7 +58,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha1 = { version = "0.10", features = ["oid"] } shannon = "0.2" -sysinfo = { version = "0.37", default-features = false, features = ["system"] } +sysinfo = { version = "0.36", default-features = false, features = ["system"] } thiserror = "2" time = { version = "0.3", features = ["formatting", "parsing"] } tokio = { version = "1", features = [ From fe7ca0d7009c59d3189d1682154ad63a8fb71332 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 18:34:40 +0200 Subject: [PATCH 527/561] refactor: move from aws-lc to ring --- .github/workflows/test.yml | 4 - Cargo.lock | 176 ++----------------------------------- core/Cargo.toml | 8 +- core/src/http_client.rs | 2 +- 4 files changed, 12 insertions(+), 178 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e84f9c0..04332316 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -170,10 +170,6 @@ jobs: - name: Checkout code uses: actions/checkout@v5 - # hyper-rustls >=0.27 uses aws-lc as default backend which requires NASM to build - - name: Install NASM - uses: ilammy/setup-nasm@v1.5.1 - - name: Install toolchain run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y diff --git a/Cargo.lock b/Cargo.lock index a6066645..21bd10f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,29 +194,6 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" -[[package]] -name = "aws-lc-rs" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" -dependencies = [ - "bindgen", - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "backtrace" version = "0.3.75" @@ -244,29 +221,6 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" -[[package]] -name = "bindgen" -version = "0.69.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" -dependencies = [ - "bitflags 2.9.1", - "cexpr", - "clang-sys", - "itertools 0.12.1", - "lazy_static", - "lazycell", - "log", - "prettyplease", - "proc-macro2", - "quote", - "regex", - "rustc-hash 1.1.0", - "shlex", - "syn", - "which", -] - [[package]] name = "bitflags" version = "1.3.2" @@ -318,8 +272,6 @@ version = "1.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" dependencies = [ - "jobserver", - "libc", "shlex", ] @@ -329,15 +281,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cexpr" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" -dependencies = [ - "nom", -] - [[package]] name = "cfg-expr" version = "0.20.2" @@ -385,26 +328,6 @@ dependencies = [ "inout", ] -[[package]] -name = "clang-sys" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" -dependencies = [ - "glob", - "libc", - "libloading 0.8.8", -] - -[[package]] -name = "cmake" -version = "0.1.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" -dependencies = [ - "cc", -] - [[package]] name = "colorchoice" version = "1.0.4" @@ -687,12 +610,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.15.0" @@ -852,12 +769,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "futures" version = "0.3.31" @@ -1075,12 +986,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "gobject-sys" version = "0.21.1" @@ -1125,7 +1030,7 @@ dependencies = [ "futures-util", "glib", "gstreamer-sys", - "itertools 0.14.0", + "itertools", "kstring", "libc", "muldiv", @@ -1718,15 +1623,6 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.14.0" @@ -1764,7 +1660,7 @@ dependencies = [ "bitflags 1.3.2", "lazy_static", "libc", - "libloading 0.7.4", + "libloading", "log", "pkg-config", ] @@ -1815,16 +1711,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" -[[package]] -name = "jobserver" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" -dependencies = [ - "getrandom 0.3.3", - "libc", -] - [[package]] name = "js-sys" version = "0.3.77" @@ -1853,12 +1739,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lazycell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" - [[package]] name = "libc" version = "0.2.175" @@ -1875,16 +1755,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "libloading" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" -dependencies = [ - "cfg-if", - "windows-targets 0.53.3", -] - [[package]] name = "libm" version = "0.2.15" @@ -2247,12 +2117,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -2347,16 +2211,6 @@ dependencies = [ "memoffset", ] -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - [[package]] name = "nonzero_ext" version = "0.3.0" @@ -2872,16 +2726,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "prettyplease" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" -dependencies = [ - "proc-macro2", - "syn", -] - [[package]] name = "priority-queue" version = "2.5.0" @@ -2994,7 +2838,7 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.31", "socket2 0.5.10", "thiserror 2.0.14", @@ -3014,7 +2858,7 @@ dependencies = [ "lru-slab", "rand 0.9.2", "ring", - "rustc-hash 2.1.1", + "rustc-hash", "rustls 0.23.31", "rustls-pki-types", "slab", @@ -3257,12 +3101,6 @@ version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" -[[package]] -name = "rustc-hash" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" - [[package]] name = "rustc-hash" version = "2.1.1" @@ -3315,7 +3153,6 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "aws-lc-rs", "log", "once_cell", "ring", @@ -3386,7 +3223,6 @@ version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ - "aws-lc-rs", "ring", "rustls-pki-types", "untrusted", @@ -3790,9 +3626,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.104" +version = "2.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" dependencies = [ "proc-macro2", "quote", diff --git a/core/Cargo.toml b/core/Cargo.toml index 5381352b..5df740be 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -77,7 +77,9 @@ uuid = { version = "1", default-features = false, features = ["v4"] } data-encoding = "2.9" flate2 = "1.1" protobuf-json-mapping = "3.7" -rustls = { version = "0.23", features = ["aws-lc-rs"] } +rustls = { version = "0.23", default-features = false, features = [ + "ring", +] } # Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies # but currently, hyper-proxy2 and tokio-tungstenite do not support it. @@ -86,7 +88,7 @@ hyper-proxy2 = { version = "0.1", default-features = false, features = [ "rustls", ] } hyper-rustls = { version = "0.27", default-features = false, features = [ - "aws-lc-rs", + "ring", "http1", "logging", "tls12", @@ -102,7 +104,7 @@ hyper-proxy2 = { version = "0.1", default-features = false, features = [ "rustls-webpki", ] } hyper-rustls = { version = "0.27", default-features = false, features = [ - "aws-lc-rs", + "ring", "http1", "logging", "tls12", diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 21ac8fa5..1f0ee6f0 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -145,7 +145,7 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process - let _ = rustls::crypto::aws_lc_rs::default_provider() + let _ = rustls::crypto::ring::default_provider() .install_default() .map_err(|e| { Error::internal(format!("unable to install default crypto provider: {e:?}")) From 416bf00888c93772ea9ac868712bcc7537b69067 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 18:40:00 +0200 Subject: [PATCH 528/561] feat: improve Gaussian dither --- playback/src/dither.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 9145e045..db0d83df 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -82,8 +82,15 @@ impl Ditherer for GaussianDitherer { fn new() -> Self { Self { cached_rng: create_rng(), - // 1/2 LSB RMS needed to linearize the response: - distribution: Normal::new(0.0, 0.5).unwrap(), + // For Gaussian to achieve equivalent decorrelation to triangular dithering, it needs + // 3-4 dB higher amplitude than TPDF's optimal 0.408 LSB. If optimizing: + // - minimum correlation: σ ≈ 0.58 + // - perceptual equivalence: σ ≈ 0.65 + // - worst-case performance: σ ≈ 0.70 + // + // σ = 0.6 LSB is a reasonable compromise that balances mathematical theory with + // empirical performance across various signal types. + distribution: Normal::new(0.0, 0.6).unwrap(), } } From 056d125cb2633efd9025188a70ecd1e38da8c1ad Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 19:05:52 +0200 Subject: [PATCH 529/561] refactor: move from native to webpki certs on all platforms --- Cargo.lock | 153 ++-------------------------------------- core/Cargo.toml | 23 +----- core/src/http_client.rs | 7 -- oauth/Cargo.toml | 7 +- 4 files changed, 11 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 21bd10f4..cf103a10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -369,16 +369,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -745,21 +735,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1316,7 +1291,6 @@ dependencies = [ "hyper-rustls 0.26.0", "hyper-util", "pin-project-lite", - "rustls-native-certs 0.7.3", "tokio", "tokio-rustls 0.25.0", "tower-service", @@ -1336,7 +1310,7 @@ dependencies = [ "hyper-util", "log", "rustls 0.22.4", - "rustls-native-certs 0.7.3", + "rustls-native-certs", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", @@ -1355,7 +1329,6 @@ dependencies = [ "hyper-util", "log", "rustls 0.23.31", - "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -1363,22 +1336,6 @@ dependencies = [ "webpki-roots 1.0.2", ] -[[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - [[package]] name = "hyper-util" version = "0.1.16" @@ -2152,23 +2109,6 @@ dependencies = [ "serde", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - [[package]] name = "ndk" version = "0.9.0" @@ -2485,50 +2425,12 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "openssl" -version = "0.10.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" -dependencies = [ - "bitflags 2.9.1", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "option-operations" version = "0.5.0" @@ -3012,7 +2914,6 @@ checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -3022,12 +2923,9 @@ dependencies = [ "http-body-util", "hyper", "hyper-rustls 0.27.7", - "hyper-tls", "hyper-util", "js-sys", "log", - "mime", - "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -3038,7 +2936,6 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-native-tls", "tokio-rustls 0.26.2", "tower", "tower-http", @@ -3172,19 +3069,7 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework 2.11.1", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework 3.3.0", + "security-framework", ] [[package]] @@ -3294,20 +3179,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.1", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" -dependencies = [ - "bitflags 2.9.1", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3676,7 +3548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.1", - "core-foundation 0.9.4", + "core-foundation", "system-configuration-sys", ] @@ -3862,16 +3734,6 @@ dependencies = [ "syn", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.25.0" @@ -3913,7 +3775,6 @@ dependencies = [ "futures-util", "log", "rustls 0.23.31", - "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.2", @@ -4145,12 +4006,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "vergen" version = "9.0.6" diff --git a/core/Cargo.toml b/core/Cargo.toml index 5df740be..8d2159be 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -77,29 +77,8 @@ uuid = { version = "1", default-features = false, features = ["v4"] } data-encoding = "2.9" flate2 = "1.1" protobuf-json-mapping = "3.7" -rustls = { version = "0.23", default-features = false, features = [ - "ring", -] } +rustls = { version = "0.23", default-features = false, features = ["ring"] } -# Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies -# but currently, hyper-proxy2 and tokio-tungstenite do not support it. -[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies] -hyper-proxy2 = { version = "0.1", default-features = false, features = [ - "rustls", -] } -hyper-rustls = { version = "0.27", default-features = false, features = [ - "ring", - "http1", - "logging", - "tls12", - "native-tokio", - "http2", -] } -tokio-tungstenite = { version = "0.27", default-features = false, features = [ - "rustls-tls-native-roots", -] } - -[target.'cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))'.dependencies] hyper-proxy2 = { version = "0.1", default-features = false, features = [ "rustls-webpki", ] } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 1f0ee6f0..bf461985 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -151,14 +151,7 @@ impl HttpClient { Error::internal(format!("unable to install default crypto provider: {e:?}")) }); - // On supported platforms, use native roots - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - let tls = HttpsConnectorBuilder::new().with_native_roots()?; - - // Otherwise, use webpki roots - #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] let tls = HttpsConnectorBuilder::new().with_webpki_roots(); - let https_connector = tls.https_or_http().enable_http1().enable_http2().build(); // When not using a proxy a dummy proxy is configured that will not intercept any traffic. diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 9a469487..dd514b00 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -11,7 +11,12 @@ edition = "2021" [dependencies] log = "0.4" oauth2 = { version = "5.0", features = ["reqwest", "reqwest-blocking"] } -reqwest = { version = "0.12", features = ["blocking"] } +reqwest = { version = "0.12", default-features = false, features = [ + "blocking", + "http2", + "rustls-tls", + "system-proxy", +] } open = "5.3" thiserror = "2" url = "2.5" From 218eced5563c65beace1121c264d14cd3e255d4b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Aug 2025 23:09:59 +0200 Subject: [PATCH 530/561] style: format with style edition 2024 --- audio/src/fetch/mod.rs | 10 ++--- audio/src/fetch/receive.rs | 2 +- connect/src/context_resolver.rs | 2 +- connect/src/shuffle_vec.rs | 2 +- connect/src/spirc.rs | 13 +++--- connect/src/state.rs | 4 +- connect/src/state/context.rs | 2 +- connect/src/state/handle.rs | 4 +- connect/src/state/options.rs | 2 +- connect/src/state/restrictions.rs | 2 +- connect/src/state/tracks.rs | 2 +- core/src/apresolve.rs | 2 +- core/src/audio_key.rs | 2 +- core/src/authentication.rs | 4 +- core/src/cache.rs | 2 +- core/src/cdn_url.rs | 24 +++++++--- core/src/channel.rs | 4 +- core/src/connection/mod.rs | 2 +- core/src/date.rs | 4 +- core/src/dealer/manager.rs | 4 +- core/src/dealer/mod.rs | 11 +++-- core/src/dealer/protocol.rs | 4 +- core/src/deserialize_with.rs | 2 +- core/src/error.rs | 2 +- core/src/file_id.rs | 2 +- core/src/http_client.rs | 16 +++---- core/src/login5.rs | 8 ++-- core/src/mercury/mod.rs | 2 +- core/src/mercury/types.rs | 2 +- core/src/session.rs | 4 +- core/src/spclient.rs | 14 +++--- core/src/util.rs | 13 +++--- discovery/src/lib.rs | 4 +- discovery/src/server.rs | 6 +-- metadata/src/album.rs | 6 +-- metadata/src/artist.rs | 4 +- metadata/src/audio/item.rs | 4 +- metadata/src/copyright.rs | 2 +- metadata/src/episode.rs | 4 +- metadata/src/image.rs | 4 +- metadata/src/playlist/annotation.rs | 2 +- metadata/src/playlist/item.rs | 2 +- metadata/src/playlist/list.rs | 4 +- metadata/src/playlist/operation.rs | 2 +- metadata/src/show.rs | 4 +- metadata/src/track.rs | 4 +- oauth/src/lib.rs | 4 +- playback/src/audio_backend/alsa.rs | 12 +++-- playback/src/audio_backend/gstreamer.rs | 4 +- playback/src/audio_backend/jackaudio.rs | 4 +- playback/src/audio_backend/pipe.rs | 4 +- playback/src/audio_backend/portaudio.rs | 2 +- playback/src/audio_backend/pulseaudio.rs | 4 +- playback/src/audio_backend/subprocess.rs | 6 ++- playback/src/config.rs | 2 +- playback/src/decoder/passthrough_decoder.rs | 2 +- playback/src/decoder/symphonia_decoder.rs | 2 +- playback/src/dither.rs | 2 +- playback/src/mixer/alsamixer.rs | 8 +++- playback/src/mixer/softmixer.rs | 2 +- playback/src/player.rs | 49 +++++++++++++++------ rustfmt.toml | 3 +- 62 files changed, 196 insertions(+), 139 deletions(-) diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 96a6eb96..cd117731 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -5,21 +5,21 @@ use std::{ fs, io::{self, Read, Seek, SeekFrom}, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, Arc, OnceLock, + atomic::{AtomicBool, AtomicUsize, Ordering}, }, time::Duration, }; -use futures_util::{future::IntoStream, StreamExt, TryFutureExt}; -use hyper::{body::Incoming, header::CONTENT_RANGE, Response, StatusCode}; +use futures_util::{StreamExt, TryFutureExt, future::IntoStream}; +use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE}; use hyper_util::client::legacy::ResponseFuture; use parking_lot::{Condvar, Mutex}; use tempfile::NamedTempFile; use thiserror::Error; -use tokio::sync::{mpsc, oneshot, Semaphore}; +use tokio::sync::{Semaphore, mpsc, oneshot}; -use librespot_core::{cdn_url::CdnUrl, Error, FileId, Session}; +use librespot_core::{Error, FileId, Session, cdn_url::CdnUrl}; use self::receive::audio_file_fetch; diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 5f92380a..3d7dfa64 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -12,7 +12,7 @@ use hyper::StatusCode; use tempfile::NamedTempFile; use tokio::sync::{mpsc, oneshot}; -use librespot_core::{http_client::HttpClient, session::Session, Error}; +use librespot_core::{Error, http_client::HttpClient, session::Session}; use crate::range_set::{Range, RangeSet}; diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs index b566e8d7..29336f0a 100644 --- a/connect/src/context_resolver.rs +++ b/connect/src/context_resolver.rs @@ -4,7 +4,7 @@ use crate::{ autoplay_context_request::AutoplayContextRequest, context::Context, transfer_state::TransferState, }, - state::{context::ContextType, ConnectState}, + state::{ConnectState, context::ContextType}, }; use std::{ cmp::PartialEq, diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs index 01000cde..ca73eb77 100644 --- a/connect/src/shuffle_vec.rs +++ b/connect/src/shuffle_vec.rs @@ -1,4 +1,4 @@ -use rand::{rngs::SmallRng, Rng, SeedableRng}; +use rand::{Rng, SeedableRng, rngs::SmallRng}; use std::{ ops::{Deref, DerefMut}, vec::IntoIter, diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 57f8b978..7613e0ef 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1,13 +1,14 @@ use crate::{ + LoadContextOptions, LoadRequestOptions, PlayContext, context_resolver::{ContextAction, ContextResolver, ResolveContext}, core::{ + Error, Session, SpotifyId, authentication::Credentials, dealer::{ manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply}, protocol::{Command, FallbackWrapper, Message, Request}, }, session::UserAttributes, - Error, Session, SpotifyId, }, model::{LoadRequest, PlayingTrack, SpircPlayStatus}, playback::{ @@ -28,15 +29,14 @@ use crate::{ provider::IsProvider, {ConnectConfig, ConnectState}, }, - LoadContextOptions, LoadRequestOptions, PlayContext, }; use futures_util::StreamExt; use librespot_protocol::context_page::ContextPage; use protobuf::MessageField; use std::{ future::Future, - sync::atomic::{AtomicUsize, Ordering}, sync::Arc, + sync::atomic::{AtomicUsize, Ordering}, time::{Duration, SystemTime, UNIX_EPOCH}, }; use thiserror::Error; @@ -951,7 +951,8 @@ impl SpircTask { { debug!( "ignoring context update for <{:?}>, because it isn't the current context <{}>", - update_context.context.uri, self.connect_state.context_uri() + update_context.context.uri, + self.connect_state.context_uri() ) } else { self.context_resolver.add(ResolveContext::from_context( @@ -1615,7 +1616,9 @@ impl SpircTask { let uri = String::from_utf8(uri)?; if self.connect_state.context_uri() != &uri { - debug!("ignoring playlist modification update for playlist <{uri}>, because it isn't the current context"); + debug!( + "ignoring playlist modification update for playlist <{uri}>, because it isn't the current context" + ); return Ok(()); } diff --git a/connect/src/state.rs b/connect/src/state.rs index 64a1e14a..9e73d861 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -9,8 +9,8 @@ mod transfer; use crate::{ core::{ - config::DeviceType, date::Date, dealer::protocol::Request, spclient::SpClientResult, - version, Error, Session, + Error, Session, config::DeviceType, date::Date, dealer::protocol::Request, + spclient::SpClientResult, version, }, model::SpircPlayStatus, protocol::{ diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index c9afbb64..eb04fe5f 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -9,9 +9,9 @@ use crate::{ }, shuffle_vec::ShuffleVec, state::{ + ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, StateError, metadata::Metadata, provider::{IsProvider, Provider}, - ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, }, }; use protobuf::MessageField; diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index 99840800..acf8bdcb 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -1,9 +1,9 @@ use crate::{ - core::{dealer::protocol::SetQueueCommand, Error}, + core::{Error, dealer::protocol::SetQueueCommand}, state::{ + ConnectState, context::{ContextType, ResetContext}, metadata::Metadata, - ConnectState, }, }; use protobuf::MessageField; diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index 80c62e6b..97288b7e 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -2,9 +2,9 @@ use crate::{ core::Error, protocol::player::ContextPlayerOptions, state::{ + ConnectState, StateError, context::{ContextType, ResetContext}, metadata::Metadata, - ConnectState, StateError, }, }; use protobuf::MessageField; diff --git a/connect/src/state/restrictions.rs b/connect/src/state/restrictions.rs index e4604c54..e1f78094 100644 --- a/connect/src/state/restrictions.rs +++ b/connect/src/state/restrictions.rs @@ -1,5 +1,5 @@ -use crate::state::provider::IsProvider; use crate::state::ConnectState; +use crate::state::provider::IsProvider; use librespot_protocol::player::Restrictions; use protobuf::MessageField; diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index c5da0351..afaf7c00 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -2,10 +2,10 @@ use crate::{ core::{Error, SpotifyId}, protocol::player::ProvidedTrack, state::{ + ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError, context::ContextType, metadata::Metadata, provider::{IsProvider, Provider}, - ConnectState, StateError, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, }, }; use protobuf::MessageField; diff --git a/core/src/apresolve.rs b/core/src/apresolve.rs index 82d37e73..38dde797 100644 --- a/core/src/apresolve.rs +++ b/core/src/apresolve.rs @@ -139,7 +139,7 @@ impl ApResolver { _ => { return Err(Error::unimplemented(format!( "No implementation to resolve access point {endpoint}" - ))) + ))); } }; diff --git a/core/src/audio_key.rs b/core/src/audio_key.rs index 79710ec5..42cad43c 100644 --- a/core/src/audio_key.rs +++ b/core/src/audio_key.rs @@ -5,7 +5,7 @@ use bytes::Bytes; use thiserror::Error; use tokio::sync::oneshot; -use crate::{packet::PacketType, util::SeqGenerator, Error, FileId, SpotifyId}; +use crate::{Error, FileId, SpotifyId, packet::PacketType, util::SeqGenerator}; #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub struct AudioKey(pub [u8; 16]); diff --git a/core/src/authentication.rs b/core/src/authentication.rs index 8dd68540..0b1678dd 100644 --- a/core/src/authentication.rs +++ b/core/src/authentication.rs @@ -1,8 +1,8 @@ use std::io::{self, Read}; use aes::Aes192; -use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; use byteorder::{BigEndian, ByteOrder}; use pbkdf2::pbkdf2_hmac; use protobuf::Enum; @@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize}; use sha1::{Digest, Sha1}; use thiserror::Error; -use crate::{protocol::authentication::AuthenticationType, Error}; +use crate::{Error, protocol::authentication::AuthenticationType}; #[derive(Debug, Error)] pub enum AuthenticationError { diff --git a/core/src/cache.rs b/core/src/cache.rs index fe38b681..2d2ef53d 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -12,7 +12,7 @@ use parking_lot::Mutex; use priority_queue::PriorityQueue; use thiserror::Error; -use crate::{authentication::Credentials, error::ErrorKind, Error, FileId}; +use crate::{Error, FileId, authentication::Credentials, error::ErrorKind}; #[derive(Debug, Error)] pub enum CacheError { diff --git a/core/src/cdn_url.rs b/core/src/cdn_url.rs index 49e4757c..818f68f5 100644 --- a/core/src/cdn_url.rs +++ b/core/src/cdn_url.rs @@ -5,11 +5,11 @@ use thiserror::Error; use time::Duration; use url::Url; -use super::{date::Date, Error, FileId, Session}; +use super::{Error, FileId, Session, date::Date}; use librespot_protocol as protocol; -use protocol::storage_resolve::storage_resolve_response::Result as StorageResolveResponse_Result; use protocol::storage_resolve::StorageResolveResponse as CdnUrlMessage; +use protocol::storage_resolve::storage_resolve_response::Result as StorageResolveResponse_Result; #[derive(Debug, Clone)] pub struct MaybeExpiringUrl(pub String, pub Option); @@ -201,7 +201,9 @@ impl TryFrom for MaybeExpiringUrls { expiry = Some(Date::from(with_margin)); } } else { - warn!("Cannot parse CDN URL expiry timestamp '{exp_str}' from '{cdn_url}'"); + warn!( + "Cannot parse CDN URL expiry timestamp '{exp_str}' from '{cdn_url}'" + ); } } else { warn!("Unknown CDN URL format: {cdn_url}"); @@ -225,10 +227,18 @@ mod test { let mut msg = CdnUrlMessage::new(); msg.result = StorageResolveResponse_Result::CDN.into(); msg.cdnurl = vec![ - format!("https://audio-cf.spotifycdn.com/audio/844ecdb297a87ebfee4399f28892ef85d9ba725f?verify={timestamp}-4R3I2w2q7OfNkR%2FGH8qH7xtIKUPlDxywBuADY%2BsvMeU%3D"), - format!("https://audio-ak-spotify-com.akamaized.net/audio/foo?__token__=exp={timestamp}~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41"), - format!("https://audio-gm-off.spotifycdn.com/audio/foo?Expires={timestamp}~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM="), - format!("https://audio4-fa.scdn.co/audio/foo?{timestamp}_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4="), + format!( + "https://audio-cf.spotifycdn.com/audio/844ecdb297a87ebfee4399f28892ef85d9ba725f?verify={timestamp}-4R3I2w2q7OfNkR%2FGH8qH7xtIKUPlDxywBuADY%2BsvMeU%3D" + ), + format!( + "https://audio-ak-spotify-com.akamaized.net/audio/foo?__token__=exp={timestamp}~hmac=4e661527574fab5793adb99cf04e1c2ce12294c71fe1d39ffbfabdcfe8ce3b41" + ), + format!( + "https://audio-gm-off.spotifycdn.com/audio/foo?Expires={timestamp}~FullPath~hmac=IIZA28qptl8cuGLq15-SjHKHtLoxzpy_6r_JpAU4MfM=" + ), + format!( + "https://audio4-fa.scdn.co/audio/foo?{timestamp}_0GKSyXjLaTW1BksFOyI4J7Tf9tZDbBUNNPu9Mt4mhH4=" + ), "https://audio4-fa.scdn.co/foo?baz".to_string(), ]; msg.fileid = vec![0]; diff --git a/core/src/channel.rs b/core/src/channel.rs index 86909978..dfdc4dc2 100644 --- a/core/src/channel.rs +++ b/core/src/channel.rs @@ -9,12 +9,12 @@ use std::{ use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; use futures_core::Stream; -use futures_util::{lock::BiLock, ready, StreamExt}; +use futures_util::{StreamExt, lock::BiLock, ready}; use num_traits::FromPrimitive; use thiserror::Error; use tokio::sync::mpsc; -use crate::{packet::PacketType, util::SeqGenerator, Error}; +use crate::{Error, packet::PacketType, util::SeqGenerator}; component! { ChannelManager : ChannelManagerInner { diff --git a/core/src/connection/mod.rs b/core/src/connection/mod.rs index f83827c0..83017189 100644 --- a/core/src/connection/mod.rs +++ b/core/src/connection/mod.rs @@ -13,7 +13,7 @@ use tokio::net::TcpStream; use tokio_util::codec::Framed; use url::Url; -use crate::{authentication::Credentials, packet::PacketType, version, Error}; +use crate::{Error, authentication::Credentials, packet::PacketType, version}; use crate::protocol::keyexchange::{APLoginFailed, ErrorCode}; diff --git a/core/src/date.rs b/core/src/date.rs index ddcaf320..e77cf5ef 100644 --- a/core/src/date.rs +++ b/core/src/date.rs @@ -1,8 +1,8 @@ use std::{fmt::Debug, ops::Deref}; use time::{ - error::ComponentRange, format_description::well_known::Iso8601, Date as _Date, OffsetDateTime, - PrimitiveDateTime, Time, + Date as _Date, OffsetDateTime, PrimitiveDateTime, Time, error::ComponentRange, + format_description::well_known::Iso8601, }; use crate::Error; diff --git a/core/src/dealer/manager.rs b/core/src/dealer/manager.rs index ebe5ddc5..98ea0265 100644 --- a/core/src/dealer/manager.rs +++ b/core/src/dealer/manager.rs @@ -7,8 +7,8 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use url::Url; use super::{ - protocol::Message, Builder, Dealer, GetUrlResult, Request, RequestHandler, Responder, Response, - Subscription, + Builder, Dealer, GetUrlResult, Request, RequestHandler, Responder, Response, Subscription, + protocol::Message, }; use crate::{Error, Session}; diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index c5158c84..4f738403 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -6,22 +6,22 @@ use std::{ iter, pin::Pin, sync::{ - atomic::{self, AtomicBool}, Arc, + atomic::{self, AtomicBool}, }, task::Poll, time::Duration, }; use futures_core::{Future, Stream}; -use futures_util::{future::join_all, SinkExt, StreamExt}; +use futures_util::{SinkExt, StreamExt, future::join_all}; use parking_lot::Mutex; use thiserror::Error; use tokio::{ select, sync::{ - mpsc::{self, UnboundedReceiver}, Semaphore, + mpsc::{self, UnboundedReceiver}, }, task::JoinHandle, }; @@ -35,9 +35,8 @@ use self::{ }; use crate::{ - socket, - util::{keep_flushing, CancelOnDrop, TimeoutOnDrop}, - Error, + Error, socket, + util::{CancelOnDrop, TimeoutOnDrop, keep_flushing}, }; type WsMessage = tungstenite::Message; diff --git a/core/src/dealer/protocol.rs b/core/src/dealer/protocol.rs index f8fdc8e9..803fe300 100644 --- a/core/src/dealer/protocol.rs +++ b/core/src/dealer/protocol.rs @@ -5,8 +5,8 @@ pub use request::*; use std::collections::HashMap; use std::io::{Error as IoError, Read}; -use crate::{deserialize_with::json_proto, Error}; -use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; +use crate::{Error, deserialize_with::json_proto}; +use base64::{DecodeError, Engine, prelude::BASE64_STANDARD}; use flate2::read::GzDecoder; use log::LevelFilter; use serde::Deserialize; diff --git a/core/src/deserialize_with.rs b/core/src/deserialize_with.rs index 0e735cbb..22fc24a6 100644 --- a/core/src/deserialize_with.rs +++ b/core/src/deserialize_with.rs @@ -1,5 +1,5 @@ -use base64::prelude::BASE64_STANDARD; use base64::Engine; +use base64::prelude::BASE64_STANDARD; use protobuf::MessageFull; use serde::de::{Error, Unexpected}; use serde::{Deserialize, Deserializer}; diff --git a/core/src/error.rs b/core/src/error.rs index 0ae2e74f..7f299579 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -15,7 +15,7 @@ use http::{ use protobuf::Error as ProtobufError; use thiserror::Error; use tokio::sync::{ - mpsc::error::SendError, oneshot::error::RecvError, AcquireError, TryAcquireError, + AcquireError, TryAcquireError, mpsc::error::SendError, oneshot::error::RecvError, }; use url::ParseError; diff --git a/core/src/file_id.rs b/core/src/file_id.rs index ca23e84d..e513d97d 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, Error}; +use crate::{Error, spotify_id::to_base16}; const RAW_LEN: usize = 20; diff --git a/core/src/http_client.rs b/core/src/http_client.rs index bf461985..a35fa9aa 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -5,17 +5,17 @@ use std::{ }; use bytes::Bytes; -use futures_util::{future::IntoStream, FutureExt}; +use futures_util::{FutureExt, future::IntoStream}; use governor::{ - clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, Quota, RateLimiter, + Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, }; -use http::{header::HeaderValue, Uri}; +use http::{Uri, header::HeaderValue}; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, header::USER_AGENT, HeaderMap, Request, Response, StatusCode}; +use hyper::{HeaderMap, Request, Response, StatusCode, body::Incoming, header::USER_AGENT}; use hyper_proxy2::{Intercept, Proxy, ProxyConnector}; use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use hyper_util::{ - client::legacy::{connect::HttpConnector, Client, ResponseFuture}, + client::legacy::{Client, ResponseFuture, connect::HttpConnector}, rt::TokioExecutor, }; use nonzero_ext::nonzero; @@ -24,10 +24,10 @@ use thiserror::Error; use url::Url; use crate::{ - config::{os_version, OS}, - date::Date, - version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING}, Error, + config::{OS, os_version}, + date::Date, + version::{FALLBACK_USER_AGENT, VERSION_STRING, spotify_version}, }; // The 30 seconds interval is documented by Spotify, but the calls per interval diff --git a/core/src/login5.rs b/core/src/login5.rs index e94978f6..6a2f9bf4 100644 --- a/core/src/login5.rs +++ b/core/src/login5.rs @@ -1,17 +1,17 @@ use crate::config::OS; use crate::spclient::CLIENT_TOKEN; use crate::token::Token; -use crate::{util, Error, SessionConfig}; +use crate::{Error, SessionConfig, util}; use bytes::Bytes; -use http::{header::ACCEPT, HeaderValue, Method, Request}; +use http::{HeaderValue, Method, Request, header::ACCEPT}; use librespot_protocol::login5::login_response::Response; use librespot_protocol::{ client_info::ClientInfo, credentials::{Password, StoredCredential}, hashcash::HashcashSolution, login5::{ - login_request::Login_method, ChallengeSolution, LoginError, LoginOk, LoginRequest, - LoginResponse, + ChallengeSolution, LoginError, LoginOk, LoginRequest, LoginResponse, + login_request::Login_method, }, }; use protobuf::well_known_types::duration::Duration as ProtoDuration; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index db8f1c0a..b32aeb11 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -11,7 +11,7 @@ use futures_util::FutureExt; use protobuf::Message; use tokio::sync::{mpsc, oneshot}; -use crate::{packet::PacketType, protocol, util::SeqGenerator, Error}; +use crate::{Error, packet::PacketType, protocol, util::SeqGenerator}; mod types; pub use self::types::*; diff --git a/core/src/mercury/types.rs b/core/src/mercury/types.rs index 8154d4f5..3cc8d8bc 100644 --- a/core/src/mercury/types.rs +++ b/core/src/mercury/types.rs @@ -4,7 +4,7 @@ use byteorder::{BigEndian, WriteBytesExt}; use protobuf::Message; use thiserror::Error; -use crate::{packet::PacketType, protocol, Error}; +use crate::{Error, packet::PacketType, protocol}; #[derive(Debug, PartialEq, Eq)] pub enum MercuryMethod { diff --git a/core/src/session.rs b/core/src/session.rs index 2d0b5987..91c41781 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -12,6 +12,7 @@ use std::{ use crate::dealer::manager::DealerManager; use crate::{ + Error, apresolve::{ApResolver, SocketAddress}, audio_key::AudioKeyManager, authentication::Credentials, @@ -26,7 +27,6 @@ use crate::{ protocol::keyexchange::ErrorCode, spclient::SpClient, token::TokenProvider, - Error, }; use byteorder::{BigEndian, ByteOrder}; use bytes::Bytes; @@ -40,7 +40,7 @@ use quick_xml::events::Event; use thiserror::Error; use tokio::{ sync::mpsc, - time::{sleep, Duration as TokioDuration, Instant as TokioInstant, Sleep}, + time::{Duration as TokioDuration, Instant as TokioInstant, Sleep, sleep}, }; use tokio_stream::wrappers::UnboundedReceiverStream; use uuid::Uuid; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 22df5006..da0e1bbf 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -3,8 +3,9 @@ use std::{ time::{Duration, Instant}, }; -use crate::config::{os_version, OS}; +use crate::config::{OS, os_version}; use crate::{ + Error, FileId, SpotifyId, apresolve::SocketAddress, config::SessionConfig, error::ErrorKind, @@ -21,15 +22,14 @@ use crate::{ token::Token, util, version::spotify_semantic_version, - Error, FileId, SpotifyId, }; use bytes::Bytes; use data_encoding::HEXUPPER_PERMISSIVE; use futures_util::future::IntoStream; -use http::{header::HeaderValue, Uri}; +use http::{Uri, header::HeaderValue}; use hyper::{ - header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE}, HeaderMap, Method, Request, + header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderName, RANGE}, }; use hyper_util::client::legacy::ResponseFuture; use protobuf::{Enum, Message, MessageFull}; @@ -335,7 +335,7 @@ impl SpClient { Some(unknown) => { return Err(Error::unimplemented(format!( "Unknown client token response type: {unknown:?}" - ))) + ))); } None => return Err(Error::failed_precondition("No client token response type")), } @@ -892,7 +892,9 @@ impl SpClient { pub async fn get_rootlist(&self, from: usize, length: Option) -> SpClientResult { let length = length.unwrap_or(120); let user = self.session().username(); - let endpoint = format!("/playlist/v2/user/{user}/rootlist?decorate=revision,attributes,length,owner,capabilities,status_code&from={from}&length={length}"); + let endpoint = format!( + "/playlist/v2/user/{user}/rootlist?decorate=revision,attributes,length,owner,capabilities,status_code&from={from}&length={length}" + ); self.request(&Method::GET, &endpoint, None, None).await } diff --git a/core/src/util.rs b/core/src/util.rs index c48378af..04a33668 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,7 +1,7 @@ use crate::Error; use byteorder::{BigEndian, ByteOrder}; use futures_core::ready; -use futures_util::{future, FutureExt, Sink, SinkExt}; +use futures_util::{FutureExt, Sink, SinkExt, future}; use hmac::digest::Digest; use sha1::Sha1; use std::time::{Duration, Instant}; @@ -62,11 +62,12 @@ 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)); + let r = ready!( + self.handle + .as_mut() + .expect("Polled after ready") + .poll_unpin(cx) + ); self.handle = None; Poll::Ready(r) } diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 68dfb0b8..fb0c1f63 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -211,7 +211,9 @@ async fn avahi_task( break 'wait_avahi; } } - log::warn!("Failed to connect to Avahi, zeroconf discovery will not work until avahi-daemon is started. Check that it is installed and running"); + log::warn!( + "Failed to connect to Avahi, zeroconf discovery will not work until avahi-daemon is started. Check that it is installed and running" + ); // If it didn't, wait for the signal match stream.next().await { diff --git a/discovery/src/server.rs b/discovery/src/server.rs index 6e4b0135..a328d9d8 100644 --- a/discovery/src/server.rs +++ b/discovery/src/server.rs @@ -6,13 +6,13 @@ use std::{ }; use aes::cipher::{KeyIvInit, StreamCipher}; -use base64::engine::general_purpose::STANDARD as BASE64; use base64::engine::Engine as _; +use base64::engine::general_purpose::STANDARD as BASE64; use bytes::Bytes; use futures_util::{FutureExt, TryFutureExt}; use hmac::{Hmac, Mac}; use http_body_util::{BodyExt, Full}; -use hyper::{body::Incoming, Method, Request, Response, StatusCode}; +use hyper::{Method, Request, Response, StatusCode, body::Incoming}; use hyper_util::{rt::TokioIo, server::graceful::GracefulShutdown}; use log::{debug, error, warn}; @@ -24,7 +24,7 @@ use super::{DiscoveryError, DiscoveryEvent}; use crate::{ core::config::DeviceType, - core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error}, + core::{Error, authentication::Credentials, diffie_hellman::DhLocalKeys}, }; type Aes128Ctr = ctr::Ctr128BE; diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 6663b9c3..9be9364c 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + Metadata, artist::Artists, availability::Availabilities, copyright::Copyrights, @@ -14,14 +15,13 @@ use crate::{ sale_period::SalePeriods, track::Tracks, util::{impl_deref_wrapped, impl_try_from_repeated}, - Metadata, }; -use librespot_core::{date::Date, Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyId, date::Date}; use librespot_protocol as protocol; -pub use protocol::metadata::album::Type as AlbumType; use protocol::metadata::Disc as DiscMessage; +pub use protocol::metadata::album::Type as AlbumType; #[derive(Debug, Clone)] pub struct Album { diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index 6a3a63fc..e875a985 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + Metadata, album::Albums, availability::Availabilities, external_id::ExternalIds, @@ -13,7 +14,6 @@ use crate::{ sale_period::SalePeriods, track::Tracks, util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}, - Metadata, }; use librespot_core::{Error, Session, SpotifyId}; @@ -294,7 +294,7 @@ impl TryFrom<&ActivityPeriodMessage> for ActivityPeriod { _ => { return Err(librespot_core::Error::failed_precondition( "ActivityPeriod is expected to be either a decade or timespan", - )) + )); } }; Ok(activity_period) diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index b2789008..d398c8a0 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -1,6 +1,7 @@ use std::fmt::Debug; use crate::{ + Metadata, artist::ArtistsWithRole, availability::{AudioItemAvailability, Availabilities, UnavailabilityReason}, episode::Episode, @@ -8,13 +9,12 @@ use crate::{ image::{ImageSize, Images}, restriction::Restrictions, track::{Track, Tracks}, - Metadata, }; use super::file::AudioFiles; use librespot_core::{ - date::Date, session::UserData, spotify_id::SpotifyItemType, Error, Session, SpotifyId, + Error, Session, SpotifyId, date::Date, session::UserData, spotify_id::SpotifyItemType, }; pub type AudioItemResult = Result; diff --git a/metadata/src/copyright.rs b/metadata/src/copyright.rs index 5a5ab4db..203647a4 100644 --- a/metadata/src/copyright.rs +++ b/metadata/src/copyright.rs @@ -6,8 +6,8 @@ use std::{ use crate::util::{impl_deref_wrapped, impl_from_repeated}; use librespot_protocol as protocol; -pub use protocol::metadata::copyright::Type as CopyrightType; use protocol::metadata::Copyright as CopyrightMessage; +pub use protocol::metadata::copyright::Type as CopyrightType; #[derive(Debug, Clone)] pub struct Copyright { diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index dcdde70d..4ba0a0da 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + Metadata, audio::file::AudioFiles, availability::Availabilities, content_rating::ContentRatings, @@ -12,10 +13,9 @@ use crate::{ restriction::Restrictions, util::{impl_deref_wrapped, impl_try_from_repeated}, video::VideoFiles, - Metadata, }; -use librespot_core::{date::Date, Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyId, date::Date}; use librespot_protocol as protocol; pub use protocol::metadata::episode::EpisodeType; diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 97f1ccb8..30a1f4ed 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -8,11 +8,11 @@ use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated use librespot_core::{FileId, SpotifyId}; use librespot_protocol as protocol; -pub use protocol::metadata::image::Size as ImageSize; use protocol::metadata::Image as ImageMessage; use protocol::metadata::ImageGroup; -use protocol::playlist4_external::PictureSize as PictureSizeMessage; +pub use protocol::metadata::image::Size as ImageSize; use protocol::playlist_annotate3::TranscodedPicture as TranscodedPictureMessage; +use protocol::playlist4_external::PictureSize as PictureSizeMessage; #[derive(Debug, Clone)] pub struct Image { diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index a21b9804..bd703ee2 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -3,9 +3,9 @@ use std::fmt::Debug; use protobuf::Message; use crate::{ + Metadata, image::TranscodedPictures, request::{MercuryRequest, RequestResult}, - Metadata, }; use librespot_core::{Error, Session, SpotifyId}; diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index 8fa5e4bc..ce03f0de 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -10,7 +10,7 @@ use super::{ permission::Capabilities, }; -use librespot_core::{date::Date, SpotifyId}; +use librespot_core::{SpotifyId, date::Date}; use librespot_protocol as protocol; use protocol::playlist4_external::Item as PlaylistItemMessage; diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 7fa0b311..49ff1188 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -4,9 +4,9 @@ use std::{ }; use crate::{ + Metadata, request::RequestResult, util::{impl_deref_wrapped, impl_from_repeated_copy, impl_try_from_repeated}, - Metadata, }; use super::{ @@ -15,9 +15,9 @@ use super::{ }; use librespot_core::{ + Error, Session, date::Date, spotify_id::{NamedSpotifyId, SpotifyId}, - Error, Session, }; use librespot_protocol as protocol; diff --git a/metadata/src/playlist/operation.rs b/metadata/src/playlist/operation.rs index bcdb54b3..8fd71287 100644 --- a/metadata/src/playlist/operation.rs +++ b/metadata/src/playlist/operation.rs @@ -12,11 +12,11 @@ use crate::{ }; use librespot_protocol as protocol; -pub use protocol::playlist4_external::op::Kind as PlaylistOperationKind; 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 { diff --git a/metadata/src/show.rs b/metadata/src/show.rs index 8f57b18c..b326c652 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -1,8 +1,8 @@ use std::fmt::Debug; use crate::{ - availability::Availabilities, copyright::Copyrights, episode::Episodes, image::Images, - restriction::Restrictions, Metadata, RequestResult, + Metadata, RequestResult, availability::Availabilities, copyright::Copyrights, + episode::Episodes, image::Images, restriction::Restrictions, }; use librespot_core::{Error, Session, SpotifyId}; diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 131cb1b3..78ea5481 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -6,6 +6,7 @@ use std::{ use uuid::Uuid; use crate::{ + Album, Metadata, RequestResult, artist::{Artists, ArtistsWithRole}, audio::file::AudioFiles, availability::Availabilities, @@ -14,10 +15,9 @@ use crate::{ restriction::Restrictions, sale_period::SalePeriods, util::{impl_deref_wrapped, impl_try_from_repeated}, - Album, Metadata, RequestResult, }; -use librespot_core::{date::Date, Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyId, date::Date}; use librespot_protocol as protocol; #[derive(Debug, Clone)] diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index c97b2bf0..1030722b 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -14,8 +14,8 @@ use log::{error, info, trace}; use oauth2::basic::BasicTokenType; use oauth2::{ - basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, EndpointNotSet, - EndpointSet, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, + AuthUrl, AuthorizationCode, ClientId, CsrfToken, EndpointNotSet, EndpointSet, + PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, basic::BasicClient, }; use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; use std::io; diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index afbf3195..0f0fd370 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -262,7 +262,9 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> } } } else { - trace!("The device's min reported Buffer size was greater than or equal to its max reported Buffer size."); + trace!( + "The device's min reported Buffer size was greater than or equal to its max reported Buffer size." + ); ZERO_FRAMES }; @@ -320,8 +322,12 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> } } } else { - trace!("The device's min reported Period size was greater than or equal to its max reported Period size,"); - trace!("or the desired min Period size was greater than or equal to the desired max Period size."); + trace!( + "The device's min reported Period size was greater than or equal to its max reported Period size," + ); + trace!( + "or the desired min Period size was greater than or equal to the desired max Period size." + ); ZERO_FRAMES }; diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 3b82f735..d4260073 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,7 +1,7 @@ use gstreamer::{ + State, event::{FlushStart, FlushStop}, prelude::*, - State, }; use gstreamer as gst; @@ -14,7 +14,7 @@ use std::sync::Arc; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::{ - config::AudioFormat, convert::Converter, decoder::AudioPacket, NUM_CHANNELS, SAMPLE_RATE, + NUM_CHANNELS, SAMPLE_RATE, config::AudioFormat, convert::Converter, decoder::AudioPacket, }; pub struct GstreamerSink { diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 9d40ee82..84b13b6f 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,12 +1,12 @@ use super::{Open, Sink, SinkError, SinkResult}; +use crate::NUM_CHANNELS; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use crate::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; -use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; +use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; pub struct JackSink { send: SyncSender, diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 59690e25..5d8369e5 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -44,7 +44,9 @@ pub struct StdoutSink { impl Open for StdoutSink { fn open(file: Option, format: AudioFormat) -> Self { if let Some("?") = file.as_deref() { - println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n"); + println!( + "\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n" + ); exit(0); } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 29fba7d9..f8b284f2 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -3,7 +3,7 @@ use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; -use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; +use portaudio_rs::device::{DeviceIndex, DeviceInfo, get_default_output_index}; use portaudio_rs::stream::*; use std::process::exit; use std::time::Duration; diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 43d7ec07..90649f94 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -10,7 +10,9 @@ use thiserror::Error; #[derive(Debug, Error)] enum PulseError { - #[error(" Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")] + #[error( + " Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}" + )] InvalidSampleSpec { pulse_format: pulse::sample::Format, format: AudioFormat, diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 0c2cd116..90eed675 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -5,7 +5,7 @@ use crate::decoder::AudioPacket; use shell_words::split; use std::io::{ErrorKind, Write}; -use std::process::{exit, Child, Command, Stdio}; +use std::process::{Child, Command, Stdio, exit}; use thiserror::Error; #[derive(Debug, Error)] @@ -68,7 +68,9 @@ pub struct SubprocessSink { impl Open for SubprocessSink { fn open(shell_command: Option, format: AudioFormat) -> Self { if let Some("?") = shell_command.as_deref() { - println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n"); + println!( + "\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n" + ); exit(0); } diff --git a/playback/src/config.rs b/playback/src/config.rs index b2ece190..a747ce38 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,6 +1,6 @@ use std::{mem, str::FromStr, time::Duration}; -pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +pub use crate::dither::{DithererBuilder, TriangularDitherer, mk_ditherer}; use crate::{convert::i24, player::duration_to_coefficient}; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 59a72163..ec05f711 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -10,8 +10,8 @@ use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; use crate::{ - metadata::audio::{AudioFileFormat, AudioFiles}, MS_PER_PAGE, PAGES_PER_MS, + metadata::audio::{AudioFileFormat, AudioFiles}, }; fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index 4df6882d..eadb7302 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -18,9 +18,9 @@ use symphonia::{ use super::{AudioDecoder, AudioPacket, AudioPacketPosition, DecoderError, DecoderResult}; use crate::{ + NUM_CHANNELS, PAGES_PER_MS, SAMPLE_RATE, metadata::audio::{AudioFileFormat, AudioFiles}, player::NormalisationData, - NUM_CHANNELS, PAGES_PER_MS, SAMPLE_RATE, }; pub struct SymphoniaDecoder { diff --git a/playback/src/dither.rs b/playback/src/dither.rs index db0d83df..55bfa3e5 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -1,5 +1,5 @@ -use rand::rngs::SmallRng; use rand::SeedableRng; +use rand::rngs::SmallRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 33ad64c4..90daaf17 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -3,9 +3,9 @@ use crate::player::{db_to_ratio, ratio_to_db}; use super::mappings::{LogMapping, MappedCtrl, VolumeMapping}; use super::{Mixer, MixerConfig, VolumeCtrl}; +use alsa::Error as AlsaError; use alsa::ctl::{ElemId, ElemIface}; use alsa::mixer::{MilliBel, SelemChannelId, SelemId}; -use alsa::Error as AlsaError; use alsa::{Ctl, Round}; use librespot_core::Error; @@ -106,7 +106,11 @@ impl Mixer for AlsaMixer { 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()); + 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 `--volume-range` if this is incorrect"); diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index c8946a9b..efb7365d 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -3,8 +3,8 @@ use super::{MappedCtrl, VolumeCtrl}; use super::{Mixer, MixerConfig}; use librespot_core::Error; use portable_atomic::AtomicU64; -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::atomic::Ordering; #[derive(Clone)] pub struct SoftMixer { diff --git a/playback/src/player.rs b/playback/src/player.rs index 13790449..0e65f554 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -7,8 +7,8 @@ use std::{ pin::Pin, process::exit, sync::{ - atomic::{AtomicUsize, Ordering}, Arc, + atomic::{AtomicUsize, Ordering}, }, task::{Context, Poll}, thread, @@ -16,8 +16,8 @@ use std::{ }; use futures_util::{ - future, future::FusedFuture, stream::futures_unordered::FuturesUnordered, StreamExt, - TryFutureExt, + StreamExt, TryFutureExt, future, future::FusedFuture, + stream::futures_unordered::FuturesUnordered, }; use parking_lot::Mutex; use symphonia::core::io::MediaSource; @@ -28,7 +28,7 @@ use crate::{ audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, - core::{util::SeqGenerator, Error, Session, SpotifyId}, + core::{Error, Session, SpotifyId, util::SeqGenerator}, decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::VolumeGetter, @@ -1115,7 +1115,9 @@ impl PlayerTrackLoader { // If the position is invalid just start from // the beginning of the track. let position_ms = if position_ms > duration_ms { - warn!("Invalid start position of {position_ms} ms exceeds track's duration of {duration_ms} ms, starting track from the beginning"); + warn!( + "Invalid start position of {position_ms} ms exceeds track's duration of {duration_ms} ms, starting track from the beginning" + ); 0 } else { position_ms @@ -1351,7 +1353,9 @@ impl Future for PlayerInternal { } } Err(e) => { - error!("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, @@ -1364,7 +1368,9 @@ impl Future for PlayerInternal { self.handle_packet(result, normalisation_factor); } Err(e) => { - error!("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, @@ -1814,7 +1820,10 @@ impl PlayerInternal { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, _ => { - return Err(Error::internal(format!("PlayerInternal::handle_command_load repeating the same track: invalid state: {:?}", self.state))); + return Err(Error::internal(format!( + "PlayerInternal::handle_command_load repeating the same track: invalid state: {:?}", + self.state + ))); } }; @@ -1825,7 +1834,10 @@ impl PlayerInternal { self.preload = PlayerPreload::None; self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - 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 Err(Error::internal(format!( + "PlayerInternal::handle_command_load repeating the same track: start_playback() did not transition to valid player state: {:?}", + self.state + ))); } return Ok(()); } @@ -1894,12 +1906,18 @@ impl PlayerInternal { self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - 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 Err(Error::internal(format!( + "PlayerInternal::handle_command_load already playing this track: start_playback() did not transition to valid player state: {:?}", + self.state + ))); } return Ok(()); } else { - return Err(Error::internal(format!("PlayerInternal::handle_command_load already playing this track: invalid state: {:?}", self.state))); + return Err(Error::internal(format!( + "PlayerInternal::handle_command_load already playing this track: invalid state: {:?}", + self.state + ))); } } } @@ -1924,7 +1942,10 @@ impl PlayerInternal { self.start_playback(track_id, play_request_id, *loaded_track, play); return Ok(()); } else { - return Err(Error::internal(format!("PlayerInternal::handle_command_loading preloaded track: invalid state: {:?}", self.state))); + return Err(Error::internal(format!( + "PlayerInternal::handle_command_loading preloaded track: invalid state: {:?}", + self.state + ))); } } } @@ -2180,7 +2201,9 @@ impl PlayerInternal { } = self.state { if is_explicit { - warn!("Currently loaded track is explicit, which client setting forbids -- skipping to next track."); + warn!( + "Currently loaded track is explicit, which client setting forbids -- skipping to next track." + ); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, diff --git a/rustfmt.toml b/rustfmt.toml index 3a26366d..f3e454b6 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1,2 @@ -edition = "2021" +edition = "2024" +style_edition = "2024" From f59766af7e15dabb8a670ee151b4775861146017 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 14 Aug 2025 00:31:59 +0200 Subject: [PATCH 531/561] perf(playback): optimize audio conversion with 16-bit dithering and bit shifts Since Spotify audio is always 16-bit depth, optimize the conversion pipeline: - Always dither at 16-bit level regardless of output format - Preserve fractional precision until final rounding for better requantization - Replace floating-point multiplication with compile-time bit shifts - Add comprehensive inlining to eliminate function call overhead - Specialize 24-bit clamping to remove runtime branching This maintains proper dithering of the original 16-bit quantization artifacts while maximizing performance through bit-shift operations and eliminating unnecessary runtime calculations. --- CHANGELOG.md | 1 + playback/src/convert.rs | 95 ++++++++++++++++++++++++----------------- playback/src/dither.rs | 3 ++ 3 files changed, 61 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 986487b8..82a893a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [metadata] Replaced `AudioFileFormat` with own enum. (breaking) - [playback] Changed trait `Mixer::open` to return `Result` instead of `Self` (breaking) - [playback] Changed type alias `MixerFn` to return `Result, Error>` instead of `Arc` (breaking) +- [playback] Optimize audio conversion to always dither at 16-bit level and use bit shifts for scaling ### Added diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 70bbc6cc..1fbaab39 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -35,81 +35,100 @@ impl Converter { } } - /// 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.; + /// Base bit positions for PCM format scaling. These represent the position + /// of the most significant bit in each format's full-scale representation. + /// For signed integers in two's complement, full scale is 2^(bits-1). + const SHIFT_S16: u8 = 15; // 16-bit: 2^15 = 32768 + const SHIFT_S24: u8 = 23; // 24-bit: 2^23 = 8388608 + const SHIFT_S32: u8 = 31; // 32-bit: 2^31 = 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.; + /// Additional bit shifts needed to scale from 16-bit to higher bit depths. + /// These are the differences between the base shift amounts above. + const SHIFT_16_TO_24: u8 = Self::SHIFT_S24 - Self::SHIFT_S16; // 23 - 15 = 8 + const SHIFT_16_TO_32: u8 = Self::SHIFT_S32 - Self::SHIFT_S16; // 31 - 15 = 16 - pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { - // From the many float to int conversion methods available, match what - // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) + /// Pre-calculated scale factor for 24-bit clamping bounds + const SCALE_S24: f64 = (1_u64 << Self::SHIFT_S24) as f64; - // Casting float to integer rounds towards zero by default, i.e. it - // truncates, and that generates larger error than rounding to nearest. + /// Scale audio samples with optimal dithering strategy for Spotify's 16-bit source material. + /// + /// Since Spotify audio is always 16-bit depth, this function: + /// 1. When dithering: applies noise at 16-bit level, preserves fractional precision, + /// then scales to target format and rounds once at the end + /// 2. When not dithering: scales directly from normalized float to target format + /// + /// The `shift` parameter specifies how many extra bits to shift beyond + /// the base 16-bit scaling (0 for 16-bit, 8 for 24-bit, 16 for 32-bit). + #[inline] + pub fn scale(&mut self, sample: f64, shift: u8) -> f64 { match self.ditherer.as_mut() { - Some(d) => (sample * factor + d.noise()).round(), - None => (sample * factor).round(), + Some(d) => { + // With dithering: Apply noise at 16-bit level to address original quantization, + // then scale up to target format while preserving sub-LSB information + let dithered_16bit = sample * (1_u64 << Self::SHIFT_S16) as f64 + d.noise(); + let scaled = dithered_16bit * (1_u64 << shift) as f64; + scaled.round() + } + None => { + // No dithering: Scale directly from normalized float to target format + // using a single bit shift operation (base 16-bit shift + additional shift) + let total_shift = Self::SHIFT_S16 + shift; + (sample * (1_u64 << total_shift) as f64).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); - + /// Clamping scale specifically for 24-bit output to prevent MSB overflow. + /// Only used for S24 formats where samples are packed in 32-bit words. + /// Ensures the most significant byte is zero to prevent overflow during dithering. + #[inline] + pub fn clamping_scale_s24(&mut self, sample: f64) -> f64 { + let int_value = self.scale(sample, Self::SHIFT_16_TO_24); + // In two's complement, there are more negative than positive values. - let min = -factor; - let max = factor - 1.0; - + let min = -Self::SCALE_S24; + let max = Self::SCALE_S24 - 1.0; + int_value.clamp(min, max) } + #[inline] pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { samples.iter().map(|sample| *sample as f32).collect() } + #[inline] pub fn f64_to_s32(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S32) as i32) + .map(|sample| self.scale(*sample, Self::SHIFT_16_TO_32) as i32) .collect() } - // S24 is 24-bit PCM packed in an upper 32-bit word + /// S24 is 24-bit PCM packed in an upper 32-bit word + #[inline] pub fn f64_to_s24(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.clamping_scale(*sample, Self::SCALE_S24) as i32) + .map(|sample| self.clamping_scale_s24(*sample) as i32) .collect() } - // S24_3 is 24-bit PCM in a 3-byte array + /// S24_3 is 24-bit PCM in a 3-byte array + #[inline] pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)) + .map(|sample| i24::from_s24(self.clamping_scale_s24(*sample) as i32)) .collect() } + #[inline] pub fn f64_to_s16(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| self.scale(*sample, Self::SCALE_S16) as i16) + .map(|sample| self.scale(*sample, 0) as i16) .collect() } } diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 55bfa3e5..d0825587 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -64,6 +64,7 @@ impl Ditherer for TriangularDitherer { Self::NAME } + #[inline] fn noise(&mut self) -> f64 { self.distribution.sample(&mut self.cached_rng) } @@ -98,6 +99,7 @@ impl Ditherer for GaussianDitherer { Self::NAME } + #[inline] fn noise(&mut self) -> f64 { self.distribution.sample(&mut self.cached_rng) } @@ -130,6 +132,7 @@ impl Ditherer for HighPassDitherer { Self::NAME } + #[inline] 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]; From 9f3780885105a7da353377cd456d90fb37d080dc Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 14 Aug 2025 00:34:18 +0200 Subject: [PATCH 532/561] docs: document move from native tls to ring and webpki --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a893a8..1d92482b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] MSRV is now 1.85 with Rust edition 2024 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. - [core] `stream_from_cdn` now accepts the URL as `TryInto` instead of `CdnUrl` (breaking) +- [core] Moved from native TLS to ring and webpki on all platforms - [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) - [connect] Changed `initial_volume` from `Option` to `u16` in `ConnectConfig` (breaking) - [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) From 19f635f90b24a96640ec20a234162ed769a5216b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 14 Aug 2025 00:53:59 +0200 Subject: [PATCH 533/561] perf: inline functions in the audio hot path --- playback/src/audio_backend/alsa.rs | 1 + playback/src/audio_backend/gstreamer.rs | 1 + playback/src/audio_backend/mod.rs | 1 + playback/src/audio_backend/pipe.rs | 1 + playback/src/audio_backend/pulseaudio.rs | 1 + playback/src/audio_backend/subprocess.rs | 1 + playback/src/convert.rs | 9 ++++----- playback/src/decoder/mod.rs | 3 +++ playback/src/decoder/symphonia_decoder.rs | 1 + playback/src/mixer/mod.rs | 1 + playback/src/mixer/softmixer.rs | 1 + playback/src/player.rs | 2 ++ 12 files changed, 18 insertions(+), 5 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 0f0fd370..bd2b4bf5 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -452,6 +452,7 @@ impl Sink for AlsaSink { } impl SinkAsBytes for AlsaSink { + #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { let mut start_index = 0; let data_len = data.len(); diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index d4260073..b9087e3d 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -171,6 +171,7 @@ impl Drop for GstreamerSink { } impl SinkAsBytes for GstreamerSink { + #[inline] 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())); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 739938d3..f8f43e3f 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -46,6 +46,7 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { + #[inline] fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::i24; use zerocopy::IntoBytes; diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 5d8369e5..8dfd21ea 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -96,6 +96,7 @@ impl Sink for StdoutSink { } impl SinkAsBytes for StdoutSink { + #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { self.output .as_deref_mut() diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 90649f94..0cc0850a 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -137,6 +137,7 @@ impl Sink for PulseAudioSink { } impl SinkAsBytes for PulseAudioSink { + #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { let sink = self.sink.as_mut().ok_or(PulseError::NotConnected)?; diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 90eed675..c624718b 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -139,6 +139,7 @@ impl Sink for SubprocessSink { } impl SinkAsBytes for SubprocessSink { + #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { // We get one attempted restart per write. // We don't want to get stuck in a restart loop. diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 1fbaab39..31e710da 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -42,7 +42,6 @@ impl Converter { const SHIFT_S24: u8 = 23; // 24-bit: 2^23 = 8388608 const SHIFT_S32: u8 = 31; // 32-bit: 2^31 = 2147483648 - /// Additional bit shifts needed to scale from 16-bit to higher bit depths. /// These are the differences between the base shift amounts above. const SHIFT_16_TO_24: u8 = Self::SHIFT_S24 - Self::SHIFT_S16; // 23 - 15 = 8 @@ -52,12 +51,12 @@ impl Converter { const SCALE_S24: f64 = (1_u64 << Self::SHIFT_S24) as f64; /// Scale audio samples with optimal dithering strategy for Spotify's 16-bit source material. - /// + /// /// Since Spotify audio is always 16-bit depth, this function: /// 1. When dithering: applies noise at 16-bit level, preserves fractional precision, /// then scales to target format and rounds once at the end /// 2. When not dithering: scales directly from normalized float to target format - /// + /// /// The `shift` parameter specifies how many extra bits to shift beyond /// the base 16-bit scaling (0 for 16-bit, 8 for 24-bit, 16 for 32-bit). #[inline] @@ -85,11 +84,11 @@ impl Converter { #[inline] pub fn clamping_scale_s24(&mut self, sample: f64) -> f64 { let int_value = self.scale(sample, Self::SHIFT_16_TO_24); - + // In two's complement, there are more negative than positive values. let min = -Self::SCALE_S24; let max = Self::SCALE_S24 - 1.0; - + int_value.clamp(min, max) } diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index f980b680..e77d6e9e 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -36,6 +36,7 @@ pub enum AudioPacket { } impl AudioPacket { + #[inline] pub fn samples(&self) -> AudioPacketResult<&[f64]> { match self { AudioPacket::Samples(s) => Ok(s), @@ -43,6 +44,7 @@ impl AudioPacket { } } + #[inline] pub fn raw(&self) -> AudioPacketResult<&[u8]> { match self { AudioPacket::Raw(d) => Ok(d), @@ -50,6 +52,7 @@ impl AudioPacket { } } + #[inline] pub fn is_empty(&self) -> bool { match self { AudioPacket::Samples(s) => s.is_empty(), diff --git a/playback/src/decoder/symphonia_decoder.rs b/playback/src/decoder/symphonia_decoder.rs index eadb7302..63d49d0f 100644 --- a/playback/src/decoder/symphonia_decoder.rs +++ b/playback/src/decoder/symphonia_decoder.rs @@ -131,6 +131,7 @@ impl SymphoniaDecoder { } } + #[inline] fn ts_to_ms(&self, ts: u64) -> u32 { match self.decoder.codec_params().time_base { Some(time_base) => { diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 86252217..89d03235 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -25,6 +25,7 @@ pub trait VolumeGetter { } impl VolumeGetter for NoOpVolume { + #[inline] fn attenuation_factor(&self) -> f64 { 1.0 } diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index efb7365d..189fc151 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -48,6 +48,7 @@ impl SoftMixer { struct SoftVolume(Arc); impl VolumeGetter for SoftVolume { + #[inline] fn attenuation_factor(&self) -> f64 { f64::from_bits(self.0.load(Ordering::Relaxed)) } diff --git a/playback/src/player.rs b/playback/src/player.rs index 0e65f554..bcb6b121 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -279,10 +279,12 @@ impl PlayerEvent { pub type PlayerEventChannel = mpsc::UnboundedReceiver; +#[inline] pub fn db_to_ratio(db: f64) -> f64 { f64::powf(10.0, db / DB_VOLTAGE_RATIO) } +#[inline] pub fn ratio_to_db(ratio: f64) -> f64 { ratio.log10() * DB_VOLTAGE_RATIO } From 9456a02afa3ba1c96470d532ebc6e9b858824a3c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 14 Aug 2025 12:00:48 +0200 Subject: [PATCH 534/561] perf(playback): optimize audio normalization for stereo processing (#1485) - Add pre-computed knee factor to eliminate division in sample loop - Replace if-else chain with match pattern for cleaner branching - Use direct references to reduce repeated array indexing - Maintain existing stereo imaging via channel coupling Addresses review comments from #1485 and incorporates optimizations inspired by Rodio's limiter implementation for improved performance in the stereo case. --- CHANGELOG.md | 3 +- playback/src/player.rs | 195 +++++++++++++++++++---------------------- 2 files changed, 93 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d92482b..c515a0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [metadata] Replaced `AudioFileFormat` with own enum. (breaking) - [playback] Changed trait `Mixer::open` to return `Result` instead of `Self` (breaking) - [playback] Changed type alias `MixerFn` to return `Result, Error>` instead of `Arc` (breaking) -- [playback] Optimize audio conversion to always dither at 16-bit level and use bit shifts for scaling +- [playback] Optimize audio conversion to always dither at 16-bit level, and improve performance +- [playback] Normalizer maintains better stereo imaging, while also being faster ### Added diff --git a/playback/src/player.rs b/playback/src/player.rs index bcb6b121..ba2e5a4c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -78,8 +78,10 @@ struct PlayerInternal { event_senders: Vec>, converter: Converter, - normalisation_integrator: f64, - normalisation_peak: f64, + normalisation_integrators: [f64; 2], + normalisation_peaks: [f64; 2], + normalisation_channel: usize, + normalisation_knee_factor: f64, auto_normalise_as_album: bool, @@ -466,6 +468,7 @@ impl Player { debug!("new Player [{player_id}]"); let converter = Converter::new(config.ditherer); + let normalisation_knee_factor = 1.0 / (8.0 * config.normalisation_knee_db); let internal = PlayerInternal { session, @@ -482,8 +485,10 @@ impl Player { event_senders: vec![], converter, - normalisation_peak: 0.0, - normalisation_integrator: 0.0, + normalisation_peaks: [0.0; 2], + normalisation_integrators: [0.0; 2], + normalisation_channel: 0, + normalisation_knee_factor, auto_normalise_as_album: false, @@ -1574,112 +1579,94 @@ impl PlayerInternal { Some((_, mut packet)) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { - // Get the volume for the packet. - // In the case of hardware volume control this will - // always be 1.0 (no change). + // Get the volume for the packet. In the case of hardware volume control + // this will always be 1.0 (no change). let volume = self.volume_getter.attenuation_factor(); - // 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. - + // 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. + // // No matter the case we apply volume attenuation last if there is any. - if !self.config.normalisation { - if volume < 1.0 { - for sample in data.iter_mut() { - *sample *= volume; - } - } - } else if self.config.normalisation_method == NormalisationMethod::Basic - && (normalisation_factor < 1.0 || volume < 1.0) - { - for sample in data.iter_mut() { - *sample *= normalisation_factor * volume; - } - } 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; - - // 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. 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; - - 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 + match (self.config.normalisation, self.config.normalisation_method) { + (false, _) => { + if volume < 1.0 { + for sample in data.iter_mut() { + *sample *= volume; } - } else { - 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 - { - // 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); } + } + (true, 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; - *sample *= volume; + for sample in data.iter_mut() { + // 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. + + // This implementation assumes audio is stereo. + + // step 0: apply gain stage + *sample *= normalisation_factor; + + // step 1-4: half-wave rectification and conversion into dB, and + // gain computer with soft knee and subtractor + let limiter_db = { + // Add slight DC offset. Some samples are silence, which is + // -inf dB and gets the limiter stuck. Adding a small + // positive offset prevents this. + *sample += f64::MIN_POSITIVE; + + let bias_db = ratio_to_db(sample.abs()) - threshold_db; + let knee_boundary_db = bias_db * 2.0; + if knee_boundary_db < -knee_db { + 0.0 + } else if knee_boundary_db.abs() <= knee_db { + let term = knee_boundary_db + knee_db; + term * term * self.normalisation_knee_factor + } else { + bias_db + } + }; + + // track left/right channel + let channel = self.normalisation_channel; + self.normalisation_channel ^= 1; + + // step 5: smooth, decoupled peak detector for each channel + // Use direct references to reduce repeated array indexing + let integrator = &mut self.normalisation_integrators[channel]; + let peak = &mut self.normalisation_peaks[channel]; + + *integrator = f64::max( + limiter_db, + release_cf * *integrator + (1.0 - release_cf) * limiter_db, + ); + *peak = attack_cf * *peak + (1.0 - attack_cf) * *integrator; + + // steps 6-8: conversion into level and multiplication into gain + // stage. Find maximum peak across both channels to couple the + // gain and maintain stereo imaging. + let max_peak = f64::max( + self.normalisation_peaks[0], + self.normalisation_peaks[1], + ); + *sample *= db_to_ratio(-max_peak) * volume; + } + } + (true, NormalisationMethod::Basic) => { + if normalisation_factor < 1.0 || volume < 1.0 { + for sample in data.iter_mut() { + *sample *= normalisation_factor * volume; + } + } } } } From 03bcdc6bda5f7e2a6c21c3a1576ef00b21ca469c Mon Sep 17 00:00:00 2001 From: waveplate <116709380+waveplate@users.noreply.github.com> Date: Sat, 16 Aug 2025 05:21:41 -0700 Subject: [PATCH 535/561] OAuth: Allow non-loopback addresses (#1514) --- CHANGELOG.md | 1 + oauth/src/lib.rs | 32 ++++++++++++++++---------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c515a0f9..8bef11c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Changed type alias `MixerFn` to return `Result, Error>` instead of `Arc` (breaking) - [playback] Optimize audio conversion to always dither at 16-bit level, and improve performance - [playback] Normalizer maintains better stereo imaging, while also being faster +- [oauth] Remove loopback address requirement from `redirect_uri` when spawning callback handling server versus using stdin. ### Added diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 1030722b..dca85528 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -187,24 +187,17 @@ fn get_authcode_listener( code } -// If the specified `redirect_uri` is HTTP, loopback, and contains a port, +// If the specified `redirect_uri` is HTTP and contains a port, // then the corresponding socket address is returned. fn get_socket_address(redirect_uri: &str) -> Option { - #![warn(missing_docs)] let url = match Url::parse(redirect_uri) { Ok(u) if u.scheme() == "http" && u.port().is_some() => u, _ => return None, }; - let socket_addr = match url.socket_addrs(|| None) { + match url.socket_addrs(|| None) { Ok(mut addrs) => addrs.pop(), _ => None, - }; - if let Some(s) = socket_addr { - if s.ip().is_loopback() { - return socket_addr; - } } - None } /// Struct that handle obtaining and refreshing access tokens. @@ -509,21 +502,21 @@ mod test { assert_eq!(get_socket_address("http://127.0.0.1/foo"), None); assert_eq!(get_socket_address("http://127.0.0.1:/foo"), None); assert_eq!(get_socket_address("http://[::1]/foo"), None); - // Not localhost - assert_eq!(get_socket_address("http://56.0.0.1:1234/foo"), None); - assert_eq!( - get_socket_address("http://[3ffe:2a00:100:7031::1]:1234/foo"), - None - ); // Not http assert_eq!(get_socket_address("https://127.0.0.1/foo"), None); } #[test] - fn get_socket_address_localhost() { + fn get_socket_address_some() { let localhost_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1234); let localhost_v6 = SocketAddr::new(IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1)), 8888); + let addr_v4 = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 1234); + let addr_v6 = SocketAddr::new( + IpAddr::V6(Ipv6Addr::new(0x2001, 0x4860, 0x4860, 0, 0, 0, 0, 0x8888)), + 8888, + ); + // Loopback addresses assert_eq!( get_socket_address("http://127.0.0.1:1234/foo"), Some(localhost_v4) @@ -536,5 +529,12 @@ mod test { get_socket_address("http://[::1]:8888/foo"), Some(localhost_v6) ); + + // Non-loopback addresses + assert_eq!(get_socket_address("http://8.8.8.8:1234/foo"), Some(addr_v4)); + assert_eq!( + get_socket_address("http://[2001:4860:4860::8888]:8888/foo"), + Some(addr_v6) + ); } } From 0a4969ffe24ffede557b6bc79515fb506eab408b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 19 Aug 2025 23:06:28 +0200 Subject: [PATCH 536/561] feat: add configurable TLS backend selection with native-tls as default (#1541) Add support for choosing between native-tls and rustls-tls backends through feature flags, with native-tls as the default for maximum platform compatibility. Key changes: - Add mutually exclusive native-tls and rustls-tls feature flags - Use conditional compilation to select TLS implementation - Configure rustls-tls with platform certificate verifier - Refactor to workspace-based dependency management - Update CI workflows with improved cross-compilation support - Add comprehensive TLS backend documentation The native-tls backend uses system TLS libraries (OpenSSL on Linux, Secure Transport on macOS, SChannel on Windows) while rustls-tls provides a pure Rust implementation with platform certificate stores. --- .devcontainer/Dockerfile | 1 - .devcontainer/Dockerfile.alpine | 1 - .devcontainer/devcontainer.json | 34 ++-- .github/workflows/build.yml | 99 +++++++++ .github/workflows/cross-compile.yml | 78 +++++++ .github/workflows/quality.yml | 79 ++++++++ .github/workflows/test.yml | 281 -------------------------- CHANGELOG.md | 2 +- COMPILING.md | 71 ++++++- Cargo.lock | 212 +++++++++++++++++-- Cargo.toml | 224 +++++++++++++------- Cross.toml | 12 ++ README.md | 10 +- audio/Cargo.toml | 24 ++- audio/src/fetch/mod.rs | 10 +- connect/Cargo.toml | 34 ++-- core/Cargo.toml | 76 ++++--- core/src/http_client.rs | 21 +- discovery/Cargo.toml | 36 ++-- discovery/examples/discovery.rs | 2 +- discovery/examples/discovery_group.rs | 2 +- metadata/Cargo.toml | 31 +-- metadata/src/lib.rs | 2 +- metadata/src/request.rs | 2 +- oauth/Cargo.toml | 28 ++- oauth/examples/oauth_async.rs | 2 +- oauth/examples/oauth_sync.rs | 2 +- oauth/src/lib.rs | 41 +++- playback/Cargo.toml | 76 ++++--- playback/src/mixer/mappings.rs | 2 +- protocol/Cargo.toml | 10 +- 31 files changed, 928 insertions(+), 577 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/cross-compile.yml create mode 100644 .github/workflows/quality.yml delete mode 100644 .github/workflows/test.yml create mode 100644 Cross.toml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d7594a55..9860815f 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -8,7 +8,6 @@ ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" ENV RUST_BACKTRACE=1 ENV RUSTFLAGS="-D warnings" - RUN apt-get update && \ apt-get install -y --no-install-recommends \ git \ diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index bec4bb71..2d949ad6 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -7,7 +7,6 @@ ENV CARGO_REGISTRIES_CRATES_IO_PROTOCOL="sparse" ENV RUST_BACKTRACE=1 ENV RUSTFLAGS="-D warnings -C target-feature=-crt-static" - RUN apk add --no-cache \ git \ nano\ diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c3ab756a..73fa03f4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,24 +1,20 @@ { "name": "Librespot Devcontainer", "dockerFile": "Dockerfile.alpine", - // Use 'postCreateCommand' to run commands after the container is created. - //"postCreateCommand": "", - "customizations": { - // Configure properties specific to VS Code. - "vscode": { - "settings": { - "dev.containers.copyGitConfig": true - }, - "extensions": [ - "eamodio.gitlens", - "github.vscode-github-actions", - "rust-lang.rust-analyzer" - ] - } - }, + "_postCreateCommand_comment": "Uncomment 'postCreateCommand' to run commands after the container is created.", + "_postCreateCommand": "", + "customizations": { + "_comment": "Configure properties specific to VS Code.", + "vscode": { + "settings": { + "dev.containers.copyGitConfig": true + }, + "extensions": ["eamodio.gitlens", "github.vscode-github-actions", "rust-lang.rust-analyzer"] + } + }, "containerEnv": { "GIT_EDITOR": "nano" - } - // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. - // "remoteUser": "root" - } + }, + "_remoteUser_comment": "Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root", + "_remoteUser": "root" +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..dd5e18e7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,99 @@ +--- +# Note, this is used in the badge URL! +name: build + +"on": + push: + branches: [dev, master] + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + - "publish.sh" + - "test.sh" + pull_request: + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + - "publish.sh" + - "test.sh" + schedule: + # Run CI every week + - cron: "00 01 * * 0" + +env: + RUST_BACKTRACE: 1 + RUSTFLAGS: -D warnings + +jobs: + test: + name: cargo +${{ matrix.toolchain }} test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + toolchain: + - "1.85" # MSRV (Minimum supported rust version) + - stable + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install developer package dependencies (Linux) + if: runner.os == 'Linux' + run: > + sudo apt-get update && sudo apt-get install -y + libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev + gstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libavahi-compat-libdnssd-dev + + - name: Fetch dependencies + run: cargo fetch --locked + + - name: Build workspace with examples + run: cargo build --frozen --workspace --examples + + - name: Run tests + run: cargo test --workspace + + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: Check packages without TLS requirements + run: cargo hack check -p librespot-protocol --each-feature + + - name: Check workspace with native-tls + run: > + cargo hack check -p librespot --each-feature --exclude-all-features + --include-features native-tls --exclude-features rustls-tls + + - name: Check workspace with rustls-tls + run: > + cargo hack check -p librespot --each-feature --exclude-all-features + --include-features rustls-tls --exclude-features native-tls + + - name: Build binary with default features + run: cargo build --frozen + + - name: Upload debug artifacts + uses: actions/upload-artifact@v4 + with: + name: librespot-${{ matrix.os }}-${{ matrix.toolchain }} + path: > + target/debug/librespot${{ runner.os == 'Windows' && '.exe' || '' }} + if-no-files-found: error diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml new file mode 100644 index 00000000..f5befd82 --- /dev/null +++ b/.github/workflows/cross-compile.yml @@ -0,0 +1,78 @@ +--- +name: cross-compile + +"on": + push: + branches: [dev, master] + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + pull_request: + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + +env: + RUST_BACKTRACE: 1 + RUSTFLAGS: -D warnings + +jobs: + cross-compile: + name: cross +${{ matrix.toolchain }} build ${{ matrix.platform.target }} + runs-on: ${{ matrix.platform.runs-on }} + continue-on-error: false + strategy: + matrix: + platform: + - arch: armv7 + runs-on: ubuntu-latest + target: armv7-unknown-linux-gnueabihf + + - arch: aarch64 + runs-on: ubuntu-latest + target: aarch64-unknown-linux-gnu + + - arch: riscv64gc + runs-on: ubuntu-latest + target: riscv64gc-unknown-linux-gnu + + toolchain: + - "1.85" # MSRV (Minimum Supported Rust Version) + - stable + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Build binary with default features + if: matrix.platform.target != 'riscv64gc-unknown-linux-gnu' + uses: houseabsolute/actions-rust-cross@v1 + with: + command: build + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: --locked --verbose + + - name: Build binary with rustls-tls and no default features + if: matrix.platform.target == 'riscv64gc-unknown-linux-gnu' + uses: houseabsolute/actions-rust-cross@v1 + with: + command: build + target: ${{ matrix.platform.target }} + toolchain: ${{ matrix.toolchain }} + args: --locked --verbose --no-default-features --features rustls-tls + + - name: Upload debug artifacts + uses: actions/upload-artifact@v4 + with: + name: librespot-${{ matrix.platform.runs-on }}-${{ matrix.platform.arch }}-${{ matrix.toolchain }} # yamllint disable-line rule:line-length + path: target/${{ matrix.platform.target }}/debug/librespot + if-no-files-found: error diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..7b42d163 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,79 @@ +--- +name: code-quality + +"on": + push: + branches: [dev, master] + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + pull_request: + paths-ignore: + - "**.md" + - "docs/**" + - "contrib/**" + - "LICENSE" + - "*.sh" + - "**/Dockerfile*" + schedule: + # Run CI every week + - cron: "00 01 * * 0" + +env: + RUST_BACKTRACE: 1 + RUSTFLAGS: -D warnings + +jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Check formatting + run: cargo fmt --all -- --check + + clippy: + needs: fmt + name: cargo clippy + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@v2 + + - name: Install developer package dependencies + run: > + sudo apt-get update && sudo apt-get install -y + libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev + gstreamer1.0-dev libgstreamer-plugins-base1.0-dev + libavahi-compat-libdnssd-dev + + - name: Install cargo-hack + uses: taiki-e/install-action@cargo-hack + + - name: Run clippy on packages without TLS requirements + run: cargo hack clippy -p librespot-protocol --each-feature + + - name: Run clippy with native-tls + run: > + cargo hack clippy -p librespot --each-feature --exclude-all-features + --include-features native-tls --exclude-features rustls-tls + + - name: Run clippy with rustls-tls + run: > + cargo hack clippy -p librespot --each-feature --exclude-all-features + --include-features rustls-tls --exclude-features native-tls diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 04332316..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,281 +0,0 @@ -# Note, this is used in the badge URL! -name: test - -on: - push: - branches: [dev, master] - paths: - [ - "**.rs", - "Cargo.toml", - "Cargo.lock", - "rustfmt.toml", - ".github/workflows/*", - "!*.md", - "!contrib/*", - "!docs/*", - "!LICENSE", - "!*.sh", - ] - pull_request: - paths: - [ - "**.rs", - "Cargo.toml", - "Cargo.lock", - "rustfmt.toml", - ".github/workflows/*", - "!*.md", - "!contrib/*", - "!docs/*", - "!LICENSE", - "!*.sh", - ] - schedule: - # Run CI every week - - cron: "00 01 * * 0" - -env: - RUST_BACKTRACE: 1 - RUSTFLAGS: -D warnings - -# The layering here is as follows: -# 1. code formatting -# 2. absence of lints -# 3. absence of errors and warnings on Linux/x86 -# 4. cross compilation on Windows and Linux/ARM - -jobs: - fmt: - name: cargo fmt - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v5 - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain stable -y - - run: cargo fmt --all -- --check - - clippy: - needs: fmt - 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@v4 - - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile default --default-toolchain ${{ matrix.toolchain }} -y - - - name: Get Rustc version - id: get-rustc-version - run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache Rust dependencies - uses: actions/cache@v4 - 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 install -y libunwind-dev && 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 - - test-linux: - name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - continue-on-error: ${{ matrix.experimental }} - needs: clippy - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - toolchain: - - "1.85" # MSRV (Minimum supported rust version) - - stable - experimental: [false] - # Ignore failures in beta - include: - - os: ubuntu-latest - toolchain: beta - experimental: true - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - - - name: Get Rustc version - id: get-rustc-version - run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache Rust dependencies - uses: actions/cache@v4 - 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 install -y libunwind-dev && 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 fetch --locked - - run: cargo build --frozen --workspace --examples - - run: cargo test --workspace - - - run: cargo install cargo-hack - - 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: clippy - name: cargo +${{ matrix.toolchain }} check (${{ matrix.os }}) - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: [windows-latest] - toolchain: - - "1.85" # MSRV (Minimum supported rust version) - - stable - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - - - name: Get Rustc version - id: get-rustc-version - run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache Rust dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - target - key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - - - run: cargo fetch --locked - - run: cargo build --frozen --workspace --examples - - run: cargo test --workspace - - - run: cargo install cargo-hack - - run: cargo hack --workspace --remove-dev-deps - - run: cargo check --no-default-features - - run: cargo check - - test-cross-linux: - name: cross +${{ matrix.toolchain }} build ${{ matrix.target }} - needs: clippy - runs-on: ${{ matrix.os }} - continue-on-error: false - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest] - target: - - armv7-unknown-linux-gnueabihf - - aarch64-unknown-linux-gnu - - riscv64gc-unknown-linux-gnu - toolchain: - - "1.85" # MSRV (Minimum supported rust version) - - stable - steps: - - name: Checkout code - uses: actions/checkout@v5 - - - name: Install toolchain - run: curl https://sh.rustup.rs -sSf | sh -s -- --profile minimal --default-toolchain ${{ matrix.toolchain }} -y - - - name: Get Rustc version - id: get-rustc-version - run: echo "version=$(rustc -V)" >> $GITHUB_OUTPUT - shell: bash - - - name: Cache Rust dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - target - key: ${{ runner.os }}-${{ matrix.target }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - - - name: Install the cross compiler rust targets - run: rustup target add ${{ matrix.target }} - - - name: Update and Install dependencies - run: sudo apt-get update && sudo apt-get install -y build-essential cmake libclang1 - - - name: Install bindgen-cli - run: cargo install --force --locked bindgen-cli - - - name: Install cross compiler - run: | - if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then - sudo apt-get install -y gcc-arm-linux-gnueabihf - fi - if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then - sudo apt-get install -y gcc-aarch64-linux-gnu - fi - if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then - sudo apt-get install -y gcc-riscv64-linux-gnu - fi - - - name: Set target link compiler - run: | - # Convert target to uppercase and replace - with _ - target=${{ matrix.target }} - target=${target^^} - target=${target//-/_} - if [ ${{ matrix.target }} = "armv7-unknown-linux-gnueabihf" ]; then - echo "CARGO_TARGET_${target}_LINKER=arm-linux-gnueabihf-gcc" >> $GITHUB_ENV - fi - if [ ${{ matrix.target }} = "aarch64-unknown-linux-gnu" ]; then - echo "CARGO_TARGET_${target}_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV - fi - if [ ${{ matrix.target }} = "riscv64gc-unknown-linux-gnu" ]; then - echo "CARGO_TARGET_${target}_LINKER=riscv64-linux-gnu-gcc" >> $GITHUB_ENV - fi - - - name: Fetch - run: cargo fetch --locked - - name: Build - run: cargo build --frozen --verbose --target ${{ matrix.target }} --no-default-features - - - name: Check binary - run: file target/${{ matrix.target }}/debug/librespot diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bef11c9..5c843571 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] MSRV is now 1.85 with Rust edition 2024 (breaking) - [core] AP connect and handshake have a combined 5 second timeout. - [core] `stream_from_cdn` now accepts the URL as `TryInto` instead of `CdnUrl` (breaking) -- [core] Moved from native TLS to ring and webpki on all platforms +- [core] Add TLS backend selection with native-tls and rustls-tls options, defaulting to native-tls - [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking) - [connect] Changed `initial_volume` from `Option` to `u16` in `ConnectConfig` (breaking) - [connect] Replaced `SpircLoadCommand` with `LoadRequest`, `LoadRequestOptions` and `LoadContextOptions` (breaking) diff --git a/COMPILING.md b/COMPILING.md index 4d238862..8eedca49 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -67,6 +67,63 @@ Depending on the chosen backend, specific development libraries are required. |dns_sd | `libavahi-compat-libdnssd-dev pkg-config` | `avahi-compat-libdns_sd-devel` | | |libmdns (default) | | | | +### TLS library dependencies +librespot requires a TLS implementation for secure connections to Spotify's servers. You can choose between two mutually exclusive options: + +#### native-tls (default) +Uses your system's native TLS implementation: +- **Linux**: OpenSSL +- **macOS**: Secure Transport (Security.framework) +- **Windows**: SChannel (Windows TLS) + +This is the **default choice** and provides the best compatibility. It integrates with your system's certificate store and is well-tested across platforms. + +**When to choose native-tls:** +- You want maximum compatibility +- You're using system-managed certificates +- You're on a standard Linux distribution with OpenSSL +- You're deploying on platforms where OpenSSL is already present + +**Dependencies:** +On Debian/Ubuntu: +```shell +sudo apt-get install libssl-dev pkg-config +``` + +On Fedora: +```shell +sudo dnf install openssl-devel pkg-config +``` + +#### rustls-tls +Uses a Rust-based TLS implementation with `rustls-platform-verifier` for certificate authority (CA) verification: +- **Linux**: Uses system ca-certificates package +- **macOS**: Uses Security.framework for CA verification +- **Windows**: Uses Windows certificate store + +**When to choose rustls-tls:** +- You want to avoid external OpenSSL dependencies +- You're building for reproducible/deterministic builds +- You're targeting platforms where OpenSSL is unavailable or problematic (musl, embedded, static linking) +- You're cross-compiling and want to avoid OpenSSL build complexity +- You prefer having cryptographic operations implemented in Rust + +**No additional system dependencies required** - rustls is implemented in Rust (with some assembly for performance-critical cryptographic operations) and doesn't require external libraries like OpenSSL. + +#### Building with specific TLS backends +```bash +# Default (native-tls) +cargo build + +# Explicitly use native-tls +cargo build --no-default-features --features "native-tls rodio-backend with-libmdns" + +# Use rustls-tls instead +cargo build --no-default-features --features "rustls-tls rodio-backend with-libmdns" +``` + +**Important:** The TLS backends are mutually exclusive. Attempting to enable both will result in a compile-time error. + ### Getting the Source 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. @@ -95,19 +152,21 @@ cargo build --release You will most likely want to build debug builds when developing, as they compile faster, and more verbose, and as the name suggests, are for the purposes of debugging. When submitting a bug report, it is recommended to use a debug build to capture stack traces. -There are also a number of compiler feature flags that you can add, in the event that you want to have certain additional features also compiled. The list of these is available on the [wiki](https://github.com/librespot-org/librespot/wiki/Compiling#addition-features). +There are also a number of compiler feature flags that you can add, in the event that you want to have certain additional features also compiled. All available features and their descriptions are documented in the main [Cargo.toml](Cargo.toml) file. Additional platform-specific information is available on the [wiki](https://github.com/librespot-org/librespot/wiki/Compiling#addition-features). -By default, librespot compiles with the ```rodio-backend``` and ```with-libmdns``` features. To compile without default features, you can run with: +By default, librespot compiles with the ```native-tls```, ```rodio-backend```, and ```with-libmdns``` features. + +**Note:** librespot requires at least one TLS backend to function. Building with `--no-default-features` alone will fail compilation. For custom feature selection, you must specify at least one TLS backend along with your desired audio and discovery backends. +For example, to build with the ALSA audio, libmdns discovery, and native-tls backends: ```bash -cargo build --no-default-features +cargo build --no-default-features --features "native-tls alsa-backend with-libmdns" ``` -Note that this will also disable zeroconf discovery backends for Spotify Connect. For normal use cases, select at least one audio and discovery backend. -For example, to build with the ALSA audio and libmdns discovery backend: +Or to use rustls-tls with ALSA: ```bash -cargo build --no-default-features --features "alsa-backend with-libmdns" +cargo build --no-default-features --features "rustls-tls alsa-backend with-libmdns" ``` ### Running diff --git a/Cargo.lock b/Cargo.lock index cf103a10..1408822f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -167,9 +167,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.88" +version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", @@ -369,6 +369,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -735,6 +745,21 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1289,13 +1314,16 @@ dependencies = [ "http", "hyper", "hyper-rustls 0.26.0", + "hyper-tls", "hyper-util", + "native-tls", "pin-project-lite", + "rustls-native-certs 0.7.3", "tokio", + "tokio-native-tls", "tokio-rustls 0.25.0", "tower-service", "webpki", - "webpki-roots 0.26.11", ] [[package]] @@ -1310,12 +1338,11 @@ dependencies = [ "hyper-util", "log", "rustls 0.22.4", - "rustls-native-certs", + "rustls-native-certs 0.7.3", "rustls-pki-types", "tokio", "tokio-rustls 0.25.0", "tower-service", - "webpki-roots 0.26.11", ] [[package]] @@ -1327,13 +1354,29 @@ dependencies = [ "http", "hyper", "hyper-util", - "log", "rustls 0.23.31", "rustls-pki-types", + "rustls-platform-verifier", "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots 1.0.2", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] @@ -1866,6 +1909,7 @@ dependencies = [ "hyper", "hyper-proxy2", "hyper-rustls 0.27.7", + "hyper-tls", "hyper-util", "librespot-oauth", "librespot-protocol", @@ -1885,7 +1929,6 @@ dependencies = [ "rand 0.9.2", "rand_distr", "rsa", - "rustls 0.23.31", "serde", "serde_json", "sha1", @@ -1970,7 +2013,6 @@ dependencies = [ "alsa", "cpal", "futures-util", - "glib", "gstreamer", "gstreamer-app", "gstreamer-audio", @@ -2109,6 +2151,23 @@ dependencies = [ "serde", ] +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + [[package]] name = "ndk" version = "0.9.0" @@ -2304,9 +2363,9 @@ dependencies = [ [[package]] name = "objc2" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551" +checksum = "561f357ba7f3a2a61563a186a163d0a3a5247e1089524a3981d49adb775078bc" dependencies = [ "objc2-encode", ] @@ -2425,12 +2484,50 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-operations" version = "0.5.0" @@ -2917,15 +3014,16 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", "hyper", "hyper-rustls 0.27.7", + "hyper-tls", "hyper-util", "js-sys", "log", + "native-tls", "percent-encoding", "pin-project-lite", "quinn", @@ -2936,6 +3034,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tower", "tower-http", @@ -2944,7 +3043,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots 1.0.2", + "webpki-roots", ] [[package]] @@ -3050,7 +3149,6 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ - "log", "once_cell", "ring", "rustls-pki-types", @@ -3069,7 +3167,19 @@ dependencies = [ "rustls-pemfile", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", ] [[package]] @@ -3091,6 +3201,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.4", + "security-framework 3.3.0", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.102.8" @@ -3179,7 +3316,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -3548,7 +3698,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.1", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3734,6 +3884,16 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.25.0" @@ -3774,12 +3934,13 @@ checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", + "native-tls", "rustls 0.23.31", "rustls-pki-types", "tokio", + "tokio-native-tls", "tokio-rustls 0.26.2", "tungstenite", - "webpki-roots 0.26.11", ] [[package]] @@ -3922,6 +4083,7 @@ dependencies = [ "http", "httparse", "log", + "native-tls", "rand 0.9.2", "rustls 0.23.31", "rustls-pki-types", @@ -4006,6 +4168,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "vergen" version = "9.0.6" @@ -4198,12 +4366,12 @@ dependencies = [ ] [[package]] -name = "webpki-roots" -version = "0.26.11" +name = "webpki-root-certs" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" dependencies = [ - "webpki-roots 1.0.2", + "rustls-pki-types", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2bdcbb9a..14b72db4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,16 +1,123 @@ [package] name = "librespot" +version.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +description = "An open source client library for Spotify, with support for Spotify Connect" +keywords = ["audio", "spotify", "music", "streaming", "connect"] +categories = ["multimedia::audio"] +repository.workspace = true +readme = "README.md" +edition.workspace = true +include = [ + "src/**/*", + "audio/**/*", + "connect/**/*", + "core/**/*", + "discovery/**/*", + "metadata/**/*", + "oauth/**/*", + "playback/**/*", + "protocol/**/*", + "Cargo.toml", + "README.md", + "LICENSE", + "COMPILING.md", + "CONTRIBUTING.md", +] + +[workspace.package] version = "0.6.0-dev" rust-version = "1.85" authors = ["Librespot Org"] license = "MIT" -description = "An open source client library for Spotify, with support for Spotify Connect" -keywords = ["spotify"] repository = "https://github.com/librespot-org/librespot" -readme = "README.md" edition = "2024" -[workspace] +[features] +default = ["native-tls", "rodio-backend", "with-libmdns"] + +# TLS backends (mutually exclusive - compile-time checks in oauth/src/lib.rs) +# Note: Feature validation is in oauth crate since it's compiled first in the dependency tree. +# See COMPILING.md for more details on TLS backend selection. + +# native-tls: Uses the system's native TLS stack (OpenSSL on Linux, Secure Transport on macOS, +# SChannel on Windows). This is the default as it's well-tested, widely compatible, and integrates +# with system certificate stores. Choose this for maximum compatibility and when you want to use +# system-managed certificates. +native-tls = ["librespot-core/native-tls", "librespot-oauth/native-tls"] + +# rustls-tls: Uses the Rust-based rustls TLS implementation with platform certificate verification. +# This provides a Rust TLS stack (with assembly optimizations) that uses rustls-platform-verifier to +# automatically select the appropriate certificate authority (CA) certificates from your system's +# trust store. Choose this for avoiding external OpenSSL dependencies, reproducible builds, or when +# targeting platforms where native TLS dependencies are unavailable or problematic (musl, embedded, +# static linking). On Linux it uses ca-certificates, on macOS it uses Security.framework, and on +# Windows it uses the Windows certificate store. +rustls-tls = ["librespot-core/rustls-tls", "librespot-oauth/rustls-tls"] + +# Audio backends - see README.md for audio backend selection guide +# Cross-platform backends: + +# rodio-backend: Cross-platform audio backend using Rodio (default). Provides good cross-platform +# compatibility with automatic backend selection. Uses ALSA on Linux, WASAPI on Windows, CoreAudio +# on macOS. +rodio-backend = ["librespot-playback/rodio-backend"] + +# rodiojack-backend: Rodio backend with JACK support for professional audio setups. +rodiojack-backend = ["librespot-playback/rodiojack-backend"] + +# gstreamer-backend: Uses GStreamer multimedia framework for audio output. +# Provides extensive audio processing capabilities. +gstreamer-backend = ["librespot-playback/gstreamer-backend"] + +# portaudio-backend: Cross-platform audio I/O library backend. +portaudio-backend = ["librespot-playback/portaudio-backend"] + +# sdl-backend: Simple DirectMedia Layer audio backend. +sdl-backend = ["librespot-playback/sdl-backend"] + +# Platform-specific backends: + +# alsa-backend: Advanced Linux Sound Architecture backend (Linux only). +# Provides low-latency audio output on Linux systems. +alsa-backend = ["librespot-playback/alsa-backend"] + +# pulseaudio-backend: PulseAudio backend (Linux only). +# Integrates with the PulseAudio sound server for advanced audio routing. +pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] + +# jackaudio-backend: JACK Audio Connection Kit backend. +# Professional audio backend for low-latency, high-quality audio routing. +jackaudio-backend = ["librespot-playback/jackaudio-backend"] + +# Network discovery backends - choose one for Spotify Connect device discovery +# See COMPILING.md for dependencies and platform support. + +# with-libmdns: Pure-Rust mDNS implementation (default). +# No external dependencies, works on all platforms. Choose this for simple deployments or when +# avoiding system dependencies. +with-libmdns = ["librespot-discovery/with-libmdns"] + +# with-avahi: Uses Avahi daemon for mDNS (Linux only). +# Integrates with system's Avahi service for network discovery. Choose this when you want to +# integrate with existing Avahi infrastructure or need advanced mDNS features. Requires +# libavahi-client-dev. +with-avahi = ["librespot-discovery/with-avahi"] + +# with-dns-sd: Uses DNS Service Discovery (cross-platform). +# On macOS uses Bonjour, on Linux uses Avahi compatibility layer. Choose this for tight system +# integration on macOS or when using Avahi's dns-sd compatibility mode on Linux. +with-dns-sd = ["librespot-discovery/with-dns-sd"] + +# Audio processing features: + +# passthrough-decoder: Enables direct passthrough of Ogg Vorbis streams without decoding. +# Useful for custom audio processing pipelines or when you want to handle audio decoding +# externally. When enabled, audio is not decoded by librespot but passed through as raw Ogg Vorbis +# data. +passthrough-decoder = ["librespot-playback/passthrough-decoder"] [lib] name = "librespot" @@ -21,40 +128,26 @@ name = "librespot" path = "src/main.rs" doc = false -[dependencies.librespot-audio] -path = "audio" -version = "0.6.0-dev" - -[dependencies.librespot-connect] -path = "connect" -version = "0.6.0-dev" - -[dependencies.librespot-core] -path = "core" -version = "0.6.0-dev" - -[dependencies.librespot-discovery] -path = "discovery" -version = "0.6.0-dev" -default-features = false - -[dependencies.librespot-metadata] -path = "metadata" -version = "0.6.0-dev" - -[dependencies.librespot-playback] -path = "playback" -version = "0.6.0-dev" - -[dependencies.librespot-protocol] -path = "protocol" -version = "0.6.0-dev" - -[dependencies.librespot-oauth] -path = "oauth" -version = "0.6.0-dev" +[workspace.dependencies] +librespot-audio = { path = "audio", default-features = false } +librespot-connect = { path = "connect", default-features = false } +librespot-core = { path = "core", default-features = false } +librespot-discovery = { path = "discovery", default-features = false } +librespot-metadata = { path = "metadata", default-features = false } +librespot-oauth = { path = "oauth", default-features = false } +librespot-playback = { path = "playback", default-features = false } +librespot-protocol = { path = "protocol", default-features = false } [dependencies] +librespot-audio.workspace = true +librespot-connect.workspace = true +librespot-core.workspace = true +librespot-discovery.workspace = true +librespot-metadata.workspace = true +librespot-oauth.workspace = true +librespot-playback.workspace = true +librespot-protocol.workspace = true + data-encoding = "2.5" env_logger = { version = "0.11.2", default-features = false, features = [ "color", @@ -77,53 +170,30 @@ tokio = { version = "1", features = [ ] } url = "2.2" -[features] -alsa-backend = ["librespot-playback/alsa-backend"] -portaudio-backend = ["librespot-playback/portaudio-backend"] -pulseaudio-backend = ["librespot-playback/pulseaudio-backend"] -jackaudio-backend = ["librespot-playback/jackaudio-backend"] -rodio-backend = ["librespot-playback/rodio-backend"] -rodiojack-backend = ["librespot-playback/rodiojack-backend"] -sdl-backend = ["librespot-playback/sdl-backend"] -gstreamer-backend = ["librespot-playback/gstreamer-backend"] - -with-avahi = ["librespot-discovery/with-avahi"] -with-dns-sd = ["librespot-discovery/with-dns-sd"] -with-libmdns = ["librespot-discovery/with-libmdns"] - -passthrough-decoder = ["librespot-playback/passthrough-decoder"] - -default = ["rodio-backend", "with-libmdns"] - [package.metadata.deb] -maintainer = "librespot-org" -copyright = "2018 Paul Liétar" +maintainer = "Librespot Organization " +copyright = "2015, Paul Liétar" license-file = ["LICENSE", "4"] depends = "$auto" +recommends = "avahi-daemon" extended-description = """\ librespot is an open source client library for Spotify. It enables applications \ -to use Spotify's service, without using the official but closed-source \ -libspotify. Additionally, it will provide extra features which are not \ -available in the official library.""" +to use Spotify's service to control and play music via various backends, and to \ +act as a Spotify Connect receiver. It is an alternative to the official and now \ +deprecated closed-source libspotify. Additionally, it provides extra features \ +which are not available in the official library. +. +This package provides the librespot binary for headless Spotify Connect playback. \ +. +Note: librespot only works with Spotify Premium accounts.""" 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", - ], + # Main binary + ["target/release/librespot", "usr/bin/", "755"], + # Documentation + ["README.md", "usr/share/doc/librespot/", "644"], + # Systemd services + ["contrib/librespot.service", "lib/systemd/system/", "644"], + ["contrib/librespot.user.service", "lib/systemd/user/", "644"], ] - -[workspace.package] -rust-version = "1.85" diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000..879c5b10 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,12 @@ +[build] +pre-build = [ + "dpkg --add-architecture $CROSS_DEB_ARCH", + "apt-get update", + "apt-get --assume-yes install libssl-dev:$CROSS_DEB_ARCH libasound2-dev:$CROSS_DEB_ARCH", +] + +[target.riscv64gc-unknown-linux-gnu] +# RISC-V: Uses rustls-tls (no system dependencies needed) +# Building with --no-default-features --features rustls-tls +# No pre-build steps required - rustls is pure Rust +pre-build = [] diff --git a/README.md b/README.md index 3ccb19cf..caa00ea0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://github.com/librespot-org/librespot/workflows/test/badge.svg)](https://github.com/librespot-org/librespot/actions) +[![Build Status](https://github.com/librespot-org/librespot/workflows/build/badge.svg)](https://github.com/librespot-org/librespot/actions) [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot) @@ -62,13 +62,15 @@ SDL Pipe Subprocess ``` -Please check the corresponding [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for backend specific dependencies. +Please check [COMPILING.md](COMPILING.md) for detailed information on TLS, audio, and discovery backend dependencies, or the [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for additional backend specific dependencies. -Once you've installed the dependencies and cloned this repository you can build *librespot* with the default backend using Cargo. +Once you've installed the dependencies and cloned this repository you can build *librespot* with the default features using Cargo. ```shell cargo build --release ``` +By default, this builds with native-tls (system TLS), rodio audio backend, and libmdns discovery. See [COMPILING.md](COMPILING.md) for information on selecting different TLS, audio, and discovery backends. + # Packages librespot is also available via official package system on various operating systems such as Linux, FreeBSD, NetBSD. [Repology](https://repology.org/project/librespot/versions) offers a good overview. @@ -115,6 +117,6 @@ This is a non exhaustive list of projects that either use or have modified libre - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. -- [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop. +- [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 - [MuPiBox](https://mupibox.de/) - Portable music box for Spotify and local media based on Raspberry Pi. Operated via touchscreen. Suitable for children and older people. diff --git a/audio/Cargo.toml b/audio/Cargo.toml index e1d24352..217cdfcb 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,25 +1,31 @@ [package] name = "librespot-audio" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Lietar "] +license.workspace = true description = "The audio fetching logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true -[dependencies.librespot-core] -path = "../core" -version = "0.6.0-dev" +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["native-tls"] + +# TLS backend propagation +native-tls = ["librespot-core/native-tls"] +rustls-tls = ["librespot-core/rustls-tls"] [dependencies] +librespot-core.workspace = true + aes = "0.8" bytes = "1" ctr = "0.9" futures-util = "0.3" -hyper = "1.6" -hyper-util = { version = "0.1", features = ["client"] } http-body-util = "0.1" +hyper = { version = "1.6", features = ["http1", "http2"] } +hyper-util = { version = "0.1", features = ["client", "http2"] } log = "0.4" parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index cd117731..781e8a32 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -162,7 +162,7 @@ impl StreamLoaderController { } pub fn range_available(&self, range: Range) -> bool { - let available = if let Some(ref shared) = self.stream_shared { + if let Some(ref shared) = self.stream_shared { let download_status = shared.download_status.lock(); range.length @@ -171,9 +171,7 @@ impl StreamLoaderController { .contained_length_from_value(range.start) } else { range.length <= self.len() - range.start - }; - - available + } } pub fn range_to_end_available(&self) -> bool { @@ -397,12 +395,12 @@ impl AudioFile { pub fn get_stream_loader_controller(&self) -> Result { let controller = match self { - AudioFile::Streaming(ref stream) => StreamLoaderController { + AudioFile::Streaming(stream) => StreamLoaderController { channel_tx: Some(stream.stream_loader_command_tx.clone()), stream_shared: Some(stream.shared.clone()), file_size: stream.shared.file_size, }, - AudioFile::Cached(ref file) => StreamLoaderController { + AudioFile::Cached(file) => StreamLoaderController { channel_tx: None, stream_shared: None, file_size: file.metadata()?.len() as usize, diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 216842aa..3da9195b 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,14 +1,26 @@ [package] name = "librespot-connect" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Lietar "] -description = "The discovery and Spotify Connect logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +license.workspace = true +description = "The Spotify Connect logic for librespot" +repository.workspace = true +edition.workspace = true + +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["native-tls"] + +# TLS backend propagation +native-tls = ["librespot-core/native-tls", "librespot-playback/native-tls"] +rustls-tls = ["librespot-core/rustls-tls", "librespot-playback/rustls-tls"] [dependencies] +librespot-core.workspace = true +librespot-playback.workspace = true +librespot-protocol.workspace = true + futures-util = "0.3" log = "0.4" protobuf = "3.7" @@ -18,15 +30,3 @@ thiserror = "2" tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio-stream = "0.1" uuid = { version = "1.18", features = ["v4"] } - -[dependencies.librespot-core] -path = "../core" -version = "0.6.0-dev" - -[dependencies.librespot-playback] -path = "../playback" -version = "0.6.0-dev" - -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.6.0-dev" diff --git a/core/Cargo.toml b/core/Cargo.toml index 8d2159be..161ac5e9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,27 +1,43 @@ [package] name = "librespot-core" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Lietar "] -build = "build.rs" +license.workspace = true description = "The core functionality provided by librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true +build = "build.rs" -[dependencies.librespot-oauth] -path = "../oauth" -version = "0.6.0-dev" +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["native-tls"] -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.6.0-dev" +# TLS backends (mutually exclusive - see oauth/src/lib.rs for compile-time checks) +# Note: Validation is in oauth since it's compiled first in the dependency tree. +native-tls = [ + "dep:hyper-tls", + "hyper-proxy2/tls", + "librespot-oauth/native-tls", + "tokio-tungstenite/native-tls", +] +rustls-tls = [ + "dep:hyper-rustls", + "hyper-proxy2/rustls", + "librespot-oauth/rustls-tls", + "tokio-tungstenite/__rustls-tls", +] [dependencies] +librespot-oauth.workspace = true +librespot-protocol.workspace = true + aes = "0.8" base64 = "0.22" byteorder = "1.5" bytes = "1" +data-encoding = "2.9" +flate2 = "1.1" form_urlencoded = "1.2" futures-core = "0.3" futures-util = { version = "0.3", features = [ @@ -37,9 +53,22 @@ governor = { version = "0.10", default-features = false, features = [ hmac = "0.12" httparse = "1.10" http = "1.3" -hyper = { version = "1.6", features = ["http1", "http2"] } -hyper-util = { version = "0.1", features = ["client"] } http-body-util = "0.1" +hyper = { version = "1.6", features = ["http1", "http2"] } +hyper-proxy2 = { version = "0.1", default-features = false } +hyper-rustls = { version = "0.27", default-features = false, features = [ + "http1", + "http2", + "ring", + "rustls-platform-verifier", + "tls12", +], optional = true } +hyper-tls = { version = "0.6", optional = true } +hyper-util = { version = "0.1", default-features = false, features = [ + "client", + "http1", + "http2", +] } log = "0.4" nonzero_ext = "0.3" num-bigint = "0.4" @@ -51,6 +80,7 @@ pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pin-project-lite = "0.2" priority-queue = "2.5" protobuf = "3.7" +protobuf-json-mapping = "3.7" quick-xml = { version = "0.38", features = ["serialize"] } rand = "0.9" rsa = "0.9" @@ -71,28 +101,10 @@ tokio = { version = "1", features = [ "time", ] } tokio-stream = "0.1" +tokio-tungstenite = { version = "0.27", default-features = false } tokio-util = { version = "0.7", features = ["codec"] } url = "2" uuid = { version = "1", default-features = false, features = ["v4"] } -data-encoding = "2.9" -flate2 = "1.1" -protobuf-json-mapping = "3.7" -rustls = { version = "0.23", default-features = false, features = ["ring"] } - -hyper-proxy2 = { version = "0.1", default-features = false, features = [ - "rustls-webpki", -] } -hyper-rustls = { version = "0.27", default-features = false, features = [ - "ring", - "http1", - "logging", - "tls12", - "webpki-tokio", - "http2", -] } -tokio-tungstenite = { version = "0.27", default-features = false, features = [ - "rustls-tls-webpki-roots", -] } [build-dependencies] rand = "0.9" diff --git a/core/src/http_client.rs b/core/src/http_client.rs index a35fa9aa..27d0da92 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -13,7 +13,6 @@ use http::{Uri, header::HeaderValue}; use http_body_util::{BodyExt, Full}; use hyper::{HeaderMap, Request, Response, StatusCode, body::Incoming, header::USER_AGENT}; use hyper_proxy2::{Intercept, Proxy, ProxyConnector}; -use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; use hyper_util::{ client::legacy::{Client, ResponseFuture, connect::HttpConnector}, rt::TokioExecutor, @@ -23,6 +22,11 @@ use parking_lot::Mutex; use thiserror::Error; use url::Url; +#[cfg(all(feature = "rustls-tls", not(feature = "native-tls")))] +use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; +#[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))] +use hyper_tls::HttpsConnector; + use crate::{ Error, config::{OS, os_version}, @@ -145,14 +149,15 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process - let _ = rustls::crypto::ring::default_provider() - .install_default() - .map_err(|e| { - Error::internal(format!("unable to install default crypto provider: {e:?}")) - }); - let tls = HttpsConnectorBuilder::new().with_webpki_roots(); - let https_connector = tls.https_or_http().enable_http1().enable_http2().build(); + #[cfg(all(feature = "rustls-tls", not(feature = "native-tls")))] + let https_connector = { + let tls = HttpsConnectorBuilder::new().with_platform_verifier(); + tls.https_or_http().enable_http1().enable_http2().build() + }; + + #[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))] + let https_connector = HttpsConnector::new(); // When not using a proxy a dummy proxy is configured that will not intercept any traffic. // This prevents needing to carry the Client Connector generics through the whole project diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 8cfbfa3a..59b1eb32 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,14 +1,29 @@ [package] name = "librespot-discovery" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Lietar "] +license.workspace = true description = "The discovery logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true + +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["with-libmdns", "native-tls"] + +# Discovery backends +with-avahi = ["dep:serde", "dep:zbus"] +with-dns-sd = ["dep:dns-sd"] +with-libmdns = ["dep:libmdns"] + +# TLS backend propagation +native-tls = ["librespot-core/native-tls"] +rustls-tls = ["librespot-core/rustls-tls"] [dependencies] +librespot-core.workspace = true + aes = "0.8" base64 = "0.22" bytes = "1" @@ -18,13 +33,13 @@ form_urlencoded = "1.2" futures-core = "0.3" futures-util = "0.3" hmac = "0.12" +http-body-util = "0.1" hyper = { version = "1.6", features = ["http1"] } hyper-util = { version = "0.1", features = [ "server-auto", "server-graceful", "service", ] } -http-body-util = "0.1" libmdns = { version = "0.9", optional = true } log = "0.4" rand = "0.9" @@ -40,18 +55,7 @@ zbus = { version = "5", default-features = false, features = [ "tokio", ], optional = true } -[dependencies.librespot-core] -path = "../core" -version = "0.6.0-dev" - [dev-dependencies] futures = "0.3" hex = "0.4" tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } - -[features] -with-avahi = ["zbus", "serde"] -with-dns-sd = ["dns-sd"] -with-libmdns = ["libmdns"] - -default = ["with-libmdns"] diff --git a/discovery/examples/discovery.rs b/discovery/examples/discovery.rs index 0b9b5b24..6d41563e 100644 --- a/discovery/examples/discovery.rs +++ b/discovery/examples/discovery.rs @@ -16,6 +16,6 @@ async fn main() { .unwrap(); while let Some(x) = server.next().await { - println!("Received {:?}", x); + println!("Received {x:?}"); } } diff --git a/discovery/examples/discovery_group.rs b/discovery/examples/discovery_group.rs index e98b1536..3022781a 100644 --- a/discovery/examples/discovery_group.rs +++ b/discovery/examples/discovery_group.rs @@ -17,6 +17,6 @@ async fn main() { .unwrap(); while let Some(x) = server.next().await { - println!("Received {:?}", x); + println!("Received {x:?}"); } } diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index bbb2cd0a..27d94724 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,27 +1,30 @@ [package] name = "librespot-metadata" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Lietar "] +license.workspace = true description = "The metadata logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true + +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["native-tls"] + +# TLS backend propagation +native-tls = ["librespot-core/native-tls"] +rustls-tls = ["librespot-core/rustls-tls"] [dependencies] +librespot-core.workspace = true +librespot-protocol.workspace = true + async-trait = "0.1" bytes = "1" log = "0.4" protobuf = "3.7" -thiserror = "2" -uuid = { version = "1", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" - -[dependencies.librespot-core] -path = "../core" -version = "0.6.0-dev" - -[dependencies.librespot-protocol] -path = "../protocol" -version = "0.6.0-dev" +thiserror = "2" +uuid = { version = "1", default-features = false } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index d4cbd4ec..1bb5b9f3 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) } diff --git a/metadata/src/request.rs b/metadata/src/request.rs index fc2c998f..720c3836 100644 --- a/metadata/src/request.rs +++ b/metadata/src/request.rs @@ -21,7 +21,7 @@ pub trait MercuryRequest { let _ = write!(metrics_uri, "&product={product}"); } - trace!("Requesting {}", metrics_uri); + trace!("Requesting {metrics_uri}"); let request = session.mercury().get(metrics_uri)?; let response = request.await?; diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index dd514b00..88e042c9 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -1,23 +1,31 @@ [package] name = "librespot-oauth" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Nick Steel "] +license.workspace = true description = "OAuth authorization code flow with PKCE for obtaining a Spotify access token" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true + +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["native-tls"] + +# TLS backends (mutually exclusive - compile-time checks in src/lib.rs) +native-tls = ["oauth2/native-tls", "reqwest/native-tls"] +rustls-tls = ["oauth2/rustls-tls", "reqwest/rustls-tls"] [dependencies] log = "0.4" -oauth2 = { version = "5.0", features = ["reqwest", "reqwest-blocking"] } -reqwest = { version = "0.12", default-features = false, features = [ - "blocking", - "http2", - "rustls-tls", - "system-proxy", +oauth2 = { version = "5.0", default-features = false, features = [ + "reqwest", + "reqwest-blocking", ] } open = "5.3" +reqwest = { version = "0.12", default-features = false, features = [ + "system-proxy", +] } thiserror = "2" url = "2.5" diff --git a/oauth/examples/oauth_async.rs b/oauth/examples/oauth_async.rs index a8b26799..4d357535 100644 --- a/oauth/examples/oauth_async.rs +++ b/oauth/examples/oauth_async.rs @@ -42,7 +42,7 @@ async fn main() { { Ok(client) => client, Err(err) => { - eprintln!("Unable to build an OAuth client: {}", err); + eprintln!("Unable to build an OAuth client: {err}"); return; } }; diff --git a/oauth/examples/oauth_sync.rs b/oauth/examples/oauth_sync.rs index af773d20..c052faac 100644 --- a/oauth/examples/oauth_sync.rs +++ b/oauth/examples/oauth_sync.rs @@ -41,7 +41,7 @@ fn main() { { Ok(client) => client, Err(err) => { - eprintln!("Unable to build an OAuth client: {}", err); + eprintln!("Unable to build an OAuth client: {err}"); return; } }; diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index dca85528..1bbe1188 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -11,23 +11,42 @@ //! a spawned http server (mimicking Spotify's client), or manually via stdin. The latter //! is appropriate for headless systems. -use log::{error, info, trace}; -use oauth2::basic::BasicTokenType; -use oauth2::{ - AuthUrl, AuthorizationCode, ClientId, CsrfToken, EndpointNotSet, EndpointSet, - PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl, basic::BasicClient, -}; -use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; -use std::io; -use std::sync::mpsc; -use std::time::{Duration, Instant}; use std::{ - io::{BufRead, BufReader, Write}, + io::{self, BufRead, BufReader, Write}, net::{SocketAddr, TcpListener}, + sync::mpsc, + time::{Duration, Instant}, }; + +use oauth2::{ + AuthUrl, AuthorizationCode, ClientId, CsrfToken, EmptyExtraTokenFields, EndpointNotSet, + EndpointSet, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, RefreshToken, Scope, + StandardTokenResponse, TokenResponse, TokenUrl, basic::BasicClient, basic::BasicTokenType, +}; + +use log::{error, info, trace}; use thiserror::Error; use url::Url; +// TLS Feature Validation +// +// These compile-time checks are placed in the oauth crate rather than core for a specific reason: +// oauth is at the bottom of the dependency tree (even librespot-core depends on librespot-oauth), +// which means it gets compiled first. This ensures TLS feature conflicts are detected early in +// the build process, providing immediate feedback to users rather than failing later during +// core compilation. +// +// The dependency chain is: workspace -> core -> oauth +// So oauth's feature validation runs before core's, catching configuration errors quickly. + +#[cfg(all(feature = "native-tls", feature = "rustls-tls"))] +compile_error!("Features 'native-tls' and 'rustls-tls' are mutually exclusive. Enable only one."); + +#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] +compile_error!( + "Either feature \"native-tls\" (default) or \"rustls-tls\" must be enabled for this crate." +); + /// Possible errors encountered during the OAuth authentication flow. #[derive(Debug, Error)] pub enum OAuthError { diff --git a/playback/Cargo.toml b/playback/Cargo.toml index c87d7f9d..6d98fac9 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,26 +1,51 @@ [package] name = "librespot-playback" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Sasha Hilton "] +license.workspace = true description = "The audio playback logic for librespot" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true -[dependencies.librespot-audio] -path = "../audio" -version = "0.6.0-dev" +[features] +# Refer to the workspace Cargo.toml for the list of features +default = ["rodio-backend", "native-tls"] -[dependencies.librespot-core] -path = "../core" -version = "0.6.0-dev" +# Audio backends +alsa-backend = ["dep:alsa"] +gstreamer-backend = [ + "dep:gstreamer", + "dep:gstreamer-app", + "dep:gstreamer-audio", +] +jackaudio-backend = ["dep:jack"] +portaudio-backend = ["dep:portaudio-rs"] +pulseaudio-backend = ["dep:libpulse-binding", "dep:libpulse-simple-binding"] +rodio-backend = ["dep:cpal", "dep:rodio"] +rodiojack-backend = ["dep:rodio", "cpal/jack"] +sdl-backend = ["dep:sdl2"] -[dependencies.librespot-metadata] -path = "../metadata" -version = "0.6.0-dev" +# Audio processing features +passthrough-decoder = ["dep:ogg"] + +# TLS backend propagation +native-tls = [ + "librespot-core/native-tls", + "librespot-audio/native-tls", + "librespot-metadata/native-tls", +] +rustls-tls = [ + "librespot-core/rustls-tls", + "librespot-audio/rustls-tls", + "librespot-metadata/rustls-tls", +] [dependencies] +librespot-audio.workspace = true +librespot-core.workspace = true +librespot-metadata.workspace = true + portable-atomic = "1" futures-util = "0.3" log = "0.4" @@ -37,21 +62,24 @@ zerocopy = { version = "0.8", features = ["derive"] } # Backends alsa = { version = "0.9", 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.13", optional = true } +portaudio-rs = { version = "0.3", optional = true } sdl2 = { version = "0.38", optional = true } + +# GStreamer dependencies gstreamer = { version = "0.24", optional = true } gstreamer-app = { version = "0.24", optional = true } gstreamer-audio = { version = "0.24", optional = true } -glib = { version = "0.21", optional = true } + +# PulseAudio dependencies +libpulse-binding = { version = "2", optional = true, default-features = false } +libpulse-simple-binding = { version = "2", optional = true, default-features = false } # Rodio dependencies +cpal = { version = "0.16", optional = true } rodio = { version = "0.21", optional = true, default-features = false, features = [ "playback", ] } -cpal = { version = "0.16", optional = true } # Container and audio decoder symphonia = { version = "0.5", default-features = false, features = [ @@ -66,15 +94,3 @@ ogg = { version = "0.9", optional = true } # Dithering rand = { version = "0.9", features = ["small_rng"] } rand_distr = "0.5" - -[features] -alsa-backend = ["alsa"] -portaudio-backend = ["portaudio-rs"] -pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] -jackaudio-backend = ["jack"] -rodio-backend = ["rodio", "cpal"] -rodiojack-backend = ["rodio", "cpal/jack"] -sdl-backend = ["sdl2"] -gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio"] - -passthrough-decoder = ["ogg"] diff --git a/playback/src/mixer/mappings.rs b/playback/src/mixer/mappings.rs index bb740997..cb238a21 100644 --- a/playback/src/mixer/mappings.rs +++ b/playback/src/mixer/mappings.rs @@ -81,7 +81,7 @@ impl MappedCtrl for VolumeCtrl { 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, + Self::Cubic(db_range) | Self::Log(db_range) => *db_range = new_db_range, _ => error!("Invalid to set dB range for volume control type {self:?}"), } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index e2143782..8ae4b70a 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,13 +1,13 @@ [package] name = "librespot-protocol" -version = "0.6.0-dev" +version.workspace = true rust-version.workspace = true authors = ["Paul Liétar "] -build = "build.rs" +license.workspace = true description = "The protobuf logic for communicating with Spotify servers" -license = "MIT" -repository = "https://github.com/librespot-org/librespot" -edition = "2021" +repository.workspace = true +edition.workspace = true +build = "build.rs" [dependencies] protobuf = "3.7" From 78ce118d32912adfb2705481f69c83df6a88211f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 20 Aug 2025 11:31:13 +0200 Subject: [PATCH 537/561] fix: rustls-tls features to support certificate stores (#1542) Add separate features for native system roots and Mozilla webpki roots. Update documentation and build configs to reflect new options. --- .github/workflows/build.yml | 8 +++-- .github/workflows/cross-compile.yml | 4 +-- .github/workflows/quality.yml | 14 +++++++-- COMPILING.md | 17 +++++++++-- Cargo.lock | 46 ++++++++--------------------- Cargo.toml | 29 +++++++++++++----- audio/Cargo.toml | 3 +- connect/Cargo.toml | 5 ++-- core/Cargo.toml | 21 +++++++++---- core/src/http_client.rs | 13 ++++---- discovery/Cargo.toml | 3 +- metadata/Cargo.toml | 3 +- oauth/Cargo.toml | 14 ++++++++- oauth/src/lib.rs | 12 ++++---- playback/Cargo.toml | 13 +++++--- 15 files changed, 126 insertions(+), 79 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd5e18e7..88b1b8c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,12 +80,14 @@ jobs: - name: Check workspace with native-tls run: > cargo hack check -p librespot --each-feature --exclude-all-features - --include-features native-tls --exclude-features rustls-tls + --include-features native-tls + --exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots - - name: Check workspace with rustls-tls + - name: Check workspace with rustls-tls-native-roots run: > cargo hack check -p librespot --each-feature --exclude-all-features - --include-features rustls-tls --exclude-features native-tls + --include-features rustls-tls-native-roots + --exclude-features native-tls,rustls-tls-webpki-roots - name: Build binary with default features run: cargo build --frozen diff --git a/.github/workflows/cross-compile.yml b/.github/workflows/cross-compile.yml index f5befd82..79f7f586 100644 --- a/.github/workflows/cross-compile.yml +++ b/.github/workflows/cross-compile.yml @@ -61,14 +61,14 @@ jobs: toolchain: ${{ matrix.toolchain }} args: --locked --verbose - - name: Build binary with rustls-tls and no default features + - name: Build binary without system dependencies if: matrix.platform.target == 'riscv64gc-unknown-linux-gnu' uses: houseabsolute/actions-rust-cross@v1 with: command: build target: ${{ matrix.platform.target }} toolchain: ${{ matrix.toolchain }} - args: --locked --verbose --no-default-features --features rustls-tls + args: --locked --verbose --no-default-features --features rustls-tls-webpki-roots - name: Upload debug artifacts uses: actions/upload-artifact@v4 diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 7b42d163..fffa8a56 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -71,9 +71,17 @@ jobs: - name: Run clippy with native-tls run: > cargo hack clippy -p librespot --each-feature --exclude-all-features - --include-features native-tls --exclude-features rustls-tls + --include-features native-tls + --exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots - - name: Run clippy with rustls-tls + - name: Run clippy with rustls-tls-native-roots run: > cargo hack clippy -p librespot --each-feature --exclude-all-features - --include-features rustls-tls --exclude-features native-tls + --include-features rustls-tls-native-roots + --exclude-features native-tls,rustls-tls-webpki-roots + + - name: Run clippy with rustls-tls-webpki-roots + run: > + cargo hack clippy -p librespot --each-feature --exclude-all-features + --include-features rustls-tls-webpki-roots + --exclude-features native-tls,rustls-tls-native-roots diff --git a/COMPILING.md b/COMPILING.md index 8eedca49..ee698f62 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -96,10 +96,18 @@ sudo dnf install openssl-devel pkg-config ``` #### rustls-tls -Uses a Rust-based TLS implementation with `rustls-platform-verifier` for certificate authority (CA) verification: +Uses a Rust-based TLS implementation with certificate authority (CA) verification. Two certificate store options are available: + +**rustls-tls-native-roots**: - **Linux**: Uses system ca-certificates package - **macOS**: Uses Security.framework for CA verification - **Windows**: Uses Windows certificate store +- Integrates with system certificate management and security updates + +**rustls-tls-webpki-roots**: +- Uses Mozilla's compiled-in certificate store (webpki-roots) +- Certificate trust is independent of host system +- Best for reproducible builds, containers, or embedded systems **When to choose rustls-tls:** - You want to avoid external OpenSSL dependencies @@ -118,8 +126,11 @@ cargo build # Explicitly use native-tls cargo build --no-default-features --features "native-tls rodio-backend with-libmdns" -# Use rustls-tls instead -cargo build --no-default-features --features "rustls-tls rodio-backend with-libmdns" +# Use rustls-tls with native certificate stores +cargo build --no-default-features --features "rustls-tls-native-roots rodio-backend with-libmdns" + +# Use rustls-tls with Mozilla's webpki certificate store +cargo build --no-default-features --features "rustls-tls-webpki-roots rodio-backend with-libmdns" ``` **Important:** The TLS backends are mutually exclusive. Attempting to enable both will result in a compile-time error. diff --git a/Cargo.lock b/Cargo.lock index 1408822f..d616e220 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1324,6 +1324,7 @@ dependencies = [ "tokio-rustls 0.25.0", "tower-service", "webpki", + "webpki-roots 0.26.11", ] [[package]] @@ -1343,6 +1344,7 @@ dependencies = [ "tokio", "tokio-rustls 0.25.0", "tower-service", + "webpki-roots 0.26.11", ] [[package]] @@ -1355,12 +1357,12 @@ dependencies = [ "hyper", "hyper-util", "rustls 0.23.31", + "rustls-native-certs 0.8.1", "rustls-pki-types", - "rustls-platform-verifier", "tokio", "tokio-rustls 0.26.2", "tower-service", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -3028,6 +3030,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.31", + "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", "serde_json", @@ -3043,7 +3046,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 1.0.2", ] [[package]] @@ -3201,33 +3204,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rustls-platform-verifier" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be59af91596cac372a6942530653ad0c3a246cdd491aaa9dcaee47f88d67d5a0" -dependencies = [ - "core-foundation 0.10.1", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls 0.23.31", - "rustls-native-certs 0.8.1", - "rustls-platform-verifier-android", - "rustls-webpki 0.103.4", - "security-framework 3.3.0", - "security-framework-sys", - "webpki-root-certs", - "windows-sys 0.52.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - [[package]] name = "rustls-webpki" version = "0.102.8" @@ -3936,11 +3912,13 @@ dependencies = [ "log", "native-tls", "rustls 0.23.31", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-native-tls", "tokio-rustls 0.26.2", "tungstenite", + "webpki-roots 0.26.11", ] [[package]] @@ -4366,12 +4344,12 @@ dependencies = [ ] [[package]] -name = "webpki-root-certs" -version = "1.0.2" +name = "webpki-roots" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "rustls-pki-types", + "webpki-roots 1.0.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 14b72db4..6810e256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,14 +48,27 @@ default = ["native-tls", "rodio-backend", "with-libmdns"] # system-managed certificates. native-tls = ["librespot-core/native-tls", "librespot-oauth/native-tls"] -# rustls-tls: Uses the Rust-based rustls TLS implementation with platform certificate verification. -# This provides a Rust TLS stack (with assembly optimizations) that uses rustls-platform-verifier to -# automatically select the appropriate certificate authority (CA) certificates from your system's -# trust store. Choose this for avoiding external OpenSSL dependencies, reproducible builds, or when -# targeting platforms where native TLS dependencies are unavailable or problematic (musl, embedded, -# static linking). On Linux it uses ca-certificates, on macOS it uses Security.framework, and on -# Windows it uses the Windows certificate store. -rustls-tls = ["librespot-core/rustls-tls", "librespot-oauth/rustls-tls"] +# rustls-tls: Uses the Rust-based rustls TLS implementation with certificate authority (CA) +# verification. This provides a Rust TLS stack (with assembly optimizations). Choose this for +# avoiding external OpenSSL dependencies, reproducible builds, or when targeting platforms where +# native TLS dependencies are unavailable or problematic (musl, embedded, static linking). +# +# Two certificate store options are available: +# +# - rustls-tls-native-roots: Uses rustls with native system certificate stores (ca-certificates on +# Linux, Security.framework on macOS, Windows certificate store on Windows). Best for most users as +# it integrates with system-managed certificates and gets security updates through the OS. +rustls-tls-native-roots = [ + "librespot-core/rustls-tls-native-roots", + "librespot-oauth/rustls-tls-native-roots", +] +# rustls-tls-webpki-roots: Uses rustls with Mozilla's compiled-in certificate store (webpki-roots). +# Best for reproducible builds, containerized environments, or when you want certificate handling +# to be independent of the host system. +rustls-tls-webpki-roots = [ + "librespot-core/rustls-tls-webpki-roots", + "librespot-oauth/rustls-tls-webpki-roots", +] # Audio backends - see README.md for audio backend selection guide # Cross-platform backends: diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 217cdfcb..9d56ef2d 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -14,7 +14,8 @@ default = ["native-tls"] # TLS backend propagation native-tls = ["librespot-core/native-tls"] -rustls-tls = ["librespot-core/rustls-tls"] +rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] librespot-core.workspace = true diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 3da9195b..b9010d73 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -13,8 +13,9 @@ edition.workspace = true default = ["native-tls"] # TLS backend propagation -native-tls = ["librespot-core/native-tls", "librespot-playback/native-tls"] -rustls-tls = ["librespot-core/rustls-tls", "librespot-playback/rustls-tls"] +native-tls = ["librespot-core/native-tls"] +rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] librespot-core.workspace = true diff --git a/core/Cargo.toml b/core/Cargo.toml index 161ac5e9..729c328b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -21,12 +21,23 @@ native-tls = [ "librespot-oauth/native-tls", "tokio-tungstenite/native-tls", ] -rustls-tls = [ - "dep:hyper-rustls", +rustls-tls-native-roots = [ + "__rustls", "hyper-proxy2/rustls", - "librespot-oauth/rustls-tls", - "tokio-tungstenite/__rustls-tls", + "hyper-rustls/native-tokio", + "librespot-oauth/rustls-tls-native-roots", + "tokio-tungstenite/rustls-tls-native-roots", ] +rustls-tls-webpki-roots = [ + "__rustls", + "hyper-proxy2/rustls-webpki", + "hyper-rustls/webpki-tokio", + "librespot-oauth/rustls-tls-webpki-roots", + "tokio-tungstenite/rustls-tls-webpki-roots", +] + +# Internal features - these are not meant to be used by end users +__rustls = [] [dependencies] librespot-oauth.workspace = true @@ -60,8 +71,6 @@ hyper-rustls = { version = "0.27", default-features = false, features = [ "http1", "http2", "ring", - "rustls-platform-verifier", - "tls12", ], optional = true } hyper-tls = { version = "0.6", optional = true } hyper-util = { version = "0.1", default-features = false, features = [ diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 27d0da92..9d5d54fc 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -22,9 +22,9 @@ use parking_lot::Mutex; use thiserror::Error; use url::Url; -#[cfg(all(feature = "rustls-tls", not(feature = "native-tls")))] +#[cfg(all(feature = "__rustls", not(feature = "native-tls")))] use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; -#[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))] +#[cfg(all(feature = "native-tls", not(feature = "__rustls")))] use hyper_tls::HttpsConnector; use crate::{ @@ -150,13 +150,16 @@ impl HttpClient { fn try_create_hyper_client(proxy_url: Option<&Url>) -> Result { // configuring TLS is expensive and should be done once per process - #[cfg(all(feature = "rustls-tls", not(feature = "native-tls")))] + #[cfg(all(feature = "__rustls", not(feature = "native-tls")))] let https_connector = { - let tls = HttpsConnectorBuilder::new().with_platform_verifier(); + #[cfg(feature = "rustls-tls-native-roots")] + let tls = HttpsConnectorBuilder::new().with_native_roots()?; + #[cfg(feature = "rustls-tls-webpki-roots")] + let tls = HttpsConnectorBuilder::new().with_webpki_roots(); tls.https_or_http().enable_http1().enable_http2().build() }; - #[cfg(all(feature = "native-tls", not(feature = "rustls-tls")))] + #[cfg(all(feature = "native-tls", not(feature = "__rustls")))] let https_connector = HttpsConnector::new(); // When not using a proxy a dummy proxy is configured that will not intercept any traffic. diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 59b1eb32..6dfa6889 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -19,7 +19,8 @@ with-libmdns = ["dep:libmdns"] # TLS backend propagation native-tls = ["librespot-core/native-tls"] -rustls-tls = ["librespot-core/rustls-tls"] +rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] librespot-core.workspace = true diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 27d94724..b6b2321d 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -14,7 +14,8 @@ default = ["native-tls"] # TLS backend propagation native-tls = ["librespot-core/native-tls"] -rustls-tls = ["librespot-core/rustls-tls"] +rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] +rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] librespot-core.workspace = true diff --git a/oauth/Cargo.toml b/oauth/Cargo.toml index 88e042c9..31d58df0 100644 --- a/oauth/Cargo.toml +++ b/oauth/Cargo.toml @@ -14,7 +14,19 @@ default = ["native-tls"] # TLS backends (mutually exclusive - compile-time checks in src/lib.rs) native-tls = ["oauth2/native-tls", "reqwest/native-tls"] -rustls-tls = ["oauth2/rustls-tls", "reqwest/rustls-tls"] +rustls-tls-native-roots = [ + "__rustls", + "oauth2/rustls-tls", + "reqwest/rustls-tls-native-roots", +] +rustls-tls-webpki-roots = [ + "__rustls", + "oauth2/rustls-tls", + "reqwest/rustls-tls-webpki-roots", +] + +# Internal features - these are not meant to be used by end users +__rustls = [] [dependencies] log = "0.4" diff --git a/oauth/src/lib.rs b/oauth/src/lib.rs index 1bbe1188..70e83339 100644 --- a/oauth/src/lib.rs +++ b/oauth/src/lib.rs @@ -39,12 +39,14 @@ use url::Url; // The dependency chain is: workspace -> core -> oauth // So oauth's feature validation runs before core's, catching configuration errors quickly. -#[cfg(all(feature = "native-tls", feature = "rustls-tls"))] -compile_error!("Features 'native-tls' and 'rustls-tls' are mutually exclusive. Enable only one."); - -#[cfg(not(any(feature = "native-tls", feature = "rustls-tls")))] +#[cfg(all(feature = "native-tls", feature = "__rustls"))] compile_error!( - "Either feature \"native-tls\" (default) or \"rustls-tls\" must be enabled for this crate." + "Feature \"native-tls\" is mutually exclusive with \"rustls-tls-native-roots\" and \"rustls-tls-webpki-roots\". Enable only one." +); + +#[cfg(not(any(feature = "native-tls", feature = "__rustls")))] +compile_error!( + "Either feature \"native-tls\" (default), \"rustls-tls-native-roots\" or \"rustls-tls-webpki-roots\" must be enabled for this crate." ); /// Possible errors encountered during the OAuth authentication flow. diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 6d98fac9..7f80d900 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -35,10 +35,15 @@ native-tls = [ "librespot-audio/native-tls", "librespot-metadata/native-tls", ] -rustls-tls = [ - "librespot-core/rustls-tls", - "librespot-audio/rustls-tls", - "librespot-metadata/rustls-tls", +rustls-tls-native-roots = [ + "librespot-core/rustls-tls-native-roots", + "librespot-audio/rustls-tls-native-roots", + "librespot-metadata/rustls-tls-native-roots", +] +rustls-tls-webpki-roots = [ + "librespot-core/rustls-tls-webpki-roots", + "librespot-audio/rustls-tls-webpki-roots", + "librespot-metadata/rustls-tls-webpki-roots", ] [dependencies] From 9d0e39f9c3e9f6bda7a241aadc7a38ad215c38db Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 24 Aug 2025 12:27:53 +0200 Subject: [PATCH 538/561] fix: fires the connect and disconnect again (#1548) --- connect/src/spirc.rs | 8 ++++---- core/src/spclient.rs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 7613e0ef..e3d56c5e 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -898,8 +898,8 @@ impl SpircTask { && cluster.active_device_id != self.session.device_id(); if became_inactive { info!("device became inactive"); - self.connect_state.became_inactive(&self.session).await?; - self.handle_stop() + self.handle_disconnect().await?; + self.handle_stop(); } else if self.connect_state.is_active() { // fixme: workaround fix, because of missing information why it behaves like it does // background: when another device sends a connect-state update, some player's position de-syncs @@ -1102,10 +1102,10 @@ impl SpircTask { ContextAction::Replace, )); + self.handle_activate(); + let timestamp = self.now_ms(); let state = &mut self.connect_state; - - state.set_active(true); state.handle_initial_transfer(&mut transfer); // adjust active context, so resolve knows for which context it should set up the state diff --git a/core/src/spclient.rs b/core/src/spclient.rs index da0e1bbf..ceb10762 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -29,7 +29,7 @@ use futures_util::future::IntoStream; use http::{Uri, header::HeaderValue}; use hyper::{ HeaderMap, Method, Request, - header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderName, RANGE}, + header::{ACCEPT, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HeaderName, RANGE}, }; use hyper_util::client::legacy::ResponseFuture; use protobuf::{Enum, Message, MessageFull}; @@ -482,6 +482,7 @@ impl SpClient { let mut request = Request::builder() .method(method) .uri(url) + .header(CONTENT_LENGTH, body.len()) .body(Bytes::copy_from_slice(body))?; // Reconnection logic: keep getting (cached) tokens because they might have expired. From ba4562c3c0fb02c6fcfd6e1d1a55a7b4f575f7c1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 16:17:37 +0200 Subject: [PATCH 539/561] fix: update version only if version.workspace is not set --- publish.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/publish.sh b/publish.sh index c2187dc5..b8f79d56 100755 --- a/publish.sh +++ b/publish.sh @@ -49,7 +49,7 @@ function updateVersion { fi crate_path="$WORKINGDIR/$CRATE_DIR/Cargo.toml" crate_path=${crate_path//\/\///} - $(replace_in_file "s/^version.*/version = \"$1\"/g" "$crate_path") + $(replace_in_file "s/^version =.*/version = \"$1\"/g" "$crate_path") echo "Path is $crate_path" if [ "$CRATE" = "librespot" ] then From ef3dca6cfb9ef60705bd8fe18d26dfbc4121e036 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 16:19:12 +0200 Subject: [PATCH 540/561] chore: update crates --- Cargo.lock | 166 +++++++++++++++++++++++++++-------------------------- 1 file changed, 84 insertions(+), 82 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d616e220..31d09674 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -229,9 +229,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "block-buffer" @@ -268,9 +268,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.32" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] @@ -293,9 +293,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -585,7 +585,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "objc2", ] @@ -762,9 +762,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -948,7 +948,7 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "futures-channel", "futures-core", "futures-executor", @@ -1040,7 +1040,7 @@ dependencies = [ "paste", "pin-project-lite", "smallvec", - "thiserror 2.0.14", + "thiserror 2.0.16", ] [[package]] @@ -1283,13 +1283,14 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", @@ -1297,6 +1298,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -1525,9 +1527,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -1556,9 +1558,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", "hashbrown", @@ -1575,11 +1577,11 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "libc", ] @@ -1646,7 +1648,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "jack-sys", "lazy_static", "libc", @@ -1787,7 +1789,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "libpulse-sys", "num-derive", @@ -1848,7 +1850,7 @@ dependencies = [ "log", "sha1", "sysinfo", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "url", ] @@ -1868,7 +1870,7 @@ dependencies = [ "log", "parking_lot", "tempfile", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", ] @@ -1884,7 +1886,7 @@ dependencies = [ "protobuf", "rand 0.9.2", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tokio-stream", "uuid", @@ -1936,7 +1938,7 @@ dependencies = [ "sha1", "shannon", "sysinfo", - "thiserror 2.0.14", + "thiserror 2.0.16", "time", "tokio", "tokio-stream", @@ -1973,7 +1975,7 @@ dependencies = [ "serde_json", "serde_repr", "sha1", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "zbus", ] @@ -1990,7 +1992,7 @@ dependencies = [ "protobuf", "serde", "serde_json", - "thiserror 2.0.14", + "thiserror 2.0.16", "uuid", ] @@ -2003,7 +2005,7 @@ dependencies = [ "oauth2", "open", "reqwest", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "url", ] @@ -2035,7 +2037,7 @@ dependencies = [ "sdl2", "shell-words", "symphonia", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "zerocopy", ] @@ -2176,7 +2178,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "jni-sys", "log", "ndk-sys", @@ -2205,7 +2207,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "cfg_aliases", "libc", @@ -2378,7 +2380,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "libc", "objc2", "objc2-core-audio", @@ -2405,7 +2407,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "objc2", ] @@ -2415,7 +2417,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "dispatch2", "objc2", ] @@ -2492,7 +2494,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "cfg-if", "foreign-types", "libc", @@ -2614,9 +2616,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "petgraph" @@ -2749,9 +2751,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.97" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61789d7719defeb74ea5fe81f2fdfdbd28a803847077cecce2ff14e1472f6f1" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -2820,9 +2822,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.38.1" +version = "0.38.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9845d9dccf565065824e69f9f235fafba1587031eda353c1f1561cd6a6be78f4" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" dependencies = [ "memchr", "serde", @@ -2842,7 +2844,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.31", "socket2 0.5.10", - "thiserror 2.0.14", + "thiserror 2.0.16", "tokio", "tracing", "web-time", @@ -2863,7 +2865,7 @@ dependencies = [ "rustls 0.23.31", "rustls-pki-types", "slab", - "thiserror 2.0.14", + "thiserror 2.0.16", "tinyvec", "tracing", "web-time", @@ -2973,14 +2975,14 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] name = "regex" -version = "1.11.1" +version = "1.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" dependencies = [ "aho-corasick", "memchr", @@ -2990,9 +2992,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" dependencies = [ "aho-corasick", "memchr", @@ -3001,9 +3003,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" @@ -3112,7 +3114,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3125,7 +3127,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "errno", "libc", "linux-raw-sys 0.9.4", @@ -3291,7 +3293,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3304,7 +3306,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3343,9 +3345,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.142" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -3624,9 +3626,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.105" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc3fcb250e53458e712715cf74285c1f889686520d79294a9ef3bd7aa1fc619" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -3673,7 +3675,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3709,15 +3711,15 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -3731,11 +3733,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0949c3a6c842cbde3f1686d6eea5a010516deb7085f79db747562d4102f41e" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ - "thiserror-impl 2.0.14", + "thiserror-impl 2.0.16", ] [[package]] @@ -3751,9 +3753,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.14" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b44b4ab9c2fdd0e0512e6bece8388e214c0749f5862b114cc5b7a25daf227" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -3815,9 +3817,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -3989,7 +3991,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", "bytes", "futures-util", "http", @@ -4066,7 +4068,7 @@ dependencies = [ "rustls 0.23.31", "rustls-pki-types", "sha1", - "thiserror 2.0.14", + "thiserror 2.0.16", "utf-8", ] @@ -4107,9 +4109,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", @@ -4391,11 +4393,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4779,9 +4781,9 @@ checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] @@ -4792,7 +4794,7 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.9.3", ] [[package]] From d53458a237d2e5790c61b2ac1f8a6e64b17c8136 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 16:49:07 +0200 Subject: [PATCH 541/561] chore: bump version to 0.7.0 and update internal dependencies --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 2 +- audio/Cargo.toml | 2 +- connect/Cargo.toml | 6 +++--- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 4 ++-- playback/Cargo.toml | 6 +++--- protocol/Cargo.toml | 2 +- 9 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31d09674..72319f01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1833,7 +1833,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "data-encoding", "env_logger", @@ -1857,7 +1857,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "aes", "bytes", @@ -1876,7 +1876,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "futures-util", "librespot-core", @@ -1894,7 +1894,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "aes", "base64", @@ -1951,7 +1951,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "aes", "base64", @@ -1982,7 +1982,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "async-trait", "bytes", @@ -1998,7 +1998,7 @@ dependencies = [ [[package]] name = "librespot-oauth" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "env_logger", "log", @@ -2012,7 +2012,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "alsa", "cpal", @@ -2044,7 +2044,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.6.0-dev" +version = "0.7.0" dependencies = [ "protobuf", "protobuf-codegen", diff --git a/Cargo.toml b/Cargo.toml index 6810e256..d0704359 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,7 @@ include = [ ] [workspace.package] -version = "0.6.0-dev" +version = "0.7.0" rust-version = "1.85" authors = ["Librespot Org"] license = "MIT" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 9d56ef2d..0e0daa34 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -18,7 +18,7 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core.workspace = true +librespot-core = { version = "0.7.0", path = "../core", default-features = false } aes = "0.8" bytes = "1" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index b9010d73..b5b519b0 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -18,9 +18,9 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core.workspace = true -librespot-playback.workspace = true -librespot-protocol.workspace = true +librespot-core = { version = "0.7.0", path = "../core", default-features = false } +librespot-playback = { version = "0.7.0", path = "../playback", default-features = false } +librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } futures-util = "0.3" log = "0.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index 729c328b..8f79fd56 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -40,8 +40,8 @@ rustls-tls-webpki-roots = [ __rustls = [] [dependencies] -librespot-oauth.workspace = true -librespot-protocol.workspace = true +librespot-oauth = { version = "0.7.0", path = "../oauth", default-features = false } +librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } aes = "0.8" base64 = "0.22" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 6dfa6889..2e754e84 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -23,7 +23,7 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core.workspace = true +librespot-core = { version = "0.7.0", path = "../core", default-features = false } aes = "0.8" base64 = "0.22" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index b6b2321d..01b0ec0b 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -18,8 +18,8 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core.workspace = true -librespot-protocol.workspace = true +librespot-core = { version = "0.7.0", path = "../core", default-features = false } +librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } async-trait = "0.1" bytes = "1" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 7f80d900..cb39541d 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -47,9 +47,9 @@ rustls-tls-webpki-roots = [ ] [dependencies] -librespot-audio.workspace = true -librespot-core.workspace = true -librespot-metadata.workspace = true +librespot-audio = { version = "0.7.0", path = "../audio", default-features = false } +librespot-core = { version = "0.7.0", path = "../core", default-features = false } +librespot-metadata = { version = "0.7.0", path = "../metadata", default-features = false } portable-atomic = "1" futures-util = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 8ae4b70a..1d880e4b 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -10,7 +10,7 @@ edition.workspace = true build = "build.rs" [dependencies] -protobuf = "3.7" +protobuf = "3" [build-dependencies] protobuf-codegen = "3" From 09dae0477f73d4d8b373479ce16d83fb2ff03035 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 16:55:12 +0200 Subject: [PATCH 542/561] chore: update changelog for 0.7.0 release date and comparison link --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c843571..57f50f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. 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. -## [0.7.0] - Unreleased +## [0.7.0] - 2025-08-24 ### Changed @@ -404,7 +404,7 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 [unreleased]: https://github.com/librespot-org/librespot/compare/v0.7.0...HEAD -[0.7.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.7.0 +[0.7.0]: https://github.com/librespot-org/librespot/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 [0.4.2]: https://github.com/librespot-org/librespot/compare/v0.4.1...v0.4.2 From b8f728380732c48bf356478aaa640ac67d40e359 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 17:03:30 +0200 Subject: [PATCH 543/561] refactor: remove remoteWait function and related call from publish.sh "cargo publish" uses its own timeout now. --- publish.sh | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/publish.sh b/publish.sh index b8f79d56..94eedf25 100755 --- a/publish.sh +++ b/publish.sh @@ -79,20 +79,6 @@ function get_crate_name { awk -v FS="name = " 'NF>1{print $2; exit}' Cargo.toml } -function remoteWait() { - IFS=: - secs=${1} - crate_name=${2} - while [ $secs -gt 0 ] - do - sleep 1 & - printf "\rSleeping to allow %s to propagate on crates.io servers. Continuing in %2d second(s)." ${crate_name} ${secs} - secs=$(( $secs - 1 )) - wait - done - echo -} - function publishCrates { for CRATE in "${crates[@]}" do @@ -123,7 +109,6 @@ function publishCrates { fi fi echo "Successfully published $crate_name to crates.io" - remoteWait 30 $crate_name done } From dd8005183dfd29d6b7c6cb2dc9219cbdd0d03d8c Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 17:03:45 +0200 Subject: [PATCH 544/561] fix: add version 0.7.0 to workspace dependencies --- Cargo.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d0704359..388bb746 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,14 +142,14 @@ path = "src/main.rs" doc = false [workspace.dependencies] -librespot-audio = { path = "audio", default-features = false } -librespot-connect = { path = "connect", default-features = false } -librespot-core = { path = "core", default-features = false } -librespot-discovery = { path = "discovery", default-features = false } -librespot-metadata = { path = "metadata", default-features = false } -librespot-oauth = { path = "oauth", default-features = false } -librespot-playback = { path = "playback", default-features = false } -librespot-protocol = { path = "protocol", default-features = false } +librespot-audio = { version = "0.7.0", path = "audio", default-features = false } +librespot-connect = { version = "0.7.0", path = "connect", default-features = false } +librespot-core = { version = "0.7.0", path = "core", default-features = false } +librespot-discovery = { version = "0.7.0", path = "discovery", default-features = false } +librespot-metadata = { version = "0.7.0", path = "metadata", default-features = false } +librespot-oauth = { version = "0.7.0", path = "oauth", default-features = false } +librespot-playback = { version = "0.7.0", path = "playback", default-features = false } +librespot-protocol = { version = "0.7.0", path = "protocol", default-features = false } [dependencies] librespot-audio.workspace = true From e024ae65f0132bce5dbffd1693040815f4735efc Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 17:07:30 +0200 Subject: [PATCH 545/561] feat: include examples directory in package --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 388bb746..9ac28ffb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ include = [ "connect/**/*", "core/**/*", "discovery/**/*", + "examples/**/*", "metadata/**/*", "oauth/**/*", "playback/**/*", From b870db2b165fa9c7bc5f679d878a28f0c95abb47 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 17:14:38 +0200 Subject: [PATCH 546/561] chore: update changelog template and Keep a Changelog link --- CHANGELOG.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57f50f9d..1c4521b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,23 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. +## [Unreleased] + +### Added + +### Changed + +### Deprecated + +### Removed + +### Fixed + +### Security + ## [0.7.0] - 2025-08-24 ### Changed From d073cb19970ea2028ae97924f085144428f06072 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Aug 2025 17:14:51 +0200 Subject: [PATCH 547/561] ci: update test.sh to sync clippy and check commands --- test.sh | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/test.sh b/test.sh index d362a227..df0e6d3c 100755 --- a/test.sh +++ b/test.sh @@ -6,7 +6,7 @@ clean() { # some shells will call EXIT after the INT signal # causing EXIT trap to be executed, so we trap EXIT after INT trap '' EXIT - + cargo clean } @@ -14,18 +14,21 @@ trap clean INT QUIT TERM EXIT # this script runs the tests and checks that also run as part of the`test.yml` github action workflow cargo clean + cargo fmt --all -- --check -cargo clippy -p librespot-core --no-default-features -cargo clippy -p librespot-core -cargo hack clippy --each-feature -p librespot-discovery -cargo hack clippy --each-feature -p librespot-playback -cargo hack clippy --each-feature +cargo hack clippy -p librespot-protocol --each-feature -cargo build --workspace --examples +cargo hack clippy -p librespot --each-feature --exclude-all-features --include-features native-tls --exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots +cargo hack clippy -p librespot --each-feature --exclude-all-features --include-features rustls-tls-native-roots --exclude-features native-tls,rustls-tls-webpki-roots +cargo hack clippy -p librespot --each-feature --exclude-all-features --include-features rustls-tls-webpki-roots --exclude-features native-tls,rustls-tls-native-roots + + +cargo fetch --locked +cargo build --frozen --workspace --examples cargo test --workspace -cargo check -p librespot-core --no-default-features -cargo check -p librespot-core -cargo hack check --no-dev-deps --each-feature -p librespot-discovery -cargo hack check --no-dev-deps --each-feature -p librespot-playback -cargo hack check --no-dev-deps --each-feature \ No newline at end of file + +cargo hack check -p librespot-protocol --each-feature +cargo hack check -p librespot --each-feature --exclude-all-features --include-features native-tls --exclude-features rustls-tls-native-roots,rustls-tls-webpki-roots +cargo hack check -p librespot --each-feature --exclude-all-features --include-features rustls-tls-native-roots --exclude-features native-tls,rustls-tls-webpki-roots +run: cargo build --frozen From 1a19d94063284a95301db150f54aa6519e95ddef Mon Sep 17 00:00:00 2001 From: Thang Pham Date: Tue, 26 Aug 2025 18:51:23 -0400 Subject: [PATCH 548/561] disable logging on rodio stream drop (#1557) --- playback/src/audio_backend/rodio.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index a17ea5f3..b6cd3461 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -193,7 +193,7 @@ fn create_sink( AudioFormat::S16 => cpal::SampleFormat::I16, }; - let stream = match rodio::OutputStreamBuilder::default() + let mut stream = match rodio::OutputStreamBuilder::default() .with_device(cpal_device.clone()) .with_config(&config.config()) .with_sample_format(sample_format) @@ -206,6 +206,9 @@ fn create_sink( } }; + // disable logging on stream drop + stream.log_on_drop(false); + let sink = rodio::Sink::connect_new(stream.mixer()); Ok((sink, stream)) } From b6931e3de557ceb8023ddee63484fb8433ef2812 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Wed, 27 Aug 2025 00:53:12 +0200 Subject: [PATCH 549/561] Fix: Fixup how the headers are set in `spclient` to prevent deleting headers (#1552) --- CHANGELOG.md | 2 ++ core/src/spclient.rs | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1c4521b8..e0dd8713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- [core] Fixed a problem where in `spclient` where a http 411 error was thrown because the header were set wrong + ### Security ## [0.7.0] - 2025-08-24 diff --git a/core/src/spclient.rs b/core/src/spclient.rs index ceb10762..7d3f39e9 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -489,9 +489,12 @@ impl SpClient { let token = self.session().login5().auth_token().await?; let headers_mut = request.headers_mut(); - if let Some(ref hdrs) = headers { - *headers_mut = hdrs.clone(); + if let Some(ref headers) = headers { + for (name, value) in headers { + headers_mut.insert(name, value.clone()); + } } + headers_mut.insert( AUTHORIZATION, HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, From c715885747a95ec22f9502825cb0d2d8833c3779 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Wed, 27 Aug 2025 11:18:02 +0200 Subject: [PATCH 550/561] Fix: Delete the connect state only on spirc shutdown (#1555) * fix: only delete the state on shutdown * chore: update changelog --- CHANGELOG.md | 1 + connect/src/spirc.rs | 11 +++++------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0dd8713..27bb7de8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- [connect] Only deletes the connect state on dealer shutdown instead on disconnecting - [core] Fixed a problem where in `spclient` where a http 411 error was thrown because the header were set wrong ### Security diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index e3d56c5e..6bb9685b 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -552,6 +552,11 @@ impl SpircTask { } } + // this should clear the active session id, leaving an empty state + if let Err(why) = self.session.spclient().delete_connect_state_request().await { + error!("error during connect state deletion: {why}") + }; + self.session.dealer().close().await; } @@ -1163,12 +1168,6 @@ impl SpircTask { self.connect_state.became_inactive(&self.session).await?; - // this should clear the active session id, leaving an empty state - self.session - .spclient() - .delete_connect_state_request() - .await?; - self.player .emit_session_disconnected_event(self.session.connection_id(), self.session.username()); From 882ed7cf4fb1bced7109a60de998332bd86824a6 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Fri, 29 Aug 2025 23:51:40 +0200 Subject: [PATCH 551/561] Fix: Use config defaults instead of type defaults (#1556) * fix: use the config instead of the type default * chore: update changelog * fix: repair build for pulseaudio-backend --- CHANGELOG.md | 2 ++ src/main.rs | 81 ++++++++++++++++++++++++++-------------------------- 2 files changed, 43 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27bb7de8..6a35e9c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [connect] Only deletes the connect state on dealer shutdown instead on disconnecting - [core] Fixed a problem where in `spclient` where a http 411 error was thrown because the header were set wrong +- [main] Use the config instead of the type default for values that are not provided by the user + ### Security diff --git a/src/main.rs b/src/main.rs index 0c6c3956..b824ea94 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1383,9 +1383,8 @@ async fn get_setup() -> Setup { let connect_config = { let connect_default_config = ConnectConfig::default(); - let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); - - if name.is_empty() { + let name = opt_str(NAME); + if matches!(name, Some(ref name) if name.is_empty()) { empty_string_error_msg(NAME, NAME_SHORT); exit(1); } @@ -1393,11 +1392,13 @@ async fn get_setup() -> Setup { #[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() - }; + let op_pulseaudio_name = name + .as_ref() + .map(|name| format!("{} - {}", connect_default_config.name, name)); + + let pulseaudio_name = op_pulseaudio_name + .as_deref() + .unwrap_or(&connect_default_config.name); set_env_var("PULSE_PROP_application.name", pulseaudio_name).await; } @@ -1467,51 +1468,51 @@ async fn get_setup() -> Setup { } else { cache.as_ref().and_then(Cache::volume) } - }) - .unwrap_or_default(); + }); - let device_type = opt_str(DEVICE_TYPE) - .as_deref() - .map(|device_type| { - DeviceType::from_str(device_type).unwrap_or_else(|_| { - invalid_error_msg( - DEVICE_TYPE, - DEVICE_TYPE_SHORT, - device_type, - "computer, tablet, smartphone, \ + let device_type = opt_str(DEVICE_TYPE).as_deref().map(|device_type| { + DeviceType::from_str(device_type).unwrap_or_else(|_| { + invalid_error_msg( + DEVICE_TYPE, + DEVICE_TYPE_SHORT, + device_type, + "computer, tablet, smartphone, \ speaker, tv, avr, stb, audiodongle, \ gameconsole, castaudio, castvideo, \ automobile, smartwatch, chromebook, \ carthing", - DeviceType::default().into(), - ); + DeviceType::default().into(), + ); - exit(1); - }) + exit(1); }) - .unwrap_or_default(); + }); - let volume_steps = opt_str(VOLUME_STEPS) - .map(|steps| match steps.parse::() { - Ok(value) => value, - _ => { - let default_value = &connect_default_config.volume_steps.to_string(); + let volume_steps = opt_str(VOLUME_STEPS).map(|steps| match steps.parse::() { + Ok(value) => value, + _ => { + let default_value = &connect_default_config.volume_steps.to_string(); - invalid_error_msg( - VOLUME_STEPS, - VOLUME_STEPS_SHORT, - &steps, - "a positive whole number <= 65535", - default_value, - ); + invalid_error_msg( + VOLUME_STEPS, + VOLUME_STEPS_SHORT, + &steps, + "a positive whole number <= 65535", + default_value, + ); - exit(1); - } - }) - .unwrap_or_else(|| connect_default_config.volume_steps); + exit(1); + } + }); let is_group = opt_present(DEVICE_IS_GROUP); + // use config defaults if not provided + let name = name.unwrap_or(connect_default_config.name); + let device_type = device_type.unwrap_or(connect_default_config.device_type); + let initial_volume = initial_volume.unwrap_or(connect_default_config.initial_volume); + let volume_steps = volume_steps.unwrap_or(connect_default_config.volume_steps); + ConnectConfig { name, device_type, From eff5ca3294246a0863a23d7fb71c865498e66378 Mon Sep 17 00:00:00 2001 From: Felix Prillwitz Date: Sun, 31 Aug 2025 20:32:01 +0200 Subject: [PATCH 552/561] Adjust: Allow repeat in combination with shuffle (#1561) * fix: incorrect autoplay resolver behavior when shuffling * refactor: store the initial track in the remote context * adjust: shuffle repeat interaction * chore: update .gitignore * chore: rename internal error * adjust: shuffle behavior to ensure consistency * fix: prefer repeat context over autoplay * chore: update changelog * chore: reduce complexity of shuffle * chore: test shuffle with first --- .gitignore | 1 + CHANGELOG.md | 5 +- connect/src/context_resolver.rs | 4 +- connect/src/shuffle_vec.rs | 120 ++++++++++++++++++++++++++---- connect/src/spirc.rs | 2 +- connect/src/state.rs | 9 ++- connect/src/state/context.rs | 21 ++++-- connect/src/state/handle.rs | 21 +++--- connect/src/state/metadata.rs | 2 + connect/src/state/options.rs | 42 ++++++++--- connect/src/state/restrictions.rs | 3 - connect/src/state/tracks.rs | 10 ++- connect/src/state/transfer.rs | 18 ++++- 13 files changed, 193 insertions(+), 65 deletions(-) diff --git a/.gitignore b/.gitignore index c9a8b06b..aa13aaf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ target .cargo spotify_appkey.key +.idea/ .vagrant/ .project .history diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a35e9c6..215a5486 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,14 +11,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- [connect] Shuffling was adjusted, so that shuffle and repeat can be used combined + ### Deprecated ### Removed ### Fixed +- [connect] Repeat context will not go into autoplay anymore and triggering autoplay while shuffling shouldn't reshuffle anymore - [connect] Only deletes the connect state on dealer shutdown instead on disconnecting -- [core] Fixed a problem where in `spclient` where a http 411 error was thrown because the header were set wrong +- [core] Fixed a problem where in `spclient` where an http 411 error was thrown because the header were set wrong - [main] Use the config instead of the type default for values that are not provided by the user diff --git a/connect/src/context_resolver.rs b/connect/src/context_resolver.rs index 29336f0a..1bc0c163 100644 --- a/connect/src/context_resolver.rs +++ b/connect/src/context_resolver.rs @@ -318,8 +318,8 @@ impl ContextResolver { let active_ctx = state.get_context(state.active_context); let res = if let Some(transfer_state) = transfer_state.take() { state.finish_transfer(transfer_state) - } else if state.shuffling_context() { - state.shuffle(None) + } else if state.shuffling_context() && next.update == ContextType::Default { + state.shuffle_new() } else if matches!(active_ctx, Ok(ctx) if ctx.index.track == 0) { // has context, and context is not touched // when the index is not zero, the next index was already evaluated elsewhere diff --git a/connect/src/shuffle_vec.rs b/connect/src/shuffle_vec.rs index ca73eb77..595a392b 100644 --- a/connect/src/shuffle_vec.rs +++ b/connect/src/shuffle_vec.rs @@ -8,6 +8,12 @@ use std::{ pub struct ShuffleVec { vec: Vec, indices: Option>, + /// This is primarily necessary to ensure that shuffle does not behave out of place. + /// + /// For that reason we swap the first track with the currently playing track. By that we ensure + /// that the shuffle state is consistent between resets of the state because the first track is + /// always the track with which we started playing when switching to shuffle. + original_first_position: Option, } impl PartialEq for ShuffleVec { @@ -41,16 +47,25 @@ impl IntoIterator for ShuffleVec { impl From> for ShuffleVec { fn from(vec: Vec) -> Self { - Self { vec, indices: None } + Self { + vec, + original_first_position: None, + indices: None, + } } } impl ShuffleVec { - pub fn shuffle_with_seed(&mut self, seed: u64) { - self.shuffle_with_rng(SmallRng::seed_from_u64(seed)) + pub fn shuffle_with_seed bool>(&mut self, seed: u64, is_first: F) { + self.shuffle_with_rng(SmallRng::seed_from_u64(seed), is_first) } - pub fn shuffle_with_rng(&mut self, mut rng: impl Rng) { + pub fn shuffle_with_rng bool>(&mut self, mut rng: impl Rng, is_first: F) { + if self.vec.len() <= 1 { + info!("skipped shuffling for less or equal one item"); + return; + } + if self.indices.is_some() { self.unshuffle() } @@ -66,7 +81,12 @@ impl ShuffleVec { self.vec.swap(i, rnd_ind); } - self.indices = Some(indices) + self.indices = Some(indices); + + self.original_first_position = self.vec.iter().position(is_first); + if let Some(first_pos) = self.original_first_position { + self.vec.swap(0, first_pos) + } } pub fn unshuffle(&mut self) { @@ -75,9 +95,16 @@ impl ShuffleVec { None => return, }; + if let Some(first_pos) = self.original_first_position { + self.vec.swap(0, first_pos); + self.original_first_position = None; + } + for i in 1..self.vec.len() { - let n = indices[self.vec.len() - i - 1]; - self.vec.swap(n, i); + match indices.get(self.vec.len() - i - 1) { + None => return, + Some(n) => self.vec.swap(*n, i), + } } } } @@ -86,25 +113,86 @@ impl ShuffleVec { mod test { use super::*; use rand::Rng; + use std::ops::Range; + + fn base(range: Range) -> (ShuffleVec, u64) { + let seed = rand::rng().random_range(0..10_000_000_000_000); + + let vec = range.collect::>(); + (vec.into(), seed) + } #[test] - fn test_shuffle_with_seed() { - let seed = rand::rng().random_range(0..10000000000000); - - let vec = (0..100).collect::>(); - let base_vec: ShuffleVec = vec.into(); + fn test_shuffle_without_first() { + let (base_vec, seed) = base(0..100); let mut shuffled_vec = base_vec.clone(); - shuffled_vec.shuffle_with_seed(seed); + shuffled_vec.shuffle_with_seed(seed, |_| false); let mut different_shuffled_vec = base_vec.clone(); - different_shuffled_vec.shuffle_with_seed(seed); + different_shuffled_vec.shuffle_with_seed(seed, |_| false); - assert_eq!(shuffled_vec, different_shuffled_vec); + assert_eq!( + shuffled_vec, different_shuffled_vec, + "shuffling with the same seed has the same result" + ); let mut unshuffled_vec = shuffled_vec.clone(); unshuffled_vec.unshuffle(); - assert_eq!(base_vec, unshuffled_vec); + assert_eq!( + base_vec, unshuffled_vec, + "unshuffle restores the original state" + ); + } + + #[test] + fn test_shuffle_with_first() { + const MAX_RANGE: usize = 200; + + let (base_vec, seed) = base(0..MAX_RANGE); + let rand_first = rand::rng().random_range(0..MAX_RANGE); + + let mut shuffled_with_first = base_vec.clone(); + shuffled_with_first.shuffle_with_seed(seed, |i| i == &rand_first); + + assert_eq!( + Some(&rand_first), + shuffled_with_first.first(), + "after shuffling the first is expected to be the given item" + ); + + let mut shuffled_without_first = base_vec.clone(); + shuffled_without_first.shuffle_with_seed(seed, |_| false); + + let mut switched_positions = Vec::with_capacity(2); + for (i, without_first_value) in shuffled_without_first.iter().enumerate() { + if without_first_value != &shuffled_with_first[i] { + switched_positions.push(i); + } else { + assert_eq!( + without_first_value, &shuffled_with_first[i], + "shuffling with the same seed has the same result" + ); + } + } + + assert_eq!( + switched_positions.len(), + 2, + "only the switched positions should be different" + ); + + assert_eq!( + shuffled_with_first[switched_positions[0]], + shuffled_without_first[switched_positions[1]], + "the switched values should be equal" + ); + + assert_eq!( + shuffled_with_first[switched_positions[1]], + shuffled_without_first[switched_positions[0]], + "the switched values should be equal" + ) } } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 6bb9685b..087384e9 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1287,7 +1287,7 @@ impl SpircTask { if self.context_resolver.has_next() { self.connect_state.update_queue_revision() } else { - self.connect_state.shuffle(None)?; + self.connect_state.shuffle_new()?; self.add_autoplay_resolving_when_required(); } } else { diff --git a/connect/src/state.rs b/connect/src/state.rs index 9e73d861..a84de234 100644 --- a/connect/src/state.rs +++ b/connect/src/state.rs @@ -23,6 +23,7 @@ use crate::{ }, state::{ context::{ContextType, ResetContext, StateContext}, + options::ShuffleState, provider::{IsProvider, Provider}, }, }; @@ -55,7 +56,7 @@ pub(super) enum StateError { #[error("the provided context has no tracks")] ContextHasNoTracks, #[error("playback of local files is not supported")] - UnsupportedLocalPlayBack, + UnsupportedLocalPlayback, #[error("track uri <{0:?}> contains invalid characters")] InvalidTrackUri(Option), } @@ -69,7 +70,7 @@ impl From for Error { | CanNotFindTrackInContext(_, _) | ContextHasNoTracks | InvalidTrackUri(_) => Error::failed_precondition(err), - CurrentlyDisallowed { .. } | UnsupportedLocalPlayBack => Error::unavailable(err), + CurrentlyDisallowed { .. } | UnsupportedLocalPlayback => Error::unavailable(err), } } } @@ -123,7 +124,7 @@ pub(super) struct ConnectState { /// the context from which we play, is used to top up prev and next tracks context: Option, /// seed extracted in [ConnectState::handle_initial_transfer] and used in [ConnectState::finish_transfer] - transfer_shuffle_seed: Option, + transfer_shuffle: Option, /// a context to keep track of the autoplay context autoplay_context: Option, @@ -395,7 +396,7 @@ impl ConnectState { self.update_context_index(self.active_context, new_index + 1)?; self.fill_up_context = self.active_context; - if !self.current_track(|t| t.is_queue()) { + if !self.current_track(|t| t.is_queue() || self.is_skip_track(t, None)) { self.set_current_track(new_index)?; } diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index eb04fe5f..7f0fc640 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -24,7 +24,6 @@ const SEARCH_IDENTIFIER: &str = "spotify:search"; #[derive(Debug)] pub struct StateContext { pub tracks: ShuffleVec, - pub skip_track: Option, pub metadata: HashMap, pub restrictions: Option, /// is used to keep track which tracks are already loaded into the next_tracks @@ -108,6 +107,7 @@ impl ConnectState { if let Ok(ctx) = self.get_context_mut(ContextType::Default) { ctx.remove_shuffle_seed(); + ctx.remove_initial_track(); ctx.tracks.unshuffle() } @@ -194,7 +194,7 @@ impl ConnectState { error!("context didn't have any tracks: {context:#?}"); Err(StateError::ContextHasNoTracks)?; } else if matches!(context.uri, Some(ref uri) if uri.starts_with(LOCAL_FILES_IDENTIFIER)) { - Err(StateError::UnsupportedLocalPlayBack)?; + Err(StateError::UnsupportedLocalPlayback)?; } let mut next_contexts = Vec::new(); @@ -377,18 +377,23 @@ impl ConnectState { StateContext { tracks: tracks.into(), - skip_track: None, restrictions, metadata, index: ContextIndex::new(), } } - pub fn is_skip_track(&self, track: &ProvidedTrack) -> bool { - self.get_context(self.active_context) - .ok() - .and_then(|t| t.skip_track.as_ref().map(|t| t.uri == track.uri)) - .unwrap_or(false) + pub fn is_skip_track(&self, track: &ProvidedTrack, iteration: Option) -> bool { + let ctx = match self.get_context(self.active_context).ok() { + None => return false, + Some(ctx) => ctx, + }; + + if ctx.get_initial_track().is_none_or(|uri| uri != &track.uri) { + return false; + } + + iteration.is_none_or(|i| i == 0) } pub fn merge_context(&mut self, new_page: Option) -> Option<()> { diff --git a/connect/src/state/handle.rs b/connect/src/state/handle.rs index acf8bdcb..e031410a 100644 --- a/connect/src/state/handle.rs +++ b/connect/src/state/handle.rs @@ -13,7 +13,7 @@ impl ConnectState { self.set_shuffle(shuffle); if shuffle { - return self.shuffle(None); + return self.shuffle_new(); } self.reset_context(ResetContext::DefaultIndex); @@ -44,17 +44,14 @@ impl ConnectState { self.set_repeat_context(repeat); if repeat { - self.set_shuffle(false); - self.reset_context(ResetContext::DefaultIndex); - - let ctx = self.get_context(ContextType::Default)?; - let current_track = ConnectState::find_index_in_context(ctx, |t| { - self.current_track(|t| &t.uri) == &t.uri - })?; - self.reset_playback_to_position(Some(current_track)) - } else { - self.update_restrictions(); - Ok(()) + if let ContextType::Autoplay = self.fill_up_context { + self.fill_up_context = ContextType::Default; + } } + + let ctx = self.get_context(ContextType::Default)?; + let current_track = + ConnectState::find_index_in_context(ctx, |t| self.current_track(|t| &t.uri) == &t.uri)?; + self.reset_playback_to_position(Some(current_track)) } } diff --git a/connect/src/state/metadata.rs b/connect/src/state/metadata.rs index 8212c276..82318dab 100644 --- a/connect/src/state/metadata.rs +++ b/connect/src/state/metadata.rs @@ -14,6 +14,7 @@ const ITERATION: &str = "iteration"; const CUSTOM_CONTEXT_INDEX: &str = "context_index"; const CUSTOM_SHUFFLE_SEED: &str = "shuffle_seed"; +const CUSTOM_INITIAL_TRACK: &str = "initial_track"; macro_rules! metadata_entry { ( $get:ident, $set:ident, $clear:ident ($key:ident: $entry:ident)) => { @@ -63,6 +64,7 @@ pub(super) trait Metadata { metadata_entry!(get_entity_uri, set_entity_uri, remove_entity_uri (entity_uri: ENTITY_URI)); metadata_entry!(get_iteration, set_iteration, remove_iteration (iteration: ITERATION)); metadata_entry!(get_shuffle_seed, set_shuffle_seed, remove_shuffle_seed (shuffle_seed: CUSTOM_SHUFFLE_SEED)); + metadata_entry!(get_initial_track, set_initial_track, remove_initial_track (initial_track: CUSTOM_INITIAL_TRACK)); } macro_rules! impl_metadata { diff --git a/connect/src/state/options.rs b/connect/src/state/options.rs index 97288b7e..efb83764 100644 --- a/connect/src/state/options.rs +++ b/connect/src/state/options.rs @@ -10,6 +10,12 @@ use crate::{ use protobuf::MessageField; use rand::Rng; +#[derive(Default, Debug)] +pub(crate) struct ShuffleState { + pub seed: u64, + pub initial_track: String, +} + impl ConnectState { fn add_options_if_empty(&mut self) { if self.player().options.is_none() { @@ -44,7 +50,7 @@ impl ConnectState { self.set_repeat_context(false); } - pub fn shuffle(&mut self, seed: Option) -> Result<(), Error> { + fn validate_shuffle_allowed(&self) -> Result<(), Error> { if let Some(reason) = self .player() .restrictions @@ -55,27 +61,39 @@ impl ConnectState { action: "shuffle", reason: reason.clone(), })? + } else { + Ok(()) } + } + pub fn shuffle_restore(&mut self, shuffle_state: ShuffleState) -> Result<(), Error> { + self.validate_shuffle_allowed()?; + + self.shuffle(shuffle_state.seed, &shuffle_state.initial_track) + } + + pub fn shuffle_new(&mut self) -> Result<(), Error> { + self.validate_shuffle_allowed()?; + + let new_seed = rand::rng().random_range(100_000_000_000..1_000_000_000_000); + let current_track = self.current_track(|t| t.uri.clone()); + + self.shuffle(new_seed, ¤t_track) + } + + fn shuffle(&mut self, seed: u64, initial_track: &str) -> Result<(), Error> { self.clear_prev_track(); self.clear_next_tracks(); - let current_track = self.current_track(|t| t.clone().take()); - self.reset_context(ResetContext::DefaultIndex); + let ctx = self.get_context_mut(ContextType::Default)?; + ctx.tracks + .shuffle_with_seed(seed, |f| f.uri == initial_track); - // we don't need to include the current track, because it is already being played - ctx.skip_track = current_track; - - let seed = - seed.unwrap_or_else(|| rand::rng().random_range(100_000_000_000..1_000_000_000_000)); - - ctx.tracks.shuffle_with_seed(seed); + ctx.set_initial_track(initial_track); ctx.set_shuffle_seed(seed); - self.set_active_context(ContextType::Default); - self.fill_up_context = ContextType::Default; self.fill_up_next_tracks()?; Ok(()) diff --git a/connect/src/state/restrictions.rs b/connect/src/state/restrictions.rs index e1f78094..29d8d475 100644 --- a/connect/src/state/restrictions.rs +++ b/connect/src/state/restrictions.rs @@ -14,7 +14,6 @@ impl ConnectState { pub fn update_restrictions(&mut self) { const NO_PREV: &str = "no previous tracks"; const AUTOPLAY: &str = "autoplay"; - const ENDLESS_CONTEXT: &str = "endless_context"; let prev_tracks_is_empty = self.prev_tracks().is_empty(); @@ -51,8 +50,6 @@ impl ConnectState { restrictions.disallow_toggling_shuffle_reasons = vec![AUTOPLAY.to_string()]; restrictions.disallow_toggling_repeat_context_reasons = vec![AUTOPLAY.to_string()]; restrictions.disallow_toggling_repeat_track_reasons = vec![AUTOPLAY.to_string()]; - } else if player.options.repeating_context { - restrictions.disallow_toggling_shuffle_reasons = vec![ENDLESS_CONTEXT.to_string()] } else { restrictions.disallow_toggling_shuffle_reasons.clear(); restrictions diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index afaf7c00..94bde92b 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -124,7 +124,6 @@ impl<'ct> ConnectState { continue; } Some(next) if next.is_unavailable() => continue, - Some(next) if self.is_skip_track(&next) => continue, other => break other, }; }; @@ -297,7 +296,8 @@ impl<'ct> ConnectState { delimiter } None if !matches!(self.fill_up_context, ContextType::Autoplay) - && self.autoplay_context.is_some() => + && self.autoplay_context.is_some() + && !self.repeat_context() => { self.update_context_index(self.fill_up_context, new_index)?; @@ -322,7 +322,11 @@ impl<'ct> ConnectState { } } None => break, - Some(ct) if ct.is_unavailable() || self.is_skip_track(ct) => { + Some(ct) if ct.is_unavailable() || self.is_skip_track(ct, Some(iteration)) => { + debug!( + "skipped track {} during fillup as it's unavailable or should be skipped", + ct.uri + ); new_index += 1; continue; } diff --git a/connect/src/state/transfer.rs b/connect/src/state/transfer.rs index 1e2f40cf..8fdb21fa 100644 --- a/connect/src/state/transfer.rs +++ b/connect/src/state/transfer.rs @@ -4,6 +4,7 @@ use crate::{ state::{ context::ContextType, metadata::Metadata, + options::ShuffleState, provider::{IsProvider, Provider}, {ConnectState, StateError}, }, @@ -54,6 +55,7 @@ impl ConnectState { } let mut shuffle_seed = None; + let mut initial_track = None; if let Some(session) = transfer.current_session.as_mut() { player.play_origin = session.play_origin.take().map(Into::into).into(); player.suppressions = session.suppressions.take().map(Into::into).into(); @@ -72,6 +74,8 @@ impl ConnectState { .get_shuffle_seed() .and_then(|seed| seed.parse().ok()); + initial_track = session.context.get_initial_track().cloned(); + if let Some(mut ctx) = session.context.take() { player.restrictions = ctx.restrictions.take().map(Into::into).into(); for (key, value) in ctx.metadata { @@ -89,7 +93,13 @@ impl ConnectState { } } - self.transfer_shuffle_seed = shuffle_seed; + self.transfer_shuffle = match (shuffle_seed, initial_track) { + (Some(seed), Some(initial_track)) => Some(ShuffleState { + seed, + initial_track, + }), + _ => None, + }; self.clear_prev_track(); self.clear_next_tracks(); @@ -163,8 +173,10 @@ impl ConnectState { self.set_current_track(current_index.unwrap_or_default())?; self.set_shuffle(true); - let previous_seed = self.transfer_shuffle_seed.take(); - self.shuffle(previous_seed)?; + match self.transfer_shuffle.take() { + None => self.shuffle_new(), + Some(state) => self.shuffle_restore(state), + }? } else { self.reset_playback_to_position(current_index)?; } From dcd90083fe32fc063dc5d115ac2e5af1f2262c42 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 31 Aug 2025 20:59:19 +0200 Subject: [PATCH 553/561] chore: update crates --- Cargo.lock | 82 +++++++++++++++++++++++++-------------------- playback/Cargo.toml | 2 +- 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 72319f01..4ec06506 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,6 +55,18 @@ dependencies = [ "libc", ] +[[package]] +name = "alsa" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" +dependencies = [ + "alsa-sys", + "bitflags 2.9.3", + "cfg-if", + "libc", +] + [[package]] name = "alsa-sys" version = "0.3.1" @@ -405,7 +417,7 @@ version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" dependencies = [ - "alsa", + "alsa 0.9.1", "coreaudio-rs", "dasp_sample", "jack", @@ -889,9 +901,9 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" dependencies = [ "unicode-width", ] @@ -919,7 +931,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.3+wasi-0.2.4", "wasm-bindgen", ] @@ -2014,7 +2026,7 @@ dependencies = [ name = "librespot-playback" version = "0.7.0" dependencies = [ - "alsa", + "alsa 0.10.0", "cpal", "futures-util", "gstreamer", @@ -2707,9 +2719,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -2832,9 +2844,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -2843,7 +2855,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.31", - "socket2 0.5.10", + "socket2 0.6.0", "thiserror 2.0.16", "tokio", "tracing", @@ -2852,9 +2864,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -2873,16 +2885,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.5.10", + "socket2 0.6.0", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -4237,11 +4249,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.3+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" dependencies = [ - "wit-bindgen-rt", + "wit-bindgen", ] [[package]] @@ -4789,13 +4801,10 @@ dependencies = [ ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags 2.9.3", -] +checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" [[package]] name = "writeable" @@ -4829,9 +4838,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb4f9a464286d42851d18a605f7193b8febaf5b0919d71c6399b7b26e5b0aad" +checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be" dependencies = [ "async-broadcast", "async-recursion", @@ -4848,7 +4857,7 @@ dependencies = [ "tokio", "tracing", "uds_windows", - "windows-sys 0.59.0", + "windows-sys 0.60.2", "winnow", "zbus_macros", "zbus_names", @@ -4857,9 +4866,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.9.0" +version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef9859f68ee0c4ee2e8cde84737c78e3f4c54f946f2a38645d0d4c7a95327659" +checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4964,9 +4973,9 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" +checksum = "999dd3be73c52b1fccd109a4a81e4fcd20fab1d3599c8121b38d04e1419498db" dependencies = [ "endi", "enumflags2", @@ -4978,9 +4987,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.6.0" +version = "5.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" +checksum = "6643fd0b26a46d226bd90d3f07c1b5321fe9bb7f04673cb37ac6d6883885b68e" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4991,14 +5000,13 @@ dependencies = [ [[package]] name = "zvariant_utils" -version = "3.2.0" +version = "3.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16edfee43e5d7b553b77872d99bc36afdda75c223ca7ad5e3fbecd82ca5fc34" +checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" dependencies = [ "proc-macro2", "quote", "serde", - "static_assertions", "syn", "winnow", ] diff --git a/playback/Cargo.toml b/playback/Cargo.toml index cb39541d..ddfa23a6 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -66,7 +66,7 @@ tokio = { version = "1", features = [ zerocopy = { version = "0.8", features = ["derive"] } # Backends -alsa = { version = "0.9", optional = true } +alsa = { version = "0.10", optional = true } jack = { version = "0.13", optional = true } portaudio-rs = { version = "0.3", optional = true } sdl2 = { version = "0.38", optional = true } From 987dfa5df2546a96d34582674a757a3dcc6036a7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 31 Aug 2025 21:05:20 +0200 Subject: [PATCH 554/561] chore: prepare for v0.7.1 release --- CHANGELOG.md | 18 +++++------------- Cargo.lock | 18 +++++++++--------- Cargo.toml | 18 +++++++++--------- audio/Cargo.toml | 2 +- connect/Cargo.toml | 6 +++--- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 2 +- metadata/Cargo.toml | 4 ++-- playback/Cargo.toml | 6 +++--- 9 files changed, 35 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 215a5486..c89b12c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,28 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. -## [Unreleased] - -### Added +## [v0.7.1] - 2025-08-31 ### Changed - [connect] Shuffling was adjusted, so that shuffle and repeat can be used combined -### Deprecated - -### Removed - ### Fixed -- [connect] Repeat context will not go into autoplay anymore and triggering autoplay while shuffling shouldn't reshuffle anymore +- [connect] Repeat context will not go into autoplay anymore and triggering autoplay while shuffling shouldn't reshuffle anymore - [connect] Only deletes the connect state on dealer shutdown instead on disconnecting -- [core] Fixed a problem where in `spclient` where an http 411 error was thrown because the header were set wrong +- [core] Fixed a problem where in `spclient` where an HTTP/411 error was thrown because the header was set wrong - [main] Use the config instead of the type default for values that are not provided by the user - -### Security - ## [0.7.0] - 2025-08-24 ### Changed @@ -425,7 +416,8 @@ v0.4.x as a stable branch until then. ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.7.0...HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.7.1...HEAD +[0.7.1]: https://github.com/librespot-org/librespot/compare/v0.7.0...v0.7.1 [0.7.0]: https://github.com/librespot-org/librespot/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/librespot-org/librespot/compare/v0.5.0...v0.6.0 [0.5.0]: https://github.com/librespot-org/librespot/compare/v0.4.2...v0.5.0 diff --git a/Cargo.lock b/Cargo.lock index 4ec06506..f2bd9aef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,7 +1845,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.7.0" +version = "0.7.1" dependencies = [ "data-encoding", "env_logger", @@ -1869,7 +1869,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.7.0" +version = "0.7.1" dependencies = [ "aes", "bytes", @@ -1888,7 +1888,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.7.0" +version = "0.7.1" dependencies = [ "futures-util", "librespot-core", @@ -1906,7 +1906,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.7.0" +version = "0.7.1" dependencies = [ "aes", "base64", @@ -1963,7 +1963,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.7.0" +version = "0.7.1" dependencies = [ "aes", "base64", @@ -1994,7 +1994,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.7.0" +version = "0.7.1" dependencies = [ "async-trait", "bytes", @@ -2010,7 +2010,7 @@ dependencies = [ [[package]] name = "librespot-oauth" -version = "0.7.0" +version = "0.7.1" dependencies = [ "env_logger", "log", @@ -2024,7 +2024,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.7.0" +version = "0.7.1" dependencies = [ "alsa 0.10.0", "cpal", @@ -2056,7 +2056,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.7.0" +version = "0.7.1" dependencies = [ "protobuf", "protobuf-codegen", diff --git a/Cargo.toml b/Cargo.toml index 9ac28ffb..bcb3664a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ include = [ ] [workspace.package] -version = "0.7.0" +version = "0.7.1" rust-version = "1.85" authors = ["Librespot Org"] license = "MIT" @@ -143,14 +143,14 @@ path = "src/main.rs" doc = false [workspace.dependencies] -librespot-audio = { version = "0.7.0", path = "audio", default-features = false } -librespot-connect = { version = "0.7.0", path = "connect", default-features = false } -librespot-core = { version = "0.7.0", path = "core", default-features = false } -librespot-discovery = { version = "0.7.0", path = "discovery", default-features = false } -librespot-metadata = { version = "0.7.0", path = "metadata", default-features = false } -librespot-oauth = { version = "0.7.0", path = "oauth", default-features = false } -librespot-playback = { version = "0.7.0", path = "playback", default-features = false } -librespot-protocol = { version = "0.7.0", path = "protocol", default-features = false } +librespot-audio = { version = "0.7.1", path = "audio", default-features = false } +librespot-connect = { version = "0.7.1", path = "connect", default-features = false } +librespot-core = { version = "0.7.1", path = "core", default-features = false } +librespot-discovery = { version = "0.7.1", path = "discovery", default-features = false } +librespot-metadata = { version = "0.7.1", path = "metadata", default-features = false } +librespot-oauth = { version = "0.7.1", path = "oauth", default-features = false } +librespot-playback = { version = "0.7.1", path = "playback", default-features = false } +librespot-protocol = { version = "0.7.1", path = "protocol", default-features = false } [dependencies] librespot-audio.workspace = true diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 0e0daa34..5a57ed76 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -18,7 +18,7 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core = { version = "0.7.0", path = "../core", default-features = false } +librespot-core = { version = "0.7.1", path = "../core", default-features = false } aes = "0.8" bytes = "1" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index b5b519b0..9e7b231b 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -18,9 +18,9 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core = { version = "0.7.0", path = "../core", default-features = false } -librespot-playback = { version = "0.7.0", path = "../playback", default-features = false } -librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } +librespot-core = { version = "0.7.1", path = "../core", default-features = false } +librespot-playback = { version = "0.7.1", path = "../playback", default-features = false } +librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false } futures-util = "0.3" log = "0.4" diff --git a/core/Cargo.toml b/core/Cargo.toml index 8f79fd56..4f5e79cc 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -40,8 +40,8 @@ rustls-tls-webpki-roots = [ __rustls = [] [dependencies] -librespot-oauth = { version = "0.7.0", path = "../oauth", default-features = false } -librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } +librespot-oauth = { version = "0.7.1", path = "../oauth", default-features = false } +librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false } aes = "0.8" base64 = "0.22" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 2e754e84..6285b1a4 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -23,7 +23,7 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core = { version = "0.7.0", path = "../core", default-features = false } +librespot-core = { version = "0.7.1", path = "../core", default-features = false } aes = "0.8" base64 = "0.22" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 01b0ec0b..d2d434ee 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -18,8 +18,8 @@ rustls-tls-native-roots = ["librespot-core/rustls-tls-native-roots"] rustls-tls-webpki-roots = ["librespot-core/rustls-tls-webpki-roots"] [dependencies] -librespot-core = { version = "0.7.0", path = "../core", default-features = false } -librespot-protocol = { version = "0.7.0", path = "../protocol", default-features = false } +librespot-core = { version = "0.7.1", path = "../core", default-features = false } +librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false } async-trait = "0.1" bytes = "1" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index ddfa23a6..dca23297 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -47,9 +47,9 @@ rustls-tls-webpki-roots = [ ] [dependencies] -librespot-audio = { version = "0.7.0", path = "../audio", default-features = false } -librespot-core = { version = "0.7.0", path = "../core", default-features = false } -librespot-metadata = { version = "0.7.0", path = "../metadata", default-features = false } +librespot-audio = { version = "0.7.1", path = "../audio", default-features = false } +librespot-core = { version = "0.7.1", path = "../core", default-features = false } +librespot-metadata = { version = "0.7.1", path = "../metadata", default-features = false } portable-atomic = "1" futures-util = "0.3" From f16a30e86a5175cc7f361f37bbe9447fe218babc Mon Sep 17 00:00:00 2001 From: Will Stott Date: Sun, 7 Sep 2025 13:39:10 +0100 Subject: [PATCH 555/561] chore: update libmdns to 0.10.1 (#1575) --- Cargo.lock | 37 ++++++++++++------------------------- discovery/Cargo.toml | 2 +- discovery/src/lib.rs | 7 +------ 3 files changed, 14 insertions(+), 32 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f2bd9aef..1461aed8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1413,7 +1413,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.0", + "socket2", "system-configuration", "tokio", "tower-service", @@ -1560,12 +1560,12 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.12.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2a33e9c38988ecbda730c85b0fd9ddcdf83c0305ac7fd21c8bb9f57f2f0cc8" +checksum = "bf39cc0423ee66021dc5eccface85580e4a001e0c5288bae8bea7ecb69225e90" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1779,9 +1779,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "libmdns" -version = "0.9.1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48854699e11b111433431b69cee2365fcab0b29b06993f48c257dfbaf6395862" +checksum = "a00dbe871d2cf9df13f68d152b949fca8cafc854b60ffd259fc6df6e8663d8d7" dependencies = [ "byteorder", "futures-util", @@ -1789,9 +1789,9 @@ dependencies = [ "if-addrs", "log", "multimap", - "rand 0.8.5", - "socket2 0.5.10", - "thiserror 1.0.69", + "rand 0.9.2", + "socket2", + "thiserror 2.0.16", "tokio", ] @@ -2163,9 +2163,6 @@ name = "multimap" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" -dependencies = [ - "serde", -] [[package]] name = "native-tls" @@ -2855,7 +2852,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls 0.23.31", - "socket2 0.6.0", + "socket2", "thiserror 2.0.16", "tokio", "tracing", @@ -2892,7 +2889,7 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.0", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -3483,16 +3480,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.0" @@ -3857,7 +3844,7 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "slab", - "socket2 0.6.0", + "socket2", "tokio-macros", "tracing", "windows-sys 0.59.0", diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 6285b1a4..4220b130 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -41,7 +41,7 @@ hyper-util = { version = "0.1", features = [ "server-graceful", "service", ] } -libmdns = { version = "0.9", optional = true } +libmdns = { version = "0.10", optional = true } log = "0.4" rand = "0.9" serde = { version = "1", default-features = false, features = [ diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index fb0c1f63..e440c67f 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -406,12 +406,7 @@ fn launch_libmdns( } .map_err(|e| DiscoveryError::DnsSdError(Box::new(e)))?; - let svc = responder.register( - DNS_SD_SERVICE_NAME.to_owned(), - name.into_owned(), - port, - &TXT_RECORD, - ); + let svc = responder.register(&DNS_SD_SERVICE_NAME, &name, port, &TXT_RECORD); let _ = shutdown_rx.blocking_recv(); From 0e5531ff5483dc57fc7557325ceec13b2e486732 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Tue, 9 Sep 2025 20:05:04 +0100 Subject: [PATCH 556/561] docs: Document examples (#1567) Document the process for running the examples. This was fairly non-obvious and I spent a fair bit of time trying and failing to get a client credentials token to work before finding in some discussion that this is known not to work. The wiki mentions using the oauth example as a way to get a token and I think that is likely to be the best way to get a token for the purposes of running the examples too. --- examples/README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 examples/README.md diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..9a5e794b --- /dev/null +++ b/examples/README.md @@ -0,0 +1,36 @@ +# Examples + +This folder contains examples of how to use the `librespot` library for various purposes. + +## How to run the examples + +In general, to invoke an example, clone down the repo and use `cargo` as follows: + +``` +cargo run --example [filename] +``` + +in which `filename` is the file name of the example, for instance `get_token` or `play`. + +### Acquiring an access token + +Most examples require an access token as the first positional argument. **Note that an access token +gained by the client credentials flow will not work**. `librespot-oauth` provides a utility to +acquire an access token using an OAuth flow, which will be able to run the examples. To invoke this, +run: + +``` +cargo run --package librespot-oauth --example oauth_sync +``` + +A browser window will open and prompt you to authorize with Spotify. Once done, take the +`access_token` property from the dumped object response and proceed to use it in examples. You may +find it convenient to save it in a shell variable like `$ACCESS_TOKEN`. + +Once you have obtained the token you can proceed to run the example. Check each individual +file to see what arguments are expected. As a demonstration, here is how to invoke the `play` +example to play a song -- the second argument is the URI of the track to play. + +``` +cargo run --example play "$ACCESS_TOKEN" 2WUy2Uywcj5cP0IXQagO3z +``` \ No newline at end of file From df5f957bdd9a29801dd16f08cdc9abc580912da7 Mon Sep 17 00:00:00 2001 From: Jay Malhotra <5047192+SapiensAnatis@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:59:53 +0100 Subject: [PATCH 557/561] refactor: Introduce SpotifyUri struct (#1538) * refactor: Introduce SpotifyUri struct Contributes to #1266 Introduces a new `SpotifyUri` struct which is layered on top of the existing `SpotifyId`, but has the capability to support URIs that do not confirm to the canonical base62 encoded format. This allows it to describe URIs like `spotify:local`, `spotify:genre` and others that `SpotifyId` cannot represent. Changed the internal player state to use these URIs as much as possible, such that the player could in the future accept a URI of the type `spotify:local`, as a means of laying the groundwork for local file support. * fix: Don't pass unknown URIs from deprecated player methods * refactor: remove SpotifyUri::to_base16 This should be deprecated for the same reason to_base62 is, and could unpredictably throw errors -- consumers should match on the inner ID if they need a base62 representation and handle failure appropriately * refactor: Store original data in SpotifyUri::Unknown Instead of assuming Unknown has a u128 SpotifyId, store the original data and type that we failed to parse. * refactor: Remove SpotifyItemType * refactor: Address review feedback * test: Add more SpotifyUri tests * chore: Correctly mark changes as breaking in CHANGELOG.md * refactor: Respond to review feedback * chore: Changelog updates --- CHANGELOG.md | 25 ++ connect/src/spirc.rs | 8 +- connect/src/state/context.rs | 8 +- connect/src/state/tracks.rs | 8 +- core/src/lib.rs | 2 + core/src/spclient.rs | 6 +- core/src/spotify_id.rs | 428 ++------------------ core/src/spotify_uri.rs | 583 ++++++++++++++++++++++++++++ examples/play.rs | 11 +- examples/playlist_tracks.rs | 5 +- metadata/src/album.rs | 18 +- metadata/src/artist.rs | 24 +- metadata/src/audio/item.rs | 32 +- metadata/src/episode.rs | 16 +- metadata/src/image.rs | 4 +- metadata/src/lib.rs | 8 +- metadata/src/playlist/annotation.rs | 26 +- metadata/src/playlist/item.rs | 6 +- metadata/src/playlist/list.rs | 37 +- metadata/src/show.rs | 16 +- metadata/src/track.rs | 16 +- playback/src/player.rs | 220 ++++++----- src/player_event_handler.rs | 55 ++- 23 files changed, 937 insertions(+), 625 deletions(-) create mode 100644 core/src/spotify_uri.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c89b12c3..a11d932e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. +## [Unreleased] + +### Added + +- [core] Add `SpotifyUri` type to represent more types of URI than `SpotifyId` can + +### Changed + +- [playback] Changed type of `SpotifyId` fields in `PlayerEvent` members to `SpotifyUri` (breaking) +- [metadata] Changed arguments for `Metadata` trait from `&SpotifyId` to `&SpotifyUri` (breaking) +- [player] `load` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) +- [player] `preload` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) +- [spclient] `get_radio_for_track` function changed from accepting a `SpotifyId` to accepting a `SpotifyUri` (breaking) + + +### Removed + +- [core] Removed `SpotifyItemType` enum; the new `SpotifyUri` is an enum over all item types and so which variant it is + describes its item type (breaking) +- [core] Removed `NamedSpotifyId` struct; it was made obsolete by `SpotifyUri` (breaking) +- [core] The following methods have been removed from `SpotifyId` and moved to `SpotifyUri` (breaking): + - `is_playable` + - `from_uri` + - `to_uri` + ## [v0.7.1] - 2025-08-31 ### Changed diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 087384e9..43702d8a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -2,7 +2,7 @@ use crate::{ LoadContextOptions, LoadRequestOptions, PlayContext, context_resolver::{ContextAction, ContextResolver, ResolveContext}, core::{ - Error, Session, SpotifyId, + Error, Session, SpotifyUri, authentication::Credentials, dealer::{ manager::{BoxedStream, BoxedStreamResult, Reply, RequestReply}, @@ -778,7 +778,7 @@ impl SpircTask { return Ok(()); } PlayerEvent::Unavailable { track_id, .. } => { - self.handle_unavailable(track_id)?; + self.handle_unavailable(&track_id)?; if self.connect_state.current_track(|t| &t.uri) == &track_id.to_uri()? { self.handle_next(None)? } @@ -1499,7 +1499,7 @@ impl SpircTask { } // Mark unavailable tracks so we can skip them later - fn handle_unavailable(&mut self, track_id: SpotifyId) -> Result<(), Error> { + fn handle_unavailable(&mut self, track_id: &SpotifyUri) -> Result<(), Error> { self.connect_state.mark_unavailable(track_id)?; self.handle_preload_next_track(); @@ -1704,7 +1704,7 @@ impl SpircTask { } let current_uri = self.connect_state.current_track(|t| &t.uri); - let id = SpotifyId::from_uri(current_uri)?; + let id = SpotifyUri::from_uri(current_uri)?; self.player.load(id, start_playing, position_ms); self.connect_state diff --git a/connect/src/state/context.rs b/connect/src/state/context.rs index 7f0fc640..e2b78720 100644 --- a/connect/src/state/context.rs +++ b/connect/src/state/context.rs @@ -1,5 +1,5 @@ use crate::{ - core::{Error, SpotifyId}, + core::{Error, SpotifyId, SpotifyUri}, protocol::{ context::Context, context_page::ContextPage, @@ -449,8 +449,10 @@ impl ConnectState { (Some(uri), _) if uri.contains(['?', '%']) => { Err(StateError::InvalidTrackUri(Some(uri.clone())))? } - (Some(uri), _) if !uri.is_empty() => SpotifyId::from_uri(uri)?, - (_, Some(gid)) if !gid.is_empty() => SpotifyId::from_raw(gid)?, + (Some(uri), _) if !uri.is_empty() => SpotifyUri::from_uri(uri)?, + (_, Some(gid)) if !gid.is_empty() => SpotifyUri::Track { + id: SpotifyId::from_raw(gid)?, + }, _ => Err(StateError::InvalidTrackUri(None))?, }; diff --git a/connect/src/state/tracks.rs b/connect/src/state/tracks.rs index 94bde92b..8619035c 100644 --- a/connect/src/state/tracks.rs +++ b/connect/src/state/tracks.rs @@ -1,5 +1,5 @@ use crate::{ - core::{Error, SpotifyId}, + core::{Error, SpotifyUri}, protocol::player::ProvidedTrack, state::{ ConnectState, SPOTIFY_MAX_NEXT_TRACKS_SIZE, SPOTIFY_MAX_PREV_TRACKS_SIZE, StateError, @@ -352,14 +352,14 @@ impl<'ct> ConnectState { Ok(()) } - pub fn preview_next_track(&mut self) -> Option { + pub fn preview_next_track(&mut self) -> Option { let next = if self.repeat_track() { self.current_track(|t| &t.uri) } else { &self.next_tracks().first()?.uri }; - SpotifyId::from_uri(next).ok() + SpotifyUri::from_uri(next).ok() } pub fn has_next_tracks(&self, min: Option) -> bool { @@ -381,7 +381,7 @@ impl<'ct> ConnectState { prev } - pub fn mark_unavailable(&mut self, id: SpotifyId) -> Result<(), Error> { + pub fn mark_unavailable(&mut self, id: &SpotifyUri) -> Result<(), Error> { let uri = id.to_uri()?; debug!("marking {uri} as unavailable"); diff --git a/core/src/lib.rs b/core/src/lib.rs index f2d6587e..f4ead234 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -32,6 +32,7 @@ mod socket; #[allow(dead_code)] pub mod spclient; pub mod spotify_id; +pub mod spotify_uri; pub mod token; #[doc(hidden)] pub mod util; @@ -42,3 +43,4 @@ pub use error::Error; pub use file_id::FileId; pub use session::Session; pub use spotify_id::SpotifyId; +pub use spotify_uri::SpotifyUri; diff --git a/core/src/spclient.rs b/core/src/spclient.rs index 7d3f39e9..7bc9d0b5 100644 --- a/core/src/spclient.rs +++ b/core/src/spclient.rs @@ -5,7 +5,7 @@ use std::{ use crate::config::{OS, os_version}; use crate::{ - Error, FileId, SpotifyId, + Error, FileId, SpotifyId, SpotifyUri, apresolve::SocketAddress, config::SessionConfig, error::ErrorKind, @@ -676,10 +676,10 @@ impl SpClient { .await } - pub async fn get_radio_for_track(&self, track_id: &SpotifyId) -> SpClientResult { + pub async fn get_radio_for_track(&self, track_uri: &SpotifyUri) -> SpClientResult { let endpoint = format!( "/inspiredby-mix/v2/seed_to_playlist/{}?response-format=json", - track_id.to_uri()? + track_uri.to_uri()? ); self.request_as_json(&Method::GET, &endpoint, None, None) diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index f7478f54..c627b551 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -1,70 +1,23 @@ -use std::{fmt, ops::Deref}; +use std::fmt; use thiserror::Error; -use crate::Error; - -use librespot_protocol as protocol; +use crate::{Error, SpotifyUri}; // re-export FileId for historic reasons, when it was part of this mod pub use crate::FileId; -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum SpotifyItemType { - Album, - Artist, - Episode, - Playlist, - Show, - Track, - Local, - Unknown, -} - -impl From<&str> for SpotifyItemType { - fn from(v: &str) -> Self { - match v { - "album" => Self::Album, - "artist" => Self::Artist, - "episode" => Self::Episode, - "playlist" => Self::Playlist, - "show" => Self::Show, - "track" => Self::Track, - "local" => Self::Local, - _ => Self::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", - SpotifyItemType::Local => "local", - _ => "unknown", - } - } -} - #[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct SpotifyId { pub id: u128, - pub item_type: SpotifyItemType, } #[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] pub enum SpotifyIdError { #[error("ID cannot be parsed")] InvalidId, - #[error("not a valid Spotify URI")] + #[error("not a valid Spotify ID")] InvalidFormat, - #[error("URI does not belong to Spotify")] - InvalidRoot, } impl From for Error { @@ -74,7 +27,6 @@ impl From for Error { } pub type SpotifyIdResult = Result; -pub type NamedSpotifyIdResult = Result; const BASE62_DIGITS: &[u8; 62] = b"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; const BASE16_DIGITS: &[u8; 16] = b"0123456789abcdef"; @@ -84,14 +36,6 @@ impl SpotifyId { const SIZE_BASE16: usize = 32; const SIZE_BASE62: usize = 22; - /// Returns whether this `SpotifyId` is for a playable audio item, if known. - pub fn is_playable(&self) -> bool { - matches!( - self.item_type, - SpotifyItemType::Episode | SpotifyItemType::Track - ) - } - /// Parses a base16 (hex) encoded [Spotify ID] into a `SpotifyId`. /// /// `src` is expected to be 32 bytes long and encoded using valid characters. @@ -114,10 +58,7 @@ impl SpotifyId { dst += p; } - Ok(Self { - id: dst, - item_type: SpotifyItemType::Unknown, - }) + Ok(Self { id: dst }) } /// Parses a base62 encoded [Spotify ID] into a `u128`. @@ -126,7 +67,7 @@ impl SpotifyId { /// /// [Spotify ID]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids pub fn from_base62(src: &str) -> SpotifyIdResult { - if src.len() != 22 { + if src.len() != Self::SIZE_BASE62 { return Err(SpotifyIdError::InvalidId.into()); } let mut dst: u128 = 0; @@ -143,10 +84,7 @@ impl SpotifyId { dst = dst.checked_add(p).ok_or(SpotifyIdError::InvalidId)?; } - Ok(Self { - id: dst, - item_type: SpotifyItemType::Unknown, - }) + Ok(Self { id: dst }) } /// Creates a `u128` from a copy of `SpotifyId::SIZE` (16) bytes in big-endian order. @@ -156,65 +94,11 @@ impl SpotifyId { match src.try_into() { Ok(dst) => Ok(Self { id: u128::from_be_bytes(dst), - item_type: SpotifyItemType::Unknown, }), Err(_) => Err(SpotifyIdError::InvalidId.into()), } } - /// Parses a [Spotify URI] into a `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:playlist:{id}`. - /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids - pub fn from_uri(src: &str) -> SpotifyIdResult { - // Basic: `spotify:{type}:{id}` - // Named: `spotify:user:{user}:{type}:{id}` - // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}` - let mut parts = src.split(':'); - - let scheme = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - - let item_type = { - let next = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - if next == "user" { - let _username = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - parts.next().ok_or(SpotifyIdError::InvalidFormat)? - } else { - next - } - }; - - let id = parts.next().ok_or(SpotifyIdError::InvalidFormat)?; - - if scheme != "spotify" { - return Err(SpotifyIdError::InvalidRoot.into()); - } - - let item_type = item_type.into(); - - // Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/ - // TODO: find a way to add this local file ID to SpotifyId. - // One possible solution would be to copy the contents of `id` to a new String field in SpotifyId, - // but then we would need to remove the derived Copy trait, which would be a breaking change. - if item_type == SpotifyItemType::Local { - return Ok(Self { item_type, id: 0 }); - } - - if id.len() != Self::SIZE_BASE62 { - return Err(SpotifyIdError::InvalidId.into()); - } - - Ok(Self { - item_type, - ..Self::from_base62(id)? - }) - } - /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. #[allow(clippy::wrong_self_convention)] @@ -274,124 +158,19 @@ impl SpotifyId { pub fn to_raw(&self) -> [u8; Self::SIZE] { self.id.to_be_bytes() } - - /// Returns the `SpotifyId` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`, - /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded - /// Spotify ID. - /// - /// If the `SpotifyId` has an associated type unrecognized by the library, `{type}` will - /// be encoded as `unknown`. - /// - /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids - #[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(); - let mut dst = String::with_capacity(31 + item_type.len()); - dst.push_str("spotify:"); - dst.push_str(item_type); - dst.push(':'); - let base_62 = self.to_base62()?; - dst.push_str(&base_62); - - Ok(dst) - } } impl fmt::Debug for SpotifyId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SpotifyId") - .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + .field(&self.to_base62().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().unwrap_or_else(|_| "invalid uri".into())) - } -} - -#[derive(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.into()); - } - - if uri_parts[0] != "spotify" { - return Err(SpotifyIdError::InvalidRoot.into()); - } - - if uri_parts[1] != "user" { - return Err(SpotifyIdError::InvalidFormat.into()); - } - - Ok(Self { - inner_id: SpotifyId::from_uri(src)?, - username: uri_parts[2].to_owned(), - }) - } - - 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(':'); - dst.push_str(item_type); - dst.push(':'); - let base_62 = self.to_base62()?; - dst.push_str(&base_62); - - Ok(dst) - } - - pub fn from_spotify_id(id: SpotifyId, username: &str) -> Self { - Self { - inner_id: id, - username: username.to_owned(), - } - } -} - -impl Deref for NamedSpotifyId { - type Target = SpotifyId; - fn deref(&self) -> &Self::Target { - &self.inner_id - } -} - -impl fmt::Debug for NamedSpotifyId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("NamedSpotifyId") - .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() - .unwrap_or_else(|_| "invalid id".into()), - ) + f.write_str(&self.to_base62().unwrap_or_else(|_| "invalid uri".into())) } } @@ -423,104 +202,20 @@ impl TryFrom<&Vec> for SpotifyId { } } -impl TryFrom<&protocol::metadata::Album> for SpotifyId { +impl TryFrom<&SpotifyUri> for SpotifyId { type Error = crate::Error; - fn try_from(album: &protocol::metadata::Album) -> Result { - Ok(Self { - item_type: SpotifyItemType::Album, - ..Self::from_raw(album.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Artist> for SpotifyId { - type Error = crate::Error; - fn try_from(artist: &protocol::metadata::Artist) -> Result { - Ok(Self { - item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Episode> for SpotifyId { - type Error = crate::Error; - fn try_from(episode: &protocol::metadata::Episode) -> Result { - Ok(Self { - item_type: SpotifyItemType::Episode, - ..Self::from_raw(episode.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Track> for SpotifyId { - type Error = crate::Error; - fn try_from(track: &protocol::metadata::Track) -> Result { - Ok(Self { - item_type: SpotifyItemType::Track, - ..Self::from_raw(track.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::Show> for SpotifyId { - type Error = crate::Error; - fn try_from(show: &protocol::metadata::Show) -> Result { - Ok(Self { - item_type: SpotifyItemType::Show, - ..Self::from_raw(show.gid())? - }) - } -} - -impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyId { - type Error = crate::Error; - fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { - Ok(Self { - item_type: SpotifyItemType::Artist, - ..Self::from_raw(artist.artist_gid())? - }) - } -} - -impl TryFrom<&protocol::playlist4_external::Item> for SpotifyId { - type Error = crate::Error; - fn try_from(item: &protocol::playlist4_external::Item) -> Result { - Ok(Self { - item_type: SpotifyItemType::Track, - ..Self::from_uri(item.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 = crate::Error; - fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { - Self::try_from(item.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 = crate::Error; - fn try_from( - playlist: &protocol::playlist4_external::SelectedListContent, - ) -> Result { - Self::try_from(playlist.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 = crate::Error; - fn try_from( - picture: &protocol::playlist_annotate3::TranscodedPicture, - ) -> Result { - Self::from_base62(picture.uri()) + fn try_from(value: &SpotifyUri) -> Result { + match value { + SpotifyUri::Album { id } + | SpotifyUri::Artist { id } + | SpotifyUri::Episode { id } + | SpotifyUri::Playlist { id, .. } + | SpotifyUri::Show { id } + | SpotifyUri::Track { id } => Ok(*id), + SpotifyUri::Local { .. } | SpotifyUri::Unknown { .. } => { + Err(SpotifyIdError::InvalidFormat.into()) + } + } } } @@ -541,8 +236,6 @@ mod tests { struct ConversionCase { id: u128, - kind: SpotifyItemType, - uri: &'static str, base16: &'static str, base62: &'static str, raw: &'static [u8], @@ -551,8 +244,6 @@ mod tests { static CONV_VALID: [ConversionCase; 5] = [ ConversionCase { id: 238762092608182713602505436543891614649, - kind: SpotifyItemType::Track, - uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", base16: "b39fe8081e1f4c54be38e8d6f9f12bb9", base62: "5sWHDYs0csV6RS48xBl0tH", raw: &[ @@ -561,8 +252,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Track, - uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -571,8 +260,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Episode, - uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -581,8 +268,6 @@ mod tests { }, ConversionCase { id: 204841891221366092811751085145916697048, - kind: SpotifyItemType::Show, - uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", base16: "9a1b1cfbc6f244569ae0356c77bbe9d8", base62: "4GNcXTGWmnZ3ySrqvol3o4", raw: &[ @@ -591,8 +276,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Local, - uri: "spotify:local:0000000000000000000000", base16: "00000000000000000000000000000000", base62: "0000000000000000000000", raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], @@ -602,9 +285,6 @@ mod tests { static CONV_INVALID: [ConversionCase; 5] = [ ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Invalid ID in the URI. - uri: "spotify:arbitrarywhatever:5sWHDYs0Bl0tH", base16: "ZZZZZ8081e1f4c54be38e8d6f9f12bb9", base62: "!!!!!Ys0csV6RS48xBl0tH", raw: &[ @@ -614,9 +294,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Missing colon between ID and type. - uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", base16: "--------------------", base62: "....................", raw: &[ @@ -626,9 +303,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Uri too short - uri: "spotify:azb:aRS48xBl0tH", // too long, should return error but not panic overflow base16: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", // too long, should return error but not panic overflow @@ -640,9 +314,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - // Uri too short - uri: "spotify:azb:aRS48xBl0tH", base16: "--------------------", // too short to encode a 128 bits int base62: "aa", @@ -653,8 +324,6 @@ mod tests { }, ConversionCase { id: 0, - kind: SpotifyItemType::Unknown, - uri: "cleary invalid uri", base16: "--------------------", // too high of a value, this would need a 132 bits int base62: "ZZZZZZZZZZZZZZZZZZZZZZ", @@ -679,10 +348,7 @@ mod tests { #[test] fn to_base62() { for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; + let id = SpotifyId { id: c.id }; assert_eq!(id.to_base62().unwrap(), c.base62); } @@ -702,60 +368,12 @@ mod tests { #[test] fn to_base16() { for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; + let id = SpotifyId { id: c.id }; assert_eq!(id.to_base16().unwrap(), c.base16); } } - #[test] - fn from_uri() { - for c in &CONV_VALID { - let actual = SpotifyId::from_uri(c.uri).unwrap(); - - assert_eq!(actual.id, c.id); - assert_eq!(actual.item_type, c.kind); - } - - for c in &CONV_INVALID { - assert!(SpotifyId::from_uri(c.uri).is_err()); - } - } - - #[test] - fn from_local_uri() { - let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap(); - - assert_eq!(actual.id, 0); - assert_eq!(actual.item_type, SpotifyItemType::Local); - } - - #[test] - fn from_named_uri() { - let actual = - NamedSpotifyId::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI") - .unwrap(); - - assert_eq!(actual.id, 136159921382084734723401526672209703396); - assert_eq!(actual.item_type, SpotifyItemType::Playlist); - assert_eq!(actual.username, "spotify"); - } - - #[test] - fn to_uri() { - for c in &CONV_VALID { - let id = SpotifyId { - id: c.id, - item_type: c.kind, - }; - - assert_eq!(id.to_uri().unwrap(), c.uri); - } - } - #[test] fn from_raw() { for c in &CONV_VALID { diff --git a/core/src/spotify_uri.rs b/core/src/spotify_uri.rs new file mode 100644 index 00000000..647ec652 --- /dev/null +++ b/core/src/spotify_uri.rs @@ -0,0 +1,583 @@ +use crate::{Error, SpotifyId}; +use std::{borrow::Cow, fmt}; +use thiserror::Error; + +use librespot_protocol as protocol; + +const SPOTIFY_ITEM_TYPE_ALBUM: &str = "album"; +const SPOTIFY_ITEM_TYPE_ARTIST: &str = "artist"; +const SPOTIFY_ITEM_TYPE_EPISODE: &str = "episode"; +const SPOTIFY_ITEM_TYPE_PLAYLIST: &str = "playlist"; +const SPOTIFY_ITEM_TYPE_SHOW: &str = "show"; +const SPOTIFY_ITEM_TYPE_TRACK: &str = "track"; +const SPOTIFY_ITEM_TYPE_LOCAL: &str = "local"; +const SPOTIFY_ITEM_TYPE_UNKNOWN: &str = "unknown"; + +#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)] +pub enum SpotifyUriError { + #[error("not a valid Spotify URI")] + InvalidFormat, + #[error("URI does not belong to Spotify")] + InvalidRoot, +} + +impl From for Error { + fn from(err: SpotifyUriError) -> Self { + Error::invalid_argument(err) + } +} + +pub type SpotifyUriResult = Result; + +#[derive(Clone, PartialEq, Eq, Hash)] +pub enum SpotifyUri { + Album { + id: SpotifyId, + }, + Artist { + id: SpotifyId, + }, + Episode { + id: SpotifyId, + }, + Playlist { + user: Option, + id: SpotifyId, + }, + Show { + id: SpotifyId, + }, + Track { + id: SpotifyId, + }, + Local { + artist: String, + album_title: String, + track_title: String, + duration: std::time::Duration, + }, + Unknown { + kind: Cow<'static, str>, + id: String, + }, +} + +impl SpotifyUri { + /// Returns whether this `SpotifyUri` is for a playable audio item, if known. + pub fn is_playable(&self) -> bool { + matches!(self, SpotifyUri::Episode { .. } | SpotifyUri::Track { .. }) + } + + /// Gets the item type of this URI as a static string + pub fn item_type(&self) -> &'static str { + match &self { + SpotifyUri::Album { .. } => SPOTIFY_ITEM_TYPE_ALBUM, + SpotifyUri::Artist { .. } => SPOTIFY_ITEM_TYPE_ARTIST, + SpotifyUri::Episode { .. } => SPOTIFY_ITEM_TYPE_EPISODE, + SpotifyUri::Playlist { .. } => SPOTIFY_ITEM_TYPE_PLAYLIST, + SpotifyUri::Show { .. } => SPOTIFY_ITEM_TYPE_SHOW, + SpotifyUri::Track { .. } => SPOTIFY_ITEM_TYPE_TRACK, + SpotifyUri::Local { .. } => SPOTIFY_ITEM_TYPE_LOCAL, + SpotifyUri::Unknown { .. } => SPOTIFY_ITEM_TYPE_UNKNOWN, + } + } + + /// Gets the ID of this URI. The resource ID is the component of the URI that identifies + /// the resource after its type label. If `self` is a named ID, the user will be omitted. + pub fn to_id(&self) -> Result { + match &self { + SpotifyUri::Album { id } + | SpotifyUri::Artist { id } + | SpotifyUri::Episode { id } + | SpotifyUri::Playlist { id, .. } + | SpotifyUri::Show { id } + | SpotifyUri::Track { id } => id.to_base62(), + SpotifyUri::Local { + artist, + album_title, + track_title, + duration, + } => { + let duration_secs = duration.as_secs(); + Ok(format!( + "{artist}:{album_title}:{track_title}:{duration_secs}" + )) + } + SpotifyUri::Unknown { id, .. } => Ok(id.clone()), + } + } + + /// Parses a [Spotify URI] into a `SpotifyUri`. + /// + /// `uri` is expected to be in the canonical form `spotify:{type}:{id}`, where `{type}` + /// can be arbitrary while `{id}` is in a format that varies based on the `{type}`: + /// + /// - For most item types, a 22-character long, base62 encoded Spotify ID is expected. + /// - For local files, an arbitrary length string with the fields + /// `{artist}:{album_title}:{track_title}:{duration_in_seconds}` is expected. + /// + /// Spotify URI: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids + pub fn from_uri(src: &str) -> SpotifyUriResult { + // Basic: `spotify:{type}:{id}` + // Named: `spotify:user:{user}:{type}:{id}` + // Local: `spotify:local:{artist}:{album_title}:{track_title}:{duration_in_seconds}` + let mut parts = src.split(':'); + + let scheme = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + + if scheme != "spotify" { + return Err(SpotifyUriError::InvalidRoot.into()); + } + + let mut username: Option = None; + + let item_type = { + let next = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + if next == "user" { + username.replace( + parts + .next() + .ok_or(SpotifyUriError::InvalidFormat)? + .to_owned(), + ); + parts.next().ok_or(SpotifyUriError::InvalidFormat)? + } else { + next + } + }; + + let name = parts.next().ok_or(SpotifyUriError::InvalidFormat)?; + match item_type { + SPOTIFY_ITEM_TYPE_ALBUM => Ok(Self::Album { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_ARTIST => Ok(Self::Artist { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_EPISODE => Ok(Self::Episode { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_PLAYLIST => Ok(Self::Playlist { + id: SpotifyId::from_base62(name)?, + user: username, + }), + SPOTIFY_ITEM_TYPE_SHOW => Ok(Self::Show { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_TRACK => Ok(Self::Track { + id: SpotifyId::from_base62(name)?, + }), + SPOTIFY_ITEM_TYPE_LOCAL => Ok(Self::Local { + artist: "unimplemented".to_owned(), + album_title: "unimplemented".to_owned(), + track_title: "unimplemented".to_owned(), + duration: Default::default(), + }), + _ => Ok(Self::Unknown { + kind: item_type.to_owned().into(), + id: name.to_owned(), + }), + } + } + + /// Returns the `SpotifyUri` as a [Spotify URI] in the canonical form `spotify:{type}:{id}`, + /// where `{type}` is an arbitrary string and `{id}` is a 22-character long, base62 encoded + /// Spotify ID. + /// + /// If the `SpotifyUri` has an associated type unrecognized by the library, `{type}` will + /// be encoded as `unknown`. + /// + /// If the `SpotifyUri` is named, it will be returned in the form + /// `spotify:user:{user}:{type}:{id}`. + /// + /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/concepts/spotify-uris-ids + pub fn to_uri(&self) -> Result { + let item_type = self.item_type(); + let name = self.to_id()?; + + if let SpotifyUri::Playlist { + id, + user: Some(user), + } = self + { + Ok(format!("spotify:user:{user}:{item_type}:{id}")) + } else { + Ok(format!("spotify:{item_type}:{name}")) + } + } + + /// Gets the name of this URI. The resource name is the component of the URI that identifies + /// the resource after its type label. If `self` is a named ID, the user will be omitted. + /// + /// Deprecated: not all IDs can be represented in Base62, so this function has been renamed to + /// [SpotifyUri::to_id], which this implementation forwards to. + #[deprecated(since = "0.8.0", note = "use to_name instead")] + pub fn to_base62(&self) -> Result { + self.to_id() + } +} + +impl fmt::Debug for SpotifyUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("SpotifyUri") + .field(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + .finish() + } +} + +impl fmt::Display for SpotifyUri { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_uri().unwrap_or_else(|_| "invalid uri".into())) + } +} + +impl TryFrom<&protocol::metadata::Album> for SpotifyUri { + type Error = crate::Error; + fn try_from(album: &protocol::metadata::Album) -> Result { + Ok(Self::Album { + id: SpotifyId::from_raw(album.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Artist> for SpotifyUri { + type Error = crate::Error; + fn try_from(artist: &protocol::metadata::Artist) -> Result { + Ok(Self::Artist { + id: SpotifyId::from_raw(artist.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Episode> for SpotifyUri { + type Error = crate::Error; + fn try_from(episode: &protocol::metadata::Episode) -> Result { + Ok(Self::Episode { + id: SpotifyId::from_raw(episode.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Track> for SpotifyUri { + type Error = crate::Error; + fn try_from(track: &protocol::metadata::Track) -> Result { + Ok(Self::Track { + id: SpotifyId::from_raw(track.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::Show> for SpotifyUri { + type Error = crate::Error; + fn try_from(show: &protocol::metadata::Show) -> Result { + Ok(Self::Show { + id: SpotifyId::from_raw(show.gid())?, + }) + } +} + +impl TryFrom<&protocol::metadata::ArtistWithRole> for SpotifyUri { + type Error = crate::Error; + fn try_from(artist: &protocol::metadata::ArtistWithRole) -> Result { + Ok(Self::Artist { + id: SpotifyId::from_raw(artist.artist_gid())?, + }) + } +} + +impl TryFrom<&protocol::playlist4_external::Item> for SpotifyUri { + type Error = crate::Error; + fn try_from(item: &protocol::playlist4_external::Item) -> Result { + Self::from_uri(item.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 SpotifyUri { + type Error = crate::Error; + fn try_from(item: &protocol::playlist4_external::MetaItem) -> Result { + Ok(Self::Unknown { + kind: "MetaItem".into(), + id: SpotifyId::try_from(item.revision())?.to_base62()?, + }) + } +} + +// Note that this is the unique revision of a playlist, not the ID of that playlist. +impl TryFrom<&protocol::playlist4_external::SelectedListContent> for SpotifyUri { + type Error = crate::Error; + fn try_from( + playlist: &protocol::playlist4_external::SelectedListContent, + ) -> Result { + Ok(Self::Unknown { + kind: "SelectedListContent".into(), + id: SpotifyId::try_from(playlist.revision())?.to_base62()?, + }) + } +} + +// 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 SpotifyUri { + type Error = crate::Error; + fn try_from( + picture: &protocol::playlist_annotate3::TranscodedPicture, + ) -> Result { + Ok(Self::Unknown { + kind: "TranscodedPicture".into(), + id: picture.uri().to_owned(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct ConversionCase { + parsed: SpotifyUri, + uri: &'static str, + base62: &'static str, + } + + static CONV_VALID: [ConversionCase; 4] = [ + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { + id: 238762092608182713602505436543891614649, + }, + }, + uri: "spotify:track:5sWHDYs0csV6RS48xBl0tH", + base62: "5sWHDYs0csV6RS48xBl0tH", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:track:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ConversionCase { + parsed: SpotifyUri::Episode { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:episode:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ConversionCase { + parsed: SpotifyUri::Show { + id: SpotifyId { + id: 204841891221366092811751085145916697048, + }, + }, + uri: "spotify:show:4GNcXTGWmnZ3ySrqvol3o4", + base62: "4GNcXTGWmnZ3ySrqvol3o4", + }, + ]; + + static CONV_INVALID: [ConversionCase; 5] = [ + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Invalid ID in the URI. + uri: "spotify:track:5sWHDYs0Bl0tH", + base62: "!!!!!Ys0csV6RS48xBl0tH", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Missing colon between ID and type. + uri: "spotify:arbitrarywhatever5sWHDYs0csV6RS48xBl0tH", + base62: "....................", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Uri too short + uri: "spotify:track:aRS48xBl0tH", + // too long, should return error but not panic overflow + base62: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + // Uri too short + uri: "spotify:track:aRS48xBl0tH", + // too short to encode a 128 bits int + base62: "aa", + }, + ConversionCase { + parsed: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + uri: "cleary invalid uri", + // too high of a value, this would need a 132 bits int + base62: "ZZZZZZZZZZZZZZZZZZZZZZ", + }, + ]; + + struct ItemTypeCase { + uri: SpotifyUri, + expected_type: &'static str, + } + + static ITEM_TYPES: [ItemTypeCase; 6] = [ + ItemTypeCase { + uri: SpotifyUri::Album { + id: SpotifyId { id: 0 }, + }, + expected_type: "album", + }, + ItemTypeCase { + uri: SpotifyUri::Artist { + id: SpotifyId { id: 0 }, + }, + expected_type: "artist", + }, + ItemTypeCase { + uri: SpotifyUri::Episode { + id: SpotifyId { id: 0 }, + }, + expected_type: "episode", + }, + ItemTypeCase { + uri: SpotifyUri::Playlist { + user: None, + id: SpotifyId { id: 0 }, + }, + expected_type: "playlist", + }, + ItemTypeCase { + uri: SpotifyUri::Show { + id: SpotifyId { id: 0 }, + }, + expected_type: "show", + }, + ItemTypeCase { + uri: SpotifyUri::Track { + id: SpotifyId { id: 0 }, + }, + expected_type: "track", + }, + ]; + + #[test] + fn to_id() { + for c in &CONV_VALID { + assert_eq!(c.parsed.to_id().unwrap(), c.base62); + } + } + + #[test] + fn item_type() { + for i in &ITEM_TYPES { + assert_eq!(i.uri.item_type(), i.expected_type); + } + + // These need to use methods that can't be used in the static context like to_owned() and + // into(). + + let local_file = SpotifyUri::Local { + artist: "".to_owned(), + album_title: "".to_owned(), + track_title: "".to_owned(), + duration: Default::default(), + }; + + assert_eq!(local_file.item_type(), "local"); + + let unknown = SpotifyUri::Unknown { + kind: "not used".into(), + id: "".to_owned(), + }; + + assert_eq!(unknown.item_type(), "unknown"); + } + + #[test] + fn from_uri() { + for c in &CONV_VALID { + let actual = SpotifyUri::from_uri(c.uri).unwrap(); + + assert_eq!(actual, c.parsed); + } + + for c in &CONV_INVALID { + assert!(SpotifyUri::from_uri(c.uri).is_err()); + } + } + + #[test] + fn from_invalid_type_uri() { + let actual = + SpotifyUri::from_uri("spotify:arbitrarywhatever:5sWHDYs0csV6RS48xBl0tH").unwrap(); + + assert_eq!( + actual, + SpotifyUri::Unknown { + kind: "arbitrarywhatever".into(), + id: "5sWHDYs0csV6RS48xBl0tH".to_owned() + } + ) + } + + #[test] + fn from_local_uri() { + let actual = SpotifyUri::from_uri("spotify:local:xyz:123").unwrap(); + + assert_eq!( + actual, + SpotifyUri::Local { + artist: "unimplemented".to_owned(), + album_title: "unimplemented".to_owned(), + track_title: "unimplemented".to_owned(), + duration: Default::default(), + } + ); + } + + #[test] + fn from_named_uri() { + let actual = + SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap(); + + let SpotifyUri::Playlist { ref user, id } = actual else { + panic!("wrong id type"); + }; + + assert_eq!(*user, Some("spotify".to_owned())); + assert_eq!( + id, + SpotifyId { + id: 136159921382084734723401526672209703396 + }, + ); + } + + #[test] + fn to_uri() { + for c in &CONV_VALID { + assert_eq!(c.parsed.to_uri().unwrap(), c.uri); + } + } + + #[test] + fn to_named_uri() { + let string = "spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI"; + + let actual = + SpotifyUri::from_uri("spotify:user:spotify:playlist:37i9dQZF1DWSw8liJZcPOI").unwrap(); + + assert_eq!(actual.to_uri().unwrap(), string); + } +} diff --git a/examples/play.rs b/examples/play.rs index fa751cbb..32a86069 100644 --- a/examples/play.rs +++ b/examples/play.rs @@ -2,10 +2,8 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, - config::SessionConfig, - session::Session, - spotify_id::{SpotifyId, SpotifyItemType}, + SpotifyUri, authentication::Credentials, config::SessionConfig, session::Session, + spotify_id::SpotifyId, }, playback::{ audio_backend, @@ -28,8 +26,9 @@ async fn main() { } let credentials = Credentials::with_access_token(&args[1]); - let mut track = SpotifyId::from_base62(&args[2]).unwrap(); - track.item_type = SpotifyItemType::Track; + let track = SpotifyUri::Track { + id: SpotifyId::from_base62(&args[2]).unwrap(), + }; let backend = audio_backend::find(None).unwrap(); diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 1d6a4266..a1b5cad5 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -2,7 +2,8 @@ use std::{env, process::exit}; use librespot::{ core::{ - authentication::Credentials, config::SessionConfig, session::Session, spotify_id::SpotifyId, + authentication::Credentials, config::SessionConfig, session::Session, + spotify_uri::SpotifyUri, }, metadata::{Metadata, Playlist, Track}, }; @@ -19,7 +20,7 @@ async fn main() { } let credentials = Credentials::with_access_token(&args[1]); - let plist_uri = SpotifyId::from_uri(&args[2]).unwrap_or_else(|_| { + let plist_uri = SpotifyUri::from_uri(&args[2]).unwrap_or_else(|_| { eprintln!( "PLAYLIST should be a playlist URI such as: \ \"spotify:playlist:37i9dQZF1DXec50AjHrNTq\"" diff --git a/metadata/src/album.rs b/metadata/src/album.rs index 9be9364c..b1b26468 100644 --- a/metadata/src/album.rs +++ b/metadata/src/album.rs @@ -17,7 +17,7 @@ use crate::{ util::{impl_deref_wrapped, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; use protocol::metadata::Disc as DiscMessage; @@ -25,7 +25,7 @@ pub use protocol::metadata::album::Type as AlbumType; #[derive(Debug, Clone)] pub struct Album { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub artists: Artists, pub album_type: AlbumType, @@ -48,9 +48,9 @@ pub struct Album { } #[derive(Debug, Clone, Default)] -pub struct Albums(pub Vec); +pub struct Albums(pub Vec); -impl_deref_wrapped!(Albums, Vec); +impl_deref_wrapped!(Albums, Vec); #[derive(Debug, Clone)] pub struct Disc { @@ -65,7 +65,7 @@ pub struct Discs(pub Vec); impl_deref_wrapped!(Discs, Vec); impl Album { - pub fn tracks(&self) -> impl Iterator { + pub fn tracks(&self) -> impl Iterator { self.discs.iter().flat_map(|disc| disc.tracks.iter()) } } @@ -74,11 +74,15 @@ impl Album { impl Metadata for Album { type Message = protocol::metadata::Album; - async fn request(session: &Session, album_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, album_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Album { id: album_id } = album_uri else { + return Err(Error::invalid_argument("album_uri")); + }; + session.spclient().get_album_metadata(album_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/metadata/src/artist.rs b/metadata/src/artist.rs index e875a985..5f443719 100644 --- a/metadata/src/artist.rs +++ b/metadata/src/artist.rs @@ -16,7 +16,7 @@ use crate::{ util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::metadata::artist_with_role::ArtistRole; @@ -29,7 +29,7 @@ use protocol::metadata::TopTracks as TopTracksMessage; #[derive(Debug, Clone)] pub struct Artist { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub popularity: i32, pub top_tracks: CountryTopTracks, @@ -56,7 +56,7 @@ impl_deref_wrapped!(Artists, Vec); #[derive(Debug, Clone)] pub struct ArtistWithRole { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub role: ArtistRole, } @@ -140,14 +140,14 @@ impl Artist { /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn albums_current(&self) -> impl Iterator { + pub fn albums_current(&self) -> impl Iterator { self.albums.current_releases() } /// Get the full list of singles, not containing duplicate variants of the same singles. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn singles_current(&self) -> impl Iterator { + pub fn singles_current(&self) -> impl Iterator { self.singles.current_releases() } @@ -155,14 +155,14 @@ impl Artist { /// compilations. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn compilations_current(&self) -> impl Iterator { + pub fn compilations_current(&self) -> impl Iterator { self.compilations.current_releases() } /// Get the full list of albums, not containing duplicate variants of the same albums. /// /// See also [`AlbumGroups`](struct@AlbumGroups) and [`AlbumGroups::current_releases`] - pub fn appears_on_albums_current(&self) -> impl Iterator { + pub fn appears_on_albums_current(&self) -> impl Iterator { self.appears_on_albums.current_releases() } } @@ -171,11 +171,15 @@ impl Artist { impl Metadata for Artist { type Message = protocol::metadata::Artist; - async fn request(session: &Session, artist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, artist_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Artist { id: artist_id } = artist_uri else { + return Err(Error::invalid_argument("artist_uri")); + }; + session.spclient().get_artist_metadata(artist_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } @@ -249,7 +253,7 @@ impl AlbumGroups { /// Get the contained albums. This will only use the latest release / variant of an album if /// multiple variants are available. This should be used if multiple variants of the same album /// are not explicitely desired. - pub fn current_releases(&self) -> impl Iterator { + pub fn current_releases(&self) -> impl Iterator { self.iter().filter_map(|agrp| agrp.first()) } } diff --git a/metadata/src/audio/item.rs b/metadata/src/audio/item.rs index d398c8a0..3df63d9e 100644 --- a/metadata/src/audio/item.rs +++ b/metadata/src/audio/item.rs @@ -13,9 +13,7 @@ use crate::{ use super::file::AudioFiles; -use librespot_core::{ - Error, Session, SpotifyId, date::Date, session::UserData, spotify_id::SpotifyItemType, -}; +use librespot_core::{Error, Session, SpotifyUri, date::Date, session::UserData}; pub type AudioItemResult = Result; @@ -29,7 +27,7 @@ pub struct CoverImage { #[derive(Debug, Clone)] pub struct AudioItem { - pub track_id: SpotifyId, + pub track_id: SpotifyUri, pub uri: String, pub files: AudioFiles, pub name: String, @@ -60,14 +58,14 @@ pub enum UniqueFields { } impl AudioItem { - pub async fn get_file(session: &Session, id: SpotifyId) -> AudioItemResult { + pub async fn get_file(session: &Session, uri: SpotifyUri) -> AudioItemResult { let image_url = session .get_user_attribute("image-url") .unwrap_or_else(|| String::from("https://i.scdn.co/image/{file_id}")); - match id.item_type { - SpotifyItemType::Track => { - let track = Track::get(session, &id).await?; + match uri { + SpotifyUri::Track { .. } => { + let track = Track::get(session, &uri).await?; if track.duration <= 0 { return Err(Error::unavailable(MetadataError::InvalidDuration( @@ -79,8 +77,7 @@ impl AudioItem { return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); } - let track_id = track.id; - let uri = track_id.to_uri()?; + let uri_string = uri.to_uri()?; let album = track.album.name; let album_artists = track @@ -123,8 +120,8 @@ impl AudioItem { }; Ok(Self { - track_id, - uri, + track_id: uri, + uri: uri_string, files: track.files, name: track.name, covers, @@ -136,8 +133,8 @@ impl AudioItem { unique_fields, }) } - SpotifyItemType::Episode => { - let episode = Episode::get(session, &id).await?; + SpotifyUri::Episode { .. } => { + let episode = Episode::get(session, &uri).await?; if episode.duration <= 0 { return Err(Error::unavailable(MetadataError::InvalidDuration( @@ -149,8 +146,7 @@ impl AudioItem { return Err(Error::unavailable(MetadataError::ExplicitContentFiltered)); } - let track_id = episode.id; - let uri = track_id.to_uri()?; + let uri_string = uri.to_uri()?; let covers = get_covers(episode.covers, image_url); @@ -167,8 +163,8 @@ impl AudioItem { }; Ok(Self { - track_id, - uri, + track_id: uri, + uri: uri_string, files: episode.audio, name: episode.name, covers, diff --git a/metadata/src/episode.rs b/metadata/src/episode.rs index 4ba0a0da..847e8941 100644 --- a/metadata/src/episode.rs +++ b/metadata/src/episode.rs @@ -15,14 +15,14 @@ use crate::{ video::VideoFiles, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; pub use protocol::metadata::episode::EpisodeType; #[derive(Debug, Clone)] pub struct Episode { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub duration: i32, pub audio: AudioFiles, @@ -49,19 +49,23 @@ pub struct Episode { } #[derive(Debug, Clone, Default)] -pub struct Episodes(pub Vec); +pub struct Episodes(pub Vec); -impl_deref_wrapped!(Episodes, Vec); +impl_deref_wrapped!(Episodes, Vec); #[async_trait] impl Metadata for Episode { type Message = protocol::metadata::Episode; - async fn request(session: &Session, episode_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, episode_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Episode { id: episode_id } = episode_uri else { + return Err(Error::invalid_argument("episode_uri")); + }; + session.spclient().get_episode_metadata(episode_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/metadata/src/image.rs b/metadata/src/image.rs index 30a1f4ed..4d201218 100644 --- a/metadata/src/image.rs +++ b/metadata/src/image.rs @@ -5,7 +5,7 @@ use std::{ use crate::util::{impl_deref_wrapped, impl_from_repeated, impl_try_from_repeated}; -use librespot_core::{FileId, SpotifyId}; +use librespot_core::{FileId, SpotifyUri}; use librespot_protocol as protocol; use protocol::metadata::Image as ImageMessage; @@ -47,7 +47,7 @@ impl_deref_wrapped!(PictureSizes, Vec); #[derive(Debug, Clone)] pub struct TranscodedPicture { pub target_name: String, - pub uri: SpotifyId, + pub uri: SpotifyUri, } #[derive(Debug, Clone)] diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 1bb5b9f3..b097f98c 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -6,7 +6,7 @@ extern crate async_trait; use protobuf::Message; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; pub mod album; pub mod artist; @@ -44,15 +44,15 @@ pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message + std::fmt::Debug; // Request a protobuf - async fn request(session: &Session, id: &SpotifyId) -> RequestResult; + async fn request(session: &Session, id: &SpotifyUri) -> RequestResult; // Request a metadata struct - async fn get(session: &Session, id: &SpotifyId) -> Result { + async fn get(session: &Session, id: &SpotifyUri) -> 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, _: &SpotifyUri) -> Result; } diff --git a/metadata/src/playlist/annotation.rs b/metadata/src/playlist/annotation.rs index bd703ee2..b11d34da 100644 --- a/metadata/src/playlist/annotation.rs +++ b/metadata/src/playlist/annotation.rs @@ -8,8 +8,7 @@ use crate::{ request::{MercuryRequest, RequestResult}, }; -use librespot_core::{Error, Session, SpotifyId}; - +use librespot_core::{Error, Session, SpotifyId, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::playlist_annotate3::AbuseReportState; @@ -26,12 +25,20 @@ pub struct PlaylistAnnotation { impl Metadata for PlaylistAnnotation { type Message = protocol::playlist_annotate3::PlaylistAnnotation; - async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult { let current_user = session.username(); + + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + Self::request_for_user(session, ¤t_user, playlist_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Ok(Self { description: msg.description().to_owned(), picture: msg.picture().to_owned(), // TODO: is this a URL or Spotify URI? @@ -60,11 +67,18 @@ impl PlaylistAnnotation { async fn get_for_user( session: &Session, username: &str, - playlist_id: &SpotifyId, + playlist_uri: &SpotifyUri, ) -> Result { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + let response = Self::request_for_user(session, username, playlist_id).await?; let msg = ::Message::parse_from_bytes(&response)?; - Self::parse(&msg, playlist_id) + Self::parse(&msg, playlist_uri) } } diff --git a/metadata/src/playlist/item.rs b/metadata/src/playlist/item.rs index ce03f0de..1746857b 100644 --- a/metadata/src/playlist/item.rs +++ b/metadata/src/playlist/item.rs @@ -10,7 +10,7 @@ use super::{ permission::Capabilities, }; -use librespot_core::{SpotifyId, date::Date}; +use librespot_core::{SpotifyUri, date::Date}; use librespot_protocol as protocol; use protocol::playlist4_external::Item as PlaylistItemMessage; @@ -19,7 +19,7 @@ use protocol::playlist4_external::MetaItem as PlaylistMetaItemMessage; #[derive(Debug, Clone)] pub struct PlaylistItem { - pub id: SpotifyId, + pub id: SpotifyUri, pub attributes: PlaylistItemAttributes, } @@ -38,7 +38,7 @@ pub struct PlaylistItemList { #[derive(Debug, Clone)] pub struct PlaylistMetaItem { - pub revision: SpotifyId, + pub revision: SpotifyUri, pub attributes: PlaylistAttributes, pub length: i32, pub timestamp: Date, diff --git a/metadata/src/playlist/list.rs b/metadata/src/playlist/list.rs index 49ff1188..1052afd8 100644 --- a/metadata/src/playlist/list.rs +++ b/metadata/src/playlist/list.rs @@ -14,12 +14,7 @@ use super::{ permission::Capabilities, }; -use librespot_core::{ - Error, Session, - date::Date, - spotify_id::{NamedSpotifyId, SpotifyId}, -}; - +use librespot_core::{Error, Session, SpotifyUri, date::Date, spotify_id::SpotifyId}; use librespot_protocol as protocol; use protocol::playlist4_external::GeoblockBlockingType as Geoblock; @@ -30,7 +25,7 @@ impl_deref_wrapped!(Geoblocks, Vec); #[derive(Debug, Clone)] pub struct Playlist { - pub id: NamedSpotifyId, + pub id: SpotifyUri, pub revision: Vec, pub length: i32, pub attributes: PlaylistAttributes, @@ -72,7 +67,7 @@ pub struct SelectedListContent { } impl Playlist { - pub fn tracks(&self) -> impl ExactSizeIterator { + pub fn tracks(&self) -> impl ExactSizeIterator { let tracks = self.contents.items.iter().map(|item| &item.id); let length = tracks.len(); @@ -93,17 +88,35 @@ impl Playlist { impl Metadata for Playlist { type Message = protocol::playlist4_external::SelectedListContent; - async fn request(session: &Session, playlist_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, playlist_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = playlist_uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + session.spclient().get_playlist(playlist_id).await } - fn parse(msg: &Self::Message, id: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, uri: &SpotifyUri) -> Result { + let SpotifyUri::Playlist { + id: playlist_id, .. + } = uri + else { + return Err(Error::invalid_argument("playlist_uri")); + }; + // 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); + + let new_uri = SpotifyUri::Playlist { + id: *playlist_id, + user: Some(playlist.owner_username), + }; Ok(Self { - id, + id: new_uri, revision: playlist.revision, length: playlist.length, attributes: playlist.attributes, diff --git a/metadata/src/show.rs b/metadata/src/show.rs index b326c652..01a55c2d 100644 --- a/metadata/src/show.rs +++ b/metadata/src/show.rs @@ -5,7 +5,7 @@ use crate::{ episode::Episodes, image::Images, restriction::Restrictions, }; -use librespot_core::{Error, Session, SpotifyId}; +use librespot_core::{Error, Session, SpotifyUri}; use librespot_protocol as protocol; pub use protocol::metadata::show::ConsumptionOrder as ShowConsumptionOrder; @@ -13,7 +13,7 @@ pub use protocol::metadata::show::MediaType as ShowMediaType; #[derive(Debug, Clone)] pub struct Show { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub description: String, pub publisher: String, @@ -27,7 +27,7 @@ pub struct Show { pub media_type: ShowMediaType, pub consumption_order: ShowConsumptionOrder, pub availability: Availabilities, - pub trailer_uri: Option, + pub trailer_uri: Option, pub has_music_and_talk: bool, pub is_audiobook: bool, } @@ -36,11 +36,15 @@ pub struct Show { impl Metadata for Show { type Message = protocol::metadata::Show; - async fn request(session: &Session, show_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, show_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Show { id: show_id } = show_uri else { + return Err(Error::invalid_argument("show_uri")); + }; + session.spclient().get_show_metadata(show_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } @@ -67,7 +71,7 @@ impl TryFrom<&::Message> for Show { .trailer_uri .as_deref() .filter(|s| !s.is_empty()) - .map(SpotifyId::from_uri) + .map(SpotifyUri::from_uri) .transpose()?, has_music_and_talk: show.music_and_talk(), is_audiobook: show.is_audiobook(), diff --git a/metadata/src/track.rs b/metadata/src/track.rs index 78ea5481..5893ca15 100644 --- a/metadata/src/track.rs +++ b/metadata/src/track.rs @@ -17,12 +17,12 @@ use crate::{ util::{impl_deref_wrapped, impl_try_from_repeated}, }; -use librespot_core::{Error, Session, SpotifyId, date::Date}; +use librespot_core::{Error, Session, SpotifyUri, date::Date}; use librespot_protocol as protocol; #[derive(Debug, Clone)] pub struct Track { - pub id: SpotifyId, + pub id: SpotifyUri, pub name: String, pub album: Album, pub artists: Artists, @@ -50,19 +50,23 @@ pub struct Track { } #[derive(Debug, Clone, Default)] -pub struct Tracks(pub Vec); +pub struct Tracks(pub Vec); -impl_deref_wrapped!(Tracks, Vec); +impl_deref_wrapped!(Tracks, Vec); #[async_trait] impl Metadata for Track { type Message = protocol::metadata::Track; - async fn request(session: &Session, track_id: &SpotifyId) -> RequestResult { + async fn request(session: &Session, track_uri: &SpotifyUri) -> RequestResult { + let SpotifyUri::Track { id: track_id } = track_uri else { + return Err(Error::invalid_argument("track_uri")); + }; + session.spclient().get_track_metadata(track_id).await } - fn parse(msg: &Self::Message, _: &SpotifyId) -> Result { + fn parse(msg: &Self::Message, _: &SpotifyUri) -> Result { Self::try_from(msg) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index ba2e5a4c..886c9cf3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -15,27 +15,26 @@ use std::{ time::{Duration, Instant}, }; -use futures_util::{ - StreamExt, TryFutureExt, future, future::FusedFuture, - stream::futures_unordered::FuturesUnordered, -}; -use parking_lot::Mutex; -use symphonia::core::io::MediaSource; -use tokio::sync::{mpsc, oneshot}; - +#[cfg(feature = "passthrough-decoder")] +use crate::decoder::PassthroughDecoder; use crate::{ audio::{AudioDecrypt, AudioFetchParams, AudioFile, StreamLoaderController}, audio_backend::Sink, config::{Bitrate, NormalisationMethod, NormalisationType, PlayerConfig}, convert::Converter, - core::{Error, Session, SpotifyId, util::SeqGenerator}, + core::{Error, Session, SpotifyId, SpotifyUri, util::SeqGenerator}, decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder}, metadata::audio::{AudioFileFormat, AudioFiles, AudioItem}, mixer::VolumeGetter, }; - -#[cfg(feature = "passthrough-decoder")] -use crate::decoder::PassthroughDecoder; +use futures_util::{ + StreamExt, TryFutureExt, future, future::FusedFuture, + stream::futures_unordered::FuturesUnordered, +}; +use librespot_metadata::track::Tracks; +use parking_lot::Mutex; +use symphonia::core::io::MediaSource; +use tokio::sync::{mpsc, oneshot}; use crate::SAMPLES_PER_SECOND; @@ -94,12 +93,12 @@ static PLAYER_COUNTER: AtomicUsize = AtomicUsize::new(0); enum PlayerCommand { Load { - track_id: SpotifyId, + track_id: SpotifyUri, play: bool, position_ms: u32, }, Preload { - track_id: SpotifyId, + track_id: SpotifyUri, }, Play, Pause, @@ -142,17 +141,17 @@ pub enum PlayerEvent { // Fired when the player is stopped (e.g. by issuing a "stop" command to the player). Stopped { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player is delayed by loading a track. Loading { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player is preloading a track. Preloading { - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player is playing a track. // This event is issued at the start of playback of whenever the position must be communicated @@ -163,31 +162,31 @@ pub enum PlayerEvent { // after a buffer-underrun Playing { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player entered a paused state. Paused { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, // The player thinks it's a good idea to issue a preload command for the next track now. // This event is intended for use within spirc. TimeToPreloadNextTrack { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player reached the end of a track. // This event is intended for use within spirc. Spirc will respond by issuing another command. EndOfTrack { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The player was unable to load the requested track. Unavailable { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, }, // The mixer volume was set to a new level. VolumeChanged { @@ -195,7 +194,7 @@ pub enum PlayerEvent { }, PositionCorrection { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, /// Requires `PlayerConfig::position_update_interval` to be set to Some. @@ -203,12 +202,12 @@ pub enum PlayerEvent { /// current playback position PositionChanged { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, Seeked { play_request_id: u64, - track_id: SpotifyId, + track_id: SpotifyUri, position_ms: u32, }, TrackChanged { @@ -526,7 +525,7 @@ impl Player { } } - pub fn load(&self, track_id: SpotifyId, start_playing: bool, position_ms: u32) { + pub fn load(&self, track_id: SpotifyUri, start_playing: bool, position_ms: u32) { self.command(PlayerCommand::Load { track_id, play: start_playing, @@ -534,7 +533,7 @@ impl Player { }); } - pub fn preload(&self, track_id: SpotifyId) { + pub fn preload(&self, track_id: SpotifyUri) { self.command(PlayerCommand::Preload { track_id }); } @@ -660,11 +659,11 @@ struct PlayerLoadedTrackData { enum PlayerPreload { None, Loading { - track_id: SpotifyId, + track_id: SpotifyUri, loader: Pin> + Send>>, }, Ready { - track_id: SpotifyId, + track_id: SpotifyUri, loaded_track: Box, }, } @@ -674,13 +673,13 @@ type Decoder = Box; enum PlayerState { Stopped, Loading { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, start_playback: bool, loader: Pin> + Send>>, }, Paused { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, decoder: Decoder, audio_item: AudioItem, @@ -694,7 +693,7 @@ enum PlayerState { is_explicit: bool, }, Playing { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, decoder: Decoder, normalisation_data: NormalisationData, @@ -709,7 +708,7 @@ enum PlayerState { is_explicit: bool, }, EndOfTrack { - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, loaded_track: PlayerLoadedTrackData, }, @@ -893,10 +892,12 @@ impl PlayerTrackLoader { None } else if !audio_item.files.is_empty() { Some(audio_item) - } else if let Some(alternatives) = &audio_item.alternatives { - let alternatives: FuturesUnordered<_> = alternatives - .iter() - .map(|alt_id| AudioItem::get_file(&self.session, *alt_id)) + } else if let Some(alternatives) = audio_item.alternatives { + let Tracks(alternatives_vec) = alternatives; // required to make `into_iter` able to move + + let alternatives: FuturesUnordered<_> = alternatives_vec + .into_iter() + .map(|alt_id| AudioItem::get_file(&self.session, alt_id)) .collect(); alternatives @@ -938,16 +939,40 @@ impl PlayerTrackLoader { async fn load_track( &self, - spotify_id: SpotifyId, + track_uri: SpotifyUri, position_ms: u32, ) -> Option { - let audio_item = match AudioItem::get_file(&self.session, spotify_id).await { + match track_uri { + SpotifyUri::Track { .. } | SpotifyUri::Episode { .. } => { + self.load_remote_track(track_uri, position_ms).await + } + _ => { + error!("Cannot handle load of track with URI: <{track_uri}>",); + None + } + } + } + + async fn load_remote_track( + &self, + track_uri: SpotifyUri, + position_ms: u32, + ) -> Option { + let track_id: SpotifyId = match (&track_uri).try_into() { + Ok(id) => id, + Err(_) => { + warn!("<{track_uri}> could not be converted to a base62 ID"); + return None; + } + }; + + let audio_item = match AudioItem::get_file(&self.session, track_uri).await { Ok(audio) => match self.find_available_alternative(audio).await { Some(audio) => audio, None => { warn!( - "<{}> is not available", - spotify_id.to_uri().unwrap_or_default() + "spotify:track:<{}> is not available", + track_id.to_base62().unwrap_or_default() ); return None; } @@ -1033,13 +1058,14 @@ impl PlayerTrackLoader { // 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 { + let key = match self.session.audio_key().request(track_id, file_id).await { Ok(key) => Some(key), Err(e) => { warn!("Unable to load key, continuing without decryption: {e}"); None } }; + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); let is_ogg_vorbis = AudioFiles::is_ogg_vorbis(format); @@ -1195,13 +1221,15 @@ impl Future for PlayerInternal { // Handle loading of a new track to play if let PlayerState::Loading { ref mut loader, - track_id, + ref track_id, start_playback, play_request_id, } = self.state { // The loader may be terminated if we are trying to load the same track // as before, and that track failed to open before. + let track_id = track_id.clone(); + if !loader.as_mut().is_terminated() { match loader.as_mut().poll(cx) { Poll::Ready(Ok(loaded_track)) => { @@ -1233,12 +1261,15 @@ impl Future for PlayerInternal { // handle pending preload requests. if let PlayerPreload::Loading { ref mut loader, - track_id, + ref track_id, } = self.preload { + let track_id = track_id.clone(); match loader.as_mut().poll(cx) { Poll::Ready(Ok(loaded_track)) => { - self.send_event(PlayerEvent::Preloading { track_id }); + self.send_event(PlayerEvent::Preloading { + track_id: track_id.clone(), + }); self.preload = PlayerPreload::Ready { track_id, loaded_track: Box::new(loaded_track), @@ -1269,7 +1300,7 @@ impl Future for PlayerInternal { self.ensure_sink_running(); if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, ref mut decoder, normalisation_factor, @@ -1278,6 +1309,7 @@ impl Future for PlayerInternal { .. } = self.state { + let track_id = track_id.clone(); match decoder.next_packet() { Ok(result) => { if let Some((ref packet_position, ref packet)) = result { @@ -1338,7 +1370,7 @@ impl Future for PlayerInternal { now.checked_sub(new_stream_position); self.send_event(PlayerEvent::PositionCorrection { play_request_id, - track_id, + track_id: track_id.clone(), position_ms: new_stream_position_ms, }); } @@ -1391,7 +1423,7 @@ impl Future for PlayerInternal { } if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, duration_ms, stream_position_ms, @@ -1400,7 +1432,7 @@ impl Future for PlayerInternal { .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, duration_ms, stream_position_ms, @@ -1409,6 +1441,8 @@ impl Future for PlayerInternal { .. } = self.state { + let track_id = track_id.clone(); + if (!*suggested_to_preload_next_track) && ((duration_ms as i64 - stream_position_ms as i64) < PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS as i64) @@ -1482,25 +1516,27 @@ impl PlayerInternal { fn handle_player_stop(&mut self) { match self.state { PlayerState::Playing { - track_id, + ref track_id, play_request_id, .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, .. } | PlayerState::EndOfTrack { - track_id, + ref track_id, play_request_id, .. } | PlayerState::Loading { - track_id, + ref track_id, play_request_id, .. } => { + let track_id = track_id.clone(); + self.ensure_sink_stopped(false); self.send_event(PlayerEvent::Stopped { track_id, @@ -1519,11 +1555,13 @@ impl PlayerInternal { fn handle_play(&mut self) { match self.state { PlayerState::Paused { - track_id, + ref track_id, play_request_id, stream_position_ms, .. } => { + let track_id = track_id.clone(); + self.state.paused_to_playing(); self.send_event(PlayerEvent::Playing { track_id, @@ -1546,11 +1584,13 @@ impl PlayerInternal { match self.state { PlayerState::Paused { .. } => self.ensure_sink_stopped(false), PlayerState::Playing { - track_id, + ref track_id, play_request_id, stream_position_ms, .. } => { + let track_id = track_id.clone(); + self.state.playing_to_paused(); self.ensure_sink_stopped(false); @@ -1681,13 +1721,13 @@ impl PlayerInternal { None => { self.state.playing_to_end_of_track(); if let PlayerState::EndOfTrack { - track_id, + ref track_id, play_request_id, .. } = self.state { self.send_event(PlayerEvent::EndOfTrack { - track_id, + track_id: track_id.clone(), play_request_id, }) } else { @@ -1700,7 +1740,7 @@ impl PlayerInternal { fn start_playback( &mut self, - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id: u64, loaded_track: PlayerLoadedTrackData, start_playback: bool, @@ -1725,7 +1765,7 @@ impl PlayerInternal { if start_playback { self.ensure_sink_running(); self.send_event(PlayerEvent::Playing { - track_id, + track_id: track_id.clone(), play_request_id, position_ms, }); @@ -1750,7 +1790,7 @@ impl PlayerInternal { self.ensure_sink_stopped(false); self.state = PlayerState::Paused { - track_id, + track_id: track_id.clone(), play_request_id, decoder: loaded_track.decoder, audio_item: loaded_track.audio_item, @@ -1774,7 +1814,7 @@ impl PlayerInternal { fn handle_command_load( &mut self, - track_id: SpotifyId, + track_id: SpotifyUri, play_request_id_option: Option, play: bool, position_ms: u32, @@ -1803,9 +1843,9 @@ impl PlayerInternal { if let PlayerState::EndOfTrack { track_id: previous_track_id, .. - } = self.state + } = &self.state { - if previous_track_id == track_id { + if *previous_track_id == track_id { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, _ => { @@ -1834,19 +1874,19 @@ 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, + track_id: ref current_track_id, ref mut stream_position_ms, ref mut decoder, .. } | PlayerState::Paused { - track_id: current_track_id, + track_id: ref current_track_id, ref mut stream_position_ms, ref mut decoder, .. } = self.state { - if current_track_id == track_id { + 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 { // This may be blocking. @@ -1915,9 +1955,9 @@ impl PlayerInternal { if let PlayerPreload::Ready { track_id: loaded_track_id, .. - } = self.preload + } = &self.preload { - if track_id == loaded_track_id { + if track_id == *loaded_track_id { let preload = std::mem::replace(&mut self.preload, PlayerPreload::None); if let PlayerPreload::Ready { track_id, @@ -1940,7 +1980,7 @@ impl PlayerInternal { } self.send_event(PlayerEvent::Loading { - track_id, + track_id: track_id.clone(), play_request_id, position_ms, }); @@ -1949,9 +1989,9 @@ impl PlayerInternal { let loader = if let PlayerPreload::Loading { track_id: loaded_track_id, .. - } = self.preload + } = &self.preload { - if (track_id == loaded_track_id) && (position_ms == 0) { + if (track_id == *loaded_track_id) && (position_ms == 0) { let mut preload = PlayerPreload::None; std::mem::swap(&mut preload, &mut self.preload); if let PlayerPreload::Loading { loader, .. } = preload { @@ -1969,7 +2009,8 @@ impl PlayerInternal { self.preload = PlayerPreload::None; // If we don't have a loader yet, create one from scratch. - let loader = loader.unwrap_or_else(|| Box::pin(self.load_track(track_id, position_ms))); + let loader = + loader.unwrap_or_else(|| Box::pin(self.load_track(track_id.clone(), position_ms))); // Set ourselves to a loading state. self.state = PlayerState::Loading { @@ -1982,7 +2023,7 @@ impl PlayerInternal { Ok(()) } - fn handle_command_preload(&mut self, track_id: SpotifyId) { + fn handle_command_preload(&mut self, track_id: SpotifyUri) { debug!("Preloading track"); let mut preload_track = true; // check whether the track is already loaded somewhere or being loaded. @@ -1993,9 +2034,9 @@ impl PlayerInternal { | PlayerPreload::Ready { track_id: currently_loading, .. - } = self.preload + } = &self.preload { - if currently_loading == track_id { + if *currently_loading == track_id { // we're already preloading the requested track. preload_track = false; } else { @@ -2015,9 +2056,9 @@ impl PlayerInternal { | PlayerState::EndOfTrack { track_id: current_track_id, .. - } = self.state + } = &self.state { - if current_track_id == track_id { + if *current_track_id == track_id { // we already have the requested track loaded. preload_track = false; } @@ -2025,7 +2066,7 @@ impl PlayerInternal { // schedule the preload of the current track if desired. if preload_track { - let loader = self.load_track(track_id, 0); + let loader = self.load_track(track_id.clone(), 0); self.preload = PlayerPreload::Loading { track_id, loader: Box::pin(loader), @@ -2039,14 +2080,14 @@ impl PlayerInternal { // that. In this case just restart the loading process but // with the requested position. if let PlayerState::Loading { - track_id, + ref track_id, play_request_id, start_playback, .. } = self.state { return self.handle_command_load( - track_id, + track_id.clone(), Some(play_request_id), start_playback, position_ms, @@ -2058,13 +2099,13 @@ impl PlayerInternal { Ok(new_position_ms) => { if let PlayerState::Playing { ref mut stream_position_ms, - track_id, + ref track_id, play_request_id, .. } | PlayerState::Paused { ref mut stream_position_ms, - track_id, + ref track_id, play_request_id, .. } = self.state @@ -2073,7 +2114,7 @@ impl PlayerInternal { self.send_event(PlayerEvent::Seeked { play_request_id, - track_id, + track_id: track_id.clone(), position_ms: new_position_ms, }); } @@ -2177,18 +2218,20 @@ impl PlayerInternal { if filter { if let PlayerState::Playing { - track_id, + ref track_id, play_request_id, is_explicit, .. } | PlayerState::Paused { - track_id, + ref track_id, play_request_id, is_explicit, .. } = self.state { + let track_id = track_id.clone(); + if is_explicit { warn!( "Currently loaded track is explicit, which client setting forbids -- skipping to next track." @@ -2213,7 +2256,7 @@ impl PlayerInternal { fn load_track( &mut self, - spotify_id: SpotifyId, + spotify_uri: SpotifyUri, position_ms: u32, ) -> impl FusedFuture> + Send + 'static { // This method creates a future that returns the loaded stream and associated info. @@ -2231,8 +2274,9 @@ impl PlayerInternal { let load_handles_clone = self.load_handles.clone(); let handle = tokio::runtime::Handle::current(); + let load_handle = thread::spawn(move || { - let data = handle.block_on(loader.load_track(spotify_id, position_ms)); + let data = handle.block_on(loader.load_track(spotify_uri, position_ms)); if let Some(data) = data { let _ = result_tx.send(data); } @@ -2376,7 +2420,7 @@ impl fmt::Debug for PlayerCommand { impl fmt::Debug for PlayerState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use PlayerState::*; - match *self { + match self { Stopped => f.debug_struct("Stopped").finish(), Loading { track_id, diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 36695c99..51495932 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -28,7 +28,7 @@ impl EventHandler { env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string()); } PlayerEvent::TrackChanged { audio_item } => { - match audio_item.track_id.to_base62() { + match audio_item.track_id.to_id() { Err(e) => { warn!("PlayerEvent::TrackChanged: Invalid track id: {e}") } @@ -104,7 +104,7 @@ impl EventHandler { } } } - PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + PlayerEvent::Stopped { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Stopped: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "stopped".to_string()); @@ -115,7 +115,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Playing: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "playing".to_string()); @@ -127,7 +127,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Paused: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "paused".to_string()); @@ -135,26 +135,24 @@ impl EventHandler { env_vars.insert("POSITION_MS", position_ms.to_string()); } }, - PlayerEvent::Loading { track_id, .. } => match track_id.to_base62() { + PlayerEvent::Loading { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Loading: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "loading".to_string()); env_vars.insert("TRACK_ID", id); } }, - PlayerEvent::Preloading { track_id, .. } => { - match track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::Preloading: Invalid track id: {e}") - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "preloading".to_string()); - env_vars.insert("TRACK_ID", id); - } + PlayerEvent::Preloading { track_id, .. } => match track_id.to_id() { + Err(e) => { + warn!("PlayerEvent::Preloading: Invalid track id: {e}") } - } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => { - match track_id.to_base62() { + match track_id.to_id() { Err(e) => warn!( "PlayerEvent::TimeToPreloadNextTrack: Invalid track id: {e}" ), @@ -164,19 +162,16 @@ impl EventHandler { } } } - PlayerEvent::EndOfTrack { track_id, .. } => { - match track_id.to_base62() { - Err(e) => { - warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}") - } - Ok(id) => { - env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); - env_vars.insert("TRACK_ID", id); - } + PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_id() { + Err(e) => { + warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}") } - } - PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() - { + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "end_of_track".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Unavailable { track_id, .. } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "unavailable".to_string()); @@ -191,7 +186,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => warn!("PlayerEvent::Seeked: Invalid track id: {e}"), Ok(id) => { env_vars.insert("PLAYER_EVENT", "seeked".to_string()); @@ -203,7 +198,7 @@ impl EventHandler { track_id, position_ms, .. - } => match track_id.to_base62() { + } => match track_id.to_id() { Err(e) => { warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}") } From 6f6cd04874b09736bd17879363d1c0a387bc9440 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 21 Sep 2025 22:43:50 +0200 Subject: [PATCH 558/561] refactor: remove parking_lot dependency and refine feature selections (#1543) - refine dependency features and versions in Cargo.toml files - switch from parking_lot to std sync primitives - remove dashmap dependency and use DefaultKeyedStateStore - update crates Replace parking_lot with std::sync::{Mutex, RwLock, Condvar} throughout the codebase. Update dependencies and code to use poisoning-aware locks, adding explicit panic messages where necessary. Update governor to use DashMapStateStore for rate limiting. --- Cargo.lock | 514 +++++++++++++----------- Cargo.toml | 1 - audio/Cargo.toml | 5 +- audio/src/fetch/mod.rs | 42 +- audio/src/fetch/receive.rs | 35 +- connect/Cargo.toml | 8 +- core/Cargo.toml | 20 +- core/src/cache.rs | 22 +- core/src/component.rs | 9 +- core/src/dealer/mod.rs | 54 ++- core/src/http_client.rs | 9 +- core/src/session.rs | 197 +++++++-- discovery/Cargo.toml | 8 +- playback/Cargo.toml | 14 +- playback/src/audio_backend/gstreamer.rs | 29 +- playback/src/player.rs | 10 +- 16 files changed, 637 insertions(+), 340 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1461aed8..0ad27773 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" dependencies = [ "alsa-sys", - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -62,7 +62,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c88dbbce13b232b26250e1e2e6ac18b6a891a646b8148285036ebce260ac5c3" dependencies = [ "alsa-sys", - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -77,12 +77,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -144,9 +138,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "arrayvec" @@ -241,9 +235,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.3" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -280,10 +274,11 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.34" +version = "1.2.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" +checksum = "80f41ae168f955c12fb8960b057d70d0ca153fb83182b57d86380443527be7e9" dependencies = [ + "find-msvc-tools", "shlex", ] @@ -295,9 +290,9 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" [[package]] name = "cfg-expr" -version = "0.20.2" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d458d63f0f0f482c8da9b7c8b76c21bd885a02056cc94c6404d861ca2b8206" +checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", "target-lexicon", @@ -317,17 +312,16 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "serde", "wasm-bindgen", - "windows-link", + "windows-link 0.2.0", ] [[package]] @@ -541,9 +535,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "d630bccd429a5bb5a64b5e94f693bfc48c9f8566418fda4c494cc94f911f87cc" dependencies = [ "powerfmt", ] @@ -597,7 +591,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "objc2", ] @@ -694,12 +688,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -730,10 +724,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "fixedbitset" -version = "0.4.2" +name = "find-msvc-tools" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +checksum = "1ced73b1dacfc750a6db6c0a0c3a3853c8b41997e2e2c563dc90804ae6867959" [[package]] name = "flate2" @@ -931,7 +925,7 @@ dependencies = [ "js-sys", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", "wasm-bindgen", ] @@ -943,24 +937,24 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "gio-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a03f2234671e5a588cfe1f59c2b22c103f5772ea351be9cc824a9ce0d06d99fd" +checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] name = "glib" -version = "0.21.1" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60bdc26493257b5794ba9301f7cbaf7ab0d69a570bfbefa4d7d360e781cb5205" +checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "futures-channel", "futures-core", "futures-executor", @@ -977,9 +971,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e772291ebea14c28eb11bb75741f62f4a4894f25e60ce80100797b6b010ef0f9" +checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f" dependencies = [ "heck", "proc-macro-crate", @@ -990,9 +984,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c43cff6a7dc43821e45ebf172399437acd6716fa2186b6852d2b397bf622d" +checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" dependencies = [ "libc", "system-deps", @@ -1000,9 +994,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.21.1" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e9a190eef2bce144a6aa8434e306974c6062c398e0a33a146d60238f9062d5c" +checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" dependencies = [ "glib-sys", "libc", @@ -1019,12 +1013,10 @@ dependencies = [ "futures-sink", "futures-timer", "futures-util", - "getrandom 0.3.3", - "hashbrown", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", - "rand 0.9.2", "smallvec", "spinning_top", "web-time", @@ -1032,9 +1024,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.24.1" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f5db514ad5ccf70ad35485058aa8b894bb81cfcf76bb994af135d9789427c6" +checksum = "3e7ba7a2584e31927b7fec6a32737b57dc991b55253c9bb7c2c8eddb5a4cb345" dependencies = [ "cfg-if", "futures-channel", @@ -1049,7 +1041,7 @@ dependencies = [ "num-integer", "num-rational", "option-operations", - "paste", + "pastey", "pin-project-lite", "smallvec", "thiserror 2.0.16", @@ -1057,9 +1049,9 @@ dependencies = [ [[package]] name = "gstreamer-app" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fad8ae64a7af6d1aa04e96db085a0cbd64a6b838d85c115c99fa053ab8902d98" +checksum = "0af5d403738faf03494dfd502d223444b4b44feb997ba28ab3f118ee6d40a0b2" dependencies = [ "futures-core", "futures-sink", @@ -1085,9 +1077,9 @@ dependencies = [ [[package]] name = "gstreamer-audio" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7404c5d0cbb2189e6a10d05801e93f47fe60b195e4d73dd1c540d055f7b340b8" +checksum = "68e540174d060cd0d7ee2c2356f152f05d8262bf102b40a5869ff799377269d8" dependencies = [ "cfg-if", "glib", @@ -1114,9 +1106,9 @@ dependencies = [ [[package]] name = "gstreamer-base" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34745d3726a080e0d57e402a314e37073d0b341f3a5754258550311ca45e4754" +checksum = "71ff9b0bbc8041f0c6c8a53b206a6542f86c7d9fa8a7dff3f27d9c374d9f39b4" dependencies = [ "atomic_refcell", "cfg-if", @@ -1128,9 +1120,9 @@ dependencies = [ [[package]] name = "gstreamer-base-sys" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfad00fa63ddd8132306feef9d5095a3636192f09d925adfd0a9be0d82b9ea91" +checksum = "fed78852b92db1459b8f4288f86e6530274073c20be2f94ba642cddaca08b00e" dependencies = [ "glib-sys", "gobject-sys", @@ -1141,9 +1133,9 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.24.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36f46b35f9dc4b5a0dca3f19d2118bb5355c3112f228a99a84ed555f48ce5cf9" +checksum = "a24ae2930e683665832a19ef02466094b09d1f2da5673f001515ed5486aa9377" dependencies = [ "cfg-if", "glib-sys", @@ -1182,6 +1174,12 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" + [[package]] name = "headers" version = "0.4.1" @@ -1244,7 +1242,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65" dependencies = [ "cfg-if", "libc", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -1370,11 +1368,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tower-service", "webpki-roots 1.0.2", ] @@ -1397,9 +1395,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64", "bytes", @@ -1423,9 +1421,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1433,7 +1431,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.0", ] [[package]] @@ -1570,12 +1568,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.11.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.0", ] [[package]] @@ -1593,7 +1591,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "libc", ] @@ -1660,7 +1658,7 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f70ca699f44c04a32d419fc9ed699aaea89657fc09014bf3fa238e91d13041b9" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "jack-sys", "lazy_static", "libc", @@ -1729,9 +1727,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "852f13bec5eba4ba9afbeb93fd7c13fe56147f055939ae21c43a29a0ecb2702e" dependencies = [ "once_cell", "wasm-bindgen", @@ -1801,7 +1799,7 @@ version = "2.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "909eb3049e16e373680fe65afe6e2a722ace06b671250cc4849557bc57d6a397" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "libc", "libpulse-sys", "num-derive", @@ -1880,7 +1878,6 @@ dependencies = [ "hyper-util", "librespot-core", "log", - "parking_lot", "tempfile", "thiserror 2.0.16", "tokio", @@ -1935,7 +1932,6 @@ dependencies = [ "num-derive", "num-integer", "num-traits", - "parking_lot", "pbkdf2", "pin-project-lite", "priority-queue", @@ -2040,7 +2036,6 @@ dependencies = [ "librespot-metadata", "log", "ogg", - "parking_lot", "portable-atomic", "portaudio-rs", "rand 0.9.2", @@ -2070,9 +2065,9 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -2092,9 +2087,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lru-slab" @@ -2187,7 +2182,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "jni-sys", "log", "ndk-sys", @@ -2216,7 +2211,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "cfg_aliases", "libc", @@ -2389,7 +2384,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "libc", "objc2", "objc2-core-audio", @@ -2416,7 +2411,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0f1cc99bb07ad2ddb6527ddf83db6a15271bb036b3eb94b801cd44fdc666ee1" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "objc2", ] @@ -2426,7 +2421,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "dispatch2", "objc2", ] @@ -2503,7 +2498,7 @@ version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "cfg-if", "foreign-types", "libc", @@ -2543,11 +2538,11 @@ dependencies = [ [[package]] name = "option-operations" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +checksum = "b31ce827892359f23d3cd1cc4c75a6c241772bbd2db17a92dcf27cbefdf52689" dependencies = [ - "paste", + "pastey", ] [[package]] @@ -2582,21 +2577,18 @@ version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ - "backtrace", "cfg-if", "libc", - "petgraph", "redox_syscall", "smallvec", - "thread-id", "windows-targets 0.52.6", ] [[package]] -name = "paste" -version = "1.0.15" +name = "pastey" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" [[package]] name = "pathdiff" @@ -2629,16 +2621,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap", -] - [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2740,22 +2722,21 @@ dependencies = [ [[package]] name = "priority-queue" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5676d703dda103cbb035b653a9f11448c0a7216c7926bd35fcb5865475d0c970" +checksum = "3e7f4ffd8645efad783fc2844ac842367aa2e912d484950192564d57dc039a3a" dependencies = [ - "autocfg", "equivalent", "indexmap", ] [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] @@ -2851,7 +2832,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.31", + "rustls 0.23.32", "socket2", "thiserror 2.0.16", "tokio", @@ -2871,7 +2852,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-pki-types", "slab", "thiserror 2.0.16", @@ -2984,7 +2965,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", ] [[package]] @@ -3040,7 +3021,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "serde", @@ -3049,7 +3030,7 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tower", "tower-http", "tower-service", @@ -3123,7 +3104,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "errno", "libc", "linux-raw-sys 0.4.15", @@ -3132,15 +3113,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.0", ] [[package]] @@ -3159,14 +3140,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.31" +version = "0.23.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +checksum = "cd3c25631629d034ce7cd9940adc9d45762d46de2b0f57193c4443b92c6d4d40" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.6", "subtle", "zeroize", ] @@ -3193,7 +3174,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.3.0", + "security-framework 3.4.0", ] [[package]] @@ -3228,9 +3209,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "8572f3c2cb9934231157b45499fc41e1f58c589fdfb81a844ba873265e80f8eb" dependencies = [ "ring", "rustls-pki-types", @@ -3260,11 +3241,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.0", ] [[package]] @@ -3302,7 +3283,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -3311,11 +3292,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +checksum = "60b369d18893388b345804dc0007963c99b7d665ae71d275812d828c6f089640" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -3324,9 +3305,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.14.0" +version = "2.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" dependencies = [ "core-foundation-sys", "libc", @@ -3334,18 +3315,28 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "0dca6411025b24b60bfa7ec1fe1f8e710ac09782dca409ee8237ba74b51295fd" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.226" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba2ba63999edb9dac981fb34b3e5c0d111a69b0924e253ed29d83f7c99e966a4" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.226" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "8db53ae22f34573731bafa1db20f04027b2d25e02d8205921b569171699cdb33" dependencies = [ "proc-macro2", "quote", @@ -3354,24 +3345,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.143" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -3674,7 +3667,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -3710,15 +3703,15 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.21.0" +version = "3.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" +checksum = "84fa4d11fadde498443cca10fd3ac23c951f0dc59e080e9f4b93d4df4e4eea53" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.2", + "windows-sys 0.61.0", ] [[package]] @@ -3761,21 +3754,11 @@ dependencies = [ "syn", ] -[[package]] -name = "thread-id" -version = "4.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8f25bbdd100db7e1d34acf7fd2dc59c4bf8f7483f505eaa7d4f12f76cc0ea" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "time" -version = "0.3.41" +version = "0.3.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" dependencies = [ "deranged", "itoa", @@ -3790,15 +3773,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" dependencies = [ "num-conv", "time-core", @@ -3840,7 +3823,6 @@ dependencies = [ "io-uring", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "slab", @@ -3884,11 +3866,11 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.2" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +checksum = "05f63835928ca123f1bef57abbcd23bb2ba0ac9ae1235f1e65bda0d06e7786bd" dependencies = [ - "rustls 0.23.31", + "rustls 0.23.32", "tokio", ] @@ -3912,12 +3894,12 @@ dependencies = [ "futures-util", "log", "native-tls", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-native-tls", - "tokio-rustls 0.26.2", + "tokio-rustls 0.26.3", "tungstenite", "webpki-roots 0.26.11", ] @@ -3943,8 +3925,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -3956,6 +3938,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -3965,7 +3956,28 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -3990,7 +4002,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.9.3", + "bitflags 2.9.4", "bytes", "futures-util", "http", @@ -4064,7 +4076,7 @@ dependencies = [ "log", "native-tls", "rand 0.9.2", - "rustls 0.23.31", + "rustls 0.23.32", "rustls-pki-types", "sha1", "thiserror 2.0.16", @@ -4090,9 +4102,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-width" @@ -4138,13 +4150,11 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom 0.3.3", - "js-sys", - "wasm-bindgen", ] [[package]] @@ -4236,30 +4246,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.3+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" +dependencies = [ + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "ab10a69fbd0a177f5f649ad4d8d3305499c42bab9aef2f7ff592d0ec8f833819" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "0bb702423545a6007bbc368fde243ba47ca275e549c8a28617f56f6ba53b1d1c" dependencies = [ "bumpalo", "log", @@ -4271,9 +4291,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "a0b221ff421256839509adbb55998214a70d829d3a28c69b4a6672e9d2a42f67" dependencies = [ "cfg-if", "js-sys", @@ -4284,9 +4304,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "fc65f4f411d91494355917b605e1480033152658d71f722a90647f56a70c88a0" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4294,9 +4314,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "ffc003a991398a8ee604a401e194b6b3a39677b3173d6e74495eb51b82e99a32" dependencies = [ "proc-macro2", "quote", @@ -4307,18 +4327,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "293c37f4efa430ca14db3721dfbe48d8c33308096bd44d80ebaa775ab71ba1cf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "fbe734895e869dc429d78c4b433f8d17d95f8d05317440b4fad5ab2d33e596dc" dependencies = [ "js-sys", "wasm-bindgen", @@ -4392,11 +4412,11 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.0", ] [[package]] @@ -4424,7 +4444,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -4455,9 +4475,22 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fe7168f7de578d2d8a05b07fd61870d2e73b4020e9f49aa00da8471723497c" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.0", + "windows-result 0.4.0", + "windows-strings 0.5.0", ] [[package]] @@ -4467,7 +4500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -4499,6 +4532,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -4506,7 +4545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4515,9 +4554,9 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", - "windows-strings", + "windows-strings 0.4.2", ] [[package]] @@ -4535,7 +4574,16 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7084dcc306f89883455a206237404d3eaf961e5bd7e0f312f7c91f57eb44167f" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4544,7 +4592,16 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7218c655a553b0bed4426cf54b20d7ba363ef543b52d515b3e48d7fd55318dda" +dependencies = [ + "windows-link 0.2.0", ] [[package]] @@ -4583,6 +4640,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e201184e40b2ede64bc2ea34968b28e33622acdbbf37104f0e4a33f7abe657aa" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -4620,7 +4686,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -4637,7 +4703,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -4789,9 +4855,9 @@ dependencies = [ [[package]] name = "wit-bindgen" -version = "0.45.0" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -4825,9 +4891,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67a073be99ace1adc48af593701c8015cd9817df372e14a1a6b0ee8f8bf043be" +checksum = "2d07e46d035fb8e375b2ce63ba4e4ff90a7f73cf2ffb0138b29e1158d2eaadf7" dependencies = [ "async-broadcast", "async-recursion", @@ -4853,9 +4919,9 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.10.0" +version = "5.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e80cd713a45a49859dcb648053f63265f4f2851b6420d47a958e5697c68b131" +checksum = "57e797a9c847ed3ccc5b6254e8bcce056494b375b511b3d6edcec0aeb4defaca" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -4880,18 +4946,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bcb3664a..63a5927a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,7 +179,6 @@ tokio = { version = "1", features = [ "macros", "signal", "sync", - "parking_lot", "process", ] } url = "2.2" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5a57ed76..3ff3aac1 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -23,12 +23,11 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false aes = "0.8" bytes = "1" ctr = "0.9" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } http-body-util = "0.1" hyper = { version = "1.6", features = ["http1", "http2"] } hyper-util = { version = "0.1", features = ["client", "http2"] } log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } tempfile = "3" thiserror = "2" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } +tokio = { version = "1", features = ["macros", "sync"] } diff --git a/audio/src/fetch/mod.rs b/audio/src/fetch/mod.rs index 781e8a32..6a6379b9 100644 --- a/audio/src/fetch/mod.rs +++ b/audio/src/fetch/mod.rs @@ -8,13 +8,14 @@ use std::{ Arc, OnceLock, atomic::{AtomicBool, AtomicUsize, Ordering}, }, + sync::{Condvar, Mutex}, time::Duration, }; use futures_util::{StreamExt, TryFutureExt, future::IntoStream}; use hyper::{Response, StatusCode, body::Incoming, header::CONTENT_RANGE}; use hyper_util::client::legacy::ResponseFuture; -use parking_lot::{Condvar, Mutex}; + use tempfile::NamedTempFile; use thiserror::Error; use tokio::sync::{Semaphore, mpsc, oneshot}; @@ -27,6 +28,8 @@ use crate::range_set::{Range, RangeSet}; pub type AudioFileResult = Result<(), librespot_core::Error>; +const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned"; + #[derive(Error, Debug)] pub enum AudioFileError { #[error("other end of channel disconnected")] @@ -163,7 +166,10 @@ impl StreamLoaderController { pub fn range_available(&self, range: Range) -> bool { if let Some(ref shared) = self.stream_shared { - let download_status = shared.download_status.lock(); + let download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); range.length <= download_status @@ -214,7 +220,10 @@ impl StreamLoaderController { self.fetch(range); if let Some(ref shared) = self.stream_shared { - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); let download_timeout = AudioFetchParams::get().download_timeout; while range.length @@ -222,11 +231,13 @@ impl StreamLoaderController { .downloaded .contained_length_from_value(range.start) { - if shared + let (new_download_status, wait_result) = shared .cond - .wait_for(&mut download_status, download_timeout) - .timed_out() - { + .wait_timeout(download_status, download_timeout) + .expect(DOWNLOAD_STATUS_POISON_MSG); + + download_status = new_download_status; + if wait_result.timed_out() { return Err(AudioFileError::WaitTimeout.into()); } @@ -558,7 +569,11 @@ 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(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -571,12 +586,14 @@ impl Read for AudioFileStreaming { let download_timeout = AudioFetchParams::get().download_timeout; while !download_status.downloaded.contains(offset) { - if self + let (new_download_status, wait_result) = self .shared .cond - .wait_for(&mut download_status, download_timeout) - .timed_out() - { + .wait_timeout(download_status, download_timeout) + .expect(DOWNLOAD_STATUS_POISON_MSG); + + download_status = new_download_status; + if wait_result.timed_out() { return Err(io::Error::new( io::ErrorKind::TimedOut, Error::deadline_exceeded(AudioFileError::WaitTimeout), @@ -619,6 +636,7 @@ impl Seek for AudioFileStreaming { .shared .download_status .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG) .downloaded .contains(requested_pos as usize); diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 3d7dfa64..4c894cf6 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -33,6 +33,7 @@ enum ReceivedData { } const ONE_SECOND: Duration = Duration::from_secs(1); +const DOWNLOAD_STATUS_POISON_MSG: &str = "audio download status mutex should not be poisoned"; async fn receive_data( shared: Arc, @@ -124,7 +125,10 @@ async fn receive_data( if bytes_remaining > 0 { { let missing_range = Range::new(offset, bytes_remaining); - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.requested.subtract_range(&missing_range); shared.cond.notify_all(); } @@ -189,7 +193,11 @@ impl AudioFileFetch { // 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(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); ranges_to_request.subtract_range_set(&download_status.downloaded); ranges_to_request.subtract_range_set(&download_status.requested); @@ -227,7 +235,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(); + let download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); missing_data.subtract_range_set(&download_status.downloaded); missing_data.subtract_range_set(&download_status.requested); } @@ -349,7 +361,11 @@ impl AudioFileFetch { let received_range = Range::new(data.offset, data.data.len()); let full = { - let mut download_status = self.shared.download_status.lock(); + let mut download_status = self + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.downloaded.add_range(&received_range); self.shared.cond.notify_all(); @@ -415,7 +431,10 @@ pub(super) async fn audio_file_fetch( initial_request.offset + initial_request.length, ); - let mut download_status = shared.download_status.lock(); + let mut download_status = shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status.requested.add_range(&requested_range); } @@ -466,7 +485,11 @@ pub(super) async fn audio_file_fetch( if fetch.shared.is_download_streaming() && fetch.has_download_slots_available() { let bytes_pending: usize = { - let download_status = fetch.shared.download_status.lock(); + let download_status = fetch + .shared + .download_status + .lock() + .expect(DOWNLOAD_STATUS_POISON_MSG); download_status .requested diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 9e7b231b..08d24f66 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -22,12 +22,12 @@ librespot-core = { version = "0.7.1", path = "../core", default-features = false librespot-playback = { version = "0.7.1", path = "../playback", default-features = false } librespot-protocol = { version = "0.7.1", path = "../protocol", default-features = false } -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } log = "0.4" protobuf = "3.7" rand = { version = "0.9", default-features = false, features = ["small_rng"] } serde_json = "1.0" thiserror = "2" -tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } -tokio-stream = "0.1" -uuid = { version = "1.18", features = ["v4"] } +tokio = { version = "1", features = ["macros", "sync"] } +tokio-stream = { version = "0.1", default-features = false } +uuid = { version = "1.18", default-features = false, features = ["v4"] } diff --git a/core/Cargo.toml b/core/Cargo.toml index 4f5e79cc..f91a1b38 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -51,16 +51,12 @@ data-encoding = "2.9" flate2 = "1.1" form_urlencoded = "1.2" futures-core = "0.3" -futures-util = { version = "0.3", features = [ +futures-util = { version = "0.3", default-features = false, features = [ "alloc", "bilock", - "sink", "unstable", ] } -governor = { version = "0.10", default-features = false, features = [ - "std", - "jitter", -] } +governor = { version = "0.10", default-features = false, features = ["std"] } hmac = "0.12" httparse = "1.10" http = "1.3" @@ -84,14 +80,13 @@ num-bigint = "0.4" num-derive = "0.4" num-integer = "0.1" num-traits = "0.2" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pin-project-lite = "0.2" priority-queue = "2.5" protobuf = "3.7" protobuf-json-mapping = "3.7" quick-xml = { version = "0.38", features = ["serialize"] } -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } rsa = "0.9" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -104,23 +99,22 @@ tokio = { version = "1", features = [ "io-util", "macros", "net", - "parking_lot", "rt", "sync", "time", ] } -tokio-stream = "0.1" +tokio-stream = { version = "0.1", default-features = false } tokio-tungstenite = { version = "0.27", default-features = false } -tokio-util = { version = "0.7", features = ["codec"] } +tokio-util = { version = "0.7", default-features = false } url = "2" uuid = { version = "1", default-features = false, features = ["v4"] } [build-dependencies] -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } rand_distr = "0.5" vergen-gitcl = { version = "1.0", default-features = false, features = [ "build", ] } [dev-dependencies] -tokio = { version = "1", features = ["macros", "parking_lot"] } +tokio = { version = "1", features = ["macros"] } diff --git a/core/src/cache.rs b/core/src/cache.rs index 2d2ef53d..15e35d21 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -4,16 +4,17 @@ use std::{ fs::{self, File}, io::{self, Read, Write}, path::{Path, PathBuf}, - sync::Arc, + sync::{Arc, Mutex}, time::SystemTime, }; -use parking_lot::Mutex; use priority_queue::PriorityQueue; use thiserror::Error; use crate::{Error, FileId, authentication::Credentials, error::ErrorKind}; +const CACHE_LIMITER_POISON_MSG: &str = "cache limiter mutex should not be poisoned"; + #[derive(Debug, Error)] pub enum CacheError { #[error("audio cache location is not configured")] @@ -189,15 +190,24 @@ impl FsSizeLimiter { } fn add(&self, file: &Path, size: u64) { - self.limiter.lock().add(file, size, SystemTime::now()) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .add(file, size, SystemTime::now()) } fn touch(&self, file: &Path) -> bool { - self.limiter.lock().update(file, SystemTime::now()) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .update(file, SystemTime::now()) } fn remove(&self, file: &Path) -> bool { - self.limiter.lock().remove(file) + self.limiter + .lock() + .expect(CACHE_LIMITER_POISON_MSG) + .remove(file) } fn prune_internal Option>(mut pop: F) -> Result<(), Error> { @@ -232,7 +242,7 @@ impl FsSizeLimiter { } fn prune(&self) -> Result<(), Error> { - Self::prune_internal(|| self.limiter.lock().pop()) + Self::prune_internal(|| self.limiter.lock().expect(CACHE_LIMITER_POISON_MSG).pop()) } fn new(path: &Path, limit: u64) -> Result { diff --git a/core/src/component.rs b/core/src/component.rs index ebe42e8d..75387ae5 100644 --- a/core/src/component.rs +++ b/core/src/component.rs @@ -1,20 +1,23 @@ +pub(crate) const COMPONENT_POISON_MSG: &str = "component mutex should not be poisoned"; + macro_rules! component { ($name:ident : $inner:ident { $($key:ident : $ty:ty = $value:expr,)* }) => { #[derive(Clone)] - pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::parking_lot::Mutex<$inner>)>); + pub struct $name(::std::sync::Arc<($crate::session::SessionWeak, ::std::sync::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, ::parking_lot::Mutex::new($inner { + $name(::std::sync::Arc::new((session, ::std::sync::Mutex::new($inner { $($key : $value,)* })))) } #[allow(dead_code)] fn lock R, R>(&self, f: F) -> R { - let mut inner = (self.0).1.lock(); + let mut inner = (self.0).1.lock() + .expect($crate::component::COMPONENT_POISON_MSG); f(&mut inner) } diff --git a/core/src/dealer/mod.rs b/core/src/dealer/mod.rs index 4f738403..63ee6e72 100644 --- a/core/src/dealer/mod.rs +++ b/core/src/dealer/mod.rs @@ -6,7 +6,7 @@ use std::{ iter, pin::Pin, sync::{ - Arc, + Arc, Mutex, atomic::{self, AtomicBool}, }, task::Poll, @@ -15,7 +15,6 @@ use std::{ use futures_core::{Future, Stream}; use futures_util::{SinkExt, StreamExt, future::join_all}; -use parking_lot::Mutex; use thiserror::Error; use tokio::{ select, @@ -57,6 +56,11 @@ const PING_TIMEOUT: Duration = Duration::from_secs(3); const RECONNECT_INTERVAL: Duration = Duration::from_secs(10); +const DEALER_REQUEST_HANDLERS_POISON_MSG: &str = + "dealer request handlers mutex should not be poisoned"; +const DEALER_MESSAGE_HANDLERS_POISON_MSG: &str = + "dealer message handlers mutex should not be poisoned"; + struct Response { pub success: bool, } @@ -350,6 +354,7 @@ impl DealerShared { if self .message_handlers .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG) .retain(split, &mut |tx| tx.send(msg.clone()).is_ok()) { return; @@ -387,7 +392,10 @@ impl DealerShared { return; }; - let handler_map = self.request_handlers.lock(); + let handler_map = self + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG); if let Some(handler) = handler_map.get(split) { handler.handle_request(payload_request, responder); @@ -425,21 +433,51 @@ impl Dealer { where H: RequestHandler, { - add_handler(&mut self.shared.request_handlers.lock(), uri, handler) + add_handler( + &mut self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + uri, + handler, + ) } pub fn remove_handler(&self, uri: &str) -> Option> { - remove_handler(&mut self.shared.request_handlers.lock(), uri) + remove_handler( + &mut self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + uri, + ) } pub fn subscribe(&self, uris: &[&str]) -> Result { - subscribe(&mut self.shared.message_handlers.lock(), uris) + subscribe( + &mut self + .shared + .message_handlers + .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG), + uris, + ) } pub fn handles(&self, uri: &str) -> bool { handles( - &self.shared.request_handlers.lock(), - &self.shared.message_handlers.lock(), + &self + .shared + .request_handlers + .lock() + .expect(DEALER_REQUEST_HANDLERS_POISON_MSG), + &self + .shared + .message_handlers + .lock() + .expect(DEALER_MESSAGE_HANDLERS_POISON_MSG), uri, ) } diff --git a/core/src/http_client.rs b/core/src/http_client.rs index 9d5d54fc..bbd63838 100644 --- a/core/src/http_client.rs +++ b/core/src/http_client.rs @@ -1,5 +1,4 @@ use std::{ - collections::HashMap, sync::OnceLock, time::{Duration, Instant}, }; @@ -7,7 +6,8 @@ use std::{ use bytes::Bytes; use futures_util::{FutureExt, future::IntoStream}; use governor::{ - Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, state::InMemoryState, + Quota, RateLimiter, clock::MonotonicClock, middleware::NoOpMiddleware, + state::keyed::DefaultKeyedStateStore, }; use http::{Uri, header::HeaderValue}; use http_body_util::{BodyExt, Full}; @@ -18,7 +18,6 @@ use hyper_util::{ rt::TokioExecutor, }; use nonzero_ext::nonzero; -use parking_lot::Mutex; use thiserror::Error; use url::Url; @@ -100,10 +99,8 @@ pub struct HttpClient { proxy_url: Option, hyper_client: OnceLock, - // while the DashMap variant is more performant, our level of concurrency - // is pretty low so we can save pulling in that extra dependency rate_limiter: - RateLimiter>, MonotonicClock, NoOpMiddleware>, + RateLimiter, MonotonicClock, NoOpMiddleware>, } impl HttpClient { diff --git a/core/src/session.rs b/core/src/session.rs index 91c41781..333678fd 100644 --- a/core/src/session.rs +++ b/core/src/session.rs @@ -4,8 +4,7 @@ use std::{ io, pin::Pin, process::exit, - sync::OnceLock, - sync::{Arc, Weak}, + sync::{Arc, OnceLock, RwLock, Weak}, task::{Context, Poll}, time::{Duration, SystemTime, UNIX_EPOCH}, }; @@ -34,7 +33,6 @@ use futures_core::TryStream; use futures_util::StreamExt; use librespot_protocol::authentication::AuthenticationType; use num_traits::FromPrimitive; -use parking_lot::RwLock; use pin_project_lite::pin_project; use quick_xml::events::Event; use thiserror::Error; @@ -45,6 +43,8 @@ use tokio::{ use tokio_stream::wrappers::UnboundedReceiverStream; use uuid::Uuid; +const SESSION_DATA_POISON_MSG: &str = "session data rwlock should not be poisoned"; + #[derive(Debug, Error)] pub enum SessionError { #[error(transparent)] @@ -338,7 +338,11 @@ impl Session { } pub fn time_delta(&self) -> i64 { - self.0.data.read().time_delta + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .time_delta } pub fn spawn(&self, task: T) @@ -388,15 +392,32 @@ impl Session { // 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() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .clone() } pub fn session_id(&self) -> String { - self.0.data.read().session_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .session_id + .clone() } pub fn set_session_id(&self, session_id: &str) { - session_id.clone_into(&mut self.0.data.write().session_id); + session_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .session_id, + ); } pub fn device_id(&self) -> &str { @@ -404,63 +425,155 @@ impl Session { } pub fn client_id(&self) -> String { - self.0.data.read().client_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_id + .clone() } pub fn set_client_id(&self, client_id: &str) { - client_id.clone_into(&mut self.0.data.write().client_id); + client_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_id, + ); } pub fn client_name(&self) -> String { - self.0.data.read().client_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_name + .clone() } pub fn set_client_name(&self, client_name: &str) { - client_name.clone_into(&mut self.0.data.write().client_name); + client_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_name, + ); } pub fn client_brand_name(&self) -> String { - self.0.data.read().client_brand_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_brand_name + .clone() } pub fn set_client_brand_name(&self, client_brand_name: &str) { - client_brand_name.clone_into(&mut self.0.data.write().client_brand_name); + client_brand_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_brand_name, + ); } pub fn client_model_name(&self) -> String { - self.0.data.read().client_model_name.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .client_model_name + .clone() } pub fn set_client_model_name(&self, client_model_name: &str) { - client_model_name.clone_into(&mut self.0.data.write().client_model_name); + client_model_name.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .client_model_name, + ); } pub fn connection_id(&self) -> String { - self.0.data.read().connection_id.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .connection_id + .clone() } pub fn set_connection_id(&self, connection_id: &str) { - connection_id.clone_into(&mut self.0.data.write().connection_id); + connection_id.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .connection_id, + ); } pub fn username(&self) -> String { - self.0.data.read().user_data.canonical_username.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .canonical_username + .clone() } pub fn set_username(&self, username: &str) { - username.clone_into(&mut self.0.data.write().user_data.canonical_username); + username.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .canonical_username, + ); } pub fn auth_data(&self) -> Vec { - self.0.data.read().auth_data.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .auth_data + .clone() } pub fn set_auth_data(&self, auth_data: &[u8]) { - auth_data.clone_into(&mut self.0.data.write().auth_data); + auth_data.clone_into( + &mut self + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .auth_data, + ); } pub fn country(&self) -> String { - self.0.data.read().user_data.country.clone() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .country + .clone() } pub fn filter_explicit_content(&self) -> bool { @@ -489,6 +602,7 @@ impl Session { self.0 .data .write() + .expect(SESSION_DATA_POISON_MSG) .user_data .attributes .insert(key.to_owned(), value.to_owned()) @@ -497,11 +611,24 @@ impl Session { pub fn set_user_attributes(&self, attributes: UserAttributes) { Self::check_catalogue(&attributes); - self.0.data.write().user_data.attributes.extend(attributes) + self.0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes + .extend(attributes) } pub fn get_user_attribute(&self, key: &str) -> Option { - self.0.data.read().user_data.attributes.get(key).cloned() + self.0 + .data + .read() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes + .get(key) + .cloned() } fn weak(&self) -> SessionWeak { @@ -510,13 +637,13 @@ impl Session { pub fn shutdown(&self) { debug!("Shutdown: Invalidating session"); - self.0.data.write().invalid = true; + self.0.data.write().expect(SESSION_DATA_POISON_MSG).invalid = true; self.mercury().shutdown(); self.channel().shutdown(); } pub fn is_invalid(&self) -> bool { - self.0.data.read().invalid + self.0.data.read().expect(SESSION_DATA_POISON_MSG).invalid } } @@ -643,7 +770,7 @@ where .unwrap_or(Duration::ZERO) .as_secs() as i64; { - let mut data = session.0.data.write(); + let mut data = session.0.data.write().expect(SESSION_DATA_POISON_MSG); data.time_delta = server_timestamp.saturating_sub(timestamp); } @@ -668,7 +795,13 @@ where Some(CountryCode) => { let country = String::from_utf8(data.as_ref().to_owned())?; info!("Country: {country:?}"); - session.0.data.write().user_data.country = country; + session + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .country = country; Ok(()) } Some(StreamChunkRes) | Some(ChannelError) => session.channel().dispatch(cmd, data), @@ -713,7 +846,13 @@ where trace!("Received product info: {user_attributes:#?}"); Session::check_catalogue(&user_attributes); - session.0.data.write().user_data.attributes = user_attributes; + session + .0 + .data + .write() + .expect(SESSION_DATA_POISON_MSG) + .user_data + .attributes = user_attributes; Ok(()) } Some(SecretBlock) diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 4220b130..2f86d5ac 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -32,7 +32,7 @@ ctr = "0.9" dns-sd = { version = "0.1", optional = true } form_urlencoded = "1.2" futures-core = "0.3" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } hmac = "0.12" http-body-util = "0.1" hyper = { version = "1.6", features = ["http1"] } @@ -43,7 +43,7 @@ hyper-util = { version = "0.1", features = [ ] } libmdns = { version = "0.10", optional = true } log = "0.4" -rand = "0.9" +rand = { version = "0.9", default-features = false, features = ["thread_rng"] } serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } @@ -51,7 +51,7 @@ serde_repr = "0.1" serde_json = "1.0" sha1 = "0.10" thiserror = "2" -tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } +tokio = { version = "1", features = ["sync", "rt"] } zbus = { version = "5", default-features = false, features = [ "tokio", ], optional = true } @@ -59,4 +59,4 @@ zbus = { version = "5", default-features = false, features = [ [dev-dependencies] futures = "0.3" hex = "0.4" -tokio = { version = "1", features = ["macros", "parking_lot", "rt"] } +tokio = { version = "1", features = ["macros", "rt"] } diff --git a/playback/Cargo.toml b/playback/Cargo.toml index dca23297..2001c680 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -51,18 +51,12 @@ librespot-audio = { version = "0.7.1", path = "../audio", default-features = fal librespot-core = { version = "0.7.1", path = "../core", default-features = false } librespot-metadata = { version = "0.7.1", path = "../metadata", default-features = false } -portable-atomic = "1" -futures-util = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } log = "0.4" -parking_lot = { version = "0.12", features = ["deadlock_detection"] } +portable-atomic = "1" shell-words = "1.1" thiserror = "2" -tokio = { version = "1", features = [ - "parking_lot", - "rt", - "rt-multi-thread", - "sync", -] } +tokio = { version = "1", features = ["rt-multi-thread", "sync"] } zerocopy = { version = "0.8", features = ["derive"] } # Backends @@ -97,5 +91,5 @@ symphonia = { version = "0.5", default-features = false, features = [ ogg = { version = "0.9", optional = true } # Dithering -rand = { version = "0.9", features = ["small_rng"] } +rand = { version = "0.9", default-features = false, features = ["small_rng"] } rand_distr = "0.5" diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index b9087e3d..f41d4333 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use gstreamer::{ State, event::{FlushStart, FlushStop}, @@ -8,8 +10,7 @@ use gstreamer as gst; use gstreamer_app as gst_app; use gstreamer_audio as gst_audio; -use parking_lot::Mutex; -use std::sync::Arc; +const GSTREAMER_ASYNC_ERROR_POISON_MSG: &str = "gstreamer async error mutex should not be poisoned"; use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; @@ -97,7 +98,9 @@ impl Open for GstreamerSink { gst::MessageView::Eos(_) => { println!("gst signaled end of stream"); - let mut async_error_storage = async_error_clone.lock(); + let mut async_error_storage = async_error_clone + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG); *async_error_storage = Some(String::from("gst signaled end of stream")); } gst::MessageView::Error(err) => { @@ -108,7 +111,9 @@ impl Open for GstreamerSink { err.debug() ); - let mut async_error_storage = async_error_clone.lock(); + let mut async_error_storage = async_error_clone + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG); *async_error_storage = Some(format!( "Error from {:?}: {} ({:?})", err.src().map(|s| s.path_string()), @@ -138,7 +143,10 @@ impl Open for GstreamerSink { impl Sink for GstreamerSink { fn start(&mut self) -> SinkResult<()> { - *self.async_error.lock() = None; + *self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None; self.appsrc.send_event(FlushStop::new(true)); self.bufferpool .set_active(true) @@ -150,7 +158,10 @@ impl Sink for GstreamerSink { } fn stop(&mut self) -> SinkResult<()> { - *self.async_error.lock() = None; + *self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) = None; self.appsrc.send_event(FlushStart::new()); self.pipeline .set_state(State::Paused) @@ -173,7 +184,11 @@ impl Drop for GstreamerSink { impl SinkAsBytes for GstreamerSink { #[inline] fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { - if let Some(async_error) = &*self.async_error.lock() { + if let Some(async_error) = &*self + .async_error + .lock() + .expect(GSTREAMER_ASYNC_ERROR_POISON_MSG) + { return Err(SinkError::OnWrite(async_error.to_string())); } diff --git a/playback/src/player.rs b/playback/src/player.rs index 886c9cf3..a4a03ca3 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -6,6 +6,7 @@ use std::{ mem, pin::Pin, process::exit, + sync::Mutex, sync::{ Arc, atomic::{AtomicUsize, Ordering}, @@ -32,7 +33,6 @@ use futures_util::{ stream::futures_unordered::FuturesUnordered, }; use librespot_metadata::track::Tracks; -use parking_lot::Mutex; use symphonia::core::io::MediaSource; use tokio::sync::{mpsc, oneshot}; @@ -46,6 +46,8 @@ pub const PCM_AT_0DBFS: f64 = 1.0; // otherwise expect in Vorbis comments. This packet isn't well-formed and players may balk at it. const SPOTIFY_OGG_HEADER_END: u64 = 0xa7; +const LOAD_HANDLES_POISON_MSG: &str = "load handles mutex should not be poisoned"; + pub type PlayerResult = Result<(), Error>; pub struct Player { @@ -2281,11 +2283,11 @@ impl PlayerInternal { let _ = result_tx.send(data); } - let mut load_handles = load_handles_clone.lock(); + let mut load_handles = load_handles_clone.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles.remove(&thread::current().id()); }); - let mut load_handles = self.load_handles.lock(); + let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles.insert(load_handle.thread().id(), load_handle); result_rx.map_err(|_| ()) @@ -2320,7 +2322,7 @@ impl Drop for PlayerInternal { let handles: Vec> = { // waiting for the thread while holding the mutex would result in a deadlock - let mut load_handles = self.load_handles.lock(); + let mut load_handles = self.load_handles.lock().expect(LOAD_HANDLES_POISON_MSG); load_handles .drain() From eb7c65e77b60da6544e2f4c1bbf3ba06705ef1b7 Mon Sep 17 00:00:00 2001 From: Dariusz Olszewski <8277636+starypatyk@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:09:36 +0200 Subject: [PATCH 559/561] Fix cross compilation (add required TLS backend selection) (#1594) --- contrib/Dockerfile | 8 ++++---- contrib/cross-compile-armv6hf/docker-build.sh | 2 +- contrib/docker-build.sh | 8 ++++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index cae7a6d7..9b3a5a81 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -8,10 +8,10 @@ # The compiled binaries will be located in /tmp/librespot-build # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" FROM debian:bookworm diff --git a/contrib/cross-compile-armv6hf/docker-build.sh b/contrib/cross-compile-armv6hf/docker-build.sh index 76158e44..08386186 100755 --- a/contrib/cross-compile-armv6hf/docker-build.sh +++ b/contrib/cross-compile-armv6hf/docker-build.sh @@ -14,4 +14,4 @@ PI1_LIB_DIRS=( export RUSTFLAGS="-C linker=$PI1_TOOLS_DIR/bin/arm-linux-gnueabihf-gcc ${PI1_LIB_DIRS[*]/#/-L}" export BINDGEN_EXTRA_CLANG_ARGS=--sysroot=$PI1_TOOLS_SYSROOT_DIR -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" diff --git a/contrib/docker-build.sh b/contrib/docker-build.sh index 50b6b3e1..84131e07 100755 --- a/contrib/docker-build.sh +++ b/contrib/docker-build.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -eux -cargo build --release --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns" -cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns" +cargo build --release --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" +cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend with-libmdns rustls-tls-native-roots" From a407beaa45e140f492f9cc5b3c5cdd3c7fe2ad69 Mon Sep 17 00:00:00 2001 From: Matthias Schaff Date: Wed, 1 Oct 2025 10:22:57 +0200 Subject: [PATCH 560/561] chore: update alpine version to 3.20 and ensure openssl-dev is included (#1608) fixes devcontainer build issue with alpine 3.19, which is not available anymore Co-authored-by: Matthias Schaff --- .devcontainer/Dockerfile.alpine | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile.alpine b/.devcontainer/Dockerfile.alpine index 2d949ad6..08e0f07d 100644 --- a/.devcontainer/Dockerfile.alpine +++ b/.devcontainer/Dockerfile.alpine @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1 -ARG alpine_version=alpine3.19 +ARG alpine_version=alpine3.20 ARG rust_version=1.85.0 FROM rust:${rust_version}-${alpine_version} @@ -15,6 +15,7 @@ RUN apk add --no-cache \ pkgconf \ musl-dev \ # developer dependencies + openssl-dev \ libunwind-dev \ pulseaudio-dev \ portaudio-dev \ From 51a752f4d5e051956807af1c4e59bd2bab7d712e Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Fri, 3 Oct 2025 15:55:52 +0100 Subject: [PATCH 561/561] Add rustfmt and clippy to toolchain components in rust-toolchain.toml (#1607) Be explicit with required components to avoid https://github.com/rust-lang/rustup/issues/4337 --- rust-toolchain.toml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 rust-toolchain.toml diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..367cc1fc --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +components = ["rustfmt", "clippy"]