Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: switch to librespot 0.5.0 #570

Merged
merged 20 commits into from
Oct 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3,092 changes: 1,930 additions & 1,162 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
## Table of Contents

- [Introduction](#introduction)
- [Important Notice](#important-notice)
- [Examples](#examples)
- [Installation](#installation)
- [Features](#features)
Expand Down Expand Up @@ -41,9 +40,6 @@
- Support running the application as [a daemon](#daemon)
- Offer a wide range of [CLI commands](#cli-commands)

## Important Notice
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)

## Examples

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):
Expand Down
47 changes: 24 additions & 23 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [Keymaps](#keymaps)

All configuration files should be placed inside the application's configuration folder (default to be `$HOME/.config/spotify-player`).

## General

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

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

- `"Changed" OLD_TRACK_ID NEW_TRACK_ID`
- `"Playing" TRACK_ID POSITION_MS DURATION_MS`
- `"Paused" TRACK_ID POSITION_MS DURATION_MS`
- `"Changed" NEW_TRACK_ID`
- `"Playing" TRACK_ID POSITION_MS`
- `"Paused" TRACK_ID POSITION_MS`
- `"EndOfTrack" TRACK_ID`

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

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
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

```shell
a.sh -b c -d Changed x y
a.sh -b c -d Changed id
```

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

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

### Layout configurations

The layout of the application can be adjusted via these options.
The layout of the application can be adjusted via these options.

| Option | Description | Default |
| -------------------------- | ---------------------------------------------------------------- | ------- |
| `library.album_percent` | The percentage of the album window in the library | `40` |
| `library.playlist_percent` | The percentage of the playlist window in the library | `40` |
| `playback_window_position` | The position of the playback window | `Top` |
| `playback_window_height` | The height of the playback window | `6` |
| Option | Description | Default |
| -------------------------- | ---------------------------------------------------- | ------- |
| `library.album_percent` | The percentage of the album window in the library | `40` |
| `library.playlist_percent` | The percentage of the playlist window in the library | `40` |
| `playback_window_position` | The position of the playback window | `Top` |
| `playback_window_height` | The height of the playback window | `6` |

Example:
Example:

``` toml
```toml

[layout]
library = { album_percent = 40, playlist_percent = 40 }
playback_window_position = "Top"

```

## Themes

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

## Actions

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`.
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`.
a list of actions can be found [here](../README.md#actions).

For example,
Expand Down
34 changes: 21 additions & 13 deletions spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,40 @@ readme = "../README.md"
anyhow = "1.0.86"
clap = { version = "4.5.8", features = ["derive", "string"] }
config_parser2 = "0.1.5"
crossterm = "0.27.0"
crossterm = "0.28.1"
dirs-next = "2.0.0"
librespot-connect = { version = "0.4.2", optional = true }
librespot-playback = { version = "0.4.2", optional = true }
librespot-core = "0.4.2"
librespot-connect = { version = "0.5.0", optional = true }
librespot-core = "0.5.0"
librespot-oauth = "0.5.0"
librespot-playback = { version = "0.5.0", optional = true }
log = "0.4.22"
chrono = "0.4.38"
reqwest = { version = "0.12.5", features = ["json"] }
rpassword = "7.3.1"
rspotify = "0.13.2"
rspotify = "0.13.3"
serde = { version = "1.0.204", features = ["derive"] }
tokio = { version = "1.38.0", features = ["rt", "rt-multi-thread", "macros", "time"] }
tokio = { version = "1.38.0", features = [
"rt",
"rt-multi-thread",
"macros",
"time",
] }
toml = "0.8.14"
tui = { package = "ratatui", version = "0.27.0" }
tui = { package = "ratatui", version = "0.29.0" }
rand = "0.8.5"
maybe-async = "0.2.10"
async-trait = "0.1.81"
parking_lot = "0.12.3"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
lyric_finder = { version = "0.1.6", path = "../lyric_finder" , optional = true }
lyric_finder = { version = "0.1.6", path = "../lyric_finder", optional = true }
backtrace = "0.3.73"
souvlaki = { version = "0.7.3", optional = true }
viuer = { version = "0.7.1", optional = true }
image = { version = "0.24.9", optional = true }
notify-rust = { version = "4.11.0", optional = true, default-features = false, features = ["d"] }
viuer = { version = "0.9.1", optional = true }
image = { version = "0.25.4", optional = true }
notify-rust = { version = "4.11.0", optional = true, default-features = false, features = [
"d",
] }
flume = "0.11.0"
serde_json = "1.0.120"
once_cell = "1.19.0"
Expand All @@ -49,6 +57,7 @@ clap_complete = "4.5.7"
which = "6.0.1"
fuzzy-matcher = { version = "0.3.7", optional = true }
html-escape = "0.2.13"
rustls = "0.23.14"

[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies.winit]
version = "0.30.3"
Expand All @@ -63,7 +72,7 @@ features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging"
"Win32_UI_WindowsAndMessaging",
]
optional = true

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

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

139 changes: 49 additions & 90 deletions spotify_player/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
use std::io::Write;

use anyhow::{anyhow, Result};
use librespot_core::{
authentication::Credentials,
cache::Cache,
config::SessionConfig,
session::{Session, SessionError},
};
use anyhow::Result;
use librespot_core::{authentication::Credentials, cache::Cache, config::SessionConfig, Session};
use librespot_oauth::get_access_token;

use crate::config;

pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
pub const CLIENT_REDIRECT_URI: &str = "http://127.0.0.1:8989/login";
pub const OAUTH_SCOPES: &[&str] = &[
"playlist-modify",
"playlist-modify-private",
"playlist-modify-public",
"playlist-read",
"playlist-read-collaborative",
"playlist-read-private",
"streaming",
"user-follow-modify",
"user-follow-read",
"user-library-modify",
"user-library-read",
"user-modify",
"user-modify-playback-state",
"user-modify-private",
"user-personalized",
"user-read-currently-playing",
"user-read-email",
"user-read-play-history",
"user-read-playback-position",
"user-read-playback-state",
"user-read-private",
"user-read-recently-played",
"user-top-read",
];

#[derive(Clone)]
pub struct AuthConfig {
pub cache: Cache,
Expand All @@ -26,6 +48,11 @@ impl Default for AuthConfig {
}

impl AuthConfig {
/// Create a `librespot::Session` from authentication configs
pub fn session(&self) -> Session {
Session::new(self.session_config.clone(), Some(self.cache.clone()))
}

pub fn new(configs: &config::Configs) -> Result<AuthConfig> {
let audio_cache_folder = if configs.app_config.device.audio_cache {
Some(configs.cache_folder.join("audio"))
Expand All @@ -42,99 +69,31 @@ impl AuthConfig {

Ok(AuthConfig {
cache,
session_config: configs.app_config.session_config(),
session_config: configs.app_config.session_config()?,
})
}
}

fn read_user_auth_details(user: Option<String>) -> Result<(String, String)> {
let mut username = String::new();
let mut stdout = std::io::stdout();
match user {
None => write!(stdout, "Username: ")?,
Some(ref u) => write!(stdout, "Username (default: {u}): ")?,
}
stdout.flush()?;
std::io::stdin().read_line(&mut username)?;
username = username.trim_end().to_string();
if username.is_empty() {
username = user.unwrap_or_default();
}
let password = rpassword::prompt_password(format!("Password for {username}: "))?;
Ok((username, password))
}

pub async fn new_session_with_new_creds(auth_config: &AuthConfig) -> Result<Session> {
tracing::info!("Creating a new session with new authentication credentials");

let mut user: Option<String> = None;

for i in 0..3 {
let (username, password) = read_user_auth_details(user)?;
user = Some(username.clone());
match Session::connect(
auth_config.session_config.clone(),
Credentials::with_password(username, password),
Some(auth_config.cache.clone()),
true,
)
.await
{
Ok((session, _)) => {
println!("Successfully authenticated as {}", user.unwrap_or_default());
return Ok(session);
}
Err(err) => {
eprintln!("Failed to authenticate, {} tries left", 2 - i);
tracing::warn!("Failed to authenticate: {err:#}")
}
}
}

Err(anyhow!("authentication failed!"))
}

/// Creates a new Librespot session
///
/// By default, the function will look for cached credentials in the `APP_CACHE_FOLDER` folder.
///
/// If `reauth` is true, re-authenticate by asking the user for Spotify's username and password.
/// The re-authentication process should only happen on the terminal using stdin/stdout.
pub async fn new_session(auth_config: &AuthConfig, reauth: bool) -> Result<Session> {
match auth_config.cache.credentials() {
/// Get Spotify credentials to authenticate the application
pub async fn get_creds(auth_config: &AuthConfig, reauth: bool) -> Result<Credentials> {
Ok(match auth_config.cache.credentials() {
None => {
let msg = "No cached credentials found, please authenticate the application first.";
if reauth {
eprintln!("{msg}");
new_session_with_new_creds(auth_config).await
get_access_token(
SPOTIFY_CLIENT_ID,
CLIENT_REDIRECT_URI,
OAUTH_SCOPES.to_vec(),
)
.map(|t| Credentials::with_access_token(t.access_token))?
} else {
anyhow::bail!(msg);
}
}
Some(creds) => {
match Session::connect(
auth_config.session_config.clone(),
creds,
Some(auth_config.cache.clone()),
true,
)
.await
{
Ok((session, _)) => {
tracing::info!(
"Successfully used the cached credentials to create a new session!"
);
Ok(session)
}
Err(err) => match err {
SessionError::AuthenticationError(err) => {
anyhow::bail!("Failed to authenticate using cached credentials: {err:#}");
}
SessionError::IoError(err) => {
anyhow::bail!("{err:#}\nPlease check your internet connection.");
}
},
}
tracing::info!("Using cached credentials");
creds
}
}
})
}
Loading
Loading