Skip to content

Commit 68b205b

Browse files
w-lfchenjuliamertzaome510
authored
fix: switch to librespot 0.5.0 (#570)
closes #520 closes #579 closes #580 - upgrade dependencies. Main change involves the migration to `librespot v0.5.0` - migrate authentication workflow to OAuth implemented in (librespot-org/librespot#1309) ## Next step - handle Spotify Connect with user-provided `client_id` Co-authored-by: Julia Mertz <info@juliamertz.dev> Co-authored-by: Thang Pham <phamducthang1234@gmail.com>
1 parent 9894fa9 commit 68b205b

16 files changed

+2167
-1426
lines changed

Cargo.lock

+1,930-1,162
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
## Table of Contents
44

55
- [Introduction](#introduction)
6-
- [Important Notice](#important-notice)
76
- [Examples](#examples)
87
- [Installation](#installation)
98
- [Features](#features)
@@ -41,9 +40,6 @@
4140
- Support running the application as [a daemon](#daemon)
4241
- Offer a wide range of [CLI commands](#cli-commands)
4342

44-
## Important Notice
45-
spotify-player throws error "Login failed with reason: Bad credentials" when authenticating from 7/29/2024 because Spotify removed username & password authentication from API through Mercury/Hermes. Please use [librespot-auth repository](https://github.com/dspearson/librespot-auth). For more details, see [#580](https://github.com/aome510/spotify-player/issues/580)
46-
4743
## Examples
4844

4945
A demo of `spotify_player` `v0.5.0-pre-release` on [youtube](https://www.youtube.com/watch/Jbfe9GLNWbA) or on [asciicast](https://asciinema.org/a/446913):

docs/config.md

+24-23
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
- [Keymaps](#keymaps)
1616

1717
All configuration files should be placed inside the application's configuration folder (default to be `$HOME/.config/spotify-player`).
18-
18+
1919
## General
2020

2121
**The default `app.toml` can be found in the example [`app.toml`](../examples/app.toml) file.**
@@ -25,10 +25,10 @@ All configuration files should be placed inside the application's configuration
2525
| Option | Description | Default |
2626
| --------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------- |
2727
| `client_id` | the Spotify client's ID | `65b708073fc0480ea92a077233ca87bd` |
28-
| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` |
28+
| `client_id_command` | a shell command that prints the Spotify client ID to stdout (overrides `client_id`) | `None` |
2929
| `client_port` | the port that the application's client is running on to handle CLI commands | `8080` |
3030
| `tracks_playback_limit` | the limit for the number of tracks played in a **tracks** playback | `50` |
31-
| `playback_format` | the format of the text in the playback's window | `{status} {track} • {artists}\n{album}\n{metadata}` |
31+
| `playback_format` | the format of the text in the playback's window | `{status} {track} • {artists}\n{album}\n{metadata}` |
3232
| `notify_format` | the format of a notification (`notify` feature only) | `{ summary = "{track} • {artists}", body = "{album}" }` |
3333
| `notify_timeout_in_secs` | the timeout (in seconds) of a notification (`notify` feature only) | `0` (no timeout) |
3434
| `player_event_hook_command` | the hook command executed when there is a new player event | `None` |
@@ -44,9 +44,9 @@ All configuration files should be placed inside the application's configuration
4444
| `enable_cover_image_cache` | store album's cover images in the cache folder | `true` |
4545
| `notify_streaming_only` | only send notification when streaming is enabled (`streaming` and `notify` feature only) | `false` |
4646
| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` |
47-
| `play_icon` | the icon to indicate playing state of a Spotify item | `` |
47+
| `play_icon` | the icon to indicate playing state of a Spotify item | `` |
4848
| `pause_icon` | the icon to indicate pause state of a Spotify item | `▌▌` |
49-
| `liked_icon` | the icon to indicate the liked state of a song | `` |
49+
| `liked_icon` | the icon to indicate the liked state of a song | `` |
5050
| `border_type` | the type of the application's borders | `Plain` |
5151
| `progress_bar_type` | the type of the playback progress bar | `Rectangle` |
5252
| `cover_img_width` | the width of the cover image (`image` feature only) | `5` |
@@ -93,17 +93,17 @@ If specified, `player_event_hook_command` should be an object with two fields `c
9393

9494
A player event is represented as a list of arguments with either of the following values:
9595

96-
- `"Changed" OLD_TRACK_ID NEW_TRACK_ID`
97-
- `"Playing" TRACK_ID POSITION_MS DURATION_MS`
98-
- `"Paused" TRACK_ID POSITION_MS DURATION_MS`
96+
- `"Changed" NEW_TRACK_ID`
97+
- `"Playing" TRACK_ID POSITION_MS`
98+
- `"Paused" TRACK_ID POSITION_MS`
9999
- `"EndOfTrack" TRACK_ID`
100100

101101
**Note**: if `args` is specified, such arguments will be called before the event's arguments.
102102

103-
For example, if `player_event_hook_command = { command = "a.sh", args = ["-b", "c", "-d"] }`, upon receiving a `Changed` event with `OLD_TRACK_ID=x`, `NEW_TRACK_ID=y`, the following command will be run
103+
For example, if `player_event_hook_command = { command = "a.sh", args = ["-b", "c", "-d"] }`, upon receiving a `Changed` event with `NEW_TRACK_ID=id`, the following command will be run
104104

105105
```shell
106-
a.sh -b c -d Changed x y
106+
a.sh -b c -d Changed id
107107
```
108108

109109
Example script that reads event's data from arguments and prints them to a file:
@@ -114,9 +114,9 @@ Example script that reads event's data from arguments and prints them to a file:
114114
set -euo pipefail
115115

116116
case "$1" in
117-
"Changed") echo "command: $1, old_track_id: $2, new_track_id: $3" >> /tmp/log.txt ;;
118-
"Playing") echo "command: $1, track_id: $2, position_ms: $3, duration_ms: $4" >> /tmp/log.txt ;;
119-
"Paused") echo "command: $1, track_id: $2, position_ms: $3, duration_ms: $4" >> /tmp/log.txt ;;
117+
"Changed") echo "command: $1, new_track_id: $2" >> /tmp/log.txt ;;
118+
"Playing") echo "command: $1, track_id: $2, position_ms: $3" >> /tmp/log.txt ;;
119+
"Paused") echo "command: $1, track_id: $2, position_ms: $3" >> /tmp/log.txt ;;
120120
"EndOfTrack") echo "command: $1, track_id: $2" >> /tmp/log.txt ;;
121121
esac
122122
```
@@ -139,24 +139,25 @@ More details on the above configuration options can be found under the [Librespo
139139

140140
### Layout configurations
141141

142-
The layout of the application can be adjusted via these options.
142+
The layout of the application can be adjusted via these options.
143143

144-
| Option | Description | Default |
145-
| -------------------------- | ---------------------------------------------------------------- | ------- |
146-
| `library.album_percent` | The percentage of the album window in the library | `40` |
147-
| `library.playlist_percent` | The percentage of the playlist window in the library | `40` |
148-
| `playback_window_position` | The position of the playback window | `Top` |
149-
| `playback_window_height` | The height of the playback window | `6` |
144+
| Option | Description | Default |
145+
| -------------------------- | ---------------------------------------------------- | ------- |
146+
| `library.album_percent` | The percentage of the album window in the library | `40` |
147+
| `library.playlist_percent` | The percentage of the playlist window in the library | `40` |
148+
| `playback_window_position` | The position of the playback window | `Top` |
149+
| `playback_window_height` | The height of the playback window | `6` |
150150

151-
Example:
151+
Example:
152152

153-
``` toml
153+
```toml
154154

155155
[layout]
156156
library = { album_percent = 40, playlist_percent = 40 }
157157
playback_window_position = "Top"
158158

159159
```
160+
160161
## Themes
161162

162163
`spotify_player` uses the `theme.toml` config file to look for user-defined themes.
@@ -286,7 +287,7 @@ key_sequence = "q"
286287

287288
## Actions
288289

289-
Actions are located in the same `keymap.toml` file as keymaps. An action can be triggered by a key sequence that is not bound to any command. Once the mapped key sequence is pressed, the corresponding action will be triggered. By default actions will act upon the currently selected item, you can change this behaviour by setting the `target` field for a keymap to either `PlayingTrack` or `SelectedItem`.
290+
Actions are located in the same `keymap.toml` file as keymaps. An action can be triggered by a key sequence that is not bound to any command. Once the mapped key sequence is pressed, the corresponding action will be triggered. By default actions will act upon the currently selected item, you can change this behaviour by setting the `target` field for a keymap to either `PlayingTrack` or `SelectedItem`.
290291
a list of actions can be found [here](../README.md#actions).
291292

292293
For example,

spotify_player/Cargo.toml

+21-13
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,40 @@ readme = "../README.md"
1313
anyhow = "1.0.86"
1414
clap = { version = "4.5.8", features = ["derive", "string"] }
1515
config_parser2 = "0.1.5"
16-
crossterm = "0.27.0"
16+
crossterm = "0.28.1"
1717
dirs-next = "2.0.0"
18-
librespot-connect = { version = "0.4.2", optional = true }
19-
librespot-playback = { version = "0.4.2", optional = true }
20-
librespot-core = "0.4.2"
18+
librespot-connect = { version = "0.5.0", optional = true }
19+
librespot-core = "0.5.0"
20+
librespot-oauth = "0.5.0"
21+
librespot-playback = { version = "0.5.0", optional = true }
2122
log = "0.4.22"
2223
chrono = "0.4.38"
2324
reqwest = { version = "0.12.5", features = ["json"] }
2425
rpassword = "7.3.1"
25-
rspotify = "0.13.2"
26+
rspotify = "0.13.3"
2627
serde = { version = "1.0.204", features = ["derive"] }
27-
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
28+
tokio = { version = "1.38.0", features = [
29+
"rt",
30+
"rt-multi-thread",
31+
"macros",
32+
"time",
33+
] }
2834
toml = "0.8.14"
29-
tui = { package = "ratatui", version = "0.27.0" }
35+
tui = { package = "ratatui", version = "0.29.0" }
3036
rand = "0.8.5"
3137
maybe-async = "0.2.10"
3238
async-trait = "0.1.81"
3339
parking_lot = "0.12.3"
3440
tracing = "0.1.40"
3541
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
36-
lyric_finder = { version = "0.1.6", path = "../lyric_finder" , optional = true }
42+
lyric_finder = { version = "0.1.6", path = "../lyric_finder", optional = true }
3743
backtrace = "0.3.73"
3844
souvlaki = { version = "0.7.3", optional = true }
39-
viuer = { version = "0.7.1", optional = true }
40-
image = { version = "0.24.9", optional = true }
41-
notify-rust = { version = "4.11.0", optional = true, default-features = false, features = ["d"] }
45+
viuer = { version = "0.9.1", optional = true }
46+
image = { version = "0.25.4", optional = true }
47+
notify-rust = { version = "4.11.0", optional = true, default-features = false, features = [
48+
"d",
49+
] }
4250
flume = "0.11.0"
4351
serde_json = "1.0.120"
4452
once_cell = "1.19.0"
@@ -49,6 +57,7 @@ clap_complete = "4.5.7"
4957
which = "6.0.1"
5058
fuzzy-matcher = { version = "0.3.7", optional = true }
5159
html-escape = "0.2.13"
60+
rustls = "0.23.14"
5261

5362
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit]
5463
version = "0.30.3"
@@ -63,7 +72,7 @@ features = [
6372
"Win32_Foundation",
6473
"Win32_Graphics_Gdi",
6574
"Win32_System_LibraryLoader",
66-
"Win32_UI_WindowsAndMessaging"
75+
"Win32_UI_WindowsAndMessaging",
6776
]
6877
optional = true
6978

@@ -89,4 +98,3 @@ default = ["rodio-backend", "media-control"]
8998

9099
[package.metadata.binstall]
91100
pkg-url = "{ repo }/releases/download/v{ version }/{ name }_{ target }{ archive-suffix }"
92-

spotify_player/src/auth.rs

+49-90
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,37 @@
1-
use std::io::Write;
2-
3-
use anyhow::{anyhow, Result};
4-
use librespot_core::{
5-
authentication::Credentials,
6-
cache::Cache,
7-
config::SessionConfig,
8-
session::{Session, SessionError},
9-
};
1+
use anyhow::Result;
2+
use librespot_core::{authentication::Credentials, cache::Cache, config::SessionConfig, Session};
3+
use librespot_oauth::get_access_token;
104

115
use crate::config;
126

7+
pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
8+
pub const CLIENT_REDIRECT_URI: &str = "http://127.0.0.1:8989/login";
9+
pub const OAUTH_SCOPES: &[&str] = &[
10+
"playlist-modify",
11+
"playlist-modify-private",
12+
"playlist-modify-public",
13+
"playlist-read",
14+
"playlist-read-collaborative",
15+
"playlist-read-private",
16+
"streaming",
17+
"user-follow-modify",
18+
"user-follow-read",
19+
"user-library-modify",
20+
"user-library-read",
21+
"user-modify",
22+
"user-modify-playback-state",
23+
"user-modify-private",
24+
"user-personalized",
25+
"user-read-currently-playing",
26+
"user-read-email",
27+
"user-read-play-history",
28+
"user-read-playback-position",
29+
"user-read-playback-state",
30+
"user-read-private",
31+
"user-read-recently-played",
32+
"user-top-read",
33+
];
34+
1335
#[derive(Clone)]
1436
pub struct AuthConfig {
1537
pub cache: Cache,
@@ -26,6 +48,11 @@ impl Default for AuthConfig {
2648
}
2749

2850
impl AuthConfig {
51+
/// Create a `librespot::Session` from authentication configs
52+
pub fn session(&self) -> Session {
53+
Session::new(self.session_config.clone(), Some(self.cache.clone()))
54+
}
55+
2956
pub fn new(configs: &config::Configs) -> Result<AuthConfig> {
3057
let audio_cache_folder = if configs.app_config.device.audio_cache {
3158
Some(configs.cache_folder.join("audio"))
@@ -42,99 +69,31 @@ impl AuthConfig {
4269

4370
Ok(AuthConfig {
4471
cache,
45-
session_config: configs.app_config.session_config(),
72+
session_config: configs.app_config.session_config()?,
4673
})
4774
}
4875
}
4976

50-
fn read_user_auth_details(user: Option<String>) -> Result<(String, String)> {
51-
let mut username = String::new();
52-
let mut stdout = std::io::stdout();
53-
match user {
54-
None => write!(stdout, "Username: ")?,
55-
Some(ref u) => write!(stdout, "Username (default: {u}): ")?,
56-
}
57-
stdout.flush()?;
58-
std::io::stdin().read_line(&mut username)?;
59-
username = username.trim_end().to_string();
60-
if username.is_empty() {
61-
username = user.unwrap_or_default();
62-
}
63-
let password = rpassword::prompt_password(format!("Password for {username}: "))?;
64-
Ok((username, password))
65-
}
66-
67-
pub async fn new_session_with_new_creds(auth_config: &AuthConfig) -> Result<Session> {
68-
tracing::info!("Creating a new session with new authentication credentials");
69-
70-
let mut user: Option<String> = None;
71-
72-
for i in 0..3 {
73-
let (username, password) = read_user_auth_details(user)?;
74-
user = Some(username.clone());
75-
match Session::connect(
76-
auth_config.session_config.clone(),
77-
Credentials::with_password(username, password),
78-
Some(auth_config.cache.clone()),
79-
true,
80-
)
81-
.await
82-
{
83-
Ok((session, _)) => {
84-
println!("Successfully authenticated as {}", user.unwrap_or_default());
85-
return Ok(session);
86-
}
87-
Err(err) => {
88-
eprintln!("Failed to authenticate, {} tries left", 2 - i);
89-
tracing::warn!("Failed to authenticate: {err:#}")
90-
}
91-
}
92-
}
93-
94-
Err(anyhow!("authentication failed!"))
95-
}
96-
97-
/// Creates a new Librespot session
98-
///
99-
/// By default, the function will look for cached credentials in the `APP_CACHE_FOLDER` folder.
100-
///
101-
/// If `reauth` is true, re-authenticate by asking the user for Spotify's username and password.
102-
/// The re-authentication process should only happen on the terminal using stdin/stdout.
103-
pub async fn new_session(auth_config: &AuthConfig, reauth: bool) -> Result<Session> {
104-
match auth_config.cache.credentials() {
77+
/// Get Spotify credentials to authenticate the application
78+
pub async fn get_creds(auth_config: &AuthConfig, reauth: bool) -> Result<Credentials> {
79+
Ok(match auth_config.cache.credentials() {
10580
None => {
10681
let msg = "No cached credentials found, please authenticate the application first.";
10782
if reauth {
10883
eprintln!("{msg}");
109-
new_session_with_new_creds(auth_config).await
84+
get_access_token(
85+
SPOTIFY_CLIENT_ID,
86+
CLIENT_REDIRECT_URI,
87+
OAUTH_SCOPES.to_vec(),
88+
)
89+
.map(|t| Credentials::with_access_token(t.access_token))?
11090
} else {
11191
anyhow::bail!(msg);
11292
}
11393
}
11494
Some(creds) => {
115-
match Session::connect(
116-
auth_config.session_config.clone(),
117-
creds,
118-
Some(auth_config.cache.clone()),
119-
true,
120-
)
121-
.await
122-
{
123-
Ok((session, _)) => {
124-
tracing::info!(
125-
"Successfully used the cached credentials to create a new session!"
126-
);
127-
Ok(session)
128-
}
129-
Err(err) => match err {
130-
SessionError::AuthenticationError(err) => {
131-
anyhow::bail!("Failed to authenticate using cached credentials: {err:#}");
132-
}
133-
SessionError::IoError(err) => {
134-
anyhow::bail!("{err:#}\nPlease check your internet connection.");
135-
}
136-
},
137-
}
95+
tracing::info!("Using cached credentials");
96+
creds
13897
}
139-
}
98+
})
14099
}

0 commit comments

Comments
 (0)