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

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from
2,747 changes: 1,631 additions & 1,116 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 16 additions & 8 deletions spotify_player/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ clap = { version = "4.5.8", features = ["derive", "string"] }
config_parser2 = "0.1.5"
crossterm = "0.27.0"
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", optional = true }
librespot-core = "0.5"
librespot-oauth = "0.5"
librespot-playback = { version = "0.5", 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"
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" }
rand = "0.8.5"
Expand All @@ -33,12 +39,14 @@ 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"] }
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: 73 additions & 66 deletions spotify_player/src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
use std::io::Write;

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

use crate::config;

// copied from https://github.com/hrkfdn/ncspot/pull/1244/commits/81f13e2f9458a378d70d864716c2d915ad8cdaa4
// TODO: adjust, this is just for debugging
pub const SPOTIFY_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
pub const CLIENT_REDIRECT_URI: &str = "http://127.0.0.1:8989/login";
static 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 Down Expand Up @@ -47,42 +74,23 @@ impl AuthConfig {
}
}

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))
fn get_credentials() -> Result<Credentials, OAuthError> {
get_access_token(
SPOTIFY_CLIENT_ID,
CLIENT_REDIRECT_URI,
OAUTH_SCOPES.to_vec(),
)
.map(|t| Credentials::with_access_token(t.access_token))
}

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;
async fn create_creds() -> Result<Credentials> {
tracing::info!("Creating new authentication credentials");

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);
match get_credentials() {
Ok(c) => {
println!("Successfully authenticated");
return Ok(c);
}
Err(err) => {
eprintln!("Failed to authenticate, {} tries left", 2 - i);
Expand All @@ -94,47 +102,46 @@ pub async fn new_session_with_new_creds(auth_config: &AuthConfig) -> Result<Sess
Err(anyhow!("authentication failed!"))
}

/// Creates a new Librespot session
/// Creates a new Librespot session and connects it
///
/// 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.
/// If `reauth` is true, re-authenticate by generating new credentials
pub async fn new_session(auth_config: &AuthConfig, reauth: bool) -> Result<Session> {
match auth_config.cache.credentials() {
// obtain credentials
let creds = 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
create_creds().await?
} 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
}
};
let session = Session::new(
auth_config.session_config.clone(),
Some(auth_config.cache.clone()),
);
// attempt to connect the session
match session.connect(creds, true).await {
Ok(()) => {
tracing::info!("Successfully created a new session!");
Ok(session)
}
Err(Error { kind, error }) => match kind {
ErrorKind::Unauthenticated => {
anyhow::bail!("Failed to authenticate using cached credentials: {error:#}");
}
ErrorKind::Unavailable => {
anyhow::bail!("{error:#}\nPlease check your internet connection.");
}
_ => anyhow::bail!("{error:#}"),
},
}
}
8 changes: 5 additions & 3 deletions spotify_player/src/cli/handlers.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
auth::{new_session, new_session_with_new_creds, AuthConfig},
auth::{new_session, AuthConfig},
client,
};

Expand Down Expand Up @@ -158,7 +158,9 @@ fn try_connect_to_client(socket: &UdpSocket, configs: &config::Configs) -> Resul
let session = rt.block_on(new_session(&auth_config, false))?;

// create a Spotify API client
let client_id = configs.app_config.get_client_id()?;
// TODO: the client id has been set for debugging
// let client_id = configs.app_config.get_client_id()?;
let client_id = crate::auth::SPOTIFY_CLIENT_ID.to_string();
let client = client::Client::new(session, auth_config, client_id);
rt.block_on(client.refresh_token())?;

