1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-03 01:39:28 +02:00

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
This commit is contained in:
Roderick van Domburg 2025-08-13 13:19:48 +02:00
parent 1d5c0d8451
commit ce1ab8ff3f
No known key found for this signature in database
GPG key ID: 607FA06CB5236AE0
25 changed files with 1662 additions and 1205 deletions

2378
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -56,14 +56,25 @@ version = "0.6.0-dev"
[dependencies] [dependencies]
data-encoding = "2.5" 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 } futures-util = { version = "0.3", default-features = false }
getopts = "0.2" getopts = "0.2"
log = "0.4" log = "0.4"
sha1 = "0.10" sha1 = "0.10"
sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } sysinfo = { version = "0.37", default-features = false, features = ["system"] }
thiserror = "2.0" thiserror = "2"
tokio = { version = "1.40", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } tokio = { version = "1", features = [
"rt",
"macros",
"signal",
"sync",
"parking_lot",
"process",
] }
url = "2.2" url = "2.2"
[features] [features]
@ -97,9 +108,21 @@ available in the official library."""
section = "sound" section = "sound"
priority = "optional" priority = "optional"
assets = [ assets = [
["target/release/librespot", "usr/bin/", "755"], [
["contrib/librespot.service", "lib/systemd/system/", "644"], "target/release/librespot",
["contrib/librespot.user.service", "lib/systemd/user/", "644"] "usr/bin/",
"755",
],
[
"contrib/librespot.service",
"lib/systemd/system/",
"644",
],
[
"contrib/librespot.user.service",
"lib/systemd/user/",
"644",
],
] ]
[workspace.package] [workspace.package]

View file

@ -17,11 +17,11 @@ aes = "0.8"
bytes = "1" bytes = "1"
ctr = "0.9" ctr = "0.9"
futures-util = "0.3" futures-util = "0.3"
hyper = { version = "1.3", features = [] } hyper = "1.6"
hyper-util = { version = "0.1", features = ["client"] } hyper-util = { version = "0.1", features = ["client"] }
http-body-util = "0.1.1" http-body-util = "0.1"
log = "0.4" log = "0.4"
parking_lot = { version = "0.12", features = ["deadlock_detection"] } parking_lot = { version = "0.12", features = ["deadlock_detection"] }
tempfile = "3" tempfile = "3"
thiserror = "2.0" thiserror = "2"
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }

View file

@ -11,13 +11,13 @@ edition = "2021"
[dependencies] [dependencies]
futures-util = "0.3" futures-util = "0.3"
log = "0.4" log = "0.4"
protobuf = "3.5" protobuf = "3.7"
rand = { version = "0.8", default-features = false, features = ["small_rng"] } rand = { version = "0.9", default-features = false, features = ["small_rng"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "2.0" thiserror = "2"
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] } tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"
uuid = { version = "1.11.0", features = ["v4"] } uuid = { version = "1.18", features = ["v4"] }
[dependencies.librespot-core] [dependencies.librespot-core]
path = "../core" path = "../core"

View file

@ -58,7 +58,7 @@ impl<T> ShuffleVec<T> {
let indices: Vec<_> = { let indices: Vec<_> = {
(1..self.vec.len()) (1..self.vec.len())
.rev() .rev()
.map(|i| rng.gen_range(0..i + 1)) .map(|i| rng.random_range(0..i + 1))
.collect() .collect()
}; };
@ -89,7 +89,7 @@ mod test {
#[test] #[test]
fn test_shuffle_with_seed() { 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::<Vec<_>>(); let vec = (0..100).collect::<Vec<_>>();
let base_vec: ShuffleVec<i32> = vec.into(); let base_vec: ShuffleVec<i32> = vec.into();

View file

@ -68,8 +68,8 @@ impl ConnectState {
// we don't need to include the current track, because it is already being played // we don't need to include the current track, because it is already being played
ctx.skip_track = current_track; ctx.skip_track = current_track;
let seed = seed let seed =
.unwrap_or_else(|| rand::thread_rng().gen_range(100_000_000_000..1_000_000_000_000)); seed.unwrap_or_else(|| rand::rng().random_range(100_000_000_000..1_000_000_000_000));
ctx.tracks.shuffle_with_seed(seed); ctx.tracks.shuffle_with_seed(seed);
ctx.set_shuffle_seed(seed); ctx.set_shuffle_seed(seed);

View file

@ -69,7 +69,7 @@ impl<'ct> ConnectState {
pub fn set_current_track_random(&mut self) -> Result<(), Error> { pub fn set_current_track_random(&mut self) -> Result<(), Error> {
let max_tracks = self.get_context(self.active_context)?.tracks.len(); 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) self.set_current_track(rng_track)
} }

View file

@ -20,64 +20,104 @@ version = "0.6.0-dev"
[dependencies] [dependencies]
aes = "0.8" aes = "0.8"
base64 = "0.22" base64 = "0.22"
byteorder = "1.4" byteorder = "1.5"
bytes = "1" bytes = "1"
form_urlencoded = "1.0" form_urlencoded = "1.2"
futures-core = "0.3" futures-core = "0.3"
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] } futures-util = { version = "0.3", features = [
governor = { version = "0.8", default-features = false, features = ["std", "jitter"] } "alloc",
"bilock",
"sink",
"unstable",
] }
governor = { version = "0.10", default-features = false, features = [
"std",
"jitter",
] }
hmac = "0.12" hmac = "0.12"
httparse = "1.7" httparse = "1.10"
http = "1.0" http = "1.3"
hyper = { version = "1.3", features = ["http1", "http2"] } hyper = { version = "1.6", features = ["http1", "http2"] }
hyper-util = { version = "0.1", features = ["client"] } hyper-util = { version = "0.1", features = ["client"] }
http-body-util = "0.1.1" http-body-util = "0.1"
log = "0.4" log = "0.4"
nonzero_ext = "0.3" nonzero_ext = "0.3"
num-bigint = { version = "0.4", features = ["rand"] } num-bigint = "0.4"
num-derive = "0.4" num-derive = "0.4"
num-integer = "0.1" num-integer = "0.1"
num-traits = "0.2" num-traits = "0.2"
once_cell = "1"
parking_lot = { version = "0.12", features = ["deadlock_detection"] } parking_lot = { version = "0.12", features = ["deadlock_detection"] }
pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] } pbkdf2 = { version = "0.12", default-features = false, features = ["hmac"] }
pin-project-lite = "0.2" pin-project-lite = "0.2"
priority-queue = "2.0" priority-queue = "2.5"
protobuf = "3.5" protobuf = "3.7"
quick-xml = { version = "0.37.1", features = ["serialize"] } quick-xml = { version = "0.38", features = ["serialize"] }
rand = "0.8" rand = "0.9"
rsa = "0.9.2" rsa = "0.9"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
sha1 = { version = "0.10", features = ["oid"] } sha1 = { version = "0.10", features = ["oid"] }
shannon = "0.2" shannon = "0.2"
sysinfo = { version = "0.33.0", default-features = false, features = ["system"] } sysinfo = { version = "0.37", default-features = false, features = ["system"] }
thiserror = "2.0" thiserror = "2"
time = { version = "0.3", features = ["formatting", "parsing"] } 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-stream = "0.1"
tokio-util = { version = "0.7", features = ["codec"] } tokio-util = { version = "0.7", features = ["codec"] }
url = "2" url = "2"
uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] } uuid = { version = "1", default-features = false, features = ["v4"] }
data-encoding = "2.5" data-encoding = "2.9"
flate2 = "1.0.33" flate2 = "1.1"
protobuf-json-mapping = "3.5" protobuf-json-mapping = "3.7"
# Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies # Eventually, this should use rustls-platform-verifier to unify the platform-specific dependencies
# but currently, hyper-proxy2 and tokio-tungstenite do not support it. # but currently, hyper-proxy2 and tokio-tungstenite do not support it.
[target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies] [target.'cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))'.dependencies]
hyper-proxy2 = { version = "0.1", default-features = false, features = ["rustls"] } hyper-proxy2 = { version = "0.1", default-features = false, features = [
hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "native-tokio", "http2"] } "rustls",
tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-native-roots"] } ] }
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] [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-proxy2 = { version = "0.1", default-features = false, features = [
hyper-rustls = { version = "0.27.2", default-features = false, features = ["aws-lc-rs", "http1", "logging", "tls12", "webpki-tokio", "http2"] } "rustls-webpki",
tokio-tungstenite = { version = "0.24", default-features = false, features = ["rustls-tls-webpki-roots"] } ] }
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] [build-dependencies]
rand = "0.8" rand = "0.9"
vergen-gitcl = { version = "1.0.0", default-features = false, features = ["build"] } rand_distr = "0.5"
vergen-gitcl = { version = "1.0", default-features = false, features = [
"build",
] }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1", features = ["macros", "parking_lot"] } tokio = { version = "1", features = ["macros", "parking_lot"] }

View file

@ -1,4 +1,5 @@
use rand::{distributions::Alphanumeric, Rng}; use rand::Rng;
use rand_distr::Alphanumeric;
use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder}; use vergen_gitcl::{BuildBuilder, Emitter, GitclBuilder};
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -18,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.expect("Unable to generate the cargo keys!"); .expect("Unable to generate the cargo keys!");
let build_id = match std::env::var("SOURCE_DATE_EPOCH") { let build_id = match std::env::var("SOURCE_DATE_EPOCH") {
Ok(val) => val, Ok(val) => val,
Err(_) => rand::thread_rng() Err(_) => rand::rng()
.sample_iter(Alphanumeric) .sample_iter(Alphanumeric)
.take(8) .take(8)
.map(char::from) .map(char::from)

View file

@ -3,7 +3,7 @@ use std::{env::consts::ARCH, io};
use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
use hmac::{Hmac, Mac}; use hmac::{Hmac, Mac};
use protobuf::Message; use protobuf::Message;
use rand::{thread_rng, RngCore}; use rand::RngCore;
use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey}; use rsa::{BigUint, Pkcs1v15Sign, RsaPublicKey};
use sha1::{Digest, Sha1}; use sha1::{Digest, Sha1};
use thiserror::Error; use thiserror::Error;
@ -49,7 +49,7 @@ pub enum HandshakeError {
pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>( pub async fn handshake<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T, mut connection: T,
) -> io::Result<Framed<T, ApCodec>> { ) -> io::Result<Framed<T, ApCodec>> {
let local_keys = DhLocalKeys::random(&mut thread_rng()); let local_keys = DhLocalKeys::random(&mut rand::rng());
let gc = local_keys.public_key(); let gc = local_keys.public_key();
let mut accumulator = client_hello(&mut connection, gc).await?; let mut accumulator = client_hello(&mut connection, gc).await?;
let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?; let message: APResponseMessage = recv_packet(&mut connection, &mut accumulator).await?;
@ -108,7 +108,7 @@ where
T: AsyncWrite + Unpin, T: AsyncWrite + Unpin,
{ {
let mut client_nonce = vec![0; 0x10]; 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 { let platform = match crate::config::OS {
"freebsd" | "netbsd" | "openbsd" => match ARCH { "freebsd" | "netbsd" | "openbsd" => match ARCH {

View file

@ -1,6 +1,6 @@
use futures_core::Stream; use futures_core::Stream;
use futures_util::StreamExt; use futures_util::StreamExt;
use std::{cell::OnceCell, pin::Pin, str::FromStr}; use std::{pin::Pin, str::FromStr, sync::OnceLock};
use thiserror::Error; use thiserror::Error;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@ -14,8 +14,8 @@ use crate::{Error, Session};
component! { component! {
DealerManager: DealerManagerInner { DealerManager: DealerManagerInner {
builder: OnceCell<Builder> = OnceCell::from(Builder::new()), builder: OnceLock<Builder> = OnceLock::from(Builder::new()),
dealer: OnceCell<Dealer> = OnceCell::new(), dealer: OnceLock<Dealer> = OnceLock::new(),
} }
} }

View file

@ -88,7 +88,7 @@ impl Responder {
}) })
.to_string(); .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); warn!("Wasn't able to reply to dealer request: {}", e);
} }
} }
@ -586,7 +586,10 @@ async fn connect(
timer.tick().await; timer.tick().await;
pong_received.store(false, atomic::Ordering::Relaxed); 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. // The sender is closed.
break; break;
} }

View file

@ -1,11 +1,12 @@
use num_bigint::{BigUint, RandBigInt}; use std::sync::LazyLock;
use num_bigint::BigUint;
use num_integer::Integer; use num_integer::Integer;
use num_traits::{One, Zero}; use num_traits::{One, Zero};
use once_cell::sync::Lazy;
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); static DH_GENERATOR: LazyLock<BigUint> = LazyLock::new(|| BigUint::from_bytes_be(&[0x02]));
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| { static DH_PRIME: LazyLock<BigUint> = LazyLock::new(|| {
BigUint::from_bytes_be(&[ BigUint::from_bytes_be(&[
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 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, 0x34, 0xc4, 0xc6, 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, 0x08, 0x8a, 0x67,
@ -40,7 +41,9 @@ pub struct DhLocalKeys {
impl DhLocalKeys { impl DhLocalKeys {
pub fn random<R: Rng + CryptoRng>(rng: &mut R) -> DhLocalKeys { pub fn random<R: Rng + CryptoRng>(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); let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME);
DhLocalKeys { DhLocalKeys {

View file

@ -1,5 +1,6 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
sync::OnceLock,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -18,7 +19,6 @@ use hyper_util::{
rt::TokioExecutor, rt::TokioExecutor,
}; };
use nonzero_ext::nonzero; use nonzero_ext::nonzero;
use once_cell::sync::OnceCell;
use parking_lot::Mutex; use parking_lot::Mutex;
use thiserror::Error; use thiserror::Error;
use url::Url; use url::Url;
@ -94,7 +94,7 @@ type HyperClient = Client<ProxyConnector<HttpsConnector<HttpConnector>>, Full<by
pub struct HttpClient { pub struct HttpClient {
user_agent: HeaderValue, user_agent: HeaderValue,
proxy_url: Option<Url>, proxy_url: Option<Url>,
hyper_client: OnceCell<HyperClient>, hyper_client: OnceLock<HyperClient>,
// while the DashMap variant is more performant, our level of concurrency // while the DashMap variant is more performant, our level of concurrency
// is pretty low so we can save pulling in that extra dependency // is pretty low so we can save pulling in that extra dependency
@ -138,7 +138,7 @@ impl HttpClient {
Self { Self {
user_agent, user_agent,
proxy_url: proxy_url.cloned(), proxy_url: proxy_url.cloned(),
hyper_client: OnceCell::new(), hyper_client: OnceLock::new(),
rate_limiter, rate_limiter,
} }
} }
@ -170,9 +170,9 @@ impl HttpClient {
Ok(client) Ok(client)
} }
fn hyper_client(&self) -> Result<&HyperClient, Error> { fn hyper_client(&self) -> &HyperClient {
self.hyper_client 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<Bytes>) -> Result<Response<Incoming>, Error> { pub async fn request(&self, req: Request<Bytes>) -> Result<Response<Incoming>, 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<HeaderValue>) -> Option<Duration> { pub fn get_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {

View file

@ -4,6 +4,7 @@ use std::{
io, io,
pin::Pin, pin::Pin,
process::exit, process::exit,
sync::OnceLock,
sync::{Arc, Weak}, sync::{Arc, Weak},
task::{Context, Poll}, task::{Context, Poll},
time::{Duration, SystemTime, UNIX_EPOCH}, time::{Duration, SystemTime, UNIX_EPOCH},
@ -33,7 +34,6 @@ use futures_core::TryStream;
use futures_util::StreamExt; use futures_util::StreamExt;
use librespot_protocol::authentication::AuthenticationType; use librespot_protocol::authentication::AuthenticationType;
use num_traits::FromPrimitive; use num_traits::FromPrimitive;
use once_cell::sync::OnceCell;
use parking_lot::RwLock; use parking_lot::RwLock;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use quick_xml::events::Event; use quick_xml::events::Event;
@ -68,6 +68,12 @@ impl From<SessionError> for Error {
} }
} }
impl From<quick_xml::encoding::EncodingError> for Error {
fn from(err: quick_xml::encoding::EncodingError) -> Self {
Error::invalid_argument(err)
}
}
pub type UserAttributes = HashMap<String, String>; pub type UserAttributes = HashMap<String, String>;
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
@ -96,16 +102,16 @@ struct SessionInternal {
data: RwLock<SessionData>, data: RwLock<SessionData>,
http_client: HttpClient, http_client: HttpClient,
tx_connection: OnceCell<mpsc::UnboundedSender<(u8, Vec<u8>)>>, tx_connection: OnceLock<mpsc::UnboundedSender<(u8, Vec<u8>)>>,
apresolver: OnceCell<ApResolver>, apresolver: OnceLock<ApResolver>,
audio_key: OnceCell<AudioKeyManager>, audio_key: OnceLock<AudioKeyManager>,
channel: OnceCell<ChannelManager>, channel: OnceLock<ChannelManager>,
mercury: OnceCell<MercuryManager>, mercury: OnceLock<MercuryManager>,
dealer: OnceCell<DealerManager>, dealer: OnceLock<DealerManager>,
spclient: OnceCell<SpClient>, spclient: OnceLock<SpClient>,
token_provider: OnceCell<TokenProvider>, token_provider: OnceLock<TokenProvider>,
login5: OnceCell<Login5Manager>, login5: OnceLock<Login5Manager>,
cache: Option<Arc<Cache>>, cache: Option<Arc<Cache>>,
handle: tokio::runtime::Handle, handle: tokio::runtime::Handle,
@ -140,16 +146,16 @@ impl Session {
config, config,
data: RwLock::new(session_data), data: RwLock::new(session_data),
http_client, http_client,
tx_connection: OnceCell::new(), tx_connection: OnceLock::new(),
cache: cache.map(Arc::new), cache: cache.map(Arc::new),
apresolver: OnceCell::new(), apresolver: OnceLock::new(),
audio_key: OnceCell::new(), audio_key: OnceLock::new(),
channel: OnceCell::new(), channel: OnceLock::new(),
mercury: OnceCell::new(), mercury: OnceLock::new(),
dealer: OnceCell::new(), dealer: OnceLock::new(),
spclient: OnceCell::new(), spclient: OnceLock::new(),
token_provider: OnceCell::new(), token_provider: OnceLock::new(),
login5: OnceCell::new(), login5: OnceLock::new(),
handle: tokio::runtime::Handle::current(), handle: tokio::runtime::Handle::current(),
})) }))
} }
@ -688,8 +694,10 @@ where
} }
Ok(Event::Text(ref value)) => { Ok(Event::Text(ref value)) => {
if !current_element.is_empty() { if !current_element.is_empty() {
let _ = user_attributes let _ = user_attributes.insert(
.insert(current_element.clone(), value.unescape()?.to_string()); current_element.clone(),
value.xml_content()?.to_string(),
);
} }
} }
Ok(Event::Eof) => break, Ok(Event::Eof) => break,

View file

@ -483,7 +483,7 @@ impl SpClient {
url, url,
"{}salt={}", "{}salt={}",
util::get_next_query_separator(&url), util::get_next_query_separator(&url),
rand::thread_rng().next_u32() rand::rng().next_u32()
); );
} }

View file

@ -13,24 +13,32 @@ aes = "0.8"
base64 = "0.22" base64 = "0.22"
bytes = "1" bytes = "1"
ctr = "0.9" ctr = "0.9"
dns-sd = { version = "0.1.3", optional = true } dns-sd = { version = "0.1", optional = true }
form_urlencoded = "1.0" form_urlencoded = "1.2"
futures-core = "0.3" futures-core = "0.3"
futures-util = "0.3" futures-util = "0.3"
hmac = "0.12" hmac = "0.12"
hyper = { version = "1.3", features = ["http1"] } hyper = { version = "1.6", features = ["http1"] }
hyper-util = { version = "0.1", features = ["server-auto", "server-graceful", "service"] } hyper-util = { version = "0.1", features = [
http-body-util = "0.1.1" "server-auto",
"server-graceful",
"service",
] }
http-body-util = "0.1"
libmdns = { version = "0.9", optional = true } libmdns = { version = "0.9", optional = true }
log = "0.4" log = "0.4"
rand = "0.8" rand = "0.9"
serde = { version = "1", default-features = false, features = ["derive"], optional = true } serde = { version = "1", default-features = false, features = [
"derive",
], optional = true }
serde_repr = "0.1" serde_repr = "0.1"
serde_json = "1.0" serde_json = "1.0"
sha1 = "0.10" sha1 = "0.10"
thiserror = "2.0" thiserror = "2"
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] } 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] [dependencies.librespot-core]
path = "../core" path = "../core"

View file

@ -51,7 +51,7 @@ impl RequestHandler {
Self { Self {
config, config,
username: Mutex::new(None), username: Mutex::new(None),
keys: DhLocalKeys::random(&mut rand::thread_rng()), keys: DhLocalKeys::random(&mut rand::rng()),
event_tx, event_tx,
} }
} }

View file

@ -12,8 +12,8 @@ edition = "2021"
async-trait = "0.1" async-trait = "0.1"
bytes = "1" bytes = "1"
log = "0.4" log = "0.4"
protobuf = "3.5" protobuf = "3.7"
thiserror = "2.0" thiserror = "2"
uuid = { version = "1", default-features = false } uuid = { version = "1", default-features = false }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"

View file

@ -10,11 +10,16 @@ edition = "2021"
[dependencies] [dependencies]
log = "0.4" log = "0.4"
oauth2 = "4.4" oauth2 = { version = "5.0", features = ["reqwest", "reqwest-blocking"] }
reqwest = { version = "0.12", features = ["blocking"] }
open = "5.3" open = "5.3"
thiserror = "2.0" thiserror = "2"
url = "2.2" url = "2.5"
[dev-dependencies] [dev-dependencies]
env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] } env_logger = { version = "0.11", default-features = false, features = [
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] } "color",
"humantime",
"auto-color",
] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }

View file

@ -13,12 +13,12 @@
use log::{error, info, trace}; use log::{error, info, trace};
use oauth2::basic::BasicTokenType; use oauth2::basic::BasicTokenType;
use oauth2::reqwest::{async_http_client, http_client};
use oauth2::{ use oauth2::{
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge, basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, EndpointNotSet,
RedirectUrl, Scope, TokenResponse, TokenUrl, EndpointSet, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl,
}; };
use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse}; use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse};
use reqwest;
use std::io; use std::io;
use std::sync::mpsc; use std::sync::mpsc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -214,7 +214,7 @@ pub struct OAuthClient {
redirect_uri: String, redirect_uri: String,
should_open_url: bool, should_open_url: bool,
message: String, message: String,
client: BasicClient, client: BasicClient<EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet>,
} }
impl OAuthClient { impl OAuthClient {
@ -281,10 +281,11 @@ impl OAuthClient {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let client = self.client.clone(); let client = self.client.clone();
std::thread::spawn(move || { std::thread::spawn(move || {
let http_client = reqwest::blocking::Client::new();
let resp = client let resp = client
.exchange_code(code) .exchange_code(code)
.set_pkce_verifier(pkce_verifier) .set_pkce_verifier(pkce_verifier)
.request(http_client); .request(&http_client);
if let Err(e) = tx.send(resp) { if let Err(e) = tx.send(resp) {
error!("OAuth channel send error: {e}"); error!("OAuth channel send error: {e}");
} }
@ -299,10 +300,11 @@ impl OAuthClient {
/// Synchronously obtain a new valid OAuth token from `refresh_token` /// Synchronously obtain a new valid OAuth token from `refresh_token`
pub fn refresh_token(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> { pub fn refresh_token(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> {
let refresh_token = RefreshToken::new(refresh_token.to_string()); let refresh_token = RefreshToken::new(refresh_token.to_string());
let http_client = reqwest::blocking::Client::new();
let resp = self let resp = self
.client .client
.exchange_refresh_token(&refresh_token) .exchange_refresh_token(&refresh_token)
.request(http_client); .request(&http_client);
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
self.build_token(resp) self.build_token(resp)
@ -318,11 +320,12 @@ impl OAuthClient {
}?; }?;
trace!("Exchange {code:?} for access token"); trace!("Exchange {code:?} for access token");
let http_client = reqwest::Client::new();
let resp = self let resp = self
.client .client
.exchange_code(code) .exchange_code(code)
.set_pkce_verifier(pkce_verifier) .set_pkce_verifier(pkce_verifier)
.request_async(async_http_client) .request_async(&http_client)
.await; .await;
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; 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` /// Asynchronously obtain a new valid OAuth token from `refresh_token`
pub async fn refresh_token_async(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> { pub async fn refresh_token_async(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> {
let refresh_token = RefreshToken::new(refresh_token.to_string()); let refresh_token = RefreshToken::new(refresh_token.to_string());
let http_client = reqwest::Client::new();
let resp = self let resp = self
.client .client
.exchange_refresh_token(&refresh_token) .exchange_refresh_token(&refresh_token)
.request_async(async_http_client) .request_async(&http_client)
.await; .await;
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?; let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
@ -393,13 +397,10 @@ impl OAuthClientBuilder {
} }
})?; })?;
let client = BasicClient::new( let client = BasicClient::new(ClientId::new(self.client_id.to_string()))
ClientId::new(self.client_id.to_string()), .set_auth_uri(auth_url)
None, .set_token_uri(token_url)
auth_url, .set_redirect_uri(redirect_url);
Some(token_url),
)
.set_redirect_uri(redirect_url);
Ok(OAuthClient { Ok(OAuthClient {
scopes: self.scopes, scopes: self.scopes,
@ -433,13 +434,10 @@ pub fn get_access_token(
uri: redirect_uri.to_string(), uri: redirect_uri.to_string(),
e, e,
})?; })?;
let client = BasicClient::new( let client = BasicClient::new(ClientId::new(client_id.to_string()))
ClientId::new(client_id.to_string()), .set_auth_uri(auth_url)
None, .set_token_uri(token_url)
auth_url, .set_redirect_uri(redirect_url);
Some(token_url),
)
.set_redirect_uri(redirect_url);
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256(); 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. // Do this sync in another thread because I am too stupid to make the async version work.
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
std::thread::spawn(move || { std::thread::spawn(move || {
let http_client = reqwest::blocking::Client::new();
let resp = client let resp = client
.exchange_code(code) .exchange_code(code)
.set_pkce_verifier(pkce_verifier) .set_pkce_verifier(pkce_verifier)
.request(http_client); .request(&http_client);
if let Err(e) = tx.send(resp) { if let Err(e) = tx.send(resp) {
error!("OAuth channel send error: {e}"); error!("OAuth channel send error: {e}");
} }

View file

@ -26,35 +26,46 @@ futures-util = "0.3"
log = "0.4" log = "0.4"
parking_lot = { version = "0.12", features = ["deadlock_detection"] } parking_lot = { version = "0.12", features = ["deadlock_detection"] }
shell-words = "1.1" shell-words = "1.1"
thiserror = "2.0" thiserror = "2"
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] } tokio = { version = "1", features = [
zerocopy = { version = "0.8.13", features = ["derive"] } "parking_lot",
"rt",
"rt-multi-thread",
"sync",
] }
zerocopy = { version = "0.8", features = ["derive"] }
# Backends # Backends
alsa = { version = "0.9.0", optional = true } alsa = { version = "0.9", optional = true }
portaudio-rs = { version = "0.3", optional = true } portaudio-rs = { version = "0.3", optional = true }
libpulse-binding = { version = "2", optional = true, default-features = false } libpulse-binding = { version = "2", optional = true, default-features = false }
libpulse-simple-binding = { version = "2", optional = true, default-features = false } libpulse-simple-binding = { version = "2", optional = true, default-features = false }
jack = { version = "0.13", optional = true } jack = { version = "0.13", optional = true }
sdl2 = { version = "0.37", optional = true } sdl2 = { version = "0.38", optional = true }
gstreamer = { version = "0.23.1", optional = true } gstreamer = { version = "0.24", optional = true }
gstreamer-app = { version = "0.23.0", optional = true } gstreamer-app = { version = "0.24", optional = true }
gstreamer-audio = { version = "0.23.0", optional = true } gstreamer-audio = { version = "0.24", optional = true }
glib = { version = "0.20.3", optional = true } glib = { version = "0.21", optional = true }
# Rodio dependencies # Rodio dependencies
rodio = { version = "0.20.1", optional = true, default-features = false } rodio = { version = "0.21", optional = true, default-features = false, features = [
cpal = { version = "0.15.1", optional = true } "playback",
] }
cpal = { version = "0.16", optional = true }
# Container and audio decoder # 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 # Legacy Ogg container decoder for the passthrough decoder
ogg = { version = "0.9", optional = true } ogg = { version = "0.9", optional = true }
# Dithering # Dithering
rand = { version = "0.8", features = ["small_rng"] } rand = { version = "0.9", features = ["small_rng"] }
rand_distr = "0.4" rand_distr = "0.5"
[features] [features]
alsa-backend = ["alsa"] alsa-backend = ["alsa"]

View file

@ -59,13 +59,24 @@ impl From<RodioError> for SinkError {
} }
} }
impl From<cpal::DefaultStreamConfigError> for RodioError {
fn from(_: cpal::DefaultStreamConfigError) -> RodioError {
RodioError::NoDeviceAvailable
}
}
impl From<cpal::SupportedStreamConfigsError> for RodioError {
fn from(_: cpal::SupportedStreamConfigsError) -> RodioError {
RodioError::NoDeviceAvailable
}
}
pub struct RodioSink { pub struct RodioSink {
rodio_sink: rodio::Sink, rodio_sink: rodio::Sink,
format: AudioFormat,
_stream: rodio::OutputStream, _stream: rodio::OutputStream,
} }
fn list_formats(device: &rodio::Device) { fn list_formats(device: &cpal::Device) {
match device.default_output_config() { match device.default_output_config() {
Ok(cfg) => { Ok(cfg) => {
debug!(" Default config:"); debug!(" Default config:");
@ -134,8 +145,9 @@ fn list_outputs(host: &cpal::Host) -> Result<(), cpal::DevicesError> {
fn create_sink( fn create_sink(
host: &cpal::Host, host: &cpal::Host,
device: Option<String>, device: Option<String>,
format: AudioFormat,
) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { ) -> 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) { Some("?") => match list_outputs(host) {
Ok(()) => exit(0), Ok(()) => exit(0),
Err(e) => { Err(e) => {
@ -144,6 +156,7 @@ fn create_sink(
} }
}, },
Some(device_name) => { Some(device_name) => {
// Ignore devices for which getting name fails, or format doesn't match
host.output_devices()? host.output_devices()?
.find(|d| d.name().ok().is_some_and(|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()))? .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))?
@ -153,14 +166,40 @@ fn create_sink(
.ok_or(RodioError::NoDeviceAvailable)?, .ok_or(RodioError::NoDeviceAvailable)?,
}; };
let name = rodio_device.name().ok(); let name = cpal_device.name().ok();
info!( info!(
"Using audio device: {}", "Using audio device: {}",
name.as_deref().unwrap_or("[unknown name]") name.as_deref().unwrap_or("[unknown name]")
); );
let (stream, handle) = rodio::OutputStream::try_from_device(&rodio_device)?; // First try native stereo 44.1 kHz playback, then fall back to the device default sample rate
let sink = rodio::Sink::try_new(&handle)?; // (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)) Ok((sink, stream))
} }
@ -174,12 +213,11 @@ pub fn open(host: cpal::Host, device: Option<String>, format: AudioFormat) -> Ro
unimplemented!("Rodio currently only supports F32 and S16 formats"); 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"); debug!("Rodio sink was created");
RodioSink { RodioSink {
rodio_sink: sink, rodio_sink: sink,
format,
_stream: stream, _stream: stream,
} }
} }
@ -200,27 +238,13 @@ impl Sink for RodioSink {
let samples = packet let samples = packet
.samples() .samples()
.map_err(|e| RodioError::Samples(e.to_string()))?; .map_err(|e| RodioError::Samples(e.to_string()))?;
match self.format { let samples_f32: &[f32] = &converter.f64_to_f32(samples);
AudioFormat::F32 => { let source = rodio::buffer::SamplesBuffer::new(
let samples_f32: &[f32] = &converter.f64_to_f32(samples); NUM_CHANNELS as cpal::ChannelCount,
let source = rodio::buffer::SamplesBuffer::new( SAMPLE_RATE,
NUM_CHANNELS as u16, samples_f32,
SAMPLE_RATE, );
samples_f32, self.rodio_sink.append(source);
);
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!(),
};
// Chunk sizes seem to be about 256 to 3000 ish items long. // Chunk sizes seem to be about 256 to 3000 ish items long.
// Assuming they're on average 1628 then a half second buffer is: // Assuming they're on average 1628 then a half second buffer is:

View file

@ -43,7 +43,7 @@ impl fmt::Display for dyn Ditherer {
} }
fn create_rng() -> SmallRng { fn create_rng() -> SmallRng {
SmallRng::from_entropy() SmallRng::from_os_rng()
} }
pub struct TriangularDitherer { pub struct TriangularDitherer {
@ -113,7 +113,9 @@ impl Ditherer for HighPassDitherer {
active_channel: 0, active_channel: 0,
previous_noises: [0.0; NUM_CHANNELS as usize], previous_noises: [0.0; NUM_CHANNELS as usize],
cached_rng: create_rng(), 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"),
} }
} }

View file

@ -10,7 +10,7 @@ repository = "https://github.com/librespot-org/librespot"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
protobuf = "3.5" protobuf = "3.7"
[build-dependencies] [build-dependencies]
protobuf-codegen = "3" protobuf-codegen = "3"