use crate::{
api::{self, ApiError, RestClient},
auth::{
AuthCodePKCE, AuthError, AuthResult, ClientCredentials,
private::{AsyncAuthFlow, AuthFlow},
scopes::Scope,
},
model::Token,
};
use async_trait::async_trait;
use bytes::Bytes;
use http::{HeaderMap, HeaderValue, Response as HttpResponse};
use parking_lot::RwLock;
use reqwest::{Client as AsyncClient, blocking::Client};
use std::{collections::HashSet, sync::Arc};
use thiserror::Error;
use url::Url;
const BASE_API_URL: &str = "https://api.spotify.com/v1/";
pub type SpotifyPKCE = Spotify<AuthCodePKCE>;
pub type SpotifyClientCredentials = Spotify<ClientCredentials>;
pub type AsyncSpotifyPKCE = AsyncSpotify<AuthCodePKCE>;
pub type AsyncSpotifyClientCredentials = AsyncSpotify<ClientCredentials>;
pub type SpotifyResult<T> = Result<T, SpotifyError>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RestError {
#[error("error setting auth header: {0}")]
AuthError(#[from] AuthError),
#[error("communication with spotify: {0}")]
Communication(#[from] reqwest::Error),
#[error("`http` error: {0}")]
Http(#[from] http::Error),
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SpotifyError {
#[error("failed to parse url: {0}")]
UrlParse(#[from] url::ParseError),
#[error("error setting auth header: {0}")]
AuthError(#[from] AuthError),
#[error("communication with spotify: {0}")]
Communication(#[from] reqwest::Error),
#[error("spotify HTTP error: {status}")]
Http {
status: reqwest::StatusCode,
},
#[error("no response from spotify")]
NoResponse,
#[error("could not parse {typename} data: {source}")]
DataType {
source: serde_json::Error,
typename: &'static str,
},
#[error("api error: {0}")]
Api(#[from] ApiError<RestError>),
}
impl SpotifyError {
pub(crate) fn data_type<T>(source: serde_json::Error) -> Self {
Self::DataType {
source,
typename: std::any::type_name::<T>(),
}
}
}
pub struct Spotify<A>
where
A: AuthFlow,
{
client: Client,
api_url: Url,
auth: A,
token: Arc<RwLock<Option<Token>>>,
token_callback: Option<Box<dyn Fn(Token) + 'static>>,
}
impl<A> Spotify<A>
where
A: AuthFlow,
{
fn new_impl(auth: A) -> SpotifyResult<Self> {
let api_url = Url::parse(BASE_API_URL)?;
let client = Client::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let api = Self {
client,
api_url,
auth,
token: Arc::new(RwLock::new(None)),
token_callback: None,
};
Ok(api)
}
fn rest_auth(
&self,
mut request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, ApiError<<Self as RestClient>::Error>> {
let is_expired = self
.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.is_expired();
let refresh_token = if is_expired {
self.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.refresh_token
.clone()
} else {
None
};
if let Some(refresh_token) = refresh_token {
let new_token = self.auth.refresh_token(&self.client, &refresh_token)?;
self.set_token(new_token);
}
let call = || -> Result<_, RestError> {
self.set_header(
request
.headers_mut()
.expect("failed to get headers on the request builder"),
)?;
let http_request = request.body(body)?;
let request = http_request.try_into()?;
let rsp = self.client.execute(request)?;
let mut http_rsp = HttpResponse::builder()
.status(rsp.status())
.version(rsp.version());
let headers = http_rsp
.headers_mut()
.expect("failed to get headers on the request builder");
for (key, value) in rsp.headers() {
headers.insert(key, value.clone());
}
Ok(http_rsp.body(rsp.bytes()?)?)
};
call().map_err(ApiError::client)
}
fn set_header<'a>(
&self,
headers: &'a mut HeaderMap<HeaderValue>,
) -> AuthResult<&'a mut HeaderMap<HeaderValue>> {
let token = self.token.read();
let token = token.as_ref().ok_or(AuthError::EmptyAccessToken)?;
let value = format!("Bearer {}", token.access_token);
let mut token_header_value = HeaderValue::from_str(&value).map_err(AuthError::from)?;
token_header_value.set_sensitive(true);
headers.insert(http::header::AUTHORIZATION, token_header_value);
Ok(headers)
}
pub fn token(&self) -> Arc<RwLock<Option<Token>>> {
self.token.clone()
}
pub fn token_to_string(&self) -> SpotifyResult<Option<String>> {
let token = self.token.read();
let Some(token) = token.as_ref() else {
return Ok(None);
};
let s = serde_json::to_string(token).map_err(SpotifyError::data_type::<Token>)?;
Ok(Some(s))
}
fn set_token(&self, mut token: Token) {
token.expires_at = chrono::Utc::now()
.checked_add_signed(chrono::Duration::seconds(token.expires_in as i64));
if let Some(callback) = &self.token_callback {
callback(token.clone());
}
*self.token.write() = Some(token);
}
}
impl Spotify<AuthCodePKCE> {
pub fn with_authorization_code_pkce(
client_id: impl Into<String>,
redirect_uri: impl Into<String>,
scopes: impl Into<Option<HashSet<Scope>>>,
) -> SpotifyResult<Self> {
let auth = AuthCodePKCE::new(client_id, redirect_uri, scopes);
Self::new_impl(auth)
}
pub fn with_token(mut self, token: Token) -> Self {
let mut scopes = HashSet::new();
if let Some(scope_str) = &token.scope {
for s in scope_str.split_whitespace() {
if let Ok(scope) = Scope::try_from(s) {
scopes.insert(scope);
}
}
}
self.auth.set_scopes(Some(scopes));
self.token = Arc::new(RwLock::new(Some(token)));
self
}
pub fn token_callback(mut self, handler: impl Fn(Token) + 'static) -> Self {
self.token_callback = Some(Box::new(handler));
self
}
pub fn user_authorization_url(&mut self) -> String {
self.auth.user_authorization_url()
}
pub fn verify_authorization_code(&self, url: &str) -> AuthResult<String> {
self.auth.verify_authorization_code(url)
}
pub fn request_token(&self, code: &str) -> Result<(), ApiError<RestError>> {
let token = self.auth.request_token(code, &self.client)?;
self.set_token(token);
Ok(())
}
pub fn request_token_from_redirect_url(&self, url: &str) -> Result<(), ApiError<RestError>> {
let token = self
.auth
.request_token_from_redirect_url(url, &self.client)?;
self.set_token(token);
Ok(())
}
pub fn refresh_token(&self) -> Result<(), ApiError<RestError>> {
let refresh_token = self
.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.refresh_token
.clone()
.ok_or(AuthError::EmptyRefreshToken)?;
let token = self.auth.refresh_token(&self.client, &refresh_token)?;
self.set_token(token);
Ok(())
}
}
impl Spotify<ClientCredentials> {
pub fn with_client_credentials(
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> SpotifyResult<Self> {
let auth = ClientCredentials::new(client_id, client_secret);
Self::new_impl(auth)
}
pub fn with_token(mut self, mut token: Token) -> Self {
token.refresh_token = None;
token.scope = None;
self.token = Arc::new(RwLock::new(Some(token)));
self
}
pub fn request_token(&self) -> Result<(), ApiError<RestError>> {
let token = self.auth.request_token(&self.client)?;
self.set_token(token);
Ok(())
}
}
impl<A> RestClient for Spotify<A>
where
A: AuthFlow,
{
type Error = RestError;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, ApiError<Self::Error>> {
log::info!("REST api call {endpoint}");
Ok(self.api_url.join(endpoint)?)
}
}
impl<A> api::Client for Spotify<A>
where
A: AuthFlow,
{
fn rest(
&self,
request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, ApiError<Self::Error>> {
self.rest_auth(request, body)
}
}
pub struct AsyncSpotify<A>
where
A: AsyncAuthFlow,
{
client: reqwest::Client,
api_url: Url,
auth: A,
token: Arc<RwLock<Option<Token>>>,
token_callback: Option<Box<dyn Fn(Token) + Send + Sync + 'static>>,
}
impl<A> AsyncSpotify<A>
where
A: AsyncAuthFlow + Sync,
{
fn new_impl(auth: A) -> SpotifyResult<Self> {
let api_url = Url::parse(BASE_API_URL)?;
let client = AsyncClient::builder()
.timeout(std::time::Duration::from_secs(10))
.build()?;
let api = Self {
client,
api_url,
auth,
token: Arc::new(RwLock::new(None)),
token_callback: None,
};
Ok(api)
}
async fn rest_async_auth(
&self,
mut request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, ApiError<<Self as RestClient>::Error>> {
use futures_util::TryFutureExt;
let is_expired = self
.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.is_expired();
let refresh_token = if is_expired {
self.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.refresh_token
.clone()
} else {
None
};
if let Some(refresh_token) = refresh_token {
let new_token = self
.auth
.refresh_token_async(&self.client, &refresh_token)
.await?;
self.set_token(new_token);
}
let call = || async {
self.set_header(
request
.headers_mut()
.expect("failed to get headers on the request builder"),
)?;
let http_request = request.body(body)?;
let request = http_request.try_into()?;
let rsp = self.client.execute(request).await?;
let mut http_rsp = HttpResponse::builder()
.status(rsp.status())
.version(rsp.version());
let headers = http_rsp
.headers_mut()
.expect("failed to get headers on the request builder");
for (key, value) in rsp.headers() {
headers.insert(key, value.clone());
}
Ok(http_rsp.body(rsp.bytes().await?)?)
};
call().map_err(ApiError::client).await
}
fn set_header<'a>(
&self,
headers: &'a mut HeaderMap<HeaderValue>,
) -> AuthResult<&'a mut HeaderMap<HeaderValue>> {
let token = self.token.read();
let token = token.as_ref().ok_or(AuthError::EmptyAccessToken)?;
let value = format!("Bearer {}", token.access_token);
let mut token_header_value = HeaderValue::from_str(&value).map_err(AuthError::from)?;
token_header_value.set_sensitive(true);
headers.insert(http::header::AUTHORIZATION, token_header_value);
Ok(headers)
}
pub fn token(&self) -> Arc<RwLock<Option<Token>>> {
self.token.clone()
}
pub fn token_to_string(&self) -> SpotifyResult<Option<String>> {
let token = self.token.read();
let Some(token) = token.as_ref() else {
return Ok(None);
};
let s = serde_json::to_string(token).map_err(SpotifyError::data_type::<Token>)?;
Ok(Some(s))
}
fn set_token(&self, mut token: Token) {
token.expires_at = chrono::Utc::now()
.checked_add_signed(chrono::Duration::seconds(token.expires_in as i64));
if let Some(callback) = &self.token_callback {
callback(token.clone());
}
*self.token.write() = Some(token);
}
}
impl AsyncSpotify<AuthCodePKCE> {
pub fn with_authorization_code_pkce(
client_id: impl Into<String>,
redirect_uri: impl Into<String>,
scopes: impl Into<Option<HashSet<Scope>>>,
) -> SpotifyResult<Self> {
let auth = AuthCodePKCE::new(client_id, redirect_uri, scopes);
Self::new_impl(auth)
}
pub fn with_token(mut self, token: Token) -> Self {
let mut scopes = HashSet::new();
if let Some(scope_str) = &token.scope {
for s in scope_str.split_whitespace() {
if let Ok(scope) = Scope::try_from(s) {
scopes.insert(scope);
}
}
}
self.auth.set_scopes(Some(scopes));
self.token = Arc::new(RwLock::new(Some(token)));
self
}
pub fn token_callback(mut self, handler: impl Fn(Token) + Send + Sync + 'static) -> Self {
self.token_callback = Some(Box::new(handler));
self
}
pub fn user_authorization_url(&mut self) -> String {
self.auth.user_authorization_url()
}
pub fn verify_authorization_code(&self, url: &str) -> AuthResult<String> {
self.auth.verify_authorization_code(url)
}
pub async fn request_token(&self, code: &str) -> Result<(), ApiError<RestError>> {
let token = self.auth.request_token_async(code, &self.client).await?;
self.set_token(token);
Ok(())
}
pub async fn request_token_from_redirect_url(
&self,
url: &str,
) -> Result<(), ApiError<RestError>> {
let token = self
.auth
.request_token_from_redirect_url_async(url, &self.client)
.await?;
self.set_token(token);
Ok(())
}
pub async fn refresh_token(&self) -> Result<(), ApiError<RestError>> {
let refresh_token = self
.token
.read()
.as_ref()
.ok_or(AuthError::EmptyAccessToken)?
.refresh_token
.clone()
.ok_or(AuthError::EmptyRefreshToken)?;
let token = self
.auth
.refresh_token_async(&self.client, &refresh_token)
.await?;
self.set_token(token);
Ok(())
}
}
impl AsyncSpotify<ClientCredentials> {
pub fn with_client_credentials(
client_id: impl Into<String>,
client_secret: impl Into<String>,
) -> SpotifyResult<Self> {
let auth = ClientCredentials::new(client_id, client_secret);
Self::new_impl(auth)
}
pub fn with_token(mut self, mut token: Token) -> Self {
token.refresh_token = None;
token.scope = None;
self.token = Arc::new(RwLock::new(Some(token)));
self
}
pub async fn request_token(&self) -> Result<(), ApiError<RestError>> {
let token = self.auth.request_token_async(&self.client).await?;
self.set_token(token);
Ok(())
}
}
#[async_trait]
impl<A> RestClient for AsyncSpotify<A>
where
A: AsyncAuthFlow,
{
type Error = RestError;
fn rest_endpoint(&self, endpoint: &str) -> Result<Url, ApiError<Self::Error>> {
log::info!("REST api call {endpoint}");
Ok(self.api_url.join(endpoint)?)
}
}
#[async_trait]
impl<A> api::AsyncClient for AsyncSpotify<A>
where
A: AsyncAuthFlow + Sync + Send,
{
async fn rest_async(
&self,
request: http::request::Builder,
body: Vec<u8>,
) -> Result<HttpResponse<Bytes>, ApiError<Self::Error>> {
self.rest_async_auth(request, body).await
}
}