aspotify/
lib.rs

1//! Note: This crate is deprecated in favour of [rspotify](https://docs.rs/rspotify).
2//!
3//! aspotify is an asynchronous client to the [Spotify
4//! API](https://developer.spotify.com/documentation/web-api/).
5//!
6//! # Examples
7//! ```
8//! # async {
9//! use aspotify::{Client, ClientCredentials};
10//!
11//! // This from_env function tries to read the CLIENT_ID and CLIENT_SECRET environment variables.
12//! // You can use the dotenv crate to read it from a file.
13//! let credentials = ClientCredentials::from_env()
14//!     .expect("CLIENT_ID and CLIENT_SECRET not found.");
15//!
16//! // Create a Spotify client.
17//! let client = Client::new(credentials);
18//!
19//! // Gets the album "Favourite Worst Nightmare" from Spotify, with no specified market.
20//! let album = client.albums().get_album("1XkGORuUX2QGOEIL4EbJKm", None).await.unwrap();
21//! # };
22//! ```
23//!
24//! # Notes
25//! - Spotify often imposes limits on endpoints, for example you can't get more than 50 tracks at
26//! once. This crate removes this limit by making multiple requests when necessary.
27#![forbid(unsafe_code)]
28#![deny(rust_2018_idioms)]
29#![warn(missing_docs, clippy::pedantic)]
30#![allow(
31    clippy::module_name_repetitions,
32    clippy::non_ascii_literal,
33    clippy::items_after_statements,
34    clippy::filter_map
35)]
36#![cfg_attr(test, allow(clippy::float_cmp))]
37
38use std::collections::HashMap;
39use std::env::{self, VarError};
40use std::error::Error as StdError;
41use std::ffi::OsStr;
42use std::fmt::{self, Display, Formatter};
43use std::time::{Duration, Instant};
44
45use reqwest::{header, RequestBuilder, Url};
46use serde::de::DeserializeOwned;
47use serde::{Deserialize, Serialize};
48use tokio::sync::{Mutex, MutexGuard};
49
50pub use authorization_url::*;
51pub use endpoints::*;
52/// Re-export from [`isocountry`].
53pub use isocountry::CountryCode;
54/// Re-export from [`isolanguage_1`].
55pub use isolanguage_1::LanguageCode;
56pub use model::*;
57
58mod authorization_url;
59pub mod endpoints;
60pub mod model;
61mod util;
62
63/// A client to the Spotify API.
64///
65/// By default it will use the [client credentials
66/// flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow)
67/// to send requests to the Spotify API. The [`set_refresh_token`](Client::set_refresh_token) and
68/// [`redirected`](Client::redirected) methods tell it to use the [authorization code
69/// flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow)
70/// instead.
71#[derive(Debug)]
72pub struct Client {
73    /// Your Spotify client credentials.
74    pub credentials: ClientCredentials,
75    client: reqwest::Client,
76    cache: Mutex<AccessToken>,
77    debug: bool,
78}
79
80impl Client {
81    /// Create a new client from your Spotify client credentials.
82    #[must_use]
83    pub fn new(credentials: ClientCredentials) -> Self {
84        Self {
85            credentials,
86            client: reqwest::Client::new(),
87            cache: Mutex::new(AccessToken::new(None)),
88            debug: false,
89        }
90    }
91    /// Create a new client with your Spotify client credentials and a refresh token.
92    #[must_use]
93    pub fn with_refresh(credentials: ClientCredentials, refresh_token: String) -> Self {
94        Self {
95            credentials,
96            client: reqwest::Client::new(),
97            cache: Mutex::new(AccessToken::new(Some(refresh_token))),
98            debug: false,
99        }
100    }
101    /// Get the client's refresh token.
102    pub async fn refresh_token(&self) -> Option<String> {
103        self.cache.lock().await.refresh_token.clone()
104    }
105    /// Set the client's refresh token.
106    pub async fn set_refresh_token(&self, refresh_token: Option<String>) {
107        self.cache.lock().await.refresh_token = refresh_token;
108    }
109    /// Get the client's access token values.
110    pub async fn current_access_token(&self) -> (String, Instant) {
111        let cache = self.cache.lock().await;
112        (cache.token.clone(), cache.expires)
113    }
114    /// Explicitly override the client's access token values. Useful if you acquire the
115    /// access token elsewhere.
116    pub async fn set_current_access_token(&self, token: String, expires: Instant) {
117        let mut cache = self.cache.lock().await;
118        cache.token = token;
119        cache.expires = expires;
120    }
121
122    async fn token_request(&self, params: TokenRequest<'_>) -> Result<AccessToken, Error> {
123        let request = self
124            .client
125            .post("https://accounts.spotify.com/api/token")
126            .basic_auth(&self.credentials.id, Some(&self.credentials.secret))
127            .form(&params)
128            .build()?;
129
130        if self.debug {
131            dbg!(&request, body_str(&request));
132        }
133
134        let response = self.client.execute(request).await?;
135        let status = response.status();
136        let text = response.text().await?;
137        if !status.is_success() {
138            if self.debug {
139                eprintln!(
140                    "Authentication failed ({}). Response body is '{}'",
141                    status, text
142                );
143            }
144            return Err(Error::Auth(serde_json::from_str(&text)?));
145        }
146
147        if self.debug {
148            dbg!(status);
149            eprintln!("Authentication response body is '{}'", text);
150        }
151
152        Ok(serde_json::from_str(&text)?)
153    }
154
155    /// Set the refresh token from the URL the client was redirected to and the state that was used
156    /// to send them there.
157    ///
158    /// Use the [`authorization_url()`] function to generate the URL to which you can send the
159    /// client to to generate the URL here.
160    ///
161    /// # Errors
162    ///
163    /// Fails if the URL is invalid in some way, the state was incorrect for the URL or Spotify
164    /// fails.
165    pub async fn redirected(&self, url: &str, state: &str) -> Result<(), RedirectedError> {
166        let url = Url::parse(url)?;
167
168        let pairs: HashMap<_, _> = url.query_pairs().collect();
169
170        if pairs
171            .get("state")
172            .map_or(true, |url_state| url_state != state)
173        {
174            return Err(RedirectedError::IncorrectState);
175        }
176
177        if let Some(error) = pairs.get("error") {
178            return Err(RedirectedError::AuthFailed(error.to_string()));
179        }
180
181        let code = pairs
182            .get("code")
183            .ok_or_else(|| RedirectedError::AuthFailed(String::new()))?;
184
185        let token = self
186            .token_request(TokenRequest::AuthorizationCode {
187                code: &*code,
188                redirect_uri: &url[..url::Position::AfterPath],
189            })
190            .await?;
191        *self.cache.lock().await = token;
192
193        Ok(())
194    }
195
196    async fn access_token(&self) -> Result<MutexGuard<'_, AccessToken>, Error> {
197        let mut cache = self.cache.lock().await;
198        if Instant::now() >= cache.expires {
199            *cache = match cache.refresh_token.take() {
200                // Authorization code flow
201                Some(refresh_token) => {
202                    let mut token = self
203                        .token_request(TokenRequest::RefreshToken {
204                            refresh_token: &refresh_token,
205                        })
206                        .await?;
207                    token.refresh_token = Some(refresh_token);
208                    token
209                }
210                // Client credentials flow
211                None => self.token_request(TokenRequest::ClientCredentials).await?,
212            }
213        }
214        Ok(cache)
215    }
216
217    async fn send_text(&self, request: RequestBuilder) -> Result<Response<String>, Error> {
218        let request = request
219            .bearer_auth(&self.access_token().await?.token)
220            .build()?;
221
222        if self.debug {
223            dbg!(&request, body_str(&request));
224        }
225
226        let response = loop {
227            let response = self.client.execute(request.try_clone().unwrap()).await?;
228            if response.status() != 429 {
229                break response;
230            }
231            let wait = response
232                .headers()
233                .get(header::RETRY_AFTER)
234                .and_then(|val| val.to_str().ok())
235                .and_then(|secs| secs.parse::<u64>().ok());
236            // 2 seconds is default retry after time; should never be used if the Spotify API and
237            // my code are both correct.
238            let wait = wait.unwrap_or(2);
239            tokio::time::sleep(std::time::Duration::from_secs(wait)).await;
240        };
241        let status = response.status();
242        let cache_control = Duration::from_secs(
243            response
244                .headers()
245                .get_all(header::CACHE_CONTROL)
246                .iter()
247                .filter_map(|value| value.to_str().ok())
248                .flat_map(|value| value.split(|c| c == ','))
249                .find_map(|value| {
250                    let mut parts = value.trim().splitn(2, '=');
251                    if parts.next().unwrap().eq_ignore_ascii_case("max-age") {
252                        parts.next().and_then(|max| max.parse::<u64>().ok())
253                    } else {
254                        None
255                    }
256                })
257                .unwrap_or_default(),
258        );
259
260        let data = response.text().await?;
261        if !status.is_success() {
262            if self.debug {
263                eprintln!("Failed ({}). Response body is '{}'", status, data);
264            }
265            return Err(Error::Endpoint(serde_json::from_str(&data)?));
266        }
267
268        if self.debug {
269            dbg!(status);
270            eprintln!("Response body is '{}'", data);
271        }
272
273        Ok(Response {
274            data,
275            expires: Instant::now() + cache_control,
276        })
277    }
278
279    async fn send_empty(&self, request: RequestBuilder) -> Result<(), Error> {
280        self.send_text(request).await?;
281        Ok(())
282    }
283
284    async fn send_opt_json<T: DeserializeOwned>(
285        &self,
286        request: RequestBuilder,
287    ) -> Result<Response<Option<T>>, Error> {
288        let res = self.send_text(request).await?;
289        Ok(Response {
290            data: if res.data.is_empty() {
291                None
292            } else {
293                serde_json::from_str(&res.data)?
294            },
295            expires: res.expires,
296        })
297    }
298
299    async fn send_json<T: DeserializeOwned>(
300        &self,
301        request: RequestBuilder,
302    ) -> Result<Response<T>, Error> {
303        let res = self.send_text(request).await?;
304        Ok(Response {
305            data: serde_json::from_str(&res.data)?,
306            expires: res.expires,
307        })
308    }
309
310    async fn send_snapshot_id(&self, request: RequestBuilder) -> Result<String, Error> {
311        #[derive(Deserialize)]
312        struct SnapshotId {
313            snapshot_id: String,
314        }
315        Ok(self
316            .send_json::<SnapshotId>(request)
317            .await?
318            .data
319            .snapshot_id)
320    }
321}
322
323/// The result of a request to a Spotify endpoint.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub struct Response<T> {
326    /// The data itself.
327    pub data: T,
328    /// When the cache expires.
329    pub expires: Instant,
330}
331
332impl<T> Response<T> {
333    /// Map the contained data if there is any.
334    pub fn map<U>(self, f: impl FnOnce(T) -> U) -> Response<U> {
335        Response {
336            data: f(self.data),
337            expires: self.expires,
338        }
339    }
340}
341
342/// An object that holds your Spotify Client ID and Client Secret.
343///
344/// See [the Spotify guide on Spotify
345/// apps](https://developer.spotify.com/documentation/general/guides/app-settings/) for how to get
346/// these.
347///
348/// # Examples
349///
350/// ```no_run
351/// use aspotify::ClientCredentials;
352///
353/// // Create from inside the program.
354/// let credentials = ClientCredentials {
355///     id: "your client id here".to_owned(),
356///     secret: "your client secret here".to_owned()
357/// };
358///
359/// // Create from CLIENT_ID and CLIENT_SECRET environment variables
360/// let credentials = ClientCredentials::from_env()
361///     .expect("CLIENT_ID or CLIENT_SECRET environment variables not set");
362///
363/// // Or use custom env var names
364/// let credentials = ClientCredentials::from_env_vars("SPOTIFY_ID", "SPOTIFY_SECRET")
365///     .expect("SPOTIFY_ID or SPOTIFY_SECRET environment variables not set");
366/// ```
367#[derive(Debug, Clone, PartialEq, Eq)]
368pub struct ClientCredentials {
369    /// The Client ID.
370    pub id: String,
371    /// The Client Secret.
372    pub secret: String,
373}
374
375impl ClientCredentials {
376    /// Attempts to create a `ClientCredentials` by reading environment variables.
377    ///
378    /// # Errors
379    ///
380    /// Fails if the environment variables are not present or are not unicode.
381    pub fn from_env_vars<I: AsRef<OsStr>, S: AsRef<OsStr>>(
382        client_id: I,
383        client_secret: S,
384    ) -> Result<Self, VarError> {
385        Ok(Self {
386            id: env::var(client_id)?,
387            secret: env::var(client_secret)?,
388        })
389    }
390    /// Attempts to create a `ClientCredentials` by reading the `CLIENT_ID` and `CLIENT_SECRET`
391    /// environment variables.
392    ///
393    /// Equivalent to `ClientCredentials::from_env_vars("CLIENT_ID", "CLIENT_SECRET")`.
394    ///
395    /// # Errors
396    ///
397    /// Fails if the environment variables are not present or are not unicode.
398    pub fn from_env() -> Result<Self, VarError> {
399        Self::from_env_vars("CLIENT_ID", "CLIENT_SECRET")
400    }
401}
402
403/// An error caused by the [`Client::redirected`] function.
404#[derive(Debug)]
405pub enum RedirectedError {
406    /// The URL is malformed.
407    InvalidUrl(url::ParseError),
408    /// The URL has no state parameter, or the state parameter was incorrect.
409    IncorrectState,
410    /// The user has not accepted the request or an error occured in Spotify.
411    ///
412    /// This contains the string returned by Spotify in the `error` parameter.
413    AuthFailed(String),
414    /// An error occurred getting the access token.
415    Token(Error),
416}
417
418impl From<url::ParseError> for RedirectedError {
419    fn from(error: url::ParseError) -> Self {
420        Self::InvalidUrl(error)
421    }
422}
423impl From<Error> for RedirectedError {
424    fn from(error: Error) -> Self {
425        Self::Token(error)
426    }
427}
428
429impl Display for RedirectedError {
430    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
431        match self {
432            Self::InvalidUrl(_) => f.write_str("malformed redirect URL"),
433            Self::IncorrectState => f.write_str("state parameter not found or is incorrect"),
434            Self::AuthFailed(_) => f.write_str("authorization failed"),
435            Self::Token(e) => e.fmt(f),
436        }
437    }
438}
439
440impl StdError for RedirectedError {
441    fn source(&self) -> Option<&(dyn StdError + 'static)> {
442        Some(match self {
443            Self::InvalidUrl(e) => e,
444            Self::Token(e) => e,
445            _ => return None,
446        })
447    }
448}
449
450#[derive(Debug, Serialize)]
451#[serde(tag = "grant_type", rename_all = "snake_case")]
452enum TokenRequest<'a> {
453    RefreshToken {
454        refresh_token: &'a String,
455    },
456    ClientCredentials,
457    AuthorizationCode {
458        code: &'a str,
459        redirect_uri: &'a str,
460    },
461}
462
463#[derive(Debug, Deserialize)]
464struct AccessToken {
465    #[serde(rename = "access_token")]
466    token: String,
467    #[serde(
468        rename = "expires_in",
469        deserialize_with = "util::deserialize_instant_seconds"
470    )]
471    expires: Instant,
472    #[serde(default)]
473    refresh_token: Option<String>,
474}
475
476impl AccessToken {
477    fn new(refresh_token: Option<String>) -> Self {
478        Self {
479            token: String::new(),
480            expires: Instant::now() - Duration::from_secs(1),
481            refresh_token,
482        }
483    }
484}
485
486/// Get the contents of a request body as a string. This is only used for debugging purposes.
487fn body_str(req: &reqwest::Request) -> Option<&str> {
488    req.body().map(|body| {
489        body.as_bytes().map_or("stream", |bytes| {
490            std::str::from_utf8(bytes).unwrap_or("opaque bytes")
491        })
492    })
493}