mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 17:59:24 +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:
parent
1d5c0d8451
commit
ce1ab8ff3f
25 changed files with 1662 additions and 1205 deletions
2378
Cargo.lock
generated
2378
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
37
Cargo.toml
37
Cargo.toml
|
@ -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]
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
100
core/Cargo.toml
100
core/Cargo.toml
|
@ -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"] }
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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> {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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,12 +397,9 @@ 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,
|
|
||||||
Some(token_url),
|
|
||||||
)
|
|
||||||
.set_redirect_uri(redirect_url);
|
.set_redirect_uri(redirect_url);
|
||||||
|
|
||||||
Ok(OAuthClient {
|
Ok(OAuthClient {
|
||||||
|
@ -433,12 +434,9 @@ 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,
|
|
||||||
Some(token_url),
|
|
||||||
)
|
|
||||||
.set_redirect_uri(redirect_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}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
||||||
|
|
|
@ -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 {
|
|
||||||
AudioFormat::F32 => {
|
|
||||||
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
let samples_f32: &[f32] = &converter.f64_to_f32(samples);
|
||||||
let source = rodio::buffer::SamplesBuffer::new(
|
let source = rodio::buffer::SamplesBuffer::new(
|
||||||
NUM_CHANNELS as u16,
|
NUM_CHANNELS as cpal::ChannelCount,
|
||||||
SAMPLE_RATE,
|
SAMPLE_RATE,
|
||||||
samples_f32,
|
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:
|
||||||
|
|
|
@ -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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue