pub mod authorization_code;
pub mod implicit_grant;
pub mod request_builder;
pub(crate) mod object;
pub(crate) mod private;
pub(crate) mod scoped;
pub(crate) mod unscoped;
use std::sync::{Arc, RwLock};
use base64::Engine;
use const_format::concatcp;
use log::debug;
use reqwest::{
header::{self, HeaderMap},
IntoUrl, Method, StatusCode,
};
use serde::Deserialize;
use self::implicit_grant::ImplicitGrantUserClientBuilder;
#[cfg(feature = "async")]
use self::{
authorization_code::{AsyncAuthorizationCodeUserClient, AsyncAuthorizationCodeUserClientBuilder},
implicit_grant::AsyncImplicitGrantUserClientBuilder,
private::AsyncClient,
};
#[cfg(feature = "sync")]
use self::{
authorization_code::{SyncAuthorizationCodeUserClient, SyncAuthorizationCodeUserClientBuilder},
implicit_grant::SyncImplicitGrantUserClientBuilder,
private::SyncClient,
};
pub use self::{scoped::ScopedClient, unscoped::UnscopedClient};
use crate::{
error::{Error, Result},
model::error::{AuthenticationErrorKind, AuthenticationErrorResponse},
};
#[cfg(feature = "async")]
pub type AsyncSpotifyClient = SpotifyClient<AsyncClient>;
#[cfg(feature = "sync")]
pub type SyncSpotifyClient = SpotifyClient<SyncClient>;
#[cfg(feature = "async")]
pub type AsyncSpotifyClientWithSecret = SpotifyClientWithSecret<AsyncClient>;
#[cfg(feature = "sync")]
pub type SyncSpotifyClientWithSecret = SpotifyClientWithSecret<SyncClient>;
const RANDOM_STATE_LENGTH: usize = 16;
const PKCE_VERIFIER_LENGTH: usize = 128; const CLIENT_CREDENTIALS_TOKEN_REQUEST_FORM: &[(&str, &str)] = &[("grant_type", "client_credentials")];
const API_BASE_URL: &str = "https://api.spotify.com/v1/";
const API_TRACKS_ENDPOINT: &str = concatcp!(API_BASE_URL, "tracks");
const API_SEARCH_ENDPOINT: &str = concatcp!(API_BASE_URL, "search");
const API_USER_PROFILE_ENDPOINT: &str = concatcp!(API_BASE_URL, "users");
const API_CURRENT_USER_PROFILE_ENDPOINT: &str = concatcp!(API_BASE_URL, "me");
const API_PLAYBACK_STATE_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player");
const API_CURRENTLY_PLAYING_ITEM_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/currently-playing");
const API_PLAYER_PLAY_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/play");
const API_PLAYER_PAUSE_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/pause");
const API_PLAYER_REPEAT_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/repeat");
const API_PLAYER_SHUFFLE_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/shuffle");
const API_PLAYER_VOLUME_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/volume");
const API_PLAYER_NEXT_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/next");
const API_PLAYER_PREVIOUS_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/previous");
const API_PLAYER_SEEK_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/seek");
const API_PLAYER_QUEUE_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/queue");
const API_PLAYER_DEVICES_ENDPOINT: &str = concatcp!(API_BASE_URL, "me/player/devices");
const ACCOUNTS_BASE_URL: &str = "https://accounts.spotify.com/";
const ACCOUNTS_AUTHORIZE_ENDPOINT: &str = concatcp!(ACCOUNTS_BASE_URL, "authorize");
const ACCOUNTS_API_TOKEN_ENDPOINT: &str = concatcp!(ACCOUNTS_BASE_URL, "api/token");
#[cfg(feature = "async")]
#[async_trait::async_trait]
pub trait AccessTokenRefreshAsync: crate::private::Sealed {
async fn refresh_access_token(&self) -> Result<()>;
}
#[cfg(feature = "sync")]
pub trait AccessTokenRefreshSync: crate::private::Sealed {
fn refresh_access_token(&self) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct SpotifyClient<C>
where
C: private::HttpClient,
{
inner: Arc<SpotifyClientRef>,
http_client: C,
}
#[derive(Debug)]
struct SpotifyClientRef {
client_id: String,
}
#[derive(Debug, Clone)]
pub struct SpotifyClientWithSecret<C>
where
C: private::HttpClient,
{
inner: Arc<SpotifyClientWithSecretRef>,
http_client: C,
}
#[derive(Debug)]
struct SpotifyClientWithSecretRef {
client_id: String,
access_token: RwLock<String>,
}
#[derive(Debug, Clone)]
pub struct SpotifyClientBuilder {
client_id: String,
}
#[derive(Debug, Clone)]
pub struct SpotifyClientWithSecretBuilder {
client_id: String,
client_secret: String,
}
#[derive(Debug, Deserialize)]
struct ClientTokenResponse {
access_token: String,
#[allow(dead_code)]
token_type: String,
#[allow(dead_code)]
expires_in: u32,
}
#[cfg(feature = "async")]
impl AsyncSpotifyClient {
pub fn implicit_grant_client<S>(&self, redirect_uri: S) -> AsyncImplicitGrantUserClientBuilder
where
S: Into<String>,
{
ImplicitGrantUserClientBuilder::new(redirect_uri.into(), Arc::clone(&self.inner), self.http_client.clone())
}
pub fn authorization_code_client_with_pkce<S>(&self, redirect_uri: S) -> AsyncAuthorizationCodeUserClientBuilder
where
S: Into<String>,
{
AsyncAuthorizationCodeUserClientBuilder::new(
redirect_uri.into(),
self.inner.client_id.clone(),
self.http_client.clone(),
)
.with_pkce()
}
pub async fn authorization_code_client_with_refresh_token_and_pkce<S>(
&self,
refresh_token: S,
) -> Result<AsyncAuthorizationCodeUserClient>
where
S: Into<String>,
{
AsyncAuthorizationCodeUserClient::new_with_refresh_token(
self.http_client.clone(),
refresh_token.into(),
Some(self.inner.client_id.clone()),
)
.await
}
}
#[cfg(feature = "sync")]
impl SyncSpotifyClient {
pub fn implicit_grant_client<S>(&self, redirect_uri: S) -> SyncImplicitGrantUserClientBuilder
where
S: Into<String>,
{
ImplicitGrantUserClientBuilder::new(redirect_uri.into(), Arc::clone(&self.inner), self.http_client.clone())
}
pub fn authorization_code_client_with_pkce<S>(&self, redirect_uri: S) -> SyncAuthorizationCodeUserClientBuilder
where
S: Into<String>,
{
SyncAuthorizationCodeUserClientBuilder::new(
redirect_uri.into(),
self.inner.client_id.clone(),
self.http_client.clone(),
)
.with_pkce()
}
pub fn authorization_code_client_with_refresh_token_and_pkce<S>(
&self,
refresh_token: S,
) -> Result<SyncAuthorizationCodeUserClient>
where
S: Into<String>,
{
SyncAuthorizationCodeUserClient::new_with_refresh_token(
self.http_client.clone(),
refresh_token.into(),
Some(self.inner.client_id.clone()),
)
}
}
#[cfg(feature = "async")]
impl AsyncSpotifyClientWithSecret {
pub fn authorization_code_client<S>(&self, redirect_uri: S) -> AsyncAuthorizationCodeUserClientBuilder
where
S: Into<String>,
{
AsyncAuthorizationCodeUserClientBuilder::new(
redirect_uri.into(),
self.inner.client_id.clone(),
self.http_client.clone(),
)
}
pub async fn authorization_code_client_with_refresh_token<S>(
&self,
refresh_token: S,
) -> Result<AsyncAuthorizationCodeUserClient>
where
S: Into<String>,
{
AsyncAuthorizationCodeUserClient::new_with_refresh_token(self.http_client.clone(), refresh_token.into(), None)
.await
}
}
#[cfg(feature = "sync")]
impl SyncSpotifyClientWithSecret {
pub fn authorization_code_client<S>(&self, redirect_uri: S) -> SyncAuthorizationCodeUserClientBuilder
where
S: Into<String>,
{
SyncAuthorizationCodeUserClientBuilder::new(
redirect_uri.into(),
self.inner.client_id.clone(),
self.http_client.clone(),
)
}
pub fn authorization_code_client_with_refresh_token<S>(
&self,
refresh_token: S,
) -> Result<SyncAuthorizationCodeUserClient>
where
S: Into<String>,
{
SyncAuthorizationCodeUserClient::new_with_refresh_token(self.http_client.clone(), refresh_token.into(), None)
}
}
impl SpotifyClientBuilder {
pub fn new<S>(client_id: S) -> Self
where
S: Into<String>,
{
Self {
client_id: client_id.into(),
}
}
pub fn client_secret<S>(self, client_secret: S) -> SpotifyClientWithSecretBuilder
where
S: Into<String>,
{
SpotifyClientWithSecretBuilder {
client_id: self.client_id,
client_secret: client_secret.into(),
}
}
#[cfg(feature = "async")]
pub fn build_async(self) -> AsyncSpotifyClient {
self.build_client()
}
#[cfg(feature = "sync")]
pub fn build_sync(self) -> SyncSpotifyClient {
self.build_client()
}
fn build_client<C>(self) -> SpotifyClient<C>
where
C: private::HttpClient + Clone,
{
SpotifyClient {
inner: Arc::new(SpotifyClientRef {
client_id: self.client_id,
}),
http_client: C::new(),
}
}
}
impl SpotifyClientWithSecretBuilder {
fn get_default_headers(&self) -> HeaderMap {
let mut default_headers = header::HeaderMap::new();
default_headers.insert(
header::AUTHORIZATION,
build_authorization_header(&self.client_id, &self.client_secret)
.parse()
.expect(
"failed to insert authorization header into header map: non-ASCII characters in value (this is \
likely a bug)",
),
);
default_headers
}
fn build_client<C>(self, token_response: ClientTokenResponse, http_client: C) -> SpotifyClientWithSecret<C>
where
C: private::HttpClient + Clone,
{
debug!("Got token response for client credentials flow: {:?}", token_response);
SpotifyClientWithSecret {
inner: Arc::new(SpotifyClientWithSecretRef {
client_id: self.client_id,
access_token: RwLock::new(token_response.access_token),
}),
http_client,
}
}
}
impl SpotifyClientWithSecretBuilder {
#[cfg(feature = "async")]
pub async fn build_async(self) -> Result<AsyncSpotifyClientWithSecret> {
debug!("Requesting access token for client credentials flow");
let http_client = AsyncClient(
reqwest::Client::builder()
.default_headers(self.get_default_headers())
.build()
.expect("failed to build HTTP client: system error or system misconfiguration"),
);
let response = http_client
.post(ACCOUNTS_API_TOKEN_ENDPOINT)
.form(CLIENT_CREDENTIALS_TOKEN_REQUEST_FORM)
.send()
.await?;
let response = extract_authentication_error_async(response)
.await
.map_err(map_client_authentication_error)?;
let token_response = response.json().await?;
Ok(self.build_client(token_response, http_client))
}
#[cfg(feature = "sync")]
pub fn build_sync(self) -> Result<SyncSpotifyClientWithSecret> {
debug!("Requesting access token for client credentials flow");
let http_client = SyncClient(
reqwest::blocking::Client::builder()
.default_headers(self.get_default_headers())
.build()
.expect("failed to build blocking HTTP client: system error or system misconfiguration"),
);
let response = http_client
.post(ACCOUNTS_API_TOKEN_ENDPOINT)
.form(CLIENT_CREDENTIALS_TOKEN_REQUEST_FORM)
.send()?;
let response = extract_authentication_error_sync(response).map_err(map_client_authentication_error)?;
let token_response = response.json()?;
Ok(self.build_client(token_response, http_client))
}
}
impl<C> crate::private::Sealed for SpotifyClientWithSecret<C> where C: private::HttpClient + Clone {}
impl<C> SpotifyClientWithSecret<C>
where
C: private::HttpClient + Clone,
{
fn save_access_token(&self, token_response: ClientTokenResponse) {
debug!("Got token response for client credentials flow: {:?}", token_response);
*self.inner.access_token.write().expect("access token rwlock poisoned") = token_response.access_token;
}
}
#[cfg(feature = "async")]
impl private::BuildHttpRequestAsync for AsyncSpotifyClientWithSecret {
fn build_http_request<U>(&self, method: Method, url: U) -> reqwest::RequestBuilder
where
U: IntoUrl,
{
let access_token = self.inner.access_token.read().expect("access token rwlock poisoned");
self.http_client.request(method, url).bearer_auth(access_token.as_str())
}
}
#[cfg(feature = "sync")]
impl private::BuildHttpRequestSync for SyncSpotifyClientWithSecret {
fn build_http_request<U>(&self, method: Method, url: U) -> reqwest::blocking::RequestBuilder
where
U: IntoUrl,
{
let access_token = self.inner.access_token.read().expect("access token rwlock poisoned");
self.http_client.request(method, url).bearer_auth(access_token.as_str())
}
}
#[cfg(feature = "async")]
impl UnscopedClient for AsyncSpotifyClientWithSecret {}
#[cfg(feature = "sync")]
impl UnscopedClient for SyncSpotifyClientWithSecret {}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl AccessTokenRefreshAsync for AsyncSpotifyClientWithSecret {
async fn refresh_access_token(&self) -> Result<()> {
debug!("Refreshing access token for client credentials flow");
let response = self
.http_client
.post(ACCOUNTS_API_TOKEN_ENDPOINT)
.form(CLIENT_CREDENTIALS_TOKEN_REQUEST_FORM)
.send()
.await?;
let response = extract_authentication_error_async(response)
.await
.map_err(map_client_authentication_error)?;
let token_response = response.json().await?;
self.save_access_token(token_response);
Ok(())
}
}
#[cfg(feature = "sync")]
impl AccessTokenRefreshSync for SyncSpotifyClientWithSecret {
fn refresh_access_token(&self) -> Result<()> {
debug!("Refreshing access token for client credentials flow");
let response = self
.http_client
.post(ACCOUNTS_API_TOKEN_ENDPOINT)
.form(CLIENT_CREDENTIALS_TOKEN_REQUEST_FORM)
.send()?;
let response = extract_authentication_error_sync(response).map_err(map_client_authentication_error)?;
let token_response = response.json()?;
self.save_access_token(token_response);
Ok(())
}
}
#[cfg(feature = "async")]
#[async_trait::async_trait]
impl private::AccessTokenExpiryAsync for AsyncSpotifyClientWithSecret {
async fn handle_access_token_expired(&self) -> Result<private::AccessTokenExpiryResult> {
self.refresh_access_token().await?;
Ok(private::AccessTokenExpiryResult::Ok)
}
}
#[cfg(feature = "sync")]
impl private::AccessTokenExpirySync for SyncSpotifyClientWithSecret {
fn handle_access_token_expired(&self) -> Result<private::AccessTokenExpiryResult> {
self.refresh_access_token()?;
Ok(private::AccessTokenExpiryResult::Ok)
}
}
fn build_authorization_header(client_id: &str, client_secret: &str) -> String {
let auth = format!("{client_id}:{client_secret}");
format!(
"Basic {}",
base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth)
)
}
#[cfg(feature = "async")]
async fn extract_authentication_error_async(response: reqwest::Response) -> Result<reqwest::Response> {
if let StatusCode::BAD_REQUEST = response.status() {
let error_response: AuthenticationErrorResponse = response.json().await?;
debug!("Authentication error response: {error_response:?}");
Err(error_response.into_unhandled_error())
} else {
Ok(response)
}
}
#[cfg(feature = "sync")]
fn extract_authentication_error_sync(response: reqwest::blocking::Response) -> Result<reqwest::blocking::Response> {
if let StatusCode::BAD_REQUEST = response.status() {
let error_response: AuthenticationErrorResponse = response.json()?;
debug!("Authentication error response: {error_response:?}");
Err(error_response.into_unhandled_error())
} else {
Ok(response)
}
}
#[cfg(feature = "sync")]
fn rate_limit_sleep_sync(sleep_time: u64) -> Result<()> {
std::thread::sleep(std::time::Duration::from_secs(sleep_time));
Ok(())
}
#[cfg(all(feature = "async", not(feature = "tokio_sleep"), not(feature = "async_std_sleep")))]
async fn rate_limit_sleep_async(sleep_time: u64) -> Result<()> {
Err(crate::error::Error::RateLimit(sleep_time))
}
#[cfg(all(feature = "async", feature = "tokio_sleep"))]
async fn rate_limit_sleep_async(sleep_time: u64) -> Result<()> {
tokio::time::sleep(std::time::Duration::from_secs(sleep_time)).await;
Ok(())
}
#[cfg(all(feature = "async", feature = "async_std_sleep", not(feature = "tokio_sleep")))]
async fn rate_limit_sleep_async(sleep_time: u64) -> Result<()> {
async_std::task::sleep(std::time::Duration::from_secs(sleep_time)).await;
Ok(())
}
fn map_client_authentication_error(err: Error) -> Error {
if let Error::UnhandledAuthenticationError(AuthenticationErrorKind::InvalidClient, description) = err {
Error::InvalidClient(description)
} else {
err
}
}