mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 17:59:24 +02:00
refactor: update to Rust 1.85 and edition 2024, use inline log args
- Update MSRV to 1.85 and Rust edition to 2024. - Refactor all logging macros to use inline argument formatting. - Fix import order in main.rs and examples. - Add async environment variable setter to main.rs as safe facade.
This commit is contained in:
parent
0aec38b07a
commit
6288e7e03c
30 changed files with 419 additions and 448 deletions
|
@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [core] MSRV is now 1.81 (breaking)
|
- [core] MSRV is now 1.85 with Rust edition 2024 (breaking)
|
||||||
- [core] AP connect and handshake have a combined 5 second timeout.
|
- [core] AP connect and handshake have a combined 5 second timeout.
|
||||||
- [core] `stream_from_cdn` now accepts the URL as `TryInto<Uri>` instead of `CdnUrl` (breaking)
|
- [core] `stream_from_cdn` now accepts the URL as `TryInto<Uri>` instead of `CdnUrl` (breaking)
|
||||||
- [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking)
|
- [connect] Replaced `has_volume_ctrl` with `disable_volume` in `ConnectConfig` (breaking)
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot"
|
name = "librespot"
|
||||||
version = "0.6.0-dev"
|
version = "0.6.0-dev"
|
||||||
rust-version = "1.81"
|
rust-version = "1.85"
|
||||||
authors = ["Librespot Org"]
|
authors = ["Librespot Org"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
description = "An open source client library for Spotify, with support for Spotify Connect"
|
description = "An open source client library for Spotify, with support for Spotify Connect"
|
||||||
keywords = ["spotify"]
|
keywords = ["spotify"]
|
||||||
repository = "https://github.com/librespot-org/librespot"
|
repository = "https://github.com/librespot-org/librespot"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
|
@ -126,4 +126,4 @@ assets = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
rust-version = "1.81"
|
rust-version = "1.85"
|
||||||
|
|
|
@ -366,11 +366,11 @@ impl AudioFile {
|
||||||
bytes_per_second: usize,
|
bytes_per_second: usize,
|
||||||
) -> Result<AudioFile, Error> {
|
) -> Result<AudioFile, Error> {
|
||||||
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
if let Some(file) = session.cache().and_then(|cache| cache.file(file_id)) {
|
||||||
debug!("File {} already in cache", file_id);
|
debug!("File {file_id} already in cache");
|
||||||
return Ok(AudioFile::Cached(file));
|
return Ok(AudioFile::Cached(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Downloading file {}", file_id);
|
debug!("Downloading file {file_id}");
|
||||||
|
|
||||||
let (complete_tx, complete_rx) = oneshot::channel();
|
let (complete_tx, complete_rx) = oneshot::channel();
|
||||||
|
|
||||||
|
@ -379,14 +379,14 @@ impl AudioFile {
|
||||||
|
|
||||||
let session_ = session.clone();
|
let session_ = session.clone();
|
||||||
session.spawn(complete_rx.map_ok(move |mut file| {
|
session.spawn(complete_rx.map_ok(move |mut file| {
|
||||||
debug!("Downloading file {} complete", file_id);
|
debug!("Downloading file {file_id} complete");
|
||||||
|
|
||||||
if let Some(cache) = session_.cache() {
|
if let Some(cache) = session_.cache() {
|
||||||
if let Some(cache_id) = cache.file_path(file_id) {
|
if let Some(cache_id) = cache.file_path(file_id) {
|
||||||
if let Err(e) = cache.save_file(file_id, &mut file) {
|
if let Err(e) = cache.save_file(file_id, &mut file) {
|
||||||
error!("Error caching file {} to {:?}: {}", file_id, cache_id, e);
|
error!("Error caching file {file_id} to {cache_id:?}: {e}");
|
||||||
} else {
|
} else {
|
||||||
debug!("File {} cached to {:?}", file_id, cache_id);
|
debug!("File {file_id} cached to {cache_id:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -465,14 +465,11 @@ impl AudioFileStreaming {
|
||||||
)));
|
)));
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Streaming from {}", url);
|
trace!("Streaming from {url}");
|
||||||
|
|
||||||
let code = response.status();
|
let code = response.status();
|
||||||
if code != StatusCode::PARTIAL_CONTENT {
|
if code != StatusCode::PARTIAL_CONTENT {
|
||||||
debug!(
|
debug!("Opening audio file expected partial content but got: {code}");
|
||||||
"Opening audio file expected partial content but got: {}",
|
|
||||||
code
|
|
||||||
);
|
|
||||||
return Err(AudioFileError::StatusCode(code).into());
|
return Err(AudioFileError::StatusCode(code).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -166,7 +166,7 @@ impl Spirc {
|
||||||
}
|
}
|
||||||
|
|
||||||
let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel);
|
let spirc_id = SPIRC_COUNTER.fetch_add(1, Ordering::AcqRel);
|
||||||
debug!("new Spirc[{}]", spirc_id);
|
debug!("new Spirc[{spirc_id}]");
|
||||||
|
|
||||||
let connect_state = ConnectState::new(config, &session);
|
let connect_state = ConnectState::new(config, &session);
|
||||||
|
|
||||||
|
@ -446,14 +446,14 @@ impl SpircTask {
|
||||||
cluster_update = self.connect_state_update.next() => unwrap! {
|
cluster_update = self.connect_state_update.next() => unwrap! {
|
||||||
cluster_update,
|
cluster_update,
|
||||||
match |cluster_update| if let Err(e) = self.handle_cluster_update(cluster_update).await {
|
match |cluster_update| if let Err(e) = self.handle_cluster_update(cluster_update).await {
|
||||||
error!("could not dispatch connect state update: {}", e);
|
error!("could not dispatch connect state update: {e}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// main dealer request handling (dealer expects an answer)
|
// main dealer request handling (dealer expects an answer)
|
||||||
request = self.connect_state_command.next() => unwrap! {
|
request = self.connect_state_command.next() => unwrap! {
|
||||||
request,
|
request,
|
||||||
|request| if let Err(e) = self.handle_connect_state_request(request).await {
|
|request| if let Err(e) = self.handle_connect_state_request(request).await {
|
||||||
error!("couldn't handle connect state command: {}", e);
|
error!("couldn't handle connect state command: {e}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// volume request handling is send separately (it's more like a fire forget)
|
// volume request handling is send separately (it's more like a fire forget)
|
||||||
|
@ -491,12 +491,12 @@ impl SpircTask {
|
||||||
},
|
},
|
||||||
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
|
cmd = async { commands?.recv().await }, if commands.is_some() => if let Some(cmd) = cmd {
|
||||||
if let Err(e) = self.handle_command(cmd).await {
|
if let Err(e) = self.handle_command(cmd).await {
|
||||||
debug!("could not dispatch command: {}", e);
|
debug!("could not dispatch command: {e}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event {
|
event = async { player_events?.recv().await }, if player_events.is_some() => if let Some(event) = event {
|
||||||
if let Err(e) = self.handle_player_event(event) {
|
if let Err(e) = self.handle_player_event(event) {
|
||||||
error!("could not dispatch player event: {}", e);
|
error!("could not dispatch player event: {e}");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ = async { sleep(UPDATE_STATE_DELAY).await }, if self.update_state => {
|
_ = async { sleep(UPDATE_STATE_DELAY).await }, if self.update_state => {
|
||||||
|
@ -606,7 +606,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
|
async fn handle_command(&mut self, cmd: SpircCommand) -> Result<(), Error> {
|
||||||
trace!("Received SpircCommand::{:?}", cmd);
|
trace!("Received SpircCommand::{cmd:?}");
|
||||||
match cmd {
|
match cmd {
|
||||||
SpircCommand::Shutdown => {
|
SpircCommand::Shutdown => {
|
||||||
trace!("Received SpircCommand::Shutdown");
|
trace!("Received SpircCommand::Shutdown");
|
||||||
|
@ -618,16 +618,15 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SpircCommand::Activate if !self.connect_state.is_active() => {
|
SpircCommand::Activate if !self.connect_state.is_active() => {
|
||||||
trace!("Received SpircCommand::{:?}", cmd);
|
trace!("Received SpircCommand::{cmd:?}");
|
||||||
self.handle_activate();
|
self.handle_activate();
|
||||||
return self.notify().await;
|
return self.notify().await;
|
||||||
}
|
}
|
||||||
SpircCommand::Activate => warn!(
|
SpircCommand::Activate => {
|
||||||
"SpircCommand::{:?} will be ignored while already active",
|
warn!("SpircCommand::{cmd:?} will be ignored while already active")
|
||||||
cmd
|
}
|
||||||
),
|
|
||||||
_ if !self.connect_state.is_active() => {
|
_ if !self.connect_state.is_active() => {
|
||||||
warn!("SpircCommand::{:?} will be ignored while Not Active", cmd)
|
warn!("SpircCommand::{cmd:?} will be ignored while Not Active")
|
||||||
}
|
}
|
||||||
SpircCommand::Disconnect { pause } => {
|
SpircCommand::Disconnect { pause } => {
|
||||||
if pause {
|
if pause {
|
||||||
|
@ -787,7 +786,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> {
|
async fn handle_connection_id_update(&mut self, connection_id: String) -> Result<(), Error> {
|
||||||
trace!("Received connection ID update: {:?}", connection_id);
|
trace!("Received connection ID update: {connection_id:?}");
|
||||||
self.session.set_connection_id(&connection_id);
|
self.session.set_connection_id(&connection_id);
|
||||||
|
|
||||||
let cluster = match self
|
let cluster = match self
|
||||||
|
@ -837,7 +836,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) {
|
fn handle_user_attributes_update(&mut self, update: UserAttributesUpdate) {
|
||||||
trace!("Received attributes update: {:#?}", update);
|
trace!("Received attributes update: {update:#?}");
|
||||||
let attributes: UserAttributes = update
|
let attributes: UserAttributes = update
|
||||||
.pairs
|
.pairs
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -863,12 +862,7 @@ impl SpircTask {
|
||||||
};
|
};
|
||||||
self.session.set_user_attribute(key, new_value);
|
self.session.set_user_attribute(key, new_value);
|
||||||
|
|
||||||
trace!(
|
trace!("Received attribute mutation, {key} was {old_value} is now {new_value}");
|
||||||
"Received attribute mutation, {} was {} is now {}",
|
|
||||||
key,
|
|
||||||
old_value,
|
|
||||||
new_value
|
|
||||||
);
|
|
||||||
|
|
||||||
if key == "filter-explicit-content" && new_value == "1" {
|
if key == "filter-explicit-content" && new_value == "1" {
|
||||||
self.player
|
self.player
|
||||||
|
@ -882,10 +876,7 @@ impl SpircTask {
|
||||||
self.add_autoplay_resolving_when_required()
|
self.add_autoplay_resolving_when_required()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
trace!(
|
trace!("Received attribute mutation for {key} but key was not found!");
|
||||||
"Received attribute mutation for {} but key was not found!",
|
|
||||||
key
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1743,7 +1734,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_volume(&mut self, volume: u16) {
|
fn set_volume(&mut self, volume: u16) {
|
||||||
debug!("SpircTask::set_volume({})", volume);
|
debug!("SpircTask::set_volume({volume})");
|
||||||
|
|
||||||
let old_volume = self.connect_state.device_info().volume;
|
let old_volume = self.connect_state.device_info().volume;
|
||||||
let new_volume = volume as u32;
|
let new_volume = volume as u32;
|
||||||
|
|
|
@ -108,7 +108,7 @@ impl ApResolver {
|
||||||
if inner.data.is_any_empty() {
|
if inner.data.is_any_empty() {
|
||||||
warn!("Failed to resolve all access points, using fallbacks");
|
warn!("Failed to resolve all access points, using fallbacks");
|
||||||
if let Some(error) = error {
|
if let Some(error) = error {
|
||||||
warn!("Resolve access points error: {}", error);
|
warn!("Resolve access points error: {error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let fallback = self.parse_resolve_to_access_points(ApResolveData::fallback());
|
let fallback = self.parse_resolve_to_access_points(ApResolveData::fallback());
|
||||||
|
|
|
@ -70,11 +70,7 @@ impl AudioKeyManager {
|
||||||
.map_err(|_| AudioKeyError::Channel)?
|
.map_err(|_| AudioKeyError::Channel)?
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
trace!(
|
trace!("Did not expect {cmd:?} AES key packet with data {data:#?}");
|
||||||
"Did not expect {:?} AES key packet with data {:#?}",
|
|
||||||
cmd,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
return Err(AudioKeyError::Packet(cmd as u8).into());
|
return Err(AudioKeyError::Packet(cmd as u8).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,7 +141,7 @@ impl FsSizeLimiter {
|
||||||
let list_dir = match fs::read_dir(path) {
|
let list_dir = match fs::read_dir(path) {
|
||||||
Ok(list_dir) => list_dir,
|
Ok(list_dir) => list_dir,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not read directory {:?} in cache dir: {}", path, e);
|
warn!("Could not read directory {path:?} in cache dir: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -150,7 +150,7 @@ impl FsSizeLimiter {
|
||||||
let entry = match entry {
|
let entry = match entry {
|
||||||
Ok(entry) => entry,
|
Ok(entry) => entry,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not directory {:?} in cache dir: {}", path, e);
|
warn!("Could not directory {path:?} in cache dir: {e}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -166,7 +166,7 @@ impl FsSizeLimiter {
|
||||||
limiter.add(&path, size, access_time);
|
limiter.add(&path, size, access_time);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Could not read file {:?} in cache dir: {}", path, e)
|
warn!("Could not read file {path:?} in cache dir: {e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +213,7 @@ impl FsSizeLimiter {
|
||||||
|
|
||||||
let res = fs::remove_file(&file);
|
let res = fs::remove_file(&file);
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
warn!("Could not remove file {:?} from cache dir: {}", file, e);
|
warn!("Could not remove file {file:?} from cache dir: {e}");
|
||||||
last_error = Some(e);
|
last_error = Some(e);
|
||||||
} else {
|
} else {
|
||||||
count += 1;
|
count += 1;
|
||||||
|
@ -221,7 +221,7 @@ impl FsSizeLimiter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if count > 0 {
|
if count > 0 {
|
||||||
info!("Removed {} cache files.", count);
|
info!("Removed {count} cache files.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(err) = last_error {
|
if let Some(err) = last_error {
|
||||||
|
@ -317,7 +317,7 @@ impl Cache {
|
||||||
// If the file did not exist, the file was probably not written
|
// If the file did not exist, the file was probably not written
|
||||||
// before. Otherwise, log the error.
|
// before. Otherwise, log the error.
|
||||||
if e.kind != ErrorKind::NotFound {
|
if e.kind != ErrorKind::NotFound {
|
||||||
warn!("Error reading credentials from cache: {}", e);
|
warn!("Error reading credentials from cache: {e}");
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -332,7 +332,7 @@ impl Cache {
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
warn!("Cannot save credentials to cache: {}", e)
|
warn!("Cannot save credentials to cache: {e}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -351,7 +351,7 @@ impl Cache {
|
||||||
Ok(v) => Some(v),
|
Ok(v) => Some(v),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind != ErrorKind::NotFound {
|
if e.kind != ErrorKind::NotFound {
|
||||||
warn!("Error reading volume from cache: {}", e);
|
warn!("Error reading volume from cache: {e}");
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -362,7 +362,7 @@ impl Cache {
|
||||||
if let Some(ref location) = self.volume_location {
|
if let Some(ref location) = self.volume_location {
|
||||||
let result = File::create(location).and_then(|mut file| write!(file, "{volume}"));
|
let result = File::create(location).and_then(|mut file| write!(file, "{volume}"));
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
warn!("Cannot save volume to cache: {}", e);
|
warn!("Cannot save volume to cache: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -375,7 +375,7 @@ impl Cache {
|
||||||
path
|
path
|
||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Invalid FileId: {}", e);
|
warn!("Invalid FileId: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -387,14 +387,14 @@ impl Cache {
|
||||||
Ok(file) => {
|
Ok(file) => {
|
||||||
if let Some(limiter) = self.size_limiter.as_deref() {
|
if let Some(limiter) = self.size_limiter.as_deref() {
|
||||||
if !limiter.touch(&path) {
|
if !limiter.touch(&path) {
|
||||||
error!("limiter could not touch {:?}", path);
|
error!("limiter could not touch {path:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(file)
|
Some(file)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
if e.kind() != io::ErrorKind::NotFound {
|
if e.kind() != io::ErrorKind::NotFound {
|
||||||
warn!("Error reading file from cache: {}", e)
|
warn!("Error reading file from cache: {e}")
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ impl CdnUrl {
|
||||||
|
|
||||||
let cdn_url = Self { file_id, urls };
|
let cdn_url = Self { file_id, urls };
|
||||||
|
|
||||||
trace!("Resolved CDN storage: {:#?}", cdn_url);
|
trace!("Resolved CDN storage: {cdn_url:#?}");
|
||||||
|
|
||||||
Ok(cdn_url)
|
Ok(cdn_url)
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,11 +186,7 @@ pub async fn authenticate(
|
||||||
Err(error_data.into())
|
Err(error_data.into())
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
trace!(
|
trace!("Did not expect {cmd:?} AES key packet with data {data:#?}");
|
||||||
"Did not expect {:?} AES key packet with data {:#?}",
|
|
||||||
cmd,
|
|
||||||
data
|
|
||||||
);
|
|
||||||
Err(AuthenticationError::Packet(cmd))
|
Err(AuthenticationError::Packet(cmd))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -89,7 +89,7 @@ impl Responder {
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
if let Err(e) = self.tx.send(WsMessage::Text(response.into())) {
|
if let Err(e) = self.tx.send(WsMessage::Text(response.into())) {
|
||||||
warn!("Wasn't able to reply to dealer request: {}", e);
|
warn!("Wasn't able to reply to dealer request: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -452,7 +452,7 @@ impl Dealer {
|
||||||
|
|
||||||
if let Some(handle) = self.handle.take() {
|
if let Some(handle) = self.handle.take() {
|
||||||
if let Err(e) = CancelOnDrop(handle).await {
|
if let Err(e) = CancelOnDrop(handle).await {
|
||||||
error!("error aborting dealer operations: {}", e);
|
error!("error aborting dealer operations: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,13 +524,13 @@ async fn connect(
|
||||||
Ok(close_frame) => ws_tx.send(WsMessage::Close(close_frame)).await,
|
Ok(close_frame) => ws_tx.send(WsMessage::Close(close_frame)).await,
|
||||||
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => ws_tx.flush().await,
|
Err(WsError::AlreadyClosed) | Err(WsError::ConnectionClosed) => ws_tx.flush().await,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Dealer finished with an error: {}", e);
|
warn!("Dealer finished with an error: {e}");
|
||||||
ws_tx.send(WsMessage::Close(None)).await
|
ws_tx.send(WsMessage::Close(None)).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
warn!("Error while closing websocket: {}", e);
|
warn!("Error while closing websocket: {e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Dropping send task");
|
debug!("Dropping send task");
|
||||||
|
@ -565,7 +565,7 @@ async fn connect(
|
||||||
_ => (), // tungstenite handles Close and Ping automatically
|
_ => (), // tungstenite handles Close and Ping automatically
|
||||||
},
|
},
|
||||||
Some(Err(e)) => {
|
Some(Err(e)) => {
|
||||||
warn!("Websocket connection failed: {}", e);
|
warn!("Websocket connection failed: {e}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -648,13 +648,13 @@ where
|
||||||
() = shared.closed() => break,
|
() = shared.closed() => break,
|
||||||
r = t0 => {
|
r = t0 => {
|
||||||
if let Err(e) = r {
|
if let Err(e) = r {
|
||||||
error!("timeout on task 0: {}", e);
|
error!("timeout on task 0: {e}");
|
||||||
}
|
}
|
||||||
tasks.0.take();
|
tasks.0.take();
|
||||||
},
|
},
|
||||||
r = t1 => {
|
r = t1 => {
|
||||||
if let Err(e) = r {
|
if let Err(e) = r {
|
||||||
error!("timeout on task 1: {}", e);
|
error!("timeout on task 1: {e}");
|
||||||
}
|
}
|
||||||
tasks.1.take();
|
tasks.1.take();
|
||||||
}
|
}
|
||||||
|
@ -671,7 +671,7 @@ where
|
||||||
match connect(&url, proxy.as_ref(), &shared).await {
|
match connect(&url, proxy.as_ref(), &shared).await {
|
||||||
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
|
Ok((s, r)) => tasks = (init_task(s), init_task(r)),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Error while connecting: {}", e);
|
error!("Error while connecting: {e}");
|
||||||
tokio::time::sleep(RECONNECT_INTERVAL).await;
|
tokio::time::sleep(RECONNECT_INTERVAL).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,7 @@ impl HttpClient {
|
||||||
);
|
);
|
||||||
|
|
||||||
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
|
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
|
||||||
error!("Invalid user agent <{}>: {}", user_agent_str, err);
|
error!("Invalid user agent <{user_agent_str}>: {err}");
|
||||||
HeaderValue::from_static(FALLBACK_USER_AGENT)
|
HeaderValue::from_static(FALLBACK_USER_AGENT)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -176,7 +176,7 @@ impl HttpClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request(&self, req: Request<Bytes>) -> Result<Response<Incoming>, Error> {
|
pub async fn request(&self, req: Request<Bytes>) -> Result<Response<Incoming>, Error> {
|
||||||
debug!("Requesting {}", req.uri().to_string());
|
debug!("Requesting {}", req.uri());
|
||||||
|
|
||||||
// `Request` does not implement `Clone` because its `Body` may be a single-shot stream.
|
// `Request` does not implement `Clone` because its `Body` may be a single-shot stream.
|
||||||
// As correct as that may be technically, we now need all this boilerplate to clone it
|
// As correct as that may be technically, we now need all this boilerplate to clone it
|
||||||
|
|
|
@ -199,7 +199,7 @@ impl Login5Manager {
|
||||||
inner.auth_token.clone()
|
inner.auth_token.clone()
|
||||||
});
|
});
|
||||||
|
|
||||||
trace!("Got auth token: {:?}", auth_token);
|
trace!("Got auth token: {auth_token:?}");
|
||||||
|
|
||||||
token.ok_or(Login5Error::NoStoredCredentials.into())
|
token.ok_or(Login5Error::NoStoredCredentials.into())
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,12 +131,12 @@ impl MercuryManager {
|
||||||
Ok(mut sub) => {
|
Ok(mut sub) => {
|
||||||
let sub_uri = sub.take_uri();
|
let sub_uri = sub.take_uri();
|
||||||
|
|
||||||
debug!("subscribed sub_uri={}", sub_uri);
|
debug!("subscribed sub_uri={sub_uri}");
|
||||||
|
|
||||||
inner.subscriptions.push((sub_uri, tx.clone()));
|
inner.subscriptions.push((sub_uri, tx.clone()));
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("could not subscribe to {}: {}", uri, e);
|
error!("could not subscribe to {uri}: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,7 +163,7 @@ impl MercuryManager {
|
||||||
|
|
||||||
manager.lock(move |inner| {
|
manager.lock(move |inner| {
|
||||||
if !inner.invalid {
|
if !inner.invalid {
|
||||||
debug!("listening to uri={}", uri);
|
debug!("listening to uri={uri}");
|
||||||
inner.subscriptions.push((uri, tx));
|
inner.subscriptions.push((uri, tx));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -283,14 +283,14 @@ impl MercuryManager {
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
debug!("unknown subscription uri={}", &response.uri);
|
debug!("unknown subscription uri={}", &response.uri);
|
||||||
trace!("response pushed over Mercury: {:?}", response);
|
trace!("response pushed over Mercury: {response:?}");
|
||||||
Err(MercuryError::Response(response).into())
|
Err(MercuryError::Response(response).into())
|
||||||
}
|
}
|
||||||
} else if let Some(cb) = pending.callback {
|
} else if let Some(cb) = pending.callback {
|
||||||
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
|
cb.send(Ok(response)).map_err(|_| MercuryError::Channel)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
} else {
|
} else {
|
||||||
error!("can't handle Mercury response: {:?}", response);
|
error!("can't handle Mercury response: {response:?}");
|
||||||
Err(MercuryError::Response(response).into())
|
Err(MercuryError::Response(response).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -273,7 +273,7 @@ impl Session {
|
||||||
let session_weak = self.weak();
|
let session_weak = self.weak();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = sender_task.await {
|
if let Err(e) = sender_task.await {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
if let Some(session) = session_weak.try_upgrade() {
|
if let Some(session) = session_weak.try_upgrade() {
|
||||||
if !session.is_invalid() {
|
if !session.is_invalid() {
|
||||||
session.shutdown();
|
session.shutdown();
|
||||||
|
@ -360,7 +360,7 @@ impl Session {
|
||||||
fn check_catalogue(attributes: &UserAttributes) {
|
fn check_catalogue(attributes: &UserAttributes) {
|
||||||
if let Some(account_type) = attributes.get("type") {
|
if let Some(account_type) = attributes.get("type") {
|
||||||
if account_type != "premium" {
|
if account_type != "premium" {
|
||||||
error!("librespot does not support {:?} accounts.", account_type);
|
error!("librespot does not support {account_type:?} accounts.");
|
||||||
info!("Please support Spotify and your artists and sign up for a premium account.");
|
info!("Please support Spotify and your artists and sign up for a premium account.");
|
||||||
|
|
||||||
// TODO: logout instead of exiting
|
// TODO: logout instead of exiting
|
||||||
|
@ -566,7 +566,7 @@ impl KeepAliveState {
|
||||||
.map(|t| t.as_secs_f64())
|
.map(|t| t.as_secs_f64())
|
||||||
.unwrap_or(f64::INFINITY);
|
.unwrap_or(f64::INFINITY);
|
||||||
|
|
||||||
trace!("keep-alive state: {:?}, timeout in {:.1}", self, delay);
|
trace!("keep-alive state: {self:?}, timeout in {delay:.1}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -619,7 +619,7 @@ where
|
||||||
let cmd = match packet_type {
|
let cmd = match packet_type {
|
||||||
Some(cmd) => cmd,
|
Some(cmd) => cmd,
|
||||||
None => {
|
None => {
|
||||||
trace!("Ignoring unknown packet {:x}", cmd);
|
trace!("Ignoring unknown packet {cmd:x}");
|
||||||
return Err(SessionError::Packet(cmd).into());
|
return Err(SessionError::Packet(cmd).into());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -667,7 +667,7 @@ where
|
||||||
}
|
}
|
||||||
Some(CountryCode) => {
|
Some(CountryCode) => {
|
||||||
let country = String::from_utf8(data.as_ref().to_owned())?;
|
let country = String::from_utf8(data.as_ref().to_owned())?;
|
||||||
info!("Country: {:?}", country);
|
info!("Country: {country:?}");
|
||||||
session.0.data.write().user_data.country = country;
|
session.0.data.write().user_data.country = country;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -710,7 +710,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Received product info: {:#?}", user_attributes);
|
trace!("Received product info: {user_attributes:#?}");
|
||||||
Session::check_catalogue(&user_attributes);
|
Session::check_catalogue(&user_attributes);
|
||||||
|
|
||||||
session.0.data.write().user_data.attributes = user_attributes;
|
session.0.data.write().user_data.attributes = user_attributes;
|
||||||
|
@ -721,7 +721,7 @@ where
|
||||||
| Some(UnknownDataAllZeros)
|
| Some(UnknownDataAllZeros)
|
||||||
| Some(LicenseVersion) => Ok(()),
|
| Some(LicenseVersion) => Ok(()),
|
||||||
_ => {
|
_ => {
|
||||||
trace!("Ignoring {:?} packet with data {:#?}", cmd, data);
|
trace!("Ignoring {cmd:?} packet with data {data:#?}");
|
||||||
Err(SessionError::Packet(cmd as u8).into())
|
Err(SessionError::Packet(cmd as u8).into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -749,7 +749,7 @@ where
|
||||||
Poll::Ready(Some(Ok((cmd, data)))) => {
|
Poll::Ready(Some(Ok((cmd, data)))) => {
|
||||||
let result = self.as_mut().dispatch(&session, cmd, data);
|
let result = self.as_mut().dispatch(&session, cmd, data);
|
||||||
if let Err(e) = result {
|
if let Err(e) = result {
|
||||||
debug!("could not dispatch command: {}", e);
|
debug!("could not dispatch command: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Poll::Ready(None) => {
|
Poll::Ready(None) => {
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::proxytunnel;
|
||||||
|
|
||||||
pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result<TcpStream> {
|
pub async fn connect(host: &str, port: u16, proxy: Option<&Url>) -> io::Result<TcpStream> {
|
||||||
let socket = if let Some(proxy_url) = proxy {
|
let socket = if let Some(proxy_url) = proxy {
|
||||||
info!("Using proxy \"{}\"", proxy_url);
|
info!("Using proxy \"{proxy_url}\"");
|
||||||
|
|
||||||
let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| {
|
let socket_addr = proxy_url.socket_addrs(|| None).and_then(|addrs| {
|
||||||
addrs.into_iter().next().ok_or_else(|| {
|
addrs.into_iter().next().ok_or_else(|| {
|
||||||
|
|
|
@ -311,20 +311,12 @@ impl SpClient {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!(
|
trace!("Answer not accepted {count}/{MAX_TRIES}: {e}");
|
||||||
"Answer not accepted {}/{}: {}",
|
|
||||||
count,
|
|
||||||
MAX_TRIES,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => trace!(
|
Err(e) => trace!(
|
||||||
"Unable to solve hash cash challenge {}/{}: {}",
|
"Unable to solve hash cash challenge {count}/{MAX_TRIES}: {e}"
|
||||||
count,
|
|
||||||
MAX_TRIES,
|
|
||||||
e
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,7 +365,7 @@ impl SpClient {
|
||||||
inner.client_token = Some(client_token);
|
inner.client_token = Some(client_token);
|
||||||
});
|
});
|
||||||
|
|
||||||
trace!("Got client token: {:?}", granted_token);
|
trace!("Got client token: {granted_token:?}");
|
||||||
|
|
||||||
Ok(access_token)
|
Ok(access_token)
|
||||||
}
|
}
|
||||||
|
@ -542,7 +534,7 @@ impl SpClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Error was: {:?}", last_response);
|
debug!("Error was: {last_response:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
last_response
|
last_response
|
||||||
|
|
|
@ -86,8 +86,7 @@ impl TokenProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Requested token in scopes {:?} unavailable or expired, requesting new token.",
|
"Requested token in scopes {scopes:?} unavailable or expired, requesting new token."
|
||||||
scopes
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let query_uri = format!(
|
let query_uri = format!(
|
||||||
|
@ -100,7 +99,7 @@ impl TokenProvider {
|
||||||
let response = request.await?;
|
let response = request.await?;
|
||||||
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
|
let data = response.payload.first().ok_or(TokenError::Empty)?.to_vec();
|
||||||
let token = Token::from_json(String::from_utf8(data)?)?;
|
let token = Token::from_json(String::from_utf8(data)?)?;
|
||||||
trace!("Got token: {:#?}", token);
|
trace!("Got token: {token:#?}");
|
||||||
self.lock(|inner| inner.tokens.push(token.clone()));
|
self.lock(|inner| inner.tokens.push(token.clone()));
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
|
@ -419,7 +419,7 @@ fn launch_libmdns(
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = inner() {
|
if let Err(e) = inner() {
|
||||||
log::error!("libmdns error: {}", e);
|
log::error!("libmdns error: {e}");
|
||||||
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
|
let _ = status_tx.send(DiscoveryEvent::ZeroconfError(e));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -170,7 +170,7 @@ impl RequestHandler {
|
||||||
.map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
|
.map_err(|_| DiscoveryError::HmacError(base_key.to_vec()))?;
|
||||||
h.update(encrypted);
|
h.update(encrypted);
|
||||||
if h.verify_slice(cksum).is_err() {
|
if h.verify_slice(cksum).is_err() {
|
||||||
warn!("Login error for user {:?}: MAC mismatch", username);
|
warn!("Login error for user {username:?}: MAC mismatch");
|
||||||
let result = json!({
|
let result = json!({
|
||||||
"status": 102,
|
"status": 102,
|
||||||
"spotifyError": 1,
|
"spotifyError": 1,
|
||||||
|
@ -314,7 +314,7 @@ impl DiscoveryServer {
|
||||||
discovery
|
discovery
|
||||||
.clone()
|
.clone()
|
||||||
.handle(request)
|
.handle(request)
|
||||||
.inspect_err(|e| error!("could not handle discovery request: {}", e))
|
.inspect_err(|e| error!("could not handle discovery request: {e}"))
|
||||||
.and_then(|x| async move { Ok(x) })
|
.and_then(|x| async move { Ok(x) })
|
||||||
.map(Result::unwrap) // guaranteed by `and_then` above
|
.map(Result::unwrap) // guaranteed by `and_then` above
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use librespot::{
|
use librespot::{
|
||||||
connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc},
|
connect::{ConnectConfig, LoadRequest, LoadRequestOptions, Spirc},
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials, cache::Cache, config::SessionConfig, session::Session, Error,
|
Error, authentication::Credentials, cache::Cache, config::SessionConfig, session::Session,
|
||||||
},
|
},
|
||||||
playback::mixer::MixerConfig,
|
playback::mixer::MixerConfig,
|
||||||
playback::{
|
playback::{
|
||||||
|
|
|
@ -78,10 +78,7 @@ impl Playlist {
|
||||||
let length = tracks.len();
|
let length = tracks.len();
|
||||||
let expected_length = self.length as usize;
|
let expected_length = self.length as usize;
|
||||||
if length != expected_length {
|
if length != expected_length {
|
||||||
warn!(
|
warn!("Got {length} tracks, but the list should contain {expected_length} tracks.",);
|
||||||
"Got {} tracks, but the list should contain {} tracks.",
|
|
||||||
length, expected_length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks
|
tracks
|
||||||
|
|
|
@ -18,7 +18,6 @@ use oauth2::{
|
||||||
EndpointSet, PkceCodeChallenge, 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};
|
||||||
|
@ -156,7 +155,7 @@ fn get_authcode_listener(
|
||||||
addr: socket_address,
|
addr: socket_address,
|
||||||
e,
|
e,
|
||||||
})?;
|
})?;
|
||||||
info!("OAuth server listening on {:?}", socket_address);
|
info!("OAuth server listening on {socket_address:?}");
|
||||||
|
|
||||||
// The server will terminate itself after collecting the first code.
|
// The server will terminate itself after collecting the first code.
|
||||||
let mut stream = listener
|
let mut stream = listener
|
||||||
|
|
|
@ -226,7 +226,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
let buffer_size = {
|
let buffer_size = {
|
||||||
let max = match hwp.get_buffer_size_max() {
|
let max = match hwp.get_buffer_size_max() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error getting the device's max Buffer size: {}", e);
|
trace!("Error getting the device's max Buffer size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -234,7 +234,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
|
|
||||||
let min = match hwp.get_buffer_size_min() {
|
let min = match hwp.get_buffer_size_min() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error getting the device's min Buffer size: {}", e);
|
trace!("Error getting the device's min Buffer size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -246,11 +246,11 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
.find(|f| (min..=max).contains(f))
|
.find(|f| (min..=max).contains(f))
|
||||||
{
|
{
|
||||||
Some(size) => {
|
Some(size) => {
|
||||||
trace!("Desired Frames per Buffer: {:?}", size);
|
trace!("Desired Frames per Buffer: {size:?}");
|
||||||
|
|
||||||
match hwp.set_buffer_size_near(size) {
|
match hwp.set_buffer_size_near(size) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error setting the device's Buffer size: {}", e);
|
trace!("Error setting the device's Buffer size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -267,17 +267,9 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
};
|
};
|
||||||
|
|
||||||
if buffer_size == ZERO_FRAMES {
|
if buffer_size == ZERO_FRAMES {
|
||||||
trace!(
|
trace!("Desired Buffer Frame range: {MIN_BUFFER:?} - {MAX_BUFFER:?}",);
|
||||||
"Desired Buffer Frame range: {:?} - {:?}",
|
|
||||||
MIN_BUFFER,
|
|
||||||
MAX_BUFFER
|
|
||||||
);
|
|
||||||
|
|
||||||
trace!(
|
trace!("Actual Buffer Frame range as reported by the device: {min:?} - {max:?}",);
|
||||||
"Actual Buffer Frame range as reported by the device: {:?} - {:?}",
|
|
||||||
min,
|
|
||||||
max
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer_size
|
buffer_size
|
||||||
|
@ -289,7 +281,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
} else {
|
} else {
|
||||||
let max = match hwp.get_period_size_max() {
|
let max = match hwp.get_period_size_max() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error getting the device's max Period size: {}", e);
|
trace!("Error getting the device's max Period size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -297,7 +289,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
|
|
||||||
let min = match hwp.get_period_size_min() {
|
let min = match hwp.get_period_size_min() {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error getting the device's min Period size: {}", e);
|
trace!("Error getting the device's min Period size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -312,11 +304,11 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
.find(|f| (min..=max).contains(f))
|
.find(|f| (min..=max).contains(f))
|
||||||
{
|
{
|
||||||
Some(size) => {
|
Some(size) => {
|
||||||
trace!("Desired Frames per Period: {:?}", size);
|
trace!("Desired Frames per Period: {size:?}");
|
||||||
|
|
||||||
match hwp.set_period_size_near(size, ValueOr::Nearest) {
|
match hwp.set_period_size_near(size, ValueOr::Nearest) {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
trace!("Error setting the device's Period size: {}", e);
|
trace!("Error setting the device's Period size: {e}");
|
||||||
ZERO_FRAMES
|
ZERO_FRAMES
|
||||||
}
|
}
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
@ -334,20 +326,14 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
};
|
};
|
||||||
|
|
||||||
if period_size == ZERO_FRAMES {
|
if period_size == ZERO_FRAMES {
|
||||||
trace!("Buffer size: {:?}", buffer_size);
|
trace!("Buffer size: {buffer_size:?}");
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})",
|
"Desired Period Frame range: {min_period:?} (Buffer size / {MIN_PERIOD_DIVISOR:?}) - {max_period:?} (Buffer size / {MAX_PERIOD_DIVISOR:?})",
|
||||||
min_period,
|
|
||||||
MIN_PERIOD_DIVISOR,
|
|
||||||
max_period,
|
|
||||||
MAX_PERIOD_DIVISOR,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
trace!(
|
trace!(
|
||||||
"Actual Period Frame range as reported by the device: {:?} - {:?}",
|
"Actual Period Frame range as reported by the device: {min:?} - {max:?}",
|
||||||
min,
|
|
||||||
max
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,14 +367,14 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)>
|
||||||
|
|
||||||
pcm.sw_params(&swp).map_err(AlsaError::Pcm)?;
|
pcm.sw_params(&swp).map_err(AlsaError::Pcm)?;
|
||||||
|
|
||||||
trace!("Actual Frames per Buffer: {:?}", frames_per_buffer);
|
trace!("Actual Frames per Buffer: {frames_per_buffer:?}");
|
||||||
trace!("Actual Frames per Period: {:?}", frames_per_period);
|
trace!("Actual Frames per Period: {frames_per_period:?}");
|
||||||
|
|
||||||
// Let ALSA do the math for us.
|
// Let ALSA do the math for us.
|
||||||
pcm.frames_to_bytes(frames_per_period) as usize
|
pcm.frames_to_bytes(frames_per_period) as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Period Buffer size in bytes: {:?}", bytes_per_period);
|
trace!("Period Buffer size in bytes: {bytes_per_period:?}");
|
||||||
|
|
||||||
Ok((pcm, bytes_per_period))
|
Ok((pcm, bytes_per_period))
|
||||||
}
|
}
|
||||||
|
@ -401,7 +387,7 @@ impl Open for AlsaSink {
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -410,7 +396,7 @@ impl Open for AlsaSink {
|
||||||
}
|
}
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
info!("Using AlsaSink with format: {:?}", format);
|
info!("Using AlsaSink with format: {format:?}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
pcm: None,
|
pcm: None,
|
||||||
|
@ -500,10 +486,7 @@ impl AlsaSink {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Capture and log the original error as a warning, and then try to recover.
|
// Capture and log the original error as a warning, and then try to recover.
|
||||||
// If recovery fails then forward that error back to player.
|
// If recovery fails then forward that error back to player.
|
||||||
warn!(
|
warn!("Error writing from AlsaSink buffer to PCM, trying to recover, {e}");
|
||||||
"Error writing from AlsaSink buffer to PCM, trying to recover, {}",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
|
|
||||||
pcm.try_recover(e, false).map_err(AlsaError::OnWrite)
|
pcm.try_recover(e, false).map_err(AlsaError::OnWrite)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,7 +48,7 @@ impl Open for StdoutSink {
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Using StdoutSink (pipe) with format: {:?}", format);
|
info!("Using StdoutSink (pipe) with format: {format:?}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
output: None,
|
output: None,
|
||||||
|
|
|
@ -72,7 +72,7 @@ impl Open for SubprocessSink {
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Using SubprocessSink with format: {:?}", format);
|
info!("Using SubprocessSink with format: {format:?}");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
shell_command,
|
shell_command,
|
||||||
|
|
|
@ -33,10 +33,7 @@ impl MappedCtrl for VolumeCtrl {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Ensure not to return -inf or NaN due to division by zero.
|
// Ensure not to return -inf or NaN due to division by zero.
|
||||||
error!(
|
error!("{self:?} does not work with 0 dB range, using linear mapping instead");
|
||||||
"{:?} does not work with 0 dB range, using linear mapping instead",
|
|
||||||
self
|
|
||||||
);
|
|
||||||
normalized_volume
|
normalized_volume
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,10 +64,7 @@ impl MappedCtrl for VolumeCtrl {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Ensure not to return -inf or NaN due to division by zero.
|
// Ensure not to return -inf or NaN due to division by zero.
|
||||||
error!(
|
error!("{self:?} does not work with 0 dB range, using linear mapping instead");
|
||||||
"{:?} does not work with 0 dB range, using linear mapping instead",
|
|
||||||
self
|
|
||||||
);
|
|
||||||
mapped_volume
|
mapped_volume
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -88,10 +82,10 @@ impl MappedCtrl for VolumeCtrl {
|
||||||
fn set_db_range(&mut self, new_db_range: f64) {
|
fn set_db_range(&mut self, new_db_range: f64) {
|
||||||
match self {
|
match self {
|
||||||
Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range,
|
Self::Cubic(ref mut db_range) | Self::Log(ref mut db_range) => *db_range = new_db_range,
|
||||||
_ => error!("Invalid to set dB range for volume control type {:?}", self),
|
_ => error!("Invalid to set dB range for volume control type {self:?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
debug!("Volume control is now {:?}", self)
|
debug!("Volume control is now {self:?}")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn range_ok(&self) -> bool {
|
fn range_ok(&self) -> bool {
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub struct SoftMixer {
|
||||||
impl Mixer for SoftMixer {
|
impl Mixer for SoftMixer {
|
||||||
fn open(config: MixerConfig) -> Result<Self, Error> {
|
fn open(config: MixerConfig) -> Result<Self, Error> {
|
||||||
let volume_ctrl = config.volume_ctrl;
|
let volume_ctrl = config.volume_ctrl;
|
||||||
info!("Mixing with softvol and volume control: {:?}", volume_ctrl);
|
info!("Mixing with softvol and volume control: {volume_ctrl:?}");
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))),
|
volume: Arc::new(AtomicU64::new(f64::to_bits(0.5))),
|
||||||
|
|
|
@ -324,8 +324,7 @@ impl NormalisationData {
|
||||||
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
let newpos = file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?;
|
||||||
if newpos != SPOTIFY_NORMALIZATION_HEADER_START_OFFSET {
|
if newpos != SPOTIFY_NORMALIZATION_HEADER_START_OFFSET {
|
||||||
error!(
|
error!(
|
||||||
"NormalisationData::parse_from_file seeking to {} but position is now {}",
|
"NormalisationData::parse_from_file seeking to {SPOTIFY_NORMALIZATION_HEADER_START_OFFSET} but position is now {newpos}"
|
||||||
SPOTIFY_NORMALIZATION_HEADER_START_OFFSET, newpos
|
|
||||||
);
|
);
|
||||||
|
|
||||||
error!("Falling back to default (non-track and non-album) normalisation data.");
|
error!("Falling back to default (non-track and non-album) normalisation data.");
|
||||||
|
@ -396,8 +395,7 @@ impl NormalisationData {
|
||||||
let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs();
|
let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs();
|
||||||
|
|
||||||
warn!(
|
warn!(
|
||||||
"This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at its peak.",
|
"This track may exceed dBFS by {factor_db:.2} dB and be subject to {limiting_db:.2} dB of dynamic limiting at its peak."
|
||||||
factor_db, limiting_db
|
|
||||||
);
|
);
|
||||||
} else if factor > threshold_ratio {
|
} else if factor > threshold_ratio {
|
||||||
let limiting_db = gain_db
|
let limiting_db = gain_db
|
||||||
|
@ -405,15 +403,14 @@ impl NormalisationData {
|
||||||
+ config.normalisation_threshold_dbfs.abs();
|
+ config.normalisation_threshold_dbfs.abs();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"This track may be subject to {:.2} dB of dynamic limiting at its peak.",
|
"This track may be subject to {limiting_db:.2} dB of dynamic limiting at its peak."
|
||||||
limiting_db
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factor
|
factor
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!("Normalisation Data: {:?}", data);
|
debug!("Normalisation Data: {data:?}");
|
||||||
debug!(
|
debug!(
|
||||||
"Calculated Normalisation Factor for {:?}: {:.2}%",
|
"Calculated Normalisation Factor for {:?}: {:.2}%",
|
||||||
config.normalisation_type,
|
config.normalisation_type,
|
||||||
|
@ -464,7 +461,7 @@ impl Player {
|
||||||
|
|
||||||
let handle = thread::spawn(move || {
|
let handle = thread::spawn(move || {
|
||||||
let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel);
|
let player_id = PLAYER_COUNTER.fetch_add(1, Ordering::AcqRel);
|
||||||
debug!("new Player [{}]", player_id);
|
debug!("new Player [{player_id}]");
|
||||||
|
|
||||||
let converter = Converter::new(config.ditherer);
|
let converter = Converter::new(config.ditherer);
|
||||||
|
|
||||||
|
@ -517,7 +514,7 @@ impl Player {
|
||||||
fn command(&self, cmd: PlayerCommand) {
|
fn command(&self, cmd: PlayerCommand) {
|
||||||
if let Some(commands) = self.commands.as_ref() {
|
if let Some(commands) = self.commands.as_ref() {
|
||||||
if let Err(e) = commands.send(cmd) {
|
if let Err(e) = commands.send(cmd) {
|
||||||
error!("Player Commands Error: {}", e);
|
error!("Player Commands Error: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -636,7 +633,7 @@ impl Drop for Player {
|
||||||
self.commands = None;
|
self.commands = None;
|
||||||
if let Some(handle) = self.thread_handle.take() {
|
if let Some(handle) = self.thread_handle.take() {
|
||||||
if let Err(e) = handle.join() {
|
if let Err(e) = handle.join() {
|
||||||
error!("Player thread Error: {:?}", e);
|
error!("Player thread Error: {e:?}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -787,10 +784,7 @@ impl PlayerState {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!(
|
error!("Called playing_to_end_of_track in non-playing state: {new_state:?}");
|
||||||
"Called playing_to_end_of_track in non-playing state: {:?}",
|
|
||||||
new_state
|
|
||||||
);
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -832,10 +826,7 @@ impl PlayerState {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!(
|
error!("PlayerState::paused_to_playing in invalid state: {new_state:?}");
|
||||||
"PlayerState::paused_to_playing in invalid state: {:?}",
|
|
||||||
new_state
|
|
||||||
);
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -876,10 +867,7 @@ impl PlayerState {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!(
|
error!("PlayerState::playing_to_paused in invalid state: {new_state:?}");
|
||||||
"PlayerState::playing_to_paused in invalid state: {:?}",
|
|
||||||
new_state
|
|
||||||
);
|
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -894,7 +882,7 @@ struct PlayerTrackLoader {
|
||||||
impl PlayerTrackLoader {
|
impl PlayerTrackLoader {
|
||||||
async fn find_available_alternative(&self, audio_item: AudioItem) -> Option<AudioItem> {
|
async fn find_available_alternative(&self, audio_item: AudioItem) -> Option<AudioItem> {
|
||||||
if let Err(e) = audio_item.availability {
|
if let Err(e) = audio_item.availability {
|
||||||
error!("Track is unavailable: {}", e);
|
error!("Track is unavailable: {e}");
|
||||||
None
|
None
|
||||||
} else if !audio_item.files.is_empty() {
|
} else if !audio_item.files.is_empty() {
|
||||||
Some(audio_item)
|
Some(audio_item)
|
||||||
|
@ -958,7 +946,7 @@ impl PlayerTrackLoader {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Unable to load audio item: {:?}", e);
|
error!("Unable to load audio item: {e:?}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1026,7 +1014,7 @@ impl PlayerTrackLoader {
|
||||||
let encrypted_file = match encrypted_file.await {
|
let encrypted_file = match encrypted_file.await {
|
||||||
Ok(encrypted_file) => encrypted_file,
|
Ok(encrypted_file) => encrypted_file,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Unable to load encrypted file: {:?}", e);
|
error!("Unable to load encrypted file: {e:?}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1041,7 +1029,7 @@ impl PlayerTrackLoader {
|
||||||
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
let key = match self.session.audio_key().request(spotify_id, file_id).await {
|
||||||
Ok(key) => Some(key),
|
Ok(key) => Some(key),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("Unable to load key, continuing without decryption: {}", e);
|
warn!("Unable to load key, continuing without decryption: {e}");
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1064,7 +1052,7 @@ impl PlayerTrackLoader {
|
||||||
) {
|
) {
|
||||||
Ok(audio_file) => audio_file,
|
Ok(audio_file) => audio_file,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("PlayerTrackLoader::load_track error opening subfile: {}", e);
|
error!("PlayerTrackLoader::load_track error opening subfile: {e}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1098,10 +1086,7 @@ impl PlayerTrackLoader {
|
||||||
let mut decoder = match decoder_type {
|
let mut decoder = match decoder_type {
|
||||||
Ok(decoder) => decoder,
|
Ok(decoder) => decoder,
|
||||||
Err(e) if is_cached => {
|
Err(e) if is_cached => {
|
||||||
warn!(
|
warn!("Unable to read cached audio file: {e}. Trying to download it.");
|
||||||
"Unable to read cached audio file: {}. Trying to download it.",
|
|
||||||
e
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.session.cache() {
|
match self.session.cache() {
|
||||||
Some(cache) => {
|
Some(cache) => {
|
||||||
|
@ -1120,7 +1105,7 @@ impl PlayerTrackLoader {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Unable to read audio file: {}", e);
|
error!("Unable to read audio file: {e}");
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1130,7 +1115,7 @@ impl PlayerTrackLoader {
|
||||||
// If the position is invalid just start from
|
// If the position is invalid just start from
|
||||||
// the beginning of the track.
|
// the beginning of the track.
|
||||||
let position_ms = if position_ms > duration_ms {
|
let position_ms = if position_ms > duration_ms {
|
||||||
warn!("Invalid start position of {} ms exceeds track's duration of {} ms, starting track from the beginning", position_ms, duration_ms);
|
warn!("Invalid start position of {position_ms} ms exceeds track's duration of {duration_ms} ms, starting track from the beginning");
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
position_ms
|
position_ms
|
||||||
|
@ -1144,8 +1129,7 @@ impl PlayerTrackLoader {
|
||||||
Ok(new_position_ms) => new_position_ms,
|
Ok(new_position_ms) => new_position_ms,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!(
|
error!(
|
||||||
"PlayerTrackLoader::load_track error seeking to starting position {}: {}",
|
"PlayerTrackLoader::load_track error seeking to starting position {position_ms}: {e}"
|
||||||
position_ms, e
|
|
||||||
);
|
);
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -1195,7 +1179,7 @@ impl Future for PlayerInternal {
|
||||||
|
|
||||||
if let Some(cmd) = cmd {
|
if let Some(cmd) = cmd {
|
||||||
if let Err(e) = self.handle_command(cmd) {
|
if let Err(e) = self.handle_command(cmd) {
|
||||||
error!("Error handling command: {}", e);
|
error!("Error handling command: {e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1225,8 +1209,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
Poll::Ready(Err(e)) => {
|
Poll::Ready(Err(e)) => {
|
||||||
error!(
|
error!(
|
||||||
"Skipping to next track, unable to load track <{:?}>: {:?}",
|
"Skipping to next track, unable to load track <{track_id:?}>: {e:?}"
|
||||||
track_id, e
|
|
||||||
);
|
);
|
||||||
self.send_event(PlayerEvent::Unavailable {
|
self.send_event(PlayerEvent::Unavailable {
|
||||||
track_id,
|
track_id,
|
||||||
|
@ -1253,7 +1236,7 @@ impl Future for PlayerInternal {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Poll::Ready(Err(_)) => {
|
Poll::Ready(Err(_)) => {
|
||||||
debug!("Unable to preload {:?}", track_id);
|
debug!("Unable to preload {track_id:?}");
|
||||||
self.preload = PlayerPreload::None;
|
self.preload = PlayerPreload::None;
|
||||||
// Let Spirc know that the track was unavailable.
|
// Let Spirc know that the track was unavailable.
|
||||||
if let PlayerState::Playing {
|
if let PlayerState::Playing {
|
||||||
|
@ -1368,7 +1351,7 @@ impl Future for PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e);
|
error!("Skipping to next track, unable to decode samples for track <{track_id:?}>: {e:?}");
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -1381,7 +1364,7 @@ impl Future for PlayerInternal {
|
||||||
self.handle_packet(result, normalisation_factor);
|
self.handle_packet(result, normalisation_factor);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e);
|
error!("Skipping to next track, unable to get next packet for track <{track_id:?}>: {e:?}");
|
||||||
self.send_event(PlayerEvent::EndOfTrack {
|
self.send_event(PlayerEvent::EndOfTrack {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -1443,7 +1426,7 @@ impl PlayerInternal {
|
||||||
match self.sink.start() {
|
match self.sink.start() {
|
||||||
Ok(()) => self.sink_status = SinkStatus::Running,
|
Ok(()) => self.sink_status = SinkStatus::Running,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
self.handle_pause();
|
self.handle_pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1466,7 +1449,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1694,7 +1677,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = self.sink.write(packet, &mut self.converter) {
|
if let Err(e) = self.sink.write(packet, &mut self.converter) {
|
||||||
error!("{}", e);
|
error!("{e}");
|
||||||
self.handle_pause();
|
self.handle_pause();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2085,7 +2068,7 @@ impl PlayerInternal {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => error!("PlayerInternal::handle_command_seek error: {}", e),
|
Err(e) => error!("PlayerInternal::handle_command_seek error: {e}"),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
error!("Player::seek called from invalid state: {:?}", self.state);
|
error!("Player::seek called from invalid state: {:?}", self.state);
|
||||||
|
@ -2107,7 +2090,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
|
fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
|
||||||
debug!("command={:?}", cmd);
|
debug!("command={cmd:?}");
|
||||||
match cmd {
|
match cmd {
|
||||||
PlayerCommand::Load {
|
PlayerCommand::Load {
|
||||||
track_id,
|
track_id,
|
||||||
|
|
65
src/main.rs
65
src/main.rs
|
@ -5,18 +5,18 @@ use librespot::playback::mixer::alsamixer::AlsaMixer;
|
||||||
use librespot::{
|
use librespot::{
|
||||||
connect::{ConnectConfig, Spirc},
|
connect::{ConnectConfig, Spirc},
|
||||||
core::{
|
core::{
|
||||||
authentication::Credentials, cache::Cache, config::DeviceType, version, Session,
|
Session, SessionConfig, authentication::Credentials, cache::Cache, config::DeviceType,
|
||||||
SessionConfig,
|
version,
|
||||||
},
|
},
|
||||||
discovery::DnsSdServiceBuilder,
|
discovery::DnsSdServiceBuilder,
|
||||||
playback::{
|
playback::{
|
||||||
audio_backend::{self, SinkBuilder, BACKENDS},
|
audio_backend::{self, BACKENDS, SinkBuilder},
|
||||||
config::{
|
config::{
|
||||||
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
AudioFormat, Bitrate, NormalisationMethod, NormalisationType, PlayerConfig, VolumeCtrl,
|
||||||
},
|
},
|
||||||
dither,
|
dither,
|
||||||
mixer::{self, MixerConfig, MixerFn},
|
mixer::{self, MixerConfig, MixerFn},
|
||||||
player::{coefficient_to_duration, duration_to_coefficient, Player},
|
player::{Player, coefficient_to_duration, duration_to_coefficient},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use librespot_oauth::OAuthClientBuilder;
|
use librespot_oauth::OAuthClientBuilder;
|
||||||
|
@ -24,6 +24,7 @@ use log::{debug, error, info, trace, warn};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
|
ffi::OsStr,
|
||||||
fs::create_dir_all,
|
fs::create_dir_all,
|
||||||
ops::RangeInclusive,
|
ops::RangeInclusive,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -34,10 +35,11 @@ use std::{
|
||||||
};
|
};
|
||||||
use sysinfo::{ProcessesToUpdate, System};
|
use sysinfo::{ProcessesToUpdate, System};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tokio::sync::Semaphore;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
mod player_event_handler;
|
mod player_event_handler;
|
||||||
use player_event_handler::{run_program_on_sink_events, EventHandler};
|
use player_event_handler::{EventHandler, run_program_on_sink_events};
|
||||||
|
|
||||||
fn device_id(name: &str) -> String {
|
fn device_id(name: &str) -> String {
|
||||||
HEXLOWER.encode(&Sha1::digest(name.as_bytes()))
|
HEXLOWER.encode(&Sha1::digest(name.as_bytes()))
|
||||||
|
@ -75,7 +77,9 @@ fn setup_logging(quiet: bool, verbose: bool) {
|
||||||
builder.init();
|
builder.init();
|
||||||
|
|
||||||
if verbose && quiet {
|
if verbose && quiet {
|
||||||
warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode.");
|
warn!(
|
||||||
|
"`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode."
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,7 +223,7 @@ struct Setup {
|
||||||
zeroconf_backend: Option<DnsSdServiceBuilder>,
|
zeroconf_backend: Option<DnsSdServiceBuilder>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_setup() -> Setup {
|
async fn get_setup() -> Setup {
|
||||||
const VALID_INITIAL_VOLUME_RANGE: RangeInclusive<u16> = 0..=100;
|
const VALID_INITIAL_VOLUME_RANGE: RangeInclusive<u16> = 0..=100;
|
||||||
const VALID_VOLUME_RANGE: RangeInclusive<f64> = 0.0..=100.0;
|
const VALID_VOLUME_RANGE: RangeInclusive<f64> = 0.0..=100.0;
|
||||||
const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive<f64> = 0.0..=10.0;
|
const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive<f64> = 0.0..=10.0;
|
||||||
|
@ -810,7 +814,9 @@ fn get_setup() -> Setup {
|
||||||
ALSA_MIXER_CONTROL,
|
ALSA_MIXER_CONTROL,
|
||||||
] {
|
] {
|
||||||
if opt_present(a) {
|
if opt_present(a) {
|
||||||
warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time.");
|
warn!(
|
||||||
|
"Alsa specific options have no effect if the alsa backend is not enabled at build time."
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1196,7 +1202,9 @@ fn get_setup() -> Setup {
|
||||||
empty_string_error_msg(USERNAME, USERNAME_SHORT);
|
empty_string_error_msg(USERNAME, USERNAME_SHORT);
|
||||||
}
|
}
|
||||||
if opt_present(PASSWORD) {
|
if opt_present(PASSWORD) {
|
||||||
error!("Invalid `--{PASSWORD}` / `-{PASSWORD_SHORT}`: Password authentication no longer supported, use OAuth");
|
error!(
|
||||||
|
"Invalid `--{PASSWORD}` / `-{PASSWORD_SHORT}`: Password authentication no longer supported, use OAuth"
|
||||||
|
);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
match cached_creds {
|
match cached_creds {
|
||||||
|
@ -1265,9 +1273,7 @@ fn get_setup() -> Setup {
|
||||||
|
|
||||||
if let Some(reason) = no_discovery_reason.as_deref() {
|
if let Some(reason) = no_discovery_reason.as_deref() {
|
||||||
if opt_present(ZEROCONF_PORT) {
|
if opt_present(ZEROCONF_PORT) {
|
||||||
warn!(
|
warn!("With {reason} `--{ZEROCONF_PORT}` / `-{ZEROCONF_PORT_SHORT}` has no effect.");
|
||||||
"With {reason} `--{ZEROCONF_PORT}` / `-{ZEROCONF_PORT_SHORT}` has no effect."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1393,31 +1399,31 @@ fn get_setup() -> Setup {
|
||||||
name.clone()
|
name.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
env::set_var("PULSE_PROP_application.name", pulseaudio_name);
|
set_env_var("PULSE_PROP_application.name", pulseaudio_name).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_application.version").is_err() {
|
if env::var("PULSE_PROP_application.version").is_err() {
|
||||||
env::set_var("PULSE_PROP_application.version", version::SEMVER);
|
set_env_var("PULSE_PROP_application.version", version::SEMVER).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_application.icon_name").is_err() {
|
if env::var("PULSE_PROP_application.icon_name").is_err() {
|
||||||
env::set_var("PULSE_PROP_application.icon_name", "audio-x-generic");
|
set_env_var("PULSE_PROP_application.icon_name", "audio-x-generic").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_application.process.binary").is_err() {
|
if env::var("PULSE_PROP_application.process.binary").is_err() {
|
||||||
env::set_var("PULSE_PROP_application.process.binary", "librespot");
|
set_env_var("PULSE_PROP_application.process.binary", "librespot").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_stream.description").is_err() {
|
if env::var("PULSE_PROP_stream.description").is_err() {
|
||||||
env::set_var("PULSE_PROP_stream.description", "Spotify Connect endpoint");
|
set_env_var("PULSE_PROP_stream.description", "Spotify Connect endpoint").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_media.software").is_err() {
|
if env::var("PULSE_PROP_media.software").is_err() {
|
||||||
env::set_var("PULSE_PROP_media.software", "Spotify");
|
set_env_var("PULSE_PROP_media.software", "Spotify").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
if env::var("PULSE_PROP_media.role").is_err() {
|
if env::var("PULSE_PROP_media.role").is_err() {
|
||||||
env::set_var("PULSE_PROP_media.role", "music");
|
set_env_var("PULSE_PROP_media.role", "music").await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1839,6 +1845,23 @@ fn get_setup() -> Setup {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize a static semaphore with only one permit, which is used to
|
||||||
|
// prevent setting environment variables from running in parallel.
|
||||||
|
static PERMIT: Semaphore = Semaphore::const_new(1);
|
||||||
|
async fn set_env_var<K: AsRef<OsStr>, V: AsRef<OsStr>>(key: K, value: V) {
|
||||||
|
let permit = PERMIT
|
||||||
|
.acquire()
|
||||||
|
.await
|
||||||
|
.expect("Failed to acquire semaphore permit");
|
||||||
|
|
||||||
|
// SAFETY: This is safe because setting the environment variable will wait if the permit is
|
||||||
|
// already acquired by other callers.
|
||||||
|
unsafe { env::set_var(key, value) }
|
||||||
|
|
||||||
|
// Drop the permit manually, so the compiler doesn't optimize it away as unused variable.
|
||||||
|
drop(permit);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
#[tokio::main(flavor = "current_thread")]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
|
const RUST_BACKTRACE: &str = "RUST_BACKTRACE";
|
||||||
|
@ -1847,10 +1870,10 @@ async fn main() {
|
||||||
const RECONNECT_RATE_LIMIT: usize = 5;
|
const RECONNECT_RATE_LIMIT: usize = 5;
|
||||||
|
|
||||||
if env::var(RUST_BACKTRACE).is_err() {
|
if env::var(RUST_BACKTRACE).is_err() {
|
||||||
env::set_var(RUST_BACKTRACE, "full")
|
set_env_var(RUST_BACKTRACE, "full").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let setup = get_setup();
|
let setup = get_setup().await;
|
||||||
|
|
||||||
let mut last_credentials = None;
|
let mut last_credentials = None;
|
||||||
let mut spirc: Option<Spirc> = None;
|
let mut spirc: Option<Spirc> = None;
|
||||||
|
|
|
@ -14,7 +14,8 @@ pub struct EventHandler {
|
||||||
impl EventHandler {
|
impl EventHandler {
|
||||||
pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self {
|
pub fn new(mut player_events: PlayerEventChannel, onevent: &str) -> Self {
|
||||||
let on_event = onevent.to_string();
|
let on_event = onevent.to_string();
|
||||||
let thread_handle = Some(thread::spawn(move || loop {
|
let thread_handle = Some(thread::spawn(move || {
|
||||||
|
loop {
|
||||||
match player_events.blocking_recv() {
|
match player_events.blocking_recv() {
|
||||||
None => break,
|
None => break,
|
||||||
Some(event) => {
|
Some(event) => {
|
||||||
|
@ -22,7 +23,8 @@ impl EventHandler {
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
PlayerEvent::PlayRequestIdChanged { play_request_id } => {
|
PlayerEvent::PlayRequestIdChanged { play_request_id } => {
|
||||||
env_vars.insert("PLAYER_EVENT", "play_request_id_changed".to_string());
|
env_vars
|
||||||
|
.insert("PLAYER_EVENT", "play_request_id_changed".to_string());
|
||||||
env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string());
|
env_vars.insert("PLAY_REQUEST_ID", play_request_id.to_string());
|
||||||
}
|
}
|
||||||
PlayerEvent::TrackChanged { audio_item } => {
|
PlayerEvent::TrackChanged { audio_item } => {
|
||||||
|
@ -31,7 +33,8 @@ impl EventHandler {
|
||||||
warn!("PlayerEvent::TrackChanged: Invalid track id: {e}")
|
warn!("PlayerEvent::TrackChanged: Invalid track id: {e}")
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "track_changed".to_string());
|
env_vars
|
||||||
|
.insert("PLAYER_EVENT", "track_changed".to_string());
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
env_vars.insert("URI", audio_item.uri);
|
env_vars.insert("URI", audio_item.uri);
|
||||||
env_vars.insert("NAME", audio_item.name);
|
env_vars.insert("NAME", audio_item.name);
|
||||||
|
@ -45,10 +48,14 @@ impl EventHandler {
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
);
|
);
|
||||||
env_vars.insert("LANGUAGE", audio_item.language.join("\n"));
|
env_vars.insert("LANGUAGE", audio_item.language.join("\n"));
|
||||||
env_vars
|
env_vars.insert(
|
||||||
.insert("DURATION_MS", audio_item.duration_ms.to_string());
|
"DURATION_MS",
|
||||||
env_vars
|
audio_item.duration_ms.to_string(),
|
||||||
.insert("IS_EXPLICIT", audio_item.is_explicit.to_string());
|
);
|
||||||
|
env_vars.insert(
|
||||||
|
"IS_EXPLICIT",
|
||||||
|
audio_item.is_explicit.to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
match audio_item.unique_fields {
|
match audio_item.unique_fields {
|
||||||
UniqueFields::Track {
|
UniqueFields::Track {
|
||||||
|
@ -69,12 +76,16 @@ impl EventHandler {
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join("\n"),
|
.join("\n"),
|
||||||
);
|
);
|
||||||
env_vars
|
env_vars.insert(
|
||||||
.insert("ALBUM_ARTISTS", album_artists.join("\n"));
|
"ALBUM_ARTISTS",
|
||||||
|
album_artists.join("\n"),
|
||||||
|
);
|
||||||
env_vars.insert("ALBUM", album);
|
env_vars.insert("ALBUM", album);
|
||||||
env_vars.insert("POPULARITY", popularity.to_string());
|
env_vars
|
||||||
|
.insert("POPULARITY", popularity.to_string());
|
||||||
env_vars.insert("NUMBER", number.to_string());
|
env_vars.insert("NUMBER", number.to_string());
|
||||||
env_vars.insert("DISC_NUMBER", disc_number.to_string());
|
env_vars
|
||||||
|
.insert("DISC_NUMBER", disc_number.to_string());
|
||||||
}
|
}
|
||||||
UniqueFields::Episode {
|
UniqueFields::Episode {
|
||||||
description,
|
description,
|
||||||
|
@ -131,13 +142,17 @@ impl EventHandler {
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
|
PlayerEvent::Preloading { track_id, .. } => {
|
||||||
Err(e) => warn!("PlayerEvent::Preloading: Invalid track id: {e}"),
|
match track_id.to_base62() {
|
||||||
|
Err(e) => {
|
||||||
|
warn!("PlayerEvent::Preloading: Invalid track id: {e}")
|
||||||
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
|
env_vars.insert("PLAYER_EVENT", "preloading".to_string());
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
|
}
|
||||||
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
|
PlayerEvent::TimeToPreloadNextTrack { track_id, .. } => {
|
||||||
match track_id.to_base62() {
|
match track_id.to_base62() {
|
||||||
Err(e) => warn!(
|
Err(e) => warn!(
|
||||||
|
@ -149,14 +164,19 @@ impl EventHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PlayerEvent::EndOfTrack { track_id, .. } => match track_id.to_base62() {
|
PlayerEvent::EndOfTrack { track_id, .. } => {
|
||||||
Err(e) => warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}"),
|
match track_id.to_base62() {
|
||||||
|
Err(e) => {
|
||||||
|
warn!("PlayerEvent::EndOfTrack: Invalid track id: {e}")
|
||||||
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "end_of_track".to_string());
|
env_vars.insert("PLAYER_EVENT", "end_of_track".to_string());
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62() {
|
}
|
||||||
|
PlayerEvent::Unavailable { track_id, .. } => match track_id.to_base62()
|
||||||
|
{
|
||||||
Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"),
|
Err(e) => warn!("PlayerEvent::Unavailable: Invalid track id: {e}"),
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
|
env_vars.insert("PLAYER_EVENT", "unavailable".to_string());
|
||||||
|
@ -188,7 +208,8 @@ impl EventHandler {
|
||||||
warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}")
|
warn!("PlayerEvent::PositionCorrection: Invalid track id: {e}")
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
env_vars.insert("PLAYER_EVENT", "position_correction".to_string());
|
env_vars
|
||||||
|
.insert("PLAYER_EVENT", "position_correction".to_string());
|
||||||
env_vars.insert("TRACK_ID", id);
|
env_vars.insert("TRACK_ID", id);
|
||||||
env_vars.insert("POSITION_MS", position_ms.to_string());
|
env_vars.insert("POSITION_MS", position_ms.to_string());
|
||||||
}
|
}
|
||||||
|
@ -215,7 +236,8 @@ impl EventHandler {
|
||||||
client_brand_name,
|
client_brand_name,
|
||||||
client_model_name,
|
client_model_name,
|
||||||
} => {
|
} => {
|
||||||
env_vars.insert("PLAYER_EVENT", "session_client_changed".to_string());
|
env_vars
|
||||||
|
.insert("PLAYER_EVENT", "session_client_changed".to_string());
|
||||||
env_vars.insert("CLIENT_ID", client_id);
|
env_vars.insert("CLIENT_ID", client_id);
|
||||||
env_vars.insert("CLIENT_NAME", client_name);
|
env_vars.insert("CLIENT_NAME", client_name);
|
||||||
env_vars.insert("CLIENT_BRAND_NAME", client_brand_name);
|
env_vars.insert("CLIENT_BRAND_NAME", client_brand_name);
|
||||||
|
@ -251,6 +273,7 @@ impl EventHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Self { thread_handle }
|
Self { thread_handle }
|
||||||
|
@ -287,9 +310,7 @@ pub fn run_program_on_sink_events(sink_status: SinkStatus, onevent: &str) {
|
||||||
fn run_program(env_vars: HashMap<&str, String>, onevent: &str) {
|
fn run_program(env_vars: HashMap<&str, String>, onevent: &str) {
|
||||||
let mut v: Vec<&str> = onevent.split_whitespace().collect();
|
let mut v: Vec<&str> = onevent.split_whitespace().collect();
|
||||||
|
|
||||||
debug!(
|
debug!("Running {onevent} with environment variables:\n{env_vars:#?}");
|
||||||
"Running {onevent} with environment variables:\n{env_vars:#?}"
|
|
||||||
);
|
|
||||||
|
|
||||||
match Command::new(v.remove(0))
|
match Command::new(v.remove(0))
|
||||||
.args(&v)
|
.args(&v)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue