1 //! Rspotify is a wrapper for the [Spotify Web API][spotify-main], inspired by
2 //! [spotipy][spotipy-github]. It includes support for all the [authorization
3 //! flows][spotify-auth-flows], and helper methods for [all available
4 //! endpoints][spotify-reference].
5 //!
6 //! ## Configuration
7 //!
8 //! ### HTTP Client
9 //!
10 //! By default, Rspotify uses the [reqwest][reqwest-docs] asynchronous HTTP
11 //! client with its default TLS, but you can customize both the HTTP client and
12 //! the TLS with the following features:
13 //!
14 //! - [reqwest][reqwest-docs]: enabling
15 //!   `client-reqwest`, TLS available:
16 //!     + `reqwest-default-tls` (reqwest's default)
17 //!     + `reqwest-rustls-tls`
18 //!     + `reqwest-native-tls`
19 //!     + `reqwest-native-tls-vendored`
20 //! - [ureq][ureq-docs]: enabling `client-ureq`, TLS
21 //!   available:
22 //!     + `ureq-rustls-tls` (ureq's default)
23 //!
24 //! If you want to use a different client or TLS than the default ones, you'll
25 //! have to disable the default features and enable whichever you want. For
26 //! example, this would compile Rspotify with `reqwest` and the native TLS:
27 //!
28 //! ```toml
29 //! [dependencies]
30 //! rspotify = {
31 //!     version = "...",
32 //!     default-features = false,
33 //!     features = ["client-reqwest", "reqwest-native-tls"]
34 //! }
35 //! ```
36 //!
37 //! [`maybe_async`] internally enables Rspotify to  use both synchronous and
38 //! asynchronous HTTP clients. You can also use `ureq`, a synchronous client,
39 //! like so:
40 //!
41 //! ```toml
42 //! [dependencies]
43 //! rspotify = {
44 //!     version = "...",
45 //!     default-features = false,
46 //!     features = ["client-ureq", "ureq-rustls-tls"]
47 //! }
48 //! ```
49 //!
50 //! ### Proxies
51 //!
52 //! [reqwest supports system proxies by default][reqwest-proxies]. It reads the
53 //! environment variables `HTTP_PROXY` and `HTTPS_PROXY` environmental variables
54 //! to set HTTP and HTTPS proxies, respectively.
55 //!
56 //! ### Environmental variables
57 //!
58 //! Rspotify supports the [`dotenv`] crate, which allows you to save credentials
59 //! in a `.env` file. These will then be automatically available as
60 //! environmental values when using methods like [`Credentials::from_env`].
61 //!
62 //! ```toml
63 //! [dependencies]
64 //! rspotify = { version = "...", features = ["env-file"] }
65 //! ```
66 //!
67 //! ### CLI utilities
68 //!
69 //! Rspotify includes basic support for Cli apps to obtain access tokens by
70 //! prompting the user, after enabling the `cli` feature. See the
71 //! [Authorization](#authorization) section for more information.
72 //!
73 //! ## Getting Started
74 //!
75 //! ### Authorization
76 //!
77 //! All endpoints require app authorization; you will need to generate a token
78 //! that indicates that the client has been granted permission to perform
79 //! requests. You can start by [registering your app to get the necessary client
80 //! credentials][spotify-register-app]. Read the [official guide for a detailed
81 //! explanation of the different authorization flows
82 //! available][spotify-auth-flows].
83 //!
84 //! Rspotify has a different client for each of the available authentication
85 //! flows. They may implement the endpoints in
86 //! [`BaseClient`](crate::clients::BaseClient) or
87 //! [`OAuthClient`](crate::clients::OAuthClient) according to what kind of
88 //! flow it is. Please refer to their documentation for more details:
89 //!
90 //! * [Client Credentials Flow][spotify-client-creds]: see
91 //!   [`ClientCredsSpotify`].
92 //! * [Authorization Code Flow][spotify-auth-code]: see [`AuthCodeSpotify`].
93 //! * [Authorization Code Flow with Proof Key for Code Exchange
94 //!   (PKCE)][spotify-auth-code-pkce]: see [`AuthCodePkceSpotify`].
95 //! * [Implicit Grant Flow][spotify-implicit-grant]: unimplemented, as Rspotify
96 //!   has not been tested on a browser yet. If you'd like support for it, let us
97 //!   know in an issue!
98 //!
99 //! In order to help other developers to get used to `rspotify`, there are
100 //! public credentials available for a dummy account. You can test `rspotify`
101 //! with this account's `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET` inside
102 //! the [`.env` file](https://github.com/ramsayleung/rspotify/blob/master/.env)
103 //! for more details.
104 //!
105 //! ### Examples
106 //!
107 //! There are some [available examples on the GitHub
108 //! repository][examples-github] which can serve as a learning tool.
109 //!
110 //! [spotipy-github]: https://github.com/plamere/spotipy
111 //! [reqwest-docs]: https://docs.rs/reqwest/
112 //! [reqwest-proxies]: https://docs.rs/reqwest/#proxies
113 //! [ureq-docs]: https://docs.rs/ureq/
114 //! [examples-github]: https://github.com/ramsayleung/rspotify/tree/master/examples
115 //! [spotify-main]: https://developer.spotify.com/documentation/web-api/
116 //! [spotify-auth-flows]: https://developer.spotify.com/documentation/general/guides/authorization/
117 //! [spotify-reference]: https://developer.spotify.com/documentation/web-api/reference/
118 //! [spotify-register-app]: https://developer.spotify.com/dashboard/applications
119 //! [spotify-client-creds]: https://developer.spotify.com/documentation/general/guides/authorization/client-credentials/
120 //! [spotify-auth-code]: https://developer.spotify.com/documentation/general/guides/authorization/code-flow
121 //! [spotify-auth-code-pkce]: https://developer.spotify.com/documentation/general/guides/authorization/code-flow
122 //! [spotify-implicit-grant]: https://developer.spotify.com/documentation/general/guides/authorization/implicit-grant
123 
124 pub mod auth_code;
125 pub mod auth_code_pkce;
126 pub mod client_creds;
127 pub mod clients;
128 
129 // Subcrate re-exports
130 pub use rspotify_http as http;
131 pub use rspotify_macros as macros;
132 pub use rspotify_model as model;
133 // Top-level re-exports
134 pub use auth_code::AuthCodeSpotify;
135 pub use auth_code_pkce::AuthCodePkceSpotify;
136 pub use client_creds::ClientCredsSpotify;
137 pub use macros::scopes;
138 pub use model::Token;
139 
140 use crate::{http::HttpError, model::Id};
141 
142 use std::{
143     collections::{HashMap, HashSet},
144     env,
145     path::PathBuf,
146 };
147 
148 use getrandom::getrandom;
149 use thiserror::Error;
150 
151 pub mod prelude {
152     pub use crate::clients::{BaseClient, OAuthClient};
153     pub use crate::model::idtypes::{Id, PlayContextId, PlayableId};
154 }
155 
156 /// Common headers as constants.
157 pub(in crate) mod params {
158     pub const CLIENT_ID: &str = "client_id";
159     pub const CODE: &str = "code";
160     pub const GRANT_TYPE: &str = "grant_type";
161     pub const GRANT_TYPE_AUTH_CODE: &str = "authorization_code";
162     pub const GRANT_TYPE_CLIENT_CREDS: &str = "client_credentials";
163     pub const GRANT_TYPE_REFRESH_TOKEN: &str = "refresh_token";
164     pub const REDIRECT_URI: &str = "redirect_uri";
165     pub const REFRESH_TOKEN: &str = "refresh_token";
166     pub const RESPONSE_TYPE_CODE: &str = "code";
167     pub const RESPONSE_TYPE: &str = "response_type";
168     pub const SCOPE: &str = "scope";
169     pub const SHOW_DIALOG: &str = "show_dialog";
170     pub const STATE: &str = "state";
171     pub const CODE_CHALLENGE: &str = "code_challenge";
172     pub const CODE_VERIFIER: &str = "code_verifier";
173     pub const CODE_CHALLENGE_METHOD: &str = "code_challenge_method";
174     pub const CODE_CHALLENGE_METHOD_S256: &str = "S256";
175 }
176 
177 /// Common alphabets for random number generation and similars
178 pub(in crate) mod alphabets {
179     pub const ALPHANUM: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
180     /// From https://datatracker.ietf.org/doc/html/rfc7636#section-4.1
181     pub const PKCE_CODE_VERIFIER: &[u8] =
182         b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~";
183 }
184 
185 pub(in crate) mod auth_urls {
186     pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize";
187     pub const TOKEN: &str = "https://accounts.spotify.com/api/token";
188 }
189 
190 /// Possible errors returned from the `rspotify` client.
191 #[derive(Debug, Error)]
192 pub enum ClientError {
193     #[error("json parse error: {0}")]
194     ParseJson(#[from] serde_json::Error),
195 
196     #[error("url parse error: {0}")]
197     ParseUrl(#[from] url::ParseError),
198 
199     // Note that this type is boxed because its size might be very large in
200     // comparison to the rest. For more information visit:
201     // https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant
202     #[error("http error: {0}")]
203     Http(Box<HttpError>),
204 
205     #[error("input/output error: {0}")]
206     Io(#[from] std::io::Error),
207 
208     #[cfg(feature = "cli")]
209     #[error("cli error: {0}")]
210     Cli(String),
211 
212     #[error("cache file error: {0}")]
213     CacheFile(String),
214 
215     #[error("model error: {0}")]
216     Model(#[from] model::ModelError),
217 }
218 
219 // The conversion has to be done manually because it's in a `Box<T>`
220 impl From<HttpError> for ClientError {
from(err: HttpError) -> Self221     fn from(err: HttpError) -> Self {
222         ClientError::Http(Box::new(err))
223     }
224 }
225 
226 pub type ClientResult<T> = Result<T, ClientError>;
227 
228 pub const DEFAULT_API_PREFIX: &str = "https://api.spotify.com/v1/";
229 pub const DEFAULT_CACHE_PATH: &str = ".spotify_token_cache.json";
230 pub const DEFAULT_PAGINATION_CHUNKS: u32 = 50;
231 
232 /// Struct to configure the Spotify client.
233 #[derive(Debug, Clone)]
234 pub struct Config {
235     /// The Spotify API prefix, [`DEFAULT_API_PREFIX`] by default.
236     pub prefix: String,
237 
238     /// The cache file path, in case it's used. By default it's
239     /// [`DEFAULT_CACHE_PATH`]
240     pub cache_path: PathBuf,
241 
242     /// The pagination chunk size used when performing automatically paginated
243     /// requests, like [`artist_albums`](crate::clients::BaseClient). This
244     /// means that a request will be performed every `pagination_chunks` items.
245     /// By default this is [`DEFAULT_PAGINATION_CHUNKS`].
246     ///
247     /// Note that most endpoints set a maximum to the number of items per
248     /// request, which most times is 50.
249     pub pagination_chunks: u32,
250 
251     /// Whether or not to save the authentication token into a JSON file,
252     /// then reread the token from JSON file when launching the program without
253     /// following the full auth process again
254     pub token_cached: bool,
255 
256     /// Whether or not to check if the token has expired when sending a
257     /// request with credentials, and in that case, automatically refresh it.
258     pub token_refreshing: bool,
259 }
260 
261 impl Default for Config {
default() -> Self262     fn default() -> Self {
263         Config {
264             prefix: String::from(DEFAULT_API_PREFIX),
265             cache_path: PathBuf::from(DEFAULT_CACHE_PATH),
266             pagination_chunks: DEFAULT_PAGINATION_CHUNKS,
267             token_cached: false,
268             token_refreshing: false,
269         }
270     }
271 }
272 
273 /// Generate `length` random chars from the Operating System.
274 ///
275 /// It is assumed that system always provides high-quality cryptographically
276 /// secure random data, ideally backed by hardware entropy sources.
generate_random_string(length: usize, alphabet: &[u8]) -> String277 pub(in crate) fn generate_random_string(length: usize, alphabet: &[u8]) -> String {
278     let mut buf = vec![0u8; length];
279     getrandom(&mut buf).unwrap();
280     let range = alphabet.len();
281 
282     buf.iter()
283         .map(|byte| alphabet[*byte as usize % range] as char)
284         .collect()
285 }
286 
287 #[inline]
join_ids<'a, T: Id + 'a + ?Sized>(ids: impl IntoIterator<Item = &'a T>) -> String288 pub(in crate) fn join_ids<'a, T: Id + 'a + ?Sized>(ids: impl IntoIterator<Item = &'a T>) -> String {
289     ids.into_iter().map(Id::id).collect::<Vec<_>>().join(",")
290 }
291 
292 #[inline]
join_scopes(scopes: &HashSet<String>) -> String293 pub(in crate) fn join_scopes(scopes: &HashSet<String>) -> String {
294     scopes
295         .iter()
296         .map(String::as_str)
297         .collect::<Vec<_>>()
298         .join(" ")
299 }
300 
301 /// Simple client credentials object for Spotify.
302 #[derive(Debug, Clone, Default)]
303 pub struct Credentials {
304     pub id: String,
305     /// PKCE doesn't require a client secret
306     pub secret: Option<String>,
307 }
308 
309 impl Credentials {
310     /// Initialization with both the client ID and the client secret
new(id: &str, secret: &str) -> Self311     pub fn new(id: &str, secret: &str) -> Self {
312         Credentials {
313             id: id.to_owned(),
314             secret: Some(secret.to_owned()),
315         }
316     }
317 
318     /// Initialization with just the client ID
new_pkce(id: &str) -> Self319     pub fn new_pkce(id: &str) -> Self {
320         Credentials {
321             id: id.to_owned(),
322             secret: None,
323         }
324     }
325 
326     /// Parses the credentials from the environment variables
327     /// `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET`. You can optionally
328     /// activate the `env-file` feature in order to read these variables from
329     /// a `.env` file.
from_env() -> Option<Self>330     pub fn from_env() -> Option<Self> {
331         #[cfg(feature = "env-file")]
332         {
333             dotenv::dotenv().ok();
334         }
335 
336         Some(Credentials {
337             id: env::var("RSPOTIFY_CLIENT_ID").ok()?,
338             secret: env::var("RSPOTIFY_CLIENT_SECRET").ok(),
339         })
340     }
341 
342     /// Generates an HTTP basic authorization header with proper formatting
343     ///
344     /// This will only work when the client secret is set to `Option::Some`.
auth_headers(&self) -> Option<HashMap<String, String>>345     pub fn auth_headers(&self) -> Option<HashMap<String, String>> {
346         let auth = "authorization".to_owned();
347         let value = format!("{}:{}", self.id, self.secret.as_ref()?);
348         let value = format!("Basic {}", base64::encode(value));
349 
350         let mut headers = HashMap::new();
351         headers.insert(auth, value);
352         Some(headers)
353     }
354 }
355 
356 /// Structure that holds the required information for requests with OAuth.
357 #[derive(Debug, Clone)]
358 pub struct OAuth {
359     pub redirect_uri: String,
360     /// The state is generated by default, as suggested by the OAuth2 spec:
361     /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12)
362     pub state: String,
363     /// You could use macro [scopes!](crate::scopes) to build it at compile time easily
364     pub scopes: HashSet<String>,
365     pub proxies: Option<String>,
366 }
367 
368 impl Default for OAuth {
default() -> Self369     fn default() -> Self {
370         OAuth {
371             redirect_uri: String::new(),
372             state: generate_random_string(16, alphabets::ALPHANUM),
373             scopes: HashSet::new(),
374             proxies: None,
375         }
376     }
377 }
378 
379 impl OAuth {
380     /// Parses the credentials from the environment variable
381     /// `RSPOTIFY_REDIRECT_URI`. You can optionally activate the `env-file`
382     /// feature in order to read these variables from a `.env` file.
from_env(scopes: HashSet<String>) -> Option<Self>383     pub fn from_env(scopes: HashSet<String>) -> Option<Self> {
384         #[cfg(feature = "env-file")]
385         {
386             dotenv::dotenv().ok();
387         }
388 
389         Some(OAuth {
390             scopes,
391             redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok()?,
392             ..Default::default()
393         })
394     }
395 }
396 
397 #[cfg(test)]
398 mod test {
399     use crate::{alphabets, generate_random_string, Credentials};
400     use std::collections::HashSet;
401 
402     #[test]
test_generate_random_string()403     fn test_generate_random_string() {
404         let mut containers = HashSet::new();
405         for _ in 1..101 {
406             containers.insert(generate_random_string(10, alphabets::ALPHANUM));
407         }
408         assert_eq!(containers.len(), 100);
409     }
410 
411     #[test]
test_basic_auth()412     fn test_basic_auth() {
413         let creds = Credentials::new_pkce("ramsay");
414         let headers = creds.auth_headers();
415         assert_eq!(headers, None);
416 
417         let creds = Credentials::new("ramsay", "123456");
418 
419         let headers = creds.auth_headers().unwrap();
420         assert_eq!(headers.len(), 1);
421         assert_eq!(
422             headers.get("authorization"),
423             Some(&"Basic cmFtc2F5OjEyMzQ1Ng==".to_owned())
424         );
425     }
426 }
427