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]
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]

View file

@ -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"] }

View file

@ -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"

View file

@ -58,7 +58,7 @@ impl<T> ShuffleVec<T> {
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::<Vec<_>>();
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
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);

View file

@ -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)
}

View file

@ -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"] }

View file

@ -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<dyn std::error::Error>> {
@ -18,7 +19,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.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)

View file

@ -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<T: AsyncRead + AsyncWrite + Unpin>(
mut connection: T,
) -> 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 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 {

View file

@ -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<Builder> = OnceCell::from(Builder::new()),
dealer: OnceCell<Dealer> = OnceCell::new(),
builder: OnceLock<Builder> = OnceLock::from(Builder::new()),
dealer: OnceLock<Dealer> = OnceLock::new(),
}
}

View file

@ -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;
}

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_traits::{One, Zero};
use once_cell::sync::Lazy;
use rand::{CryptoRng, Rng};
static DH_GENERATOR: Lazy<BigUint> = Lazy::new(|| BigUint::from_bytes_be(&[0x02]));
static DH_PRIME: Lazy<BigUint> = Lazy::new(|| {
static DH_GENERATOR: LazyLock<BigUint> = LazyLock::new(|| BigUint::from_bytes_be(&[0x02]));
static DH_PRIME: LazyLock<BigUint> = 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<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);
DhLocalKeys {

View file

@ -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<ProxyConnector<HttpsConnector<HttpConnector>>, Full<by
pub struct HttpClient {
user_agent: HeaderValue,
proxy_url: Option<Url>,
hyper_client: OnceCell<HyperClient>,
hyper_client: OnceLock<HyperClient>,
// 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<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> {

View file

@ -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<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>;
#[derive(Debug, Clone, Default)]
@ -96,16 +102,16 @@ struct SessionInternal {
data: RwLock<SessionData>,
http_client: HttpClient,
tx_connection: OnceCell<mpsc::UnboundedSender<(u8, Vec<u8>)>>,
tx_connection: OnceLock<mpsc::UnboundedSender<(u8, Vec<u8>)>>,
apresolver: OnceCell<ApResolver>,
audio_key: OnceCell<AudioKeyManager>,
channel: OnceCell<ChannelManager>,
mercury: OnceCell<MercuryManager>,
dealer: OnceCell<DealerManager>,
spclient: OnceCell<SpClient>,
token_provider: OnceCell<TokenProvider>,
login5: OnceCell<Login5Manager>,
apresolver: OnceLock<ApResolver>,
audio_key: OnceLock<AudioKeyManager>,
channel: OnceLock<ChannelManager>,
mercury: OnceLock<MercuryManager>,
dealer: OnceLock<DealerManager>,
spclient: OnceLock<SpClient>,
token_provider: OnceLock<TokenProvider>,
login5: OnceLock<Login5Manager>,
cache: Option<Arc<Cache>>,
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,

View file

@ -483,7 +483,7 @@ impl SpClient {
url,
"{}salt={}",
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"
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"

View file

@ -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,
}
}

View file

@ -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"

View file

@ -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"] }

View file

@ -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<EndpointSet, EndpointNotSet, EndpointNotSet, EndpointNotSet, EndpointSet>,
}
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<OAuthToken, OAuthError> {
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<OAuthToken, OAuthError> {
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}");
}

View file

@ -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"]

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 {
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<String>,
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<String>, 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:

View file

@ -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"),
}
}

View file

@ -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"