mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'dev' into new-api and update crates
This commit is contained in:
commit
05b9b13cf8
41 changed files with 975 additions and 804 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -191,7 +191,7 @@ jobs:
|
||||||
- name: Install cross
|
- name: Install cross
|
||||||
run: cargo install cross || true
|
run: cargo install cross || true
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cross build --locked --target ${{ matrix.target }} --no-default-features
|
run: cross build --target ${{ matrix.target }} --no-default-features
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
needs: [test-cross-arm, test-windows]
|
needs: [test-cross-arm, test-windows]
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,5 +5,4 @@ spotify_appkey.key
|
||||||
.project
|
.project
|
||||||
.history
|
.history
|
||||||
*.save
|
*.save
|
||||||
|
*.*~
|
||||||
|
|
||||||
|
|
82
CHANGELOG.md
82
CHANGELOG.md
|
@ -8,38 +8,64 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- [main] Enforce reasonable ranges for option values (breaking).
|
- [playback] `subprocess`: Better error handling
|
||||||
- [main] Don't evaluate options that would otherwise have no effect.
|
- [playback] `pipe`: Better error handling
|
||||||
- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend.
|
|
||||||
- [contrib] Hardened security of the systemd service units
|
|
||||||
- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted).
|
|
||||||
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking).
|
|
||||||
- [playback] `pipe`: create file if it doesn't already exist
|
|
||||||
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
|
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
- [cache] Add `disable-credential-cache` flag (breaking).
|
- [playback] `pipe`: Implement stop
|
||||||
- [main] Use different option descriptions and error messages based on what backends are enabled at build time.
|
|
||||||
- [main] Add a `-q`, `--quiet` option that changes the logging level to warn.
|
|
||||||
- [main] Add a short name for every flag and option.
|
|
||||||
- [main] Add the ability to parse environment variables.
|
|
||||||
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence). (breaking)
|
|
||||||
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence). (breaking)
|
|
||||||
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence). (breaking)
|
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given.
|
- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa`
|
||||||
- [main] Don't panic when parsing options. Instead list valid values and exit.
|
- [main] fix `--opt=value` line argument logging
|
||||||
- [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`.
|
|
||||||
- [core] Removed unsafe code (breaking)
|
|
||||||
- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor.
|
|
||||||
- [playback] `alsa`: Use `--volume-range` overrides for softvol controls
|
|
||||||
- [connect] Don't panic when activating shuffle without previous interaction.
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed.
|
|
||||||
- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed.
|
## [0.4.1] - 2022-05-23
|
||||||
- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed.
|
|
||||||
|
### Changed
|
||||||
|
- [chore] The MSRV is now 1.56
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [playback] Fixed dependency issues when installing from crate
|
||||||
|
|
||||||
|
## [0.4.0] - 2022-05-21
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- [chore] The MSRV is now 1.53
|
||||||
|
- [contrib] Hardened security of the `systemd` service units
|
||||||
|
- [core] `Session`: `connect()` now returns the long-term credentials
|
||||||
|
- [core] `Session`: `connect()` now accepts a flag if the credentails should be stored via the cache
|
||||||
|
- [main] Different option descriptions and error messages based on what backends are enabled at build time
|
||||||
|
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
|
||||||
|
- [playback] `alsa`: improve `--device ?` output for the Alsa backend
|
||||||
|
- [playback] `gstreamer`: create own context, set correct states and use sync handler
|
||||||
|
- [playback] `pipe`: create file if it doesn't already exist
|
||||||
|
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- [main] Enforce reasonable ranges for option values (breaking)
|
||||||
|
- [main] Add the ability to parse environment variables
|
||||||
|
- [main] Log now emits warning when trying to use options that would otherwise have no effect
|
||||||
|
- [main] Verbose logging now logs all parsed environment variables and command line arguments (credentials are redacted)
|
||||||
|
- [main] Add a `-q`, `--quiet` option that changes the logging level to WARN
|
||||||
|
- [main] Add `disable-credential-cache` flag (breaking)
|
||||||
|
- [main] Add a short name for every flag and option
|
||||||
|
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence) (breaking)
|
||||||
|
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence) (breaking)
|
||||||
|
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence) (breaking)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- [connect] Don't panic when activating shuffle without previous interaction
|
||||||
|
- [core] Removed unsafe code (breaking)
|
||||||
|
- [main] Fix crash when built with Avahi support but Avahi is locally unavailable
|
||||||
|
- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given
|
||||||
|
- [main] Don't panic when parsing options, instead list valid values and exit
|
||||||
|
- [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`.
|
||||||
|
- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor
|
||||||
|
- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been removed
|
||||||
|
|
||||||
## [0.3.1] - 2021-10-24
|
## [0.3.1] - 2021-10-24
|
||||||
|
|
||||||
|
@ -114,7 +140,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [0.1.0] - 2019-11-06
|
## [0.1.0] - 2019-11-06
|
||||||
|
|
||||||
[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.1..HEAD
|
[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD
|
||||||
|
[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1
|
||||||
|
[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0
|
||||||
[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1
|
[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1
|
||||||
[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0
|
[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0
|
||||||
[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0
|
[0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0
|
||||||
|
|
|
@ -7,7 +7,11 @@ In order to compile librespot, you will first need to set up a suitable Rust bui
|
||||||
### Install Rust
|
### Install Rust
|
||||||
The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once that’s installed, Rust's standard tools should be set up and ready to use.
|
The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once that’s installed, Rust's standard tools should be set up and ready to use.
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
*Note: The current minimum required Rust version at the time of writing is 1.56.*
|
*Note: The current minimum required Rust version at the time of writing is 1.56.*
|
||||||
|
=======
|
||||||
|
*Note: The current minimum required Rust version at the time of writing is 1.56, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
|
||||||
|
>>>>>>> dev
|
||||||
|
|
||||||
#### Additional Rust tools - `rustfmt`
|
#### Additional Rust tools - `rustfmt`
|
||||||
To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with:
|
To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with:
|
||||||
|
|
815
Cargo.lock
generated
815
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot"
|
name = "librespot"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
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"
|
||||||
|
@ -22,31 +22,31 @@ doc = false
|
||||||
|
|
||||||
[dependencies.librespot-audio]
|
[dependencies.librespot-audio]
|
||||||
path = "audio"
|
path = "audio"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-connect]
|
[dependencies.librespot-connect]
|
||||||
path = "connect"
|
path = "connect"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "core"
|
path = "core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-discovery]
|
[dependencies.librespot-discovery]
|
||||||
path = "discovery"
|
path = "discovery"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-metadata]
|
[dependencies.librespot-metadata]
|
||||||
path = "metadata"
|
path = "metadata"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-playback]
|
[dependencies.librespot-playback]
|
||||||
path = "playback"
|
path = "playback"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "protocol"
|
path = "protocol"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] }
|
env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] }
|
||||||
|
@ -54,8 +54,8 @@ futures-util = { version = "0.3", default_features = false }
|
||||||
getopts = "0.2"
|
getopts = "0.2"
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rpassword = "5.0"
|
rpassword = "7.0"
|
||||||
sha-1 = "0.10"
|
sha1 = "0.10"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", 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"
|
||||||
|
|
|
@ -2,7 +2,17 @@
|
||||||
|
|
||||||
## How To
|
## How To
|
||||||
|
|
||||||
The bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script.
|
Read through this paragraph in its entirety before running anything.
|
||||||
|
|
||||||
|
The Bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows from the project root: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script.
|
||||||
|
|
||||||
|
Make sure that you are are starting from a clean working directory for both `dev` and `master`, completely up to date with remote and all local changes either committed and pushed or stashed.
|
||||||
|
|
||||||
|
You will want to perform a dry run first: `./publish --dry-run 0.1.0`. Please make note of any errors or warnings. In particular, you may need to explicitly inform Git which remote you want to track for the `master` branch like so: `git --track origin/master` (or whatever you have called the `librespot-org` remote `master` branch).
|
||||||
|
|
||||||
|
Depending on your system the script may fail to publish the main `librespot` crate after having published all the `librespot-xyz` sub-crates. If so then make sure the working directory is committed and pushed (watch `Cargo.toml`) and then run `cargo publish` manually after `publish.sh` finished.
|
||||||
|
|
||||||
|
To publish the crates your GitHub account needs to be authorized on `crates.io` by `librespot-org`.
|
||||||
|
|
||||||
## What the script does
|
## What the script does
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-audio"
|
name = "librespot-audio"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
description="The audio fetching and processing logic for librespot"
|
description = "The audio fetching logic for librespot"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
repository = "https://github.com/librespot-org/librespot"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = { version = "0.7", features = ["ctr"] }
|
aes = "0.8"
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
ctr = "0.9"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
hyper = { version = "0.14", features = ["client"] }
|
hyper = { version = "0.14", features = ["client"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
|
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use aes::{
|
use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
|
||||||
cipher::{generic_array::GenericArray, NewCipher, StreamCipher, StreamCipherSeek},
|
|
||||||
Aes128Ctr,
|
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
|
||||||
};
|
|
||||||
|
|
||||||
use librespot_core::audio_key::AudioKey;
|
use librespot_core::audio_key::AudioKey;
|
||||||
|
|
||||||
|
@ -19,12 +18,12 @@ pub struct AudioDecrypt<T: io::Read> {
|
||||||
|
|
||||||
impl<T: io::Read> AudioDecrypt<T> {
|
impl<T: io::Read> AudioDecrypt<T> {
|
||||||
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
|
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
|
||||||
let cipher = key.map(|key| {
|
let cipher = if let Some(key) = key {
|
||||||
Aes128Ctr::new(
|
Aes128Ctr::new_from_slices(&key.0, &AUDIO_AESIV).ok()
|
||||||
GenericArray::from_slice(&key.0),
|
} else {
|
||||||
GenericArray::from_slice(&AUDIO_AESIV),
|
// some files are unencrypted
|
||||||
)
|
None
|
||||||
});
|
};
|
||||||
|
|
||||||
AudioDecrypt { cipher, reader }
|
AudioDecrypt { cipher, reader }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-connect"
|
name = "librespot-connect"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
description = "The discovery and Spotify Connect logic for librespot"
|
description = "The discovery and Spotify Connect logic for librespot"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -21,12 +21,12 @@ tokio-stream = "0.1"
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-playback]
|
[dependencies.librespot-playback]
|
||||||
path = "../playback"
|
path = "../playback"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "../protocol"
|
path = "../protocol"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
|
@ -331,7 +331,7 @@ impl Spirc {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect *after* all message listeners are registered
|
// Connect *after* all message listeners are registered
|
||||||
session.connect(credentials).await?;
|
session.connect(credentials, true).await?;
|
||||||
|
|
||||||
let canonical_username = &session.username();
|
let canonical_username = &session.username();
|
||||||
debug!("canonical_username: {}", canonical_username);
|
debug!("canonical_username: {}", canonical_username);
|
||||||
|
@ -612,6 +612,7 @@ impl SpircTask {
|
||||||
}
|
}
|
||||||
SpircCommand::Shutdown => {
|
SpircCommand::Shutdown => {
|
||||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
||||||
|
self.player.stop();
|
||||||
self.shutdown = true;
|
self.shutdown = true;
|
||||||
if let Some(rx) = self.commands.as_mut() {
|
if let Some(rx) = self.commands.as_mut() {
|
||||||
rx.close()
|
rx.close()
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-core"
|
name = "librespot-core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "The core functionality provided by librespot"
|
description = "The core functionality provided by librespot"
|
||||||
|
@ -10,10 +10,10 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "../protocol"
|
path = "../protocol"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = "0.7"
|
aes = "0.8"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
byteorder = "1.4"
|
byteorder = "1.4"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
|
@ -22,7 +22,7 @@ form_urlencoded = "1.0"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
httparse = "1.5"
|
httparse = "1.7"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] }
|
hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] }
|
||||||
hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] }
|
hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] }
|
||||||
|
@ -33,30 +33,30 @@ num-bigint = { version = "0.4", features = ["rand"] }
|
||||||
num-derive = "0.3"
|
num-derive = "0.3"
|
||||||
num-integer = "0.1"
|
num-integer = "0.1"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.9"
|
once_cell = "1"
|
||||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||||
pbkdf2 = { version = "0.10", default-features = false, features = ["hmac"] }
|
pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] }
|
||||||
priority-queue = "1.2.1"
|
priority-queue = "1.2"
|
||||||
protobuf = "2"
|
protobuf = "2"
|
||||||
quick-xml = { version = "0.22", features = ["serialize"] }
|
quick-xml = { version = "0.23", features = ["serialize"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rsa = { version = "0.5", default-features = false, features = ["alloc"] }
|
rsa = "0.6"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sha-1 = "0.10"
|
sha1 = "0.10"
|
||||||
shannon = "0.2"
|
shannon = "0.2"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
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-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] }
|
tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] }
|
||||||
tokio-util = { version = "0.6", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "0.8", default-features = false, features = ["v4"] }
|
uuid = { version = "1", default-features = false, features = ["v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
vergen = { version = "6", default-features = false, features = ["build", "git"] }
|
vergen = { version = "7", default-features = false, features = ["build", "git"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
|
|
|
@ -13,11 +13,14 @@ fn main() {
|
||||||
|
|
||||||
vergen(config).expect("Unable to generate the cargo keys!");
|
vergen(config).expect("Unable to generate the cargo keys!");
|
||||||
|
|
||||||
let build_id: String = rand::thread_rng()
|
let build_id = match std::env::var("SOURCE_DATE_EPOCH") {
|
||||||
.sample_iter(Alphanumeric)
|
Ok(val) => val,
|
||||||
.take(8)
|
Err(_) => rand::thread_rng()
|
||||||
.map(char::from)
|
.sample_iter(Alphanumeric)
|
||||||
.collect();
|
.take(8)
|
||||||
|
.map(char::from)
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
|
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::io::{self, Read};
|
use std::io::{self, Read};
|
||||||
|
|
||||||
use aes::{Aes192, BlockDecrypt};
|
use aes::Aes192;
|
||||||
use byteorder::{BigEndian, ByteOrder};
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use pbkdf2::pbkdf2;
|
use pbkdf2::pbkdf2;
|
||||||
|
@ -106,13 +106,12 @@ impl Credentials {
|
||||||
|
|
||||||
// decrypt data using ECB mode without padding
|
// decrypt data using ECB mode without padding
|
||||||
let blob = {
|
let blob = {
|
||||||
use aes::cipher::generic_array::typenum::Unsigned;
|
|
||||||
use aes::cipher::generic_array::GenericArray;
|
use aes::cipher::generic_array::GenericArray;
|
||||||
use aes::cipher::{BlockCipher, NewBlockCipher};
|
use aes::cipher::{BlockDecrypt, BlockSizeUser, KeyInit};
|
||||||
|
|
||||||
let mut data = base64::decode(encrypted_blob)?;
|
let mut data = base64::decode(encrypted_blob)?;
|
||||||
let cipher = Aes192::new(GenericArray::from_slice(&key));
|
let cipher = Aes192::new(GenericArray::from_slice(&key));
|
||||||
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
|
let block_size = Aes192::block_size();
|
||||||
|
|
||||||
for chunk in data.chunks_exact_mut(block_size) {
|
for chunk in data.chunks_exact_mut(block_size) {
|
||||||
cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
|
cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
|
||||||
|
|
|
@ -375,7 +375,7 @@ impl Cache {
|
||||||
path
|
path
|
||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warn!("{}", e);
|
warn!("Invalid FileId: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ pub struct SessionConfig {
|
||||||
|
|
||||||
impl Default for SessionConfig {
|
impl Default for SessionConfig {
|
||||||
fn default() -> SessionConfig {
|
fn default() -> SessionConfig {
|
||||||
let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
let device_id = uuid::Uuid::new_v4().as_hyphenated().to_string();
|
||||||
SessionConfig {
|
SessionConfig {
|
||||||
client_id: KEYMASTER_CLIENT_ID.to_owned(),
|
client_id: KEYMASTER_CLIENT_ID.to_owned(),
|
||||||
device_id,
|
device_id,
|
||||||
|
|
|
@ -126,7 +126,11 @@ impl Session {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn connect(&self, credentials: Credentials) -> Result<(), Error> {
|
pub async fn connect(
|
||||||
|
&self,
|
||||||
|
credentials: Credentials,
|
||||||
|
store_credentials: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let ap = self.apresolver().resolve("accesspoint").await?;
|
let ap = self.apresolver().resolve("accesspoint").await?;
|
||||||
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
|
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
|
||||||
let mut transport = connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;
|
let mut transport = connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;
|
||||||
|
@ -136,7 +140,9 @@ impl Session {
|
||||||
info!("Authenticated as \"{}\" !", reusable_credentials.username);
|
info!("Authenticated as \"{}\" !", reusable_credentials.username);
|
||||||
self.set_username(&reusable_credentials.username);
|
self.set_username(&reusable_credentials.username);
|
||||||
if let Some(cache) = self.cache() {
|
if let Some(cache) = self.cache() {
|
||||||
cache.save_credentials(&reusable_credentials);
|
if store_credentials {
|
||||||
|
cache.save_credentials(&reusable_credentials);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (tx_connection, rx_connection) = mpsc::unbounded_channel();
|
let (tx_connection, rx_connection) = mpsc::unbounded_channel();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
convert::TryInto,
|
convert::TryInto,
|
||||||
|
fmt::Write,
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -273,7 +274,7 @@ impl SpClient {
|
||||||
Some(_) => "&",
|
Some(_) => "&",
|
||||||
None => "?",
|
None => "?",
|
||||||
};
|
};
|
||||||
url.push_str(&format!("{}product=0", separator));
|
let _ = write!(url, "{}product=0", separator);
|
||||||
|
|
||||||
let mut request = Request::builder()
|
let mut request = Request::builder()
|
||||||
.method(method)
|
.method(method)
|
||||||
|
@ -464,7 +465,7 @@ impl SpClient {
|
||||||
Some(_) => "&",
|
Some(_) => "&",
|
||||||
None => "?",
|
None => "?",
|
||||||
};
|
};
|
||||||
url.push_str(&format!("{}cid={}", separator, self.session().client_id()));
|
let _ = write!(url, "{}cid={}", separator, self.session().client_id());
|
||||||
|
|
||||||
self.request_url(url).await
|
self.request_url(url).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -200,7 +200,6 @@ impl SpotifyId {
|
||||||
/// character long `String`.
|
/// character long `String`.
|
||||||
///
|
///
|
||||||
/// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
pub fn to_base62(&self) -> Result<String, Error> {
|
pub fn to_base62(&self) -> Result<String, Error> {
|
||||||
let mut dst = [0u8; 22];
|
let mut dst = [0u8; 22];
|
||||||
|
@ -258,7 +257,6 @@ impl SpotifyId {
|
||||||
/// be encoded as `unknown`.
|
/// be encoded as `unknown`.
|
||||||
///
|
///
|
||||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||||
|
|
||||||
#[allow(clippy::wrong_self_convention)]
|
#[allow(clippy::wrong_self_convention)]
|
||||||
pub fn to_uri(&self) -> Result<String, Error> {
|
pub fn to_uri(&self) -> Result<String, Error> {
|
||||||
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
||||||
|
|
|
@ -7,7 +7,7 @@ use librespot_core::{authentication::Credentials, config::SessionConfig, session
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_connection() {
|
async fn test_connection() {
|
||||||
timeout(Duration::from_secs(30), async {
|
timeout(Duration::from_secs(30), async {
|
||||||
let result = Session::new(SessionConfig::default(), None)
|
let result = Session::new(SessionConfig::default(), None, false)
|
||||||
.connect(Credentials::with_password("test", "test"))
|
.connect(Credentials::with_password("test", "test"))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-discovery"
|
name = "librespot-discovery"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
description = "The discovery logic for librespot"
|
description = "The discovery logic for librespot"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -8,26 +8,27 @@ repository = "https://github.com/librespot-org/librespot"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
aes = { version = "0.7", features = ["ctr"] }
|
aes = "0.8"
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
cfg-if = "1.0"
|
cfg-if = "1.0"
|
||||||
|
ctr = "0.9"
|
||||||
dns-sd = { version = "0.1.3", optional = true }
|
dns-sd = { version = "0.1.3", optional = true }
|
||||||
form_urlencoded = "1.0"
|
form_urlencoded = "1.0"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
hyper = { version = "0.14", features = ["http1", "server", "tcp"] }
|
hyper = { version = "0.14", features = ["http1", "server", "tcp"] }
|
||||||
libmdns = "0.6"
|
libmdns = "0.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
sha-1 = "0.10"
|
sha1 = "0.10"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
|
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
|
|
@ -52,13 +52,13 @@ pub struct Builder {
|
||||||
/// Errors that can occur while setting up a [`Discovery`] instance.
|
/// Errors that can occur while setting up a [`Discovery`] instance.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum DiscoveryError {
|
pub enum DiscoveryError {
|
||||||
/// Setting up service discovery via DNS-SD failed.
|
#[error("Creating SHA1 block cipher failed")]
|
||||||
|
AesError(#[from] aes::cipher::InvalidLength),
|
||||||
#[error("Setting up dns-sd failed: {0}")]
|
#[error("Setting up dns-sd failed: {0}")]
|
||||||
DnsSdError(#[from] io::Error),
|
DnsSdError(#[from] io::Error),
|
||||||
/// Setting up the http server failed.
|
|
||||||
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
|
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
|
||||||
HmacError(Vec<u8>),
|
HmacError(Vec<u8>),
|
||||||
#[error("Setting up the http server failed: {0}")]
|
#[error("Setting up the HTTP server failed: {0}")]
|
||||||
HttpServerError(#[from] hyper::Error),
|
HttpServerError(#[from] hyper::Error),
|
||||||
#[error("Missing params for key {0}")]
|
#[error("Missing params for key {0}")]
|
||||||
ParamsError(&'static str),
|
ParamsError(&'static str),
|
||||||
|
@ -67,6 +67,7 @@ pub enum DiscoveryError {
|
||||||
impl From<DiscoveryError> for Error {
|
impl From<DiscoveryError> for Error {
|
||||||
fn from(err: DiscoveryError) -> Self {
|
fn from(err: DiscoveryError) -> Self {
|
||||||
match err {
|
match err {
|
||||||
|
DiscoveryError::AesError(_) => Error::unavailable(err),
|
||||||
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
|
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
|
||||||
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
|
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
|
||||||
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
|
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
|
||||||
|
|
|
@ -8,11 +8,7 @@ use std::{
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use aes::{
|
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||||
cipher::generic_array::GenericArray,
|
|
||||||
cipher::{NewCipher, StreamCipher},
|
|
||||||
Aes128Ctr,
|
|
||||||
};
|
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use futures_util::{FutureExt, TryFutureExt};
|
use futures_util::{FutureExt, TryFutureExt};
|
||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
|
@ -32,6 +28,8 @@ use crate::{
|
||||||
core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error},
|
core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
|
||||||
|
|
||||||
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
||||||
|
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -151,10 +149,8 @@ impl RequestHandler {
|
||||||
|
|
||||||
let decrypted = {
|
let decrypted = {
|
||||||
let mut data = encrypted.to_vec();
|
let mut data = encrypted.to_vec();
|
||||||
let mut cipher = Aes128Ctr::new(
|
let mut cipher = Aes128Ctr::new_from_slices(&encryption_key[0..16], iv)
|
||||||
GenericArray::from_slice(&encryption_key[0..16]),
|
.map_err(DiscoveryError::AesError)?;
|
||||||
GenericArray::from_slice(iv),
|
|
||||||
);
|
|
||||||
cipher.apply_keystream(&mut data);
|
cipher.apply_keystream(&mut data);
|
||||||
data
|
data
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,7 @@ async fn main() {
|
||||||
let credentials = Credentials::with_password(&args[1], &args[2]);
|
let credentials = Credentials::with_password(&args[1], &args[2]);
|
||||||
let session = Session::new(session_config, None);
|
let session = Session::new(session_config, None);
|
||||||
|
|
||||||
match session.connect(credentials).await {
|
match session.connect(credentials, false).await {
|
||||||
Ok(()) => println!(
|
Ok(()) => println!(
|
||||||
"Token: {:#?}",
|
"Token: {:#?}",
|
||||||
session.token_provider().get_token(SCOPES).await.unwrap()
|
session.token_provider().get_token(SCOPES).await.unwrap()
|
||||||
|
|
|
@ -7,6 +7,7 @@ use librespot::{
|
||||||
playback::{
|
playback::{
|
||||||
audio_backend,
|
audio_backend,
|
||||||
config::{AudioFormat, PlayerConfig},
|
config::{AudioFormat, PlayerConfig},
|
||||||
|
mixer::NoOpVolume,
|
||||||
player::Player,
|
player::Player,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -30,12 +31,12 @@ async fn main() {
|
||||||
|
|
||||||
println!("Connecting...");
|
println!("Connecting...");
|
||||||
let session = Session::new(session_config, None);
|
let session = Session::new(session_config, None);
|
||||||
if let Err(e) = session.connect(credentials).await {
|
if let Err(e) = session.connect(credentials, false).await {
|
||||||
println!("Error connecting: {}", e);
|
println!("Error connecting: {}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (mut player, _) = Player::new(player_config, session, None, move || {
|
let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || {
|
||||||
backend(None, audio_format)
|
backend(None, audio_format)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ async fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
let session = Session::new(session_config, None);
|
let session = Session::new(session_config, None);
|
||||||
if let Err(e) = session.connect(credentials).await {
|
if let Err(e) = session.connect(credentials, false).await {
|
||||||
println!("Error connecting: {}", e);
|
println!("Error connecting: {}", e);
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-metadata"
|
name = "librespot-metadata"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||||
description = "The metadata logic for librespot"
|
description = "The metadata logic for librespot"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -14,12 +14,12 @@ bytes = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
protobuf = "2"
|
protobuf = "2"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
uuid = { version = "0.8", default-features = false }
|
uuid = { version = "1", default-features = false }
|
||||||
|
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies.librespot-protocol]
|
[dependencies.librespot-protocol]
|
||||||
path = "../protocol"
|
path = "../protocol"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
use crate::MetadataError;
|
use crate::MetadataError;
|
||||||
|
|
||||||
use librespot_core::{Error, Session};
|
use librespot_core::{Error, Session};
|
||||||
|
@ -13,10 +15,10 @@ pub trait MercuryRequest {
|
||||||
Some(_) => "&",
|
Some(_) => "&",
|
||||||
None => "?",
|
None => "?",
|
||||||
};
|
};
|
||||||
metrics_uri.push_str(&format!("{}country={}", separator, session.country()));
|
let _ = write!(metrics_uri, "{}country={}", separator, session.country());
|
||||||
|
|
||||||
if let Some(product) = session.get_user_attribute("type") {
|
if let Some(product) = session.get_user_attribute("type") {
|
||||||
metrics_uri.push_str(&format!("&product={}", product));
|
let _ = write!(metrics_uri, "&product={}", product);
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Requesting {}", metrics_uri);
|
trace!("Requesting {}", metrics_uri);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-playback"
|
name = "librespot-playback"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
|
authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
|
||||||
description = "The audio playback logic for librespot"
|
description = "The audio playback logic for librespot"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
@ -9,21 +9,21 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies.librespot-audio]
|
[dependencies.librespot-audio]
|
||||||
path = "../audio"
|
path = "../audio"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
[dependencies.librespot-core]
|
[dependencies.librespot-core]
|
||||||
path = "../core"
|
path = "../core"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
[dependencies.librespot-metadata]
|
[dependencies.librespot-metadata]
|
||||||
path = "../metadata"
|
path = "../metadata"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
byteorder = "1"
|
byteorder = "1"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||||
shell-words = "1.0.0"
|
shell-words = "1.1"
|
||||||
thiserror = "1.0"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
|
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
|
||||||
zerocopy = "0.6"
|
zerocopy = "0.6"
|
||||||
|
|
||||||
|
@ -32,11 +32,11 @@ alsa = { version = "0.6", 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.8", optional = true }
|
jack = { version = "0.10", optional = true }
|
||||||
sdl2 = { version = "0.35", optional = true }
|
sdl2 = { version = "0.35", optional = true }
|
||||||
gst = { package = "gstreamer", version = "0.18", optional = true }
|
gstreamer = { version = "0.18", optional = true }
|
||||||
gst-app = { package = "gstreamer-app", version = "0.18", optional = true }
|
gstreamer-app = { version = "0.18", optional = true }
|
||||||
gst-audio = { package = "gstreamer-audio", version = "0.18", optional = true }
|
gstreamer-audio = { version = "0.18", optional = true }
|
||||||
glib = { version = "0.15", optional = true }
|
glib = { version = "0.15", optional = true }
|
||||||
|
|
||||||
# Rodio dependencies
|
# Rodio dependencies
|
||||||
|
@ -61,6 +61,6 @@ jackaudio-backend = ["jack"]
|
||||||
rodio-backend = ["rodio", "cpal"]
|
rodio-backend = ["rodio", "cpal"]
|
||||||
rodiojack-backend = ["rodio", "cpal/jack"]
|
rodiojack-backend = ["rodio", "cpal/jack"]
|
||||||
sdl-backend = ["sdl2"]
|
sdl-backend = ["sdl2"]
|
||||||
gstreamer-backend = ["gst", "gst-app", "gst-audio", "glib"]
|
gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio", "glib"]
|
||||||
|
|
||||||
passthrough-decoder = ["ogg"]
|
passthrough-decoder = ["ogg"]
|
||||||
|
|
|
@ -6,7 +6,6 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
||||||
use alsa::device_name::HintIter;
|
use alsa::device_name::HintIter;
|
||||||
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
||||||
use alsa::{Direction, ValueOr};
|
use alsa::{Direction, ValueOr};
|
||||||
use std::cmp::min;
|
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -467,7 +466,7 @@ impl SinkAsBytes for AlsaSink {
|
||||||
loop {
|
loop {
|
||||||
let data_left = data_len - start_index;
|
let data_left = data_len - start_index;
|
||||||
let space_left = capacity - self.period_buffer.len();
|
let space_left = capacity - self.period_buffer.len();
|
||||||
let data_to_buffer = min(data_left, space_left);
|
let data_to_buffer = data_left.min(space_left);
|
||||||
let end_index = start_index + data_to_buffer;
|
let end_index = start_index + data_to_buffer;
|
||||||
|
|
||||||
self.period_buffer
|
self.period_buffer
|
||||||
|
|
|
@ -1,9 +1,13 @@
|
||||||
use gst::{
|
use gstreamer::{
|
||||||
event::{FlushStart, FlushStop},
|
event::{FlushStart, FlushStop},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
State,
|
State,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use gstreamer as gst;
|
||||||
|
use gstreamer_app as gst_app;
|
||||||
|
use gstreamer_audio as gst_audio;
|
||||||
|
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -41,7 +45,7 @@ impl Open for GstreamerSink {
|
||||||
let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps");
|
let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps");
|
||||||
|
|
||||||
let sample_size = format.size();
|
let sample_size = format.size();
|
||||||
let gst_bytes = NUM_CHANNELS as usize * 1024 * sample_size;
|
let gst_bytes = NUM_CHANNELS as usize * 2048 * sample_size;
|
||||||
|
|
||||||
let pipeline = gst::Pipeline::new(None);
|
let pipeline = gst::Pipeline::new(None);
|
||||||
let appsrc = gst::ElementFactory::make("appsrc", None)
|
let appsrc = gst::ElementFactory::make("appsrc", None)
|
||||||
|
|
|
@ -2,9 +2,38 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
||||||
use crate::config::AudioFormat;
|
use crate::config::AudioFormat;
|
||||||
use crate::convert::Converter;
|
use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
use crate::decoder::AudioPacket;
|
||||||
|
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::process::exit;
|
use std::process::exit;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum StdoutError {
|
||||||
|
#[error("<StdoutSink> {0}")]
|
||||||
|
OnWrite(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<StdoutSink> File Path {file} Can Not be Opened and/or Created, {e}")]
|
||||||
|
OpenFailure { file: String, e: std::io::Error },
|
||||||
|
|
||||||
|
#[error("<StdoutSink> Failed to Flush the Output Stream, {0}")]
|
||||||
|
FlushFailure(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<StdoutSink> The Output Stream is None")]
|
||||||
|
NoOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StdoutError> for SinkError {
|
||||||
|
fn from(e: StdoutError) -> SinkError {
|
||||||
|
use StdoutError::*;
|
||||||
|
let es = e.to_string();
|
||||||
|
match e {
|
||||||
|
FlushFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
||||||
|
OpenFailure { .. } => SinkError::ConnectionRefused(es),
|
||||||
|
NoOutput => SinkError::NotConnected(es),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct StdoutSink {
|
pub struct StdoutSink {
|
||||||
output: Option<Box<dyn Write>>,
|
output: Option<Box<dyn Write>>,
|
||||||
|
@ -15,13 +44,12 @@ pub struct StdoutSink {
|
||||||
impl Open for StdoutSink {
|
impl Open for StdoutSink {
|
||||||
fn open(file: Option<String>, format: AudioFormat) -> Self {
|
fn open(file: Option<String>, format: AudioFormat) -> Self {
|
||||||
if let Some("?") = file.as_deref() {
|
if let Some("?") = file.as_deref() {
|
||||||
info!("Usage:");
|
println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n");
|
||||||
println!(" Output to stdout: --backend pipe");
|
|
||||||
println!(" Output to file: --backend pipe --device {{filename}}");
|
|
||||||
exit(0);
|
exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("Using pipe sink with format: {:?}", format);
|
info!("Using StdoutSink (pipe) with format: {:?}", format);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
output: None,
|
output: None,
|
||||||
file,
|
file,
|
||||||
|
@ -32,21 +60,31 @@ impl Open for StdoutSink {
|
||||||
|
|
||||||
impl Sink for StdoutSink {
|
impl Sink for StdoutSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
if self.output.is_none() {
|
self.output.get_or_insert({
|
||||||
let output: Box<dyn Write> = match self.file.as_deref() {
|
match self.file.as_deref() {
|
||||||
Some(file) => {
|
Some(file) => Box::new(
|
||||||
let open_op = OpenOptions::new()
|
OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
.create(true)
|
.create(true)
|
||||||
.open(file)
|
.open(file)
|
||||||
.map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
|
.map_err(|e| StdoutError::OpenFailure {
|
||||||
Box::new(open_op)
|
file: file.to_string(),
|
||||||
}
|
e,
|
||||||
|
})?,
|
||||||
|
),
|
||||||
None => Box::new(io::stdout()),
|
None => Box::new(io::stdout()),
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
self.output = Some(output);
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stop(&mut self) -> SinkResult<()> {
|
||||||
|
self.output
|
||||||
|
.take()
|
||||||
|
.ok_or(StdoutError::NoOutput)?
|
||||||
|
.flush()
|
||||||
|
.map_err(StdoutError::FlushFailure)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -56,19 +94,11 @@ impl Sink for StdoutSink {
|
||||||
|
|
||||||
impl SinkAsBytes for StdoutSink {
|
impl SinkAsBytes for StdoutSink {
|
||||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||||
match self.output.as_deref_mut() {
|
self.output
|
||||||
Some(output) => {
|
.as_deref_mut()
|
||||||
output
|
.ok_or(StdoutError::NoOutput)?
|
||||||
.write_all(data)
|
.write_all(data)
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
.map_err(StdoutError::OnWrite)?;
|
||||||
output
|
|
||||||
.flush()
|
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
return Err(SinkError::NotConnected("Output is None".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,30 +4,75 @@ use crate::convert::Converter;
|
||||||
use crate::decoder::AudioPacket;
|
use crate::decoder::AudioPacket;
|
||||||
use shell_words::split;
|
use shell_words::split;
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::{ErrorKind, Write};
|
||||||
use std::process::{exit, Child, Command, Stdio};
|
use std::process::{exit, Child, Command, Stdio};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
enum SubprocessError {
|
||||||
|
#[error("<SubprocessSink> {0}")]
|
||||||
|
OnWrite(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Command {command} Can Not be Executed, {e}")]
|
||||||
|
SpawnFailure { command: String, e: std::io::Error },
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Failed to Parse Command args for {command}, {e}")]
|
||||||
|
InvalidArgs {
|
||||||
|
command: String,
|
||||||
|
e: shell_words::ParseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Failed to Flush the Subprocess, {0}")]
|
||||||
|
FlushFailure(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Failed to Kill the Subprocess, {0}")]
|
||||||
|
KillFailure(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Failed to Wait for the Subprocess to Exit, {0}")]
|
||||||
|
WaitFailure(std::io::Error),
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> The Subprocess is no longer able to accept Bytes")]
|
||||||
|
WriteZero,
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> Missing Required Shell Command")]
|
||||||
|
MissingCommand,
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> The Subprocess is None")]
|
||||||
|
NoChild,
|
||||||
|
|
||||||
|
#[error("<SubprocessSink> The Subprocess's stdin is None")]
|
||||||
|
NoStdin,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SubprocessError> for SinkError {
|
||||||
|
fn from(e: SubprocessError) -> SinkError {
|
||||||
|
use SubprocessError::*;
|
||||||
|
let es = e.to_string();
|
||||||
|
match e {
|
||||||
|
FlushFailure(_) | KillFailure(_) | WaitFailure(_) | OnWrite(_) | WriteZero => {
|
||||||
|
SinkError::OnWrite(es)
|
||||||
|
}
|
||||||
|
SpawnFailure { .. } => SinkError::ConnectionRefused(es),
|
||||||
|
MissingCommand | InvalidArgs { .. } => SinkError::InvalidParams(es),
|
||||||
|
NoChild | NoStdin => SinkError::NotConnected(es),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SubprocessSink {
|
pub struct SubprocessSink {
|
||||||
shell_command: String,
|
shell_command: Option<String>,
|
||||||
child: Option<Child>,
|
child: Option<Child>,
|
||||||
format: AudioFormat,
|
format: AudioFormat,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Open for SubprocessSink {
|
impl Open for SubprocessSink {
|
||||||
fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
|
fn open(shell_command: Option<String>, format: AudioFormat) -> Self {
|
||||||
let shell_command = match shell_command.as_deref() {
|
if let Some("?") = shell_command.as_deref() {
|
||||||
Some("?") => {
|
println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n");
|
||||||
info!("Usage: --backend subprocess --device {{shell_command}}");
|
exit(0);
|
||||||
exit(0);
|
}
|
||||||
}
|
|
||||||
Some(cmd) => cmd.to_owned(),
|
|
||||||
None => {
|
|
||||||
error!("subprocess sink requires specifying a shell command");
|
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
info!("Using subprocess sink with format: {:?}", format);
|
info!("Using SubprocessSink with format: {:?}", format);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
shell_command,
|
shell_command,
|
||||||
|
@ -39,26 +84,53 @@ impl Open for SubprocessSink {
|
||||||
|
|
||||||
impl Sink for SubprocessSink {
|
impl Sink for SubprocessSink {
|
||||||
fn start(&mut self) -> SinkResult<()> {
|
fn start(&mut self) -> SinkResult<()> {
|
||||||
let args = split(&self.shell_command).unwrap();
|
self.child.get_or_insert({
|
||||||
let child = Command::new(&args[0])
|
match self.shell_command.as_deref() {
|
||||||
.args(&args[1..])
|
Some(command) => {
|
||||||
.stdin(Stdio::piped())
|
let args = split(command).map_err(|e| SubprocessError::InvalidArgs {
|
||||||
.spawn()
|
command: command.to_string(),
|
||||||
.map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
|
e,
|
||||||
self.child = Some(child);
|
})?;
|
||||||
|
|
||||||
|
Command::new(&args[0])
|
||||||
|
.args(&args[1..])
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| SubprocessError::SpawnFailure {
|
||||||
|
command: command.to_string(),
|
||||||
|
e,
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
None => return Err(SubprocessError::MissingCommand.into()),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&mut self) -> SinkResult<()> {
|
fn stop(&mut self) -> SinkResult<()> {
|
||||||
if let Some(child) = &mut self.child.take() {
|
let child = &mut self.child.take().ok_or(SubprocessError::NoChild)?;
|
||||||
child
|
|
||||||
.kill()
|
match child.try_wait() {
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
// The process has already exited
|
||||||
child
|
// nothing to do.
|
||||||
.wait()
|
Ok(Some(_)) => Ok(()),
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
Ok(_) => {
|
||||||
|
// The process Must DIE!!!
|
||||||
|
child
|
||||||
|
.stdin
|
||||||
|
.take()
|
||||||
|
.ok_or(SubprocessError::NoStdin)?
|
||||||
|
.flush()
|
||||||
|
.map_err(SubprocessError::FlushFailure)?;
|
||||||
|
|
||||||
|
child.kill().map_err(SubprocessError::KillFailure)?;
|
||||||
|
child.wait().map_err(SubprocessError::WaitFailure)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => Err(SubprocessError::WaitFailure(e).into()),
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sink_as_bytes!();
|
sink_as_bytes!();
|
||||||
|
@ -66,22 +138,67 @@ impl Sink for SubprocessSink {
|
||||||
|
|
||||||
impl SinkAsBytes for SubprocessSink {
|
impl SinkAsBytes for SubprocessSink {
|
||||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||||
if let Some(child) = &mut self.child {
|
// We get one attempted restart per write.
|
||||||
let child_stdin = child
|
// We don't want to get stuck in a restart loop.
|
||||||
|
let mut restarted = false;
|
||||||
|
let mut start_index = 0;
|
||||||
|
let data_len = data.len();
|
||||||
|
let mut end_index = data_len;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match self
|
||||||
|
.child
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(SubprocessError::NoChild)?
|
||||||
.stdin
|
.stdin
|
||||||
.as_mut()
|
.as_ref()
|
||||||
.ok_or_else(|| SinkError::NotConnected("Child is None".to_string()))?;
|
.ok_or(SubprocessError::NoStdin)?
|
||||||
child_stdin
|
.write(&data[start_index..end_index])
|
||||||
.write_all(data)
|
{
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
Ok(0) => {
|
||||||
child_stdin
|
// Potentially fatal.
|
||||||
.flush()
|
// As per the docs a return value of 0
|
||||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
// means we shouldn't try to write to the
|
||||||
|
// process anymore so let's try a restart
|
||||||
|
// if we haven't already.
|
||||||
|
self.try_restart(SubprocessError::WriteZero, &mut restarted)?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Ok(bytes_written) => {
|
||||||
|
// What we want, a successful write.
|
||||||
|
start_index = data_len.min(start_index + bytes_written);
|
||||||
|
end_index = data_len.min(start_index + bytes_written);
|
||||||
|
|
||||||
|
if end_index == data_len {
|
||||||
|
break Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Non-fatal, retry the write.
|
||||||
|
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||||
|
Err(e) => {
|
||||||
|
// Very possibly fatal,
|
||||||
|
// but let's try a restart anyway if we haven't already.
|
||||||
|
self.try_restart(SubprocessError::OnWrite(e), &mut restarted)?;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubprocessSink {
|
impl SubprocessSink {
|
||||||
pub const NAME: &'static str = "subprocess";
|
pub const NAME: &'static str = "subprocess";
|
||||||
|
|
||||||
|
fn try_restart(&mut self, e: SubprocessError, restarted: &mut bool) -> SinkResult<()> {
|
||||||
|
// If the restart fails throw the original error back.
|
||||||
|
if !*restarted && self.stop().is_ok() && self.start().is_ok() {
|
||||||
|
*restarted = true;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(e.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,23 +104,33 @@ impl Mixer for AlsaMixer {
|
||||||
|
|
||||||
let min_db = min_millibel.to_db() as f64;
|
let min_db = min_millibel.to_db() as f64;
|
||||||
let max_db = max_millibel.to_db() as f64;
|
let max_db = max_millibel.to_db() as f64;
|
||||||
let mut db_range = f64::abs(max_db - min_db);
|
let reported_db_range = f64::abs(max_db - min_db);
|
||||||
|
|
||||||
// Synchronize the volume control dB range with the mixer control,
|
// Synchronize the volume control dB range with the mixer control,
|
||||||
// unless it was already set with a command line option.
|
// unless it was already set with a command line option.
|
||||||
if !config.volume_ctrl.range_ok() {
|
let db_range = if config.volume_ctrl.range_ok() {
|
||||||
if db_range > 100.0 {
|
|
||||||
debug!("Alsa mixer reported dB range > 100, which is suspect");
|
|
||||||
warn!("Please manually set `--volume-range` if this is incorrect");
|
|
||||||
}
|
|
||||||
config.volume_ctrl.set_db_range(db_range);
|
|
||||||
} else {
|
|
||||||
let db_range_override = config.volume_ctrl.db_range();
|
let db_range_override = config.volume_ctrl.db_range();
|
||||||
|
if db_range_override.is_normal() {
|
||||||
|
db_range_override
|
||||||
|
} else {
|
||||||
|
reported_db_range
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
config.volume_ctrl.set_db_range(reported_db_range);
|
||||||
|
reported_db_range
|
||||||
|
};
|
||||||
|
|
||||||
|
if reported_db_range == db_range {
|
||||||
|
debug!("Alsa dB volume range was reported as {}", reported_db_range);
|
||||||
|
if reported_db_range > 100.0 {
|
||||||
|
debug!("Alsa mixer reported dB range > 100, which is suspect");
|
||||||
|
debug!("Please manually set `--volume-range` if this is incorrect");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
debug!(
|
debug!(
|
||||||
"Alsa dB volume range was detected as {} but overridden as {}",
|
"Alsa dB volume range was reported as {} but overridden to {}",
|
||||||
db_range, db_range_override
|
reported_db_range, db_range
|
||||||
);
|
);
|
||||||
db_range = db_range_override;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For hardware controls with a small range (24 dB or less),
|
// For hardware controls with a small range (24 dB or less),
|
||||||
|
|
|
@ -3,6 +3,8 @@ use crate::config::VolumeCtrl;
|
||||||
pub mod mappings;
|
pub mod mappings;
|
||||||
use self::mappings::MappedCtrl;
|
use self::mappings::MappedCtrl;
|
||||||
|
|
||||||
|
pub struct NoOpVolume;
|
||||||
|
|
||||||
pub trait Mixer: Send {
|
pub trait Mixer: Send {
|
||||||
fn open(config: MixerConfig) -> Self
|
fn open(config: MixerConfig) -> Self
|
||||||
where
|
where
|
||||||
|
@ -11,13 +13,19 @@ pub trait Mixer: Send {
|
||||||
fn set_volume(&self, volume: u16);
|
fn set_volume(&self, volume: u16);
|
||||||
fn volume(&self) -> u16;
|
fn volume(&self) -> u16;
|
||||||
|
|
||||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||||
None
|
Box::new(NoOpVolume)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait AudioFilter {
|
pub trait VolumeGetter {
|
||||||
fn modify_stream(&self, data: &mut [f64]);
|
fn attenuation_factor(&self) -> f64;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VolumeGetter for NoOpVolume {
|
||||||
|
fn attenuation_factor(&self) -> f64 {
|
||||||
|
1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod softmixer;
|
pub mod softmixer;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use super::AudioFilter;
|
use super::VolumeGetter;
|
||||||
use super::{MappedCtrl, VolumeCtrl};
|
use super::{MappedCtrl, VolumeCtrl};
|
||||||
use super::{Mixer, MixerConfig};
|
use super::{Mixer, MixerConfig};
|
||||||
|
|
||||||
|
@ -35,10 +35,8 @@ impl Mixer for SoftMixer {
|
||||||
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||||
Some(Box::new(SoftVolumeApplier {
|
Box::new(SoftVolume(self.volume.clone()))
|
||||||
volume: self.volume.clone(),
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,17 +44,10 @@ impl SoftMixer {
|
||||||
pub const NAME: &'static str = "softvol";
|
pub const NAME: &'static str = "softvol";
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SoftVolumeApplier {
|
struct SoftVolume(Arc<AtomicU64>);
|
||||||
volume: Arc<AtomicU64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AudioFilter for SoftVolumeApplier {
|
impl VolumeGetter for SoftVolume {
|
||||||
fn modify_stream(&self, data: &mut [f64]) {
|
fn attenuation_factor(&self) -> f64 {
|
||||||
let volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
|
f64::from_bits(self.0.load(Ordering::Relaxed))
|
||||||
if volume < 1.0 {
|
|
||||||
for x in data.iter_mut() {
|
|
||||||
*x *= volume;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ use crate::{
|
||||||
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
||||||
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
||||||
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
||||||
mixer::AudioFilter,
|
mixer::VolumeGetter,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "passthrough-decoder")]
|
#[cfg(feature = "passthrough-decoder")]
|
||||||
|
@ -81,7 +81,7 @@ struct PlayerInternal {
|
||||||
sink: Box<dyn Sink>,
|
sink: Box<dyn Sink>,
|
||||||
sink_status: SinkStatus,
|
sink_status: SinkStatus,
|
||||||
sink_event_callback: Option<SinkEventCallback>,
|
sink_event_callback: Option<SinkEventCallback>,
|
||||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||||
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||||
converter: Converter,
|
converter: Converter,
|
||||||
|
|
||||||
|
@ -368,7 +368,7 @@ impl Player {
|
||||||
pub fn new<F>(
|
pub fn new<F>(
|
||||||
config: PlayerConfig,
|
config: PlayerConfig,
|
||||||
session: Session,
|
session: Session,
|
||||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||||
sink_builder: F,
|
sink_builder: F,
|
||||||
) -> (Player, PlayerEventChannel)
|
) -> (Player, PlayerEventChannel)
|
||||||
where
|
where
|
||||||
|
@ -420,7 +420,7 @@ impl Player {
|
||||||
sink: sink_builder(),
|
sink: sink_builder(),
|
||||||
sink_status: SinkStatus::Closed,
|
sink_status: SinkStatus::Closed,
|
||||||
sink_event_callback: None,
|
sink_event_callback: None,
|
||||||
audio_filter,
|
volume_getter,
|
||||||
event_senders: [event_sender].to_vec(),
|
event_senders: [event_sender].to_vec(),
|
||||||
converter,
|
converter,
|
||||||
|
|
||||||
|
@ -1445,109 +1445,111 @@ impl PlayerInternal {
|
||||||
Some((_, mut packet)) => {
|
Some((_, mut packet)) => {
|
||||||
if !packet.is_empty() {
|
if !packet.is_empty() {
|
||||||
if let AudioPacket::Samples(ref mut data) = packet {
|
if let AudioPacket::Samples(ref mut data) = packet {
|
||||||
|
// Get the volume for the packet.
|
||||||
|
// In the case of hardware volume control this will
|
||||||
|
// always be 1.0 (no change).
|
||||||
|
let volume = self.volume_getter.attenuation_factor();
|
||||||
|
|
||||||
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
|
// For the basic normalisation method, a normalisation factor of 1.0 indicates that
|
||||||
// there is nothing to normalise (all samples should pass unaltered). For the
|
// there is nothing to normalise (all samples should pass unaltered). For the
|
||||||
// dynamic method, there may still be peaks that we want to shave off.
|
// dynamic method, there may still be peaks that we want to shave off.
|
||||||
if self.config.normalisation {
|
|
||||||
if self.config.normalisation_method == NormalisationMethod::Basic
|
|
||||||
&& normalisation_factor < 1.0
|
|
||||||
{
|
|
||||||
for sample in data.iter_mut() {
|
|
||||||
*sample *= normalisation_factor;
|
|
||||||
}
|
|
||||||
} else if self.config.normalisation_method
|
|
||||||
== NormalisationMethod::Dynamic
|
|
||||||
{
|
|
||||||
// zero-cost shorthands
|
|
||||||
let threshold_db = self.config.normalisation_threshold_dbfs;
|
|
||||||
let knee_db = self.config.normalisation_knee_db;
|
|
||||||
let attack_cf = self.config.normalisation_attack_cf;
|
|
||||||
let release_cf = self.config.normalisation_release_cf;
|
|
||||||
|
|
||||||
for sample in data.iter_mut() {
|
// No matter the case we apply volume attenuation last if there is any.
|
||||||
*sample *= normalisation_factor;
|
if !self.config.normalisation && volume < 1.0 {
|
||||||
|
for sample in data.iter_mut() {
|
||||||
// Feedforward limiter in the log domain
|
*sample *= volume;
|
||||||
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
|
|
||||||
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
|
|
||||||
// Engineering Society, 60, 399-408.
|
|
||||||
|
|
||||||
// Some tracks have samples that are precisely 0.0. That's silence
|
|
||||||
// and we know we don't need to limit that, in which we can spare
|
|
||||||
// the CPU cycles.
|
|
||||||
//
|
|
||||||
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
|
||||||
// peak detector stuck. Also catch the unlikely case where a sample
|
|
||||||
// is decoded as `NaN` or some other non-normal value.
|
|
||||||
let limiter_db = if sample.is_normal() {
|
|
||||||
// step 1-4: half-wave rectification and conversion into dB
|
|
||||||
// and gain computer with soft knee and subtractor
|
|
||||||
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
|
||||||
let knee_boundary_db = bias_db * 2.0;
|
|
||||||
|
|
||||||
if knee_boundary_db < -knee_db {
|
|
||||||
0.0
|
|
||||||
} else if knee_boundary_db.abs() <= knee_db {
|
|
||||||
// The textbook equation:
|
|
||||||
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
|
||||||
// Simplifies to:
|
|
||||||
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// Which in our case further simplifies to:
|
|
||||||
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
// because knee_boundary_db is 2.0 * bias_db.
|
|
||||||
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
|
||||||
} else {
|
|
||||||
// Textbook:
|
|
||||||
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
|
||||||
bias_db
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
0.0
|
|
||||||
};
|
|
||||||
|
|
||||||
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
|
||||||
// were in attack or (3) we were in release, and that attack/
|
|
||||||
// release wasn't finished yet.
|
|
||||||
if limiter_db > 0.0
|
|
||||||
|| self.normalisation_integrator > 0.0
|
|
||||||
|| self.normalisation_peak > 0.0
|
|
||||||
{
|
|
||||||
// step 5: smooth, decoupled peak detector
|
|
||||||
// Textbook:
|
|
||||||
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
|
||||||
// Simplifies to:
|
|
||||||
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
|
||||||
self.normalisation_integrator = f64::max(
|
|
||||||
limiter_db,
|
|
||||||
release_cf * self.normalisation_integrator
|
|
||||||
- release_cf * limiter_db
|
|
||||||
+ limiter_db,
|
|
||||||
);
|
|
||||||
// Textbook:
|
|
||||||
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
|
||||||
// Simplifies to:
|
|
||||||
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
|
||||||
self.normalisation_peak = attack_cf
|
|
||||||
* self.normalisation_peak
|
|
||||||
- attack_cf * self.normalisation_integrator
|
|
||||||
+ self.normalisation_integrator;
|
|
||||||
|
|
||||||
// step 6: make-up gain applied later (volume attenuation)
|
|
||||||
// Applying the standard normalisation factor here won't work,
|
|
||||||
// because there are tracks with peaks as high as 6 dB above
|
|
||||||
// the default threshold, so that would clip.
|
|
||||||
|
|
||||||
// steps 7-8: conversion into level and multiplication into gain stage
|
|
||||||
*sample *= db_to_ratio(-self.normalisation_peak);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
} else if self.config.normalisation_method == NormalisationMethod::Basic
|
||||||
|
&& (normalisation_factor < 1.0 || volume < 1.0)
|
||||||
|
{
|
||||||
|
for sample in data.iter_mut() {
|
||||||
|
*sample *= normalisation_factor * volume;
|
||||||
|
}
|
||||||
|
} else if self.config.normalisation_method == NormalisationMethod::Dynamic {
|
||||||
|
// zero-cost shorthands
|
||||||
|
let threshold_db = self.config.normalisation_threshold_dbfs;
|
||||||
|
let knee_db = self.config.normalisation_knee_db;
|
||||||
|
let attack_cf = self.config.normalisation_attack_cf;
|
||||||
|
let release_cf = self.config.normalisation_release_cf;
|
||||||
|
|
||||||
// Apply volume attenuation last. TODO: make this so we can chain
|
for sample in data.iter_mut() {
|
||||||
// the normaliser and mixer as a processing pipeline.
|
*sample *= normalisation_factor;
|
||||||
if let Some(ref editor) = self.audio_filter {
|
|
||||||
editor.modify_stream(data)
|
// Feedforward limiter in the log domain
|
||||||
|
// After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic
|
||||||
|
// Range Compressor Design—A Tutorial and Analysis. Journal of The Audio
|
||||||
|
// Engineering Society, 60, 399-408.
|
||||||
|
|
||||||
|
// Some tracks have samples that are precisely 0.0. That's silence
|
||||||
|
// and we know we don't need to limit that, in which we can spare
|
||||||
|
// the CPU cycles.
|
||||||
|
//
|
||||||
|
// Also, calling `ratio_to_db(0.0)` returns `inf` and would get the
|
||||||
|
// peak detector stuck. Also catch the unlikely case where a sample
|
||||||
|
// is decoded as `NaN` or some other non-normal value.
|
||||||
|
let limiter_db = if sample.is_normal() {
|
||||||
|
// step 1-4: half-wave rectification and conversion into dB
|
||||||
|
// and gain computer with soft knee and subtractor
|
||||||
|
let bias_db = ratio_to_db(sample.abs()) - threshold_db;
|
||||||
|
let knee_boundary_db = bias_db * 2.0;
|
||||||
|
|
||||||
|
if knee_boundary_db < -knee_db {
|
||||||
|
0.0
|
||||||
|
} else if knee_boundary_db.abs() <= knee_db {
|
||||||
|
// The textbook equation:
|
||||||
|
// ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db))
|
||||||
|
// Simplifies to:
|
||||||
|
// ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
// Which in our case further simplifies to:
|
||||||
|
// (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
// because knee_boundary_db is 2.0 * bias_db.
|
||||||
|
(knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db)
|
||||||
|
} else {
|
||||||
|
// Textbook:
|
||||||
|
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
||||||
|
bias_db
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Spare the CPU unless (1) the limiter is engaged, (2) we
|
||||||
|
// were in attack or (3) we were in release, and that attack/
|
||||||
|
// release wasn't finished yet.
|
||||||
|
if limiter_db > 0.0
|
||||||
|
|| self.normalisation_integrator > 0.0
|
||||||
|
|| self.normalisation_peak > 0.0
|
||||||
|
{
|
||||||
|
// step 5: smooth, decoupled peak detector
|
||||||
|
// Textbook:
|
||||||
|
// release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db
|
||||||
|
// Simplifies to:
|
||||||
|
// release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db
|
||||||
|
self.normalisation_integrator = f64::max(
|
||||||
|
limiter_db,
|
||||||
|
release_cf * self.normalisation_integrator
|
||||||
|
- release_cf * limiter_db
|
||||||
|
+ limiter_db,
|
||||||
|
);
|
||||||
|
// Textbook:
|
||||||
|
// attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator
|
||||||
|
// Simplifies to:
|
||||||
|
// attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator
|
||||||
|
self.normalisation_peak = attack_cf * self.normalisation_peak
|
||||||
|
- attack_cf * self.normalisation_integrator
|
||||||
|
+ self.normalisation_integrator;
|
||||||
|
|
||||||
|
// step 6: make-up gain applied later (volume attenuation)
|
||||||
|
// Applying the standard normalisation factor here won't work,
|
||||||
|
// because there are tracks with peaks as high as 6 dB above
|
||||||
|
// the default threshold, so that would clip.
|
||||||
|
|
||||||
|
// steps 7-8: conversion into level and multiplication into gain stage
|
||||||
|
*sample *= db_to_ratio(-self.normalisation_peak);
|
||||||
|
}
|
||||||
|
|
||||||
|
*sample *= volume;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1979,7 +1981,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);
|
||||||
let result = match cmd {
|
match cmd {
|
||||||
PlayerCommand::Load {
|
PlayerCommand::Load {
|
||||||
track_id,
|
track_id,
|
||||||
play_request_id,
|
play_request_id,
|
||||||
|
@ -2034,7 +2036,7 @@ impl PlayerInternal {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(result)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn send_event(&mut self, event: PlayerEvent) {
|
fn send_event(&mut self, event: PlayerEvent) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "librespot-protocol"
|
name = "librespot-protocol"
|
||||||
version = "0.3.1"
|
version = "0.4.1"
|
||||||
authors = ["Paul Liétar <paul@lietar.net>"]
|
authors = ["Paul Liétar <paul@lietar.net>"]
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
description = "The protobuf logic for communicating with Spotify servers"
|
description = "The protobuf logic for communicating with Spotify servers"
|
||||||
|
|
|
@ -27,14 +27,17 @@ function updateVersion {
|
||||||
do
|
do
|
||||||
if [ "$CRATE" = "librespot" ]
|
if [ "$CRATE" = "librespot" ]
|
||||||
then
|
then
|
||||||
CRATE=''
|
CRATE_DIR=''
|
||||||
|
else
|
||||||
|
CRATE_DIR=$CRATE
|
||||||
fi
|
fi
|
||||||
crate_path="$WORKINGDIR/$CRATE/Cargo.toml"
|
crate_path="$WORKINGDIR/$CRATE_DIR/Cargo.toml"
|
||||||
crate_path=${crate_path//\/\///}
|
crate_path=${crate_path//\/\///}
|
||||||
sed -i '' "s/^version.*/version = \"$1\"/g" "$crate_path"
|
sed -i '' "s/^version.*/version = \"$1\"/g" "$crate_path"
|
||||||
echo "Path is $crate_path"
|
echo "Path is $crate_path"
|
||||||
if [ "$CRATE" = "librespot" ]
|
if [ "$CRATE" = "librespot" ]
|
||||||
then
|
then
|
||||||
|
echo "Updating lockfile"
|
||||||
if [ "$DRY_RUN" = 'true' ] ; then
|
if [ "$DRY_RUN" = 'true' ] ; then
|
||||||
cargo update --dry-run
|
cargo update --dry-run
|
||||||
git add . && git commit --dry-run -a -m "Update Cargo.lock"
|
git add . && git commit --dry-run -a -m "Update Cargo.lock"
|
||||||
|
|
39
src/main.rs
39
src/main.rs
|
@ -668,7 +668,15 @@ fn get_setup() -> Setup {
|
||||||
trace!("Command line argument(s):");
|
trace!("Command line argument(s):");
|
||||||
|
|
||||||
for (index, key) in args.iter().enumerate() {
|
for (index, key) in args.iter().enumerate() {
|
||||||
let opt = key.trim_start_matches('-');
|
let opt = {
|
||||||
|
let key = key.trim_start_matches('-');
|
||||||
|
|
||||||
|
if let Some((s, _)) = key.split_once('=') {
|
||||||
|
s
|
||||||
|
} else {
|
||||||
|
key
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if index > 0
|
if index > 0
|
||||||
&& key.starts_with('-')
|
&& key.starts_with('-')
|
||||||
|
@ -678,13 +686,13 @@ fn get_setup() -> Setup {
|
||||||
{
|
{
|
||||||
if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) {
|
if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) {
|
||||||
// Don't log creds.
|
// Don't log creds.
|
||||||
trace!("\t\t{} \"XXXXXXXX\"", key);
|
trace!("\t\t{} \"XXXXXXXX\"", opt);
|
||||||
} else {
|
} else {
|
||||||
let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string());
|
let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string());
|
||||||
if value.is_empty() {
|
if value.is_empty() {
|
||||||
trace!("\t\t{}", key);
|
trace!("\t\t{}", opt);
|
||||||
} else {
|
} else {
|
||||||
trace!("\t\t{} \"{}\"", key, value);
|
trace!("\t\t{} \"{}\"", opt, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1072,7 +1080,7 @@ fn get_setup() -> Setup {
|
||||||
Some(creds) if username == creds.username => Some(creds),
|
Some(creds) if username == creds.username => Some(creds),
|
||||||
_ => {
|
_ => {
|
||||||
let prompt = &format!("Password for {}: ", username);
|
let prompt = &format!("Password for {}: ", username);
|
||||||
match rpassword::prompt_password_stderr(prompt) {
|
match rpassword::prompt_password(prompt) {
|
||||||
Ok(password) => {
|
Ok(password) => {
|
||||||
if !password.is_empty() {
|
if !password.is_empty() {
|
||||||
Some(Credentials::with_password(username, password))
|
Some(Credentials::with_password(username, password))
|
||||||
|
@ -1599,24 +1607,25 @@ async fn main() {
|
||||||
|
|
||||||
if setup.enable_discovery {
|
if setup.enable_discovery {
|
||||||
let device_id = setup.session_config.device_id.clone();
|
let device_id = setup.session_config.device_id.clone();
|
||||||
|
match librespot::discovery::Discovery::builder(device_id)
|
||||||
discovery = match librespot::discovery::Discovery::builder(device_id)
|
|
||||||
.name(setup.connect_config.name.clone())
|
.name(setup.connect_config.name.clone())
|
||||||
.device_type(setup.connect_config.device_type)
|
.device_type(setup.connect_config.device_type)
|
||||||
.port(setup.zeroconf_port)
|
.port(setup.zeroconf_port)
|
||||||
.launch()
|
.launch()
|
||||||
{
|
{
|
||||||
Ok(d) => Some(d),
|
Ok(d) => discovery = Some(d),
|
||||||
Err(e) => {
|
Err(err) => warn!("Could not initialise discovery: {}.", err),
|
||||||
error!("Discovery Error: {}", e);
|
};
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(credentials) = setup.credentials {
|
if let Some(credentials) = setup.credentials {
|
||||||
last_credentials = Some(credentials);
|
last_credentials = Some(credentials);
|
||||||
connecting = true;
|
connecting = true;
|
||||||
|
} else if discovery.is_none() {
|
||||||
|
error!(
|
||||||
|
"Discovery is unavailable and no credentials provided. Authentication is not possible."
|
||||||
|
);
|
||||||
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
@ -1656,12 +1665,12 @@ async fn main() {
|
||||||
let player_config = setup.player_config.clone();
|
let player_config = setup.player_config.clone();
|
||||||
let connect_config = setup.connect_config.clone();
|
let connect_config = setup.connect_config.clone();
|
||||||
|
|
||||||
let audio_filter = mixer.get_audio_filter();
|
let soft_volume = mixer.get_soft_volume();
|
||||||
let format = setup.format;
|
let format = setup.format;
|
||||||
let backend = setup.backend;
|
let backend = setup.backend;
|
||||||
let device = setup.device.clone();
|
let device = setup.device.clone();
|
||||||
let (player, event_channel) =
|
let (player, event_channel) =
|
||||||
Player::new(player_config, session.clone(), audio_filter, move || {
|
Player::new(player_config, session.clone(), soft_volume, move || {
|
||||||
(backend)(device, format)
|
(backend)(device, format)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -17,17 +17,17 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
old_track_id,
|
old_track_id,
|
||||||
new_track_id,
|
new_track_id,
|
||||||
} => match old_track_id.to_base62() {
|
} => match old_track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Changed: Invalid old track id",
|
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(old_id) => match new_track_id.to_base62() {
|
Ok(old_id) => match new_track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Changed: Invalid new track id",
|
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(new_id) => {
|
Ok(new_id) => {
|
||||||
|
@ -38,10 +38,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PlayerEvent::Started { track_id, .. } => match track_id.to_base62() {
|
PlayerEvent::Started { track_id, .. } => match track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Started: Invalid track id",
|
format!("PlayerEvent::Started: Invalid track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
@ -50,10 +50,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() {
|
PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Stopped: Invalid track id",
|
format!("PlayerEvent::Stopped: Invalid track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
@ -67,10 +67,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Playing: Invalid track id",
|
format!("PlayerEvent::Playing: Invalid track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
@ -86,10 +86,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
position_ms,
|
position_ms,
|
||||||
..
|
..
|
||||||
} => match track_id.to_base62() {
|
} => match track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Paused: Invalid track id",
|
format!("PlayerEvent::Paused: Invalid track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
@ -100,10 +100,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
|
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
|
||||||
Err(_) => {
|
Err(e) => {
|
||||||
return Some(Err(Error::new(
|
return Some(Err(Error::new(
|
||||||
ErrorKind::InvalidData,
|
ErrorKind::InvalidData,
|
||||||
"PlayerEvent::Preloading: Invalid track id",
|
format!("PlayerEvent::Preloading: Invalid track id: {}", e),
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
Ok(id) => {
|
Ok(id) => {
|
||||||
|
|
Loading…
Reference in a new issue