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