Expand All @@ -184,7 +186,7 @@ pub fn handle_cli_subcommand(cmd: &str, args: &ArgMatches) -> Result<()> {
"authenticate" => {
let auth_config = AuthConfig::new(configs)?;
let rt = tokio::runtime::Runtime::new()?;
rt.block_on(new_session_with_new_creds(&auth_config))?;
rt.block_on(new_session(&auth_config, true))?;
std::process::exit(0);
}
"generate" => {
Expand Down
16 changes: 10 additions & 6 deletions spotify_player/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ impl Client {
let mut stream_conn = self.stream_conn.lock();
// shutdown old streaming connection and replace it with a new connection
if let Some(conn) = stream_conn.as_ref() {
conn.shutdown();
let _ = conn.shutdown();
}
*stream_conn = Some(new_conn);
}
Expand Down Expand Up @@ -832,8 +832,8 @@ impl Client {
let response = session
.mercury()
.get(autoplay_query_url)
.await
.map_err(|_| anyhow::anyhow!("Failed to get autoplay URI: got a Mercury error"))?;
.map_err(|_| anyhow::anyhow!("Failed to get autoplay URI: got a Mercury error"))?
.await?;
if response.status_code != 200 {
anyhow::bail!(
"Failed to get autoplay URI: got non-OK status code: {}",
Expand All @@ -844,9 +844,13 @@ impl Client {

// Retrieve radio's data based on the autoplay URI
let radio_query_url = format!("hm://radio-apollo/v3/stations/{autoplay_uri}");
let response = session.mercury().get(radio_query_url).await.map_err(|_| {
anyhow::anyhow!("Failed to get radio data of {autoplay_uri}: got a Mercury error")
})?;
let response = session
.mercury()
.get(radio_query_url)
.map_err(|_| {
anyhow::anyhow!("Failed to get radio data of {autoplay_uri}: got a Mercury error")
})?
.await?;
if response.status_code != 200 {
anyhow::bail!(
"Failed to get radio data of {autoplay_uri}: got non-OK status code: {}",
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/client/spotify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ impl BaseClient for Spotify {
return Ok(old_token);
}

match token::get_token(&session, &self.client_id).await {
match token::get_token(&session).await {
Ok(token) => Ok(Some(token)),
Err(err) => {
tracing::error!("Failed to get a new token: {err:#}");
Expand Down
1 change: 1 addition & 0 deletions spotify_player/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,7 @@ impl AppConfig {
SessionConfig {
proxy,
ap_port: self.ap_port,
client_id: crate::auth::SPOTIFY_CLIENT_ID.to_string(),
..Default::default()
}
}
Expand Down
28 changes: 17 additions & 11 deletions spotify_player/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ mod utils;

use anyhow::{Context, Result};
use rspotify::clients::BaseClient;
use std::io::Write;
//use std::io::Write;

async fn init_spotify(
client_pub: &flume::Sender<client::ClientRequest>,
Expand Down Expand Up @@ -54,23 +54,29 @@ fn init_logging(cache_folder: &std::path::Path) -> Result<()> {
.with_writer(std::sync::Mutex::new(log_file))
.init();

// TODO: reenable the panic hook; it's disabled for debugging
// initialize the application's panic backtrace
let backtrace_file =
std::fs::File::create(cache_folder.join(format!("{log_prefix}.backtrace")))
.context("failed to create backtrace file")?;
let backtrace_file = std::sync::Mutex::new(backtrace_file);
std::panic::set_hook(Box::new(move |info| {
let mut file = backtrace_file.lock().unwrap();
let backtrace = backtrace::Backtrace::new();
writeln!(&mut file, "Got a panic: {info:#?}\n").unwrap();
writeln!(&mut file, "Stack backtrace:\n{backtrace:?}").unwrap();
}));
//let backtrace_file =
// std::fs::File::create(cache_folder.join(format!("{log_prefix}.backtrace")))
// .context("failed to create backtrace file")?;
//let backtrace_file = std::sync::Mutex::new(backtrace_file);
//std::panic::set_hook(Box::new(move |info| {
// let mut file = backtrace_file.lock().unwrap();
// let backtrace = backtrace::Backtrace::new();
// writeln!(&mut file, "Got a panic: {info:#?}\n").unwrap();
// writeln!(&mut file, "Reason: {:#?}\n", info.payload()).unwrap();
// writeln!(&mut file, "Stack backtrace:\n{backtrace:?}").unwrap();
//}));

Ok(())
}

#[tokio::main]
async fn start_app(state: &state::SharedState) -> Result<()> {
// this is needed for some reason
// TODO: figure out why
rustls::crypto::aws_lc_rs::default_provider().install_default().unwrap();

let configs = config::get_config();

if !state.is_daemon {
Expand Down
Loading