mirror of
https://github.com/librespot-org/librespot.git
synced 2025-10-03 01:39:28 +02:00
OAuth process made by a struct, allowing customization options. (#1462)
* get refresh token. Optional auth url browser opening * changelog * access token accepts custom message * docs updated * CustomParams renamed * OAuthToken can be cloned * builder pattern on token management * changelog * docs and format issues * split methods and finish documentation * new example and minor adjustments * typo * remove unnecessary dependency * requested changes * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> * Update CHANGELOG.md Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> * Update oauth/src/lib.rs Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> * Update oauth/src/lib.rs Co-authored-by: Nick Steel <nick@nsteel.co.uk> * Update oauth/src/lib.rs Co-authored-by: Nick Steel <nick@nsteel.co.uk> * remove veil. Oauth flow fix * debug trait instead of veil * Update main.rs Co-authored-by: Nick Steel <nick@nsteel.co.uk> --------- Co-authored-by: Felix Prillwitz <photovoltex@mailbox.org> Co-authored-by: Nick Steel <nick@nsteel.co.uk>
This commit is contained in:
parent
581c8d61ea
commit
f497806fb1
8 changed files with 471 additions and 53 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -23,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking)
|
- [connect] Add `pause` parameter to `Spirc::disconnect` method (breaking)
|
||||||
- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking)
|
- [playback] Add `track` field to `PlayerEvent::RepeatChanged` (breaking)
|
||||||
- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient`
|
- [core] Add `request_with_options` and `request_with_protobuf_and_options` to `SpClient`
|
||||||
|
- [oauth] Add `OAuthClient` and `OAuthClientBuilder` structs to achieve a more customizable login process
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
@ -39,6 +40,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
- [connect] Handle transfer of playback with empty "uri" field
|
- [connect] Handle transfer of playback with empty "uri" field
|
||||||
- [connect] Correctly apply playing/paused state when transferring playback
|
- [connect] Correctly apply playing/paused state when transferring playback
|
||||||
|
|
||||||
|
### Deprecated
|
||||||
|
|
||||||
|
- [oauth] `get_access_token()` function marked for deprecation
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- [core] Removed `get_canvases` from SpClient (breaking)
|
- [core] Removed `get_canvases` from SpClient (breaking)
|
||||||
|
@ -76,7 +81,7 @@ backend for Spotify Connect discovery.
|
||||||
## [0.5.0] - 2024-10-15
|
## [0.5.0] - 2024-10-15
|
||||||
|
|
||||||
This version is be a major departure from the architecture up until now. It
|
This version is be a major departure from the architecture up until now. It
|
||||||
focuses on implementing the "new Spotify API". This means moving large parts
|
focuses on implementing the "new Spotify API". This means moving large parts
|
||||||
of the Spotify protocol from Mercury to HTTP. A lot of this was reverse
|
of the Spotify protocol from Mercury to HTTP. A lot of this was reverse
|
||||||
engineered before by @devgianlu of librespot-java. It was long overdue that we
|
engineered before by @devgianlu of librespot-java. It was long overdue that we
|
||||||
started implementing it too, not in the least because new features like the
|
started implementing it too, not in the least because new features like the
|
||||||
|
@ -219,14 +224,17 @@ to offer. But, unless anything big comes up, it is also intended as the last
|
||||||
release to be based on the old API. Happy listening.
|
release to be based on the old API. Happy listening.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [playback] `pipe`: Better error handling
|
- [playback] `pipe`: Better error handling
|
||||||
- [playback] `subprocess`: Better error handling
|
- [playback] `subprocess`: Better error handling
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors
|
- [core] `apresolve`: Blacklist ap-gew4 and ap-gue1 access points that cause channel errors
|
||||||
- [playback] `pipe`: Implement stop
|
- [playback] `pipe`: Implement stop
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- [main] fix `--opt=value` line argument logging
|
- [main] fix `--opt=value` line argument logging
|
||||||
- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa`
|
- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa`
|
||||||
|
|
||||||
|
@ -235,9 +243,11 @@ release to be based on the old API. Happy listening.
|
||||||
This release fixes dependency issues when installing from crates.
|
This release fixes dependency issues when installing from crates.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [chore] The MSRV is now 1.56
|
- [chore] The MSRV is now 1.56
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- [playback] Fixed dependency issues when installing from crate
|
- [playback] Fixed dependency issues when installing from crate
|
||||||
|
|
||||||
## [0.4.0] - 2022-05-21
|
## [0.4.0] - 2022-05-21
|
||||||
|
@ -253,6 +263,7 @@ Targeting that major effort for a v0.5 release sometime, we intend to maintain
|
||||||
v0.4.x as a stable branch until then.
|
v0.4.x as a stable branch until then.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [chore] The MSRV is now 1.53
|
- [chore] The MSRV is now 1.53
|
||||||
- [contrib] Hardened security of the `systemd` service units
|
- [contrib] Hardened security of the `systemd` service units
|
||||||
- [core] `Session`: `connect()` now returns the long-term credentials
|
- [core] `Session`: `connect()` now returns the long-term credentials
|
||||||
|
@ -265,6 +276,7 @@ v0.4.x as a stable branch until then.
|
||||||
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking)
|
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [main] Enforce reasonable ranges for option values (breaking)
|
- [main] Enforce reasonable ranges for option values (breaking)
|
||||||
- [main] Add the ability to parse environment variables
|
- [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] Log now emits warning when trying to use options that would otherwise have no effect
|
||||||
|
@ -277,6 +289,7 @@ v0.4.x as a stable branch until then.
|
||||||
- [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)
|
- [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
|
||||||
|
|
||||||
- [connect] Don't panic when activating shuffle without previous interaction
|
- [connect] Don't panic when activating shuffle without previous interaction
|
||||||
- [core] Removed unsafe code (breaking)
|
- [core] Removed unsafe code (breaking)
|
||||||
- [main] Fix crash when built with Avahi support but Avahi is locally unavailable
|
- [main] Fix crash when built with Avahi support but Avahi is locally unavailable
|
||||||
|
@ -287,20 +300,24 @@ v0.4.x as a stable branch until then.
|
||||||
- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls
|
- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been 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
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Include build profile in the displayed version information
|
- Include build profile in the displayed version information
|
||||||
- [playback] Improve dithering CPU usage by about 33%
|
- [playback] Improve dithering CPU usage by about 33%
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- [connect] Partly fix behavior after last track of an album/playlist
|
- [connect] Partly fix behavior after last track of an album/playlist
|
||||||
|
|
||||||
## [0.3.0] - 2021-10-13
|
## [0.3.0] - 2021-10-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
|
- [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`.
|
||||||
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking)
|
- [playback] Add support for dithering with `--dither` for lower requantization error (breaking)
|
||||||
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
|
- [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves
|
||||||
|
@ -309,6 +326,7 @@ v0.4.x as a stable branch until then.
|
||||||
- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically
|
- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
|
- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking)
|
||||||
- [audio, playback] Use `Duration` for time constants and functions (breaking)
|
- [audio, playback] Use `Duration` for time constants and functions (breaking)
|
||||||
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
|
- [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate
|
||||||
|
@ -325,17 +343,20 @@ v0.4.x as a stable branch until then.
|
||||||
- [playback] `player`: default normalisation type is now `auto`
|
- [playback] `player`: default normalisation type is now `auto`
|
||||||
|
|
||||||
### Deprecated
|
### Deprecated
|
||||||
|
|
||||||
- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate
|
- [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate
|
||||||
- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device`
|
- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device`
|
||||||
- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control`
|
- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control`
|
||||||
- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index`
|
- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index`
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
- [connect] Removed no-op mixer started/stopped logic (breaking)
|
- [connect] Removed no-op mixer started/stopped logic (breaking)
|
||||||
- [playback] Removed `with-vorbis` and `with-tremor` features
|
- [playback] Removed `with-vorbis` and `with-tremor` features
|
||||||
- [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa
|
- [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- [connect] Fix step size on volume up/down events
|
- [connect] Fix step size on volume up/down events
|
||||||
- [connect] Fix looping back to the first track after the last track of an album or playlist
|
- [connect] Fix looping back to the first track after the last track of an album or playlist
|
||||||
- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
|
- [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream
|
||||||
|
|
38
Cargo.lock
generated
38
Cargo.lock
generated
|
@ -1729,6 +1729,25 @@ version = "2.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-docker"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "is-wsl"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5"
|
||||||
|
dependencies = [
|
||||||
|
"is-docker",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "is_terminal_polyfill"
|
name = "is_terminal_polyfill"
|
||||||
version = "1.70.1"
|
version = "1.70.1"
|
||||||
|
@ -2124,7 +2143,9 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
|
"open",
|
||||||
"thiserror 2.0.11",
|
"thiserror 2.0.11",
|
||||||
|
"tokio",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2534,6 +2555,17 @@ version = "1.20.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "open"
|
||||||
|
version = "5.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95"
|
||||||
|
dependencies = [
|
||||||
|
"is-wsl",
|
||||||
|
"libc",
|
||||||
|
"pathdiff",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
@ -2597,6 +2629,12 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathdiff"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pbkdf2"
|
name = "pbkdf2"
|
||||||
version = "0.12.2"
|
version = "0.12.2"
|
||||||
|
|
|
@ -11,8 +11,10 @@ edition = "2021"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
oauth2 = "4.4"
|
oauth2 = "4.4"
|
||||||
|
open = "5.3"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
url = "2.2"
|
url = "2.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] }
|
env_logger = { version = "0.11.2", default-features = false, features = ["color", "humantime", "auto-color"] }
|
||||||
|
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] }
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
use std::env;
|
|
||||||
|
|
||||||
use librespot_oauth::get_access_token;
|
|
||||||
|
|
||||||
const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
|
|
||||||
const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login";
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut builder = env_logger::Builder::new();
|
|
||||||
builder.parse_filters("librespot=trace");
|
|
||||||
builder.init();
|
|
||||||
|
|
||||||
let args: Vec<_> = env::args().collect();
|
|
||||||
let (client_id, redirect_uri, scopes) = if args.len() == 4 {
|
|
||||||
// You can use your own client ID, along with it's associated redirect URI.
|
|
||||||
(
|
|
||||||
args[1].as_str(),
|
|
||||||
args[2].as_str(),
|
|
||||||
args[3].split(',').collect::<Vec<&str>>(),
|
|
||||||
)
|
|
||||||
} else if args.len() == 1 {
|
|
||||||
(SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"])
|
|
||||||
} else {
|
|
||||||
eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
match get_access_token(client_id, redirect_uri, scopes) {
|
|
||||||
Ok(token) => println!("Success: {token:#?}"),
|
|
||||||
Err(e) => println!("Failed: {e}"),
|
|
||||||
};
|
|
||||||
}
|
|
65
oauth/examples/oauth_async.rs
Normal file
65
oauth/examples/oauth_async.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use librespot_oauth::OAuthClientBuilder;
|
||||||
|
|
||||||
|
const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
|
||||||
|
const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login";
|
||||||
|
|
||||||
|
const RESPONSE: &str = r#"
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Return to your app!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let mut builder = env_logger::Builder::new();
|
||||||
|
builder.parse_filters("librespot=trace");
|
||||||
|
builder.init();
|
||||||
|
|
||||||
|
let args: Vec<_> = env::args().collect();
|
||||||
|
let (client_id, redirect_uri, scopes) = if args.len() == 4 {
|
||||||
|
// You can use your own client ID, along with it's associated redirect URI.
|
||||||
|
(
|
||||||
|
args[1].as_str(),
|
||||||
|
args[2].as_str(),
|
||||||
|
args[3].split(',').collect::<Vec<&str>>(),
|
||||||
|
)
|
||||||
|
} else if args.len() == 1 {
|
||||||
|
(SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"])
|
||||||
|
} else {
|
||||||
|
eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = match OAuthClientBuilder::new(client_id, redirect_uri, scopes)
|
||||||
|
.open_in_browser()
|
||||||
|
.with_custom_message(RESPONSE)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Unable to build an OAuth client: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_token = match client.get_access_token_async().await {
|
||||||
|
Ok(token) => {
|
||||||
|
println!("OAuth Token: {token:#?}");
|
||||||
|
token.refresh_token
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Unable to get OAuth Token: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.refresh_token_async(&refresh_token).await {
|
||||||
|
Ok(token) => println!("New refreshed OAuth Token: {token:#?}"),
|
||||||
|
Err(err) => println!("Unable to get refreshed OAuth Token: {err}"),
|
||||||
|
}
|
||||||
|
}
|
64
oauth/examples/oauth_sync.rs
Normal file
64
oauth/examples/oauth_sync.rs
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
use librespot_oauth::OAuthClientBuilder;
|
||||||
|
|
||||||
|
const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
|
||||||
|
const SPOTIFY_REDIRECT_URI: &str = "http://127.0.0.1:8898/login";
|
||||||
|
|
||||||
|
const RESPONSE: &str = r#"
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Return to your app!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut builder = env_logger::Builder::new();
|
||||||
|
builder.parse_filters("librespot=trace");
|
||||||
|
builder.init();
|
||||||
|
|
||||||
|
let args: Vec<_> = env::args().collect();
|
||||||
|
let (client_id, redirect_uri, scopes) = if args.len() == 4 {
|
||||||
|
// You can use your own client ID, along with it's associated redirect URI.
|
||||||
|
(
|
||||||
|
args[1].as_str(),
|
||||||
|
args[2].as_str(),
|
||||||
|
args[3].split(',').collect::<Vec<&str>>(),
|
||||||
|
)
|
||||||
|
} else if args.len() == 1 {
|
||||||
|
(SPOTIFY_CLIENT_ID, SPOTIFY_REDIRECT_URI, vec!["streaming"])
|
||||||
|
} else {
|
||||||
|
eprintln!("Usage: {} [CLIENT_ID REDIRECT_URI SCOPES]", args[0]);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = match OAuthClientBuilder::new(client_id, redirect_uri, scopes)
|
||||||
|
.open_in_browser()
|
||||||
|
.with_custom_message(RESPONSE)
|
||||||
|
.build()
|
||||||
|
{
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Unable to build an OAuth client: {}", err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let refresh_token = match client.get_access_token() {
|
||||||
|
Ok(token) => {
|
||||||
|
println!("OAuth Token: {token:#?}");
|
||||||
|
token.refresh_token
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
println!("Unable to get OAuth Token: {err}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match client.refresh_token(&refresh_token) {
|
||||||
|
Ok(token) => println!("New refreshed OAuth Token: {token:#?}"),
|
||||||
|
Err(err) => println!("Unable to get refreshed OAuth Token: {err}"),
|
||||||
|
}
|
||||||
|
}
|
277
oauth/src/lib.rs
277
oauth/src/lib.rs
|
@ -1,3 +1,4 @@
|
||||||
|
#![warn(missing_docs)]
|
||||||
//! Provides a Spotify access token using the OAuth authorization code flow
|
//! Provides a Spotify access token using the OAuth authorization code flow
|
||||||
//! with PKCE.
|
//! with PKCE.
|
||||||
//!
|
//!
|
||||||
|
@ -11,66 +12,108 @@
|
||||||
//! is appropriate for headless systems.
|
//! is appropriate for headless systems.
|
||||||
|
|
||||||
use log::{error, info, trace};
|
use log::{error, info, trace};
|
||||||
use oauth2::reqwest::http_client;
|
use oauth2::basic::BasicTokenType;
|
||||||
|
use oauth2::reqwest::{async_http_client, http_client};
|
||||||
use oauth2::{
|
use oauth2::{
|
||||||
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge,
|
basic::BasicClient, AuthUrl, AuthorizationCode, ClientId, CsrfToken, PkceCodeChallenge,
|
||||||
RedirectUrl, Scope, TokenResponse, TokenUrl,
|
RedirectUrl, Scope, TokenResponse, TokenUrl,
|
||||||
};
|
};
|
||||||
|
use oauth2::{EmptyExtraTokenFields, PkceCodeVerifier, RefreshToken, StandardTokenResponse};
|
||||||
use std::io;
|
use std::io;
|
||||||
|
use std::sync::mpsc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
use std::{
|
use std::{
|
||||||
io::{BufRead, BufReader, Write},
|
io::{BufRead, BufReader, Write},
|
||||||
net::{SocketAddr, TcpListener},
|
net::{SocketAddr, TcpListener},
|
||||||
sync::mpsc,
|
|
||||||
};
|
};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
/// Possible errors encountered during the OAuth authentication flow.
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum OAuthError {
|
pub enum OAuthError {
|
||||||
|
/// The redirect URI cannot be parsed as a valid URL.
|
||||||
#[error("Unable to parse redirect URI {uri} ({e})")]
|
#[error("Unable to parse redirect URI {uri} ({e})")]
|
||||||
AuthCodeBadUri { uri: String, e: url::ParseError },
|
AuthCodeBadUri {
|
||||||
|
/// Auth URI.
|
||||||
|
uri: String,
|
||||||
|
/// Inner error code.
|
||||||
|
e: url::ParseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// The authorization code parameter is missing in the redirect URI.
|
||||||
#[error("Auth code param not found in URI {uri}")]
|
#[error("Auth code param not found in URI {uri}")]
|
||||||
AuthCodeNotFound { uri: String },
|
AuthCodeNotFound {
|
||||||
|
/// Auth URI.
|
||||||
|
uri: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Failed to read input from standard input when manually collecting auth code.
|
||||||
#[error("Failed to read redirect URI from stdin")]
|
#[error("Failed to read redirect URI from stdin")]
|
||||||
AuthCodeStdinRead,
|
AuthCodeStdinRead,
|
||||||
|
|
||||||
|
/// Could not bind TCP listener to the specified socket address for OAuth callback.
|
||||||
#[error("Failed to bind server to {addr} ({e})")]
|
#[error("Failed to bind server to {addr} ({e})")]
|
||||||
AuthCodeListenerBind { addr: SocketAddr, e: io::Error },
|
AuthCodeListenerBind {
|
||||||
|
/// Callback address.
|
||||||
|
addr: SocketAddr,
|
||||||
|
/// Inner error code.
|
||||||
|
e: io::Error,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Listener terminated before receiving an OAuth callback connection.
|
||||||
#[error("Listener terminated without accepting a connection")]
|
#[error("Listener terminated without accepting a connection")]
|
||||||
AuthCodeListenerTerminated,
|
AuthCodeListenerTerminated,
|
||||||
|
|
||||||
|
/// Failed to read incoming HTTP request containing OAuth callback.
|
||||||
#[error("Failed to read redirect URI from HTTP request")]
|
#[error("Failed to read redirect URI from HTTP request")]
|
||||||
AuthCodeListenerRead,
|
AuthCodeListenerRead,
|
||||||
|
|
||||||
|
/// Received malformed HTTP request for OAuth callback.
|
||||||
#[error("Failed to parse redirect URI from HTTP request")]
|
#[error("Failed to parse redirect URI from HTTP request")]
|
||||||
AuthCodeListenerParse,
|
AuthCodeListenerParse,
|
||||||
|
|
||||||
|
/// Could not send HTTP response after handling OAuth callback.
|
||||||
#[error("Failed to write HTTP response")]
|
#[error("Failed to write HTTP response")]
|
||||||
AuthCodeListenerWrite,
|
AuthCodeListenerWrite,
|
||||||
|
|
||||||
|
/// Invalid Spotify authorization endpoint URL.
|
||||||
#[error("Invalid Spotify OAuth URI")]
|
#[error("Invalid Spotify OAuth URI")]
|
||||||
InvalidSpotifyUri,
|
InvalidSpotifyUri,
|
||||||
|
|
||||||
|
/// Redirect URI failed validation.
|
||||||
#[error("Invalid Redirect URI {uri} ({e})")]
|
#[error("Invalid Redirect URI {uri} ({e})")]
|
||||||
InvalidRedirectUri { uri: String, e: url::ParseError },
|
InvalidRedirectUri {
|
||||||
|
/// Auth URI.
|
||||||
|
uri: String,
|
||||||
|
/// Inner error code
|
||||||
|
e: url::ParseError,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Channel communication failure.
|
||||||
#[error("Failed to receive code")]
|
#[error("Failed to receive code")]
|
||||||
Recv,
|
Recv,
|
||||||
|
|
||||||
|
/// Token exchange failure with Spotify's authorization server.
|
||||||
#[error("Failed to exchange code for access token ({e})")]
|
#[error("Failed to exchange code for access token ({e})")]
|
||||||
ExchangeCode { e: String },
|
ExchangeCode {
|
||||||
|
/// Inner error description
|
||||||
|
e: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// Represents an OAuth token used for accessing Spotify's Web API and sessions.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
pub struct OAuthToken {
|
pub struct OAuthToken {
|
||||||
|
/// Bearer token used for authenticated Spotify API requests
|
||||||
pub access_token: String,
|
pub access_token: String,
|
||||||
|
/// Long-lived token used to obtain new access tokens
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
|
/// Instant when the access token becomes invalid
|
||||||
pub expires_at: Instant,
|
pub expires_at: Instant,
|
||||||
|
/// Type of token
|
||||||
pub token_type: String,
|
pub token_type: String,
|
||||||
|
/// Permission scopes granted by this token
|
||||||
pub scopes: Vec<String>,
|
pub scopes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,7 +147,10 @@ fn get_authcode_stdin() -> Result<AuthorizationCode, OAuthError> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code.
|
/// Spawn HTTP server at provided socket address to accept OAuth callback and return auth code.
|
||||||
fn get_authcode_listener(socket_address: SocketAddr) -> Result<AuthorizationCode, OAuthError> {
|
fn get_authcode_listener(
|
||||||
|
socket_address: SocketAddr,
|
||||||
|
message: String,
|
||||||
|
) -> Result<AuthorizationCode, OAuthError> {
|
||||||
let listener =
|
let listener =
|
||||||
TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind {
|
TcpListener::bind(socket_address).map_err(|e| OAuthError::AuthCodeListenerBind {
|
||||||
addr: socket_address,
|
addr: socket_address,
|
||||||
|
@ -130,7 +176,6 @@ fn get_authcode_listener(socket_address: SocketAddr) -> Result<AuthorizationCode
|
||||||
.ok_or(OAuthError::AuthCodeListenerParse)?;
|
.ok_or(OAuthError::AuthCodeListenerParse)?;
|
||||||
let code = get_code(&("http://localhost".to_string() + redirect_url));
|
let code = get_code(&("http://localhost".to_string() + redirect_url));
|
||||||
|
|
||||||
let message = "Go back to your terminal :)";
|
|
||||||
let response = format!(
|
let response = format!(
|
||||||
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
|
"HTTP/1.1 200 OK\r\ncontent-length: {}\r\n\r\n{}",
|
||||||
message.len(),
|
message.len(),
|
||||||
|
@ -146,6 +191,7 @@ fn get_authcode_listener(socket_address: SocketAddr) -> Result<AuthorizationCode
|
||||||
// If the specified `redirect_uri` is HTTP, loopback, and contains a port,
|
// If the specified `redirect_uri` is HTTP, loopback, and contains a port,
|
||||||
// then the corresponding socket address is returned.
|
// then the corresponding socket address is returned.
|
||||||
fn get_socket_address(redirect_uri: &str) -> Option<SocketAddr> {
|
fn get_socket_address(redirect_uri: &str) -> Option<SocketAddr> {
|
||||||
|
#![warn(missing_docs)]
|
||||||
let url = match Url::parse(redirect_uri) {
|
let url = match Url::parse(redirect_uri) {
|
||||||
Ok(u) if u.scheme() == "http" && u.port().is_some() => u,
|
Ok(u) if u.scheme() == "http" && u.port().is_some() => u,
|
||||||
_ => return None,
|
_ => return None,
|
||||||
|
@ -162,6 +208,215 @@ fn get_socket_address(redirect_uri: &str) -> Option<SocketAddr> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Struct that handle obtaining and refreshing access tokens.
|
||||||
|
pub struct OAuthClient {
|
||||||
|
scopes: Vec<String>,
|
||||||
|
redirect_uri: String,
|
||||||
|
should_open_url: bool,
|
||||||
|
message: String,
|
||||||
|
client: BasicClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuthClient {
|
||||||
|
/// Generates and opens/shows the authorization URL to obtain an access token.
|
||||||
|
///
|
||||||
|
/// Returns a verifier that must be included in the final request for validation.
|
||||||
|
fn set_auth_url(&self) -> PkceCodeVerifier {
|
||||||
|
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||||
|
// Generate the full authorization URL.
|
||||||
|
// Some of these scopes are unavailable for custom client IDs. Which?
|
||||||
|
let request_scopes: Vec<oauth2::Scope> =
|
||||||
|
self.scopes.iter().map(|s| Scope::new(s.into())).collect();
|
||||||
|
let (auth_url, _) = self
|
||||||
|
.client
|
||||||
|
.authorize_url(CsrfToken::new_random)
|
||||||
|
.add_scopes(request_scopes)
|
||||||
|
.set_pkce_challenge(pkce_challenge)
|
||||||
|
.url();
|
||||||
|
|
||||||
|
if self.should_open_url {
|
||||||
|
open::that_in_background(auth_url.as_str());
|
||||||
|
}
|
||||||
|
println!("Browse to: {}", auth_url);
|
||||||
|
|
||||||
|
pkce_verifier
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_token(
|
||||||
|
&self,
|
||||||
|
resp: StandardTokenResponse<EmptyExtraTokenFields, BasicTokenType>,
|
||||||
|
) -> Result<OAuthToken, OAuthError> {
|
||||||
|
trace!("Obtained new access token: {resp:?}");
|
||||||
|
|
||||||
|
let token_scopes: Vec<String> = match resp.scopes() {
|
||||||
|
Some(s) => s.iter().map(|s| s.to_string()).collect(),
|
||||||
|
_ => self.scopes.clone(),
|
||||||
|
};
|
||||||
|
let refresh_token = match resp.refresh_token() {
|
||||||
|
Some(t) => t.secret().to_string(),
|
||||||
|
_ => "".to_string(), // Spotify always provides a refresh token.
|
||||||
|
};
|
||||||
|
Ok(OAuthToken {
|
||||||
|
access_token: resp.access_token().secret().to_string(),
|
||||||
|
refresh_token,
|
||||||
|
expires_at: Instant::now()
|
||||||
|
+ resp
|
||||||
|
.expires_in()
|
||||||
|
.unwrap_or_else(|| Duration::from_secs(3600)),
|
||||||
|
token_type: format!("{:?}", resp.token_type()),
|
||||||
|
scopes: token_scopes,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Syncronously obtain a Spotify access token using the authorization code with PKCE OAuth flow.
|
||||||
|
pub fn get_access_token(&self) -> Result<OAuthToken, OAuthError> {
|
||||||
|
let pkce_verifier = self.set_auth_url();
|
||||||
|
|
||||||
|
let code = match get_socket_address(&self.redirect_uri) {
|
||||||
|
Some(addr) => get_authcode_listener(addr, self.message.clone()),
|
||||||
|
_ => get_authcode_stdin(),
|
||||||
|
}?;
|
||||||
|
trace!("Exchange {code:?} for access token");
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let client = self.client.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let resp = client
|
||||||
|
.exchange_code(code)
|
||||||
|
.set_pkce_verifier(pkce_verifier)
|
||||||
|
.request(http_client);
|
||||||
|
if let Err(e) = tx.send(resp) {
|
||||||
|
error!("OAuth channel send error: {e}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let channel_response = rx.recv().map_err(|_| OAuthError::Recv)?;
|
||||||
|
let token_response =
|
||||||
|
channel_response.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
|
||||||
|
|
||||||
|
self.build_token(token_response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Synchronously obtain a new valid OAuth token from `refresh_token`
|
||||||
|
pub fn refresh_token(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> {
|
||||||
|
let refresh_token = RefreshToken::new(refresh_token.to_string());
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.exchange_refresh_token(&refresh_token)
|
||||||
|
.request(http_client);
|
||||||
|
|
||||||
|
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
|
||||||
|
self.build_token(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asyncronously obtain a Spotify access token using the authorization code with PKCE OAuth flow.
|
||||||
|
pub async fn get_access_token_async(&self) -> Result<OAuthToken, OAuthError> {
|
||||||
|
let pkce_verifier = self.set_auth_url();
|
||||||
|
|
||||||
|
let code = match get_socket_address(&self.redirect_uri) {
|
||||||
|
Some(addr) => get_authcode_listener(addr, self.message.clone()),
|
||||||
|
_ => get_authcode_stdin(),
|
||||||
|
}?;
|
||||||
|
trace!("Exchange {code:?} for access token");
|
||||||
|
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.exchange_code(code)
|
||||||
|
.set_pkce_verifier(pkce_verifier)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
|
||||||
|
self.build_token(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Asynchronously obtain a new valid OAuth token from `refresh_token`
|
||||||
|
pub async fn refresh_token_async(&self, refresh_token: &str) -> Result<OAuthToken, OAuthError> {
|
||||||
|
let refresh_token = RefreshToken::new(refresh_token.to_string());
|
||||||
|
let resp = self
|
||||||
|
.client
|
||||||
|
.exchange_refresh_token(&refresh_token)
|
||||||
|
.request_async(async_http_client)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let resp = resp.map_err(|e| OAuthError::ExchangeCode { e: e.to_string() })?;
|
||||||
|
self.build_token(resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder struct through which structures of type OAuthClient are instantiated.
|
||||||
|
pub struct OAuthClientBuilder {
|
||||||
|
client_id: String,
|
||||||
|
redirect_uri: String,
|
||||||
|
scopes: Vec<String>,
|
||||||
|
should_open_url: bool,
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OAuthClientBuilder {
|
||||||
|
/// Create a new OAuthClientBuilder with provided params and default config.
|
||||||
|
///
|
||||||
|
/// `redirect_uri` must match to the registered Uris of `client_id`
|
||||||
|
pub fn new(client_id: &str, redirect_uri: &str, scopes: Vec<&str>) -> Self {
|
||||||
|
Self {
|
||||||
|
client_id: client_id.to_string(),
|
||||||
|
redirect_uri: redirect_uri.to_string(),
|
||||||
|
scopes: scopes.into_iter().map(Into::into).collect(),
|
||||||
|
should_open_url: false,
|
||||||
|
message: String::from("Go back to your terminal :)"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When this function is added to the building process pipeline, the auth url will be
|
||||||
|
/// opened with the default web browser. Otherwise, it will be printed to standard output.
|
||||||
|
pub fn open_in_browser(mut self) -> Self {
|
||||||
|
self.should_open_url = true;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When this function is added to the building process pipeline, the body of the response to
|
||||||
|
/// the callback request will be `message`. This is useful to load custom HTMLs to that &str.
|
||||||
|
pub fn with_custom_message(mut self, message: &str) -> Self {
|
||||||
|
self.message = message.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End of the building process pipeline. If Ok, a OAuthClient instance will be returned.
|
||||||
|
pub fn build(self) -> Result<OAuthClient, OAuthError> {
|
||||||
|
let auth_url = AuthUrl::new("https://accounts.spotify.com/authorize".to_string())
|
||||||
|
.map_err(|_| OAuthError::InvalidSpotifyUri)?;
|
||||||
|
let token_url = TokenUrl::new("https://accounts.spotify.com/api/token".to_string())
|
||||||
|
.map_err(|_| OAuthError::InvalidSpotifyUri)?;
|
||||||
|
let redirect_url = RedirectUrl::new(self.redirect_uri.clone()).map_err(|e| {
|
||||||
|
OAuthError::InvalidRedirectUri {
|
||||||
|
uri: self.redirect_uri.clone(),
|
||||||
|
e,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let client = BasicClient::new(
|
||||||
|
ClientId::new(self.client_id.to_string()),
|
||||||
|
None,
|
||||||
|
auth_url,
|
||||||
|
Some(token_url),
|
||||||
|
)
|
||||||
|
.set_redirect_uri(redirect_url);
|
||||||
|
|
||||||
|
Ok(OAuthClient {
|
||||||
|
scopes: self.scopes,
|
||||||
|
should_open_url: self.should_open_url,
|
||||||
|
message: self.message,
|
||||||
|
redirect_uri: self.redirect_uri,
|
||||||
|
client,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow.
|
||||||
|
/// The `redirect_uri` must match what is registered to the client ID.
|
||||||
|
#[deprecated(
|
||||||
|
since = "0.7.0",
|
||||||
|
note = "please use builder pattern with `OAuthClientBuilder` instead"
|
||||||
|
)]
|
||||||
/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow.
|
/// Obtain a Spotify access token using the authorization code with PKCE OAuth flow.
|
||||||
/// The redirect_uri must match what is registered to the client ID.
|
/// The redirect_uri must match what is registered to the client ID.
|
||||||
pub fn get_access_token(
|
pub fn get_access_token(
|
||||||
|
@ -204,7 +459,7 @@ pub fn get_access_token(
|
||||||
println!("Browse to: {}", auth_url);
|
println!("Browse to: {}", auth_url);
|
||||||
|
|
||||||
let code = match get_socket_address(redirect_uri) {
|
let code = match get_socket_address(redirect_uri) {
|
||||||
Some(addr) => get_authcode_listener(addr),
|
Some(addr) => get_authcode_listener(addr, String::from("Go back to your terminal :)")),
|
||||||
_ => get_authcode_stdin(),
|
_ => get_authcode_stdin(),
|
||||||
}?;
|
}?;
|
||||||
trace!("Exchange {code:?} for access token");
|
trace!("Exchange {code:?} for access token");
|
||||||
|
|
23
src/main.rs
23
src/main.rs
|
@ -30,6 +30,7 @@ use librespot::{
|
||||||
player::{coefficient_to_duration, duration_to_coefficient, Player},
|
player::{coefficient_to_duration, duration_to_coefficient, Player},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use librespot_oauth::OAuthClientBuilder;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use sha1::{Digest, Sha1};
|
use sha1::{Digest, Sha1};
|
||||||
use sysinfo::{ProcessesToUpdate, System};
|
use sysinfo::{ProcessesToUpdate, System};
|
||||||
|
@ -1895,18 +1896,22 @@ async fn main() {
|
||||||
Some(port) => format!(":{port}"),
|
Some(port) => format!(":{port}"),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
};
|
};
|
||||||
let access_token = match librespot::oauth::get_access_token(
|
let client = OAuthClientBuilder::new(
|
||||||
&setup.session_config.client_id,
|
&setup.session_config.client_id,
|
||||||
&format!("http://127.0.0.1{port_str}/login"),
|
&format!("http://127.0.0.1{port_str}/login"),
|
||||||
OAUTH_SCOPES.to_vec(),
|
OAUTH_SCOPES.to_vec(),
|
||||||
) {
|
)
|
||||||
Ok(token) => token.access_token,
|
.open_in_browser()
|
||||||
Err(e) => {
|
.build()
|
||||||
error!("Failed to get Spotify access token: {e}");
|
.unwrap_or_else(|e| {
|
||||||
exit(1);
|
error!("Failed to create OAuth client: {e}");
|
||||||
}
|
exit(1);
|
||||||
};
|
});
|
||||||
last_credentials = Some(Credentials::with_access_token(access_token));
|
let oauth_token = client.get_access_token().unwrap_or_else(|e| {
|
||||||
|
error!("Failed to get Spotify access token: {e}");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
last_credentials = Some(Credentials::with_access_token(oauth_token.access_token));
|
||||||
connecting = true;
|
connecting = true;
|
||||||
} else if discovery.is_none() {
|
} else if discovery.is_none() {
|
||||||
error!(
|
error!(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue