From 05f906bc1092b8105047ce7ecbbf7062dfdc18d7 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Thu, 29 Apr 2021 00:01:50 +0200 Subject: [PATCH 01/56] moving http to a new crate --- Cargo.toml | 36 ++---- rspotify-http/Cargo.toml | 118 ++++++++++++++++++ src/http/mod.rs => rspotify-http/src/lib.rs | 17 +++ {src => rspotify-http/src}/pagination/iter.rs | 0 {src => rspotify-http/src}/pagination/mod.rs | 0 .../src}/pagination/stream.rs | 0 {src/http => rspotify-http/src}/reqwest.rs | 0 {src/http => rspotify-http/src}/ureq.rs | 0 rspotify-macros/Cargo.toml | 2 +- rspotify-model/Cargo.toml | 2 +- src/lib.rs | 23 +--- 11 files changed, 151 insertions(+), 47 deletions(-) create mode 100644 rspotify-http/Cargo.toml rename src/http/mod.rs => rspotify-http/src/lib.rs (93%) rename {src => rspotify-http/src}/pagination/iter.rs (100%) rename {src => rspotify-http/src}/pagination/mod.rs (100%) rename {src => rspotify-http/src}/pagination/stream.rs (100%) rename {src/http => rspotify-http/src}/reqwest.rs (100%) rename {src/http => rspotify-http/src}/ureq.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 13f08d0c..ef1469e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ edition = "2018" [workspace] members = [ "rspotify-macros", - "rspotify-model" + "rspotify-model", + "rspotify-http" ] exclude = [ "examples/webapp" @@ -25,9 +26,9 @@ resolver = "2" [dependencies] rspotify-macros = { path = "rspotify-macros", version = "0.10.0" } rspotify-model = { path = "rspotify-model", version = "0.10.0" } +rspotify-http = { path = "rspotify-http", version = "0.10.0" } ### Client ### -async-stream = { version = "0.3.0", optional = true } chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } derive_builder = "0.10.0" dotenv = { version = "0.15.0", optional = true } @@ -38,6 +39,7 @@ maybe-async = "0.2.1" serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0.57" thiserror = "1.0.20" +url = "2.1.1" webbrowser = { version = "0.5.5", optional = true } ### Auth ### @@ -46,18 +48,6 @@ webbrowser = { version = "0.5.5", optional = true } # maybe-async = "0.2.1" # thiserror = "1.0.20" -### HTTP ### -# Temporary until https://github.com/rust-lang/rfcs/issues/2739, for -# `maybe_async`. -async-trait = { version = "0.1.48", optional = true } -base64 = "0.13.0" -# log = "0.4.11" -# maybe-async = "0.2.1" -reqwest = { version = "0.11.0", default-features = false, features = ["json", "socks"], optional = true } -# thiserror = "1.0.20" -ureq = { version = "2.0", default-features = false, features = ["json", "cookies"], optional = true } -url = "2.1.1" - [dev-dependencies] env_logger = "0.8.1" tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } @@ -73,20 +63,20 @@ env-file = ["dotenv"] ### HTTP ### # Available clients. By default they don't include a TLS so that it can be # configured. -client-ureq = ["ureq", "__sync"] -client-reqwest = ["reqwest", "__async"] +client-ureq = ["rspotify-http/client-ureq", "__sync"] +client-reqwest = ["rspotify-http/client-reqwest", "__async"] # Passing the TLS features to reqwest. -reqwest-default-tls = ["reqwest/default-tls"] -reqwest-rustls-tls = ["reqwest/rustls-tls"] -reqwest-native-tls = ["reqwest/native-tls"] -reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] +reqwest-default-tls = ["rspotify-http/reqwest-default-tls"] +reqwest-rustls-tls = ["rspotify-http/reqwest-rustls-tls"] +reqwest-native-tls = ["rspotify-http/reqwest-native-tls"] +reqwest-native-tls-vendored = ["rspotify-http/reqwest-native-tls-vendored"] # Same for ureq. -ureq-rustls-tls = ["ureq/tls"] +ureq-rustls-tls = ["rspotify-http/ureq-rustls-tls"] # Internal features for checking async or sync compilation -__async = ["async-trait", "async-stream", "futures"] -__sync = ["maybe-async/is_sync"] +__async = ["futures"] +__sync = [] [package.metadata.docs.rs] # Also documenting the CLI methods diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml new file mode 100644 index 00000000..156df948 --- /dev/null +++ b/rspotify-http/Cargo.toml @@ -0,0 +1,118 @@ +[package] +authors = ["Ramsay Leung "] +name = "rspotify-http" +version = "0.10.0" +license = "MIT" +readme = "README.md" +description = "HTTP compatibility layer for Rspotify" +homepage = "https://github.com/ramsayleung/rspotify" +repository = "https://github.com/ramsayleung/rspotify" +keywords = ["spotify", "api"] +edition = "2018" + +[dependencies] +async-stream = { version = "0.3.0", optional = true } +# Temporary until https://github.com/rust-lang/rfcs/issues/2739, for +# `maybe_async`. +async-trait = { version = "0.1.48", optional = true } +base64 = "0.13.0" +futures = { version = "0.3.8", optional = true } +log = "0.4.11" +maybe-async = "0.2.1" +reqwest = { version = "0.11.0", default-features = false, features = ["json", "socks"], optional = true } +thiserror = "1.0.20" +ureq = { version = "2.0", default-features = false, features = ["json", "cookies"], optional = true } +url = "2.1.1" + +[dev-dependencies] +env_logger = "0.8.1" +tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } + +[features] +default = ["client-reqwest", "reqwest-default-tls"] + +# Available clients. By default they don't include a TLS so that it can be +# configured. +client-ureq = ["ureq", "__sync"] +client-reqwest = ["reqwest", "__async"] + +# Passing the TLS features to reqwest. +reqwest-default-tls = ["reqwest/default-tls"] +reqwest-rustls-tls = ["reqwest/rustls-tls"] +reqwest-native-tls = ["reqwest/native-tls"] +reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] +# Same for ureq. +ureq-rustls-tls = ["ureq/tls"] + +# Internal features for checking async or sync compilation +__async = ["async-trait", "async-stream", "futures"] +__sync = ["maybe-async/is_sync"] + +[package.metadata.docs.rs] +# Also documenting the CLI methods +features = ["cli"] + +[[example]] +name = "album" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/album.rs" + +[[example]] +name = "current_user_recently_played" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/current_user_recently_played.rs" + +[[example]] +name = "oauth_tokens" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/oauth_tokens.rs" + +[[example]] +name = "track" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/track.rs" + +[[example]] +name = "tracks" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/tracks.rs" + +[[example]] +name = "with_refresh_token" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/with_refresh_token.rs" + +[[example]] +name = "device" +required-features = ["env-file", "cli", "client-ureq"] +path = "examples/ureq/device.rs" + +[[example]] +name = "me" +required-features = ["env-file", "cli", "client-ureq"] +path = "examples/ureq/me.rs" + +[[example]] +name = "search" +required-features = ["env-file", "cli", "client-ureq"] +path = "examples/ureq/search.rs" + +[[example]] +name = "seek_track" +required-features = ["env-file", "cli", "client-ureq"] +path = "examples/ureq/seek_track.rs" + +[[example]] +name = "pagination_manual" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/pagination_manual.rs" + +[[example]] +name = "pagination_sync" +required-features = ["env-file", "cli", "client-ureq"] +path = "examples/pagination_sync.rs" + +[[example]] +name = "pagination_async" +required-features = ["env-file", "cli", "client-reqwest"] +path = "examples/pagination_async.rs" diff --git a/src/http/mod.rs b/rspotify-http/src/lib.rs similarity index 93% rename from src/http/mod.rs rename to rspotify-http/src/lib.rs index edd5ea95..0197cbb0 100644 --- a/src/http/mod.rs +++ b/rspotify-http/src/lib.rs @@ -1,11 +1,28 @@ //! The HTTP client may vary depending on which one the user configures. This //! module contains the required logic to use different clients interchangeably. +pub mod pagination; #[cfg(feature = "client-reqwest")] mod reqwest; #[cfg(feature = "client-ureq")] mod ureq; +// #[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] +// #[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] + +#[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] +compile_error!( + "`client-reqwest` and `client-ureq` features cannot both be enabled at \ + the same time, if you want to use `client-ureq` you need to set \ + `default-features = false`" +); + +#[cfg(not(any(feature = "client-reqwest", feature = "client-ureq")))] +compile_error!( + "You have to enable at least one of the available clients with the \ + `client-reqwest` or `client-ureq` features." +); + use crate::client::{ClientResult, Spotify}; use std::collections::HashMap; diff --git a/src/pagination/iter.rs b/rspotify-http/src/pagination/iter.rs similarity index 100% rename from src/pagination/iter.rs rename to rspotify-http/src/pagination/iter.rs diff --git a/src/pagination/mod.rs b/rspotify-http/src/pagination/mod.rs similarity index 100% rename from src/pagination/mod.rs rename to rspotify-http/src/pagination/mod.rs diff --git a/src/pagination/stream.rs b/rspotify-http/src/pagination/stream.rs similarity index 100% rename from src/pagination/stream.rs rename to rspotify-http/src/pagination/stream.rs diff --git a/src/http/reqwest.rs b/rspotify-http/src/reqwest.rs similarity index 100% rename from src/http/reqwest.rs rename to rspotify-http/src/reqwest.rs diff --git a/src/http/ureq.rs b/rspotify-http/src/ureq.rs similarity index 100% rename from src/http/ureq.rs rename to rspotify-http/src/ureq.rs diff --git a/rspotify-macros/Cargo.toml b/rspotify-macros/Cargo.toml index 96c1c89f..47c854fe 100644 --- a/rspotify-macros/Cargo.toml +++ b/rspotify-macros/Cargo.toml @@ -4,7 +4,7 @@ authors = ["Ramsay Leung "] version = "0.10.0" license = "MIT" readme = "README.md" -description = "Spotify API wrapper" +description = "Macros for Rspotify" homepage = "https://github.com/ramsayleung/rspotify" repository = "https://github.com/ramsayleung/rspotify" keywords = ["spotify", "api", "rspotify"] diff --git a/rspotify-model/Cargo.toml b/rspotify-model/Cargo.toml index b7fd30fe..94050191 100644 --- a/rspotify-model/Cargo.toml +++ b/rspotify-model/Cargo.toml @@ -4,7 +4,7 @@ version = "0.10.0" authors = ["Ramsay Leung "] license = "MIT" readme = "README.md" -description = "Spotify API wrapper" +description = "Model for Rspotify" homepage = "https://github.com/ramsayleung/rspotify" repository = "https://github.com/ramsayleung/rspotify" keywords = ["spotify", "api", "rspotify"] diff --git a/src/lib.rs b/src/lib.rs index 8604162f..6ae88d94 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,34 +161,13 @@ // This way only the compile error below gets shown instead of a whole list of // confusing errors.. -#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] -#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] pub mod client; -#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] -#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] -mod http; -#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] -#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] pub mod oauth2; -#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] -pub mod pagination; // Subcrate re-exports pub use rspotify_macros as macros; pub use rspotify_model as model; +pub use rspotify_http as http; // Top-level re-exports pub use macros::scopes; - -#[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] -compile_error!( - "`client-reqwest` and `client-ureq` features cannot both be enabled at \ - the same time, if you want to use `client-ureq` you need to set \ - `default-features = false`" -); - -#[cfg(not(any(feature = "client-reqwest", feature = "client-ureq")))] -compile_error!( - "You have to enable at least one of the available clients with the \ - `client-reqwest` or `client-ureq` features." -); From 68d625f584069f34f8016324adf4c036a0a556f9 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Thu, 29 Apr 2021 20:24:19 +0200 Subject: [PATCH 02/56] rspotify-http now working more or less --- Cargo.toml | 3 +- rspotify-http/Cargo.toml | 8 +- rspotify-http/src/lib.rs | 182 +- rspotify-http/src/reqwest.rs | 32 +- rspotify-http/src/ureq.rs | 27 +- rspotify-model/src/error.rs | 20 + rspotify-model/src/lib.rs | 3 +- src/client.rs | 2257 ----------------- src/client_creds.rs | 37 + src/code_auth.rs | 45 + src/code_auth_pkce.rs | 44 + src/endpoints/base.rs | 27 + src/endpoints/mod.rs | 152 ++ src/endpoints/oauth.rs | 10 + .../src => src/endpoints}/pagination/iter.rs | 0 .../src => src/endpoints}/pagination/mod.rs | 0 .../endpoints}/pagination/stream.rs | 0 src/lib.rs | 2209 +++++++++++++++- src/mod.rs | 21 + 19 files changed, 2629 insertions(+), 2448 deletions(-) create mode 100644 rspotify-model/src/error.rs delete mode 100644 src/client.rs create mode 100644 src/client_creds.rs create mode 100644 src/code_auth.rs create mode 100644 src/code_auth_pkce.rs create mode 100644 src/endpoints/base.rs create mode 100644 src/endpoints/mod.rs create mode 100644 src/endpoints/oauth.rs rename {rspotify-http/src => src/endpoints}/pagination/iter.rs (100%) rename {rspotify-http/src => src/endpoints}/pagination/mod.rs (100%) rename {rspotify-http/src => src/endpoints}/pagination/stream.rs (100%) create mode 100644 src/mod.rs diff --git a/Cargo.toml b/Cargo.toml index ef1469e9..f69504b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ rspotify-model = { path = "rspotify-model", version = "0.10.0" } rspotify-http = { path = "rspotify-http", version = "0.10.0" } ### Client ### +async-stream = { version = "0.3.0", optional = true } chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } derive_builder = "0.10.0" dotenv = { version = "0.15.0", optional = true } @@ -75,7 +76,7 @@ reqwest-native-tls-vendored = ["rspotify-http/reqwest-native-tls-vendored"] ureq-rustls-tls = ["rspotify-http/ureq-rustls-tls"] # Internal features for checking async or sync compilation -__async = ["futures"] +__async = ["futures", "async-stream"] __sync = [] [package.metadata.docs.rs] diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml index 156df948..f64b1d6f 100644 --- a/rspotify-http/Cargo.toml +++ b/rspotify-http/Cargo.toml @@ -11,15 +11,17 @@ keywords = ["spotify", "api"] edition = "2018" [dependencies] -async-stream = { version = "0.3.0", optional = true } +rspotify-model = { path = "../rspotify-model", version = "0.10.0" } + # Temporary until https://github.com/rust-lang/rfcs/issues/2739, for # `maybe_async`. async-trait = { version = "0.1.48", optional = true } base64 = "0.13.0" futures = { version = "0.3.8", optional = true } log = "0.4.11" -maybe-async = "0.2.1" +maybe-async = "0.2.4" reqwest = { version = "0.11.0", default-features = false, features = ["json", "socks"], optional = true } +serde_json = "1.0.57" thiserror = "1.0.20" ureq = { version = "2.0", default-features = false, features = ["json", "cookies"], optional = true } url = "2.1.1" @@ -45,7 +47,7 @@ reqwest-native-tls-vendored = ["reqwest/native-tls-vendored"] ureq-rustls-tls = ["ureq/tls"] # Internal features for checking async or sync compilation -__async = ["async-trait", "async-stream", "futures"] +__async = ["async-trait", "futures"] __sync = ["maybe-async/is_sync"] [package.metadata.docs.rs] diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index 0197cbb0..a54340e2 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -1,7 +1,6 @@ //! The HTTP client may vary depending on which one the user configures. This //! module contains the required logic to use different clients interchangeably. -pub mod pagination; #[cfg(feature = "client-reqwest")] mod reqwest; #[cfg(feature = "client-ureq")] @@ -23,26 +22,23 @@ compile_error!( `client-reqwest` or `client-ureq` features." ); -use crate::client::{ClientResult, Spotify}; - use std::collections::HashMap; use std::fmt; use maybe_async::maybe_async; use serde_json::Value; +use rspotify_model::ApiError; #[cfg(feature = "client-reqwest")] -pub use self::reqwest::ReqwestClient as HTTPClient; +pub use self::reqwest::ReqwestClient as Client; #[cfg(feature = "client-ureq")] -pub use self::ureq::UreqClient as HTTPClient; +pub use self::ureq::UreqClient as Client; pub type Headers = HashMap; pub type Query<'a> = HashMap<&'a str, &'a str>; pub type Form<'a> = HashMap<&'a str, &'a str>; pub mod headers { - use crate::oauth2::Token; - // Common headers as constants pub const CLIENT_ID: &str = "client_id"; pub const CODE: &str = "code"; @@ -57,25 +53,31 @@ pub mod headers { pub const SCOPE: &str = "scope"; pub const SHOW_DIALOG: &str = "show_dialog"; pub const STATE: &str = "state"; +} - /// Generates an HTTP token authorization header with proper formatting - pub fn bearer_auth(tok: &Token) -> (String, String) { - let auth = "authorization".to_owned(); - let value = format!("Bearer {}", tok.access_token); +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("request unauthorized")] + Unauthorized, - (auth, value) - } + #[error("exceeded request limit")] + RateLimited(Option), - /// Generates an HTTP basic authorization header with proper formatting - pub fn basic_auth(user: &str, password: &str) -> (String, String) { - let auth = "authorization".to_owned(); - let value = format!("{}:{}", user, password); - let value = format!("Basic {}", base64::encode(value)); + #[error("request error: {0}")] + Request(String), - (auth, value) - } + #[error("status code {0}: {1}")] + StatusCode(u16, String), + + #[error("spotify error: {0}")] + Api(#[from] ApiError), + + #[error("input/output error: {0}")] + Io(#[from] std::io::Error), } +pub type Result = std::result::Result; + /// This trait represents the interface to be implemented for an HTTP client, /// which is kept separate from the Spotify client for cleaner code. Thus, it /// also requires other basic traits that are needed for the Spotify client. @@ -87,172 +89,44 @@ pub mod headers { /// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make /// much sense). #[maybe_async] -pub trait BaseHttpClient: Default + Clone + fmt::Debug { +pub trait BaseClient: Default + Clone + fmt::Debug { // This internal function should always be given an object value in JSON. async fn get( &self, url: &str, headers: Option<&Headers>, payload: &Query, - ) -> ClientResult; + ) -> Result; async fn post( &self, url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult; + ) -> Result; async fn post_form<'a>( &self, url: &str, headers: Option<&Headers>, payload: &Form<'a>, - ) -> ClientResult; + ) -> Result; async fn put( &self, url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult; + ) -> Result; async fn delete( &self, url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult; + ) -> Result; } -/// HTTP-related methods for the Spotify client. It wraps the basic HTTP client -/// with features needed of higher level. -/// -/// The Spotify client has two different wrappers to perform requests: -/// -/// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only -/// append the configured Spotify API URL to the relative URL provided so that -/// it's not forgotten. They're used in the authentication process to request -/// an access token and similars. -/// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, -/// `endpoint_delete`. These append the authentication headers for endpoint -/// requests to reduce the code needed for endpoints and make them as concise -/// as possible. -impl Spotify { - /// If it's a relative URL like "me", the prefix is appended to it. - /// Otherwise, the same URL is returned. - fn endpoint_url(&self, url: &str) -> String { - // Using the client's prefix in case it's a relative route. - if !url.starts_with("http") { - self.prefix.clone() + url - } else { - url.to_string() - } - } - - /// The headers required for authenticated requests to the API - fn auth_headers(&self) -> ClientResult { - let mut auth = Headers::new(); - let (key, val) = headers::bearer_auth(self.get_token()?); - auth.insert(key, val); - - Ok(auth) - } - - #[inline] - #[maybe_async] - pub(crate) async fn get( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Query<'_>, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.get(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn post( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.post(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn post_form( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Form<'_>, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.post_form(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn put( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.put(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn delete( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.delete(&url, headers, payload).await - } - - /// The wrapper for the endpoints, which also includes the required - /// autentication. - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_get( - &self, - url: &str, - payload: &Query<'_>, - ) -> ClientResult { - let headers = self.auth_headers()?; - self.get(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.post(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.put(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.delete(url, Some(&headers), payload).await - } -} #[cfg(test)] mod test { diff --git a/rspotify-http/src/reqwest.rs b/rspotify-http/src/reqwest.rs index cef391ae..bbef9e73 100644 --- a/rspotify-http/src/reqwest.rs +++ b/rspotify-http/src/reqwest.rs @@ -1,16 +1,16 @@ //! The client implementation for the reqwest HTTP client, which is async by //! default. -use maybe_async::async_impl; -use reqwest::{Method, RequestBuilder, StatusCode}; -use serde_json::Value; +use super::{BaseClient, Error, Result, Form, Headers, Query}; use std::convert::TryInto; -use super::{BaseHttpClient, Form, Headers, Query}; -use crate::client::{ApiError, ClientError, ClientResult}; +use maybe_async::async_impl; +use reqwest::{Method, RequestBuilder, StatusCode}; +use rspotify_model::ApiError; +use serde_json::Value; -impl ClientError { +impl Error { pub async fn from_response(response: reqwest::Response) -> Self { match response.status() { StatusCode::UNAUTHORIZED => Self::Unauthorized, @@ -31,13 +31,13 @@ impl ClientError { } } -impl From for ClientError { +impl From for Error { fn from(err: reqwest::Error) -> Self { Self::Request(err.to_string()) } } -impl From for ClientError { +impl From for Error { fn from(code: reqwest::StatusCode) -> Self { Self::StatusCode( code.as_u16(), @@ -59,7 +59,7 @@ impl ReqwestClient { url: &str, headers: Option<&Headers>, add_data: D, - ) -> ClientResult + ) -> Result where D: Fn(RequestBuilder) -> RequestBuilder, { @@ -88,20 +88,20 @@ impl ReqwestClient { if response.status().is_success() { response.text().await.map_err(Into::into) } else { - Err(ClientError::from_response(response).await) + Err(Error::from_response(response).await) } } } #[async_impl] -impl BaseHttpClient for ReqwestClient { +impl BaseClient for ReqwestClient { #[inline] async fn get( &self, url: &str, headers: Option<&Headers>, payload: &Query, - ) -> ClientResult { + ) -> Result { self.request(Method::GET, url, headers, |req| req.query(payload)) .await } @@ -112,7 +112,7 @@ impl BaseHttpClient for ReqwestClient { url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult { + ) -> Result { self.request(Method::POST, url, headers, |req| req.json(payload)) .await } @@ -123,7 +123,7 @@ impl BaseHttpClient for ReqwestClient { url: &str, headers: Option<&Headers>, payload: &Form<'a>, - ) -> ClientResult { + ) -> Result { self.request(Method::POST, url, headers, |req| req.form(payload)) .await } @@ -134,7 +134,7 @@ impl BaseHttpClient for ReqwestClient { url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult { + ) -> Result { self.request(Method::PUT, url, headers, |req| req.json(payload)) .await } @@ -145,7 +145,7 @@ impl BaseHttpClient for ReqwestClient { url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult { + ) -> Result { self.request(Method::DELETE, url, headers, |req| req.json(payload)) .await } diff --git a/rspotify-http/src/ureq.rs b/rspotify-http/src/ureq.rs index e5da4b36..c84dbf8a 100644 --- a/rspotify-http/src/ureq.rs +++ b/rspotify-http/src/ureq.rs @@ -1,15 +1,14 @@ //! The client implementation for the ureq HTTP client, which is blocking. -use super::{BaseHttpClient, Form, Headers, Query}; -use crate::client::{ClientError, ClientResult}; +use super::{BaseClient, Form, Headers, Query, Result, Error}; use maybe_async::sync_impl; use serde_json::Value; use ureq::{Request, Response}; -impl ClientError { +impl Error { pub fn from_response(r: ureq::Response) -> Self { - ClientError::StatusCode(r.status(), r.status_text().to_string()) + Error::StatusCode(r.status(), r.status_text().to_string()) } } @@ -30,9 +29,9 @@ impl UreqClient { mut request: Request, headers: Option<&Headers>, send_request: D, - ) -> ClientResult + ) -> Result where - D: Fn(Request) -> Result, + D: Fn(Request) -> std::result::Result, { // Setting the headers, which will be the token auth if unspecified. if let Some(headers) = headers { @@ -46,17 +45,17 @@ impl UreqClient { // Successful request Ok(response) => response.into_string().map_err(Into::into), // HTTP status error - Err(ureq::Error::Status(_, response)) => Err(ClientError::from_response(response)), + Err(ureq::Error::Status(_, response)) => Err(Error::from_response(response)), // Some kind of IO/transport error - Err(err) => Err(ClientError::Request(err.to_string())), + Err(err) => Err(Error::Request(err.to_string())), } } } #[sync_impl] -impl BaseHttpClient for UreqClient { +impl BaseClient for UreqClient { #[inline] - fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> ClientResult { + fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result { let request = ureq::get(url); let sender = |mut req: Request| { for (key, val) in payload.iter() { @@ -68,7 +67,7 @@ impl BaseHttpClient for UreqClient { } #[inline] - fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> ClientResult { + fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result { let request = ureq::post(url); let sender = |req: Request| req.send_json(payload.clone()); self.request(request, headers, sender) @@ -80,7 +79,7 @@ impl BaseHttpClient for UreqClient { url: &str, headers: Option<&Headers>, payload: &Form<'a>, - ) -> ClientResult { + ) -> Result { let request = ureq::post(url); let sender = |req: Request| { let payload = payload @@ -95,7 +94,7 @@ impl BaseHttpClient for UreqClient { } #[inline] - fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> ClientResult { + fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result { let request = ureq::put(url); let sender = |req: Request| req.send_json(payload.clone()); self.request(request, headers, sender) @@ -107,7 +106,7 @@ impl BaseHttpClient for UreqClient { url: &str, headers: Option<&Headers>, payload: &Value, - ) -> ClientResult { + ) -> Result { let request = ureq::delete(url); let sender = |req: Request| req.send_json(payload.clone()); self.request(request, headers, sender) diff --git a/rspotify-model/src/error.rs b/rspotify-model/src/error.rs new file mode 100644 index 00000000..d64175bc --- /dev/null +++ b/rspotify-model/src/error.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; + +/// Matches errors that are returned from the Spotfiy +/// API as part of the JSON response object. +#[derive(Debug, thiserror::Error, Deserialize)] +pub enum ApiError { + /// See [Error Object](https://developer.spotify.com/documentation/web-api/reference/#object-errorobject) + #[error("{status}: {message}")] + #[serde(alias = "error")] + Regular { status: u16, message: String }, + + /// See [Play Error Object](https://developer.spotify.com/documentation/web-api/reference/#object-playererrorobject) + #[error("{status} ({reason}): {message}")] + #[serde(alias = "error")] + Player { + status: u16, + message: String, + reason: String, + }, +} diff --git a/rspotify-model/src/lib.rs b/rspotify-model/src/lib.rs index eedfcb52..f666ba88 100644 --- a/rspotify-model/src/lib.rs +++ b/rspotify-model/src/lib.rs @@ -9,6 +9,7 @@ pub mod category; pub mod context; pub mod device; pub mod enums; +pub mod error; pub mod idtypes; pub mod image; pub mod offset; @@ -238,7 +239,7 @@ pub use idtypes::{ UserIdBuf, }; pub use { - album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, image::*, + album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, error::*, image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, user::*, }; diff --git a/src/client.rs b/src/client.rs deleted file mode 100644 index da1c0461..00000000 --- a/src/client.rs +++ /dev/null @@ -1,2257 +0,0 @@ -//! Client to Spotify API endpoint - -use derive_builder::Builder; -use log::error; -use maybe_async::maybe_async; -use serde::Deserialize; -use serde_json::{json, map::Map, Value}; -use thiserror::Error; - -use std::collections::HashMap; -use std::path::PathBuf; -use std::time; - -use crate::http::{HTTPClient, Query}; -use crate::macros::{build_json, build_map}; -use crate::model::{ - idtypes::{IdType, PlayContextIdType}, - *, -}; -use crate::oauth2::{Credentials, OAuth, Token}; -use crate::pagination::{paginate, Paginator}; - -/// Possible errors returned from the `rspotify` client. -#[derive(Debug, Error)] -pub enum ClientError { - /// Raised when the authentication isn't configured properly. - #[error("invalid client authentication: {0}")] - InvalidAuth(String), - - #[error("request unauthorized")] - Unauthorized, - - #[error("exceeded request limit")] - RateLimited(Option), - - #[error("request error: {0}")] - Request(String), - - #[error("status code {0}: {1}")] - StatusCode(u16, String), - - #[error("spotify error: {0}")] - Api(#[from] ApiError), - - #[error("json parse error: {0}")] - ParseJson(#[from] serde_json::Error), - - #[error("url parse error: {0}")] - ParseUrl(#[from] url::ParseError), - - #[error("input/output error: {0}")] - Io(#[from] std::io::Error), - - #[cfg(feature = "cli")] - #[error("cli error: {0}")] - Cli(String), - - #[error("cache file error: {0}")] - CacheFile(String), -} - -pub type ClientResult = Result; - -/// Matches errors that are returned from the Spotfiy -/// API as part of the JSON response object. -#[derive(Debug, Error, Deserialize)] -pub enum ApiError { - /// See [Error Object](https://developer.spotify.com/documentation/web-api/reference/#object-errorobject) - #[error("{status}: {message}")] - #[serde(alias = "error")] - Regular { status: u16, message: String }, - - /// See [Play Error Object](https://developer.spotify.com/documentation/web-api/reference/#object-playererrorobject) - #[error("{status} ({reason}): {message}")] - #[serde(alias = "error")] - Player { - status: u16, - message: String, - reason: String, - }, -} - -pub const DEFAULT_API_PREFIX: &str = "https://api.spotify.com/v1/"; -pub const DEFAULT_CACHE_PATH: &str = ".spotify_token_cache.json"; -pub const DEFAULT_PAGINATION_CHUNKS: u32 = 50; - -/// Spotify API object -#[derive(Builder, Debug, Clone)] -pub struct Spotify { - /// Internal member to perform requests to the Spotify API. - #[builder(setter(skip))] - pub(in crate) http: HTTPClient, - - /// The access token information required for requests to the Spotify API. - #[builder(setter(strip_option), default)] - pub token: Option, - - /// The credentials needed for obtaining a new access token, for requests. - /// without OAuth authentication. - #[builder(setter(strip_option), default)] - pub credentials: Option, - - /// The OAuth information required for obtaining a new access token, for - /// requests with OAuth authentication. `credentials` also needs to be - /// set up. - #[builder(setter(strip_option), default)] - pub oauth: Option, - - /// The Spotify API prefix, [`DEFAULT_API_PREFIX`] by default. - #[builder(setter(into), default = "String::from(DEFAULT_API_PREFIX)")] - pub prefix: String, - - /// The cache file path, in case it's used. By default it's - /// [`DEFAULT_CACHE_PATH`] - #[builder(default = "PathBuf::from(DEFAULT_CACHE_PATH)")] - pub cache_path: PathBuf, - - /// The pagination chunk size used when performing automatically paginated - /// requests, like [`Spotify::artist_albums`]. This means that a request - /// will be performed every `pagination_chunks` items. By default this is - /// [`DEFAULT_PAGINATION_CHUNKS`]. - /// - /// Note that most endpoints set a maximum to the number of items per - /// request, which most times is 50. - #[builder(default = "DEFAULT_PAGINATION_CHUNKS")] - pub pagination_chunks: u32, -} - -// Endpoint-related methods for the client. -impl Spotify { - /// Returns the access token, or an error in case it's not configured. - pub(in crate) fn get_token(&self) -> ClientResult<&Token> { - self.token - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no access token configured".to_string())) - } - - /// Returns the credentials, or an error in case it's not configured. - pub(in crate) fn get_creds(&self) -> ClientResult<&Credentials> { - self.credentials - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no credentials configured".to_string())) - } - - /// Returns the oauth information, or an error in case it's not configured. - pub(in crate) fn get_oauth(&self) -> ClientResult<&OAuth> { - self.oauth - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no oauth configured".to_string())) - } - - /// Converts a JSON response from Spotify into its model. - fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { - serde_json::from_str::(input).map_err(Into::into) - } - - /// Append device ID to an API path. - fn append_device_id(&self, path: &str, device_id: Option<&str>) -> String { - let mut new_path = path.to_string(); - if let Some(_device_id) = device_id { - if path.contains('?') { - new_path.push_str(&format!("&device_id={}", _device_id)); - } else { - new_path.push_str(&format!("?device_id={}", _device_id)); - } - } - new_path - } - - /// Returns a single track given the track's ID, URI or URL. - /// - /// Parameters: - /// - track_id - a spotify URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track) - #[maybe_async] - pub async fn track(&self, track_id: &TrackId) -> ClientResult { - let url = format!("tracks/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of tracks given a list of track IDs, URIs, or URLs. - /// - /// Parameters: - /// - track_ids - a list of spotify URIs, URLs or IDs - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) - #[maybe_async] - pub async fn tracks<'a>( - &self, - track_ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(track_ids); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("tracks/?ids={}", ids); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) - } - - /// Returns a single artist given the artist's ID, URI or URL. - /// - /// Parameters: - /// - artist_id - an artist ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artist) - #[maybe_async] - pub async fn artist(&self, artist_id: &ArtistId) -> ClientResult { - let url = format!("artists/{}", artist_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of artists given the artist IDs, URIs, or URLs. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs, URIs or URLs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) - #[maybe_async] - pub async fn artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(artist_ids); - let url = format!("artists/?ids={}", ids); - let result = self.endpoint_get(&url, &Query::new()).await?; - - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Get Spotify catalog information about an artist's albums. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - album_type - 'album', 'single', 'appears_on', 'compilation' - /// - market - limit the response to one particular country. - /// - limit - the number of albums to return - /// - offset - the index of the first album to return - /// - /// See [`Spotify::artist_albums_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) - pub fn artist_albums<'a>( - &'a self, - artist_id: &'a ArtistId, - album_type: Option<&'a AlbumType>, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::artist_albums`]. - #[maybe_async] - pub async fn artist_albums_manual( - &self, - artist_id: &ArtistId, - album_type: Option<&AlbumType>, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit: Option = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "album_type": album_type.map(|x| x.as_ref()), - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("artists/{}/albums", artist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information about an artist's top 10 tracks by - /// country. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - market - limit the response to one particular country. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-top-tracks) - #[maybe_async] - pub async fn artist_top_tracks( - &self, - artist_id: &ArtistId, - market: Market, - ) -> ClientResult> { - let params = build_map! { - "market": market.as_ref() - }; - - let url = format!("artists/{}/top-tracks", artist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) - } - - /// Get Spotify catalog information about artists similar to an identified - /// artist. Similarity is based on analysis of the Spotify community's - /// listening history. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-related-artists) - #[maybe_async] - pub async fn artist_related_artists( - &self, - artist_id: &ArtistId, - ) -> ClientResult> { - let url = format!("artists/{}/related-artists", artist_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Returns a single album given the album's ID, URIs or URL. - /// - /// Parameters: - /// - album_id - the album ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album) - #[maybe_async] - pub async fn album(&self, album_id: &AlbumId) -> ClientResult { - let url = format!("albums/{}", album_id.id()); - - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of albums given the album IDs, URIs, or URLs. - /// - /// Parameters: - /// - albums_ids - a list of album IDs, URIs or URLs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) - #[maybe_async] - pub async fn albums<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(album_ids); - let url = format!("albums/?ids={}", ids); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result).map(|x| x.albums) - } - - /// Search for an Item. Get Spotify catalog information about artists, - /// albums, tracks or playlists that match a keyword string. - /// - /// Parameters: - /// - q - the search query - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - type - the type of item to return. One of 'artist', 'album', 'track', - /// 'playlist', 'show' or 'episode' - /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - include_external: Optional.Possible values: audio. If - /// include_external=audio is specified the response will include any - /// relevant audio content that is hosted externally. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#category-search) - #[maybe_async] - pub async fn search( - &self, - q: &str, - _type: SearchType, - market: Option<&Market>, - include_external: Option<&IncludeExternal>, - limit: Option, - offset: Option, - ) -> ClientResult { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - "q": q, - "type": _type.as_ref(), - optional "market": market.map(|x| x.as_ref()), - optional "include_external": include_external.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("search", ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information about an album's tracks. - /// - /// Parameters: - /// - album_id - the album ID, URI or URL - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::album_track_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) - pub fn album_track<'a>( - &'a self, - album_id: &'a AlbumId, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::album_track`]. - #[maybe_async] - pub async fn album_track_manual( - &self, - album_id: &AlbumId, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("albums/{}/tracks", album_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Gets basic profile information about a Spotify User. - /// - /// Parameters: - /// - user - the id of the usr - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-profile) - #[maybe_async] - pub async fn user(&self, user_id: &UserId) -> ClientResult { - let url = format!("users/{}", user_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get full details about Spotify playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlist) - #[maybe_async] - pub async fn playlist( - &self, - playlist_id: &PlaylistId, - fields: Option<&str>, - market: Option<&Market>, - ) -> ClientResult { - let params = build_map! { - optional "fields": fields, - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("playlists/{}", playlist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get current user playlists without required getting his profile. - /// - /// Parameters: - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::current_user_playlists_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - pub fn current_user_playlists(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_playlists`]. - #[maybe_async] - pub async fn current_user_playlists_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/playlists", ¶ms).await?; - self.convert_result(&result) - } - - /// Gets playlists of a user. - /// - /// Parameters: - /// - user_id - the id of the usr - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::user_playlists_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - pub fn user_playlists<'a>( - &'a self, - user_id: &'a UserId, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::user_playlists`]. - #[maybe_async] - pub async fn user_playlists_manual( - &self, - user_id: &UserId, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("users/{}/playlists", user_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Gets playlist of a user. - /// - /// Parameters: - /// - user_id - the id of the user - /// - playlist_id - the id of the playlist - /// - fields - which fields to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - #[maybe_async] - pub async fn user_playlist( - &self, - user_id: &UserId, - playlist_id: Option<&PlaylistId>, - fields: Option<&str>, - ) -> ClientResult { - let params = build_map! { - optional "fields": fields, - }; - - let url = match playlist_id { - Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), - None => format!("users/{}/starred", user_id.id()), - }; - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get full details of the tracks of a playlist owned by a user. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - fields - which fields to return - /// - limit - the maximum number of tracks to return - /// - offset - the index of the first track to return - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// See [`Spotify::playlist_tracks`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) - pub fn playlist_tracks<'a>( - &'a self, - playlist_id: &'a PlaylistId, - fields: Option<&'a str>, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::playlist_tracks`]. - #[maybe_async] - pub async fn playlist_tracks_manual( - &self, - playlist_id: &PlaylistId, - fields: Option<&str>, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "fields": fields, - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Creates a playlist for a user. - /// - /// Parameters: - /// - user_id - the id of the user - /// - name - the name of the playlist - /// - public - is the created playlist public - /// - description - the description of the playlist - /// - collaborative - if the playlist will be collaborative - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist) - #[maybe_async] - pub async fn user_playlist_create( - &self, - user_id: &UserId, - name: &str, - public: Option, - collaborative: Option, - description: Option<&str>, - ) -> ClientResult { - let params = build_json! { - "name": name, - optional "public": public, - optional "collaborative": collaborative, - optional "description": description, - }; - - let url = format!("users/{}/playlists", user_id.id()); - let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Changes a playlist's name and/or public/private state. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - name - optional name of the playlist - /// - public - optional is the playlist public - /// - collaborative - optional is the playlist collaborative - /// - description - optional description of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-change-playlist-details) - #[maybe_async] - pub async fn playlist_change_detail( - &self, - playlist_id: &str, - name: Option<&str>, - public: Option, - description: Option<&str>, - collaborative: Option, - ) -> ClientResult { - let params = build_json! { - optional "name": name, - optional "public": public, - optional "collaborative": collaborative, - optional "description": description, - }; - - let url = format!("playlists/{}", playlist_id); - self.endpoint_put(&url, ¶ms).await - } - - /// Unfollows (deletes) a playlist for a user. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-playlist) - #[maybe_async] - pub async fn playlist_unfollow(&self, playlist_id: &str) -> ClientResult { - let url = format!("playlists/{}/followers", playlist_id); - self.endpoint_delete(&url, &json!({})).await - } - - /// Adds tracks to a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - track_ids - a list of track URIs, URLs or IDs - /// - position - the position to add the tracks - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) - #[maybe_async] - pub async fn playlist_add_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - position: Option, - ) -> ClientResult { - let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); - let params = build_json! { - "uris": uris, - "position": position, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Replace all tracks in a playlist - /// - /// Parameters: - /// - user - the id of the user - /// - playlist_id - the id of the playlist - /// - tracks - the list of track ids to add to the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - #[maybe_async] - pub async fn playlist_replace_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); - let params = build_json! { - "uris": uris - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Reorder tracks in a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - uris - a list of Spotify URIs to replace or clear - /// - range_start - the position of the first track to be reordered - /// - insert_before - the position where the tracks should be inserted - /// - range_length - optional the number of tracks to be reordered (default: - /// 1) - /// - snapshot_id - optional playlist's snapshot ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - #[maybe_async] - pub async fn playlist_reorder_tracks( - &self, - playlist_id: &PlaylistId, - uris: Option<&[&Id]>, - range_start: Option, - insert_before: Option, - range_length: Option, - snapshot_id: Option<&str>, - ) -> ClientResult { - let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); - let params = build_json! { - "playlist_id": playlist_id, - optional "uris": uris, - optional "range_start": range_start, - optional "insert_before": insert_before, - optional "range_length": range_length, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_put(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Removes all occurrences of the given tracks from the given playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - track_ids - the list of track ids to add to the playlist - /// - snapshot_id - optional id of the playlist snapshot - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - #[maybe_async] - pub async fn playlist_remove_all_occurrences_of_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - snapshot_id: Option<&str>, - ) -> ClientResult { - let tracks = track_ids - .into_iter() - .map(|id| { - let mut map = Map::with_capacity(1); - map.insert("uri".to_owned(), id.uri().into()); - map - }) - .collect::>(); - - let params = build_json! { - "tracks": tracks, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Removes specfic occurrences of the given tracks from the given playlist. - /// - /// Parameters: - /// - playlist_id: the id of the playlist - /// - tracks: an array of map containing Spotify URIs of the tracks to - /// remove with their current positions in the playlist. For example: - /// - /// ```json - /// { - /// "tracks":[ - /// { - /// "uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh", - /// "positions":[ - /// 0, - /// 3 - /// ] - /// }, - /// { - /// "uri":"spotify:track:1301WleyT98MSxVHPZCA6M", - /// "positions":[ - /// 7 - /// ] - /// } - /// ] - /// } - /// ``` - /// - snapshot_id: optional id of the playlist snapshot - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - #[maybe_async] - pub async fn playlist_remove_specific_occurrences_of_tracks( - &self, - playlist_id: &PlaylistId, - tracks: Vec>, - snapshot_id: Option<&str>, - ) -> ClientResult { - let tracks = tracks - .into_iter() - .map(|track| { - let mut map = Map::new(); - map.insert("uri".to_owned(), track.id.uri().into()); - map.insert("positions".to_owned(), track.positions.into()); - map - }) - .collect::>(); - - let params = build_json! { - "tracks": tracks, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Add the current authenticated user as a follower of a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-playlist) - #[maybe_async] - pub async fn playlist_follow( - &self, - playlist_id: &PlaylistId, - public: Option, - ) -> ClientResult<()> { - let url = format!("playlists/{}/followers", playlist_id.id()); - - let params = build_json! { - optional "public": public, - }; - - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Check to see if the given users are following the given playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - user_ids - the ids of the users that you want to - /// check to see if they follow the playlist. Maximum: 5 ids. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) - #[maybe_async] - pub async fn playlist_check_follow( - &self, - playlist_id: &PlaylistId, - user_ids: &[&UserId], - ) -> ClientResult> { - if user_ids.len() > 5 { - error!("The maximum length of user ids is limited to 5 :-)"); - } - let url = format!( - "playlists/{}/followers/contains?ids={}", - playlist_id.id(), - user_ids - .iter() - .map(|id| id.id()) - .collect::>() - .join(","), - ); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get detailed profile information about the current user. - /// An alias for the 'current_user' method. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) - #[maybe_async] - pub async fn me(&self) -> ClientResult { - let result = self.endpoint_get("me/", &Query::new()).await?; - self.convert_result(&result) - } - - /// Get detailed profile information about the current user. - /// An alias for the 'me' method. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) - #[maybe_async] - pub async fn current_user(&self) -> ClientResult { - self.me().await - } - - /// Get information about the current users currently playing track. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) - #[maybe_async] - pub async fn current_user_playing_track( - &self, - ) -> ClientResult> { - let result = self - .get("me/player/currently-playing", None, &Query::new()) - .await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) - } - } - - /// Gets a list of the albums saved in the current authorized user's - /// "Your Music" library - /// - /// Parameters: - /// - limit - the number of albums to return - /// - offset - the index of the first album to return - /// - market - Provide this parameter if you want to apply Track Relinking. - /// - /// See [`Spotify::current_user_saved_albums`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - pub fn current_user_saved_albums(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of - /// [`Spotify::current_user_saved_albums`]. - #[maybe_async] - pub async fn current_user_saved_albums_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/albums", ¶ms).await?; - self.convert_result(&result) - } - - /// Get a list of the songs saved in the current Spotify user's "Your Music" - /// library. - /// - /// Parameters: - /// - limit - the number of tracks to return - /// - offset - the index of the first track to return - /// - market - Provide this parameter if you want to apply Track Relinking. - /// - /// See [`Spotify::current_user_saved_tracks_manual`] for a manually - /// paginated version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - pub fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of - /// [`Spotify::current_user_saved_tracks`]. - #[maybe_async] - pub async fn current_user_saved_tracks_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/tracks", ¶ms).await?; - self.convert_result(&result) - } - - /// Gets a list of the artists followed by the current authorized user. - /// - /// Parameters: - /// - after - the last artist ID retrieved from the previous request - /// - limit - the number of tracks to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-followed) - #[maybe_async] - pub async fn current_user_followed_artists( - &self, - after: Option<&str>, - limit: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let params = build_map! { - "r#type": Type::Artist.as_ref(), - optional "after": after, - optional "limit": limit.as_deref(), - }; - - let result = self.endpoint_get("me/following", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Remove one or more tracks from the current user's "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) - #[maybe_async] - pub async fn current_user_saved_tracks_delete<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/tracks/?ids={}", join_ids(track_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check if one or more tracks is already saved in the current Spotify - /// user’s "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) - #[maybe_async] - pub async fn current_user_saved_tracks_contains<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Save one or more tracks to the current user's "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) - #[maybe_async] - pub async fn current_user_saved_tracks_add<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/tracks/?ids={}", join_ids(track_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Get the current user's top artists. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - offset - the index of the first entity to return - /// - time_range - Over what time frame are the affinities computed - /// - /// See [`Spotify::current_user_top_artists_manual`] for a manually - /// paginated version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - pub fn current_user_top_artists<'a>( - &'a self, - time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_top_artists`]. - #[maybe_async] - pub async fn current_user_top_artists_manual( - &self, - time_range: Option<&TimeRange>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "time_range": time_range.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; - self.convert_result(&result) - } - - /// Get the current user's top tracks. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - offset - the index of the first entity to return - /// - time_range - Over what time frame are the affinities computed - /// - /// See [`Spotify::current_user_top_tracks_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - pub fn current_user_top_tracks<'a>( - &'a self, - time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_top_tracks`]. - #[maybe_async] - pub async fn current_user_top_tracks_manual( - &self, - time_range: Option<&TimeRange>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "time_range": time_range.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/top/tracks", ¶ms).await?; - self.convert_result(&result) - } - - /// Get the current user's recently played tracks. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track) - #[maybe_async] - pub async fn current_user_recently_played( - &self, - limit: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - }; - - let result = self - .endpoint_get("me/player/recently-played", ¶ms) - .await?; - self.convert_result(&result) - } - - /// Add one or more albums to the current user's "Your Music" library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) - #[maybe_async] - pub async fn current_user_saved_albums_add<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/albums/?ids={}", join_ids(album_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Remove one or more albums from the current user's "Your Music" library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) - #[maybe_async] - pub async fn current_user_saved_albums_delete<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/albums/?ids={}", join_ids(album_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check if one or more albums is already saved in the current Spotify - /// user’s "Your Music” library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) - #[maybe_async] - pub async fn current_user_saved_albums_contains<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Follow one or more artists. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - #[maybe_async] - pub async fn user_follow_artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Unfollow one or more artists. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - #[maybe_async] - pub async fn user_unfollow_artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check to see if the current user is following one or more artists or - /// other Spotify users. - /// - /// Parameters: - /// - artist_ids - the ids of the users that you want to - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) - #[maybe_async] - pub async fn user_artist_check_follow<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!( - "me/following/contains?type=artist&ids={}", - join_ids(artist_ids) - ); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Follow one or more users. - /// - /// Parameters: - /// - user_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - #[maybe_async] - pub async fn user_follow_users<'a>( - &self, - user_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Unfollow one or more users. - /// - /// Parameters: - /// - user_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - #[maybe_async] - pub async fn user_unfollow_users<'a>( - &self, - user_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Get a list of Spotify featured playlists. - /// - /// Parameters: - /// - locale - The desired language, consisting of a lowercase ISO 639 - /// language code and an uppercase ISO 3166-1 alpha-2 country code, - /// joined by an underscore. - /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use - /// this parameter to specify the user's local time to get results - /// tailored for that specific date and time in the day - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 - /// (the first object). Use with limit to get the next set of - /// items. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-featured-playlists) - #[maybe_async] - pub async fn featured_playlists( - &self, - locale: Option<&str>, - country: Option<&Market>, - timestamp: Option>, - limit: Option, - offset: Option, - ) -> ClientResult { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let timestamp = timestamp.map(|x| x.to_rfc3339()); - let params = build_map! { - optional "locale": locale, - optional "country": country.map(|x| x.as_ref()), - optional "timestamp": timestamp.as_deref(), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self - .endpoint_get("browse/featured-playlists", ¶ms) - .await?; - self.convert_result(&result) - } - - /// Get a list of new album releases featured in Spotify. - /// - /// Parameters: - /// - country - An ISO 3166-1 alpha-2 country code or string from_token. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::new_releases_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) - pub fn new_releases<'a>( - &'a self, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::new_releases`]. - #[maybe_async] - pub async fn new_releases_manual( - &self, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("browse/new-releases", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.albums) - } - - /// Get a list of new album releases featured in Spotify - /// - /// Parameters: - /// - country - An ISO 3166-1 alpha-2 country code or string from_token. - /// - locale - The desired language, consisting of an ISO 639 language code - /// and an ISO 3166-1 alpha-2 country code, joined by an underscore. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::categories_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) - pub fn categories<'a>( - &'a self, - locale: Option<&'a str>, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::categories`]. - #[maybe_async] - pub async fn categories_manual( - &self, - locale: Option<&str>, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "locale": locale, - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - let result = self.endpoint_get("browse/categories", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.categories) - } - - /// Get a list of playlists in a category in Spotify - /// - /// Parameters: - /// - category_id - The category id to get playlists from. - /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::category_playlists_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) - pub fn category_playlists<'a>( - &'a self, - category_id: &'a str, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::category_playlists`]. - #[maybe_async] - pub async fn category_playlists_manual( - &self, - category_id: &str, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("browse/categories/{}/playlists", category_id); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.playlists) - } - - /// Get Recommendations Based on Seeds - /// - /// Parameters: - /// - seed_artists - a list of artist IDs, URIs or URLs - /// - seed_tracks - a list of artist IDs, URIs or URLs - /// - seed_genres - a list of genre names. Available genres for - /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. If provided, all - /// results will be playable in this country. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 100 - /// - min/max/target_ - For the tuneable track attributes listed - /// in the documentation, these values provide filters and targeting on - /// results. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recommendations) - #[maybe_async] - pub async fn recommendations( - &self, - payload: &Map, - seed_artists: Option>, - seed_genres: Option>, - seed_tracks: Option>, - limit: Option, - market: Option<&Market>, - ) -> ClientResult { - let seed_artists = seed_artists.map(join_ids); - let seed_genres = seed_genres.map(|x| x.join(",")); - let seed_tracks = seed_tracks.map(join_ids); - let limit = limit.map(|x| x.to_string()); - let mut params = build_map! { - optional "seed_artists": seed_artists.as_ref(), - optional "seed_genres": seed_genres.as_ref(), - optional "seed_tracks": seed_tracks.as_ref(), - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_ref(), - }; - - // TODO: this probably can be improved. - let attributes = [ - "acousticness", - "danceability", - "duration_ms", - "energy", - "instrumentalness", - "key", - "liveness", - "loudness", - "mode", - "popularity", - "speechiness", - "tempo", - "time_signature", - "valence", - ]; - let mut map_to_hold_owned_value = HashMap::new(); - let prefixes = ["min", "max", "target"]; - for attribute in attributes.iter() { - for prefix in prefixes.iter() { - let param = format!("{}_{}", prefix, attribute); - if let Some(value) = payload.get(¶m) { - // TODO: not sure if this `to_string` is what we want. It - // might add quotes to the strings. - map_to_hold_owned_value.insert(param, value.to_string()); - } - } - } - - for (ref key, ref value) in &map_to_hold_owned_value { - params.insert(key, value); - } - - let result = self.endpoint_get("recommendations", ¶ms).await?; - self.convert_result(&result) - } - - /// Get audio features for a track - /// - /// Parameters: - /// - track - track URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-features) - #[maybe_async] - pub async fn track_features(&self, track_id: &TrackId) -> ClientResult { - let url = format!("audio-features/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get Audio Features for Several Tracks - /// - /// Parameters: - /// - tracks a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) - #[maybe_async] - pub async fn tracks_features<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult>> { - let url = format!("audio-features/?ids={}", join_ids(track_ids)); - - let result = self.endpoint_get(&url, &Query::new()).await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result::>(&result) - .map(|option_payload| option_payload.map(|x| x.audio_features)) - } - } - - /// Get Audio Analysis for a Track - /// - /// Parameters: - /// - track_id - a track URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-analysis) - #[maybe_async] - pub async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { - let url = format!("audio-analysis/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get a User’s Available Devices - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-users-available-devices) - #[maybe_async] - pub async fn device(&self) -> ClientResult> { - let result = self - .endpoint_get("me/player/devices", &Query::new()) - .await?; - self.convert_result::(&result) - .map(|x| x.devices) - } - - /// Get Information About The User’s Current Playback - /// - /// Parameters: - /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. - /// - additional_types: Optional. A comma-separated list of item types that - /// your client supports besides the default track type. Valid types are: - /// `track` and `episode`. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-information-about-the-users-current-playback) - #[maybe_async] - pub async fn current_playback( - &self, - country: Option<&Market>, - additional_types: Option>, - ) -> ClientResult> { - let additional_types = - additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "additional_types": additional_types.as_ref(), - }; - - let result = self.endpoint_get("me/player", ¶ms).await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) - } - } - - /// Get the User’s Currently Playing Track - /// - /// Parameters: - /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. - /// - additional_types: Optional. A comma-separated list of item types that - /// your client supports besides the default track type. Valid types are: - /// `track` and `episode`. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) - #[maybe_async] - pub async fn current_playing( - &self, - market: Option<&Market>, - additional_types: Option>, - ) -> ClientResult> { - let additional_types = - additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - optional "additional_types": additional_types.as_ref(), - }; - - let result = self - .get("me/player/currently-playing", None, ¶ms) - .await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) - } - } - - /// Transfer a User’s Playback. - /// - /// Note: Although an array is accepted, only a single device_id is - /// currently supported. Supplying more than one will return 400 Bad Request - /// - /// Parameters: - /// - device_id - transfer playback to this device - /// - force_play - true: after transfer, play. false: - /// keep current state. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-transfer-a-users-playback) - #[maybe_async] - pub async fn transfer_playback( - &self, - device_id: &str, - force_play: Option, - ) -> ClientResult<()> { - let params = build_json! { - "device_ids": [device_id], - optional "force_play": force_play, - }; - - self.endpoint_put("me/player", ¶ms).await?; - Ok(()) - } - - /// Start/Resume a User’s Playback. - /// - /// Provide a `context_uri` to start playback or a album, artist, or - /// playlist. Provide a `uris` list to start playback of one or more tracks. - /// Provide `offset` as {"position": } or {"uri": ""} to - /// start playback at a particular offset. - /// - /// Parameters: - /// - device_id - device target for playback - /// - context_uri - spotify context uri to play - /// - uris - spotify track uris - /// - offset - offset into context by index or track - /// - position_ms - Indicates from what position to start playback. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) - #[maybe_async] - pub async fn start_context_playback( - &self, - context_uri: &Id, - device_id: Option<&str>, - offset: Option>, - position_ms: Option, - ) -> ClientResult<()> { - let params = build_json! { - "context_uri": context_uri.uri(), - optional "offset": offset.map(|x| match x { - Offset::Position(position) => json!({ "position": position }), - Offset::Uri(uri) => json!({ "uri": uri.uri() }), - }), - optional "position_ms": position_ms, - - }; - - let url = self.append_device_id("me/player/play", device_id); - self.put(&url, None, ¶ms).await?; - - Ok(()) - } - - #[maybe_async] - pub async fn start_uris_playback( - &self, - uris: &[&Id], - device_id: Option<&str>, - offset: Option>, - position_ms: Option, - ) -> ClientResult<()> { - let params = build_json! { - "uris": uris.iter().map(|id| id.uri()).collect::>(), - optional "position_ms": position_ms, - optional "offset": offset.map(|x| match x { - Offset::Position(position) => json!({ "position": position }), - Offset::Uri(uri) => json!({ "uri": uri.uri() }), - }), - }; - - let url = self.append_device_id("me/player/play", device_id); - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Pause a User’s Playback. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) - #[maybe_async] - pub async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/pause", device_id); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Skip User’s Playback To Next Track. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) - #[maybe_async] - pub async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/next", device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Skip User’s Playback To Previous Track. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) - #[maybe_async] - pub async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/previous", device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Seek To Position In Currently Playing Track. - /// - /// Parameters: - /// - position_ms - position in milliseconds to seek to - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) - #[maybe_async] - pub async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( - &format!("me/player/seek?position_ms={}", position_ms), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Set Repeat Mode On User’s Playback. - /// - /// Parameters: - /// - state - `track`, `context`, or `off` - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) - #[maybe_async] - pub async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( - &format!("me/player/repeat?state={}", state.as_ref()), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Set Volume For User’s Playback. - /// - /// Parameters: - /// - volume_percent - volume between 0 and 100 - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) - #[maybe_async] - pub async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { - if volume_percent > 100u8 { - error!("volume must be between 0 and 100, inclusive"); - } - let url = self.append_device_id( - &format!("me/player/volume?volume_percent={}", volume_percent), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Toggle Shuffle For User’s Playback. - /// - /// Parameters: - /// - state - true or false - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) - #[maybe_async] - pub async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Add an item to the end of the user's playback queue. - /// - /// Parameters: - /// - uri - The uri of the item to add, Track or Episode - /// - device id - The id of the device targeting - /// - If no device ID provided the user's currently active device is - /// targeted - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) - #[maybe_async] - pub async fn add_item_to_queue( - &self, - item: &Id, - device_id: Option<&str>, - ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Add a show or a list of shows to a user’s library. - /// - /// Parameters: - /// - ids(Required) A comma-separated list of Spotify IDs for the shows to - /// be added to the user’s library. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) - #[maybe_async] - pub async fn save_shows<'a>( - &self, - show_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/shows/?ids={}", join_ids(show_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Get a list of shows saved in the current Spotify user’s library. - /// Optional parameters can be used to limit the number of shows returned. - /// - /// Parameters: - /// - limit(Optional). The maximum number of shows to return. Default: 20. - /// Minimum: 1. Maximum: 50. - /// - offset(Optional). The index of the first show to return. Default: 0 - /// (the first object). Use with limit to get the next set of shows. - /// - /// See [`Spotify::get_saved_show_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - pub fn get_saved_show(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::get_saved_show`]. - #[maybe_async] - pub async fn get_saved_show_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "limit": limit.as_ref(), - optional "offset": offset.as_ref(), - }; - - let result = self.endpoint_get("me/shows", ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for a single show identified by its unique Spotify ID. - /// - /// Path Parameters: - /// - id: The Spotify ID for the show. - /// - /// Query Parameters - /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) - #[maybe_async] - pub async fn get_a_show(&self, id: &ShowId, market: Option<&Market>) -> ClientResult { - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("shows/{}", id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for multiple shows based on their - /// Spotify IDs. - /// - /// Query Parameters - /// - ids(Required) A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) - #[maybe_async] - pub async fn get_several_shows<'a>( - &self, - ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get("shows", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.shows) - } - - /// Get Spotify catalog information about an show’s episodes. Optional - /// parameters can be used to limit the number of episodes returned. - /// - /// Path Parameters - /// - id: The Spotify ID for the show. - /// - /// Query Parameters - /// - limit: Optional. The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50. - /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// See [`Spotify::get_shows_episodes_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) - pub fn get_shows_episodes<'a>( - &'a self, - id: &'a ShowId, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::get_shows_episodes`]. - #[maybe_async] - pub async fn get_shows_episodes_manual( - &self, - id: &ShowId, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_ref(), - optional "offset": offset.as_ref(), - }; - - let url = format!("shows/{}/episodes", id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. - /// - /// Path Parameters - /// - id: The Spotify ID for the episode. - /// - /// Query Parameters - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-episode) - #[maybe_async] - pub async fn get_an_episode( - &self, - id: &EpisodeId, - market: Option<&Market>, - ) -> ClientResult { - let url = format!("episodes/{}", id.id()); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for multiple episodes based on their Spotify IDs. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) - #[maybe_async] - pub async fn get_several_episodes<'a>( - &self, - ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get("episodes", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.episodes) - } - - /// Check if one or more shows is already saved in the current Spotify user’s library. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) - #[maybe_async] - pub async fn check_users_saved_shows<'a>( - &self, - ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - }; - let result = self.endpoint_get("me/shows/contains", ¶ms).await?; - self.convert_result(&result) - } - - /// Delete one or more shows from current Spotify user's library. - /// Changes to a user's saved shows may not be visible in other Spotify applications immediately. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) - #[maybe_async] - pub async fn remove_users_saved_shows<'a>( - &self, - show_ids: impl IntoIterator, - country: Option<&Market>, - ) -> ClientResult<()> { - let url = format!("me/shows?ids={}", join_ids(show_ids)); - let params = build_json! { - optional "country": country.map(|x| x.as_ref()) - }; - self.endpoint_delete(&url, ¶ms).await?; - - Ok(()) - } -} - -#[inline] -fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { - ids.into_iter().collect::>().join(",") -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_parse_response_code() { - let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; - let spotify = SpotifyBuilder::default().build().unwrap(); - let code = spotify.parse_response_code(url).unwrap(); - assert_eq!(code, "AQD0yXvFEOvw"); - } - - #[test] - fn test_append_device_id_without_question_mark() { - let path = "me/player/play"; - let device_id = Some("fdafdsadfa"); - let spotify = SpotifyBuilder::default().build().unwrap(); - let new_path = spotify.append_device_id(path, device_id); - assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); - } - - #[test] - fn test_append_device_id_with_question_mark() { - let path = "me/player/shuffle?state=true"; - let device_id = Some("fdafdsadfa"); - let spotify = SpotifyBuilder::default().build().unwrap(); - let new_path = spotify.append_device_id(path, device_id); - assert_eq!( - new_path, - "me/player/shuffle?state=true&device_id=fdafdsadfa" - ); - } -} diff --git a/src/client_creds.rs b/src/client_creds.rs new file mode 100644 index 00000000..601242a1 --- /dev/null +++ b/src/client_creds.rs @@ -0,0 +1,37 @@ +use crate::{prelude::*, Credentials, HTTPClient, Token}; + +#[derive(Clone, Debug)] +pub struct ClientCredentialsSpotify { + creds: Credentials, + tok: Option, + http: HTTPClient, +} + +impl ClientCredentialsSpotify { + pub fn new(creds: Credentials) -> Self { + ClientCredentialsSpotify { + creds, + tok: None, + http: HTTPClient {}, + } + } + + pub fn request_token(&mut self) { + self.tok = Some(Token("client credentials token".to_string())) + } +} + +// This could even use a macro +impl BaseClient for ClientCredentialsSpotify { + fn get_http(&self) -> &HTTPClient { + &self.http + } + + fn get_token(&self) -> Option<&Token> { + self.tok.as_ref() + } + + fn get_creds(&self) -> &Credentials { + &self.creds + } +} diff --git a/src/code_auth.rs b/src/code_auth.rs new file mode 100644 index 00000000..e08cb779 --- /dev/null +++ b/src/code_auth.rs @@ -0,0 +1,45 @@ +use crate::{prelude::*, Credentials, HTTPClient, OAuth, Token}; + +#[derive(Clone, Debug)] +pub struct CodeAuthSpotify { + creds: Credentials, + oauth: OAuth, + tok: Option, + http: HTTPClient, +} + +impl CodeAuthSpotify { + pub fn new(creds: Credentials, oauth: OAuth) -> Self { + CodeAuthSpotify { + creds, + oauth, + tok: None, + http: HTTPClient {}, + } + } + + pub fn prompt_for_user_token(&mut self) { + self.tok = Some(Token("code auth token".to_string())) + } +} + +impl BaseClient for CodeAuthSpotify { + fn get_http(&self) -> &HTTPClient { + &self.http + } + + fn get_token(&self) -> Option<&Token> { + self.tok.as_ref() + } + + fn get_creds(&self) -> &Credentials { + &self.creds + } +} + +// This could also be a macro (less important) +impl OAuthClient for CodeAuthSpotify { + fn get_oauth(&self) -> &OAuth { + &self.oauth + } +} diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs new file mode 100644 index 00000000..a3b4f831 --- /dev/null +++ b/src/code_auth_pkce.rs @@ -0,0 +1,44 @@ +use crate::{prelude::*, Credentials, HTTPClient, OAuth, Token}; + +#[derive(Clone, Debug)] +pub struct CodeAuthPKCESpotify { + creds: Credentials, + oauth: OAuth, + tok: Option, + http: HTTPClient, +} + +impl CodeAuthPKCESpotify { + pub fn new(creds: Credentials, oauth: OAuth) -> Self { + CodeAuthPKCESpotify { + creds, + oauth, + tok: None, + http: HTTPClient {}, + } + } + + pub fn prompt_for_user_token(&mut self) { + self.tok = Some(Token("code auth pkce token".to_string())) + } +} + +impl BaseClient for CodeAuthPKCESpotify { + fn get_http(&self) -> &HTTPClient { + &self.http + } + + fn get_token(&self) -> Option<&Token> { + self.tok.as_ref() + } + + fn get_creds(&self) -> &Credentials { + &self.creds + } +} + +impl OAuthClient for CodeAuthPKCESpotify { + fn get_oauth(&self) -> &OAuth { + &self.oauth + } +} diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs new file mode 100644 index 00000000..1dd3f5dd --- /dev/null +++ b/src/endpoints/base.rs @@ -0,0 +1,27 @@ +use crate::{Credentials, HTTPClient, Token}; +use std::collections::HashMap; + +pub trait BaseClient { + fn get_http(&self) -> &HTTPClient; + fn get_token(&self) -> Option<&Token>; + fn get_creds(&self) -> &Credentials; + + // Existing + fn request(&self, mut params: HashMap) { + let http = self.get_http(); + params.insert("url".to_string(), "...".to_string()); + http.request(params); + } + + // Existing + fn endpoint_request(&self) { + let mut params = HashMap::new(); + params.insert("token".to_string(), self.get_token().unwrap().0.clone()); + self.request(params); + } + + fn base_endpoint(&self) { + println!("Performing base request"); + self.endpoint_request(); + } +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs new file mode 100644 index 00000000..77908150 --- /dev/null +++ b/src/endpoints/mod.rs @@ -0,0 +1,152 @@ +pub mod base; +pub mod oauth; + +pub use base::SimpleClient; +pub use oauth::OAuthClient; + +/// HTTP-related methods for the Spotify client. It wraps the basic HTTP client +/// with features needed of higher level. +/// +/// The Spotify client has two different wrappers to perform requests: +/// +/// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only +/// append the configured Spotify API URL to the relative URL provided so that +/// it's not forgotten. They're used in the authentication process to request +/// an access token and similars. +/// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, +/// `endpoint_delete`. These append the authentication headers for endpoint +/// requests to reduce the code needed for endpoints and make them as concise +/// as possible. +impl Spotify { + /// If it's a relative URL like "me", the prefix is appended to it. + /// Otherwise, the same URL is returned. + fn endpoint_url(&self, url: &str) -> String { + // Using the client's prefix in case it's a relative route. + if !url.starts_with("http") { + self.prefix.clone() + url + } else { + url.to_string() + } + } + + /// The headers required for authenticated requests to the API + fn auth_headers(&self) -> ClientResult { + let mut auth = Headers::new(); + let (key, val) = headers::bearer_auth(self.get_token()?); + auth.insert(key, val); + + Ok(auth) + } + + #[inline] + #[maybe_async] + pub(crate) async fn get( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Query<'_>, + ) -> ClientResult { + let url = self.endpoint_url(url); + self.http.get(&url, headers, payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn post( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + self.http.post(&url, headers, payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn post_form( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Form<'_>, + ) -> ClientResult { + let url = self.endpoint_url(url); + self.http.post_form(&url, headers, payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn put( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + self.http.put(&url, headers, payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn delete( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + self.http.delete(&url, headers, payload).await + } + + /// The wrapper for the endpoints, which also includes the required + /// autentication. + #[inline] + #[maybe_async] + pub(crate) async fn endpoint_get( + &self, + url: &str, + payload: &Query<'_>, + ) -> ClientResult { + let headers = self.auth_headers()?; + self.get(url, Some(&headers), payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.post(url, Some(&headers), payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.put(url, Some(&headers), payload).await + } + + #[inline] + #[maybe_async] + pub(crate) async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.delete(url, Some(&headers), payload).await + } +} + + + /// Generates an HTTP token authorization header with proper formatting + pub fn bearer_auth(tok: &Token) -> (String, String) { + let auth = "authorization".to_owned(); + let value = format!("Bearer {}", tok.access_token); + + (auth, value) + } + + /// Generates an HTTP basic authorization header with proper formatting + pub fn basic_auth(user: &str, password: &str) -> (String, String) { + let auth = "authorization".to_owned(); + let value = format!("{}:{}", user, password); + let value = format!("Basic {}", base64::encode(value)); + + (auth, value) + } diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs new file mode 100644 index 00000000..c8e1f820 --- /dev/null +++ b/src/endpoints/oauth.rs @@ -0,0 +1,10 @@ +use crate::rspotify::{prelude::*, OAuth}; + +pub trait OAuthClient: BaseClient { + fn get_oauth(&self) -> &OAuth; + + fn user_endpoint(&self) { + println!("Performing OAuth request"); + self.endpoint_request(); + } +} diff --git a/rspotify-http/src/pagination/iter.rs b/src/endpoints/pagination/iter.rs similarity index 100% rename from rspotify-http/src/pagination/iter.rs rename to src/endpoints/pagination/iter.rs diff --git a/rspotify-http/src/pagination/mod.rs b/src/endpoints/pagination/mod.rs similarity index 100% rename from rspotify-http/src/pagination/mod.rs rename to src/endpoints/pagination/mod.rs diff --git a/rspotify-http/src/pagination/stream.rs b/src/endpoints/pagination/stream.rs similarity index 100% rename from rspotify-http/src/pagination/stream.rs rename to src/endpoints/pagination/stream.rs diff --git a/src/lib.rs b/src/lib.rs index 6ae88d94..43cd4772 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,8 +161,10 @@ // This way only the compile error below gets shown instead of a whole list of // confusing errors.. -pub mod client; -pub mod oauth2; +pub mod endpoints; +pub mod client_creds; +pub mod code_auth; +pub mod code_auth_pkce; // Subcrate re-exports pub use rspotify_macros as macros; @@ -171,3 +173,2206 @@ pub use rspotify_http as http; // Top-level re-exports pub use macros::scopes; + +/// Possible errors returned from the `rspotify` client. +#[derive(Debug, Error)] +pub enum ClientError { + /// Raised when the authentication isn't configured properly. + #[error("invalid client authentication: {0}")] + InvalidAuth(String), + + #[error("json parse error: {0}")] + ParseJson(#[from] serde_json::Error), + + #[error("url parse error: {0}")] + ParseUrl(#[from] url::ParseError), + + #[error("input/output error: {0}")] + Io(#[from] std::io::Error), + + #[cfg(feature = "cli")] + #[error("cli error: {0}")] + Cli(String), + + #[error("cache file error: {0}")] + CacheFile(String), +} + +pub type ClientResult = Result; + +pub const DEFAULT_API_PREFIX: &str = "https://api.spotify.com/v1/"; +pub const DEFAULT_CACHE_PATH: &str = ".spotify_token_cache.json"; +pub const DEFAULT_PAGINATION_CHUNKS: u32 = 50; + + +/// Spotify API object +#[derive(Builder, Debug, Clone)] +pub struct Spotify { + /// Internal member to perform requests to the Spotify API. + #[builder(setter(skip))] + pub(in crate) http: HTTPClient, + + /// The access token information required for requests to the Spotify API. + #[builder(setter(strip_option), default)] + pub token: Option, + + /// The credentials needed for obtaining a new access token, for requests. + /// without OAuth authentication. + #[builder(setter(strip_option), default)] + pub credentials: Option, + + /// The OAuth information required for obtaining a new access token, for + /// requests with OAuth authentication. `credentials` also needs to be + /// set up. + #[builder(setter(strip_option), default)] + pub oauth: Option, + + /// The Spotify API prefix, [`DEFAULT_API_PREFIX`] by default. + #[builder(setter(into), default = "String::from(DEFAULT_API_PREFIX)")] + pub prefix: String, + + /// The cache file path, in case it's used. By default it's + /// [`DEFAULT_CACHE_PATH`] + #[builder(default = "PathBuf::from(DEFAULT_CACHE_PATH)")] + pub cache_path: PathBuf, + + /// The pagination chunk size used when performing automatically paginated + /// requests, like [`Spotify::artist_albums`]. This means that a request + /// will be performed every `pagination_chunks` items. By default this is + /// [`DEFAULT_PAGINATION_CHUNKS`]. + /// + /// Note that most endpoints set a maximum to the number of items per + /// request, which most times is 50. + #[builder(default = "DEFAULT_PAGINATION_CHUNKS")] + pub pagination_chunks: u32, +} + +// Endpoint-related methods for the client. +impl Spotify { + /// Returns the access token, or an error in case it's not configured. + pub(in crate) fn get_token(&self) -> ClientResult<&Token> { + self.token + .as_ref() + .ok_or_else(|| ClientError::InvalidAuth("no access token configured".to_string())) + } + + /// Returns the credentials, or an error in case it's not configured. + pub(in crate) fn get_creds(&self) -> ClientResult<&Credentials> { + self.credentials + .as_ref() + .ok_or_else(|| ClientError::InvalidAuth("no credentials configured".to_string())) + } + + /// Returns the oauth information, or an error in case it's not configured. + pub(in crate) fn get_oauth(&self) -> ClientResult<&OAuth> { + self.oauth + .as_ref() + .ok_or_else(|| ClientError::InvalidAuth("no oauth configured".to_string())) + } + + /// Converts a JSON response from Spotify into its model. + fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { + serde_json::from_str::(input).map_err(Into::into) + } + + /// Append device ID to an API path. + fn append_device_id(&self, path: &str, device_id: Option<&str>) -> String { + let mut new_path = path.to_string(); + if let Some(_device_id) = device_id { + if path.contains('?') { + new_path.push_str(&format!("&device_id={}", _device_id)); + } else { + new_path.push_str(&format!("?device_id={}", _device_id)); + } + } + new_path + } + + /// Returns a single track given the track's ID, URI or URL. + /// + /// Parameters: + /// - track_id - a spotify URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track) + #[maybe_async] + pub async fn track(&self, track_id: &TrackId) -> ClientResult { + let url = format!("tracks/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of tracks given a list of track IDs, URIs, or URLs. + /// + /// Parameters: + /// - track_ids - a list of spotify URIs, URLs or IDs + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) + #[maybe_async] + pub async fn tracks<'a>( + &self, + track_ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(track_ids); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("tracks/?ids={}", ids); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result).map(|x| x.tracks) + } + + /// Returns a single artist given the artist's ID, URI or URL. + /// + /// Parameters: + /// - artist_id - an artist ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artist) + #[maybe_async] + pub async fn artist(&self, artist_id: &ArtistId) -> ClientResult { + let url = format!("artists/{}", artist_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of artists given the artist IDs, URIs, or URLs. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs, URIs or URLs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) + #[maybe_async] + pub async fn artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(artist_ids); + let url = format!("artists/?ids={}", ids); + let result = self.endpoint_get(&url, &Query::new()).await?; + + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Get Spotify catalog information about an artist's albums. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// - album_type - 'album', 'single', 'appears_on', 'compilation' + /// - market - limit the response to one particular country. + /// - limit - the number of albums to return + /// - offset - the index of the first album to return + /// + /// See [`Spotify::artist_albums_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) + pub fn artist_albums<'a>( + &'a self, + artist_id: &'a ArtistId, + album_type: Option<&'a AlbumType>, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::artist_albums`]. + #[maybe_async] + pub async fn artist_albums_manual( + &self, + artist_id: &ArtistId, + album_type: Option<&AlbumType>, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit: Option = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "album_type": album_type.map(|x| x.as_ref()), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("artists/{}/albums", artist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information about an artist's top 10 tracks by + /// country. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// - market - limit the response to one particular country. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-top-tracks) + #[maybe_async] + pub async fn artist_top_tracks( + &self, + artist_id: &ArtistId, + market: Market, + ) -> ClientResult> { + let params = build_map! { + "market": market.as_ref() + }; + + let url = format!("artists/{}/top-tracks", artist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result).map(|x| x.tracks) + } + + /// Get Spotify catalog information about artists similar to an identified + /// artist. Similarity is based on analysis of the Spotify community's + /// listening history. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-related-artists) + #[maybe_async] + pub async fn artist_related_artists( + &self, + artist_id: &ArtistId, + ) -> ClientResult> { + let url = format!("artists/{}/related-artists", artist_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Returns a single album given the album's ID, URIs or URL. + /// + /// Parameters: + /// - album_id - the album ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album) + #[maybe_async] + pub async fn album(&self, album_id: &AlbumId) -> ClientResult { + let url = format!("albums/{}", album_id.id()); + + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of albums given the album IDs, URIs, or URLs. + /// + /// Parameters: + /// - albums_ids - a list of album IDs, URIs or URLs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) + #[maybe_async] + pub async fn albums<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(album_ids); + let url = format!("albums/?ids={}", ids); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result::(&result).map(|x| x.albums) + } + + /// Search for an Item. Get Spotify catalog information about artists, + /// albums, tracks or playlists that match a keyword string. + /// + /// Parameters: + /// - q - the search query + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// - type - the type of item to return. One of 'artist', 'album', 'track', + /// 'playlist', 'show' or 'episode' + /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - include_external: Optional.Possible values: audio. If + /// include_external=audio is specified the response will include any + /// relevant audio content that is hosted externally. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#category-search) + #[maybe_async] + pub async fn search( + &self, + q: &str, + _type: SearchType, + market: Option<&Market>, + include_external: Option<&IncludeExternal>, + limit: Option, + offset: Option, + ) -> ClientResult { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + "q": q, + "type": _type.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "include_external": include_external.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("search", ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information about an album's tracks. + /// + /// Parameters: + /// - album_id - the album ID, URI or URL + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::album_track_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) + pub fn album_track<'a>( + &'a self, + album_id: &'a AlbumId, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::album_track`]. + #[maybe_async] + pub async fn album_track_manual( + &self, + album_id: &AlbumId, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("albums/{}/tracks", album_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Gets basic profile information about a Spotify User. + /// + /// Parameters: + /// - user - the id of the usr + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-profile) + #[maybe_async] + pub async fn user(&self, user_id: &UserId) -> ClientResult { + let url = format!("users/{}", user_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get full details about Spotify playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlist) + #[maybe_async] + pub async fn playlist( + &self, + playlist_id: &PlaylistId, + fields: Option<&str>, + market: Option<&Market>, + ) -> ClientResult { + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("playlists/{}", playlist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get current user playlists without required getting his profile. + /// + /// Parameters: + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::current_user_playlists_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) + pub fn current_user_playlists(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_playlists`]. + #[maybe_async] + pub async fn current_user_playlists_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/playlists", ¶ms).await?; + self.convert_result(&result) + } + + /// Gets playlists of a user. + /// + /// Parameters: + /// - user_id - the id of the usr + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::user_playlists_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) + pub fn user_playlists<'a>( + &'a self, + user_id: &'a UserId, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::user_playlists`]. + #[maybe_async] + pub async fn user_playlists_manual( + &self, + user_id: &UserId, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("users/{}/playlists", user_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Gets playlist of a user. + /// + /// Parameters: + /// - user_id - the id of the user + /// - playlist_id - the id of the playlist + /// - fields - which fields to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) + #[maybe_async] + pub async fn user_playlist( + &self, + user_id: &UserId, + playlist_id: Option<&PlaylistId>, + fields: Option<&str>, + ) -> ClientResult { + let params = build_map! { + optional "fields": fields, + }; + + let url = match playlist_id { + Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), + None => format!("users/{}/starred", user_id.id()), + }; + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get full details of the tracks of a playlist owned by a user. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - fields - which fields to return + /// - limit - the maximum number of tracks to return + /// - offset - the index of the first track to return + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// See [`Spotify::playlist_tracks`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) + pub fn playlist_tracks<'a>( + &'a self, + playlist_id: &'a PlaylistId, + fields: Option<&'a str>, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::playlist_tracks`]. + #[maybe_async] + pub async fn playlist_tracks_manual( + &self, + playlist_id: &PlaylistId, + fields: Option<&str>, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Creates a playlist for a user. + /// + /// Parameters: + /// - user_id - the id of the user + /// - name - the name of the playlist + /// - public - is the created playlist public + /// - description - the description of the playlist + /// - collaborative - if the playlist will be collaborative + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist) + #[maybe_async] + pub async fn user_playlist_create( + &self, + user_id: &UserId, + name: &str, + public: Option, + collaborative: Option, + description: Option<&str>, + ) -> ClientResult { + let params = build_json! { + "name": name, + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + + let url = format!("users/{}/playlists", user_id.id()); + let result = self.endpoint_post(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Changes a playlist's name and/or public/private state. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - name - optional name of the playlist + /// - public - optional is the playlist public + /// - collaborative - optional is the playlist collaborative + /// - description - optional description of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-change-playlist-details) + #[maybe_async] + pub async fn playlist_change_detail( + &self, + playlist_id: &str, + name: Option<&str>, + public: Option, + description: Option<&str>, + collaborative: Option, + ) -> ClientResult { + let params = build_json! { + optional "name": name, + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + + let url = format!("playlists/{}", playlist_id); + self.endpoint_put(&url, ¶ms).await + } + + /// Unfollows (deletes) a playlist for a user. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-playlist) + #[maybe_async] + pub async fn playlist_unfollow(&self, playlist_id: &str) -> ClientResult { + let url = format!("playlists/{}/followers", playlist_id); + self.endpoint_delete(&url, &json!({})).await + } + + /// Adds tracks to a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - track_ids - a list of track URIs, URLs or IDs + /// - position - the position to add the tracks + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) + #[maybe_async] + pub async fn playlist_add_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + position: Option, + ) -> ClientResult { + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris, + "position": position, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_post(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Replace all tracks in a playlist + /// + /// Parameters: + /// - user - the id of the user + /// - playlist_id - the id of the playlist + /// - tracks - the list of track ids to add to the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) + #[maybe_async] + pub async fn playlist_replace_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Reorder tracks in a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - uris - a list of Spotify URIs to replace or clear + /// - range_start - the position of the first track to be reordered + /// - insert_before - the position where the tracks should be inserted + /// - range_length - optional the number of tracks to be reordered (default: + /// 1) + /// - snapshot_id - optional playlist's snapshot ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) + #[maybe_async] + pub async fn playlist_reorder_tracks( + &self, + playlist_id: &PlaylistId, + uris: Option<&[&Id]>, + range_start: Option, + insert_before: Option, + range_length: Option, + snapshot_id: Option<&str>, + ) -> ClientResult { + let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); + let params = build_json! { + "playlist_id": playlist_id, + optional "uris": uris, + optional "range_start": range_start, + optional "insert_before": insert_before, + optional "range_length": range_length, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_put(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Removes all occurrences of the given tracks from the given playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - track_ids - the list of track ids to add to the playlist + /// - snapshot_id - optional id of the playlist snapshot + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) + #[maybe_async] + pub async fn playlist_remove_all_occurrences_of_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + snapshot_id: Option<&str>, + ) -> ClientResult { + let tracks = track_ids + .into_iter() + .map(|id| { + let mut map = Map::with_capacity(1); + map.insert("uri".to_owned(), id.uri().into()); + map + }) + .collect::>(); + + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_delete(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Removes specfic occurrences of the given tracks from the given playlist. + /// + /// Parameters: + /// - playlist_id: the id of the playlist + /// - tracks: an array of map containing Spotify URIs of the tracks to + /// remove with their current positions in the playlist. For example: + /// + /// ```json + /// { + /// "tracks":[ + /// { + /// "uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh", + /// "positions":[ + /// 0, + /// 3 + /// ] + /// }, + /// { + /// "uri":"spotify:track:1301WleyT98MSxVHPZCA6M", + /// "positions":[ + /// 7 + /// ] + /// } + /// ] + /// } + /// ``` + /// - snapshot_id: optional id of the playlist snapshot + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) + #[maybe_async] + pub async fn playlist_remove_specific_occurrences_of_tracks( + &self, + playlist_id: &PlaylistId, + tracks: Vec>, + snapshot_id: Option<&str>, + ) -> ClientResult { + let tracks = tracks + .into_iter() + .map(|track| { + let mut map = Map::new(); + map.insert("uri".to_owned(), track.id.uri().into()); + map.insert("positions".to_owned(), track.positions.into()); + map + }) + .collect::>(); + + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_delete(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Add the current authenticated user as a follower of a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-playlist) + #[maybe_async] + pub async fn playlist_follow( + &self, + playlist_id: &PlaylistId, + public: Option, + ) -> ClientResult<()> { + let url = format!("playlists/{}/followers", playlist_id.id()); + + let params = build_json! { + optional "public": public, + }; + + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Check to see if the given users are following the given playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - user_ids - the ids of the users that you want to + /// check to see if they follow the playlist. Maximum: 5 ids. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) + #[maybe_async] + pub async fn playlist_check_follow( + &self, + playlist_id: &PlaylistId, + user_ids: &[&UserId], + ) -> ClientResult> { + if user_ids.len() > 5 { + error!("The maximum length of user ids is limited to 5 :-)"); + } + let url = format!( + "playlists/{}/followers/contains?ids={}", + playlist_id.id(), + user_ids + .iter() + .map(|id| id.id()) + .collect::>() + .join(","), + ); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get detailed profile information about the current user. + /// An alias for the 'current_user' method. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) + #[maybe_async] + pub async fn me(&self) -> ClientResult { + let result = self.endpoint_get("me/", &Query::new()).await?; + self.convert_result(&result) + } + + /// Get detailed profile information about the current user. + /// An alias for the 'me' method. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) + #[maybe_async] + pub async fn current_user(&self) -> ClientResult { + self.me().await + } + + /// Get information about the current users currently playing track. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) + #[maybe_async] + pub async fn current_user_playing_track( + &self, + ) -> ClientResult> { + let result = self + .get("me/player/currently-playing", None, &Query::new()) + .await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Gets a list of the albums saved in the current authorized user's + /// "Your Music" library + /// + /// Parameters: + /// - limit - the number of albums to return + /// - offset - the index of the first album to return + /// - market - Provide this parameter if you want to apply Track Relinking. + /// + /// See [`Spotify::current_user_saved_albums`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) + pub fn current_user_saved_albums(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of + /// [`Spotify::current_user_saved_albums`]. + #[maybe_async] + pub async fn current_user_saved_albums_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/albums", ¶ms).await?; + self.convert_result(&result) + } + + /// Get a list of the songs saved in the current Spotify user's "Your Music" + /// library. + /// + /// Parameters: + /// - limit - the number of tracks to return + /// - offset - the index of the first track to return + /// - market - Provide this parameter if you want to apply Track Relinking. + /// + /// See [`Spotify::current_user_saved_tracks_manual`] for a manually + /// paginated version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) + pub fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of + /// [`Spotify::current_user_saved_tracks`]. + #[maybe_async] + pub async fn current_user_saved_tracks_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/tracks", ¶ms).await?; + self.convert_result(&result) + } + + /// Gets a list of the artists followed by the current authorized user. + /// + /// Parameters: + /// - after - the last artist ID retrieved from the previous request + /// - limit - the number of tracks to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-followed) + #[maybe_async] + pub async fn current_user_followed_artists( + &self, + after: Option<&str>, + limit: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let params = build_map! { + "r#type": Type::Artist.as_ref(), + optional "after": after, + optional "limit": limit.as_deref(), + }; + + let result = self.endpoint_get("me/following", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Remove one or more tracks from the current user's "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) + #[maybe_async] + pub async fn current_user_saved_tracks_delete<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check if one or more tracks is already saved in the current Spotify + /// user’s "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) + #[maybe_async] + pub async fn current_user_saved_tracks_contains<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Save one or more tracks to the current user's "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) + #[maybe_async] + pub async fn current_user_saved_tracks_add<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Get the current user's top artists. + /// + /// Parameters: + /// - limit - the number of entities to return + /// - offset - the index of the first entity to return + /// - time_range - Over what time frame are the affinities computed + /// + /// See [`Spotify::current_user_top_artists_manual`] for a manually + /// paginated version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) + pub fn current_user_top_artists<'a>( + &'a self, + time_range: Option<&'a TimeRange>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_top_artists`]. + #[maybe_async] + pub async fn current_user_top_artists_manual( + &self, + time_range: Option<&TimeRange>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; + self.convert_result(&result) + } + + /// Get the current user's top tracks. + /// + /// Parameters: + /// - limit - the number of entities to return + /// - offset - the index of the first entity to return + /// - time_range - Over what time frame are the affinities computed + /// + /// See [`Spotify::current_user_top_tracks_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) + pub fn current_user_top_tracks<'a>( + &'a self, + time_range: Option<&'a TimeRange>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_top_tracks`]. + #[maybe_async] + pub async fn current_user_top_tracks_manual( + &self, + time_range: Option<&TimeRange>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/top/tracks", ¶ms).await?; + self.convert_result(&result) + } + + /// Get the current user's recently played tracks. + /// + /// Parameters: + /// - limit - the number of entities to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track) + #[maybe_async] + pub async fn current_user_recently_played( + &self, + limit: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + }; + + let result = self + .endpoint_get("me/player/recently-played", ¶ms) + .await?; + self.convert_result(&result) + } + + /// Add one or more albums to the current user's "Your Music" library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) + #[maybe_async] + pub async fn current_user_saved_albums_add<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/albums/?ids={}", join_ids(album_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Remove one or more albums from the current user's "Your Music" library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) + #[maybe_async] + pub async fn current_user_saved_albums_delete<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/albums/?ids={}", join_ids(album_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check if one or more albums is already saved in the current Spotify + /// user’s "Your Music” library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) + #[maybe_async] + pub async fn current_user_saved_albums_contains<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Follow one or more artists. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) + #[maybe_async] + pub async fn user_follow_artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Unfollow one or more artists. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) + #[maybe_async] + pub async fn user_unfollow_artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check to see if the current user is following one or more artists or + /// other Spotify users. + /// + /// Parameters: + /// - artist_ids - the ids of the users that you want to + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) + #[maybe_async] + pub async fn user_artist_check_follow<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!( + "me/following/contains?type=artist&ids={}", + join_ids(artist_ids) + ); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Follow one or more users. + /// + /// Parameters: + /// - user_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) + #[maybe_async] + pub async fn user_follow_users<'a>( + &self, + user_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Unfollow one or more users. + /// + /// Parameters: + /// - user_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) + #[maybe_async] + pub async fn user_unfollow_users<'a>( + &self, + user_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Get a list of Spotify featured playlists. + /// + /// Parameters: + /// - locale - The desired language, consisting of a lowercase ISO 639 + /// language code and an uppercase ISO 3166-1 alpha-2 country code, + /// joined by an underscore. + /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use + /// this parameter to specify the user's local time to get results + /// tailored for that specific date and time in the day + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 + /// (the first object). Use with limit to get the next set of + /// items. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-featured-playlists) + #[maybe_async] + pub async fn featured_playlists( + &self, + locale: Option<&str>, + country: Option<&Market>, + timestamp: Option>, + limit: Option, + offset: Option, + ) -> ClientResult { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let timestamp = timestamp.map(|x| x.to_rfc3339()); + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "timestamp": timestamp.as_deref(), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self + .endpoint_get("browse/featured-playlists", ¶ms) + .await?; + self.convert_result(&result) + } + + /// Get a list of new album releases featured in Spotify. + /// + /// Parameters: + /// - country - An ISO 3166-1 alpha-2 country code or string from_token. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::new_releases_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) + pub fn new_releases<'a>( + &'a self, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::new_releases`]. + #[maybe_async] + pub async fn new_releases_manual( + &self, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("browse/new-releases", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.albums) + } + + /// Get a list of new album releases featured in Spotify + /// + /// Parameters: + /// - country - An ISO 3166-1 alpha-2 country code or string from_token. + /// - locale - The desired language, consisting of an ISO 639 language code + /// and an ISO 3166-1 alpha-2 country code, joined by an underscore. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::categories_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) + pub fn categories<'a>( + &'a self, + locale: Option<&'a str>, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::categories`]. + #[maybe_async] + pub async fn categories_manual( + &self, + locale: Option<&str>, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get("browse/categories", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.categories) + } + + /// Get a list of playlists in a category in Spotify + /// + /// Parameters: + /// - category_id - The category id to get playlists from. + /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::category_playlists_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) + pub fn category_playlists<'a>( + &'a self, + category_id: &'a str, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::category_playlists`]. + #[maybe_async] + pub async fn category_playlists_manual( + &self, + category_id: &str, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("browse/categories/{}/playlists", category_id); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.playlists) + } + + /// Get Recommendations Based on Seeds + /// + /// Parameters: + /// - seed_artists - a list of artist IDs, URIs or URLs + /// - seed_tracks - a list of artist IDs, URIs or URLs + /// - seed_genres - a list of genre names. Available genres for + /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. If provided, all + /// results will be playable in this country. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 100 + /// - min/max/target_ - For the tuneable track attributes listed + /// in the documentation, these values provide filters and targeting on + /// results. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recommendations) + #[maybe_async] + pub async fn recommendations( + &self, + payload: &Map, + seed_artists: Option>, + seed_genres: Option>, + seed_tracks: Option>, + limit: Option, + market: Option<&Market>, + ) -> ClientResult { + let seed_artists = seed_artists.map(join_ids); + let seed_genres = seed_genres.map(|x| x.join(",")); + let seed_tracks = seed_tracks.map(join_ids); + let limit = limit.map(|x| x.to_string()); + let mut params = build_map! { + optional "seed_artists": seed_artists.as_ref(), + optional "seed_genres": seed_genres.as_ref(), + optional "seed_tracks": seed_tracks.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + }; + + // TODO: this probably can be improved. + let attributes = [ + "acousticness", + "danceability", + "duration_ms", + "energy", + "instrumentalness", + "key", + "liveness", + "loudness", + "mode", + "popularity", + "speechiness", + "tempo", + "time_signature", + "valence", + ]; + let mut map_to_hold_owned_value = HashMap::new(); + let prefixes = ["min", "max", "target"]; + for attribute in attributes.iter() { + for prefix in prefixes.iter() { + let param = format!("{}_{}", prefix, attribute); + if let Some(value) = payload.get(¶m) { + // TODO: not sure if this `to_string` is what we want. It + // might add quotes to the strings. + map_to_hold_owned_value.insert(param, value.to_string()); + } + } + } + + for (ref key, ref value) in &map_to_hold_owned_value { + params.insert(key, value); + } + + let result = self.endpoint_get("recommendations", ¶ms).await?; + self.convert_result(&result) + } + + /// Get audio features for a track + /// + /// Parameters: + /// - track - track URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-features) + #[maybe_async] + pub async fn track_features(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-features/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get Audio Features for Several Tracks + /// + /// Parameters: + /// - tracks a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) + #[maybe_async] + pub async fn tracks_features<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult>> { + let url = format!("audio-features/?ids={}", join_ids(track_ids)); + + let result = self.endpoint_get(&url, &Query::new()).await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result::>(&result) + .map(|option_payload| option_payload.map(|x| x.audio_features)) + } + } + + /// Get Audio Analysis for a Track + /// + /// Parameters: + /// - track_id - a track URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-analysis) + #[maybe_async] + pub async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-analysis/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get a User’s Available Devices + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-users-available-devices) + #[maybe_async] + pub async fn device(&self) -> ClientResult> { + let result = self + .endpoint_get("me/player/devices", &Query::new()) + .await?; + self.convert_result::(&result) + .map(|x| x.devices) + } + + /// Get Information About The User’s Current Playback + /// + /// Parameters: + /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. + /// - additional_types: Optional. A comma-separated list of item types that + /// your client supports besides the default track type. Valid types are: + /// `track` and `episode`. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-information-about-the-users-current-playback) + #[maybe_async] + pub async fn current_playback( + &self, + country: Option<&Market>, + additional_types: Option>, + ) -> ClientResult> { + let additional_types = + additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_ref(), + }; + + let result = self.endpoint_get("me/player", ¶ms).await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Get the User’s Currently Playing Track + /// + /// Parameters: + /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. + /// - additional_types: Optional. A comma-separated list of item types that + /// your client supports besides the default track type. Valid types are: + /// `track` and `episode`. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) + #[maybe_async] + pub async fn current_playing( + &self, + market: Option<&Market>, + additional_types: Option>, + ) -> ClientResult> { + let additional_types = + additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_ref(), + }; + + let result = self + .get("me/player/currently-playing", None, ¶ms) + .await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Transfer a User’s Playback. + /// + /// Note: Although an array is accepted, only a single device_id is + /// currently supported. Supplying more than one will return 400 Bad Request + /// + /// Parameters: + /// - device_id - transfer playback to this device + /// - force_play - true: after transfer, play. false: + /// keep current state. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-transfer-a-users-playback) + #[maybe_async] + pub async fn transfer_playback( + &self, + device_id: &str, + force_play: Option, + ) -> ClientResult<()> { + let params = build_json! { + "device_ids": [device_id], + optional "force_play": force_play, + }; + + self.endpoint_put("me/player", ¶ms).await?; + Ok(()) + } + + /// Start/Resume a User’s Playback. + /// + /// Provide a `context_uri` to start playback or a album, artist, or + /// playlist. Provide a `uris` list to start playback of one or more tracks. + /// Provide `offset` as {"position": } or {"uri": ""} to + /// start playback at a particular offset. + /// + /// Parameters: + /// - device_id - device target for playback + /// - context_uri - spotify context uri to play + /// - uris - spotify track uris + /// - offset - offset into context by index or track + /// - position_ms - Indicates from what position to start playback. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) + #[maybe_async] + pub async fn start_context_playback( + &self, + context_uri: &Id, + device_id: Option<&str>, + offset: Option>, + position_ms: Option, + ) -> ClientResult<()> { + let params = build_json! { + "context_uri": context_uri.uri(), + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), + optional "position_ms": position_ms, + + }; + + let url = self.append_device_id("me/player/play", device_id); + self.put(&url, None, ¶ms).await?; + + Ok(()) + } + + #[maybe_async] + pub async fn start_uris_playback( + &self, + uris: &[&Id], + device_id: Option<&str>, + offset: Option>, + position_ms: Option, + ) -> ClientResult<()> { + let params = build_json! { + "uris": uris.iter().map(|id| id.uri()).collect::>(), + optional "position_ms": position_ms, + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), + }; + + let url = self.append_device_id("me/player/play", device_id); + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Pause a User’s Playback. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) + #[maybe_async] + pub async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/pause", device_id); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Skip User’s Playback To Next Track. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) + #[maybe_async] + pub async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/next", device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Skip User’s Playback To Previous Track. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) + #[maybe_async] + pub async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/previous", device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Seek To Position In Currently Playing Track. + /// + /// Parameters: + /// - position_ms - position in milliseconds to seek to + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) + #[maybe_async] + pub async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id( + &format!("me/player/seek?position_ms={}", position_ms), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Set Repeat Mode On User’s Playback. + /// + /// Parameters: + /// - state - `track`, `context`, or `off` + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) + #[maybe_async] + pub async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id( + &format!("me/player/repeat?state={}", state.as_ref()), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Set Volume For User’s Playback. + /// + /// Parameters: + /// - volume_percent - volume between 0 and 100 + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) + #[maybe_async] + pub async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { + if volume_percent > 100u8 { + error!("volume must be between 0 and 100, inclusive"); + } + let url = self.append_device_id( + &format!("me/player/volume?volume_percent={}", volume_percent), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Toggle Shuffle For User’s Playback. + /// + /// Parameters: + /// - state - true or false + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) + #[maybe_async] + pub async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Add an item to the end of the user's playback queue. + /// + /// Parameters: + /// - uri - The uri of the item to add, Track or Episode + /// - device id - The id of the device targeting + /// - If no device ID provided the user's currently active device is + /// targeted + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) + #[maybe_async] + pub async fn add_item_to_queue( + &self, + item: &Id, + device_id: Option<&str>, + ) -> ClientResult<()> { + let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Add a show or a list of shows to a user’s library. + /// + /// Parameters: + /// - ids(Required) A comma-separated list of Spotify IDs for the shows to + /// be added to the user’s library. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) + #[maybe_async] + pub async fn save_shows<'a>( + &self, + show_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/shows/?ids={}", join_ids(show_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Get a list of shows saved in the current Spotify user’s library. + /// Optional parameters can be used to limit the number of shows returned. + /// + /// Parameters: + /// - limit(Optional). The maximum number of shows to return. Default: 20. + /// Minimum: 1. Maximum: 50. + /// - offset(Optional). The index of the first show to return. Default: 0 + /// (the first object). Use with limit to get the next set of shows. + /// + /// See [`Spotify::get_saved_show_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) + pub fn get_saved_show(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::get_saved_show`]. + #[maybe_async] + pub async fn get_saved_show_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + + let result = self.endpoint_get("me/shows", ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for a single show identified by its unique Spotify ID. + /// + /// Path Parameters: + /// - id: The Spotify ID for the show. + /// + /// Query Parameters + /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) + #[maybe_async] + pub async fn get_a_show(&self, id: &ShowId, market: Option<&Market>) -> ClientResult { + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("shows/{}", id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for multiple shows based on their + /// Spotify IDs. + /// + /// Query Parameters + /// - ids(Required) A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) + #[maybe_async] + pub async fn get_several_shows<'a>( + &self, + ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get("shows", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.shows) + } + + /// Get Spotify catalog information about an show’s episodes. Optional + /// parameters can be used to limit the number of episodes returned. + /// + /// Path Parameters + /// - id: The Spotify ID for the show. + /// + /// Query Parameters + /// - limit: Optional. The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50. + /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// See [`Spotify::get_shows_episodes_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) + pub fn get_shows_episodes<'a>( + &'a self, + id: &'a ShowId, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::get_shows_episodes`]. + #[maybe_async] + pub async fn get_shows_episodes_manual( + &self, + id: &ShowId, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + + let url = format!("shows/{}/episodes", id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. + /// + /// Path Parameters + /// - id: The Spotify ID for the episode. + /// + /// Query Parameters + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-episode) + #[maybe_async] + pub async fn get_an_episode( + &self, + id: &EpisodeId, + market: Option<&Market>, + ) -> ClientResult { + let url = format!("episodes/{}", id.id()); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for multiple episodes based on their Spotify IDs. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) + #[maybe_async] + pub async fn get_several_episodes<'a>( + &self, + ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get("episodes", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.episodes) + } + + /// Check if one or more shows is already saved in the current Spotify user’s library. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) + #[maybe_async] + pub async fn check_users_saved_shows<'a>( + &self, + ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + }; + let result = self.endpoint_get("me/shows/contains", ¶ms).await?; + self.convert_result(&result) + } + + /// Delete one or more shows from current Spotify user's library. + /// Changes to a user's saved shows may not be visible in other Spotify applications immediately. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) + #[maybe_async] + pub async fn remove_users_saved_shows<'a>( + &self, + show_ids: impl IntoIterator, + country: Option<&Market>, + ) -> ClientResult<()> { + let url = format!("me/shows?ids={}", join_ids(show_ids)); + let params = build_json! { + optional "country": country.map(|x| x.as_ref()) + }; + self.endpoint_delete(&url, ¶ms).await?; + + Ok(()) + } +} + +#[inline] +fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { + ids.into_iter().collect::>().join(",") +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_parse_response_code() { + let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; + let spotify = SpotifyBuilder::default().build().unwrap(); + let code = spotify.parse_response_code(url).unwrap(); + assert_eq!(code, "AQD0yXvFEOvw"); + } + + #[test] + fn test_append_device_id_without_question_mark() { + let path = "me/player/play"; + let device_id = Some("fdafdsadfa"); + let spotify = SpotifyBuilder::default().build().unwrap(); + let new_path = spotify.append_device_id(path, device_id); + assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); + } + + #[test] + fn test_append_device_id_with_question_mark() { + let path = "me/player/shuffle?state=true"; + let device_id = Some("fdafdsadfa"); + let spotify = SpotifyBuilder::default().build().unwrap(); + let new_path = spotify.append_device_id(path, device_id); + assert_eq!( + new_path, + "me/player/shuffle?state=true&device_id=fdafdsadfa" + ); + } +} diff --git a/src/mod.rs b/src/mod.rs new file mode 100644 index 00000000..cbb4d349 --- /dev/null +++ b/src/mod.rs @@ -0,0 +1,21 @@ +//! Client to Spotify API endpoint + +use derive_builder::Builder; +use log::error; +use maybe_async::maybe_async; +use serde::Deserialize; +use serde_json::{json, map::Map, Value}; +use thiserror::Error; + +use std::collections::HashMap; +use std::path::PathBuf; +use std::time; + +use crate::http::{HTTPClient, Query}; +use crate::macros::{build_json, build_map}; +use crate::model::{ + idtypes::{IdType, PlayContextIdType}, + *, +}; +use crate::oauth2::{Credentials, OAuth, Token}; +use crate::pagination::{paginate, Paginator}; From bbbb3af82ce1cc7a0c79f7c15d4decd978f287e1 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Thu, 29 Apr 2021 21:09:24 +0200 Subject: [PATCH 03/56] moving endpoints to respective traits --- Cargo.toml | 1 + rspotify-http/Cargo.toml | 69 -- rspotify-http/src/lib.rs | 36 +- rspotify-http/src/reqwest.rs | 23 +- rspotify-http/src/ureq.rs | 9 +- rspotify-model/src/lib.rs | 6 +- src/client_creds.rs | 27 +- src/endpoints/base.rs | 842 ++++++++++++- src/endpoints/mod.rs | 60 +- src/endpoints/oauth.rs | 1164 ++++++++++++++++- src/lib.rs | 2262 +++------------------------------- src/mod.rs | 21 - src/oauth2.rs | 206 ---- 13 files changed, 2259 insertions(+), 2467 deletions(-) delete mode 100644 src/mod.rs diff --git a/Cargo.toml b/Cargo.toml index f69504b0..9352678b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ rspotify-http = { path = "rspotify-http", version = "0.10.0" } ### Client ### async-stream = { version = "0.3.0", optional = true } +async-trait = { version = "0.1.48", optional = true } chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } derive_builder = "0.10.0" dotenv = { version = "0.15.0", optional = true } diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml index f64b1d6f..465a2d41 100644 --- a/rspotify-http/Cargo.toml +++ b/rspotify-http/Cargo.toml @@ -49,72 +49,3 @@ ureq-rustls-tls = ["ureq/tls"] # Internal features for checking async or sync compilation __async = ["async-trait", "futures"] __sync = ["maybe-async/is_sync"] - -[package.metadata.docs.rs] -# Also documenting the CLI methods -features = ["cli"] - -[[example]] -name = "album" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/album.rs" - -[[example]] -name = "current_user_recently_played" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/current_user_recently_played.rs" - -[[example]] -name = "oauth_tokens" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/oauth_tokens.rs" - -[[example]] -name = "track" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/track.rs" - -[[example]] -name = "tracks" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/tracks.rs" - -[[example]] -name = "with_refresh_token" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/with_refresh_token.rs" - -[[example]] -name = "device" -required-features = ["env-file", "cli", "client-ureq"] -path = "examples/ureq/device.rs" - -[[example]] -name = "me" -required-features = ["env-file", "cli", "client-ureq"] -path = "examples/ureq/me.rs" - -[[example]] -name = "search" -required-features = ["env-file", "cli", "client-ureq"] -path = "examples/ureq/search.rs" - -[[example]] -name = "seek_track" -required-features = ["env-file", "cli", "client-ureq"] -path = "examples/ureq/seek_track.rs" - -[[example]] -name = "pagination_manual" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/pagination_manual.rs" - -[[example]] -name = "pagination_sync" -required-features = ["env-file", "cli", "client-ureq"] -path = "examples/pagination_sync.rs" - -[[example]] -name = "pagination_async" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/pagination_async.rs" diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index a54340e2..51c2a07b 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -26,13 +26,13 @@ use std::collections::HashMap; use std::fmt; use maybe_async::maybe_async; -use serde_json::Value; use rspotify_model::ApiError; +use serde_json::Value; #[cfg(feature = "client-reqwest")] -pub use self::reqwest::ReqwestClient as Client; +pub use self::reqwest::ReqwestClient as HttpClient; #[cfg(feature = "client-ureq")] -pub use self::ureq::UreqClient as Client; +pub use self::ureq::UreqClient as HttpClient; pub type Headers = HashMap; pub type Query<'a> = HashMap<&'a str, &'a str>; @@ -91,19 +91,9 @@ pub type Result = std::result::Result; #[maybe_async] pub trait BaseClient: Default + Clone + fmt::Debug { // This internal function should always be given an object value in JSON. - async fn get( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Query, - ) -> Result; + async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result; - async fn post( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result; + async fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; async fn post_form<'a>( &self, @@ -112,22 +102,12 @@ pub trait BaseClient: Default + Clone + fmt::Debug { payload: &Form<'a>, ) -> Result; - async fn put( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result; + async fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; - async fn delete( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result; + async fn delete(&self, url: &str, headers: Option<&Headers>, payload: &Value) + -> Result; } - #[cfg(test)] mod test { use super::*; diff --git a/rspotify-http/src/reqwest.rs b/rspotify-http/src/reqwest.rs index bbef9e73..390cf574 100644 --- a/rspotify-http/src/reqwest.rs +++ b/rspotify-http/src/reqwest.rs @@ -1,7 +1,7 @@ //! The client implementation for the reqwest HTTP client, which is async by //! default. -use super::{BaseClient, Error, Result, Form, Headers, Query}; +use super::{BaseClient, Error, Form, Headers, Query, Result}; use std::convert::TryInto; @@ -96,23 +96,13 @@ impl ReqwestClient { #[async_impl] impl BaseClient for ReqwestClient { #[inline] - async fn get( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Query, - ) -> Result { + async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result { self.request(Method::GET, url, headers, |req| req.query(payload)) .await } #[inline] - async fn post( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result { + async fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result { self.request(Method::POST, url, headers, |req| req.json(payload)) .await } @@ -129,12 +119,7 @@ impl BaseClient for ReqwestClient { } #[inline] - async fn put( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result { + async fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result { self.request(Method::PUT, url, headers, |req| req.json(payload)) .await } diff --git a/rspotify-http/src/ureq.rs b/rspotify-http/src/ureq.rs index c84dbf8a..c923b9f2 100644 --- a/rspotify-http/src/ureq.rs +++ b/rspotify-http/src/ureq.rs @@ -1,6 +1,6 @@ //! The client implementation for the ureq HTTP client, which is blocking. -use super::{BaseClient, Form, Headers, Query, Result, Error}; +use super::{BaseClient, Error, Form, Headers, Query, Result}; use maybe_async::sync_impl; use serde_json::Value; @@ -101,12 +101,7 @@ impl BaseClient for UreqClient { } #[inline] - fn delete( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> Result { + fn delete(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result { let request = ureq::delete(url); let sender = |req: Request| req.send_json(payload.clone()); self.request(request, headers, sender) diff --git a/rspotify-model/src/lib.rs b/rspotify-model/src/lib.rs index f666ba88..c0f0838a 100644 --- a/rspotify-model/src/lib.rs +++ b/rspotify-model/src/lib.rs @@ -239,9 +239,9 @@ pub use idtypes::{ UserIdBuf, }; pub use { - album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, error::*, image::*, - offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, track::*, - user::*, + album::*, artist::*, audio::*, category::*, context::*, device::*, enums::*, error::*, + image::*, offset::*, page::*, playing::*, playlist::*, recommend::*, search::*, show::*, + track::*, user::*, }; #[cfg(test)] diff --git a/src/client_creds.rs b/src/client_creds.rs index 601242a1..26b11b1a 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,23 +1,34 @@ -use crate::{prelude::*, Credentials, HTTPClient, Token}; +use crate::{endpoints::BaseClient, prelude::*, Config, Credentials, HTTPClient, Token}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { - creds: Credentials, - tok: Option, - http: HTTPClient, + pub config: Config, + pub creds: Credentials, + pub token: Option, + pub(in crate) http: HTTPClient, } impl ClientCredentialsSpotify { pub fn new(creds: Credentials) -> Self { ClientCredentialsSpotify { creds, - tok: None, + token: None, + http: HTTPClient {}, + ..Default::default() + } + } + + pub fn with_config(creds: Credentials, config: Config) { + ClientCredentialsSpotify { + creds, + config, + token: None, http: HTTPClient {}, } } pub fn request_token(&mut self) { - self.tok = Some(Token("client credentials token".to_string())) + self.token = Some(Token("client credentials token".to_string())) } } @@ -28,7 +39,7 @@ impl BaseClient for ClientCredentialsSpotify { } fn get_token(&self) -> Option<&Token> { - self.tok.as_ref() + self.token.as_ref() } fn get_creds(&self) -> &Credentials { diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 1dd3f5dd..a14a6812 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,8 +1,23 @@ -use crate::{Credentials, HTTPClient, Token}; +use crate::{ + endpoints::{ + join_ids, + pagination::{paginate, Paginator}, + }, + http::HttpClient, + http::Query, + macros::{build_json, build_map}, + model::*, + ClientResult, Config, Credentials, Token, +}; + use std::collections::HashMap; +use maybe_async::maybe_async; + +#[maybe_async] pub trait BaseClient { - fn get_http(&self) -> &HTTPClient; + fn get_config(&self) -> &Config; + fn get_http(&self) -> &HttpClient; fn get_token(&self) -> Option<&Token>; fn get_creds(&self) -> &Credentials; @@ -20,8 +35,825 @@ pub trait BaseClient { self.request(params); } - fn base_endpoint(&self) { - println!("Performing base request"); - self.endpoint_request(); + /// Returns a single track given the track's ID, URI or URL. + /// + /// Parameters: + /// - track_id - a spotify URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track) + async fn track(&self, track_id: &TrackId) -> ClientResult { + let url = format!("tracks/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of tracks given a list of track IDs, URIs, or URLs. + /// + /// Parameters: + /// - track_ids - a list of spotify URIs, URLs or IDs + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) + async fn tracks<'a>( + &self, + track_ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(track_ids); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("tracks/?ids={}", ids); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result).map(|x| x.tracks) + } + + /// Returns a single artist given the artist's ID, URI or URL. + /// + /// Parameters: + /// - artist_id - an artist ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artist) + async fn artist(&self, artist_id: &ArtistId) -> ClientResult { + let url = format!("artists/{}", artist_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of artists given the artist IDs, URIs, or URLs. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs, URIs or URLs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) + async fn artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(artist_ids); + let url = format!("artists/?ids={}", ids); + let result = self.endpoint_get(&url, &Query::new()).await?; + + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Get Spotify catalog information about an artist's albums. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// - album_type - 'album', 'single', 'appears_on', 'compilation' + /// - market - limit the response to one particular country. + /// - limit - the number of albums to return + /// - offset - the index of the first album to return + /// + /// See [`Spotify::artist_albums_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) + fn artist_albums<'a>( + &'a self, + artist_id: &'a ArtistId, + album_type: Option<&'a AlbumType>, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::artist_albums`]. + async fn artist_albums_manual( + &self, + artist_id: &ArtistId, + album_type: Option<&AlbumType>, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit: Option = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "album_type": album_type.map(|x| x.as_ref()), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("artists/{}/albums", artist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information about an artist's top 10 tracks by + /// country. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// - market - limit the response to one particular country. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-top-tracks) + async fn artist_top_tracks( + &self, + artist_id: &ArtistId, + market: Market, + ) -> ClientResult> { + let params = build_map! { + "market": market.as_ref() + }; + + let url = format!("artists/{}/top-tracks", artist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result).map(|x| x.tracks) + } + + /// Get Spotify catalog information about artists similar to an identified + /// artist. Similarity is based on analysis of the Spotify community's + /// listening history. + /// + /// Parameters: + /// - artist_id - the artist ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-related-artists) + async fn artist_related_artists(&self, artist_id: &ArtistId) -> ClientResult> { + let url = format!("artists/{}/related-artists", artist_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Returns a single album given the album's ID, URIs or URL. + /// + /// Parameters: + /// - album_id - the album ID, URI or URL + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album) + async fn album(&self, album_id: &AlbumId) -> ClientResult { + let url = format!("albums/{}", album_id.id()); + + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Returns a list of albums given the album IDs, URIs, or URLs. + /// + /// Parameters: + /// - albums_ids - a list of album IDs, URIs or URLs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) + async fn albums<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(album_ids); + let url = format!("albums/?ids={}", ids); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result::(&result).map(|x| x.albums) + } + + /// Search for an Item. Get Spotify catalog information about artists, + /// albums, tracks or playlists that match a keyword string. + /// + /// Parameters: + /// - q - the search query + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// - type - the type of item to return. One of 'artist', 'album', 'track', + /// 'playlist', 'show' or 'episode' + /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - include_external: Optional.Possible values: audio. If + /// include_external=audio is specified the response will include any + /// relevant audio content that is hosted externally. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#category-search) + async fn search( + &self, + q: &str, + _type: SearchType, + market: Option<&Market>, + include_external: Option<&IncludeExternal>, + limit: Option, + offset: Option, + ) -> ClientResult { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + "q": q, + "type": _type.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "include_external": include_external.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("search", ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information about an album's tracks. + /// + /// Parameters: + /// - album_id - the album ID, URI or URL + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::album_track_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) + fn album_track<'a>( + &'a self, + album_id: &'a AlbumId, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::album_track`]. + async fn album_track_manual( + &self, + album_id: &AlbumId, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("albums/{}/tracks", album_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Gets basic profile information about a Spotify User. + /// + /// Parameters: + /// - user - the id of the usr + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-profile) + async fn user(&self, user_id: &UserId) -> ClientResult { + let url = format!("users/{}", user_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get full details about Spotify playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlist) + async fn playlist( + &self, + playlist_id: &PlaylistId, + fields: Option<&str>, + market: Option<&Market>, + ) -> ClientResult { + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("playlists/{}", playlist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for a single show identified by its unique Spotify ID. + /// + /// Path Parameters: + /// - id: The Spotify ID for the show. + /// + /// Query Parameters + /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) + async fn get_a_show(&self, id: &ShowId, market: Option<&Market>) -> ClientResult { + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let url = format!("shows/{}", id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for multiple shows based on their + /// Spotify IDs. + /// + /// Query Parameters + /// - ids(Required) A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) + async fn get_several_shows<'a>( + &self, + ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get("shows", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.shows) + } + + /// Get Spotify catalog information about an show’s episodes. Optional + /// parameters can be used to limit the number of episodes returned. + /// + /// Path Parameters + /// - id: The Spotify ID for the show. + /// + /// Query Parameters + /// - limit: Optional. The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50. + /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// See [`Spotify::get_shows_episodes_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) + fn get_shows_episodes<'a>( + &'a self, + id: &'a ShowId, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::get_shows_episodes`]. + async fn get_shows_episodes_manual( + &self, + id: &ShowId, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + + let url = format!("shows/{}/episodes", id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. + /// + /// Path Parameters + /// - id: The Spotify ID for the episode. + /// + /// Query Parameters + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-episode) + async fn get_an_episode( + &self, + id: &EpisodeId, + market: Option<&Market>, + ) -> ClientResult { + let url = format!("episodes/{}", id.id()); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Get Spotify catalog information for multiple episodes based on their Spotify IDs. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) + async fn get_several_episodes<'a>( + &self, + ids: impl IntoIterator, + market: Option<&Market>, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + optional "market": market.map(|x| x.as_ref()), + }; + + let result = self.endpoint_get("episodes", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.episodes) + } + + /// Get audio features for a track + /// + /// Parameters: + /// - track - track URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-features) + async fn track_features(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-features/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get Audio Features for Several Tracks + /// + /// Parameters: + /// - tracks a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) + async fn tracks_features<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult>> { + let url = format!("audio-features/?ids={}", join_ids(track_ids)); + + let result = self.endpoint_get(&url, &Query::new()).await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result::>(&result) + .map(|option_payload| option_payload.map(|x| x.audio_features)) + } + } + + /// Get Audio Analysis for a Track + /// + /// Parameters: + /// - track_id - a track URI, URL or ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-analysis) + async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { + let url = format!("audio-analysis/{}", track_id.id()); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get a list of new album releases featured in Spotify + /// + /// Parameters: + /// - country - An ISO 3166-1 alpha-2 country code or string from_token. + /// - locale - The desired language, consisting of an ISO 639 language code + /// and an ISO 3166-1 alpha-2 country code, joined by an underscore. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::categories_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) + fn categories<'a>( + &'a self, + locale: Option<&'a str>, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::categories`]. + async fn categories_manual( + &self, + locale: Option<&str>, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + let result = self.endpoint_get("browse/categories", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.categories) + } + + /// Get a list of playlists in a category in Spotify + /// + /// Parameters: + /// - category_id - The category id to get playlists from. + /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::category_playlists_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) + fn category_playlists<'a>( + &'a self, + category_id: &'a str, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::category_playlists`]. + async fn category_playlists_manual( + &self, + category_id: &str, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("browse/categories/{}/playlists", category_id); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.playlists) + } + + /// Get a list of Spotify featured playlists. + /// + /// Parameters: + /// - locale - The desired language, consisting of a lowercase ISO 639 + /// language code and an uppercase ISO 3166-1 alpha-2 country code, + /// joined by an underscore. + /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. + /// - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use + /// this parameter to specify the user's local time to get results + /// tailored for that specific date and time in the day + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 + /// (the first object). Use with limit to get the next set of + /// items. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-featured-playlists) + async fn featured_playlists( + &self, + locale: Option<&str>, + country: Option<&Market>, + timestamp: Option>, + limit: Option, + offset: Option, + ) -> ClientResult { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let timestamp = timestamp.map(|x| x.to_rfc3339()); + let params = build_map! { + optional "locale": locale, + optional "country": country.map(|x| x.as_ref()), + optional "timestamp": timestamp.as_deref(), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self + .endpoint_get("browse/featured-playlists", ¶ms) + .await?; + self.convert_result(&result) + } + + /// Get a list of new album releases featured in Spotify. + /// + /// Parameters: + /// - country - An ISO 3166-1 alpha-2 country code or string from_token. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 50 + /// - offset - The index of the first item to return. Default: 0 (the first + /// object). Use with limit to get the next set of items. + /// + /// See [`Spotify::new_releases_manual`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) + fn new_releases<'a>( + &'a self, + country: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::new_releases`]. + async fn new_releases_manual( + &self, + country: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("browse/new-releases", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.albums) + } + + /// Get Recommendations Based on Seeds + /// + /// Parameters: + /// - seed_artists - a list of artist IDs, URIs or URLs + /// - seed_tracks - a list of artist IDs, URIs or URLs + /// - seed_genres - a list of genre names. Available genres for + /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. If provided, all + /// results will be playable in this country. + /// - limit - The maximum number of items to return. Default: 20. + /// Minimum: 1. Maximum: 100 + /// - min/max/target_ - For the tuneable track attributes listed + /// in the documentation, these values provide filters and targeting on + /// results. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recommendations) + async fn recommendations( + &self, + payload: &Map, + seed_artists: Option>, + seed_genres: Option>, + seed_tracks: Option>, + limit: Option, + market: Option<&Market>, + ) -> ClientResult { + let seed_artists = seed_artists.map(join_ids); + let seed_genres = seed_genres.map(|x| x.join(",")); + let seed_tracks = seed_tracks.map(join_ids); + let limit = limit.map(|x| x.to_string()); + let mut params = build_map! { + optional "seed_artists": seed_artists.as_ref(), + optional "seed_genres": seed_genres.as_ref(), + optional "seed_tracks": seed_tracks.as_ref(), + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_ref(), + }; + + // TODO: this probably can be improved. + let attributes = [ + "acousticness", + "danceability", + "duration_ms", + "energy", + "instrumentalness", + "key", + "liveness", + "loudness", + "mode", + "popularity", + "speechiness", + "tempo", + "time_signature", + "valence", + ]; + let mut map_to_hold_owned_value = HashMap::new(); + let prefixes = ["min", "max", "target"]; + for attribute in attributes.iter() { + for prefix in prefixes.iter() { + let param = format!("{}_{}", prefix, attribute); + if let Some(value) = payload.get(¶m) { + // TODO: not sure if this `to_string` is what we want. It + // might add quotes to the strings. + map_to_hold_owned_value.insert(param, value.to_string()); + } + } + } + + for (ref key, ref value) in &map_to_hold_owned_value { + params.insert(key, value); + } + + let result = self.endpoint_get("recommendations", ¶ms).await?; + self.convert_result(&result) + } + + /// Get full details of the tracks of a playlist owned by a user. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - fields - which fields to return + /// - limit - the maximum number of tracks to return + /// - offset - the index of the first track to return + /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// See [`Spotify::playlist_tracks`] for a manually paginated version of + /// this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) + fn playlist_tracks<'a>( + &'a self, + playlist_id: &'a PlaylistId, + fields: Option<&'a str>, + market: Option<&'a Market>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::playlist_tracks`]. + async fn playlist_tracks_manual( + &self, + playlist_id: &PlaylistId, + fields: Option<&str>, + market: Option<&Market>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "fields": fields, + optional "market": market.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Gets playlists of a user. + /// + /// Parameters: + /// - user_id - the id of the usr + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::user_playlists_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) + fn user_playlists<'a>( + &'a self, + user_id: &'a UserId, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::user_playlists`]. + async fn user_playlists_manual( + &self, + user_id: &UserId, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let url = format!("users/{}/playlists", user_id.id()); + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) } } diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 77908150..6aee967d 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -1,9 +1,40 @@ pub mod base; pub mod oauth; +pub mod pagination; -pub use base::SimpleClient; +pub use base::BaseClient; pub use oauth::OAuthClient; +use crate::{ + model::{idtypes::IdType, Id}, + ClientResult, +}; + +use serde::Deserialize; + +/// Converts a JSON response from Spotify into its model. +pub(in crate) fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> ClientResult { + serde_json::from_str::(input).map_err(Into::into) +} + +/// Append device ID to an API path. +pub(in crate) fn append_device_id(path: &str, device_id: Option<&str>) -> String { + let mut new_path = path.to_string(); + if let Some(_device_id) = device_id { + if path.contains('?') { + new_path.push_str(&format!("&device_id={}", _device_id)); + } else { + new_path.push_str(&format!("?device_id={}", _device_id)); + } + } + new_path +} + +#[inline] +pub(in crate) fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { + ids.into_iter().collect::>().join(",") +} + /// HTTP-related methods for the Spotify client. It wraps the basic HTTP client /// with features needed of higher level. /// @@ -133,20 +164,19 @@ impl Spotify { } } +/// Generates an HTTP token authorization header with proper formatting +pub fn bearer_auth(tok: &Token) -> (String, String) { + let auth = "authorization".to_owned(); + let value = format!("Bearer {}", tok.access_token); - /// Generates an HTTP token authorization header with proper formatting - pub fn bearer_auth(tok: &Token) -> (String, String) { - let auth = "authorization".to_owned(); - let value = format!("Bearer {}", tok.access_token); - - (auth, value) - } + (auth, value) +} - /// Generates an HTTP basic authorization header with proper formatting - pub fn basic_auth(user: &str, password: &str) -> (String, String) { - let auth = "authorization".to_owned(); - let value = format!("{}:{}", user, password); - let value = format!("Basic {}", base64::encode(value)); +/// Generates an HTTP basic authorization header with proper formatting +pub fn basic_auth(user: &str, password: &str) -> (String, String) { + let auth = "authorization".to_owned(); + let value = format!("{}:{}", user, password); + let value = format!("Basic {}", base64::encode(value)); - (auth, value) - } + (auth, value) +} diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index c8e1f820..d73a4c39 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -1,5 +1,23 @@ -use crate::rspotify::{prelude::*, OAuth}; +use std::time; +use crate::{ + endpoints::{ + join_ids, + pagination::{paginate, Paginator}, + BaseClient, + }, + http::Query, + macros::{build_json, build_map}, + model::*, + ClientResult, OAuth, +}; + +use log::error; +use maybe_async::maybe_async; +use rspotify_model::idtypes::PlayContextIdType; +use serde_json::{json, Map}; + +#[maybe_async] pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; @@ -7,4 +25,1148 @@ pub trait OAuthClient: BaseClient { println!("Performing OAuth request"); self.endpoint_request(); } + + /// Get current user playlists without required getting his profile. + /// + /// Parameters: + /// - limit - the number of items to return + /// - offset - the index of the first item to return + /// + /// See [`Spotify::current_user_playlists_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) + fn current_user_playlists(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_playlists`]. + async fn current_user_playlists_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/playlists", ¶ms).await?; + self.convert_result(&result) + } + + /// Gets playlist of a user. + /// + /// Parameters: + /// - user_id - the id of the user + /// - playlist_id - the id of the playlist + /// - fields - which fields to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) + async fn user_playlist( + &self, + user_id: &UserId, + playlist_id: Option<&PlaylistId>, + fields: Option<&str>, + ) -> ClientResult { + let params = build_map! { + optional "fields": fields, + }; + + let url = match playlist_id { + Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), + None => format!("users/{}/starred", user_id.id()), + }; + let result = self.endpoint_get(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Creates a playlist for a user. + /// + /// Parameters: + /// - user_id - the id of the user + /// - name - the name of the playlist + /// - public - is the created playlist public + /// - description - the description of the playlist + /// - collaborative - if the playlist will be collaborative + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist) + async fn user_playlist_create( + &self, + user_id: &UserId, + name: &str, + public: Option, + collaborative: Option, + description: Option<&str>, + ) -> ClientResult { + let params = build_json! { + "name": name, + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + + let url = format!("users/{}/playlists", user_id.id()); + let result = self.endpoint_post(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Changes a playlist's name and/or public/private state. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - name - optional name of the playlist + /// - public - optional is the playlist public + /// - collaborative - optional is the playlist collaborative + /// - description - optional description of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-change-playlist-details) + async fn playlist_change_detail( + &self, + playlist_id: &str, + name: Option<&str>, + public: Option, + description: Option<&str>, + collaborative: Option, + ) -> ClientResult { + let params = build_json! { + optional "name": name, + optional "public": public, + optional "collaborative": collaborative, + optional "description": description, + }; + + let url = format!("playlists/{}", playlist_id); + self.endpoint_put(&url, ¶ms).await + } + + /// Unfollows (deletes) a playlist for a user. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-playlist) + async fn playlist_unfollow(&self, playlist_id: &str) -> ClientResult { + let url = format!("playlists/{}/followers", playlist_id); + self.endpoint_delete(&url, &json!({})).await + } + + /// Adds tracks to a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - track_ids - a list of track URIs, URLs or IDs + /// - position - the position to add the tracks + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) + async fn playlist_add_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + position: Option, + ) -> ClientResult { + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris, + "position": position, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_post(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Replace all tracks in a playlist + /// + /// Parameters: + /// - user - the id of the user + /// - playlist_id - the id of the playlist + /// - tracks - the list of track ids to add to the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) + async fn playlist_replace_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); + let params = build_json! { + "uris": uris + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Reorder tracks in a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - uris - a list of Spotify URIs to replace or clear + /// - range_start - the position of the first track to be reordered + /// - insert_before - the position where the tracks should be inserted + /// - range_length - optional the number of tracks to be reordered (default: + /// 1) + /// - snapshot_id - optional playlist's snapshot ID + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) + async fn playlist_reorder_tracks( + &self, + playlist_id: &PlaylistId, + uris: Option<&[&Id]>, + range_start: Option, + insert_before: Option, + range_length: Option, + snapshot_id: Option<&str>, + ) -> ClientResult { + let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); + let params = build_json! { + "playlist_id": playlist_id, + optional "uris": uris, + optional "range_start": range_start, + optional "insert_before": insert_before, + optional "range_length": range_length, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_put(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Removes all occurrences of the given tracks from the given playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - track_ids - the list of track ids to add to the playlist + /// - snapshot_id - optional id of the playlist snapshot + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) + async fn playlist_remove_all_occurrences_of_tracks<'a>( + &self, + playlist_id: &PlaylistId, + track_ids: impl IntoIterator, + snapshot_id: Option<&str>, + ) -> ClientResult { + let tracks = track_ids + .into_iter() + .map(|id| { + let mut map = Map::with_capacity(1); + map.insert("uri".to_owned(), id.uri().into()); + map + }) + .collect::>(); + + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_delete(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Removes specfic occurrences of the given tracks from the given playlist. + /// + /// Parameters: + /// - playlist_id: the id of the playlist + /// - tracks: an array of map containing Spotify URIs of the tracks to + /// remove with their current positions in the playlist. For example: + /// + /// ```json + /// { + /// "tracks":[ + /// { + /// "uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh", + /// "positions":[ + /// 0, + /// 3 + /// ] + /// }, + /// { + /// "uri":"spotify:track:1301WleyT98MSxVHPZCA6M", + /// "positions":[ + /// 7 + /// ] + /// } + /// ] + /// } + /// ``` + /// - snapshot_id: optional id of the playlist snapshot + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) + async fn playlist_remove_specific_occurrences_of_tracks( + &self, + playlist_id: &PlaylistId, + tracks: Vec>, + snapshot_id: Option<&str>, + ) -> ClientResult { + let tracks = tracks + .into_iter() + .map(|track| { + let mut map = Map::new(); + map.insert("uri".to_owned(), track.id.uri().into()); + map.insert("positions".to_owned(), track.positions.into()); + map + }) + .collect::>(); + + let params = build_json! { + "tracks": tracks, + optional "snapshot_id": snapshot_id, + }; + + let url = format!("playlists/{}/tracks", playlist_id.id()); + let result = self.endpoint_delete(&url, ¶ms).await?; + self.convert_result(&result) + } + + /// Add the current authenticated user as a follower of a playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-playlist) + async fn playlist_follow( + &self, + playlist_id: &PlaylistId, + public: Option, + ) -> ClientResult<()> { + let url = format!("playlists/{}/followers", playlist_id.id()); + + let params = build_json! { + optional "public": public, + }; + + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Check to see if the given users are following the given playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - user_ids - the ids of the users that you want to + /// check to see if they follow the playlist. Maximum: 5 ids. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) + async fn playlist_check_follow( + &self, + playlist_id: &PlaylistId, + user_ids: &[&UserId], + ) -> ClientResult> { + if user_ids.len() > 5 { + error!("The maximum length of user ids is limited to 5 :-)"); + } + let url = format!( + "playlists/{}/followers/contains?ids={}", + playlist_id.id(), + user_ids + .iter() + .map(|id| id.id()) + .collect::>() + .join(","), + ); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Get detailed profile information about the current user. + /// An alias for the 'current_user' method. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) + async fn me(&self) -> ClientResult { + let result = self.endpoint_get("me/", &Query::new()).await?; + self.convert_result(&result) + } + + /// Get detailed profile information about the current user. + /// An alias for the 'me' method. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) + async fn current_user(&self) -> ClientResult { + self.me().await + } + + /// Get information about the current users currently playing track. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) + async fn current_user_playing_track(&self) -> ClientResult> { + let result = self + .get("me/player/currently-playing", None, &Query::new()) + .await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Gets a list of the albums saved in the current authorized user's + /// "Your Music" library + /// + /// Parameters: + /// - limit - the number of albums to return + /// - offset - the index of the first album to return + /// - market - Provide this parameter if you want to apply Track Relinking. + /// + /// See [`Spotify::current_user_saved_albums`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) + fn current_user_saved_albums(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of + /// [`Spotify::current_user_saved_albums`]. + async fn current_user_saved_albums_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/albums", ¶ms).await?; + self.convert_result(&result) + } + + /// Get a list of the songs saved in the current Spotify user's "Your Music" + /// library. + /// + /// Parameters: + /// - limit - the number of tracks to return + /// - offset - the index of the first track to return + /// - market - Provide this parameter if you want to apply Track Relinking. + /// + /// See [`Spotify::current_user_saved_tracks_manual`] for a manually + /// paginated version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) + fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of + /// [`Spotify::current_user_saved_tracks`]. + async fn current_user_saved_tracks_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/tracks", ¶ms).await?; + self.convert_result(&result) + } + + /// Gets a list of the artists followed by the current authorized user. + /// + /// Parameters: + /// - after - the last artist ID retrieved from the previous request + /// - limit - the number of tracks to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-followed) + async fn current_user_followed_artists( + &self, + after: Option<&str>, + limit: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let params = build_map! { + "r#type": Type::Artist.as_ref(), + optional "after": after, + optional "limit": limit.as_deref(), + }; + + let result = self.endpoint_get("me/following", ¶ms).await?; + self.convert_result::(&result) + .map(|x| x.artists) + } + + /// Remove one or more tracks from the current user's "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) + async fn current_user_saved_tracks_delete<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check if one or more tracks is already saved in the current Spotify + /// user’s "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) + async fn current_user_saved_tracks_contains<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Save one or more tracks to the current user's "Your Music" library. + /// + /// Parameters: + /// - track_ids - a list of track URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) + async fn current_user_saved_tracks_add<'a>( + &self, + track_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/tracks/?ids={}", join_ids(track_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Get the current user's top artists. + /// + /// Parameters: + /// - limit - the number of entities to return + /// - offset - the index of the first entity to return + /// - time_range - Over what time frame are the affinities computed + /// + /// See [`Spotify::current_user_top_artists_manual`] for a manually + /// paginated version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) + fn current_user_top_artists<'a>( + &'a self, + time_range: Option<&'a TimeRange>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_top_artists`]. + async fn current_user_top_artists_manual( + &self, + time_range: Option<&TimeRange>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|s| s.to_string()); + let offset = offset.map(|s| s.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; + self.convert_result(&result) + } + + /// Get the current user's top tracks. + /// + /// Parameters: + /// - limit - the number of entities to return + /// - offset - the index of the first entity to return + /// - time_range - Over what time frame are the affinities computed + /// + /// See [`Spotify::current_user_top_tracks_manual`] for a manually paginated + /// version of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) + fn current_user_top_tracks<'a>( + &'a self, + time_range: Option<&'a TimeRange>, + ) -> impl Paginator> + 'a { + paginate( + move |limit, offset| { + self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) + }, + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::current_user_top_tracks`]. + async fn current_user_top_tracks_manual( + &self, + time_range: Option<&TimeRange>, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "time_range": time_range.map(|x| x.as_ref()), + optional "limit": limit.as_deref(), + optional "offset": offset.as_deref(), + }; + + let result = self.endpoint_get("me/top/tracks", ¶ms).await?; + self.convert_result(&result) + } + + /// Get the current user's recently played tracks. + /// + /// Parameters: + /// - limit - the number of entities to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track) + async fn current_user_recently_played( + &self, + limit: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_deref(), + }; + + let result = self + .endpoint_get("me/player/recently-played", ¶ms) + .await?; + self.convert_result(&result) + } + + /// Add one or more albums to the current user's "Your Music" library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) + async fn current_user_saved_albums_add<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/albums/?ids={}", join_ids(album_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Remove one or more albums from the current user's "Your Music" library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) + async fn current_user_saved_albums_delete<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/albums/?ids={}", join_ids(album_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check if one or more albums is already saved in the current Spotify + /// user’s "Your Music” library. + /// + /// Parameters: + /// - album_ids - a list of album URIs, URLs or IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) + async fn current_user_saved_albums_contains<'a>( + &self, + album_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Follow one or more artists. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) + async fn user_follow_artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Unfollow one or more artists. + /// + /// Parameters: + /// - artist_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) + async fn user_unfollow_artists<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Check to see if the current user is following one or more artists or + /// other Spotify users. + /// + /// Parameters: + /// - artist_ids - the ids of the users that you want to + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) + async fn user_artist_check_follow<'a>( + &self, + artist_ids: impl IntoIterator, + ) -> ClientResult> { + let url = format!( + "me/following/contains?type=artist&ids={}", + join_ids(artist_ids) + ); + let result = self.endpoint_get(&url, &Query::new()).await?; + self.convert_result(&result) + } + + /// Follow one or more users. + /// + /// Parameters: + /// - user_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) + async fn user_follow_users<'a>( + &self, + user_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Unfollow one or more users. + /// + /// Parameters: + /// - user_ids - a list of artist IDs + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) + async fn user_unfollow_users<'a>( + &self, + user_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); + self.endpoint_delete(&url, &json!({})).await?; + + Ok(()) + } + + /// Get a User’s Available Devices + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-users-available-devices) + async fn device(&self) -> ClientResult> { + let result = self + .endpoint_get("me/player/devices", &Query::new()) + .await?; + self.convert_result::(&result) + .map(|x| x.devices) + } + + /// Get Information About The User’s Current Playback + /// + /// Parameters: + /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. + /// - additional_types: Optional. A comma-separated list of item types that + /// your client supports besides the default track type. Valid types are: + /// `track` and `episode`. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-information-about-the-users-current-playback) + async fn current_playback( + &self, + country: Option<&Market>, + additional_types: Option>, + ) -> ClientResult> { + let additional_types = + additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); + let params = build_map! { + optional "country": country.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_ref(), + }; + + let result = self.endpoint_get("me/player", ¶ms).await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Get the User’s Currently Playing Track + /// + /// Parameters: + /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. + /// - additional_types: Optional. A comma-separated list of item types that + /// your client supports besides the default track type. Valid types are: + /// `track` and `episode`. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) + async fn current_playing( + &self, + market: Option<&Market>, + additional_types: Option>, + ) -> ClientResult> { + let additional_types = + additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); + let params = build_map! { + optional "market": market.map(|x| x.as_ref()), + optional "additional_types": additional_types.as_ref(), + }; + + let result = self + .get("me/player/currently-playing", None, ¶ms) + .await?; + if result.is_empty() { + Ok(None) + } else { + self.convert_result(&result) + } + } + + /// Transfer a User’s Playback. + /// + /// Note: Although an array is accepted, only a single device_id is + /// currently supported. Supplying more than one will return 400 Bad Request + /// + /// Parameters: + /// - device_id - transfer playback to this device + /// - force_play - true: after transfer, play. false: + /// keep current state. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-transfer-a-users-playback) + async fn transfer_playback( + &self, + device_id: &str, + force_play: Option, + ) -> ClientResult<()> { + let params = build_json! { + "device_ids": [device_id], + optional "force_play": force_play, + }; + + self.endpoint_put("me/player", ¶ms).await?; + Ok(()) + } + + /// Start/Resume a User’s Playback. + /// + /// Provide a `context_uri` to start playback or a album, artist, or + /// playlist. Provide a `uris` list to start playback of one or more tracks. + /// Provide `offset` as {"position": } or {"uri": ""} to + /// start playback at a particular offset. + /// + /// Parameters: + /// - device_id - device target for playback + /// - context_uri - spotify context uri to play + /// - uris - spotify track uris + /// - offset - offset into context by index or track + /// - position_ms - Indicates from what position to start playback. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) + async fn start_context_playback( + &self, + context_uri: &Id, + device_id: Option<&str>, + offset: Option>, + position_ms: Option, + ) -> ClientResult<()> { + let params = build_json! { + "context_uri": context_uri.uri(), + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), + optional "position_ms": position_ms, + + }; + + let url = self.append_device_id("me/player/play", device_id); + self.put(&url, None, ¶ms).await?; + + Ok(()) + } + + async fn start_uris_playback( + &self, + uris: &[&Id], + device_id: Option<&str>, + offset: Option>, + position_ms: Option, + ) -> ClientResult<()> { + let params = build_json! { + "uris": uris.iter().map(|id| id.uri()).collect::>(), + optional "position_ms": position_ms, + optional "offset": offset.map(|x| match x { + Offset::Position(position) => json!({ "position": position }), + Offset::Uri(uri) => json!({ "uri": uri.uri() }), + }), + }; + + let url = self.append_device_id("me/player/play", device_id); + self.endpoint_put(&url, ¶ms).await?; + + Ok(()) + } + + /// Pause a User’s Playback. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) + async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/pause", device_id); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Skip User’s Playback To Next Track. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) + async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/next", device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Skip User’s Playback To Previous Track. + /// + /// Parameters: + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) + async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id("me/player/previous", device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Seek To Position In Currently Playing Track. + /// + /// Parameters: + /// - position_ms - position in milliseconds to seek to + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) + async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id( + &format!("me/player/seek?position_ms={}", position_ms), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Set Repeat Mode On User’s Playback. + /// + /// Parameters: + /// - state - `track`, `context`, or `off` + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) + async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id( + &format!("me/player/repeat?state={}", state.as_ref()), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Set Volume For User’s Playback. + /// + /// Parameters: + /// - volume_percent - volume between 0 and 100 + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) + async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { + if volume_percent > 100u8 { + error!("volume must be between 0 and 100, inclusive"); + } + let url = self.append_device_id( + &format!("me/player/volume?volume_percent={}", volume_percent), + device_id, + ); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Toggle Shuffle For User’s Playback. + /// + /// Parameters: + /// - state - true or false + /// - device_id - device target for playback + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) + async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { + let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Add an item to the end of the user's playback queue. + /// + /// Parameters: + /// - uri - The uri of the item to add, Track or Episode + /// - device id - The id of the device targeting + /// - If no device ID provided the user's currently active device is + /// targeted + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) + async fn add_item_to_queue( + &self, + item: &Id, + device_id: Option<&str>, + ) -> ClientResult<()> { + let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); + self.endpoint_post(&url, &json!({})).await?; + + Ok(()) + } + + /// Add a show or a list of shows to a user’s library. + /// + /// Parameters: + /// - ids(Required) A comma-separated list of Spotify IDs for the shows to + /// be added to the user’s library. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) + async fn save_shows<'a>( + &self, + show_ids: impl IntoIterator, + ) -> ClientResult<()> { + let url = format!("me/shows/?ids={}", join_ids(show_ids)); + self.endpoint_put(&url, &json!({})).await?; + + Ok(()) + } + + /// Get a list of shows saved in the current Spotify user’s library. + /// Optional parameters can be used to limit the number of shows returned. + /// + /// Parameters: + /// - limit(Optional). The maximum number of shows to return. Default: 20. + /// Minimum: 1. Maximum: 50. + /// - offset(Optional). The index of the first show to return. Default: 0 + /// (the first object). Use with limit to get the next set of shows. + /// + /// See [`Spotify::get_saved_show_manual`] for a manually paginated version + /// of this. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) + fn get_saved_show(&self) -> impl Paginator> + '_ { + paginate( + move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), + self.pagination_chunks, + ) + } + + /// The manually paginated version of [`Spotify::get_saved_show`]. + async fn get_saved_show_manual( + &self, + limit: Option, + offset: Option, + ) -> ClientResult> { + let limit = limit.map(|x| x.to_string()); + let offset = offset.map(|x| x.to_string()); + let params = build_map! { + optional "limit": limit.as_ref(), + optional "offset": offset.as_ref(), + }; + + let result = self.endpoint_get("me/shows", ¶ms).await?; + self.convert_result(&result) + } + + /// Check if one or more shows is already saved in the current Spotify user’s library. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) + async fn check_users_saved_shows<'a>( + &self, + ids: impl IntoIterator, + ) -> ClientResult> { + let ids = join_ids(ids); + let params = build_map! { + "ids": &ids, + }; + let result = self.endpoint_get("me/shows/contains", ¶ms).await?; + self.convert_result(&result) + } + + /// Delete one or more shows from current Spotify user's library. + /// Changes to a user's saved shows may not be visible in other Spotify applications immediately. + /// + /// Query Parameters + /// - ids: Required. A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. + /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) + async fn remove_users_saved_shows<'a>( + &self, + show_ids: impl IntoIterator, + country: Option<&Market>, + ) -> ClientResult<()> { + let url = format!("me/shows?ids={}", join_ids(show_ids)); + let params = build_json! { + optional "country": country.map(|x| x.as_ref()) + }; + self.endpoint_delete(&url, ¶ms).await?; + + Ok(()) + } } diff --git a/src/lib.rs b/src/lib.rs index 43cd4772..a8598b38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -161,19 +161,32 @@ // This way only the compile error below gets shown instead of a whole list of // confusing errors.. -pub mod endpoints; pub mod client_creds; pub mod code_auth; pub mod code_auth_pkce; +pub mod endpoints; // Subcrate re-exports +pub use rspotify_http as http; pub use rspotify_macros as macros; pub use rspotify_model as model; -pub use rspotify_http as http; - // Top-level re-exports pub use macros::scopes; +use std::{ + collections::{HashMap, HashSet}, + env, fs, + io::{Read, Write}, + path::Path, + path::PathBuf, +}; + +use chrono::{DateTime, Duration, Utc}; +use derive_builder::Builder; +use getrandom::getrandom; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] pub enum ClientError { @@ -204,36 +217,13 @@ pub const DEFAULT_API_PREFIX: &str = "https://api.spotify.com/v1/"; pub const DEFAULT_CACHE_PATH: &str = ".spotify_token_cache.json"; pub const DEFAULT_PAGINATION_CHUNKS: u32 = 50; - -/// Spotify API object -#[derive(Builder, Debug, Clone)] -pub struct Spotify { - /// Internal member to perform requests to the Spotify API. - #[builder(setter(skip))] - pub(in crate) http: HTTPClient, - - /// The access token information required for requests to the Spotify API. - #[builder(setter(strip_option), default)] - pub token: Option, - - /// The credentials needed for obtaining a new access token, for requests. - /// without OAuth authentication. - #[builder(setter(strip_option), default)] - pub credentials: Option, - - /// The OAuth information required for obtaining a new access token, for - /// requests with OAuth authentication. `credentials` also needs to be - /// set up. - #[builder(setter(strip_option), default)] - pub oauth: Option, - +/// Struct to configure the Spotify client. +pub struct Config { /// The Spotify API prefix, [`DEFAULT_API_PREFIX`] by default. - #[builder(setter(into), default = "String::from(DEFAULT_API_PREFIX)")] pub prefix: String, /// The cache file path, in case it's used. By default it's /// [`DEFAULT_CACHE_PATH`] - #[builder(default = "PathBuf::from(DEFAULT_CACHE_PATH)")] pub cache_path: PathBuf, /// The pagination chunk size used when performing automatically paginated @@ -243,2106 +233,208 @@ pub struct Spotify { /// /// Note that most endpoints set a maximum to the number of items per /// request, which most times is 50. - #[builder(default = "DEFAULT_PAGINATION_CHUNKS")] pub pagination_chunks: u32, } -// Endpoint-related methods for the client. -impl Spotify { - /// Returns the access token, or an error in case it's not configured. - pub(in crate) fn get_token(&self) -> ClientResult<&Token> { - self.token - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no access token configured".to_string())) - } - - /// Returns the credentials, or an error in case it's not configured. - pub(in crate) fn get_creds(&self) -> ClientResult<&Credentials> { - self.credentials - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no credentials configured".to_string())) - } - - /// Returns the oauth information, or an error in case it's not configured. - pub(in crate) fn get_oauth(&self) -> ClientResult<&OAuth> { - self.oauth - .as_ref() - .ok_or_else(|| ClientError::InvalidAuth("no oauth configured".to_string())) - } - - /// Converts a JSON response from Spotify into its model. - fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { - serde_json::from_str::(input).map_err(Into::into) - } - - /// Append device ID to an API path. - fn append_device_id(&self, path: &str, device_id: Option<&str>) -> String { - let mut new_path = path.to_string(); - if let Some(_device_id) = device_id { - if path.contains('?') { - new_path.push_str(&format!("&device_id={}", _device_id)); - } else { - new_path.push_str(&format!("?device_id={}", _device_id)); - } +impl Default for Config { + fn default() -> Self { + Config { + prefix: String::from(DEFAULT_API_PREFIX), + cache_path: PathBuf::from(DEFAULT_CACHE_PATH), + pagination_chunks: DEFAULT_PAGINATION_CHUNKS, } - new_path - } - - /// Returns a single track given the track's ID, URI or URL. - /// - /// Parameters: - /// - track_id - a spotify URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-track) - #[maybe_async] - pub async fn track(&self, track_id: &TrackId) -> ClientResult { - let url = format!("tracks/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of tracks given a list of track IDs, URIs, or URLs. - /// - /// Parameters: - /// - track_ids - a list of spotify URIs, URLs or IDs - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) - #[maybe_async] - pub async fn tracks<'a>( - &self, - track_ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(track_ids); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("tracks/?ids={}", ids); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) - } - - /// Returns a single artist given the artist's ID, URI or URL. - /// - /// Parameters: - /// - artist_id - an artist ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artist) - #[maybe_async] - pub async fn artist(&self, artist_id: &ArtistId) -> ClientResult { - let url = format!("artists/{}", artist_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of artists given the artist IDs, URIs, or URLs. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs, URIs or URLs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) - #[maybe_async] - pub async fn artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(artist_ids); - let url = format!("artists/?ids={}", ids); - let result = self.endpoint_get(&url, &Query::new()).await?; - - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Get Spotify catalog information about an artist's albums. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - album_type - 'album', 'single', 'appears_on', 'compilation' - /// - market - limit the response to one particular country. - /// - limit - the number of albums to return - /// - offset - the index of the first album to return - /// - /// See [`Spotify::artist_albums_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) - pub fn artist_albums<'a>( - &'a self, - artist_id: &'a ArtistId, - album_type: Option<&'a AlbumType>, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::artist_albums`]. - #[maybe_async] - pub async fn artist_albums_manual( - &self, - artist_id: &ArtistId, - album_type: Option<&AlbumType>, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit: Option = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "album_type": album_type.map(|x| x.as_ref()), - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("artists/{}/albums", artist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information about an artist's top 10 tracks by - /// country. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - market - limit the response to one particular country. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-top-tracks) - #[maybe_async] - pub async fn artist_top_tracks( - &self, - artist_id: &ArtistId, - market: Market, - ) -> ClientResult> { - let params = build_map! { - "market": market.as_ref() - }; - - let url = format!("artists/{}/top-tracks", artist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) - } - - /// Get Spotify catalog information about artists similar to an identified - /// artist. Similarity is based on analysis of the Spotify community's - /// listening history. - /// - /// Parameters: - /// - artist_id - the artist ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-related-artists) - #[maybe_async] - pub async fn artist_related_artists( - &self, - artist_id: &ArtistId, - ) -> ClientResult> { - let url = format!("artists/{}/related-artists", artist_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Returns a single album given the album's ID, URIs or URL. - /// - /// Parameters: - /// - album_id - the album ID, URI or URL - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-album) - #[maybe_async] - pub async fn album(&self, album_id: &AlbumId) -> ClientResult { - let url = format!("albums/{}", album_id.id()); - - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Returns a list of albums given the album IDs, URIs, or URLs. - /// - /// Parameters: - /// - albums_ids - a list of album IDs, URIs or URLs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) - #[maybe_async] - pub async fn albums<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(album_ids); - let url = format!("albums/?ids={}", ids); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result).map(|x| x.albums) - } - - /// Search for an Item. Get Spotify catalog information about artists, - /// albums, tracks or playlists that match a keyword string. - /// - /// Parameters: - /// - q - the search query - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - type - the type of item to return. One of 'artist', 'album', 'track', - /// 'playlist', 'show' or 'episode' - /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - include_external: Optional.Possible values: audio. If - /// include_external=audio is specified the response will include any - /// relevant audio content that is hosted externally. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#category-search) - #[maybe_async] - pub async fn search( - &self, - q: &str, - _type: SearchType, - market: Option<&Market>, - include_external: Option<&IncludeExternal>, - limit: Option, - offset: Option, - ) -> ClientResult { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - "q": q, - "type": _type.as_ref(), - optional "market": market.map(|x| x.as_ref()), - optional "include_external": include_external.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("search", ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information about an album's tracks. - /// - /// Parameters: - /// - album_id - the album ID, URI or URL - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::album_track_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) - pub fn album_track<'a>( - &'a self, - album_id: &'a AlbumId, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::album_track`]. - #[maybe_async] - pub async fn album_track_manual( - &self, - album_id: &AlbumId, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("albums/{}/tracks", album_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Gets basic profile information about a Spotify User. - /// - /// Parameters: - /// - user - the id of the usr - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-profile) - #[maybe_async] - pub async fn user(&self, user_id: &UserId) -> ClientResult { - let url = format!("users/{}", user_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get full details about Spotify playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlist) - #[maybe_async] - pub async fn playlist( - &self, - playlist_id: &PlaylistId, - fields: Option<&str>, - market: Option<&Market>, - ) -> ClientResult { - let params = build_map! { - optional "fields": fields, - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("playlists/{}", playlist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get current user playlists without required getting his profile. - /// - /// Parameters: - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::current_user_playlists_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - pub fn current_user_playlists(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_playlists`]. - #[maybe_async] - pub async fn current_user_playlists_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/playlists", ¶ms).await?; - self.convert_result(&result) - } - - /// Gets playlists of a user. - /// - /// Parameters: - /// - user_id - the id of the usr - /// - limit - the number of items to return - /// - offset - the index of the first item to return - /// - /// See [`Spotify::user_playlists_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - pub fn user_playlists<'a>( - &'a self, - user_id: &'a UserId, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::user_playlists`]. - #[maybe_async] - pub async fn user_playlists_manual( - &self, - user_id: &UserId, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("users/{}/playlists", user_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Gets playlist of a user. - /// - /// Parameters: - /// - user_id - the id of the user - /// - playlist_id - the id of the playlist - /// - fields - which fields to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - #[maybe_async] - pub async fn user_playlist( - &self, - user_id: &UserId, - playlist_id: Option<&PlaylistId>, - fields: Option<&str>, - ) -> ClientResult { - let params = build_map! { - optional "fields": fields, - }; - - let url = match playlist_id { - Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), - None => format!("users/{}/starred", user_id.id()), - }; - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get full details of the tracks of a playlist owned by a user. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - fields - which fields to return - /// - limit - the maximum number of tracks to return - /// - offset - the index of the first track to return - /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// See [`Spotify::playlist_tracks`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) - pub fn playlist_tracks<'a>( - &'a self, - playlist_id: &'a PlaylistId, - fields: Option<&'a str>, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::playlist_tracks`]. - #[maybe_async] - pub async fn playlist_tracks_manual( - &self, - playlist_id: &PlaylistId, - fields: Option<&str>, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "fields": fields, - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Creates a playlist for a user. - /// - /// Parameters: - /// - user_id - the id of the user - /// - name - the name of the playlist - /// - public - is the created playlist public - /// - description - the description of the playlist - /// - collaborative - if the playlist will be collaborative - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-create-playlist) - #[maybe_async] - pub async fn user_playlist_create( - &self, - user_id: &UserId, - name: &str, - public: Option, - collaborative: Option, - description: Option<&str>, - ) -> ClientResult { - let params = build_json! { - "name": name, - optional "public": public, - optional "collaborative": collaborative, - optional "description": description, - }; - - let url = format!("users/{}/playlists", user_id.id()); - let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Changes a playlist's name and/or public/private state. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - name - optional name of the playlist - /// - public - optional is the playlist public - /// - collaborative - optional is the playlist collaborative - /// - description - optional description of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-change-playlist-details) - #[maybe_async] - pub async fn playlist_change_detail( - &self, - playlist_id: &str, - name: Option<&str>, - public: Option, - description: Option<&str>, - collaborative: Option, - ) -> ClientResult { - let params = build_json! { - optional "name": name, - optional "public": public, - optional "collaborative": collaborative, - optional "description": description, - }; - - let url = format!("playlists/{}", playlist_id); - self.endpoint_put(&url, ¶ms).await - } - - /// Unfollows (deletes) a playlist for a user. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-playlist) - #[maybe_async] - pub async fn playlist_unfollow(&self, playlist_id: &str) -> ClientResult { - let url = format!("playlists/{}/followers", playlist_id); - self.endpoint_delete(&url, &json!({})).await - } - - /// Adds tracks to a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - track_ids - a list of track URIs, URLs or IDs - /// - position - the position to add the tracks - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) - #[maybe_async] - pub async fn playlist_add_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - position: Option, - ) -> ClientResult { - let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); - let params = build_json! { - "uris": uris, - "position": position, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Replace all tracks in a playlist - /// - /// Parameters: - /// - user - the id of the user - /// - playlist_id - the id of the playlist - /// - tracks - the list of track ids to add to the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - #[maybe_async] - pub async fn playlist_replace_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); - let params = build_json! { - "uris": uris - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Reorder tracks in a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - uris - a list of Spotify URIs to replace or clear - /// - range_start - the position of the first track to be reordered - /// - insert_before - the position where the tracks should be inserted - /// - range_length - optional the number of tracks to be reordered (default: - /// 1) - /// - snapshot_id - optional playlist's snapshot ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - #[maybe_async] - pub async fn playlist_reorder_tracks( - &self, - playlist_id: &PlaylistId, - uris: Option<&[&Id]>, - range_start: Option, - insert_before: Option, - range_length: Option, - snapshot_id: Option<&str>, - ) -> ClientResult { - let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); - let params = build_json! { - "playlist_id": playlist_id, - optional "uris": uris, - optional "range_start": range_start, - optional "insert_before": insert_before, - optional "range_length": range_length, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_put(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Removes all occurrences of the given tracks from the given playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - track_ids - the list of track ids to add to the playlist - /// - snapshot_id - optional id of the playlist snapshot - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - #[maybe_async] - pub async fn playlist_remove_all_occurrences_of_tracks<'a>( - &self, - playlist_id: &PlaylistId, - track_ids: impl IntoIterator, - snapshot_id: Option<&str>, - ) -> ClientResult { - let tracks = track_ids - .into_iter() - .map(|id| { - let mut map = Map::with_capacity(1); - map.insert("uri".to_owned(), id.uri().into()); - map - }) - .collect::>(); - - let params = build_json! { - "tracks": tracks, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Removes specfic occurrences of the given tracks from the given playlist. - /// - /// Parameters: - /// - playlist_id: the id of the playlist - /// - tracks: an array of map containing Spotify URIs of the tracks to - /// remove with their current positions in the playlist. For example: - /// - /// ```json - /// { - /// "tracks":[ - /// { - /// "uri":"spotify:track:4iV5W9uYEdYUVa79Axb7Rh", - /// "positions":[ - /// 0, - /// 3 - /// ] - /// }, - /// { - /// "uri":"spotify:track:1301WleyT98MSxVHPZCA6M", - /// "positions":[ - /// 7 - /// ] - /// } - /// ] - /// } - /// ``` - /// - snapshot_id: optional id of the playlist snapshot - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - #[maybe_async] - pub async fn playlist_remove_specific_occurrences_of_tracks( - &self, - playlist_id: &PlaylistId, - tracks: Vec>, - snapshot_id: Option<&str>, - ) -> ClientResult { - let tracks = tracks - .into_iter() - .map(|track| { - let mut map = Map::new(); - map.insert("uri".to_owned(), track.id.uri().into()); - map.insert("positions".to_owned(), track.positions.into()); - map - }) - .collect::>(); - - let params = build_json! { - "tracks": tracks, - optional "snapshot_id": snapshot_id, - }; - - let url = format!("playlists/{}/tracks", playlist_id.id()); - let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Add the current authenticated user as a follower of a playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-playlist) - #[maybe_async] - pub async fn playlist_follow( - &self, - playlist_id: &PlaylistId, - public: Option, - ) -> ClientResult<()> { - let url = format!("playlists/{}/followers", playlist_id.id()); - - let params = build_json! { - optional "public": public, - }; - - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Check to see if the given users are following the given playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - user_ids - the ids of the users that you want to - /// check to see if they follow the playlist. Maximum: 5 ids. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) - #[maybe_async] - pub async fn playlist_check_follow( - &self, - playlist_id: &PlaylistId, - user_ids: &[&UserId], - ) -> ClientResult> { - if user_ids.len() > 5 { - error!("The maximum length of user ids is limited to 5 :-)"); - } - let url = format!( - "playlists/{}/followers/contains?ids={}", - playlist_id.id(), - user_ids - .iter() - .map(|id| id.id()) - .collect::>() - .join(","), - ); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Get detailed profile information about the current user. - /// An alias for the 'current_user' method. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) - #[maybe_async] - pub async fn me(&self) -> ClientResult { - let result = self.endpoint_get("me/", &Query::new()).await?; - self.convert_result(&result) - } - - /// Get detailed profile information about the current user. - /// An alias for the 'me' method. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) - #[maybe_async] - pub async fn current_user(&self) -> ClientResult { - self.me().await - } - - /// Get information about the current users currently playing track. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) - #[maybe_async] - pub async fn current_user_playing_track( - &self, - ) -> ClientResult> { - let result = self - .get("me/player/currently-playing", None, &Query::new()) - .await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) - } - } - - /// Gets a list of the albums saved in the current authorized user's - /// "Your Music" library - /// - /// Parameters: - /// - limit - the number of albums to return - /// - offset - the index of the first album to return - /// - market - Provide this parameter if you want to apply Track Relinking. - /// - /// See [`Spotify::current_user_saved_albums`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - pub fn current_user_saved_albums(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of - /// [`Spotify::current_user_saved_albums`]. - #[maybe_async] - pub async fn current_user_saved_albums_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/albums", ¶ms).await?; - self.convert_result(&result) - } - - /// Get a list of the songs saved in the current Spotify user's "Your Music" - /// library. - /// - /// Parameters: - /// - limit - the number of tracks to return - /// - offset - the index of the first track to return - /// - market - Provide this parameter if you want to apply Track Relinking. - /// - /// See [`Spotify::current_user_saved_tracks_manual`] for a manually - /// paginated version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - pub fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of - /// [`Spotify::current_user_saved_tracks`]. - #[maybe_async] - pub async fn current_user_saved_tracks_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/tracks", ¶ms).await?; - self.convert_result(&result) - } - - /// Gets a list of the artists followed by the current authorized user. - /// - /// Parameters: - /// - after - the last artist ID retrieved from the previous request - /// - limit - the number of tracks to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-followed) - #[maybe_async] - pub async fn current_user_followed_artists( - &self, - after: Option<&str>, - limit: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let params = build_map! { - "r#type": Type::Artist.as_ref(), - optional "after": after, - optional "limit": limit.as_deref(), - }; - - let result = self.endpoint_get("me/following", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.artists) - } - - /// Remove one or more tracks from the current user's "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) - #[maybe_async] - pub async fn current_user_saved_tracks_delete<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/tracks/?ids={}", join_ids(track_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check if one or more tracks is already saved in the current Spotify - /// user’s "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) - #[maybe_async] - pub async fn current_user_saved_tracks_contains<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Save one or more tracks to the current user's "Your Music" library. - /// - /// Parameters: - /// - track_ids - a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) - #[maybe_async] - pub async fn current_user_saved_tracks_add<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/tracks/?ids={}", join_ids(track_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Get the current user's top artists. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - offset - the index of the first entity to return - /// - time_range - Over what time frame are the affinities computed - /// - /// See [`Spotify::current_user_top_artists_manual`] for a manually - /// paginated version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - pub fn current_user_top_artists<'a>( - &'a self, - time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_top_artists`]. - #[maybe_async] - pub async fn current_user_top_artists_manual( - &self, - time_range: Option<&TimeRange>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|s| s.to_string()); - let offset = offset.map(|s| s.to_string()); - let params = build_map! { - optional "time_range": time_range.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; - self.convert_result(&result) - } - - /// Get the current user's top tracks. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - offset - the index of the first entity to return - /// - time_range - Over what time frame are the affinities computed - /// - /// See [`Spotify::current_user_top_tracks_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - pub fn current_user_top_tracks<'a>( - &'a self, - time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::current_user_top_tracks`]. - #[maybe_async] - pub async fn current_user_top_tracks_manual( - &self, - time_range: Option<&TimeRange>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "time_range": time_range.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self.endpoint_get("me/top/tracks", ¶ms).await?; - self.convert_result(&result) - } - - /// Get the current user's recently played tracks. - /// - /// Parameters: - /// - limit - the number of entities to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-the-users-currently-playing-track) - #[maybe_async] - pub async fn current_user_recently_played( - &self, - limit: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let params = build_map! { - optional "limit": limit.as_deref(), - }; - - let result = self - .endpoint_get("me/player/recently-played", ¶ms) - .await?; - self.convert_result(&result) - } - - /// Add one or more albums to the current user's "Your Music" library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) - #[maybe_async] - pub async fn current_user_saved_albums_add<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/albums/?ids={}", join_ids(album_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Remove one or more albums from the current user's "Your Music" library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) - #[maybe_async] - pub async fn current_user_saved_albums_delete<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/albums/?ids={}", join_ids(album_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check if one or more albums is already saved in the current Spotify - /// user’s "Your Music” library. - /// - /// Parameters: - /// - album_ids - a list of album URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) - #[maybe_async] - pub async fn current_user_saved_albums_contains<'a>( - &self, - album_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) - } - - /// Follow one or more artists. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - #[maybe_async] - pub async fn user_follow_artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Unfollow one or more artists. - /// - /// Parameters: - /// - artist_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - #[maybe_async] - pub async fn user_unfollow_artists<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Check to see if the current user is following one or more artists or - /// other Spotify users. - /// - /// Parameters: - /// - artist_ids - the ids of the users that you want to - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) - #[maybe_async] - pub async fn user_artist_check_follow<'a>( - &self, - artist_ids: impl IntoIterator, - ) -> ClientResult> { - let url = format!( - "me/following/contains?type=artist&ids={}", - join_ids(artist_ids) - ); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) } +} - /// Follow one or more users. - /// - /// Parameters: - /// - user_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - #[maybe_async] - pub async fn user_follow_users<'a>( - &self, - user_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Unfollow one or more users. - /// - /// Parameters: - /// - user_ids - a list of artist IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - #[maybe_async] - pub async fn user_unfollow_users<'a>( - &self, - user_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); - self.endpoint_delete(&url, &json!({})).await?; - - Ok(()) - } - - /// Get a list of Spotify featured playlists. - /// - /// Parameters: - /// - locale - The desired language, consisting of a lowercase ISO 639 - /// language code and an uppercase ISO 3166-1 alpha-2 country code, - /// joined by an underscore. - /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - timestamp - A timestamp in ISO 8601 format: yyyy-MM-ddTHH:mm:ss. Use - /// this parameter to specify the user's local time to get results - /// tailored for that specific date and time in the day - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 - /// (the first object). Use with limit to get the next set of - /// items. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-featured-playlists) - #[maybe_async] - pub async fn featured_playlists( - &self, - locale: Option<&str>, - country: Option<&Market>, - timestamp: Option>, - limit: Option, - offset: Option, - ) -> ClientResult { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let timestamp = timestamp.map(|x| x.to_rfc3339()); - let params = build_map! { - optional "locale": locale, - optional "country": country.map(|x| x.as_ref()), - optional "timestamp": timestamp.as_deref(), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let result = self - .endpoint_get("browse/featured-playlists", ¶ms) - .await?; - self.convert_result(&result) - } +/// Generate `length` random chars +pub(in crate) fn generate_random_string(length: usize) -> String { + let alphanum: &[u8] = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".as_bytes(); + let mut buf = vec![0u8; length]; + getrandom(&mut buf).unwrap(); + let range = alphanum.len(); + + buf.iter() + .map(|byte| alphanum[*byte as usize % range] as char) + .collect() +} - /// Get a list of new album releases featured in Spotify. - /// - /// Parameters: - /// - country - An ISO 3166-1 alpha-2 country code or string from_token. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::new_releases_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) - pub fn new_releases<'a>( - &'a self, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), - self.pagination_chunks, - ) - } +mod auth_urls { + pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize"; + pub const TOKEN: &str = "https://accounts.spotify.com/api/token"; +} - /// The manually paginated version of [`Spotify::new_releases`]. - #[maybe_async] - pub async fn new_releases_manual( - &self, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; +mod duration_second { + use chrono::Duration; + use serde::{de, Deserialize, Serializer}; - let result = self.endpoint_get("browse/new-releases", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.albums) + /// Deserialize `chrono::Duration` from seconds (represented as u64) + pub(in crate) fn deserialize<'de, D>(d: D) -> Result + where + D: de::Deserializer<'de>, + { + let duration: i64 = Deserialize::deserialize(d)?; + Ok(Duration::seconds(duration)) } - /// Get a list of new album releases featured in Spotify - /// - /// Parameters: - /// - country - An ISO 3166-1 alpha-2 country code or string from_token. - /// - locale - The desired language, consisting of an ISO 639 language code - /// and an ISO 3166-1 alpha-2 country code, joined by an underscore. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::categories_manual`] for a manually paginated version of - /// this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) - pub fn categories<'a>( - &'a self, - locale: Option<&'a str>, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), - self.pagination_chunks, - ) + /// Serialize `chrono::Duration` to seconds (represented as u64) + pub(in crate) fn serialize(x: &Duration, s: S) -> Result + where + S: Serializer, + { + s.serialize_i64(x.num_seconds()) } +} - /// The manually paginated version of [`Spotify::categories`]. - #[maybe_async] - pub async fn categories_manual( - &self, - locale: Option<&str>, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "locale": locale, - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - let result = self.endpoint_get("browse/categories", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.categories) - } +mod space_separated_scope { + use serde::{de, Deserialize, Serializer}; + use std::collections::HashSet; - /// Get a list of playlists in a category in Spotify - /// - /// Parameters: - /// - category_id - The category id to get playlists from. - /// - country - An ISO 3166-1 alpha-2 country code or the string from_token. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 50 - /// - offset - The index of the first item to return. Default: 0 (the first - /// object). Use with limit to get the next set of items. - /// - /// See [`Spotify::category_playlists_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) - pub fn category_playlists<'a>( - &'a self, - category_id: &'a str, - country: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) + pub(crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> + where + D: de::Deserializer<'de>, + { + let scope: &str = Deserialize::deserialize(d)?; + Ok(scope.split_whitespace().map(|x| x.to_owned()).collect()) } - /// The manually paginated version of [`Spotify::category_playlists`]. - #[maybe_async] - pub async fn category_playlists_manual( - &self, - category_id: &str, - country: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "limit": limit.as_deref(), - optional "offset": offset.as_deref(), - }; - - let url = format!("browse/categories/{}/playlists", category_id); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.playlists) + pub(crate) fn serialize(scope: &HashSet, s: S) -> Result + where + S: Serializer, + { + let scope = scope.clone().into_iter().collect::>().join(" "); + s.serialize_str(&scope) } +} - /// Get Recommendations Based on Seeds - /// - /// Parameters: - /// - seed_artists - a list of artist IDs, URIs or URLs - /// - seed_tracks - a list of artist IDs, URIs or URLs - /// - seed_genres - a list of genre names. Available genres for - /// - market - An ISO 3166-1 alpha-2 country code or the string from_token. If provided, all - /// results will be playable in this country. - /// - limit - The maximum number of items to return. Default: 20. - /// Minimum: 1. Maximum: 100 - /// - min/max/target_ - For the tuneable track attributes listed - /// in the documentation, these values provide filters and targeting on - /// results. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recommendations) - #[maybe_async] - pub async fn recommendations( - &self, - payload: &Map, - seed_artists: Option>, - seed_genres: Option>, - seed_tracks: Option>, - limit: Option, - market: Option<&Market>, - ) -> ClientResult { - let seed_artists = seed_artists.map(join_ids); - let seed_genres = seed_genres.map(|x| x.join(",")); - let seed_tracks = seed_tracks.map(join_ids); - let limit = limit.map(|x| x.to_string()); - let mut params = build_map! { - optional "seed_artists": seed_artists.as_ref(), - optional "seed_genres": seed_genres.as_ref(), - optional "seed_tracks": seed_tracks.as_ref(), - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_ref(), - }; +/// Spotify access token information +/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/) +#[derive(Builder, Clone, Debug, Serialize, Deserialize)] +pub struct Token { + /// An access token that can be provided in subsequent calls + #[builder(setter(into))] + pub access_token: String, + /// The time period for which the access token is valid. + #[builder(default = "Duration::seconds(0)")] + #[serde(with = "duration_second")] + pub expires_in: Duration, + /// The valid time for which the access token is available represented + /// in ISO 8601 combined date and time. + #[builder(setter(strip_option), default = "Some(Utc::now())")] + pub expires_at: Option>, + /// A token that can be sent to the Spotify Accounts service + /// in place of an authorization code + #[builder(setter(into, strip_option), default)] + pub refresh_token: Option, + /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) + /// which have been granted for this `access_token` + /// You could use macro [scopes!](crate::scopes) to build it at compile time easily + #[builder(default)] + #[serde(default, with = "space_separated_scope")] + pub scope: HashSet, +} - // TODO: this probably can be improved. - let attributes = [ - "acousticness", - "danceability", - "duration_ms", - "energy", - "instrumentalness", - "key", - "liveness", - "loudness", - "mode", - "popularity", - "speechiness", - "tempo", - "time_signature", - "valence", - ]; - let mut map_to_hold_owned_value = HashMap::new(); - let prefixes = ["min", "max", "target"]; - for attribute in attributes.iter() { - for prefix in prefixes.iter() { - let param = format!("{}_{}", prefix, attribute); - if let Some(value) = payload.get(¶m) { - // TODO: not sure if this `to_string` is what we want. It - // might add quotes to the strings. - map_to_hold_owned_value.insert(param, value.to_string()); +impl TokenBuilder { + /// Tries to initialize the token from a cache file. + pub fn from_cache>(path: T) -> Self { + if let Ok(mut file) = fs::File::open(path) { + let mut tok_str = String::new(); + if file.read_to_string(&mut tok_str).is_ok() { + if let Ok(tok) = serde_json::from_str::(&tok_str) { + return TokenBuilder { + access_token: Some(tok.access_token), + expires_in: Some(tok.expires_in), + expires_at: Some(tok.expires_at), + refresh_token: Some(tok.refresh_token), + scope: Some(tok.scope), + }; } } } - for (ref key, ref value) in &map_to_hold_owned_value { - params.insert(key, value); - } - - let result = self.endpoint_get("recommendations", ¶ms).await?; - self.convert_result(&result) - } - - /// Get audio features for a track - /// - /// Parameters: - /// - track - track URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-features) - #[maybe_async] - pub async fn track_features(&self, track_id: &TrackId) -> ClientResult { - let url = format!("audio-features/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + TokenBuilder::default() } +} - /// Get Audio Features for Several Tracks - /// - /// Parameters: - /// - tracks a list of track URIs, URLs or IDs - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) - #[maybe_async] - pub async fn tracks_features<'a>( - &self, - track_ids: impl IntoIterator, - ) -> ClientResult>> { - let url = format!("audio-features/?ids={}", join_ids(track_ids)); +impl Token { + /// Saves the token information into its cache file. + pub fn write_cache>(&self, path: T) -> ClientResult<()> { + let token_info = serde_json::to_string(&self)?; - let result = self.endpoint_get(&url, &Query::new()).await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result::>(&result) - .map(|option_payload| option_payload.map(|x| x.audio_features)) - } - } + let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?; + file.set_len(0)?; + file.write_all(token_info.as_bytes())?; - /// Get Audio Analysis for a Track - /// - /// Parameters: - /// - track_id - a track URI, URL or ID - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-audio-analysis) - #[maybe_async] - pub async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { - let url = format!("audio-analysis/{}", track_id.id()); - let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + Ok(()) } - /// Get a User’s Available Devices - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-users-available-devices) - #[maybe_async] - pub async fn device(&self) -> ClientResult> { - let result = self - .endpoint_get("me/player/devices", &Query::new()) - .await?; - self.convert_result::(&result) - .map(|x| x.devices) + /// Check if the token is expired + pub fn is_expired(&self) -> bool { + self.expires_at + .map_or(true, |x| Utc::now().timestamp() > x.timestamp()) } +} - /// Get Information About The User’s Current Playback - /// - /// Parameters: - /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. - /// - additional_types: Optional. A comma-separated list of item types that - /// your client supports besides the default track type. Valid types are: - /// `track` and `episode`. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-information-about-the-users-current-playback) - #[maybe_async] - pub async fn current_playback( - &self, - country: Option<&Market>, - additional_types: Option>, - ) -> ClientResult> { - let additional_types = - additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - let params = build_map! { - optional "country": country.map(|x| x.as_ref()), - optional "additional_types": additional_types.as_ref(), - }; +/// Simple client credentials object for Spotify. +#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] +pub struct Credentials { + #[builder(setter(into))] + pub id: String, + #[builder(setter(into))] + pub secret: String, +} - let result = self.endpoint_get("me/player", ¶ms).await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) +impl CredentialsBuilder { + /// Parses the credentials from the environment variables + /// `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET`. You can optionally + /// activate the `env-file` feature in order to read these variables from + /// a `.env` file. + pub fn from_env() -> Self { + #[cfg(feature = "env-file")] + { + dotenv::dotenv().ok(); } - } - /// Get the User’s Currently Playing Track - /// - /// Parameters: - /// - market: Optional. an ISO 3166-1 alpha-2 country code or the string from_token. - /// - additional_types: Optional. A comma-separated list of item types that - /// your client supports besides the default track type. Valid types are: - /// `track` and `episode`. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-recently-played) - #[maybe_async] - pub async fn current_playing( - &self, - market: Option<&Market>, - additional_types: Option>, - ) -> ClientResult> { - let additional_types = - additional_types.map(|x| x.iter().map(|x| x.as_ref()).collect::>().join(",")); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - optional "additional_types": additional_types.as_ref(), - }; - - let result = self - .get("me/player/currently-playing", None, ¶ms) - .await?; - if result.is_empty() { - Ok(None) - } else { - self.convert_result(&result) + CredentialsBuilder { + id: env::var("RSPOTIFY_CLIENT_ID").ok(), + secret: env::var("RSPOTIFY_CLIENT_SECRET").ok(), } } +} - /// Transfer a User’s Playback. - /// - /// Note: Although an array is accepted, only a single device_id is - /// currently supported. Supplying more than one will return 400 Bad Request - /// - /// Parameters: - /// - device_id - transfer playback to this device - /// - force_play - true: after transfer, play. false: - /// keep current state. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-transfer-a-users-playback) - #[maybe_async] - pub async fn transfer_playback( - &self, - device_id: &str, - force_play: Option, - ) -> ClientResult<()> { - let params = build_json! { - "device_ids": [device_id], - optional "force_play": force_play, - }; - - self.endpoint_put("me/player", ¶ms).await?; - Ok(()) - } - - /// Start/Resume a User’s Playback. - /// - /// Provide a `context_uri` to start playback or a album, artist, or - /// playlist. Provide a `uris` list to start playback of one or more tracks. - /// Provide `offset` as {"position": } or {"uri": ""} to - /// start playback at a particular offset. - /// - /// Parameters: - /// - device_id - device target for playback - /// - context_uri - spotify context uri to play - /// - uris - spotify track uris - /// - offset - offset into context by index or track - /// - position_ms - Indicates from what position to start playback. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) - #[maybe_async] - pub async fn start_context_playback( - &self, - context_uri: &Id, - device_id: Option<&str>, - offset: Option>, - position_ms: Option, - ) -> ClientResult<()> { - let params = build_json! { - "context_uri": context_uri.uri(), - optional "offset": offset.map(|x| match x { - Offset::Position(position) => json!({ "position": position }), - Offset::Uri(uri) => json!({ "uri": uri.uri() }), - }), - optional "position_ms": position_ms, - - }; - - let url = self.append_device_id("me/player/play", device_id); - self.put(&url, None, ¶ms).await?; - - Ok(()) - } - - #[maybe_async] - pub async fn start_uris_playback( - &self, - uris: &[&Id], - device_id: Option<&str>, - offset: Option>, - position_ms: Option, - ) -> ClientResult<()> { - let params = build_json! { - "uris": uris.iter().map(|id| id.uri()).collect::>(), - optional "position_ms": position_ms, - optional "offset": offset.map(|x| match x { - Offset::Position(position) => json!({ "position": position }), - Offset::Uri(uri) => json!({ "uri": uri.uri() }), - }), - }; - - let url = self.append_device_id("me/player/play", device_id); - self.endpoint_put(&url, ¶ms).await?; - - Ok(()) - } - - /// Pause a User’s Playback. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) - #[maybe_async] - pub async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/pause", device_id); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Skip User’s Playback To Next Track. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) - #[maybe_async] - pub async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/next", device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Skip User’s Playback To Previous Track. - /// - /// Parameters: - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) - #[maybe_async] - pub async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/previous", device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Seek To Position In Currently Playing Track. - /// - /// Parameters: - /// - position_ms - position in milliseconds to seek to - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) - #[maybe_async] - pub async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( - &format!("me/player/seek?position_ms={}", position_ms), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Set Repeat Mode On User’s Playback. - /// - /// Parameters: - /// - state - `track`, `context`, or `off` - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) - #[maybe_async] - pub async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( - &format!("me/player/repeat?state={}", state.as_ref()), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } +/// Structure that holds the required information for requests with OAuth. +#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] +pub struct OAuth { + #[builder(setter(into))] + pub redirect_uri: String, + /// The state is generated by default, as suggested by the OAuth2 spec: + /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) + #[builder(setter(into), default = "generate_random_string(16)")] + pub state: String, + /// You could use macro [scopes!](crate::scopes) to build it at compile time easily + #[builder(default)] + pub scope: HashSet, + #[builder(setter(into, strip_option), default)] + pub proxies: Option, +} - /// Set Volume For User’s Playback. - /// - /// Parameters: - /// - volume_percent - volume between 0 and 100 - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-volume-for-users-playback) - #[maybe_async] - pub async fn volume(&self, volume_percent: u8, device_id: Option<&str>) -> ClientResult<()> { - if volume_percent > 100u8 { - error!("volume must be between 0 and 100, inclusive"); +impl OAuthBuilder { + /// Parses the credentials from the environment variable + /// `RSPOTIFY_REDIRECT_URI`. You can optionally activate the `env-file` + /// feature in order to read these variables from a `.env` file. + pub fn from_env() -> Self { + #[cfg(feature = "env-file")] + { + dotenv::dotenv().ok(); } - let url = self.append_device_id( - &format!("me/player/volume?volume_percent={}", volume_percent), - device_id, - ); - self.endpoint_put(&url, &json!({})).await?; - Ok(()) - } - - /// Toggle Shuffle For User’s Playback. - /// - /// Parameters: - /// - state - true or false - /// - device_id - device target for playback - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) - #[maybe_async] - pub async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Add an item to the end of the user's playback queue. - /// - /// Parameters: - /// - uri - The uri of the item to add, Track or Episode - /// - device id - The id of the device targeting - /// - If no device ID provided the user's currently active device is - /// targeted - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) - #[maybe_async] - pub async fn add_item_to_queue( - &self, - item: &Id, - device_id: Option<&str>, - ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); - self.endpoint_post(&url, &json!({})).await?; - - Ok(()) - } - - /// Add a show or a list of shows to a user’s library. - /// - /// Parameters: - /// - ids(Required) A comma-separated list of Spotify IDs for the shows to - /// be added to the user’s library. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) - #[maybe_async] - pub async fn save_shows<'a>( - &self, - show_ids: impl IntoIterator, - ) -> ClientResult<()> { - let url = format!("me/shows/?ids={}", join_ids(show_ids)); - self.endpoint_put(&url, &json!({})).await?; - - Ok(()) - } - - /// Get a list of shows saved in the current Spotify user’s library. - /// Optional parameters can be used to limit the number of shows returned. - /// - /// Parameters: - /// - limit(Optional). The maximum number of shows to return. Default: 20. - /// Minimum: 1. Maximum: 50. - /// - offset(Optional). The index of the first show to return. Default: 0 - /// (the first object). Use with limit to get the next set of shows. - /// - /// See [`Spotify::get_saved_show_manual`] for a manually paginated version - /// of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - pub fn get_saved_show(&self) -> impl Paginator> + '_ { - paginate( - move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::get_saved_show`]. - #[maybe_async] - pub async fn get_saved_show_manual( - &self, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "limit": limit.as_ref(), - optional "offset": offset.as_ref(), - }; - - let result = self.endpoint_get("me/shows", ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for a single show identified by its unique Spotify ID. - /// - /// Path Parameters: - /// - id: The Spotify ID for the show. - /// - /// Query Parameters - /// - market(Optional): An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-show) - #[maybe_async] - pub async fn get_a_show(&self, id: &ShowId, market: Option<&Market>) -> ClientResult { - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let url = format!("shows/{}", id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for multiple shows based on their - /// Spotify IDs. - /// - /// Query Parameters - /// - ids(Required) A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) - #[maybe_async] - pub async fn get_several_shows<'a>( - &self, - ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get("shows", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.shows) - } - - /// Get Spotify catalog information about an show’s episodes. Optional - /// parameters can be used to limit the number of episodes returned. - /// - /// Path Parameters - /// - id: The Spotify ID for the show. - /// - /// Query Parameters - /// - limit: Optional. The maximum number of episodes to return. Default: 20. Minimum: 1. Maximum: 50. - /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// See [`Spotify::get_shows_episodes_manual`] for a manually paginated - /// version of this. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) - pub fn get_shows_episodes<'a>( - &'a self, - id: &'a ShowId, - market: Option<&'a Market>, - ) -> impl Paginator> + 'a { - paginate( - move |limit, offset| { - self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) - }, - self.pagination_chunks, - ) - } - - /// The manually paginated version of [`Spotify::get_shows_episodes`]. - #[maybe_async] - pub async fn get_shows_episodes_manual( - &self, - id: &ShowId, - market: Option<&Market>, - limit: Option, - offset: Option, - ) -> ClientResult> { - let limit = limit.map(|x| x.to_string()); - let offset = offset.map(|x| x.to_string()); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - optional "limit": limit.as_ref(), - optional "offset": offset.as_ref(), - }; - - let url = format!("shows/{}/episodes", id.id()); - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. - /// - /// Path Parameters - /// - id: The Spotify ID for the episode. - /// - /// Query Parameters - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-episode) - #[maybe_async] - pub async fn get_an_episode( - &self, - id: &EpisodeId, - market: Option<&Market>, - ) -> ClientResult { - let url = format!("episodes/{}", id.id()); - let params = build_map! { - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) - } - - /// Get Spotify catalog information for multiple episodes based on their Spotify IDs. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of the Spotify IDs for the episodes. Maximum: 50 IDs. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) - #[maybe_async] - pub async fn get_several_episodes<'a>( - &self, - ids: impl IntoIterator, - market: Option<&Market>, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - optional "market": market.map(|x| x.as_ref()), - }; - - let result = self.endpoint_get("episodes", ¶ms).await?; - self.convert_result::(&result) - .map(|x| x.episodes) - } - - /// Check if one or more shows is already saved in the current Spotify user’s library. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) - #[maybe_async] - pub async fn check_users_saved_shows<'a>( - &self, - ids: impl IntoIterator, - ) -> ClientResult> { - let ids = join_ids(ids); - let params = build_map! { - "ids": &ids, - }; - let result = self.endpoint_get("me/shows/contains", ¶ms).await?; - self.convert_result(&result) - } - - /// Delete one or more shows from current Spotify user's library. - /// Changes to a user's saved shows may not be visible in other Spotify applications immediately. - /// - /// Query Parameters - /// - ids: Required. A comma-separated list of Spotify IDs for the shows to be deleted from the user’s library. - /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) - #[maybe_async] - pub async fn remove_users_saved_shows<'a>( - &self, - show_ids: impl IntoIterator, - country: Option<&Market>, - ) -> ClientResult<()> { - let url = format!("me/shows?ids={}", join_ids(show_ids)); - let params = build_json! { - optional "country": country.map(|x| x.as_ref()) - }; - self.endpoint_delete(&url, ¶ms).await?; - - Ok(()) + OAuthBuilder { + redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok(), + ..Default::default() + } } } -#[inline] -fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { - ids.into_iter().collect::>().join(",") -} - #[cfg(test)] mod test { use super::*; diff --git a/src/mod.rs b/src/mod.rs deleted file mode 100644 index cbb4d349..00000000 --- a/src/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Client to Spotify API endpoint - -use derive_builder::Builder; -use log::error; -use maybe_async::maybe_async; -use serde::Deserialize; -use serde_json::{json, map::Map, Value}; -use thiserror::Error; - -use std::collections::HashMap; -use std::path::PathBuf; -use std::time; - -use crate::http::{HTTPClient, Query}; -use crate::macros::{build_json, build_map}; -use crate::model::{ - idtypes::{IdType, PlayContextIdType}, - *, -}; -use crate::oauth2::{Credentials, OAuth, Token}; -use crate::pagination::{paginate, Paginator}; diff --git a/src/oauth2.rs b/src/oauth2.rs index 50f5996a..18ec6073 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -1,211 +1,5 @@ //! User authorization and client credentials management. -use chrono::prelude::*; -use derive_builder::Builder; -use getrandom::getrandom; -use maybe_async::maybe_async; -use serde::{Deserialize, Serialize}; -use url::Url; - -use chrono::Duration; -use std::collections::{HashMap, HashSet}; -use std::{ - env, fs, - io::{Read, Write}, - path::Path, -}; - -use crate::client::{ClientResult, Spotify}; -use crate::http::{headers, Form, Headers}; - -/// Generate `length` random chars -pub(in crate) fn generate_random_string(length: usize) -> String { - let alphanum: &[u8] = - "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".as_bytes(); - let mut buf = vec![0u8; length]; - getrandom(&mut buf).unwrap(); - let range = alphanum.len(); - - buf.iter() - .map(|byte| alphanum[*byte as usize % range] as char) - .collect() -} - -mod auth_urls { - pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize"; - pub const TOKEN: &str = "https://accounts.spotify.com/api/token"; -} - -mod duration_second { - use chrono::Duration; - use serde::{de, Deserialize, Serializer}; - - /// Deserialize `chrono::Duration` from seconds (represented as u64) - pub(in crate) fn deserialize<'de, D>(d: D) -> Result - where - D: de::Deserializer<'de>, - { - let duration: i64 = Deserialize::deserialize(d)?; - Ok(Duration::seconds(duration)) - } - - /// Serialize `chrono::Duration` to seconds (represented as u64) - pub(in crate) fn serialize(x: &Duration, s: S) -> Result - where - S: Serializer, - { - s.serialize_i64(x.num_seconds()) - } -} - -mod space_separated_scope { - use serde::{de, Deserialize, Serializer}; - use std::collections::HashSet; - - pub(crate) fn deserialize<'de, D>(d: D) -> Result, D::Error> - where - D: de::Deserializer<'de>, - { - let scope: &str = Deserialize::deserialize(d)?; - Ok(scope.split_whitespace().map(|x| x.to_owned()).collect()) - } - - pub(crate) fn serialize(scope: &HashSet, s: S) -> Result - where - S: Serializer, - { - let scope = scope.clone().into_iter().collect::>().join(" "); - s.serialize_str(&scope) - } -} - -/// Spotify access token information -/// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/) -#[derive(Builder, Clone, Debug, Serialize, Deserialize)] -pub struct Token { - /// An access token that can be provided in subsequent calls - #[builder(setter(into))] - pub access_token: String, - /// The time period for which the access token is valid. - #[builder(default = "Duration::seconds(0)")] - #[serde(with = "duration_second")] - pub expires_in: Duration, - /// The valid time for which the access token is available represented - /// in ISO 8601 combined date and time. - #[builder(setter(strip_option), default = "Some(Utc::now())")] - pub expires_at: Option>, - /// A token that can be sent to the Spotify Accounts service - /// in place of an authorization code - #[builder(setter(into, strip_option), default)] - pub refresh_token: Option, - /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) - /// which have been granted for this `access_token` - /// You could use macro [scopes!](crate::scopes) to build it at compile time easily - #[builder(default)] - #[serde(default, with = "space_separated_scope")] - pub scope: HashSet, -} - -impl TokenBuilder { - /// Tries to initialize the token from a cache file. - pub fn from_cache>(path: T) -> Self { - if let Ok(mut file) = fs::File::open(path) { - let mut tok_str = String::new(); - if file.read_to_string(&mut tok_str).is_ok() { - if let Ok(tok) = serde_json::from_str::(&tok_str) { - return TokenBuilder { - access_token: Some(tok.access_token), - expires_in: Some(tok.expires_in), - expires_at: Some(tok.expires_at), - refresh_token: Some(tok.refresh_token), - scope: Some(tok.scope), - }; - } - } - } - - TokenBuilder::default() - } -} - -impl Token { - /// Saves the token information into its cache file. - pub fn write_cache>(&self, path: T) -> ClientResult<()> { - let token_info = serde_json::to_string(&self)?; - - let mut file = fs::OpenOptions::new().write(true).create(true).open(path)?; - file.set_len(0)?; - file.write_all(token_info.as_bytes())?; - - Ok(()) - } - - /// Check if the token is expired - pub fn is_expired(&self) -> bool { - self.expires_at - .map_or(true, |x| Utc::now().timestamp() > x.timestamp()) - } -} - -/// Simple client credentials object for Spotify. -#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] -pub struct Credentials { - #[builder(setter(into))] - pub id: String, - #[builder(setter(into))] - pub secret: String, -} - -impl CredentialsBuilder { - /// Parses the credentials from the environment variables - /// `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET`. You can optionally - /// activate the `env-file` feature in order to read these variables from - /// a `.env` file. - pub fn from_env() -> Self { - #[cfg(feature = "env-file")] - { - dotenv::dotenv().ok(); - } - - CredentialsBuilder { - id: env::var("RSPOTIFY_CLIENT_ID").ok(), - secret: env::var("RSPOTIFY_CLIENT_SECRET").ok(), - } - } -} - -/// Structure that holds the required information for requests with OAuth. -#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] -pub struct OAuth { - #[builder(setter(into))] - pub redirect_uri: String, - /// The state is generated by default, as suggested by the OAuth2 spec: - /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) - #[builder(setter(into), default = "generate_random_string(16)")] - pub state: String, - /// You could use macro [scopes!](crate::scopes) to build it at compile time easily - #[builder(default)] - pub scope: HashSet, - #[builder(setter(into, strip_option), default)] - pub proxies: Option, -} - -impl OAuthBuilder { - /// Parses the credentials from the environment variable - /// `RSPOTIFY_REDIRECT_URI`. You can optionally activate the `env-file` - /// feature in order to read these variables from a `.env` file. - pub fn from_env() -> Self { - #[cfg(feature = "env-file")] - { - dotenv::dotenv().ok(); - } - - OAuthBuilder { - redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok(), - ..Default::default() - } - } -} /// Authorization-related methods for the client. impl Spotify { From 1a4487b121922f7014e7d11e8f117c3f6aaa1422 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Thu, 29 Apr 2021 23:58:53 +0200 Subject: [PATCH 04/56] hit a wall with `impl` --- Cargo.toml | 3 +- rspotify-http/src/lib.rs | 2 +- rspotify-http/src/reqwest.rs | 4 +- rspotify-http/src/ureq.rs | 4 +- src/client_creds.rs | 19 ++- src/code_auth.rs | 17 +- src/code_auth_pkce.rs | 17 +- src/endpoints/base.rs | 261 ++++++++++++++++++++--------- src/endpoints/mod.rs | 131 +-------------- src/endpoints/oauth.rs | 185 ++++++++++---------- src/endpoints/pagination/iter.rs | 8 +- src/endpoints/pagination/stream.rs | 7 +- src/lib.rs | 10 +- 13 files changed, 328 insertions(+), 340 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9352678b..d6c10292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,11 +26,12 @@ resolver = "2" [dependencies] rspotify-macros = { path = "rspotify-macros", version = "0.10.0" } rspotify-model = { path = "rspotify-model", version = "0.10.0" } -rspotify-http = { path = "rspotify-http", version = "0.10.0" } +rspotify-http = { path = "rspotify-http", version = "0.10.0", default-features = false } ### Client ### async-stream = { version = "0.3.0", optional = true } async-trait = { version = "0.1.48", optional = true } +base64 = "0.13.0" chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } derive_builder = "0.10.0" dotenv = { version = "0.15.0", optional = true } diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index 51c2a07b..de5fecb2 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -89,7 +89,7 @@ pub type Result = std::result::Result; /// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make /// much sense). #[maybe_async] -pub trait BaseClient: Default + Clone + fmt::Debug { +pub trait BaseHttpClient: Default + Clone + fmt::Debug { // This internal function should always be given an object value in JSON. async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result; diff --git a/rspotify-http/src/reqwest.rs b/rspotify-http/src/reqwest.rs index 390cf574..8fadafcb 100644 --- a/rspotify-http/src/reqwest.rs +++ b/rspotify-http/src/reqwest.rs @@ -1,7 +1,7 @@ //! The client implementation for the reqwest HTTP client, which is async by //! default. -use super::{BaseClient, Error, Form, Headers, Query, Result}; +use super::{BaseHttpClient, Error, Form, Headers, Query, Result}; use std::convert::TryInto; @@ -94,7 +94,7 @@ impl ReqwestClient { } #[async_impl] -impl BaseClient for ReqwestClient { +impl BaseHttpClient for ReqwestClient { #[inline] async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result { self.request(Method::GET, url, headers, |req| req.query(payload)) diff --git a/rspotify-http/src/ureq.rs b/rspotify-http/src/ureq.rs index c923b9f2..db671458 100644 --- a/rspotify-http/src/ureq.rs +++ b/rspotify-http/src/ureq.rs @@ -1,6 +1,6 @@ //! The client implementation for the ureq HTTP client, which is blocking. -use super::{BaseClient, Error, Form, Headers, Query, Result}; +use super::{BaseHttpClient, Error, Form, Headers, Query, Result}; use maybe_async::sync_impl; use serde_json::Value; @@ -53,7 +53,7 @@ impl UreqClient { } #[sync_impl] -impl BaseClient for UreqClient { +impl BaseHttpClient for UreqClient { #[inline] fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result { let request = ureq::get(url); diff --git a/src/client_creds.rs b/src/client_creds.rs index 26b11b1a..48f506b4 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,40 +1,37 @@ -use crate::{endpoints::BaseClient, prelude::*, Config, Credentials, HTTPClient, Token}; +use crate::{endpoints::BaseClient, Config, Credentials, http::HttpClient, Token}; #[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { pub config: Config, pub creds: Credentials, pub token: Option, - pub(in crate) http: HTTPClient, + pub(in crate) http: HttpClient, } impl ClientCredentialsSpotify { pub fn new(creds: Credentials) -> Self { ClientCredentialsSpotify { creds, - token: None, - http: HTTPClient {}, ..Default::default() } } - pub fn with_config(creds: Credentials, config: Config) { + pub fn with_config(creds: Credentials, config: Config) -> Self { ClientCredentialsSpotify { creds, config, - token: None, - http: HTTPClient {}, + ..Default::default() } } pub fn request_token(&mut self) { - self.token = Some(Token("client credentials token".to_string())) + todo!() } } // This could even use a macro impl BaseClient for ClientCredentialsSpotify { - fn get_http(&self) -> &HTTPClient { + fn get_http(&self) -> &HttpClient { &self.http } @@ -45,4 +42,8 @@ impl BaseClient for ClientCredentialsSpotify { fn get_creds(&self) -> &Credentials { &self.creds } + + fn get_config(&self) -> &Config { + &self.config + } } diff --git a/src/code_auth.rs b/src/code_auth.rs index e08cb779..b9715d65 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,11 +1,11 @@ -use crate::{prelude::*, Credentials, HTTPClient, OAuth, Token}; +use crate::{prelude::*, Credentials, http::HttpClient, OAuth, Token, Config}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { creds: Credentials, oauth: OAuth, tok: Option, - http: HTTPClient, + http: HttpClient, } impl CodeAuthSpotify { @@ -13,18 +13,17 @@ impl CodeAuthSpotify { CodeAuthSpotify { creds, oauth, - tok: None, - http: HTTPClient {}, + ..Default::default() } } pub fn prompt_for_user_token(&mut self) { - self.tok = Some(Token("code auth token".to_string())) + todo!() } } impl BaseClient for CodeAuthSpotify { - fn get_http(&self) -> &HTTPClient { + fn get_http(&self) -> &HttpClient { &self.http } @@ -35,6 +34,10 @@ impl BaseClient for CodeAuthSpotify { fn get_creds(&self) -> &Credentials { &self.creds } + + fn get_config(&self) -> &Config { + todo!() + } } // This could also be a macro (less important) diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index a3b4f831..9f8945ed 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -1,11 +1,11 @@ -use crate::{prelude::*, Credentials, HTTPClient, OAuth, Token}; +use crate::{endpoints::{BaseClient, OAuthClient}, Credentials, http::HttpClient, OAuth, Token, Config}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct CodeAuthPKCESpotify { creds: Credentials, oauth: OAuth, tok: Option, - http: HTTPClient, + http: HttpClient, } impl CodeAuthPKCESpotify { @@ -13,18 +13,17 @@ impl CodeAuthPKCESpotify { CodeAuthPKCESpotify { creds, oauth, - tok: None, - http: HTTPClient {}, + ..Default::default() } } pub fn prompt_for_user_token(&mut self) { - self.tok = Some(Token("code auth pkce token".to_string())) + todo!() } } impl BaseClient for CodeAuthPKCESpotify { - fn get_http(&self) -> &HTTPClient { + fn get_http(&self) -> &HttpClient { &self.http } @@ -35,6 +34,10 @@ impl BaseClient for CodeAuthPKCESpotify { fn get_creds(&self) -> &Credentials { &self.creds } + + fn get_config(&self) -> &Config { + todo!() + } } impl OAuthClient for CodeAuthPKCESpotify { diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index a14a6812..3e0b34ed 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,11 +1,10 @@ use crate::{ endpoints::{ - join_ids, + join_ids, bearer_auth, convert_result, pagination::{paginate, Paginator}, }, - http::HttpClient, - http::Query, - macros::{build_json, build_map}, + http::{Query, Headers, HttpClient, Form, BaseHttpClient}, + macros::build_map, model::*, ClientResult, Config, Credentials, Token, }; @@ -13,6 +12,21 @@ use crate::{ use std::collections::HashMap; use maybe_async::maybe_async; +use serde_json::{Map, Value}; + +/// HTTP-related methods for the Spotify client. It wraps the basic HTTP client +/// with features needed of higher level. +/// +/// The Spotify client has two different wrappers to perform requests: +/// +/// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only +/// append the configured Spotify API URL to the relative URL provided so that +/// it's not forgotten. They're used in the authentication process to request +/// an access token and similars. +/// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, +/// `endpoint_delete`. These append the authentication headers for endpoint +/// requests to reduce the code needed for endpoints and make them as concise +/// as possible. #[maybe_async] pub trait BaseClient { @@ -21,18 +35,109 @@ pub trait BaseClient { fn get_token(&self) -> Option<&Token>; fn get_creds(&self) -> &Credentials; - // Existing - fn request(&self, mut params: HashMap) { - let http = self.get_http(); - params.insert("url".to_string(), "...".to_string()); - http.request(params); + /// If it's a relative URL like "me", the prefix is appended to it. + /// Otherwise, the same URL is returned. + fn endpoint_url(&self, url: &str) -> String { + // Using the client's prefix in case it's a relative route. + if !url.starts_with("http") { + self.get_config().prefix.clone() + url + } else { + url.to_string() + } + } + + /// The headers required for authenticated requests to the API + fn auth_headers(&self) -> ClientResult { + let mut auth = Headers::new(); + let (key, val) = bearer_auth(self.get_token().expect("Rspotify not authenticated")); + auth.insert(key, val); + + Ok(auth) + } + + #[inline] + async fn get( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Query<'_>, + ) -> ClientResult { + let url = self.endpoint_url(url); + Ok(self.get_http().get(&url, headers, payload).await?) + } + + #[inline] + async fn post( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + Ok(self.get_http().post(&url, headers, payload).await?) + } + + #[inline] + async fn post_form( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Form<'_>, + ) -> ClientResult { + let url = self.endpoint_url(url); + Ok(self.get_http().post_form(&url, headers, payload).await?) + } + + #[inline] + async fn put( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + Ok(self.get_http().put(&url, headers, payload).await?) + } + + #[inline] + async fn delete( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Value, + ) -> ClientResult { + let url = self.endpoint_url(url); + Ok(self.get_http().delete(&url, headers, payload).await?) + } + + /// The wrapper for the endpoints, which also includes the required + /// autentication. + #[inline] + async fn endpoint_get( + &self, + url: &str, + payload: &Query<'_>, + ) -> ClientResult { + let headers = self.auth_headers()?; + Ok(self.get(url, Some(&headers), payload).await?) + } + + #[inline] + async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.post(url, Some(&headers), payload).await + } + + #[inline] + async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.put(url, Some(&headers), payload).await } - // Existing - fn endpoint_request(&self) { - let mut params = HashMap::new(); - params.insert("token".to_string(), self.get_token().unwrap().0.clone()); - self.request(params); + #[inline] + async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult { + let headers = self.auth_headers()?; + self.delete(url, Some(&headers), payload).await } /// Returns a single track given the track's ID, URI or URL. @@ -44,7 +149,7 @@ pub trait BaseClient { async fn track(&self, track_id: &TrackId) -> ClientResult { let url = format!("tracks/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Returns a list of tracks given a list of track IDs, URIs, or URLs. @@ -54,9 +159,9 @@ pub trait BaseClient { /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) - async fn tracks<'a>( + async fn tracks<'a, Tracks: IntoIterator>( &self, - track_ids: impl IntoIterator, + track_ids: Tracks, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(track_ids); @@ -66,7 +171,7 @@ pub trait BaseClient { let url = format!("tracks/?ids={}", ids); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) + convert_result::(&result).map(|x| x.tracks) } /// Returns a single artist given the artist's ID, URI or URL. @@ -78,7 +183,7 @@ pub trait BaseClient { async fn artist(&self, artist_id: &ArtistId) -> ClientResult { let url = format!("artists/{}", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Returns a list of artists given the artist IDs, URIs, or URLs. @@ -87,15 +192,15 @@ pub trait BaseClient { /// - artist_ids - a list of artist IDs, URIs or URLs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-artists) - async fn artists<'a>( + async fn artists<'a, Artists: IntoIterator>( &self, - artist_ids: impl IntoIterator, + artist_ids: Artists, ) -> ClientResult> { let ids = join_ids(artist_ids); let url = format!("artists/?ids={}", ids); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.artists) } @@ -112,17 +217,17 @@ pub trait BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) - fn artist_albums<'a>( + fn artist_albums<'a, Pag: Paginator> + 'a>( &'a self, artist_id: &'a ArtistId, album_type: Option<&'a AlbumType>, market: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -146,7 +251,7 @@ pub trait BaseClient { let url = format!("artists/{}/albums", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information about an artist's top 10 tracks by @@ -168,7 +273,7 @@ pub trait BaseClient { let url = format!("artists/{}/top-tracks", artist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result).map(|x| x.tracks) + convert_result::(&result).map(|x| x.tracks) } /// Get Spotify catalog information about artists similar to an identified @@ -182,7 +287,7 @@ pub trait BaseClient { async fn artist_related_artists(&self, artist_id: &ArtistId) -> ClientResult> { let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.artists) } @@ -196,7 +301,7 @@ pub trait BaseClient { let url = format!("albums/{}", album_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Returns a list of albums given the album IDs, URIs, or URLs. @@ -205,14 +310,14 @@ pub trait BaseClient { /// - albums_ids - a list of album IDs, URIs or URLs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-albums) - async fn albums<'a>( + async fn albums<'a, Albums: IntoIterator>( &self, - album_ids: impl IntoIterator, + album_ids: Albums, ) -> ClientResult> { let ids = join_ids(album_ids); let url = format!("albums/?ids={}", ids); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result::(&result).map(|x| x.albums) + convert_result::(&result).map(|x| x.albums) } /// Search for an Item. Get Spotify catalog information about artists, @@ -251,7 +356,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("search", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information about an album's tracks. @@ -265,13 +370,13 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) - fn album_track<'a>( + fn album_track<'a, Pag: Paginator> + 'a>( &'a self, album_id: &'a AlbumId, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -291,7 +396,7 @@ pub trait BaseClient { let url = format!("albums/{}/tracks", album_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Gets basic profile information about a Spotify User. @@ -303,7 +408,7 @@ pub trait BaseClient { async fn user(&self, user_id: &UserId) -> ClientResult { let url = format!("users/{}", user_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Get full details about Spotify playlist. @@ -326,7 +431,7 @@ pub trait BaseClient { let url = format!("playlists/{}", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information for a single show identified by its unique Spotify ID. @@ -345,7 +450,7 @@ pub trait BaseClient { let url = format!("shows/{}", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information for multiple shows based on their @@ -356,9 +461,9 @@ pub trait BaseClient { /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) - async fn get_several_shows<'a>( + async fn get_several_shows<'a, Shows: IntoIterator>( &self, - ids: impl IntoIterator, + ids: Shows, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(ids); @@ -368,7 +473,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("shows", ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.shows) } @@ -387,16 +492,16 @@ pub trait BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) - fn get_shows_episodes<'a>( + fn get_shows_episodes<'a, Pag: Paginator> + 'a>( &'a self, id: &'a ShowId, market: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -418,7 +523,7 @@ pub trait BaseClient { let url = format!("shows/{}/episodes", id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information for a single episode identified by its unique Spotify ID. @@ -441,7 +546,7 @@ pub trait BaseClient { }; let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Spotify catalog information for multiple episodes based on their Spotify IDs. @@ -451,9 +556,9 @@ pub trait BaseClient { /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) - async fn get_several_episodes<'a>( + async fn get_several_episodes<'a, Eps: IntoIterator>( &self, - ids: impl IntoIterator, + ids: Eps, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(ids); @@ -463,7 +568,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("episodes", ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.episodes) } @@ -476,7 +581,7 @@ pub trait BaseClient { async fn track_features(&self, track_id: &TrackId) -> ClientResult { let url = format!("audio-features/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Get Audio Features for Several Tracks @@ -485,9 +590,9 @@ pub trait BaseClient { /// - tracks a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) - async fn tracks_features<'a>( + async fn tracks_features<'a, Tracks: IntoIterator>( &self, - track_ids: impl IntoIterator, + track_ids: Tracks, ) -> ClientResult>> { let url = format!("audio-features/?ids={}", join_ids(track_ids)); @@ -495,7 +600,7 @@ pub trait BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result::>(&result) + convert_result::>(&result) .map(|option_payload| option_payload.map(|x| x.audio_features)) } } @@ -509,7 +614,7 @@ pub trait BaseClient { async fn track_analysis(&self, track_id: &TrackId) -> ClientResult { let url = format!("audio-analysis/{}", track_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Get a list of new album releases featured in Spotify @@ -527,14 +632,14 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) - fn categories<'a>( + fn categories<'a, Pag: Paginator> + 'a>( &'a self, locale: Option<&'a str>, country: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -555,7 +660,7 @@ pub trait BaseClient { optional "offset": offset.as_deref(), }; let result = self.endpoint_get("browse/categories", ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.categories) } @@ -573,16 +678,16 @@ pub trait BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) - fn category_playlists<'a>( + fn category_playlists<'a, Pag: Paginator> + 'a>( &'a self, category_id: &'a str, country: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -604,7 +709,7 @@ pub trait BaseClient { let url = format!("browse/categories/{}/playlists", category_id); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.playlists) } @@ -647,7 +752,7 @@ pub trait BaseClient { let result = self .endpoint_get("browse/featured-playlists", ¶ms) .await?; - self.convert_result(&result) + convert_result(&result) } /// Get a list of new album releases featured in Spotify. @@ -663,13 +768,13 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) - fn new_releases<'a>( + fn new_releases<'a, Pag: Paginator> + 'a>( &'a self, country: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -689,7 +794,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("browse/new-releases", ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.albums) } @@ -764,7 +869,7 @@ pub trait BaseClient { } let result = self.endpoint_get("recommendations", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get full details of the tracks of a playlist owned by a user. @@ -780,17 +885,17 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) - fn playlist_tracks<'a>( + fn playlist_tracks<'a, Pag: Paginator> + 'a>( &'a self, playlist_id: &'a PlaylistId, fields: Option<&'a str>, market: Option<&'a Market>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -814,7 +919,7 @@ pub trait BaseClient { let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Gets playlists of a user. @@ -828,13 +933,13 @@ pub trait BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - fn user_playlists<'a>( + fn user_playlists<'a, Pag: Paginator> + 'a>( &'a self, user_id: &'a UserId, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -854,6 +959,6 @@ pub trait BaseClient { let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } } diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 6aee967d..f14d3b13 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -7,7 +7,7 @@ pub use oauth::OAuthClient; use crate::{ model::{idtypes::IdType, Id}, - ClientResult, + ClientResult, Token, }; use serde::Deserialize; @@ -35,135 +35,6 @@ pub(in crate) fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>().join(",") } -/// HTTP-related methods for the Spotify client. It wraps the basic HTTP client -/// with features needed of higher level. -/// -/// The Spotify client has two different wrappers to perform requests: -/// -/// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only -/// append the configured Spotify API URL to the relative URL provided so that -/// it's not forgotten. They're used in the authentication process to request -/// an access token and similars. -/// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, -/// `endpoint_delete`. These append the authentication headers for endpoint -/// requests to reduce the code needed for endpoints and make them as concise -/// as possible. -impl Spotify { - /// If it's a relative URL like "me", the prefix is appended to it. - /// Otherwise, the same URL is returned. - fn endpoint_url(&self, url: &str) -> String { - // Using the client's prefix in case it's a relative route. - if !url.starts_with("http") { - self.prefix.clone() + url - } else { - url.to_string() - } - } - - /// The headers required for authenticated requests to the API - fn auth_headers(&self) -> ClientResult { - let mut auth = Headers::new(); - let (key, val) = headers::bearer_auth(self.get_token()?); - auth.insert(key, val); - - Ok(auth) - } - - #[inline] - #[maybe_async] - pub(crate) async fn get( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Query<'_>, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.get(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn post( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.post(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn post_form( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Form<'_>, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.post_form(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn put( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.put(&url, headers, payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn delete( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Value, - ) -> ClientResult { - let url = self.endpoint_url(url); - self.http.delete(&url, headers, payload).await - } - - /// The wrapper for the endpoints, which also includes the required - /// autentication. - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_get( - &self, - url: &str, - payload: &Query<'_>, - ) -> ClientResult { - let headers = self.auth_headers()?; - self.get(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_post(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.post(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_put(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.put(url, Some(&headers), payload).await - } - - #[inline] - #[maybe_async] - pub(crate) async fn endpoint_delete(&self, url: &str, payload: &Value) -> ClientResult { - let headers = self.auth_headers()?; - self.delete(url, Some(&headers), payload).await - } -} - /// Generates an HTTP token authorization header with proper formatting pub fn bearer_auth(tok: &Token) -> (String, String) { let auth = "authorization".to_owned(); diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index d73a4c39..f1ecf2a6 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -2,7 +2,7 @@ use std::time; use crate::{ endpoints::{ - join_ids, + join_ids, convert_result, append_device_id, pagination::{paginate, Paginator}, BaseClient, }, @@ -21,11 +21,6 @@ use serde_json::{json, Map}; pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; - fn user_endpoint(&self) { - println!("Performing OAuth request"); - self.endpoint_request(); - } - /// Get current user playlists without required getting his profile. /// /// Parameters: @@ -36,10 +31,10 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - fn current_user_playlists(&self) -> impl Paginator> + '_ { + fn current_user_playlists>>(&self) -> Pag { paginate( move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -57,7 +52,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/playlists", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Gets playlist of a user. @@ -83,7 +78,7 @@ pub trait OAuthClient: BaseClient { None => format!("users/{}/starred", user_id.id()), }; let result = self.endpoint_get(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Creates a playlist for a user. @@ -113,7 +108,7 @@ pub trait OAuthClient: BaseClient { let url = format!("users/{}/playlists", user_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Changes a playlist's name and/or public/private state. @@ -164,10 +159,10 @@ pub trait OAuthClient: BaseClient { /// - position - the position to add the tracks /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) - async fn playlist_add_tracks<'a>( + async fn playlist_add_tracks<'a, Tracks: IntoIterator>( &self, playlist_id: &PlaylistId, - track_ids: impl IntoIterator, + track_ids: Tracks, position: Option, ) -> ClientResult { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -178,7 +173,7 @@ pub trait OAuthClient: BaseClient { let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_post(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Replace all tracks in a playlist @@ -189,10 +184,10 @@ pub trait OAuthClient: BaseClient { /// - tracks - the list of track ids to add to the playlist /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - async fn playlist_replace_tracks<'a>( + async fn playlist_replace_tracks<'a, Tracks: IntoIterator>( &self, playlist_id: &PlaylistId, - track_ids: impl IntoIterator, + track_ids: Tracks, ) -> ClientResult<()> { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); let params = build_json! { @@ -217,10 +212,10 @@ pub trait OAuthClient: BaseClient { /// - snapshot_id - optional playlist's snapshot ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - async fn playlist_reorder_tracks( + async fn playlist_reorder_tracks( &self, playlist_id: &PlaylistId, - uris: Option<&[&Id]>, + uris: Option<&[&Id]>, range_start: Option, insert_before: Option, range_length: Option, @@ -238,7 +233,7 @@ pub trait OAuthClient: BaseClient { let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_put(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Removes all occurrences of the given tracks from the given playlist. @@ -249,10 +244,10 @@ pub trait OAuthClient: BaseClient { /// - snapshot_id - optional id of the playlist snapshot /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - async fn playlist_remove_all_occurrences_of_tracks<'a>( + async fn playlist_remove_all_occurrences_of_tracks<'a, Tracks: IntoIterator>( &self, playlist_id: &PlaylistId, - track_ids: impl IntoIterator, + track_ids: Tracks, snapshot_id: Option<&str>, ) -> ClientResult { let tracks = track_ids @@ -271,7 +266,7 @@ pub trait OAuthClient: BaseClient { let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Removes specfic occurrences of the given tracks from the given playlist. @@ -326,7 +321,7 @@ pub trait OAuthClient: BaseClient { let url = format!("playlists/{}/tracks", playlist_id.id()); let result = self.endpoint_delete(&url, ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Add the current authenticated user as a follower of a playlist. @@ -377,7 +372,7 @@ pub trait OAuthClient: BaseClient { .join(","), ); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Get detailed profile information about the current user. @@ -386,7 +381,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-current-users-profile) async fn me(&self) -> ClientResult { let result = self.endpoint_get("me/", &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Get detailed profile information about the current user. @@ -407,7 +402,7 @@ pub trait OAuthClient: BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result(&result) + convert_result(&result) } } @@ -423,10 +418,10 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - fn current_user_saved_albums(&self) -> impl Paginator> + '_ { + fn current_user_saved_albums>>(&self) -> Pag { paginate( move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -445,7 +440,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/albums", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get a list of the songs saved in the current Spotify user's "Your Music" @@ -460,10 +455,10 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - fn current_user_saved_tracks(&self) -> impl Paginator> + '_ { + fn current_user_saved_tracks>>(&self) -> Pag { paginate( move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -482,7 +477,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/tracks", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Gets a list of the artists followed by the current authorized user. @@ -505,7 +500,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/following", ¶ms).await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.artists) } @@ -515,9 +510,9 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) - async fn current_user_saved_tracks_delete<'a>( + async fn current_user_saved_tracks_delete<'a, Tracks: IntoIterator>( &self, - track_ids: impl IntoIterator, + track_ids: Tracks, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -532,13 +527,13 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) - async fn current_user_saved_tracks_contains<'a>( + async fn current_user_saved_tracks_contains<'a, Tracks: IntoIterator>( &self, - track_ids: impl IntoIterator, + track_ids: Tracks, ) -> ClientResult> { let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Save one or more tracks to the current user's "Your Music" library. @@ -547,9 +542,9 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) - async fn current_user_saved_tracks_add<'a>( + async fn current_user_saved_tracks_add<'a, Tracks: IntoIterator>( &self, - track_ids: impl IntoIterator, + track_ids: Tracks, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -568,15 +563,15 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - fn current_user_top_artists<'a>( + fn current_user_top_artists<'a, Pag: Paginator> + 'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -596,7 +591,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get(&"me/top/artists", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get the current user's top tracks. @@ -610,15 +605,15 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - fn current_user_top_tracks<'a>( + fn current_user_top_tracks<'a, Pag: Paginator> + 'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> impl Paginator> + 'a { + ) -> Pag { paginate( move |limit, offset| { self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) }, - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -638,7 +633,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/top/tracks", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get the current user's recently played tracks. @@ -659,7 +654,7 @@ pub trait OAuthClient: BaseClient { let result = self .endpoint_get("me/player/recently-played", ¶ms) .await?; - self.convert_result(&result) + convert_result(&result) } /// Add one or more albums to the current user's "Your Music" library. @@ -668,9 +663,9 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) - async fn current_user_saved_albums_add<'a>( + async fn current_user_saved_albums_add<'a, Albums: IntoIterator>( &self, - album_ids: impl IntoIterator, + album_ids: Albums, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -684,9 +679,9 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) - async fn current_user_saved_albums_delete<'a>( + async fn current_user_saved_albums_delete<'a, Albums: IntoIterator>( &self, - album_ids: impl IntoIterator, + album_ids: Albums, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -701,13 +696,13 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) - async fn current_user_saved_albums_contains<'a>( + async fn current_user_saved_albums_contains<'a, Albums: IntoIterator>( &self, - album_ids: impl IntoIterator, + album_ids: Albums, ) -> ClientResult> { let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Follow one or more artists. @@ -716,9 +711,9 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - async fn user_follow_artists<'a>( + async fn user_follow_artists<'a, Artists: IntoIterator>( &self, - artist_ids: impl IntoIterator, + artist_ids: Artists, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -732,9 +727,9 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - async fn user_unfollow_artists<'a>( + async fn user_unfollow_artists<'a, Artists: IntoIterator>( &self, - artist_ids: impl IntoIterator, + artist_ids: Artists, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -749,16 +744,16 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - the ids of the users that you want to /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) - async fn user_artist_check_follow<'a>( + async fn user_artist_check_follow<'a, Artists: IntoIterator>( &self, - artist_ids: impl IntoIterator, + artist_ids: Artists, ) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", join_ids(artist_ids) ); let result = self.endpoint_get(&url, &Query::new()).await?; - self.convert_result(&result) + convert_result(&result) } /// Follow one or more users. @@ -767,9 +762,9 @@ pub trait OAuthClient: BaseClient { /// - user_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - async fn user_follow_users<'a>( + async fn user_follow_users<'a, Users: IntoIterator>( &self, - user_ids: impl IntoIterator, + user_ids: Users, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -783,9 +778,9 @@ pub trait OAuthClient: BaseClient { /// - user_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - async fn user_unfollow_users<'a>( + async fn user_unfollow_users<'a, Users: IntoIterator>( &self, - user_ids: impl IntoIterator, + user_ids: Users, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -800,7 +795,7 @@ pub trait OAuthClient: BaseClient { let result = self .endpoint_get("me/player/devices", &Query::new()) .await?; - self.convert_result::(&result) + convert_result::(&result) .map(|x| x.devices) } @@ -829,7 +824,7 @@ pub trait OAuthClient: BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result(&result) + convert_result(&result) } } @@ -860,7 +855,7 @@ pub trait OAuthClient: BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result(&result) + convert_result(&result) } } @@ -904,11 +899,11 @@ pub trait OAuthClient: BaseClient { /// - position_ms - Indicates from what position to start playback. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-start-a-users-playback) - async fn start_context_playback( + async fn start_context_playback( &self, - context_uri: &Id, + context_uri: &Id, device_id: Option<&str>, - offset: Option>, + offset: Option>, position_ms: Option, ) -> ClientResult<()> { let params = build_json! { @@ -921,7 +916,7 @@ pub trait OAuthClient: BaseClient { }; - let url = self.append_device_id("me/player/play", device_id); + let url = append_device_id("me/player/play", device_id); self.put(&url, None, ¶ms).await?; Ok(()) @@ -943,7 +938,7 @@ pub trait OAuthClient: BaseClient { }), }; - let url = self.append_device_id("me/player/play", device_id); + let url = append_device_id("me/player/play", device_id); self.endpoint_put(&url, ¶ms).await?; Ok(()) @@ -956,7 +951,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-pause-a-users-playback) async fn pause_playback(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/pause", device_id); + let url = append_device_id("me/player/pause", device_id); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -969,7 +964,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-next-track) async fn next_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/next", device_id); + let url = append_device_id("me/player/next", device_id); self.endpoint_post(&url, &json!({})).await?; Ok(()) @@ -982,7 +977,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-skip-users-playback-to-previous-track) async fn previous_track(&self, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id("me/player/previous", device_id); + let url = append_device_id("me/player/previous", device_id); self.endpoint_post(&url, &json!({})).await?; Ok(()) @@ -996,7 +991,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-seek-to-position-in-currently-playing-track) async fn seek_track(&self, position_ms: u32, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( + let url = append_device_id( &format!("me/player/seek?position_ms={}", position_ms), device_id, ); @@ -1013,7 +1008,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) async fn repeat(&self, state: RepeatState, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( + let url = append_device_id( &format!("me/player/repeat?state={}", state.as_ref()), device_id, ); @@ -1033,7 +1028,7 @@ pub trait OAuthClient: BaseClient { if volume_percent > 100u8 { error!("volume must be between 0 and 100, inclusive"); } - let url = self.append_device_id( + let url = append_device_id( &format!("me/player/volume?volume_percent={}", volume_percent), device_id, ); @@ -1050,7 +1045,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-toggle-shuffle-for-users-playback) async fn shuffle(&self, state: bool, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); + let url = append_device_id(&format!("me/player/shuffle?state={}", state), device_id); self.endpoint_put(&url, &json!({})).await?; Ok(()) @@ -1065,12 +1060,12 @@ pub trait OAuthClient: BaseClient { /// targeted /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-to-queue) - async fn add_item_to_queue( + async fn add_item_to_queue( &self, - item: &Id, + item: &Id, device_id: Option<&str>, ) -> ClientResult<()> { - let url = self.append_device_id(&format!("me/player/queue?uri={}", item), device_id); + let url = append_device_id(&format!("me/player/queue?uri={}", item), device_id); self.endpoint_post(&url, &json!({})).await?; Ok(()) @@ -1083,9 +1078,9 @@ pub trait OAuthClient: BaseClient { /// be added to the user’s library. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) - async fn save_shows<'a>( + async fn save_shows<'a, Shows: IntoIterator>( &self, - show_ids: impl IntoIterator, + show_ids: Shows, ) -> ClientResult<()> { let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -1106,10 +1101,10 @@ pub trait OAuthClient: BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - fn get_saved_show(&self) -> impl Paginator> + '_ { + fn get_saved_show>>(&self) -> Pag { paginate( move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), - self.pagination_chunks, + self.get_config().pagination_chunks, ) } @@ -1127,7 +1122,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/shows", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Check if one or more shows is already saved in the current Spotify user’s library. @@ -1136,16 +1131,16 @@ pub trait OAuthClient: BaseClient { /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) - async fn check_users_saved_shows<'a>( + async fn check_users_saved_shows<'a, Shows: IntoIterator>( &self, - ids: impl IntoIterator, + ids: Shows, ) -> ClientResult> { let ids = join_ids(ids); let params = build_map! { "ids": &ids, }; let result = self.endpoint_get("me/shows/contains", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Delete one or more shows from current Spotify user's library. @@ -1156,9 +1151,9 @@ pub trait OAuthClient: BaseClient { /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) - async fn remove_users_saved_shows<'a>( + async fn remove_users_saved_shows<'a, Shows: IntoIterator>( &self, - show_ids: impl IntoIterator, + show_ids: Shows, country: Option<&Market>, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); diff --git a/src/endpoints/pagination/iter.rs b/src/endpoints/pagination/iter.rs index 69475652..733dbb05 100644 --- a/src/endpoints/pagination/iter.rs +++ b/src/endpoints/pagination/iter.rs @@ -1,20 +1,20 @@ //! Synchronous implementation of automatic pagination requests. -use crate::client::{ClientError, ClientResult}; -use crate::model::Page; +use crate::{model::Page, ClientError, ClientResult}; /// Alias for `Iterator`, since sync mode is enabled. pub trait Paginator: Iterator {} impl> Paginator for I {} /// This is used to handle paginated requests automatically. -pub fn paginate<'a, T, Request>( +pub fn paginate<'a, T, Request, It>( req: Request, page_size: u32, -) -> impl Iterator> + 'a +) -> It where T: 'a, Request: Fn(u32, u32) -> ClientResult> + 'a, + It: Iterator> + 'a { let pages = PageIterator { req, diff --git a/src/endpoints/pagination/stream.rs b/src/endpoints/pagination/stream.rs index dce0be01..0f1bd16c 100644 --- a/src/endpoints/pagination/stream.rs +++ b/src/endpoints/pagination/stream.rs @@ -1,6 +1,6 @@ //! Asynchronous implementation of automatic pagination requests. -use crate::client::ClientResult; +use crate::ClientResult; use crate::model::Page; use futures::future::Future; use futures::stream::Stream; @@ -10,14 +10,15 @@ pub trait Paginator: Stream {} impl> Paginator for I {} /// This is used to handle paginated requests automatically. -pub fn paginate<'a, T, Fut, Request>( +pub fn paginate<'a, T, Fut, Request, S>( req: Request, page_size: u32, -) -> impl Stream> + 'a +) -> S where T: Unpin + 'a, Fut: Future>>, Request: Fn(u32, u32) -> Fut + 'a, + S: Stream> + 'a { use async_stream::stream; let mut offset = 0; diff --git a/src/lib.rs b/src/lib.rs index a8598b38..7ae246f6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -174,7 +174,7 @@ pub use rspotify_model as model; pub use macros::scopes; use std::{ - collections::{HashMap, HashSet}, + collections::HashSet, env, fs, io::{Read, Write}, path::Path, @@ -187,6 +187,10 @@ use getrandom::getrandom; use serde::{Deserialize, Serialize}; use thiserror::Error; +pub mod prelude { + pub use crate::endpoints::{BaseClient, OAuthClient}; +} + /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] pub enum ClientError { @@ -200,6 +204,9 @@ pub enum ClientError { #[error("url parse error: {0}")] ParseUrl(#[from] url::ParseError), + #[error("http error: {0}")] + Http(#[from] http::Error), + #[error("input/output error: {0}")] Io(#[from] std::io::Error), @@ -218,6 +225,7 @@ pub const DEFAULT_CACHE_PATH: &str = ".spotify_token_cache.json"; pub const DEFAULT_PAGINATION_CHUNKS: u32 = 50; /// Struct to configure the Spotify client. +#[derive(Debug, Clone)] pub struct Config { /// The Spotify API prefix, [`DEFAULT_API_PREFIX`] by default. pub prefix: String, From 61edf5dbb6acfdf42b01dd7b8369a0311a051d4b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 01:12:39 +0200 Subject: [PATCH 05/56] it compiles... but at what cost --- src/client_creds.rs | 2 +- src/code_auth.rs | 2 +- src/code_auth_pkce.rs | 6 +- src/endpoints/base.rs | 93 +++++++++++++----------------- src/endpoints/oauth.rs | 53 ++++++++--------- src/endpoints/pagination/iter.rs | 5 +- src/endpoints/pagination/stream.rs | 7 +-- 7 files changed, 80 insertions(+), 88 deletions(-) diff --git a/src/client_creds.rs b/src/client_creds.rs index 48f506b4..470600d2 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,4 +1,4 @@ -use crate::{endpoints::BaseClient, Config, Credentials, http::HttpClient, Token}; +use crate::{endpoints::BaseClient, http::HttpClient, Config, Credentials, Token}; #[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { diff --git a/src/code_auth.rs b/src/code_auth.rs index b9715d65..47b1d706 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Credentials, http::HttpClient, OAuth, Token, Config}; +use crate::{http::HttpClient, prelude::*, Config, Credentials, OAuth, Token}; #[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 9f8945ed..1ccbf4d0 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -1,4 +1,8 @@ -use crate::{endpoints::{BaseClient, OAuthClient}, Credentials, http::HttpClient, OAuth, Token, Config}; +use crate::{ + endpoints::{BaseClient, OAuthClient}, + http::HttpClient, + Config, Credentials, OAuth, Token, +}; #[derive(Clone, Debug, Default)] pub struct CodeAuthPKCESpotify { diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 3e0b34ed..626b2388 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,9 +1,9 @@ use crate::{ endpoints::{ - join_ids, bearer_auth, convert_result, + bearer_auth, convert_result, join_ids, pagination::{paginate, Paginator}, }, - http::{Query, Headers, HttpClient, Form, BaseHttpClient}, + http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, model::*, ClientResult, Config, Credentials, Token, @@ -113,11 +113,7 @@ pub trait BaseClient { /// The wrapper for the endpoints, which also includes the required /// autentication. #[inline] - async fn endpoint_get( - &self, - url: &str, - payload: &Query<'_>, - ) -> ClientResult { + async fn endpoint_get(&self, url: &str, payload: &Query<'_>) -> ClientResult { let headers = self.auth_headers()?; Ok(self.get(url, Some(&headers), payload).await?) } @@ -200,8 +196,7 @@ pub trait BaseClient { let url = format!("artists/?ids={}", ids); let result = self.endpoint_get(&url, &Query::new()).await?; - convert_result::(&result) - .map(|x| x.artists) + convert_result::(&result).map(|x| x.artists) } /// Get Spotify catalog information about an artist's albums. @@ -217,18 +212,18 @@ pub trait BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) - fn artist_albums<'a, Pag: Paginator> + 'a>( + fn artist_albums<'a>( &'a self, artist_id: &'a ArtistId, album_type: Option<&'a AlbumType>, market: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::artist_albums`]. @@ -287,8 +282,7 @@ pub trait BaseClient { async fn artist_related_artists(&self, artist_id: &ArtistId) -> ClientResult> { let url = format!("artists/{}/related-artists", artist_id.id()); let result = self.endpoint_get(&url, &Query::new()).await?; - convert_result::(&result) - .map(|x| x.artists) + convert_result::(&result).map(|x| x.artists) } /// Returns a single album given the album's ID, URIs or URL. @@ -373,11 +367,11 @@ pub trait BaseClient { fn album_track<'a, Pag: Paginator> + 'a>( &'a self, album_id: &'a AlbumId, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::album_track`]. @@ -473,8 +467,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("shows", ¶ms).await?; - convert_result::(&result) - .map(|x| x.shows) + convert_result::(&result).map(|x| x.shows) } /// Get Spotify catalog information about an show’s episodes. Optional @@ -492,17 +485,17 @@ pub trait BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) - fn get_shows_episodes<'a, Pag: Paginator> + 'a>( + fn get_shows_episodes<'a>( &'a self, id: &'a ShowId, market: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::get_shows_episodes`]. @@ -568,8 +561,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("episodes", ¶ms).await?; - convert_result::(&result) - .map(|x| x.episodes) + convert_result::(&result).map(|x| x.episodes) } /// Get audio features for a track @@ -632,15 +624,15 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) - fn categories<'a, Pag: Paginator> + 'a>( + fn categories<'a>( &'a self, locale: Option<&'a str>, country: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::categories`]. @@ -660,8 +652,7 @@ pub trait BaseClient { optional "offset": offset.as_deref(), }; let result = self.endpoint_get("browse/categories", ¶ms).await?; - convert_result::(&result) - .map(|x| x.categories) + convert_result::(&result).map(|x| x.categories) } /// Get a list of playlists in a category in Spotify @@ -678,17 +669,17 @@ pub trait BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) - fn category_playlists<'a, Pag: Paginator> + 'a>( + fn category_playlists<'a>( &'a self, category_id: &'a str, country: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::category_playlists`]. @@ -709,8 +700,7 @@ pub trait BaseClient { let url = format!("browse/categories/{}/playlists", category_id); let result = self.endpoint_get(&url, ¶ms).await?; - convert_result::(&result) - .map(|x| x.playlists) + convert_result::(&result).map(|x| x.playlists) } /// Get a list of Spotify featured playlists. @@ -768,14 +758,14 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) - fn new_releases<'a, Pag: Paginator> + 'a>( + fn new_releases<'a>( &'a self, country: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::new_releases`]. @@ -794,8 +784,7 @@ pub trait BaseClient { }; let result = self.endpoint_get("browse/new-releases", ¶ms).await?; - convert_result::(&result) - .map(|x| x.albums) + convert_result::(&result).map(|x| x.albums) } /// Get Recommendations Based on Seeds @@ -885,18 +874,18 @@ pub trait BaseClient { /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) - fn playlist_tracks<'a, Pag: Paginator> + 'a>( + fn playlist_tracks<'a>( &'a self, playlist_id: &'a PlaylistId, fields: Option<&'a str>, market: Option<&'a Market>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::playlist_tracks`]. @@ -933,14 +922,14 @@ pub trait BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - fn user_playlists<'a, Pag: Paginator> + 'a>( + fn user_playlists<'a>( &'a self, user_id: &'a UserId, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::user_playlists`]. diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f1ecf2a6..efc8513a 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -2,7 +2,7 @@ use std::time; use crate::{ endpoints::{ - join_ids, convert_result, append_device_id, + append_device_id, convert_result, join_ids, pagination::{paginate, Paginator}, BaseClient, }, @@ -31,11 +31,11 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - fn current_user_playlists>>(&self) -> Pag { - paginate( + fn current_user_playlists(&self) -> Box> + '_>{ + Box::new(paginate( move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::current_user_playlists`]. @@ -244,7 +244,10 @@ pub trait OAuthClient: BaseClient { /// - snapshot_id - optional id of the playlist snapshot /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - async fn playlist_remove_all_occurrences_of_tracks<'a, Tracks: IntoIterator>( + async fn playlist_remove_all_occurrences_of_tracks< + 'a, + Tracks: IntoIterator, + >( &self, playlist_id: &PlaylistId, track_ids: Tracks, @@ -418,11 +421,11 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - fn current_user_saved_albums>>(&self) -> Pag { - paginate( + fn current_user_saved_albums(&self) -> Box> + '_> { + Box::new(paginate( move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of @@ -455,11 +458,11 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - fn current_user_saved_tracks>>(&self) -> Pag { - paginate( + fn current_user_saved_tracks(&self) -> Box> + '_> { + Box::new(paginate( move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of @@ -500,8 +503,7 @@ pub trait OAuthClient: BaseClient { }; let result = self.endpoint_get("me/following", ¶ms).await?; - convert_result::(&result) - .map(|x| x.artists) + convert_result::(&result).map(|x| x.artists) } /// Remove one or more tracks from the current user's "Your Music" library. @@ -563,16 +565,16 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - fn current_user_top_artists<'a, Pag: Paginator> + 'a>( + fn current_user_top_artists<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::current_user_top_artists`]. @@ -605,16 +607,16 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) - fn current_user_top_tracks<'a, Pag: Paginator> + 'a>( + fn current_user_top_tracks<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> Pag { - paginate( + ) -> Box> + 'a> { + Box::new(paginate( move |limit, offset| { self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::current_user_top_tracks`]. @@ -795,8 +797,7 @@ pub trait OAuthClient: BaseClient { let result = self .endpoint_get("me/player/devices", &Query::new()) .await?; - convert_result::(&result) - .map(|x| x.devices) + convert_result::(&result).map(|x| x.devices) } /// Get Information About The User’s Current Playback @@ -1101,11 +1102,11 @@ pub trait OAuthClient: BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - fn get_saved_show>>(&self) -> Pag { - paginate( + fn get_saved_show(&self) -> Box> + '_> { + Box::new(paginate( move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - ) + )) } /// The manually paginated version of [`Spotify::get_saved_show`]. diff --git a/src/endpoints/pagination/iter.rs b/src/endpoints/pagination/iter.rs index 733dbb05..1b6cb6ed 100644 --- a/src/endpoints/pagination/iter.rs +++ b/src/endpoints/pagination/iter.rs @@ -7,14 +7,13 @@ pub trait Paginator: Iterator {} impl> Paginator for I {} /// This is used to handle paginated requests automatically. -pub fn paginate<'a, T, Request, It>( +pub fn paginate<'a, T, Request>( req: Request, page_size: u32, -) -> It +) -> impl Iterator> + 'a where T: 'a, Request: Fn(u32, u32) -> ClientResult> + 'a, - It: Iterator> + 'a { let pages = PageIterator { req, diff --git a/src/endpoints/pagination/stream.rs b/src/endpoints/pagination/stream.rs index 0f1bd16c..815267f3 100644 --- a/src/endpoints/pagination/stream.rs +++ b/src/endpoints/pagination/stream.rs @@ -1,7 +1,7 @@ //! Asynchronous implementation of automatic pagination requests. -use crate::ClientResult; use crate::model::Page; +use crate::ClientResult; use futures::future::Future; use futures::stream::Stream; @@ -10,15 +10,14 @@ pub trait Paginator: Stream {} impl> Paginator for I {} /// This is used to handle paginated requests automatically. -pub fn paginate<'a, T, Fut, Request, S>( +pub fn paginate<'a, T, Fut, Request>( req: Request, page_size: u32, -) -> S +) -> impl Stream> + 'a where T: Unpin + 'a, Fut: Future>>, Request: Fn(u32, u32) -> Fut + 'a, - S: Stream> + 'a { use async_stream::stream; let mut offset = 0; From 82f1825ed87c3c88bac0d164aac552c2ad1616ae Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 01:15:17 +0200 Subject: [PATCH 06/56] format --- src/endpoints/oauth.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index efc8513a..12afdcfe 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -31,7 +31,7 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - fn current_user_playlists(&self) -> Box> + '_>{ + fn current_user_playlists(&self) -> Box> + '_> { Box::new(paginate( move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, From b47f27379025867084c49d1cf3286c4bdd8e8430 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 01:52:19 +0200 Subject: [PATCH 07/56] numerous fixes --- Cargo.toml | 2 +- rspotify-http/src/lib.rs | 17 ---- src/client_creds.rs | 50 ++++++++---- src/code_auth.rs | 170 ++++++++++++++++++++++++++++++++++----- src/code_auth_pkce.rs | 36 +++++---- src/endpoints/oauth.rs | 4 +- src/lib.rs | 42 ++++++++-- src/oauth2.rs | 139 -------------------------------- 8 files changed, 244 insertions(+), 216 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d6c10292..89a3a3a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ reqwest-native-tls-vendored = ["rspotify-http/reqwest-native-tls-vendored"] ureq-rustls-tls = ["rspotify-http/ureq-rustls-tls"] # Internal features for checking async or sync compilation -__async = ["futures", "async-stream"] +__async = ["futures", "async-stream", "async-trait"] __sync = [] [package.metadata.docs.rs] diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index de5fecb2..a125b214 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -38,23 +38,6 @@ pub type Headers = HashMap; pub type Query<'a> = HashMap<&'a str, &'a str>; pub type Form<'a> = HashMap<&'a str, &'a str>; -pub mod headers { - // Common headers as constants - pub const CLIENT_ID: &str = "client_id"; - pub const CODE: &str = "code"; - pub const GRANT_AUTH_CODE: &str = "authorization_code"; - pub const GRANT_CLIENT_CREDS: &str = "client_credentials"; - pub const GRANT_REFRESH_TOKEN: &str = "refresh_token"; - pub const GRANT_TYPE: &str = "grant_type"; - pub const REDIRECT_URI: &str = "redirect_uri"; - pub const REFRESH_TOKEN: &str = "refresh_token"; - pub const RESPONSE_CODE: &str = "code"; - pub const RESPONSE_TYPE: &str = "response_type"; - pub const SCOPE: &str = "scope"; - pub const SHOW_DIALOG: &str = "show_dialog"; - pub const STATE: &str = "state"; -} - #[derive(thiserror::Error, Debug)] pub enum Error { #[error("request unauthorized")] diff --git a/src/client_creds.rs b/src/client_creds.rs index 470600d2..dde1b536 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -8,6 +8,25 @@ pub struct ClientCredentialsSpotify { pub(in crate) http: HttpClient, } +// This could even use a macro +impl BaseClient for ClientCredentialsSpotify { + fn get_http(&self) -> &HttpClient { + &self.http + } + + fn get_token(&self) -> Option<&Token> { + self.token.as_ref() + } + + fn get_creds(&self) -> &Credentials { + &self.creds + } + + fn get_config(&self) -> &Config { + &self.config + } +} + impl ClientCredentialsSpotify { pub fn new(creds: Credentials) -> Self { ClientCredentialsSpotify { @@ -24,26 +43,23 @@ impl ClientCredentialsSpotify { } } - pub fn request_token(&mut self) { - todo!() - } -} + /// Obtains the client access token for the app without saving it into the + /// cache file. The resulting token is saved internally. + #[maybe_async] + pub async fn request_client_token_without_cache(&mut self) -> ClientResult<()> { + let mut data = Form::new(); + data.insert(headers::GRANT_TYPE, headers::GRANT_CLIENT_CREDS); -// This could even use a macro -impl BaseClient for ClientCredentialsSpotify { - fn get_http(&self) -> &HttpClient { - &self.http - } + self.token = Some(self.fetch_access_token(&data).await?); - fn get_token(&self) -> Option<&Token> { - self.token.as_ref() - } - - fn get_creds(&self) -> &Credentials { - &self.creds + Ok(()) } - fn get_config(&self) -> &Config { - &self.config + /// The same as `request_client_token_without_cache`, but saves the token + /// into the cache file if possible. + #[maybe_async] + pub async fn request_client_token(&mut self) -> ClientResult<()> { + self.request_client_token_without_cache().await?; + self.write_token_cache() } } diff --git a/src/code_auth.rs b/src/code_auth.rs index 47b1d706..5ed78bc5 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,34 +1,32 @@ -use crate::{http::HttpClient, prelude::*, Config, Credentials, OAuth, Token}; +use crate::{ + auth_urls, + endpoints::{BaseClient, OAuthClient}, + headers, + http::{Form, HttpClient}, + ClientResult, Config, Credentials, OAuth, Token, +}; + +use std::collections::HashMap; + +use maybe_async::maybe_async; +use url::Url; #[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { creds: Credentials, oauth: OAuth, - tok: Option, + config: Config, + token: Option, http: HttpClient, } -impl CodeAuthSpotify { - pub fn new(creds: Credentials, oauth: OAuth) -> Self { - CodeAuthSpotify { - creds, - oauth, - ..Default::default() - } - } - - pub fn prompt_for_user_token(&mut self) { - todo!() - } -} - impl BaseClient for CodeAuthSpotify { fn get_http(&self) -> &HttpClient { &self.http } fn get_token(&self) -> Option<&Token> { - self.tok.as_ref() + self.token.as_ref() } fn get_creds(&self) -> &Credentials { @@ -36,7 +34,7 @@ impl BaseClient for CodeAuthSpotify { } fn get_config(&self) -> &Config { - todo!() + &self.config } } @@ -46,3 +44,139 @@ impl OAuthClient for CodeAuthSpotify { &self.oauth } } + +impl CodeAuthSpotify { + pub fn new(creds: Credentials, oauth: OAuth) -> Self { + CodeAuthSpotify { + creds, + oauth, + ..Default::default() + } + } + + pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { + CodeAuthSpotify { + creds, + oauth, + config, + ..Default::default() + } + } + + /// Gets the required URL to authorize the current client to start the + /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). + pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { + let mut payload: HashMap<&str, &str> = HashMap::new(); + let oauth = self.get_oauth(); + let scope = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); + payload.insert(headers::CLIENT_ID, &self.get_creds().id); + payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); + payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); + payload.insert(headers::SCOPE, &scope); + payload.insert(headers::STATE, &oauth.state); + + if show_dialog { + payload.insert(headers::SHOW_DIALOG, "true"); + } + + let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; + Ok(parsed.into_string()) + } + + /// Refreshes the access token with the refresh token provided by the + /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow), + /// without saving it into the cache file. + /// + /// The obtained token will be saved internally. + #[maybe_async] + pub async fn refresh_user_token_without_cache( + &mut self, + refresh_token: &str, + ) -> ClientResult<()> { + let mut data = Form::new(); + data.insert(headers::REFRESH_TOKEN, refresh_token); + data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); + + let mut tok = self.fetch_access_token(&data).await?; + tok.refresh_token = Some(refresh_token.to_string()); + self.token = Some(tok); + + Ok(()) + } + + /// The same as `refresh_user_token_without_cache`, but saves the token + /// into the cache file if possible. + #[maybe_async] + pub async fn refresh_user_token(&mut self, refresh_token: &str) -> ClientResult<()> { + self.refresh_user_token_without_cache(refresh_token).await?; + + Ok(()) + } + + /// Parse the response code in the given response url. If the URL cannot be + /// parsed or the `code` parameter is not present, this will return `None`. + pub fn parse_response_code(&self, url: &str) -> Option { + let url = Url::parse(url).ok()?; + let mut params = url.query_pairs(); + let (_, url) = params.find(|(key, _)| key == "code")?; + Some(url.to_string()) + } + + /// Obtains the user access token for the app with the given code without + /// saving it into the cache file, as part of the OAuth authentication. + /// The access token will be saved inside the Spotify instance. + #[maybe_async] + pub async fn request_user_token_without_cache(&mut self, code: &str) -> ClientResult<()> { + let oauth = self.get_oauth()?; + let mut data = Form::new(); + let scopes = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); + data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); + data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); + data.insert(headers::CODE, code); + data.insert(headers::SCOPE, scopes.as_ref()); + data.insert(headers::STATE, oauth.state.as_ref()); + + self.token = Some(self.fetch_access_token(&data).await?); + + Ok(()) + } + + /// Tries to open the authorization URL in the user's browser, and returns + /// the obtained code. + /// + /// Note: this method requires the `cli` feature. + #[cfg(feature = "cli")] + fn get_code_from_user(&self) -> ClientResult { + use crate::client::ClientError; + + let url = self.get_authorize_url(false)?; + + match webbrowser::open(&url) { + Ok(_) => println!("Opened {} in your browser.", url), + Err(why) => eprintln!( + "Error when trying to open an URL in your browser: {:?}. \ + Please navigate here manually: {}", + why, url + ), + } + + println!("Please enter the URL you were redirected to: "); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let code = self + .parse_response_code(&input) + .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; + + Ok(code) + } +} diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 1ccbf4d0..cf010f16 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -8,24 +8,11 @@ use crate::{ pub struct CodeAuthPKCESpotify { creds: Credentials, oauth: OAuth, + config: Config, tok: Option, http: HttpClient, } -impl CodeAuthPKCESpotify { - pub fn new(creds: Credentials, oauth: OAuth) -> Self { - CodeAuthPKCESpotify { - creds, - oauth, - ..Default::default() - } - } - - pub fn prompt_for_user_token(&mut self) { - todo!() - } -} - impl BaseClient for CodeAuthPKCESpotify { fn get_http(&self) -> &HttpClient { &self.http @@ -40,7 +27,7 @@ impl BaseClient for CodeAuthPKCESpotify { } fn get_config(&self) -> &Config { - todo!() + &self.config } } @@ -49,3 +36,22 @@ impl OAuthClient for CodeAuthPKCESpotify { &self.oauth } } + +impl CodeAuthPKCESpotify { + pub fn new(creds: Credentials, oauth: OAuth) -> Self { + CodeAuthPKCESpotify { + creds, + oauth, + ..Default::default() + } + } + + pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { + CodeAuthPKCESpotify { + creds, + oauth, + config, + ..Default::default() + } + } +} diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 12afdcfe..8b87a7b9 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -1152,9 +1152,9 @@ pub trait OAuthClient: BaseClient { /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) - async fn remove_users_saved_shows<'a, Shows: IntoIterator>( + async fn remove_users_saved_shows<'a>( &self, - show_ids: Shows, + show_ids: impl IntoIterator + Send + 'a, country: Option<&Market>, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); diff --git a/src/lib.rs b/src/lib.rs index 7ae246f6..1005e9d6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -171,6 +171,9 @@ pub use rspotify_http as http; pub use rspotify_macros as macros; pub use rspotify_model as model; // Top-level re-exports +pub use client_creds::ClientCredentialsSpotify; +pub use code_auth::CodeAuthSpotify; +pub use code_auth_pkce::CodeAuthPKCESpotify; pub use macros::scopes; use std::{ @@ -191,6 +194,28 @@ pub mod prelude { pub use crate::endpoints::{BaseClient, OAuthClient}; } +pub(in crate) mod headers { + // Common headers as constants + pub const CLIENT_ID: &str = "client_id"; + pub const CODE: &str = "code"; + pub const GRANT_AUTH_CODE: &str = "authorization_code"; + pub const GRANT_CLIENT_CREDS: &str = "client_credentials"; + pub const GRANT_REFRESH_TOKEN: &str = "refresh_token"; + pub const GRANT_TYPE: &str = "grant_type"; + pub const REDIRECT_URI: &str = "redirect_uri"; + pub const REFRESH_TOKEN: &str = "refresh_token"; + pub const RESPONSE_CODE: &str = "code"; + pub const RESPONSE_TYPE: &str = "response_type"; + pub const SCOPE: &str = "scope"; + pub const SHOW_DIALOG: &str = "show_dialog"; + pub const STATE: &str = "state"; +} + +pub(in crate) mod auth_urls { + pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize"; + pub const TOKEN: &str = "https://accounts.spotify.com/api/token"; +} + /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] pub enum ClientError { @@ -242,6 +267,12 @@ pub struct Config { /// Note that most endpoints set a maximum to the number of items per /// request, which most times is 50. pub pagination_chunks: u32, + + /// TODO + pub token_cached: bool, + + /// TODO + pub token_refreshing: bool, } impl Default for Config { @@ -250,6 +281,8 @@ impl Default for Config { prefix: String::from(DEFAULT_API_PREFIX), cache_path: PathBuf::from(DEFAULT_CACHE_PATH), pagination_chunks: DEFAULT_PAGINATION_CHUNKS, + token_cached: false, + token_refreshing: false, } } } @@ -267,11 +300,6 @@ pub(in crate) fn generate_random_string(length: usize) -> String { .collect() } -mod auth_urls { - pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize"; - pub const TOKEN: &str = "https://accounts.spotify.com/api/token"; -} - mod duration_second { use chrono::Duration; use serde::{de, Deserialize, Serializer}; @@ -445,12 +473,12 @@ impl OAuthBuilder { #[cfg(test)] mod test { - use super::*; + use super::ClientCredentialsSpotify; #[test] fn test_parse_response_code() { let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; - let spotify = SpotifyBuilder::default().build().unwrap(); + let spotify = ClientCredentialsSpotify::default(); let code = spotify.parse_response_code(url).unwrap(); assert_eq!(code, "AQD0yXvFEOvw"); } diff --git a/src/oauth2.rs b/src/oauth2.rs index 18ec6073..d33e7856 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -12,31 +12,6 @@ impl Spotify { Ok(()) } - /// Gets the required URL to authorize the current client to start the - /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). - pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { - let oauth = self.get_oauth()?; - let mut payload: HashMap<&str, &str> = HashMap::new(); - let scope = oauth - .scope - .clone() - .into_iter() - .collect::>() - .join(" "); - payload.insert(headers::CLIENT_ID, &self.get_creds()?.id); - payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); - payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); - payload.insert(headers::SCOPE, &scope); - payload.insert(headers::STATE, &oauth.state); - - if show_dialog { - payload.insert(headers::SHOW_DIALOG, "true"); - } - - let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; - Ok(parsed.into_string()) - } - /// Tries to read the cache file's token, which may not exist. #[maybe_async] pub async fn read_token_cache(&mut self) -> Option { @@ -68,93 +43,7 @@ impl Spotify { Ok(tok) } - /// Refreshes the access token with the refresh token provided by the - /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow), - /// without saving it into the cache file. - /// - /// The obtained token will be saved internally. - #[maybe_async] - pub async fn refresh_user_token_without_cache( - &mut self, - refresh_token: &str, - ) -> ClientResult<()> { - let mut data = Form::new(); - data.insert(headers::REFRESH_TOKEN, refresh_token); - data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - - let mut tok = self.fetch_access_token(&data).await?; - tok.refresh_token = Some(refresh_token.to_string()); - self.token = Some(tok); - - Ok(()) - } - - /// The same as `refresh_user_token_without_cache`, but saves the token - /// into the cache file if possible. - #[maybe_async] - pub async fn refresh_user_token(&mut self, refresh_token: &str) -> ClientResult<()> { - self.refresh_user_token_without_cache(refresh_token).await?; - - Ok(()) - } - - /// Obtains the client access token for the app without saving it into the - /// cache file. The resulting token is saved internally. - #[maybe_async] - pub async fn request_client_token_without_cache(&mut self) -> ClientResult<()> { - let mut data = Form::new(); - data.insert(headers::GRANT_TYPE, headers::GRANT_CLIENT_CREDS); - self.token = Some(self.fetch_access_token(&data).await?); - - Ok(()) - } - - /// The same as `request_client_token_without_cache`, but saves the token - /// into the cache file if possible. - #[maybe_async] - pub async fn request_client_token(&mut self) -> ClientResult<()> { - self.request_client_token_without_cache().await?; - self.write_token_cache() - } - - /// Parse the response code in the given response url. If the URL cannot be - /// parsed or the `code` parameter is not present, this will return `None`. - /// - /// Step 2 of the [Authorization Code Flow - /// ](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). - pub fn parse_response_code(&self, url: &str) -> Option { - let url = Url::parse(url).ok()?; - let mut params = url.query_pairs(); - let (_, url) = params.find(|(key, _)| key == "code")?; - Some(url.to_string()) - } - - /// Obtains the user access token for the app with the given code without - /// saving it into the cache file, as part of the OAuth authentication. - /// The access token will be saved inside the Spotify instance. - /// - /// Step 3 of the [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). - #[maybe_async] - pub async fn request_user_token_without_cache(&mut self, code: &str) -> ClientResult<()> { - let oauth = self.get_oauth()?; - let mut data = Form::new(); - let scopes = oauth - .scope - .clone() - .into_iter() - .collect::>() - .join(" "); - data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); - data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); - data.insert(headers::CODE, code); - data.insert(headers::SCOPE, scopes.as_ref()); - data.insert(headers::STATE, oauth.state.as_ref()); - - self.token = Some(self.fetch_access_token(&data).await?); - - Ok(()) - } /// The same as `request_user_token_without_cache`, but saves the token into /// the cache file if possible. @@ -200,34 +89,6 @@ impl Spotify { Ok(()) } - /// Tries to open the authorization URL in the user's browser, and returns - /// the obtained code. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - fn get_code_from_user(&self) -> ClientResult { - use crate::client::ClientError; - - let url = self.get_authorize_url(false)?; - - match webbrowser::open(&url) { - Ok(_) => println!("Opened {} in your browser.", url), - Err(why) => eprintln!( - "Error when trying to open an URL in your browser: {:?}. \ - Please navigate here manually: {}", - why, url - ), - } - - println!("Please enter the URL you were redirected to: "); - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let code = self - .parse_response_code(&input) - .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; - - Ok(code) - } } #[cfg(test)] From 1107624f380de0a8ef573f701e6b4aa44224ac15 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 15:01:29 +0200 Subject: [PATCH 08/56] some fixes --- examples/album.rs | 3 +- rspotify-http/src/lib.rs | 2 +- src/client_creds.rs | 4 +- src/code_auth.rs | 94 ++++++++---------------------- src/code_auth_pkce.rs | 32 ++++++++-- src/endpoints/base.rs | 52 ++++++++++------- src/endpoints/mod.rs | 3 + src/endpoints/oauth.rs | 122 +++++++++++++++++++++++++-------------- src/lib.rs | 2 + src/oauth2.rs | 17 ------ 10 files changed, 171 insertions(+), 160 deletions(-) diff --git a/examples/album.rs b/examples/album.rs index f2213185..49ebe6e9 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,6 +1,5 @@ -use rspotify::client::SpotifyBuilder; +use rspotify::{ClientCredentialsSpotify, Credentials}; use rspotify::model::Id; -use rspotify::oauth2::CredentialsBuilder; #[tokio::main] async fn main() { diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index a125b214..cdf750f2 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -72,7 +72,7 @@ pub type Result = std::result::Result; /// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make /// much sense). #[maybe_async] -pub trait BaseHttpClient: Default + Clone + fmt::Debug { +pub trait BaseHttpClient: Send + Default + Clone + fmt::Debug { // This internal function should always be given an object value in JSON. async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result; diff --git a/src/client_creds.rs b/src/client_creds.rs index dde1b536..a2701920 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,4 +1,6 @@ -use crate::{endpoints::BaseClient, http::HttpClient, Config, Credentials, Token}; +use crate::{endpoints::BaseClient, http::{HttpClient, Form}, Config, Credentials, Token, headers, ClientResult}; + +use maybe_async::maybe_async; #[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { diff --git a/src/code_auth.rs b/src/code_auth.rs index 5ed78bc5..35e95fdf 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,13 +1,14 @@ use crate::{ auth_urls, - endpoints::{BaseClient, OAuthClient}, + endpoints::{BaseClient, OAuthClient, basic_auth}, headers, - http::{Form, HttpClient}, + http::{Form, HttpClient, Headers}, ClientResult, Config, Credentials, OAuth, Token, }; use std::collections::HashMap; +use chrono::Utc; use maybe_async::maybe_async; use url::Url; @@ -63,14 +64,13 @@ impl CodeAuthSpotify { } } - /// Gets the required URL to authorize the current client to start the - /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). + /// Gets the required URL to authorize the current client to begin the + /// authorization flow. pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); let scope = oauth .scope - .clone() .into_iter() .collect::>() .join(" "); @@ -88,52 +88,14 @@ impl CodeAuthSpotify { Ok(parsed.into_string()) } - /// Refreshes the access token with the refresh token provided by the - /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow), - /// without saving it into the cache file. - /// - /// The obtained token will be saved internally. - #[maybe_async] - pub async fn refresh_user_token_without_cache( - &mut self, - refresh_token: &str, - ) -> ClientResult<()> { - let mut data = Form::new(); - data.insert(headers::REFRESH_TOKEN, refresh_token); - data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - - let mut tok = self.fetch_access_token(&data).await?; - tok.refresh_token = Some(refresh_token.to_string()); - self.token = Some(tok); - - Ok(()) - } - - /// The same as `refresh_user_token_without_cache`, but saves the token - /// into the cache file if possible. - #[maybe_async] - pub async fn refresh_user_token(&mut self, refresh_token: &str) -> ClientResult<()> { - self.refresh_user_token_without_cache(refresh_token).await?; - - Ok(()) - } - - /// Parse the response code in the given response url. If the URL cannot be - /// parsed or the `code` parameter is not present, this will return `None`. - pub fn parse_response_code(&self, url: &str) -> Option { - let url = Url::parse(url).ok()?; - let mut params = url.query_pairs(); - let (_, url) = params.find(|(key, _)| key == "code")?; - Some(url.to_string()) - } - /// Obtains the user access token for the app with the given code without /// saving it into the cache file, as part of the OAuth authentication. /// The access token will be saved inside the Spotify instance. + // TODO: implement with and without cache. #[maybe_async] - pub async fn request_user_token_without_cache(&mut self, code: &str) -> ClientResult<()> { - let oauth = self.get_oauth()?; + pub async fn request_token(&mut self, code: &str) -> ClientResult<()> { let mut data = Form::new(); + let oauth = self.get_oauth(); let scopes = oauth .scope .clone() @@ -151,32 +113,24 @@ impl CodeAuthSpotify { Ok(()) } - /// Tries to open the authorization URL in the user's browser, and returns - /// the obtained code. + /// Refreshes the access token with the refresh token provided by the + /// without saving it into the cache file. /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - fn get_code_from_user(&self) -> ClientResult { - use crate::client::ClientError; - - let url = self.get_authorize_url(false)?; - - match webbrowser::open(&url) { - Ok(_) => println!("Opened {} in your browser.", url), - Err(why) => eprintln!( - "Error when trying to open an URL in your browser: {:?}. \ - Please navigate here manually: {}", - why, url - ), - } + /// The obtained token will be saved internally. + // TODO: implement with and without cache + #[maybe_async] + pub async fn refresh_token( + &mut self, + refresh_token: &str, + ) -> ClientResult<()> { + let mut data = Form::new(); + data.insert(headers::REFRESH_TOKEN, refresh_token); + data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - println!("Please enter the URL you were redirected to: "); - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let code = self - .parse_response_code(&input) - .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; + let mut tok = self.fetch_access_token(&data).await?; + tok.refresh_token = Some(refresh_token.to_string()); + self.token = Some(tok); - Ok(code) + Ok(()) } } diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index cf010f16..0d515f88 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -1,8 +1,8 @@ -use crate::{ - endpoints::{BaseClient, OAuthClient}, - http::HttpClient, - Config, Credentials, OAuth, Token, -}; +use url::Url; + +use crate::{ClientResult, Config, Credentials, OAuth, Token, auth_urls, endpoints::{BaseClient, OAuthClient}, headers, http::HttpClient}; + +use std::collections::HashMap; #[derive(Clone, Debug, Default)] pub struct CodeAuthPKCESpotify { @@ -54,4 +54,26 @@ impl CodeAuthPKCESpotify { ..Default::default() } } + + /// Gets the required URL to authorize the current client to start the + /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). + pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { + let mut payload: HashMap<&str, &str> = HashMap::new(); + let oauth = self.get_oauth(); + let scope = oauth + .scope + .into_iter() + .collect::>() + .join(" "); + payload.insert(headers::CLIENT_ID, &self.get_creds().id); + payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); + payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); + payload.insert(headers::SCOPE, &scope); + payload.insert(headers::STATE, &oauth.state); + payload.insert(headers::CODE_CHALLENGE, todo!()); + payload.insert(headers::CODE_CHALLENGE_METHOD, "S256"); + + let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; + Ok(parsed.into_string()) + } } diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 626b2388..5910fd70 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,16 +1,11 @@ -use crate::{ - endpoints::{ - bearer_auth, convert_result, join_ids, +use crate::{ClientResult, Config, Credentials, Token, auth_urls, endpoints::{ + bearer_auth, convert_result, join_ids, basic_auth, pagination::{paginate, Paginator}, - }, - http::{BaseHttpClient, Form, Headers, HttpClient, Query}, - macros::build_map, - model::*, - ClientResult, Config, Credentials, Token, -}; + }, http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, model::*}; -use std::collections::HashMap; +use std::{collections::HashMap, fmt}; +use chrono::Utc; use maybe_async::maybe_async; use serde_json::{Map, Value}; @@ -28,8 +23,8 @@ use serde_json::{Map, Value}; /// requests to reduce the code needed for endpoints and make them as concise /// as possible. -#[maybe_async] -pub trait BaseClient { +#[maybe_async(?Send)] +pub trait BaseClient: where Self: Send + Sync + Default + Clone + fmt::Debug { fn get_config(&self) -> &Config; fn get_http(&self) -> &HttpClient; fn get_token(&self) -> Option<&Token>; @@ -136,6 +131,23 @@ pub trait BaseClient { self.delete(url, Some(&headers), payload).await } + /// Sends a request to Spotify for an access token. + #[maybe_async] + async fn fetch_access_token(&self, payload: &Form<'_>) -> ClientResult { + // This request uses a specific content type, and the client ID/secret + // as the authentication, since the access token isn't available yet. + let mut head = Headers::new(); + let (key, val) = basic_auth(&self.get_creds().id, &self.get_creds().secret); + head.insert(key, val); + + let response = self + .post_form(auth_urls::TOKEN, Some(&head), payload) + .await?; + let mut tok = serde_json::from_str::(&response)?; + tok.expires_at = Utc::now().checked_add_signed(tok.expires_in); + Ok(tok) + } + /// Returns a single track given the track's ID, URI or URL. /// /// Parameters: @@ -155,9 +167,9 @@ pub trait BaseClient { /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) - async fn tracks<'a, Tracks: IntoIterator>( + async fn tracks<'a>( &self, - track_ids: Tracks, + track_ids: impl IntoIterator, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(track_ids); @@ -455,9 +467,9 @@ pub trait BaseClient { /// - market(Optional) An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-shows) - async fn get_several_shows<'a, Shows: IntoIterator>( + async fn get_several_shows<'a>( &self, - ids: Shows, + ids: impl IntoIterator + 'a, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(ids); @@ -549,9 +561,9 @@ pub trait BaseClient { /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-multiple-episodes) - async fn get_several_episodes<'a, Eps: IntoIterator>( + async fn get_several_episodes<'a>( &self, - ids: Eps, + ids: impl IntoIterator + 'a, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(ids); @@ -582,9 +594,9 @@ pub trait BaseClient { /// - tracks a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-audio-features) - async fn tracks_features<'a, Tracks: IntoIterator>( + async fn tracks_features<'a>( &self, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, ) -> ClientResult>> { let url = format!("audio-features/?ids={}", join_ids(track_ids)); diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index f14d3b13..317f6a89 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -30,11 +30,13 @@ pub(in crate) fn append_device_id(path: &str, device_id: Option<&str>) -> String new_path } +// TODO: move to `lib.rs` #[inline] pub(in crate) fn join_ids<'a, T: 'a + IdType>(ids: impl IntoIterator>) -> String { ids.into_iter().collect::>().join(",") } +// TODO: move to `lib.rs` or integrate into Token. /// Generates an HTTP token authorization header with proper formatting pub fn bearer_auth(tok: &Token) -> (String, String) { let auth = "authorization".to_owned(); @@ -43,6 +45,7 @@ pub fn bearer_auth(tok: &Token) -> (String, String) { (auth, value) } +// TODO: move to `lib.rs` or integrate into Credentials. /// Generates an HTTP basic authorization header with proper formatting pub fn basic_auth(user: &str, password: &str) -> (String, String) { let auth = "authorization".to_owned(); diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 8b87a7b9..b63ebba8 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -9,18 +9,55 @@ use crate::{ http::Query, macros::{build_json, build_map}, model::*, - ClientResult, OAuth, + ClientResult, ClientError, OAuth, }; use log::error; use maybe_async::maybe_async; use rspotify_model::idtypes::PlayContextIdType; use serde_json::{json, Map}; +use url::Url; -#[maybe_async] +#[maybe_async(?Send)] pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; + /// Parse the response code in the given response url. If the URL cannot be + /// parsed or the `code` parameter is not present, this will return `None`. + fn parse_response_code(&self, url: &str) -> Option { + let url = Url::parse(url).ok()?; + let mut params = url.query_pairs(); + let (_, url) = params.find(|(key, _)| key == "code")?; + Some(url.to_string()) + } + + /// Tries to open the authorization URL in the user's browser, and returns + /// the obtained code. + /// + /// Note: this method requires the `cli` feature. + #[cfg(feature = "cli")] + fn get_code_from_user(&self) -> ClientResult { + let url = self.get_authorize_url(false)?; + + match webbrowser::open(&url) { + Ok(_) => println!("Opened {} in your browser.", url), + Err(why) => eprintln!( + "Error when trying to open an URL in your browser: {:?}. \ + Please navigate here manually: {}", + why, url + ), + } + + println!("Please enter the URL you were redirected to: "); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let code = self + .parse_response_code(&input) + .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; + + Ok(code) + } + /// Get current user playlists without required getting his profile. /// /// Parameters: @@ -159,10 +196,10 @@ pub trait OAuthClient: BaseClient { /// - position - the position to add the tracks /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-add-tracks-to-playlist) - async fn playlist_add_tracks<'a, Tracks: IntoIterator>( + async fn playlist_add_tracks<'a>( &self, playlist_id: &PlaylistId, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, position: Option, ) -> ClientResult { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); @@ -184,10 +221,10 @@ pub trait OAuthClient: BaseClient { /// - tracks - the list of track ids to add to the playlist /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - async fn playlist_replace_tracks<'a, Tracks: IntoIterator>( + async fn playlist_replace_tracks<'a>( &self, playlist_id: &PlaylistId, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let uris = track_ids.into_iter().map(|id| id.uri()).collect::>(); let params = build_json! { @@ -212,16 +249,16 @@ pub trait OAuthClient: BaseClient { /// - snapshot_id - optional playlist's snapshot ID /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-reorder-or-replace-playlists-tracks) - async fn playlist_reorder_tracks( + async fn playlist_reorder_tracks<'a, T: PlayableIdType + 'a>( &self, playlist_id: &PlaylistId, - uris: Option<&[&Id]>, + uris: Option> + 'a>, range_start: Option, insert_before: Option, range_length: Option, snapshot_id: Option<&str>, ) -> ClientResult { - let uris = uris.map(|u| u.iter().map(|id| id.uri()).collect::>()); + let uris = uris.map(|u| u.into_iter().map(|id| id.uri()).collect::>()); let params = build_json! { "playlist_id": playlist_id, optional "uris": uris, @@ -244,13 +281,10 @@ pub trait OAuthClient: BaseClient { /// - snapshot_id - optional id of the playlist snapshot /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-playlist) - async fn playlist_remove_all_occurrences_of_tracks< - 'a, - Tracks: IntoIterator, - >( + async fn playlist_remove_all_occurrences_of_tracks<'a>( &self, playlist_id: &PlaylistId, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, snapshot_id: Option<&str>, ) -> ClientResult { let tracks = track_ids @@ -512,9 +546,9 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-tracks-user) - async fn current_user_saved_tracks_delete<'a, Tracks: IntoIterator>( + async fn current_user_saved_tracks_delete<'a>( &self, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -529,9 +563,9 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-tracks) - async fn current_user_saved_tracks_contains<'a, Tracks: IntoIterator>( + async fn current_user_saved_tracks_contains<'a>( &self, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, ) -> ClientResult> { let url = format!("me/tracks/contains/?ids={}", join_ids(track_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; @@ -544,9 +578,9 @@ pub trait OAuthClient: BaseClient { /// - track_ids - a list of track URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-tracks-user) - async fn current_user_saved_tracks_add<'a, Tracks: IntoIterator>( + async fn current_user_saved_tracks_add<'a>( &self, - track_ids: Tracks, + track_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/tracks/?ids={}", join_ids(track_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -665,9 +699,9 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-albums-user) - async fn current_user_saved_albums_add<'a, Albums: IntoIterator>( + async fn current_user_saved_albums_add<'a>( &self, - album_ids: Albums, + album_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -681,9 +715,9 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-albums-user) - async fn current_user_saved_albums_delete<'a, Albums: IntoIterator>( + async fn current_user_saved_albums_delete<'a>( &self, - album_ids: Albums, + album_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/albums/?ids={}", join_ids(album_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -698,9 +732,9 @@ pub trait OAuthClient: BaseClient { /// - album_ids - a list of album URIs, URLs or IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-albums) - async fn current_user_saved_albums_contains<'a, Albums: IntoIterator>( + async fn current_user_saved_albums_contains<'a>( &self, - album_ids: Albums, + album_ids: impl IntoIterator + 'a, ) -> ClientResult> { let url = format!("me/albums/contains/?ids={}", join_ids(album_ids)); let result = self.endpoint_get(&url, &Query::new()).await?; @@ -713,9 +747,9 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - async fn user_follow_artists<'a, Artists: IntoIterator>( + async fn user_follow_artists<'a>( &self, - artist_ids: Artists, + artist_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -729,9 +763,9 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - async fn user_unfollow_artists<'a, Artists: IntoIterator>( + async fn user_unfollow_artists<'a>( &self, - artist_ids: Artists, + artist_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", join_ids(artist_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -746,9 +780,9 @@ pub trait OAuthClient: BaseClient { /// - artist_ids - the ids of the users that you want to /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-current-user-follows) - async fn user_artist_check_follow<'a, Artists: IntoIterator>( + async fn user_artist_check_follow<'a>( &self, - artist_ids: Artists, + artist_ids: impl IntoIterator + 'a, ) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", @@ -764,9 +798,9 @@ pub trait OAuthClient: BaseClient { /// - user_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-follow-artists-users) - async fn user_follow_users<'a, Users: IntoIterator>( + async fn user_follow_users<'a>( &self, - user_ids: Users, + user_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -780,9 +814,9 @@ pub trait OAuthClient: BaseClient { /// - user_ids - a list of artist IDs /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-unfollow-artists-users) - async fn user_unfollow_users<'a, Users: IntoIterator>( + async fn user_unfollow_users<'a>( &self, - user_ids: Users, + user_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", join_ids(user_ids)); self.endpoint_delete(&url, &json!({})).await?; @@ -923,15 +957,15 @@ pub trait OAuthClient: BaseClient { Ok(()) } - async fn start_uris_playback( + async fn start_uris_playback<'a, T: PlayableIdType + 'a>( &self, - uris: &[&Id], + uris: impl IntoIterator> + 'a, device_id: Option<&str>, offset: Option>, position_ms: Option, ) -> ClientResult<()> { let params = build_json! { - "uris": uris.iter().map(|id| id.uri()).collect::>(), + "uris": uris.into_iter().map(|id| id.uri()).collect::>(), optional "position_ms": position_ms, optional "offset": offset.map(|x| match x { Offset::Position(position) => json!({ "position": position }), @@ -1079,9 +1113,9 @@ pub trait OAuthClient: BaseClient { /// be added to the user’s library. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-save-shows-user) - async fn save_shows<'a, Shows: IntoIterator>( + async fn save_shows<'a>( &self, - show_ids: Shows, + show_ids: impl IntoIterator + 'a, ) -> ClientResult<()> { let url = format!("me/shows/?ids={}", join_ids(show_ids)); self.endpoint_put(&url, &json!({})).await?; @@ -1132,9 +1166,9 @@ pub trait OAuthClient: BaseClient { /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-users-saved-shows) - async fn check_users_saved_shows<'a, Shows: IntoIterator>( + async fn check_users_saved_shows<'a>( &self, - ids: Shows, + ids: impl IntoIterator + 'a, ) -> ClientResult> { let ids = join_ids(ids); let params = build_map! { @@ -1154,7 +1188,7 @@ pub trait OAuthClient: BaseClient { /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-remove-shows-user) async fn remove_users_saved_shows<'a>( &self, - show_ids: impl IntoIterator + Send + 'a, + show_ids: impl IntoIterator + 'a, country: Option<&Market>, ) -> ClientResult<()> { let url = format!("me/shows?ids={}", join_ids(show_ids)); diff --git a/src/lib.rs b/src/lib.rs index 1005e9d6..235f1049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -209,6 +209,8 @@ pub(in crate) mod headers { pub const SCOPE: &str = "scope"; pub const SHOW_DIALOG: &str = "show_dialog"; pub const STATE: &str = "state"; + pub const CODE_CHALLENGE: &str = "code_challenge"; + pub const CODE_CHALLENGE_METHOD: &str = "code_challenge_method"; } pub(in crate) mod auth_urls { diff --git a/src/oauth2.rs b/src/oauth2.rs index d33e7856..bb9b14da 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -26,23 +26,6 @@ impl Spotify { } } - /// Sends a request to Spotify for an access token. - #[maybe_async] - async fn fetch_access_token(&self, payload: &Form<'_>) -> ClientResult { - // This request uses a specific content type, and the client ID/secret - // as the authentication, since the access token isn't available yet. - let mut head = Headers::new(); - let (key, val) = headers::basic_auth(&self.get_creds()?.id, &self.get_creds()?.secret); - head.insert(key, val); - - let response = self - .post_form(auth_urls::TOKEN, Some(&head), payload) - .await?; - let mut tok = serde_json::from_str::(&response)?; - tok.expires_at = Utc::now().checked_add_signed(tok.expires_in); - Ok(tok) - } - /// The same as `request_user_token_without_cache`, but saves the token into From 02c4c0ce571da5184ad00333d632086289f46f0b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 15:12:48 +0200 Subject: [PATCH 09/56] format --- examples/album.rs | 2 +- src/client_creds.rs | 7 ++++++- src/code_auth.rs | 15 ++++----------- src/code_auth_pkce.rs | 14 ++++++++------ src/endpoints/base.rs | 18 ++++++++++++++---- src/endpoints/oauth.rs | 2 +- 6 files changed, 34 insertions(+), 24 deletions(-) diff --git a/examples/album.rs b/examples/album.rs index 49ebe6e9..c3766957 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,5 +1,5 @@ -use rspotify::{ClientCredentialsSpotify, Credentials}; use rspotify::model::Id; +use rspotify::{ClientCredentialsSpotify, Credentials}; #[tokio::main] async fn main() { diff --git a/src/client_creds.rs b/src/client_creds.rs index a2701920..99f2b09f 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,4 +1,9 @@ -use crate::{endpoints::BaseClient, http::{HttpClient, Form}, Config, Credentials, Token, headers, ClientResult}; +use crate::{ + endpoints::BaseClient, + headers, + http::{Form, HttpClient}, + ClientResult, Config, Credentials, Token, +}; use maybe_async::maybe_async; diff --git a/src/code_auth.rs b/src/code_auth.rs index 35e95fdf..6465863a 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,8 +1,8 @@ use crate::{ auth_urls, - endpoints::{BaseClient, OAuthClient, basic_auth}, + endpoints::{basic_auth, BaseClient, OAuthClient}, headers, - http::{Form, HttpClient, Headers}, + http::{Form, Headers, HttpClient}, ClientResult, Config, Credentials, OAuth, Token, }; @@ -69,11 +69,7 @@ impl CodeAuthSpotify { pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); - let scope = oauth - .scope - .into_iter() - .collect::>() - .join(" "); + let scope = oauth.scope.into_iter().collect::>().join(" "); payload.insert(headers::CLIENT_ID, &self.get_creds().id); payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); @@ -119,10 +115,7 @@ impl CodeAuthSpotify { /// The obtained token will be saved internally. // TODO: implement with and without cache #[maybe_async] - pub async fn refresh_token( - &mut self, - refresh_token: &str, - ) -> ClientResult<()> { + pub async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { let mut data = Form::new(); data.insert(headers::REFRESH_TOKEN, refresh_token); data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 0d515f88..dff48764 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -1,6 +1,12 @@ use url::Url; -use crate::{ClientResult, Config, Credentials, OAuth, Token, auth_urls, endpoints::{BaseClient, OAuthClient}, headers, http::HttpClient}; +use crate::{ + auth_urls, + endpoints::{BaseClient, OAuthClient}, + headers, + http::HttpClient, + ClientResult, Config, Credentials, OAuth, Token, +}; use std::collections::HashMap; @@ -60,11 +66,7 @@ impl CodeAuthPKCESpotify { pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); - let scope = oauth - .scope - .into_iter() - .collect::>() - .join(" "); + let scope = oauth.scope.into_iter().collect::>().join(" "); payload.insert(headers::CLIENT_ID, &self.get_creds().id); payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 4ab0baf8..cc3b7d37 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,7 +1,14 @@ -use crate::{ClientResult, Config, Credentials, Token, auth_urls, endpoints::{ - bearer_auth, convert_result, join_ids, basic_auth, +use crate::{ + auth_urls, + endpoints::{ + basic_auth, bearer_auth, convert_result, join_ids, pagination::{paginate, Paginator}, - }, http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, model::*}; + }, + http::{BaseHttpClient, Form, Headers, HttpClient, Query}, + macros::build_map, + model::*, + ClientResult, Config, Credentials, Token, +}; use std::{collections::HashMap, fmt}; @@ -24,7 +31,10 @@ use serde_json::{Map, Value}; /// as possible. #[maybe_async(?Send)] -pub trait BaseClient: where Self: Send + Sync + Default + Clone + fmt::Debug { +pub trait BaseClient +where + Self: Send + Sync + Default + Clone + fmt::Debug, +{ fn get_config(&self) -> &Config; fn get_http(&self) -> &HttpClient; fn get_token(&self) -> Option<&Token>; diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f8317a6b..33054cf4 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -9,7 +9,7 @@ use crate::{ http::Query, macros::{build_json, build_map}, model::*, - ClientResult, ClientError, OAuth, + ClientError, ClientResult, OAuth, }; use log::error; From 8b409040aab425e416ab4a89a0b429885d3795a0 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 15:23:11 +0200 Subject: [PATCH 10/56] now compiling! --- src/code_auth.rs | 12 ++++++++---- src/code_auth_pkce.rs | 13 +++++++++---- src/endpoints/base.rs | 37 ++++++++++++++++++++++++++++++------- src/endpoints/oauth.rs | 33 ++++++++++++++++++++++++--------- src/oauth2.rs | 22 ---------------------- 5 files changed, 71 insertions(+), 46 deletions(-) diff --git a/src/code_auth.rs b/src/code_auth.rs index 6465863a..df4ad6df 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -1,14 +1,13 @@ use crate::{ auth_urls, - endpoints::{basic_auth, BaseClient, OAuthClient}, + endpoints::{BaseClient, OAuthClient}, headers, - http::{Form, Headers, HttpClient}, + http::{Form, HttpClient}, ClientResult, Config, Credentials, OAuth, Token, }; use std::collections::HashMap; -use chrono::Utc; use maybe_async::maybe_async; use url::Url; @@ -69,7 +68,12 @@ impl CodeAuthSpotify { pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); - let scope = oauth.scope.into_iter().collect::>().join(" "); + let scope = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); payload.insert(headers::CLIENT_ID, &self.get_creds().id); payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index dff48764..380c2a85 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -61,12 +61,17 @@ impl CodeAuthPKCESpotify { } } - /// Gets the required URL to authorize the current client to start the - /// [Authorization Code Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow). - pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { + /// Gets the required URL to authorize the current client to begin the + /// authorization flow. + pub fn get_authorize_url(&self) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); - let scope = oauth.scope.into_iter().collect::>().join(" "); + let scope = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); payload.insert(headers::CLIENT_ID, &self.get_creds().id); payload.insert(headers::RESPONSE_TYPE, headers::RESPONSE_CODE); payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index cc3b7d37..4fc3545b 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -7,7 +7,7 @@ use crate::{ http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, model::*, - ClientResult, Config, Credentials, Token, + ClientResult, Config, Credentials, Token, TokenBuilder, }; use std::{collections::HashMap, fmt}; @@ -141,8 +141,31 @@ where self.delete(url, Some(&headers), payload).await } + /// Updates the cache file at the internal cache path. + fn write_token_cache(&self) -> ClientResult<()> { + if let Some(tok) = self.get_token().as_ref() { + tok.write_cache(&self.get_config().cache_path)?; + } + + Ok(()) + } + + /// Tries to read the cache file's token, which may not exist. + async fn read_token_cache(&mut self) -> Option { + let tok = TokenBuilder::from_cache(&self.get_config().cache_path) + .build() + .ok()?; + + if tok.is_expired() { + // Invalid token, since it doesn't have at least the currently + // required scopes or it's expired. + None + } else { + Some(tok) + } + } + /// Sends a request to Spotify for an access token. - #[maybe_async] async fn fetch_access_token(&self, payload: &Form<'_>) -> ClientResult { // This request uses a specific content type, and the client ID/secret // as the authentication, since the access token isn't available yet. @@ -179,7 +202,7 @@ where /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-several-tracks) async fn tracks<'a>( &self, - track_ids: impl IntoIterator, + track_ids: impl IntoIterator + 'a, market: Option<&Market>, ) -> ClientResult> { let ids = join_ids(track_ids); @@ -827,9 +850,9 @@ where async fn recommendations<'a>( &self, payload: &Map, - seed_artists: Option>, - seed_genres: Option>, - seed_tracks: Option>, + seed_artists: Option + 'a>, + seed_genres: Option + 'a>, + seed_tracks: Option + 'a>, market: Option<&Market>, limit: Option, ) -> ClientResult { @@ -885,7 +908,7 @@ where } let result = self.endpoint_get("recommendations", ¶ms).await?; - self.convert_result(&result) + convert_result(&result) } /// Get full details of the tracks of a playlist owned by a user. diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 33054cf4..f7eb2be2 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -9,7 +9,7 @@ use crate::{ http::Query, macros::{build_json, build_map}, model::*, - ClientError, ClientResult, OAuth, + ClientResult, OAuth, Token, TokenBuilder, }; use log::error; @@ -22,6 +22,21 @@ use url::Url; pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; + /// Tries to read the cache file's token, which may not exist. + async fn read_token_cache(&mut self) -> Option { + let tok = TokenBuilder::from_cache(&self.get_config().cache_path) + .build() + .ok()?; + + if !self.get_oauth().scope.is_subset(&tok.scope) || tok.is_expired() { + // Invalid token, since it doesn't have at least the currently + // required scopes or it's expired. + None + } else { + Some(tok) + } + } + /// Parse the response code in the given response url. If the URL cannot be /// parsed or the `code` parameter is not present, this will return `None`. fn parse_response_code(&self, url: &str) -> Option { @@ -337,7 +352,7 @@ pub trait OAuthClient: BaseClient { async fn playlist_remove_specific_occurrences_of_tracks<'a>( &self, playlist_id: &PlaylistId, - tracks: impl IntoIterator>, + tracks: impl IntoIterator> + 'a, snapshot_id: Option<&str>, ) -> ClientResult { let tracks = tracks @@ -845,7 +860,7 @@ pub trait OAuthClient: BaseClient { async fn current_playback<'a>( &self, country: Option<&Market>, - additional_types: Option>, + additional_types: Option + 'a>, ) -> ClientResult> { let additional_types = additional_types.map(|x| { x.into_iter() @@ -862,7 +877,7 @@ pub trait OAuthClient: BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result(&result) + convert_result(&result) } } @@ -878,7 +893,7 @@ pub trait OAuthClient: BaseClient { async fn current_playing<'a>( &self, market: Option<&'a Market>, - additional_types: Option>, + additional_types: Option + 'a>, ) -> ClientResult> { let additional_types = additional_types.map(|x| { x.into_iter() @@ -897,7 +912,7 @@ pub trait OAuthClient: BaseClient { if result.is_empty() { Ok(None) } else { - self.convert_result(&result) + convert_result(&result) } } @@ -961,7 +976,7 @@ pub trait OAuthClient: BaseClient { async fn start_uris_playback<'a, T: PlayableIdType + 'a>( &self, - uris: impl IntoIterator>, + uris: impl IntoIterator> + 'a, device_id: Option<&str>, offset: Option>, position_ms: Option, @@ -975,7 +990,7 @@ pub trait OAuthClient: BaseClient { }), }; - let url = self.append_device_id("me/player/play", device_id); + let url = append_device_id("me/player/play", device_id); self.endpoint_put(&url, ¶ms).await?; Ok(()) @@ -1045,7 +1060,7 @@ pub trait OAuthClient: BaseClient { /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-set-repeat-mode-on-users-playback) async fn repeat(&self, state: &RepeatState, device_id: Option<&str>) -> ClientResult<()> { - let url = self.append_device_id( + let url = append_device_id( &format!("me/player/repeat?state={}", state.as_ref()), device_id, ); diff --git a/src/oauth2.rs b/src/oauth2.rs index bb9b14da..08e9dae1 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -3,28 +3,6 @@ /// Authorization-related methods for the client. impl Spotify { - /// Updates the cache file at the internal cache path. - pub fn write_token_cache(&self) -> ClientResult<()> { - if let Some(tok) = self.token.as_ref() { - tok.write_cache(&self.cache_path)?; - } - - Ok(()) - } - - /// Tries to read the cache file's token, which may not exist. - #[maybe_async] - pub async fn read_token_cache(&mut self) -> Option { - let tok = TokenBuilder::from_cache(&self.cache_path).build().ok()?; - - if !self.get_oauth().ok()?.scope.is_subset(&tok.scope) || tok.is_expired() { - // Invalid token, since it doesn't have at least the currently - // required scopes or it's expired. - None - } else { - Some(tok) - } - } From 7ece549de4f52d05c616beaff3ce0f3395993642 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 15:29:20 +0200 Subject: [PATCH 11/56] fixed album example --- examples/album.rs | 9 +++------ src/client_creds.rs | 11 ++--------- src/code_auth.rs | 29 +++++++++++++++++++++++++++++ src/endpoints/oauth.rs | 27 --------------------------- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/examples/album.rs b/examples/album.rs index c3766957..dfea922d 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,5 +1,5 @@ use rspotify::model::Id; -use rspotify::{ClientCredentialsSpotify, Credentials}; +use rspotify::{ClientCredentialsSpotify, CredentialsBuilder, prelude::*}; #[tokio::main] async fn main() { @@ -23,15 +23,12 @@ async fn main() { // .unwrap(); let creds = CredentialsBuilder::from_env().build().unwrap(); - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .build() - .unwrap(); + let mut spotify = ClientCredentialsSpotify::new(creds); // Obtaining the access token. Requires to be mutable because the internal // token will be modified. We don't need OAuth for this specific endpoint, // so `...` is used instead of `prompt_for_user_token`. - spotify.request_client_token().await.unwrap(); + spotify.request_token().await.unwrap(); // Running the requests let birdy_uri = Id::from_uri("spotify:album:0sNOF9WDwhWunNAHPD3Baj").unwrap(); diff --git a/src/client_creds.rs b/src/client_creds.rs index 99f2b09f..8da1ec66 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -52,8 +52,9 @@ impl ClientCredentialsSpotify { /// Obtains the client access token for the app without saving it into the /// cache file. The resulting token is saved internally. + // TODO: handle with and without cache #[maybe_async] - pub async fn request_client_token_without_cache(&mut self) -> ClientResult<()> { + pub async fn request_token(&mut self) -> ClientResult<()> { let mut data = Form::new(); data.insert(headers::GRANT_TYPE, headers::GRANT_CLIENT_CREDS); @@ -61,12 +62,4 @@ impl ClientCredentialsSpotify { Ok(()) } - - /// The same as `request_client_token_without_cache`, but saves the token - /// into the cache file if possible. - #[maybe_async] - pub async fn request_client_token(&mut self) -> ClientResult<()> { - self.request_client_token_without_cache().await?; - self.write_token_cache() - } } diff --git a/src/code_auth.rs b/src/code_auth.rs index df4ad6df..6ad91a57 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -130,4 +130,33 @@ impl CodeAuthSpotify { Ok(()) } + + /// Tries to open the authorization URL in the user's browser, and returns + /// the obtained code. + /// + /// Note: this method requires the `cli` feature. + #[cfg(feature = "cli")] + fn get_code_from_user(&self) -> ClientResult { + use crate::ClientError; + + let url = self.get_authorize_url(false)?; + + match webbrowser::open(&url) { + Ok(_) => println!("Opened {} in your browser.", url), + Err(why) => eprintln!( + "Error when trying to open an URL in your browser: {:?}. \ + Please navigate here manually: {}", + why, url + ), + } + + println!("Please enter the URL you were redirected to: "); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let code = self + .parse_response_code(&input) + .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; + + Ok(code) + } } diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f7eb2be2..ef20e540 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -46,33 +46,6 @@ pub trait OAuthClient: BaseClient { Some(url.to_string()) } - /// Tries to open the authorization URL in the user's browser, and returns - /// the obtained code. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - fn get_code_from_user(&self) -> ClientResult { - let url = self.get_authorize_url(false)?; - - match webbrowser::open(&url) { - Ok(_) => println!("Opened {} in your browser.", url), - Err(why) => eprintln!( - "Error when trying to open an URL in your browser: {:?}. \ - Please navigate here manually: {}", - why, url - ), - } - - println!("Please enter the URL you were redirected to: "); - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let code = self - .parse_response_code(&input) - .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; - - Ok(code) - } - /// Get current user playlists without required getting his profile. /// /// Parameters: From ab390892629863973338e992cfb74b29c2414e16 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 15:54:49 +0200 Subject: [PATCH 12/56] some fixes to the examples --- Cargo.toml | 1 - examples/album.rs | 16 ++--- examples/current_user_recently_played.rs | 31 ++++----- src/client_creds.rs | 4 ++ src/code_auth.rs | 6 +- src/code_auth_pkce.rs | 4 ++ src/endpoints/base.rs | 9 ++- src/endpoints/oauth.rs | 29 ++++++-- src/lib.rs | 87 ++++++++++++------------ src/oauth2.rs | 2 +- 10 files changed, 106 insertions(+), 83 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 89a3a3a4..d0a17c24 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,6 @@ async-stream = { version = "0.3.0", optional = true } async-trait = { version = "0.1.48", optional = true } base64 = "0.13.0" chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } -derive_builder = "0.10.0" dotenv = { version = "0.15.0", optional = true } futures = { version = "0.3.8", optional = true } getrandom = "0.2.0" diff --git a/examples/album.rs b/examples/album.rs index dfea922d..b59a1005 100644 --- a/examples/album.rs +++ b/examples/album.rs @@ -1,5 +1,4 @@ -use rspotify::model::Id; -use rspotify::{ClientCredentialsSpotify, CredentialsBuilder, prelude::*}; +use rspotify::{model::Id, prelude::*, ClientCredentialsSpotify, Credentials}; #[tokio::main] async fn main() { @@ -16,12 +15,13 @@ async fn main() { // // Otherwise, set client_id and client_secret explictly: // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + // ``` + // let creds = Credentials { + // client_id: "this-is-my-client-id".to_string(), + // client_secret: "this-is-my-client-secret".to_string() + // }; + // ``` + let creds = Credentials::from_env().unwrap(); let mut spotify = ClientCredentialsSpotify::new(creds); diff --git a/examples/current_user_recently_played.rs b/examples/current_user_recently_played.rs index d68b0eb1..c08c16b0 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/current_user_recently_played.rs @@ -1,6 +1,4 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -17,12 +15,13 @@ async fn main() { // // Otherwise, set client_id and client_secret explictly: // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + // ``` + // let creds = Credentials { + // client_id: "this-is-my-client-id".to_string(), + // client_secret: "this-is-my-client-secret".to_string() + // }; + // ``` + let creds = Credentials::from_env().unwrap(); // Or set the redirect_uri explictly: // @@ -30,19 +29,13 @@ async fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-read-recently-played")) - .build() - .unwrap(); + let mut oauth = OAuth::from_env().unwrap(); + oauth.scope = scopes!("user-read-recently-played"); - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().await.unwrap(); + spotify.prompt_for_token().await.unwrap(); // Running the requests let history = spotify.current_user_recently_played(Some(10)).await; diff --git a/src/client_creds.rs b/src/client_creds.rs index 8da1ec66..88652f24 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -25,6 +25,10 @@ impl BaseClient for ClientCredentialsSpotify { self.token.as_ref() } + fn get_token_mut(&mut self) -> Option<&mut Token> { + self.token.as_mut() + } + fn get_creds(&self) -> &Credentials { &self.creds } diff --git a/src/code_auth.rs b/src/code_auth.rs index 6ad91a57..aabbb6e1 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -29,6 +29,10 @@ impl BaseClient for CodeAuthSpotify { self.token.as_ref() } + fn get_token_mut(&mut self) -> Option<&mut Token> { + self.token.as_mut() + } + fn get_creds(&self) -> &Credentials { &self.creds } @@ -136,7 +140,7 @@ impl CodeAuthSpotify { /// /// Note: this method requires the `cli` feature. #[cfg(feature = "cli")] - fn get_code_from_user(&self) -> ClientResult { + pub fn get_code_from_user(&self) -> ClientResult { use crate::ClientError; let url = self.get_authorize_url(false)?; diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 380c2a85..39ae3081 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -28,6 +28,10 @@ impl BaseClient for CodeAuthPKCESpotify { self.tok.as_ref() } + fn get_token_mut(&mut self) -> Option<&mut Token> { + self.tok.as_mut() + } + fn get_creds(&self) -> &Credentials { &self.creds } diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 4fc3545b..ab3fbf8d 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -7,7 +7,7 @@ use crate::{ http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, model::*, - ClientResult, Config, Credentials, Token, TokenBuilder, + ClientResult, Config, Credentials, Token, }; use std::{collections::HashMap, fmt}; @@ -33,11 +33,12 @@ use serde_json::{Map, Value}; #[maybe_async(?Send)] pub trait BaseClient where - Self: Send + Sync + Default + Clone + fmt::Debug, + Self: Default + Clone + fmt::Debug, { fn get_config(&self) -> &Config; fn get_http(&self) -> &HttpClient; fn get_token(&self) -> Option<&Token>; + fn get_token_mut(&mut self) -> Option<&mut Token>; fn get_creds(&self) -> &Credentials; /// If it's a relative URL like "me", the prefix is appended to it. @@ -152,9 +153,7 @@ where /// Tries to read the cache file's token, which may not exist. async fn read_token_cache(&mut self) -> Option { - let tok = TokenBuilder::from_cache(&self.get_config().cache_path) - .build() - .ok()?; + let tok = Token::from_cache(&self.get_config().cache_path)?; if tok.is_expired() { // Invalid token, since it doesn't have at least the currently diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index ef20e540..4df9f0a5 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -9,7 +9,7 @@ use crate::{ http::Query, macros::{build_json, build_map}, model::*, - ClientResult, OAuth, Token, TokenBuilder, + ClientResult, OAuth, Token, }; use log::error; @@ -24,9 +24,7 @@ pub trait OAuthClient: BaseClient { /// Tries to read the cache file's token, which may not exist. async fn read_token_cache(&mut self) -> Option { - let tok = TokenBuilder::from_cache(&self.get_config().cache_path) - .build() - .ok()?; + let tok = Token::from_cache(&self.get_config().cache_path)?; if !self.get_oauth().scope.is_subset(&tok.scope) || tok.is_expired() { // Invalid token, since it doesn't have at least the currently @@ -37,6 +35,29 @@ pub trait OAuthClient: BaseClient { } } + /// The same as the `prompt_for_user_token_without_cache` method, but it + /// will try to use the user token into the cache file, and save it in + /// case it didn't exist/was invalid. + /// + /// Note: this method requires the `cli` feature. + // TODO: handle with and without cache + #[cfg(feature = "cli")] + #[maybe_async] + async fn prompt_for_token(&mut self) -> ClientResult<()> { + // TODO: shouldn't this also refresh the obtained token? + let mut token = self.get_token_mut(); + token = self.read_token_cache().await; + + // Otherwise following the usual procedure to get the token. + if self.get_token().is_none() { + let code = self.get_code_from_user()?; + // Will write to the cache file if successful + self.request_user_token(&code).await?; + } + + Ok(()) + } + /// Parse the response code in the given response url. If the URL cannot be /// parsed or the `code` parameter is not present, this will return `None`. fn parse_response_code(&self, url: &str) -> Option { diff --git a/src/lib.rs b/src/lib.rs index 235f1049..089a98d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -185,7 +185,6 @@ use std::{ }; use chrono::{DateTime, Duration, Utc}; -use derive_builder::Builder; use getrandom::getrandom; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -347,54 +346,49 @@ mod space_separated_scope { /// Spotify access token information /// [Reference](https://developer.spotify.com/documentation/general/guides/authorization-guide/) -#[derive(Builder, Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Token { /// An access token that can be provided in subsequent calls - #[builder(setter(into))] pub access_token: String, /// The time period for which the access token is valid. - #[builder(default = "Duration::seconds(0)")] #[serde(with = "duration_second")] pub expires_in: Duration, /// The valid time for which the access token is available represented /// in ISO 8601 combined date and time. - #[builder(setter(strip_option), default = "Some(Utc::now())")] pub expires_at: Option>, /// A token that can be sent to the Spotify Accounts service /// in place of an authorization code - #[builder(setter(into, strip_option), default)] pub refresh_token: Option, /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) /// which have been granted for this `access_token` /// You could use macro [scopes!](crate::scopes) to build it at compile time easily - #[builder(default)] #[serde(default, with = "space_separated_scope")] pub scope: HashSet, } -impl TokenBuilder { - /// Tries to initialize the token from a cache file. - pub fn from_cache>(path: T) -> Self { - if let Ok(mut file) = fs::File::open(path) { - let mut tok_str = String::new(); - if file.read_to_string(&mut tok_str).is_ok() { - if let Ok(tok) = serde_json::from_str::(&tok_str) { - return TokenBuilder { - access_token: Some(tok.access_token), - expires_in: Some(tok.expires_in), - expires_at: Some(tok.expires_at), - refresh_token: Some(tok.refresh_token), - scope: Some(tok.scope), - }; - } - } +impl Default for Token { + fn default() -> Self { + Token { + access_token: String::new(), + expires_in: Duration::seconds(0), + expires_at: Some(Utc::now()), + refresh_token: None, + scope: HashSet::new(), } - - TokenBuilder::default() } } impl Token { + /// Tries to initialize the token from a cache file. + // TODO: maybe ClientResult for these things instead? + pub fn from_cache>(path: T) -> Option { + let mut file = fs::File::open(path).ok()?; + let mut tok_str = String::new(); + file.read_to_string(&mut tok_str).ok()?; + + serde_json::from_str::(&tok_str).ok() + } + /// Saves the token information into its cache file. pub fn write_cache>(&self, path: T) -> ClientResult<()> { let token_info = serde_json::to_string(&self)?; @@ -414,62 +408,67 @@ impl Token { } /// Simple client credentials object for Spotify. -#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub struct Credentials { - #[builder(setter(into))] pub id: String, - #[builder(setter(into))] pub secret: String, } -impl CredentialsBuilder { +impl Credentials { /// Parses the credentials from the environment variables /// `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET`. You can optionally /// activate the `env-file` feature in order to read these variables from /// a `.env` file. - pub fn from_env() -> Self { + pub fn from_env() -> Option { #[cfg(feature = "env-file")] { dotenv::dotenv().ok(); } - CredentialsBuilder { - id: env::var("RSPOTIFY_CLIENT_ID").ok(), - secret: env::var("RSPOTIFY_CLIENT_SECRET").ok(), - } + Some(Credentials { + id: env::var("RSPOTIFY_CLIENT_ID").ok()?, + secret: env::var("RSPOTIFY_CLIENT_SECRET").ok()?, + }) } } /// Structure that holds the required information for requests with OAuth. -#[derive(Builder, Debug, Default, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct OAuth { - #[builder(setter(into))] pub redirect_uri: String, /// The state is generated by default, as suggested by the OAuth2 spec: /// [Cross-Site Request Forgery](https://tools.ietf.org/html/rfc6749#section-10.12) - #[builder(setter(into), default = "generate_random_string(16)")] pub state: String, /// You could use macro [scopes!](crate::scopes) to build it at compile time easily - #[builder(default)] pub scope: HashSet, - #[builder(setter(into, strip_option), default)] pub proxies: Option, } -impl OAuthBuilder { +impl Default for OAuth { + fn default() -> Self { + OAuth { + redirect_uri: String::new(), + state: generate_random_string(16), + scope: HashSet::new(), + proxies: None, + } + } +} + +impl OAuth { /// Parses the credentials from the environment variable /// `RSPOTIFY_REDIRECT_URI`. You can optionally activate the `env-file` /// feature in order to read these variables from a `.env` file. - pub fn from_env() -> Self { + pub fn from_env() -> Option { #[cfg(feature = "env-file")] { dotenv::dotenv().ok(); } - OAuthBuilder { - redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok(), + Some(OAuth { + redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok()?, ..Default::default() - } + }) } } diff --git a/src/oauth2.rs b/src/oauth2.rs index 08e9dae1..ec51b3f4 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -36,7 +36,7 @@ impl Spotify { /// Note: this method requires the `cli` feature. #[cfg(feature = "cli")] #[maybe_async] - pub async fn prompt_for_user_token(&mut self) -> ClientResult<()> { + pub async fn prompt_for_token(&mut self) -> ClientResult<()> { // TODO: shouldn't this also refresh the obtained token? self.token = self.read_token_cache().await; From 2c63e4c37ef86ffb74d620bef82e7a9e837d973a Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 22:26:45 +0200 Subject: [PATCH 13/56] remove old oauth file --- src/endpoints/oauth.rs | 2 +- src/lib.rs | 11 +++++++ src/oauth2.rs | 68 ------------------------------------------ 3 files changed, 12 insertions(+), 69 deletions(-) delete mode 100644 src/oauth2.rs diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 4df9f0a5..be443cb2 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -46,7 +46,7 @@ pub trait OAuthClient: BaseClient { async fn prompt_for_token(&mut self) -> ClientResult<()> { // TODO: shouldn't this also refresh the obtained token? let mut token = self.get_token_mut(); - token = self.read_token_cache().await; + token = OAuthClient::read_token_cache(&mut self).await; // Otherwise following the usual procedure to get the token. if self.get_token().is_none() { diff --git a/src/lib.rs b/src/lib.rs index 089a98d0..815173ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -474,7 +474,9 @@ impl OAuth { #[cfg(test)] mod test { + use super::generate_random_string; use super::ClientCredentialsSpotify; + use std::collections::HashSet; #[test] fn test_parse_response_code() { @@ -504,4 +506,13 @@ mod test { "me/player/shuffle?state=true&device_id=fdafdsadfa" ); } + + #[test] + fn test_generate_random_string() { + let mut containers = HashSet::new(); + for _ in 1..101 { + containers.insert(generate_random_string(10)); + } + assert_eq!(containers.len(), 100); + } } diff --git a/src/oauth2.rs b/src/oauth2.rs deleted file mode 100644 index ec51b3f4..00000000 --- a/src/oauth2.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! User authorization and client credentials management. - - -/// Authorization-related methods for the client. -impl Spotify { - - - - /// The same as `request_user_token_without_cache`, but saves the token into - /// the cache file if possible. - #[maybe_async] - pub async fn request_user_token(&mut self, code: &str) -> ClientResult<()> { - self.request_user_token_without_cache(code).await?; - self.write_token_cache() - } - - /// Opens up the authorization URL in the user's browser so that it can - /// authenticate. It also reads from the standard input the redirect URI - /// in order to obtain the access token information. The resulting access - /// token will be saved internally once the operation is successful. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - #[maybe_async] - pub async fn prompt_for_user_token_without_cache(&mut self) -> ClientResult<()> { - let code = self.get_code_from_user()?; - self.request_user_token_without_cache(&code).await?; - - Ok(()) - } - - /// The same as the `prompt_for_user_token_without_cache` method, but it - /// will try to use the user token into the cache file, and save it in - /// case it didn't exist/was invalid. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - #[maybe_async] - pub async fn prompt_for_token(&mut self) -> ClientResult<()> { - // TODO: shouldn't this also refresh the obtained token? - self.token = self.read_token_cache().await; - - // Otherwise following the usual procedure to get the token. - if self.token.is_none() { - let code = self.get_code_from_user()?; - // Will write to the cache file if successful - self.request_user_token(&code).await?; - } - - Ok(()) - } - -} - -#[cfg(test)] -mod test { - use super::generate_random_string; - use std::collections::HashSet; - - #[test] - fn test_generate_random_string() { - let mut containers = HashSet::new(); - for _ in 1..101 { - containers.insert(generate_random_string(10)); - } - assert_eq!(containers.len(), 100); - } -} From 6d152a640d0e7ecc1bbe420ebe12fe9542fc6052 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 22:48:48 +0200 Subject: [PATCH 14/56] fix second example --- examples/current_user_recently_played.rs | 3 +- src/code_auth.rs | 41 +++++++++++------------- src/endpoints/oauth.rs | 38 ++++++++++++---------- 3 files changed, 42 insertions(+), 40 deletions(-) diff --git a/examples/current_user_recently_played.rs b/examples/current_user_recently_played.rs index c08c16b0..85de614c 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/current_user_recently_played.rs @@ -35,7 +35,8 @@ async fn main() { let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_token().await.unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).await.unwrap(); // Running the requests let history = spotify.current_user_recently_played(Some(10)).await; diff --git a/src/code_auth.rs b/src/code_auth.rs index aabbb6e1..1e1635a0 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -135,32 +135,29 @@ impl CodeAuthSpotify { Ok(()) } - /// Tries to open the authorization URL in the user's browser, and returns - /// the obtained code. + /// The same as the `prompt_for_user_token_without_cache` method, but it + /// will try to use the user token into the cache file, and save it in + /// case it didn't exist/was invalid. /// /// Note: this method requires the `cli` feature. + // TODO: handle with and without cache #[cfg(feature = "cli")] - pub fn get_code_from_user(&self) -> ClientResult { - use crate::ClientError; - - let url = self.get_authorize_url(false)?; - - match webbrowser::open(&url) { - Ok(_) => println!("Opened {} in your browser.", url), - Err(why) => eprintln!( - "Error when trying to open an URL in your browser: {:?}. \ - Please navigate here manually: {}", - why, url - ), + #[maybe_async] + pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { + match self.read_oauth_token_cache().await { + // TODO: shouldn't this also refresh the obtained token? + Some(mut new_token) => { + let mut cur_token = self.get_token_mut(); + cur_token.replace(&mut new_token); + } + // Otherwise following the usual procedure to get the token. + None => { + let code = self.get_code_from_user(url)?; + // Will write to the cache file if successful + self.request_token(&code).await?; + } } - println!("Please enter the URL you were redirected to: "); - let mut input = String::new(); - std::io::stdin().read_line(&mut input)?; - let code = self - .parse_response_code(&input) - .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; - - Ok(code) + Ok(()) } } diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index be443cb2..e3df011a 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -23,7 +23,7 @@ pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; /// Tries to read the cache file's token, which may not exist. - async fn read_token_cache(&mut self) -> Option { + async fn read_oauth_token_cache(&mut self) -> Option { let tok = Token::from_cache(&self.get_config().cache_path)?; if !self.get_oauth().scope.is_subset(&tok.scope) || tok.is_expired() { @@ -35,27 +35,31 @@ pub trait OAuthClient: BaseClient { } } - /// The same as the `prompt_for_user_token_without_cache` method, but it - /// will try to use the user token into the cache file, and save it in - /// case it didn't exist/was invalid. + /// Tries to open the authorization URL in the user's browser, and returns + /// the obtained code. /// /// Note: this method requires the `cli` feature. - // TODO: handle with and without cache #[cfg(feature = "cli")] - #[maybe_async] - async fn prompt_for_token(&mut self) -> ClientResult<()> { - // TODO: shouldn't this also refresh the obtained token? - let mut token = self.get_token_mut(); - token = OAuthClient::read_token_cache(&mut self).await; - - // Otherwise following the usual procedure to get the token. - if self.get_token().is_none() { - let code = self.get_code_from_user()?; - // Will write to the cache file if successful - self.request_user_token(&code).await?; + fn get_code_from_user(&self, url: &str) -> ClientResult { + use crate::ClientError; + + match webbrowser::open(&url) { + Ok(_) => println!("Opened {} in your browser.", url), + Err(why) => eprintln!( + "Error when trying to open an URL in your browser: {:?}. \ + Please navigate here manually: {}", + why, url + ), } - Ok(()) + println!("Please enter the URL you were redirected to: "); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let code = self + .parse_response_code(&input) + .ok_or_else(|| ClientError::Cli("unable to parse the response code".to_string()))?; + + Ok(code) } /// Parse the response code in the given response url. If the URL cannot be From 01e581fa366cd6d7bdc20e74acb30cab3e349ba1 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 23:24:12 +0200 Subject: [PATCH 15/56] tidy up examples, fix current_playing --- Cargo.toml | 21 +++---- examples/{album.rs => client_credentials.rs} | 4 +- examples/code_auth.rs | 57 +++++++++++++++++++ ...r_recently_played.rs => code_auth_pkce.rs} | 23 ++++---- examples/pagination_async.rs | 43 +++----------- examples/track.rs | 42 -------------- examples/tracks.rs | 42 -------------- src/endpoints/oauth.rs | 2 +- 8 files changed, 88 insertions(+), 146 deletions(-) rename examples/{album.rs => client_credentials.rs} (90%) create mode 100644 examples/code_auth.rs rename examples/{current_user_recently_played.rs => code_auth_pkce.rs} (60%) delete mode 100644 examples/track.rs delete mode 100644 examples/tracks.rs diff --git a/Cargo.toml b/Cargo.toml index d0a17c24..0aa7f42b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,29 +85,24 @@ __sync = [] features = ["cli"] [[example]] -name = "album" +name = "client_credentials" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/album.rs" +path = "examples/client_credentials.rs" [[example]] -name = "current_user_recently_played" +name = "code_auth" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/current_user_recently_played.rs" +path = "examples/code_auth.rs" [[example]] -name = "oauth_tokens" +name = "code_auth_pkce" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/oauth_tokens.rs" +path = "examples/code_auth_pkce.rs" [[example]] -name = "track" -required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/track.rs" - -[[example]] -name = "tracks" +name = "oauth_tokens" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/tracks.rs" +path = "examples/oauth_tokens.rs" [[example]] name = "with_refresh_token" diff --git a/examples/album.rs b/examples/client_credentials.rs similarity index 90% rename from examples/album.rs rename to examples/client_credentials.rs index b59a1005..08852ca9 100644 --- a/examples/album.rs +++ b/examples/client_credentials.rs @@ -17,8 +17,8 @@ async fn main() { // // ``` // let creds = Credentials { - // client_id: "this-is-my-client-id".to_string(), - // client_secret: "this-is-my-client-secret".to_string() + // id: "this-is-my-client-id".to_string(), + // secret: "this-is-my-client-secret".to_string() // }; // ``` let creds = Credentials::from_env().unwrap(); diff --git a/examples/code_auth.rs b/examples/code_auth.rs new file mode 100644 index 00000000..fe83836a --- /dev/null +++ b/examples/code_auth.rs @@ -0,0 +1,57 @@ +use rspotify::{ + model::{AdditionalType, Country, Market}, + prelude::*, + scopes, CodeAuthSpotify, Credentials, OAuth, +}; + +#[tokio::main] +async fn main() { + // You can use any logger for debugging. + env_logger::init(); + + // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and + // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: + // + // export RSPOTIFY_CLIENT_ID="your client_id" + // export RSPOTIFY_CLIENT_SECRET="secret" + // + // These will then be read with `from_env`. + // + // Otherwise, set client_id and client_secret explictly: + // + // ``` + // let creds = Credentials { + // id: "this-is-my-client-id".to_string(), + // secret: "this-is-my-client-secret".to_string() + // }; + // ``` + let creds = Credentials::from_env().unwrap(); + + // Or set the redirect_uri explictly: + // + // ``` + // let mut oauth = OAuth { + // redirect_uri: "http://localhost:8888/callback".to_string(), + // scope: scopes!("user-read-recently-played"), + // ..Default::default(), + // }; + // ``` + let mut oauth = OAuth::from_env().unwrap(); + oauth.scope = scopes!("user-read-currently-playing"); + println!("{:?}", oauth); + + let mut spotify = CodeAuthSpotify::new(creds, oauth); + + // Obtaining the access token + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).await.unwrap(); + + // Running the requests + let market = Market::Country(Country::Spain); + let additional_types = [AdditionalType::Episode]; + let artists = spotify + .current_playing(Some(&market), Some(&additional_types)) + .await; + + println!("Response: {:?}", artists); +} diff --git a/examples/current_user_recently_played.rs b/examples/code_auth_pkce.rs similarity index 60% rename from examples/current_user_recently_played.rs rename to examples/code_auth_pkce.rs index 85de614c..eadf22cb 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/code_auth_pkce.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, CodeAuthPKCESpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -17,29 +17,32 @@ async fn main() { // // ``` // let creds = Credentials { - // client_id: "this-is-my-client-id".to_string(), - // client_secret: "this-is-my-client-secret".to_string() + // id: "this-is-my-client-id".to_string(), + // secret: "this-is-my-client-secret".to_string() // }; // ``` let creds = Credentials::from_env().unwrap(); // Or set the redirect_uri explictly: // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); + // ``` + // let mut oauth = OAuth { + // redirect_uri: "http://localhost:8888/callback".to_string(), + // scope: scopes!("user-read-recently-played"), + // ..Default::default(), + // }; + // ``` let mut oauth = OAuth::from_env().unwrap(); oauth.scope = scopes!("user-read-recently-played"); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = CodeAuthPKCESpotify::new(creds, oauth); // Obtaining the access token - let url = spotify.get_authorize_url(false).unwrap(); + let url = spotify.get_authorize_url().unwrap(); spotify.prompt_for_token(&url).await.unwrap(); // Running the requests - let history = spotify.current_user_recently_played(Some(10)).await; + let history = spotify.current_playback(None, None).await; println!("Response: {:?}", history); } diff --git a/examples/pagination_async.rs b/examples/pagination_async.rs index 874ce60c..1ab71edf 100644 --- a/examples/pagination_async.rs +++ b/examples/pagination_async.rs @@ -8,51 +8,22 @@ use futures::stream::TryStreamExt; use futures_util::pin_mut; -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{scopes, CodeAuthSpotify, Credentials}; #[tokio::main] async fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let mut oauth = OAuth::from_env().unwrap(); + oauth.scope = scopes!("user-library-read"); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-library-read")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().await.unwrap(); + let url = spotify.get_authorize_url(false); + spotify.prompt_for_user_token(url).await.unwrap(); // Executing the futures sequentially let stream = spotify.current_user_saved_tracks(); diff --git a/examples/track.rs b/examples/track.rs deleted file mode 100644 index 66446771..00000000 --- a/examples/track.rs +++ /dev/null @@ -1,42 +0,0 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::model::Id; -use rspotify::oauth2::CredentialsBuilder; - -#[tokio::main] -async fn main() { - // You can use any logger for debugging. - env_logger::init(); - - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .build() - .unwrap(); - - // Obtaining the access token. Requires to be mutable because the internal - // token will be modified. We don't need OAuth for this specific endpoint, - // so `...` is used instead of `prompt_for_user_token`. - spotify.request_client_token().await.unwrap(); - - // Running the requests - let birdy_uri = Id::from_uri("spotify:track:6rqhFgbbKwnb9MLmUQDhG6").unwrap(); - let track = spotify.track(birdy_uri).await; - - println!("Response: {:#?}", track); -} diff --git a/examples/tracks.rs b/examples/tracks.rs deleted file mode 100644 index 7549ecd7..00000000 --- a/examples/tracks.rs +++ /dev/null @@ -1,42 +0,0 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::model::Id; -use rspotify::oauth2::CredentialsBuilder; - -#[tokio::main] -async fn main() { - // You can use any logger for debugging. - env_logger::init(); - - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .build() - .unwrap(); - - // Obtaining the access token. Requires to be mutable because the internal - // token will be modified. We don't need OAuth for this specific endpoint, - // so `...` is used instead of `prompt_for_user_token`. - spotify.request_client_token().await.unwrap(); - - let birdy_uri1 = Id::from_uri("spotify:track:3n3Ppam7vgaVa1iaRUc9Lp").unwrap(); - let birdy_uri2 = Id::from_uri("spotify:track:3twNvmDtFQtAd5gMKedhLD").unwrap(); - let track_uris = vec![birdy_uri1, birdy_uri2]; - let tracks = spotify.tracks(track_uris, None).await; - println!("Response: {:?}", tracks); -} diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index e3df011a..f7d84aa7 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -905,7 +905,7 @@ pub trait OAuthClient: BaseClient { }; let result = self - .get("me/player/currently-playing", None, ¶ms) + .endpoint_get("me/player/currently-playing", ¶ms) .await?; if result.is_empty() { Ok(None) From a825ae75a1db2e50e314c140340931ffdc4d207b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 30 Apr 2021 23:24:47 +0200 Subject: [PATCH 16/56] remove println --- examples/code_auth.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/code_auth.rs b/examples/code_auth.rs index fe83836a..f58147db 100644 --- a/examples/code_auth.rs +++ b/examples/code_auth.rs @@ -38,7 +38,6 @@ async fn main() { // ``` let mut oauth = OAuth::from_env().unwrap(); oauth.scope = scopes!("user-read-currently-playing"); - println!("{:?}", oauth); let mut spotify = CodeAuthSpotify::new(creds, oauth); From 4ac19cc18de9190a3b64c3357b22695345120945 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 00:24:20 +0200 Subject: [PATCH 17/56] fixes most examples --- examples/code_auth.rs | 3 +-- examples/oauth_tokens.rs | 19 ++++++--------- examples/pagination_async.rs | 9 ++++---- examples/pagination_manual.rs | 42 +++++----------------------------- examples/pagination_sync.rs | 42 +++++----------------------------- examples/with_refresh_token.rs | 37 +++++++++--------------------- src/code_auth.rs | 10 ++++---- src/code_auth_pkce.rs | 10 ++++---- src/lib.rs | 3 ++- 9 files changed, 47 insertions(+), 128 deletions(-) diff --git a/examples/code_auth.rs b/examples/code_auth.rs index f58147db..7b246583 100644 --- a/examples/code_auth.rs +++ b/examples/code_auth.rs @@ -36,8 +36,7 @@ async fn main() { // ..Default::default(), // }; // ``` - let mut oauth = OAuth::from_env().unwrap(); - oauth.scope = scopes!("user-read-currently-playing"); + let mut oauth = OAuth::from_env(scopes!("user-read-currently-playing")).unwrap(); let mut spotify = CodeAuthSpotify::new(creds, oauth); diff --git a/examples/oauth_tokens.rs b/examples/oauth_tokens.rs index de127938..1339c17a 100644 --- a/examples/oauth_tokens.rs +++ b/examples/oauth_tokens.rs @@ -5,9 +5,7 @@ //! an .env file or export them manually as environmental variables for this to //! work. -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -16,10 +14,10 @@ async fn main() { // The credentials must be available in the environment. Enable // `env-file` in order to read them from an `.env` file. - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); // Using every possible scope - let scope = scopes!( + let scopes = scopes!( "user-read-email", "user-read-private", "user-top-read", @@ -38,15 +36,12 @@ async fn main() { "playlist-modify-private", "ugc-image-upload" ); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); + let oauth = OAuth::from_env(scopes).unwrap(); - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); - spotify.prompt_for_user_token().await.unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).await.unwrap(); let token = spotify.token.as_ref().unwrap(); println!("Access token: {}", &token.access_token); diff --git a/examples/pagination_async.rs b/examples/pagination_async.rs index 1ab71edf..6f202647 100644 --- a/examples/pagination_async.rs +++ b/examples/pagination_async.rs @@ -8,7 +8,7 @@ use futures::stream::TryStreamExt; use futures_util::pin_mut; -use rspotify::{scopes, CodeAuthSpotify, Credentials}; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -16,14 +16,13 @@ async fn main() { env_logger::init(); let creds = Credentials::from_env().unwrap(); - let mut oauth = OAuth::from_env().unwrap(); - oauth.scope = scopes!("user-library-read"); + let mut oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - let url = spotify.get_authorize_url(false); - spotify.prompt_for_user_token(url).await.unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).await.unwrap(); // Executing the futures sequentially let stream = spotify.current_user_saved_tracks(); diff --git a/examples/pagination_manual.rs b/examples/pagination_manual.rs index 80b338fd..50a7c3a5 100644 --- a/examples/pagination_manual.rs +++ b/examples/pagination_manual.rs @@ -1,51 +1,21 @@ //! This example shows how manual pagination works. It's what the raw API //! returns, but harder to use than an iterator or stream. -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-library-read")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().await.unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).await.unwrap(); // Manual pagination. You may choose the number of items returned per // iteration. diff --git a/examples/pagination_sync.rs b/examples/pagination_sync.rs index eb127588..99a1fe2c 100644 --- a/examples/pagination_sync.rs +++ b/examples/pagination_sync.rs @@ -9,50 +9,20 @@ //! } //! ``` -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-library-read")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).unwrap(); // Typical iteration, no extra boilerplate needed. let stream = spotify.current_user_saved_tracks(); diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 98e45e81..fedab8fb 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -15,14 +15,11 @@ //! tokens](https://github.com/felix-hilden/tekore/issues/86), so in the case of //! Spotify it doesn't seem to revoke them at all. -use rspotify::client::{Spotify, SpotifyBuilder}; -use rspotify::model::Id; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{model::Id, prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. -async fn do_things(spotify: Spotify) { +async fn do_things(spotify: CodeAuthSpotify) { let artists = vec![ Id::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash Id::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order @@ -57,20 +54,16 @@ async fn main() { env_logger::init(); // The default credentials from the `.env` file will be used by default. - let creds = CredentialsBuilder::from_env().build().unwrap(); - let scope = scopes!("user-follow-read user-follow-modify"); - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); - let mut spotify = SpotifyBuilder::default() - .credentials(creds.clone()) - .oauth(oauth.clone()) - .build() - .unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-follow-read user-follow-modify")).unwrap(); + let mut spotify = CodeAuthSpotify::new(creds.clone(), oauth.clone()); // In the first session of the application we authenticate and obtain the // refresh token. We can also do some requests here. println!(">>> Session one, obtaining refresh token and running some requests:"); + let url = spotify.get_authorize_url(false).unwrap(); spotify - .prompt_for_user_token_without_cache() + .prompt_for_token(&url) .await .expect("couldn't authenticate successfully"); let refresh_token = spotify @@ -86,14 +79,10 @@ async fn main() { // At a different time, the refresh token can be used to refresh an access // token directly and run requests: println!(">>> Session two, running some requests:"); - let mut spotify = SpotifyBuilder::default() - .credentials(creds.clone()) - .oauth(oauth.clone()) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds.clone(), oauth.clone()); // No `prompt_for_user_token_without_cache` needed. spotify - .refresh_user_token(&refresh_token) + .refresh_token(&refresh_token) .await .expect("couldn't refresh user token"); do_things(spotify).await; @@ -101,13 +90,9 @@ async fn main() { // This process can now be repeated multiple times by using only the // refresh token that was obtained at the beginning. println!(">>> Session three, running some requests:"); - let mut spotify = SpotifyBuilder::default() - .credentials(creds.clone()) - .oauth(oauth.clone()) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); spotify - .refresh_user_token(&refresh_token) + .refresh_token(&refresh_token) .await .expect("couldn't refresh user token"); do_things(spotify).await; diff --git a/src/code_auth.rs b/src/code_auth.rs index 1e1635a0..8742f03a 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -13,11 +13,11 @@ use url::Url; #[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { - creds: Credentials, - oauth: OAuth, - config: Config, - token: Option, - http: HttpClient, + pub creds: Credentials, + pub oauth: OAuth, + pub config: Config, + pub token: Option, + pub(in crate) http: HttpClient, } impl BaseClient for CodeAuthSpotify { diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 39ae3081..f40b40f2 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -12,11 +12,11 @@ use std::collections::HashMap; #[derive(Clone, Debug, Default)] pub struct CodeAuthPKCESpotify { - creds: Credentials, - oauth: OAuth, - config: Config, - tok: Option, - http: HttpClient, + pub creds: Credentials, + pub oauth: OAuth, + pub config: Config, + pub tok: Option, + pub(in crate) http: HttpClient, } impl BaseClient for CodeAuthPKCESpotify { diff --git a/src/lib.rs b/src/lib.rs index 815173ad..6168c843 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -459,7 +459,7 @@ impl OAuth { /// Parses the credentials from the environment variable /// `RSPOTIFY_REDIRECT_URI`. You can optionally activate the `env-file` /// feature in order to read these variables from a `.env` file. - pub fn from_env() -> Option { + pub fn from_env(scope: HashSet) -> Option { #[cfg(feature = "env-file")] { dotenv::dotenv().ok(); @@ -467,6 +467,7 @@ impl OAuth { Some(OAuth { redirect_uri: env::var("RSPOTIFY_REDIRECT_URI").ok()?, + scope, ..Default::default() }) } From 3ce657e6527cdbdb4ac42681ee0e11cbd2353757 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 01:16:30 +0200 Subject: [PATCH 18/56] fix pagination --- Cargo.toml | 1 + examples/code_auth.rs | 4 ++-- examples/code_auth_pkce.rs | 4 ++-- examples/pagination_async.rs | 2 +- src/endpoints/base.rs | 37 ++++++++++++++++++------------------ src/endpoints/mod.rs | 5 +++++ src/endpoints/oauth.rs | 32 +++++++++++++++---------------- 7 files changed, 44 insertions(+), 41 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0aa7f42b..99cb47a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ base64 = "0.13.0" chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } dotenv = { version = "0.15.0", optional = true } futures = { version = "0.3.8", optional = true } +futures-util = "0.3.8" # TODO getrandom = "0.2.0" log = "0.4.11" maybe-async = "0.2.1" diff --git a/examples/code_auth.rs b/examples/code_auth.rs index 7b246583..e3113d77 100644 --- a/examples/code_auth.rs +++ b/examples/code_auth.rs @@ -30,13 +30,13 @@ async fn main() { // Or set the redirect_uri explictly: // // ``` - // let mut oauth = OAuth { + // let oauth = OAuth { // redirect_uri: "http://localhost:8888/callback".to_string(), // scope: scopes!("user-read-recently-played"), // ..Default::default(), // }; // ``` - let mut oauth = OAuth::from_env(scopes!("user-read-currently-playing")).unwrap(); + let oauth = OAuth::from_env(scopes!("user-read-currently-playing")).unwrap(); let mut spotify = CodeAuthSpotify::new(creds, oauth); diff --git a/examples/code_auth_pkce.rs b/examples/code_auth_pkce.rs index eadf22cb..a4f6afde 100644 --- a/examples/code_auth_pkce.rs +++ b/examples/code_auth_pkce.rs @@ -26,13 +26,13 @@ async fn main() { // Or set the redirect_uri explictly: // // ``` - // let mut oauth = OAuth { + // let oauth = OAuth { // redirect_uri: "http://localhost:8888/callback".to_string(), // scope: scopes!("user-read-recently-played"), // ..Default::default(), // }; // ``` - let mut oauth = OAuth::from_env().unwrap(); + let oauth = OAuth::from_env().unwrap(); oauth.scope = scopes!("user-read-recently-played"); let mut spotify = CodeAuthPKCESpotify::new(creds, oauth); diff --git a/examples/pagination_async.rs b/examples/pagination_async.rs index 6f202647..35824699 100644 --- a/examples/pagination_async.rs +++ b/examples/pagination_async.rs @@ -16,7 +16,7 @@ async fn main() { env_logger::init(); let creds = Credentials::from_env().unwrap(); - let mut oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); + let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); let mut spotify = CodeAuthSpotify::new(creds, oauth); diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index ab3fbf8d..bdb1fc48 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,8 +1,7 @@ use crate::{ auth_urls, endpoints::{ - basic_auth, bearer_auth, convert_result, join_ids, - pagination::{paginate, Paginator}, + basic_auth, bearer_auth, convert_result, join_ids, pagination::paginate, DynPaginator, }, http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, @@ -261,8 +260,8 @@ where artist_id: &'a ArtistId, album_type: Option<&'a AlbumType>, market: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) }, @@ -408,11 +407,11 @@ where /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) - fn album_track<'a, Pag: Paginator> + 'a>( + fn album_track<'a>( &'a self, album_id: &'a AlbumId, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -533,8 +532,8 @@ where &'a self, id: &'a ShowId, market: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) }, @@ -672,8 +671,8 @@ where &'a self, locale: Option<&'a str>, country: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -717,8 +716,8 @@ where &'a self, category_id: &'a str, country: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) }, @@ -805,8 +804,8 @@ where fn new_releases<'a>( &'a self, country: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -928,8 +927,8 @@ where playlist_id: &'a PlaylistId, fields: Option<&'a str>, market: Option<&'a Market>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) }, @@ -974,8 +973,8 @@ where fn user_playlists<'a>( &'a self, user_id: &'a UserId, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, )) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 317f6a89..a49562a2 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -6,12 +6,17 @@ pub use base::BaseClient; pub use oauth::OAuthClient; use crate::{ + endpoints::pagination::Paginator, model::{idtypes::IdType, Id}, ClientResult, Token, }; +use std::pin::Pin; + use serde::Deserialize; +pub(in crate) type DynPaginator<'a, T> = Pin + 'a>>; + /// Converts a JSON response from Spotify into its model. pub(in crate) fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> ClientResult { serde_json::from_str::(input).map_err(Into::into) diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f7d84aa7..14fe150c 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -1,10 +1,6 @@ -use std::time; - use crate::{ endpoints::{ - append_device_id, convert_result, join_ids, - pagination::{paginate, Paginator}, - BaseClient, + append_device_id, convert_result, join_ids, pagination::paginate, BaseClient, DynPaginator, }, http::Query, macros::{build_json, build_map}, @@ -12,6 +8,8 @@ use crate::{ ClientResult, OAuth, Token, }; +use std::time; + use log::error; use maybe_async::maybe_async; use rspotify_model::idtypes::PlayContextIdType; @@ -81,8 +79,8 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - fn current_user_playlists(&self) -> Box> + '_> { - Box::new(paginate( + fn current_user_playlists(&self) -> DynPaginator<'_, ClientResult> { + Box::pin(paginate( move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -467,8 +465,8 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - fn current_user_saved_albums(&self) -> Box> + '_> { - Box::new(paginate( + fn current_user_saved_albums(&self) -> DynPaginator<'_, ClientResult> { + Box::pin(paginate( move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -504,8 +502,8 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - fn current_user_saved_tracks(&self) -> Box> + '_> { - Box::new(paginate( + fn current_user_saved_tracks(&self) -> DynPaginator<'_, ClientResult> { + Box::pin(paginate( move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, )) @@ -614,8 +612,8 @@ pub trait OAuthClient: BaseClient { fn current_user_top_artists<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) }, @@ -656,8 +654,8 @@ pub trait OAuthClient: BaseClient { fn current_user_top_tracks<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> Box> + 'a> { - Box::new(paginate( + ) -> DynPaginator<'a, ClientResult> { + Box::pin(paginate( move |limit, offset| { self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) }, @@ -1151,8 +1149,8 @@ pub trait OAuthClient: BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - fn get_saved_show(&self) -> Box> + '_> { - Box::new(paginate( + fn get_saved_show(&self) -> DynPaginator<'_, ClientResult> { + Box::pin(paginate( move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, )) From 9112d75595997f0e097ec6a450d90d5cc82bed97 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 01:36:57 +0200 Subject: [PATCH 19/56] all examples work now --- examples/ureq/device.rs | 42 ++++-------------------- examples/ureq/me.rs | 42 ++++-------------------- examples/ureq/search.rs | 64 ++++++++++--------------------------- examples/ureq/seek_track.rs | 42 ++++-------------------- src/endpoints/base.rs | 44 ++++++++++++------------- src/endpoints/oauth.rs | 4 +-- 6 files changed, 59 insertions(+), 179 deletions(-) diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index 4154394f..0e57c675 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -1,47 +1,17 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-read-playback-state")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).unwrap(); let devices = spotify.device(); diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index e089c2fd..e15b22bc 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -1,47 +1,17 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-read-playback-state")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).unwrap(); let user = spotify.me(); println!("Request: {:?}", user); diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index 2a601877..abd8a979 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -1,51 +1,21 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::model::{Country, Market, SearchType}; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{ + model::{Country, Market, SearchType}, + prelude::*, + ClientCredentialsSpotify, Credentials, +}; fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); - - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-read-playback-state")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let creds = Credentials::from_env().unwrap(); + let mut spotify = ClientCredentialsSpotify::new(creds); // Obtaining the access token - spotify.request_client_token().unwrap(); + spotify.request_token().unwrap(); let album_query = "album:arrival artist:abba"; - let result = spotify.search(album_query, SearchType::Album, None, None, Some(10), None); + let result = spotify.search(album_query, &SearchType::Album, None, None, Some(10), None); match result { Ok(album) => println!("searched album:{:?}", album), Err(err) => println!("search error!{:?}", err), @@ -54,8 +24,8 @@ fn main() { let artist_query = "tania bowra"; let result = spotify.search( artist_query, - SearchType::Artist, - Some(Market::Country(Country::UnitedStates)), + &SearchType::Artist, + Some(&Market::Country(Country::UnitedStates)), None, Some(10), None, @@ -68,8 +38,8 @@ fn main() { let playlist_query = "\"doom metal\""; let result = spotify.search( playlist_query, - SearchType::Playlist, - Some(Market::Country(Country::UnitedStates)), + &SearchType::Playlist, + Some(&Market::Country(Country::UnitedStates)), None, Some(10), None, @@ -82,8 +52,8 @@ fn main() { let track_query = "abba"; let result = spotify.search( track_query, - SearchType::Track, - Some(Market::Country(Country::UnitedStates)), + &SearchType::Track, + Some(&Market::Country(Country::UnitedStates)), None, Some(10), None, @@ -94,7 +64,7 @@ fn main() { } let show_query = "love"; - let result = spotify.search(show_query, SearchType::Show, None, None, Some(10), None); + let result = spotify.search(show_query, &SearchType::Show, None, None, Some(10), None); match result { Ok(show) => println!("searched show:{:?}", show), Err(err) => println!("search error!{:?}", err), @@ -103,7 +73,7 @@ fn main() { let episode_query = "love"; let result = spotify.search( episode_query, - SearchType::Episode, + &SearchType::Episode, None, None, Some(10), diff --git a/examples/ureq/seek_track.rs b/examples/ureq/seek_track.rs index 5194ddc5..62715c34 100644 --- a/examples/ureq/seek_track.rs +++ b/examples/ureq/seek_track.rs @@ -1,47 +1,17 @@ -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; -use rspotify::scopes; +use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. env_logger::init(); - // Set RSPOTIFY_CLIENT_ID, RSPOTIFY_CLIENT_SECRET and - // RSPOTIFY_REDIRECT_URI in an .env file or export them manually: - // - // export RSPOTIFY_CLIENT_ID="your client_id" - // export RSPOTIFY_CLIENT_SECRET="secret" - // - // These will then be read with `from_env`. - // - // Otherwise, set client_id and client_secret explictly: - // - // let creds = CredentialsBuilder::default() - // .client_id("this-is-my-client-id") - // .client_secret("this-is-my-client-secret") - // .build() - // .unwrap(); - let creds = CredentialsBuilder::from_env().build().unwrap(); + let creds = Credentials::from_env().unwrap(); + let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - // Or set the redirect_uri explictly: - // - // let oauth = OAuthBuilder::default() - // .redirect_uri("http://localhost:8888/callback") - // .build() - // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope(scopes!("user-read-playback-state")) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); // Obtaining the access token - spotify.prompt_for_user_token().unwrap(); + let url = spotify.get_authorize_url(false).unwrap(); + spotify.prompt_for_token(&url).unwrap(); match spotify.seek_track(25000, None) { Ok(_) => println!("change to previous playback successful"), diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index bdb1fc48..cc1a0ffc 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -15,20 +15,6 @@ use chrono::Utc; use maybe_async::maybe_async; use serde_json::{Map, Value}; -/// HTTP-related methods for the Spotify client. It wraps the basic HTTP client -/// with features needed of higher level. -/// -/// The Spotify client has two different wrappers to perform requests: -/// -/// * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only -/// append the configured Spotify API URL to the relative URL provided so that -/// it's not forgotten. They're used in the authentication process to request -/// an access token and similars. -/// * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, -/// `endpoint_delete`. These append the authentication headers for endpoint -/// requests to reduce the code needed for endpoints and make them as concise -/// as possible. - #[maybe_async(?Send)] pub trait BaseClient where @@ -60,6 +46,20 @@ where Ok(auth) } + // HTTP-related methods for the Spotify client. It wraps the basic HTTP + // client with features needed of higher level. + // + // The Spotify client has two different wrappers to perform requests: + // + // * Basic wrappers: `get`, `post`, `put`, `delete`, `post_form`. These only + // append the configured Spotify API URL to the relative URL provided so + // that it's not forgotten. They're used in the authentication process to + // request an access token and similars. + // * Endpoint wrappers: `endpoint_get`, `endpoint_post`, `endpoint_put`, + // `endpoint_delete`. These append the authentication headers for endpoint + // requests to reduce the code needed for endpoints and make them as + // concise as possible. + #[inline] async fn get( &self, @@ -260,7 +260,7 @@ where artist_id: &'a ArtistId, album_type: Option<&'a AlbumType>, market: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) @@ -410,7 +410,7 @@ where fn album_track<'a>( &'a self, album_id: &'a AlbumId, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, @@ -532,7 +532,7 @@ where &'a self, id: &'a ShowId, market: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) @@ -671,7 +671,7 @@ where &'a self, locale: Option<&'a str>, country: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), self.get_config().pagination_chunks, @@ -716,7 +716,7 @@ where &'a self, category_id: &'a str, country: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) @@ -804,7 +804,7 @@ where fn new_releases<'a>( &'a self, country: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), self.get_config().pagination_chunks, @@ -927,7 +927,7 @@ where playlist_id: &'a PlaylistId, fields: Option<&'a str>, market: Option<&'a Market>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) @@ -973,7 +973,7 @@ where fn user_playlists<'a>( &'a self, user_id: &'a UserId, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 14fe150c..f7512de3 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -612,7 +612,7 @@ pub trait OAuthClient: BaseClient { fn current_user_top_artists<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) @@ -654,7 +654,7 @@ pub trait OAuthClient: BaseClient { fn current_user_top_tracks<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> DynPaginator<'a, ClientResult> { + ) -> DynPaginator<'_, ClientResult> { Box::pin(paginate( move |limit, offset| { self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) From 3b41775bf42546def319da7cb88e08c4382dfe3e Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:10:18 +0200 Subject: [PATCH 20/56] fix tests --- src/endpoints/base.rs | 2 +- src/endpoints/mod.rs | 24 ++++++ src/lib.rs | 39 ++-------- tests/test_oauth2.rs | 139 +++++++++++++++------------------- tests/test_with_credential.rs | 21 ++--- tests/test_with_oauth.rs | 42 +++++----- 6 files changed, 120 insertions(+), 147 deletions(-) diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index cc1a0ffc..e34fb6d9 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -767,7 +767,7 @@ where &self, locale: Option<&str>, country: Option<&Market>, - timestamp: Option>, + timestamp: Option<&chrono::DateTime>, limit: Option, offset: Option, ) -> ClientResult { diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index a49562a2..03307d49 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -59,3 +59,27 @@ pub fn basic_auth(user: &str, password: &str) -> (String, String) { (auth, value) } + +#[cfg(test)] +mod test { + use super::append_device_id; + + #[test] + fn test_append_device_id_without_question_mark() { + let path = "me/player/play"; + let device_id = Some("fdafdsadfa"); + let new_path = append_device_id(path, device_id); + assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); + } + + #[test] + fn test_append_device_id_with_question_mark() { + let path = "me/player/shuffle?state=true"; + let device_id = Some("fdafdsadfa"); + let new_path = append_device_id(path, device_id); + assert_eq!( + new_path, + "me/player/shuffle?state=true&device_id=fdafdsadfa" + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index 6168c843..c82e05c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -415,6 +415,13 @@ pub struct Credentials { } impl Credentials { + pub fn new(id: &str, secret: &str) -> Self { + Credentials { + id: id.to_owned(), + secret: secret.to_owned(), + } + } + /// Parses the credentials from the environment variables /// `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET`. You can optionally /// activate the `env-file` feature in order to read these variables from @@ -475,39 +482,9 @@ impl OAuth { #[cfg(test)] mod test { - use super::generate_random_string; - use super::ClientCredentialsSpotify; + use super::{generate_random_string, ClientCredentialsSpotify}; use std::collections::HashSet; - #[test] - fn test_parse_response_code() { - let url = "http://localhost:8888/callback?code=AQD0yXvFEOvw&state=sN#_=_"; - let spotify = ClientCredentialsSpotify::default(); - let code = spotify.parse_response_code(url).unwrap(); - assert_eq!(code, "AQD0yXvFEOvw"); - } - - #[test] - fn test_append_device_id_without_question_mark() { - let path = "me/player/play"; - let device_id = Some("fdafdsadfa"); - let spotify = SpotifyBuilder::default().build().unwrap(); - let new_path = spotify.append_device_id(path, device_id); - assert_eq!(new_path, "me/player/play?device_id=fdafdsadfa"); - } - - #[test] - fn test_append_device_id_with_question_mark() { - let path = "me/player/shuffle?state=true"; - let device_id = Some("fdafdsadfa"); - let spotify = SpotifyBuilder::default().build().unwrap(); - let new_path = spotify.append_device_id(path, device_id); - assert_eq!( - new_path, - "me/player/shuffle?state=true&device_id=fdafdsadfa" - ); - } - #[test] fn test_generate_random_string() { let mut containers = HashSet::new(); diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index ba1dbaed..422dbc67 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -3,9 +3,10 @@ mod common; use chrono::prelude::*; use chrono::Duration; use maybe_async::maybe_async; -use rspotify::client::SpotifyBuilder; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, Token, TokenBuilder}; -use rspotify::scopes; +use rspotify::{ + prelude::*, scopes, ClientCredentialsSpotify, CodeAuthSpotify, Config, Credentials, OAuth, + Token, +}; use std::{collections::HashMap, fs, io::Read, path::PathBuf, thread::sleep}; use url::Url; @@ -13,24 +14,15 @@ use common::maybe_async_test; #[test] fn test_get_authorize_url() { - let oauth = OAuthBuilder::default() - .state("fdsafdsfa") - .redirect_uri("localhost") - .scope(scopes!("playlist-read-private")) - .build() - .unwrap(); - - let creds = CredentialsBuilder::default() - .id("this-is-my-client-id") - .secret("this-is-my-client-secret") - .build() - .unwrap(); - - let spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); + let oauth = OAuth { + state: "fdsafdsfa".to_owned(), + redirect_uri: "localhost".to_owned(), + scope: scopes!("playlist-read-private"), + ..Default::default() + }; + let creds = Credentials::new("this-is-my-client-id", "this-is-my-client-secret"); + + let spotify = CodeAuthSpotify::new(creds, oauth); let authorize_url = spotify.get_authorize_url(false).unwrap(); let hash_query: HashMap<_, _> = Url::parse(&authorize_url) @@ -49,41 +41,32 @@ fn test_get_authorize_url() { #[maybe_async] #[maybe_async_test] async fn test_read_token_cache() { - let now: DateTime = Utc::now(); + let now = Utc::now(); let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(3600)) - .expires_at(now) - .scope(scope.clone()) - .refresh_token("...") - .build() - .unwrap(); - - let predefined_spotify = SpotifyBuilder::default() - .token(tok.clone()) - .cache_path(PathBuf::from(".test_read_token_cache.json")) - .build() - .unwrap(); + let tok = Token { + access_token: "test-access_token".to_owned(), + expires_in: Duration::seconds(3600), + expires_at: Some(now), + scope: scope.clone(), + refresh_token: Some("...".to_owned()), + }; + + let config = Config { + cache_path: PathBuf::from(".test_read_token_cache.json"), + ..Default::default() + }; + let mut predefined_spotify = ClientCredentialsSpotify::default(); + predefined_spotify.config = config.clone(); + predefined_spotify.token = Some(tok.clone()); // write token data to cache_path predefined_spotify.write_token_cache().unwrap(); - assert!(predefined_spotify.cache_path.exists()); - - let oauth_scope = scopes!("playlist-read-private"); - let oauth = OAuthBuilder::default() - .state("fdasfasfdasd") - .redirect_uri("http://localhost:8000") - .scope(oauth_scope) - .build() - .unwrap(); - - let mut spotify = SpotifyBuilder::default() - .oauth(oauth) - .cache_path(PathBuf::from(".test_read_token_cache.json")) - .build() - .unwrap(); + assert!(predefined_spotify.config.cache_path.exists()); + + let mut spotify = ClientCredentialsSpotify::default(); + spotify.config = config; + // read token from cache file let tok_from_file = spotify.read_token_cache().await.unwrap(); assert_eq!(tok_from_file.scope, scope); @@ -92,33 +75,34 @@ async fn test_read_token_cache() { assert_eq!(tok_from_file.expires_at.unwrap(), now); // delete cache file in the end - fs::remove_file(&spotify.cache_path).unwrap(); + fs::remove_file(&spotify.config.cache_path).unwrap(); } #[test] fn test_write_token() { - let now: DateTime = Utc::now(); + let now = Utc::now(); let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(3600)) - .expires_at(now) - .scope(scope.clone()) - .refresh_token("...") - .build() - .unwrap(); - - let spotify = SpotifyBuilder::default() - .token(tok.clone()) - .cache_path(PathBuf::from(".test_write_token_cache.json")) - .build() - .unwrap(); + let tok = Token { + access_token: "test-access_token".to_owned(), + expires_in: Duration::seconds(3600), + expires_at: Some(now), + scope: scope.clone(), + refresh_token: Some("...".to_owned()), + }; + + let config = Config { + cache_path: PathBuf::from(".test_write_token_cache.json"), + ..Default::default() + }; + let mut spotify = ClientCredentialsSpotify::default(); + spotify.token = Some(tok.clone()); + spotify.config = config; let tok_str = serde_json::to_string(&tok).unwrap(); spotify.write_token_cache().unwrap(); - let mut file = fs::File::open(&spotify.cache_path).unwrap(); + let mut file = fs::File::open(&spotify.config.cache_path).unwrap(); let mut tok_str_file = String::new(); file.read_to_string(&mut tok_str_file).unwrap(); @@ -129,21 +113,20 @@ fn test_write_token() { assert_eq!(tok_from_file.expires_at.unwrap(), now); // delete cache file in the end - fs::remove_file(&spotify.cache_path).unwrap(); + fs::remove_file(&spotify.config.cache_path).unwrap(); } #[test] fn test_token_is_expired() { let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(1)) - .expires_at(Utc::now()) - .scope(scope) - .refresh_token("...") - .build() - .unwrap(); + let tok = Token { + scope, + access_token: "test-access_token".to_owned(), + expires_in: Duration::seconds(1), + expires_at: Some(Utc::now()), + refresh_token: Some("...".to_owned()), + }; assert!(!tok.is_expired()); sleep(std::time::Duration::from_secs(2)); assert!(tok.is_expired()); @@ -151,7 +134,7 @@ fn test_token_is_expired() { #[test] fn test_parse_response_code() { - let spotify = SpotifyBuilder::default().build().unwrap(); + let spotify = CodeAuthSpotify::default(); let url = "http://localhost:8888/callback"; let code = spotify.parse_response_code(url); diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index 81ec48af..ad12a40d 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -1,19 +1,19 @@ mod common; use common::maybe_async_test; -use rspotify::oauth2::CredentialsBuilder; use rspotify::{ - client::{Spotify, SpotifyBuilder}, model::{AlbumType, Country, Id, Market}, + prelude::*, + ClientCredentialsSpotify, Credentials, }; use maybe_async::maybe_async; /// Generating a new basic client for the requests. #[maybe_async] -pub async fn creds_client() -> Spotify { +pub async fn creds_client() -> ClientCredentialsSpotify { // The credentials must be available in the environment. - let creds = CredentialsBuilder::from_env().build().unwrap_or_else(|_| { + let creds = Credentials::from_env().unwrap_or_else(|| { panic!( "No credentials configured. Make sure that either the `env-file` \ feature is enabled, or that the required environment variables are \ @@ -21,13 +21,8 @@ pub async fn creds_client() -> Spotify { ) }); - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .build() - .unwrap(); - - spotify.request_client_token().await.unwrap(); - + let mut spotify = ClientCredentialsSpotify::new(creds); + spotify.request_token().await.unwrap(); spotify } @@ -208,7 +203,7 @@ mod test_pagination { #[test] fn test_pagination_sync() { let mut client = creds_client(); - client.pagination_chunks = 2; + client.config.pagination_chunks = 2; let album = Id::from_uri(ALBUM).unwrap(); let names = client @@ -226,7 +221,7 @@ mod test_pagination { use futures_util::StreamExt; let mut client = creds_client().await; - client.pagination_chunks = 2; + client.config.pagination_chunks = 2; let album = Id::from_uri(ALBUM).unwrap(); let names = client diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 6a10236c..9fd1849d 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -17,15 +17,13 @@ mod common; use common::maybe_async_test; -use rspotify::model::offset::Offset; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; use rspotify::{ - client::{Spotify, SpotifyBuilder}, model::{ - Country, EpisodeId, Id, Market, RepeatState, SearchType, ShowId, TimeRange, TrackId, - TrackPositions, + Country, EpisodeId, Id, Market, Offset, RepeatState, SearchType, ShowId, TimeRange, + TrackId, TrackPositions, }, - scopes, + prelude::*, + scopes, CodeAuthSpotify, Credentials, OAuth, Token, }; use chrono::prelude::*; @@ -35,18 +33,20 @@ use std::env; /// Generating a new OAuth client for the requests. #[maybe_async] -pub async fn oauth_client() -> Spotify { +pub async fn oauth_client() -> CodeAuthSpotify { if let Ok(access_token) = env::var("RSPOTIFY_ACCESS_TOKEN") { - let tok = TokenBuilder::default() - .access_token(access_token) - .build() - .unwrap(); - - SpotifyBuilder::default().token(tok).build().unwrap() + let tok = Token { + access_token, + ..Default::default() + }; + + let mut client = CodeAuthSpotify::default(); + client.token = Some(tok); + client } else if let Ok(refresh_token) = env::var("RSPOTIFY_REFRESH_TOKEN") { // The credentials must be available in the environment. Enable // `env-file` in order to read them from an `.env` file. - let creds = CredentialsBuilder::from_env().build().unwrap_or_else(|_| { + let creds = Credentials::from_env().unwrap_or_else(|| { panic!( "No credentials configured. Make sure that either the \ `env-file` feature is enabled, or that the required \ @@ -75,16 +75,10 @@ pub async fn oauth_client() -> Spotify { "ugc-image-upload" ); // Using every possible scope - let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); - - let mut spotify = SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .build() - .unwrap(); - - spotify.refresh_user_token(&refresh_token).await.unwrap(); + let oauth = OAuth::from_env(scope).unwrap(); + let mut spotify = CodeAuthSpotify::new(creds, oauth); + spotify.refresh_token(&refresh_token).await.unwrap(); spotify } else { panic!( @@ -133,7 +127,7 @@ async fn test_category_playlists() { async fn test_current_playback() { oauth_client() .await - .current_playback::<&[_]>(None, None) + .current_playback(None, None::<&[_]>) .await .unwrap(); } From 17781e599daa8f6db88366095c88ccf7ead08c4b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:22:25 +0200 Subject: [PATCH 21/56] fully fix pagination --- src/endpoints/base.rs | 51 +++++++++++++++--------------- src/endpoints/mod.rs | 2 -- src/endpoints/oauth.rs | 40 ++++++++++++----------- src/endpoints/pagination/iter.rs | 10 +++--- src/endpoints/pagination/stream.rs | 12 +++---- 5 files changed, 57 insertions(+), 58 deletions(-) diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index e34fb6d9..d1e485ec 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -1,7 +1,8 @@ use crate::{ auth_urls, endpoints::{ - basic_auth, bearer_auth, convert_result, join_ids, pagination::paginate, DynPaginator, + basic_auth, bearer_auth, convert_result, join_ids, + pagination::{paginate, Paginator}, }, http::{BaseHttpClient, Form, Headers, HttpClient, Query}, macros::build_map, @@ -260,13 +261,13 @@ where artist_id: &'a ArtistId, album_type: Option<&'a AlbumType>, market: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.artist_albums_manual(artist_id, album_type, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::artist_albums`]. @@ -410,11 +411,11 @@ where fn album_track<'a>( &'a self, album_id: &'a AlbumId, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.album_track_manual(album_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::album_track`]. @@ -532,13 +533,13 @@ where &'a self, id: &'a ShowId, market: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.get_shows_episodes_manual(id, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::get_shows_episodes`]. @@ -671,11 +672,11 @@ where &'a self, locale: Option<&'a str>, country: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.categories_manual(locale, country, Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::categories`]. @@ -716,13 +717,13 @@ where &'a self, category_id: &'a str, country: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.category_playlists_manual(category_id, country, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::category_playlists`]. @@ -804,11 +805,11 @@ where fn new_releases<'a>( &'a self, country: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.new_releases_manual(country, Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::new_releases`]. @@ -927,13 +928,13 @@ where playlist_id: &'a PlaylistId, fields: Option<&'a str>, market: Option<&'a Market>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.playlist_tracks_manual(playlist_id, fields, market, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::playlist_tracks`]. @@ -973,11 +974,11 @@ where fn user_playlists<'a>( &'a self, user_id: &'a UserId, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.user_playlists_manual(user_id, Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::user_playlists`]. diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 03307d49..b494d314 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -15,8 +15,6 @@ use std::pin::Pin; use serde::Deserialize; -pub(in crate) type DynPaginator<'a, T> = Pin + 'a>>; - /// Converts a JSON response from Spotify into its model. pub(in crate) fn convert_result<'a, T: Deserialize<'a>>(input: &'a str) -> ClientResult { serde_json::from_str::(input).map_err(Into::into) diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f7512de3..39de1ab7 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -1,6 +1,8 @@ use crate::{ endpoints::{ - append_device_id, convert_result, join_ids, pagination::paginate, BaseClient, DynPaginator, + append_device_id, convert_result, join_ids, + pagination::{paginate, Paginator}, + BaseClient, }, http::Query, macros::{build_json, build_map}, @@ -79,11 +81,11 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) - fn current_user_playlists(&self) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + fn current_user_playlists(&self) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.current_user_playlists_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::current_user_playlists`]. @@ -465,11 +467,11 @@ pub trait OAuthClient: BaseClient { /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) - fn current_user_saved_albums(&self) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + fn current_user_saved_albums(&self) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.current_user_saved_albums_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of @@ -502,11 +504,11 @@ pub trait OAuthClient: BaseClient { /// paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) - fn current_user_saved_tracks(&self) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + fn current_user_saved_tracks(&self) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.current_user_saved_tracks_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of @@ -612,13 +614,13 @@ pub trait OAuthClient: BaseClient { fn current_user_top_artists<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.current_user_top_artists_manual(time_range, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::current_user_top_artists`]. @@ -654,13 +656,13 @@ pub trait OAuthClient: BaseClient { fn current_user_top_tracks<'a>( &'a self, time_range: Option<&'a TimeRange>, - ) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + ) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| { self.current_user_top_tracks_manual(time_range, Some(limit), Some(offset)) }, self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::current_user_top_tracks`]. @@ -1149,11 +1151,11 @@ pub trait OAuthClient: BaseClient { /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) - fn get_saved_show(&self) -> DynPaginator<'_, ClientResult> { - Box::pin(paginate( + fn get_saved_show(&self) -> Paginator<'_, ClientResult> { + paginate( move |limit, offset| self.get_saved_show_manual(Some(limit), Some(offset)), self.get_config().pagination_chunks, - )) + ) } /// The manually paginated version of [`Spotify::get_saved_show`]. diff --git a/src/endpoints/pagination/iter.rs b/src/endpoints/pagination/iter.rs index 1e7ead0f..80432f0d 100644 --- a/src/endpoints/pagination/iter.rs +++ b/src/endpoints/pagination/iter.rs @@ -3,11 +3,13 @@ use crate::{model::Page, ClientError, ClientResult}; /// Alias for `Iterator`, since sync mode is enabled. -pub trait Paginator: Iterator {} -impl> Paginator for I {} +pub type Paginator<'a, T> = Box + 'a>; /// This is used to handle paginated requests automatically. -pub fn paginate(req: Request, page_size: u32) -> impl Iterator> +pub fn paginate<'a, T: 'a, Request: 'a>( + req: Request, + page_size: u32, +) -> Paginator<'a, ClientResult> where Request: Fn(u32, u32) -> ClientResult>, { @@ -18,7 +20,7 @@ where page_size, }; - pages.flat_map(|result| ResultIter::new(result.map(|page| page.items.into_iter()))) + Box::new(pages.flat_map(|result| ResultIter::new(result.map(|page| page.items.into_iter())))) } /// Iterator that repeatedly calls a function that returns a page until an empty diff --git a/src/endpoints/pagination/stream.rs b/src/endpoints/pagination/stream.rs index 947804d0..d9e5541f 100644 --- a/src/endpoints/pagination/stream.rs +++ b/src/endpoints/pagination/stream.rs @@ -6,14 +6,10 @@ use futures::future::Future; use futures::stream::Stream; /// Alias for `futures::stream::Stream`, since async mode is enabled. -pub trait Paginator: Stream {} -impl> Paginator for I {} +pub type Paginator<'a, T> = Pin + 'a>>; /// This is used to handle paginated requests automatically. -pub fn paginate( - req: Request, - page_size: u32, -) -> impl Stream> +pub fn paginate<'a, T, Fut, Request>(req: Request, page_size: u32) -> Paginator<'a, T> where T: Unpin, Fut: Future>>, @@ -21,7 +17,7 @@ where { use async_stream::stream; let mut offset = 0; - stream! { + Box::pin(stream! { loop { let page = req(page_size, offset).await?; offset += page.items.len() as u32; @@ -32,5 +28,5 @@ where break; } } - } + }) } From bf3235af04b2ccc0b03cdb4df41d55e2738ee699 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:26:43 +0200 Subject: [PATCH 22/56] some more fixes --- src/endpoints/mod.rs | 3 --- src/endpoints/pagination/stream.rs | 14 +++++++++----- src/lib.rs | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b494d314..7b97f4e5 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -6,13 +6,10 @@ pub use base::BaseClient; pub use oauth::OAuthClient; use crate::{ - endpoints::pagination::Paginator, model::{idtypes::IdType, Id}, ClientResult, Token, }; -use std::pin::Pin; - use serde::Deserialize; /// Converts a JSON response from Spotify into its model. diff --git a/src/endpoints/pagination/stream.rs b/src/endpoints/pagination/stream.rs index d9e5541f..8c383019 100644 --- a/src/endpoints/pagination/stream.rs +++ b/src/endpoints/pagination/stream.rs @@ -1,15 +1,19 @@ //! Asynchronous implementation of automatic pagination requests. -use crate::model::Page; -use crate::ClientResult; -use futures::future::Future; -use futures::stream::Stream; +use crate::{model::Page, ClientResult}; + +use std::pin::Pin; + +use futures::{future::Future, stream::Stream}; /// Alias for `futures::stream::Stream`, since async mode is enabled. pub type Paginator<'a, T> = Pin + 'a>>; /// This is used to handle paginated requests automatically. -pub fn paginate<'a, T, Fut, Request>(req: Request, page_size: u32) -> Paginator<'a, T> +pub fn paginate<'a, T: 'a, Fut, Request: 'a>( + req: Request, + page_size: u32, +) -> Paginator<'a, ClientResult> where T: Unpin, Fut: Future>>, diff --git a/src/lib.rs b/src/lib.rs index c82e05c7..5ddd317e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -482,7 +482,7 @@ impl OAuth { #[cfg(test)] mod test { - use super::{generate_random_string, ClientCredentialsSpotify}; + use super::generate_random_string; use std::collections::HashSet; #[test] From 12a97ef123d89d2679c36663865b044d8ba27a16 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:30:26 +0200 Subject: [PATCH 23/56] fix clippy --- src/client_creds.rs | 2 +- src/code_auth_pkce.rs | 5 +++-- src/endpoints/base.rs | 2 +- src/lib.rs | 5 +++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client_creds.rs b/src/client_creds.rs index 88652f24..86f7bc55 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -48,8 +48,8 @@ impl ClientCredentialsSpotify { pub fn with_config(creds: Credentials, config: Config) -> Self { ClientCredentialsSpotify { - creds, config, + creds, ..Default::default() } } diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index f40b40f2..9dda677e 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -81,8 +81,9 @@ impl CodeAuthPKCESpotify { payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); payload.insert(headers::SCOPE, &scope); payload.insert(headers::STATE, &oauth.state); - payload.insert(headers::CODE_CHALLENGE, todo!()); - payload.insert(headers::CODE_CHALLENGE_METHOD, "S256"); + // TODO + // payload.insert(headers::CODE_CHALLENGE, todo!()); + // payload.insert(headers::CODE_CHALLENGE_METHOD, "S256"); let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; Ok(parsed.into_string()) diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index d1e485ec..8fa8faa0 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -121,7 +121,7 @@ where #[inline] async fn endpoint_get(&self, url: &str, payload: &Query<'_>) -> ClientResult { let headers = self.auth_headers()?; - Ok(self.get(url, Some(&headers), payload).await?) + self.get(url, Some(&headers), payload).await } #[inline] diff --git a/src/lib.rs b/src/lib.rs index 5ddd317e..044ddd67 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,8 +208,9 @@ pub(in crate) mod headers { pub const SCOPE: &str = "scope"; pub const SHOW_DIALOG: &str = "show_dialog"; pub const STATE: &str = "state"; - pub const CODE_CHALLENGE: &str = "code_challenge"; - pub const CODE_CHALLENGE_METHOD: &str = "code_challenge_method"; + // TODO: + // pub const CODE_CHALLENGE: &str = "code_challenge"; + // pub const CODE_CHALLENGE_METHOD: &str = "code_challenge_method"; } pub(in crate) mod auth_urls { From c87b3406f8dd514e262d0b6366d8d5180f7182e1 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:37:58 +0200 Subject: [PATCH 24/56] i think this should fully pass CI --- examples/code_auth_pkce.rs | 4 ++-- rspotify-macros/src/lib.rs | 21 +++++++++------------ src/code_auth_pkce.rs | 12 ++++++------ src/lib.rs | 2 +- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/examples/code_auth_pkce.rs b/examples/code_auth_pkce.rs index a4f6afde..90b8977f 100644 --- a/examples/code_auth_pkce.rs +++ b/examples/code_auth_pkce.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthPKCESpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, CodeAuthPkceSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -35,7 +35,7 @@ async fn main() { let oauth = OAuth::from_env().unwrap(); oauth.scope = scopes!("user-read-recently-played"); - let mut spotify = CodeAuthPKCESpotify::new(creds, oauth); + let mut spotify = CodeAuthPkceSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url().unwrap(); diff --git a/rspotify-macros/src/lib.rs b/rspotify-macros/src/lib.rs index a8063c36..a31af828 100644 --- a/rspotify-macros/src/lib.rs +++ b/rspotify-macros/src/lib.rs @@ -6,21 +6,18 @@ /// Example: /// /// ``` -/// use rspotify::oauth2::TokenBuilder; -/// use rspotify::scopes; +/// use rspotify::{Token, scopes}; /// use std::collections::HashSet; -/// use chrono::prelude::*; -/// use chrono::Duration; +/// use chrono::{Duration, prelude::*}; /// /// let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); -/// let tok = TokenBuilder::default() -/// .access_token("test-access_token") -/// .expires_in(Duration::seconds(1)) -/// .expires_at(Utc::now()) -/// .scope(scope) -/// .refresh_token("...") -/// .build() -/// .unwrap(); +/// let tok = Token { +/// scope, +/// access_token: "test-access_token".to_owned(), +/// expires_in: Duration::seconds(1), +/// expires_at: Some(Utc::now().to_owned()), +/// refresh_token: Some("...".to_owned()), +/// }; /// ``` #[macro_export] macro_rules! scopes { diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 9dda677e..8e00ab16 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -11,7 +11,7 @@ use crate::{ use std::collections::HashMap; #[derive(Clone, Debug, Default)] -pub struct CodeAuthPKCESpotify { +pub struct CodeAuthPkceSpotify { pub creds: Credentials, pub oauth: OAuth, pub config: Config, @@ -19,7 +19,7 @@ pub struct CodeAuthPKCESpotify { pub(in crate) http: HttpClient, } -impl BaseClient for CodeAuthPKCESpotify { +impl BaseClient for CodeAuthPkceSpotify { fn get_http(&self) -> &HttpClient { &self.http } @@ -41,15 +41,15 @@ impl BaseClient for CodeAuthPKCESpotify { } } -impl OAuthClient for CodeAuthPKCESpotify { +impl OAuthClient for CodeAuthPkceSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } } -impl CodeAuthPKCESpotify { +impl CodeAuthPkceSpotify { pub fn new(creds: Credentials, oauth: OAuth) -> Self { - CodeAuthPKCESpotify { + CodeAuthPkceSpotify { creds, oauth, ..Default::default() @@ -57,7 +57,7 @@ impl CodeAuthPKCESpotify { } pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { - CodeAuthPKCESpotify { + CodeAuthPkceSpotify { creds, oauth, config, diff --git a/src/lib.rs b/src/lib.rs index 044ddd67..a2ed5545 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -173,7 +173,7 @@ pub use rspotify_model as model; // Top-level re-exports pub use client_creds::ClientCredentialsSpotify; pub use code_auth::CodeAuthSpotify; -pub use code_auth_pkce::CodeAuthPKCESpotify; +pub use code_auth_pkce::CodeAuthPkceSpotify; pub use macros::scopes; use std::{ From a4ce9efe012d2e66b620160da8ef15d632f8e1fa Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 1 May 2021 02:52:32 +0200 Subject: [PATCH 25/56] ClientError::InvalidAuth can be removed now --- src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index a2ed5545..256ec8a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,10 +221,6 @@ pub(in crate) mod auth_urls { /// Possible errors returned from the `rspotify` client. #[derive(Debug, Error)] pub enum ClientError { - /// Raised when the authentication isn't configured properly. - #[error("invalid client authentication: {0}")] - InvalidAuth(String), - #[error("json parse error: {0}")] ParseJson(#[from] serde_json::Error), From f7721b122bddab9fb55a40487fd03d61a1050983 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 00:50:03 +0200 Subject: [PATCH 26/56] fix compilation conflicts --- rspotify-http/src/common.rs | 63 ++++++++++++++++++++++++ rspotify-http/src/lib.rs | 93 +++++++++-------------------------- rspotify-macros/src/lib.rs | 3 +- rspotify-model/src/idtypes.rs | 18 +++---- src/client_creds.rs | 7 ++- src/code_auth.rs | 7 +++ src/lib.rs | 73 ++++++++++++++++----------- 7 files changed, 152 insertions(+), 112 deletions(-) create mode 100644 rspotify-http/src/common.rs diff --git a/rspotify-http/src/common.rs b/rspotify-http/src/common.rs new file mode 100644 index 00000000..bad4fe4b --- /dev/null +++ b/rspotify-http/src/common.rs @@ -0,0 +1,63 @@ +use std::collections::HashMap; +use std::fmt; + +use maybe_async::maybe_async; +use rspotify_model::ApiError; +use serde_json::Value; + +pub type Headers = HashMap; +pub type Query<'a> = HashMap<&'a str, &'a str>; +pub type Form<'a> = HashMap<&'a str, &'a str>; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("request unauthorized")] + Unauthorized, + + #[error("exceeded request limit")] + RateLimited(Option), + + #[error("request error: {0}")] + Request(String), + + #[error("status code {0}: {1}")] + StatusCode(u16, String), + + #[error("spotify error: {0}")] + Api(#[from] ApiError), + + #[error("input/output error: {0}")] + Io(#[from] std::io::Error), +} + +pub type Result = std::result::Result; + +/// This trait represents the interface to be implemented for an HTTP client, +/// which is kept separate from the Spotify client for cleaner code. Thus, it +/// also requires other basic traits that are needed for the Spotify client. +/// +/// When a request doesn't need to pass parameters, the empty or default value +/// of the payload type should be passed, like `json!({})` or `Query::new()`. +/// This avoids using `Option` because `Value` itself may be null in other +/// different ways (`Value::Null`, an empty `Value::Object`...), so this removes +/// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make +/// much sense). +#[maybe_async] +pub trait BaseHttpClient: Send + Default + Clone + fmt::Debug { + // This internal function should always be given an object value in JSON. + async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result; + + async fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; + + async fn post_form<'a>( + &self, + url: &str, + headers: Option<&Headers>, + payload: &Form<'a>, + ) -> Result; + + async fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; + + async fn delete(&self, url: &str, headers: Option<&Headers>, payload: &Value) + -> Result; +} diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index cdf750f2..477ed732 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -1,13 +1,33 @@ //! The HTTP client may vary depending on which one the user configures. This //! module contains the required logic to use different clients interchangeably. +// Disable all modules when both client features are enabled or when none are. +// This way only the compile error below gets shown instead of a whole list of +// confusing errors.. + #[cfg(feature = "client-reqwest")] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] mod reqwest; + #[cfg(feature = "client-ureq")] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] mod ureq; -// #[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] -// #[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] +#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] +mod common; + +#[cfg(feature = "client-reqwest")] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] +pub use self::reqwest::ReqwestClient as HttpClient; + +#[cfg(feature = "client-ureq")] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] +pub use self::ureq::UreqClient as HttpClient; + +#[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] +#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] +pub use common::{Error, Result, BaseHttpClient, Form, Headers, Query}; #[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] compile_error!( @@ -22,75 +42,6 @@ compile_error!( `client-reqwest` or `client-ureq` features." ); -use std::collections::HashMap; -use std::fmt; - -use maybe_async::maybe_async; -use rspotify_model::ApiError; -use serde_json::Value; - -#[cfg(feature = "client-reqwest")] -pub use self::reqwest::ReqwestClient as HttpClient; -#[cfg(feature = "client-ureq")] -pub use self::ureq::UreqClient as HttpClient; - -pub type Headers = HashMap; -pub type Query<'a> = HashMap<&'a str, &'a str>; -pub type Form<'a> = HashMap<&'a str, &'a str>; - -#[derive(thiserror::Error, Debug)] -pub enum Error { - #[error("request unauthorized")] - Unauthorized, - - #[error("exceeded request limit")] - RateLimited(Option), - - #[error("request error: {0}")] - Request(String), - - #[error("status code {0}: {1}")] - StatusCode(u16, String), - - #[error("spotify error: {0}")] - Api(#[from] ApiError), - - #[error("input/output error: {0}")] - Io(#[from] std::io::Error), -} - -pub type Result = std::result::Result; - -/// This trait represents the interface to be implemented for an HTTP client, -/// which is kept separate from the Spotify client for cleaner code. Thus, it -/// also requires other basic traits that are needed for the Spotify client. -/// -/// When a request doesn't need to pass parameters, the empty or default value -/// of the payload type should be passed, like `json!({})` or `Query::new()`. -/// This avoids using `Option` because `Value` itself may be null in other -/// different ways (`Value::Null`, an empty `Value::Object`...), so this removes -/// redundancy and edge cases (a `Some(Value::Null), for example, doesn't make -/// much sense). -#[maybe_async] -pub trait BaseHttpClient: Send + Default + Clone + fmt::Debug { - // This internal function should always be given an object value in JSON. - async fn get(&self, url: &str, headers: Option<&Headers>, payload: &Query) -> Result; - - async fn post(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; - - async fn post_form<'a>( - &self, - url: &str, - headers: Option<&Headers>, - payload: &Form<'a>, - ) -> Result; - - async fn put(&self, url: &str, headers: Option<&Headers>, payload: &Value) -> Result; - - async fn delete(&self, url: &str, headers: Option<&Headers>, payload: &Value) - -> Result; -} - #[cfg(test)] mod test { use super::*; diff --git a/rspotify-macros/src/lib.rs b/rspotify-macros/src/lib.rs index a31af828..8b641cce 100644 --- a/rspotify-macros/src/lib.rs +++ b/rspotify-macros/src/lib.rs @@ -1,7 +1,6 @@ /// Create a [`HashSet`](std::collections::HashSet) from a list of `&str` (which /// will be converted to String internally), to easily create scopes for -/// [`Token`](rspotify::oauth2::Token) or -/// [`OAuthBuilder`](rspotify::oauth2::OAuthBuilder). +/// [`Token`](rspotify::Token) or [`OAuth`](rspotify::OAuth). /// /// Example: /// diff --git a/rspotify-model/src/idtypes.rs b/rspotify-model/src/idtypes.rs index 25eb6181..a2f0622c 100644 --- a/rspotify-model/src/idtypes.rs +++ b/rspotify-model/src/idtypes.rs @@ -61,10 +61,10 @@ pub type UserIdBuf = IdBuf; pub type ShowIdBuf = IdBuf; pub type EpisodeIdBuf = IdBuf; -/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// A Spotify object id of given [type](crate::enums::types::Type). /// -/// This is a not-owning type, it stores a `&str` only. -/// See [IdBuf](crate::model::idtypes::IdBuf) for owned version of the type. +/// This is a not-owning type, it stores a `&str` only. See +/// [IdBuf](crate::idtypes::IdBuf) for owned version of the type. #[derive(Debug, PartialEq, Eq, Serialize)] pub struct Id { #[serde(default)] @@ -73,10 +73,10 @@ pub struct Id { id: str, } -/// A Spotify object id of given [type](crate::model::enums::types::Type) +/// A Spotify object id of given [type](crate::enums::types::Type) /// -/// This is an owning type, it stores a `String`. -/// See [Id](crate::model::idtypes::Id) for light-weight non-owning type. +/// This is an owning type, it stores a `String`. See [Id](crate::idtypes::Id) +/// for light-weight non-owning type. /// /// Use `Id::from_id(val).to_owned()`, `Id::from_uri(val).to_owned()` or /// `Id::from_id_or_uri(val).to_owned()` to construct an instance of this type. @@ -111,7 +111,7 @@ impl Deref for IdBuf { } impl IdBuf { - /// Get a [`Type`](crate::model::enums::types::Type) of the id + /// Get a [`Type`](crate::enums::types::Type) of the id pub fn _type(&self) -> Type { T::TYPE } @@ -134,7 +134,7 @@ impl IdBuf { /// Spotify id or URI parsing error /// -/// See also [`Id`](crate::model::idtypes::Id) for details. +/// See also [`Id`](crate::idtypes::Id) for details. #[derive(Debug, PartialEq, Eq, Clone, Copy, Display, Error)] pub enum IdError { /// Spotify URI prefix is not `spotify:` or `spotify/` @@ -176,7 +176,7 @@ impl std::str::FromStr for IdBuf { } impl Id { - /// Owned version of the id [`IdBuf`](crate::model::idtypes::IdBuf) + /// Owned version of the id [`IdBuf`](crate::idtypes::IdBuf). pub fn to_owned(&self) -> IdBuf { IdBuf { _type: PhantomData, diff --git a/src/client_creds.rs b/src/client_creds.rs index 86f7bc55..0349520e 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -7,6 +7,12 @@ use crate::{ use maybe_async::maybe_async; +/// The [Client Credentials +/// Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow) +/// client for the Spotify API. +/// +/// Note: This flow does not include authorization and therefore cannot be used +/// to access or to manage endpoints related to user private data. #[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { pub config: Config, @@ -15,7 +21,6 @@ pub struct ClientCredentialsSpotify { pub(in crate) http: HttpClient, } -// This could even use a macro impl BaseClient for ClientCredentialsSpotify { fn get_http(&self) -> &HttpClient { &self.http diff --git a/src/code_auth.rs b/src/code_auth.rs index 8742f03a..8a604ea7 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -11,6 +11,13 @@ use std::collections::HashMap; use maybe_async::maybe_async; use url::Url; +/// The [Authorization Code +/// Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow) +/// client for the Spotify API. +/// +/// This includes user authorization, and thus has access to endpoints related +/// to user private data, unlike the [Client Credentials +/// Flow](crate::ClientCredentialsSpotify) client. #[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { pub creds: Credentials, diff --git a/src/lib.rs b/src/lib.rs index 256ec8a6..d3a42a03 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,7 @@ -//! Rspotify is a wrapper for the [Spotify Web API -//! ](https://developer.spotify.com/web-api/), inspired by [spotipy -//! ](https://github.com/plamere/spotipy). It includes support for all the -//! [authorization flows](https://developer.spotify.com/documentation/general/guides/authorization-guide/), -//! and helper methods for [all available endpoints -//! ](https://developer.spotify.com/documentation/web-api/reference/). +//! Rspotify is a wrapper for the [Spotify Web API](spotify-main), inspired by +//! [spotipy](spotipy). It includes support for all the [authorization +//! flows](spotify-auth-flows), and helper methods for [all available endpoints +//! ](spotify-reference). //! //! ## Configuration //! @@ -13,13 +11,13 @@ //! default TLS, but you can customize both the HTTP client and the TLS with the //! following features: //! -//! - [`reqwest`](https://github.com/seanmonstar/reqwest): enabling +//! - [`reqwest`](reqwest): enabling //! `client-reqwest`, TLS available: //! + `reqwest-default-tls` (reqwest's default) //! + `reqwest-rustls-tls` //! + `reqwest-native-tls` //! + `reqwest-native-tls-vendored` -//! - [`ureq`](https://github.com/algesten/ureq): enabling `client-ureq`, TLS +//! - [`ureq`](ureq): enabling `client-ureq`, TLS //! available: //! + `ureq-rustls-tls` (ureq's default) //! @@ -59,8 +57,7 @@ //! //! Rspotify supports the [`dotenv`] crate, which allows you to save credentials //! in a `.env` file. These will then be automatically available as -//! environmental values when using methods like -//! [`CredentialsBuilder::from_env`](crate::oauth2::CredentialsBuilder::from_env): +//! environmental values when using methods like [`Credentials::from_env`]. //! //! ```toml //! [dependencies] @@ -77,20 +74,29 @@ //! //! ### Authorization //! -//! All endpoints require authorization. You will need to generate a token -//! that indicates that the client has been granted permission to perform -//! requests. You will need to [register your app to get the necessary client -//! credentials](https://developer.spotify.com/dashboard/applications). Read -//! the [official guide for a detailed explanation of the different -//! authorization flows available -//! ](https://developer.spotify.com/documentation/general/guides/authorization-guide/). +//! All endpoints require authorization. You will need to generate a token that +//! indicates that the client has been granted permission to perform requests. +//! You will need to [register your app to get the necessary client +//! credentials](spotify-register-app). Read the [official guide for a detailed +//! explanation of the different authorization flows +//! available](spotify-auth-flows). +//! +//! Rspotify has a different client for each of the available authentication +//! flows. Please refer to their documentation for more info: +//! +//! * [Client Credentials Flow](spotify-client-creds): see +//! [`ClientCredentialsSpotify`]. +//! * [Authorization Code Flow](spotify-auth-code): see [`CodeAuthSpotify`]. +//! * [Authorization Code Flow with Proof Key for Code Exchange +//! (PKCE)](spotify-auth-code-pkce): see [`CodeAuthPkceSpotify`]. +//! * [Implicit Grant Flow](spotify-implicit-grant): unimplemented, as Rspotify +//! has not been tested on a browser yet. If you'd like support for it, let us +//! know in an issue! //! //! The most basic authentication flow, named the [Client Credentials flow -//! ](https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow), -//! consists on requesting a token to Spotify given some client credentials. -//! This can be done with [`Spotify::request_client_token` -//! ](crate::client::Spotify::request_client_token), as seen in -//! [this example +//! ](client-creds), consists on requesting a token to Spotify given some client +//! credentials. This can be done with [`Spotify::request_client_token` +//! ](crate::client::Spotify::request_client_token), as seen in [this example //! ](https://github.com/ramsayleung/rspotify/blob/master/examples/album.rs). //! //! Some of the available endpoints also require access to the user's personal @@ -153,13 +159,22 @@ //! //! ### Examples //! -//! There are some [available examples -//! ](https://github.com/ramsayleung/rspotify/tree/master/examples) -//! which can serve as a learning tool. - -// Disable all modules when both client features are enabled or when none are. -// This way only the compile error below gets shown instead of a whole list of -// confusing errors.. +//! There are some [available examples](examples) which can serve as a learning +//! tool. +//! +//! [spotipy]: https://github.com/plamere/spotipy +//! [reqwest]: https://github.com/seanmonstar/reqwest +//! [ureq]: https://github.com/algesten/ureq +//! [examples]: https://github.com/ramsayleung/rspotify/tree/master/examples +//! [spotify-main]: https://developer.spotify.com/web-api/ +//! [spotify-auth-flows]: https://developer.spotify.com/documentation/general/guides/authorization-guide +//! [spotify-reference]: https://developer.spotify.com/documentation/web-api/reference/ +//! [spotify-register-app]: https://developer.spotify.com/dashboard/applications +//! [spotify]: https://developer.spotify.com/documentation/general/guides/authorization-guide/ +//! [spotify-client-creds]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow +//! [spotify-auth-code]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow +//! [spotify-auth-code-pkce]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce +//! [spotify-implicit-grant]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow pub mod client_creds; pub mod code_auth; From 1651affda7a714753414fc7f18202fb396e0510c Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 00:50:27 +0200 Subject: [PATCH 27/56] add rspotify-http tests to CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 843ed8b2..b4c55b0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -118,4 +118,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: test - args: -p rspotify-macros -p rspotify-model + args: -p rspotify-macros -p rspotify-model -p rspotify-http From 738f9de9ab37595349492f5335494e997a6c7712 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 01:04:39 +0200 Subject: [PATCH 28/56] format and add `with_token` constructor --- rspotify-http/src/lib.rs | 68 +--------------------------------------- src/client_creds.rs | 10 ++++++ src/code_auth.rs | 10 ++++++ src/code_auth_pkce.rs | 18 ++++++++--- src/endpoints/mod.rs | 58 +++++++++++++++++++++++++++++++++- tests/test_oauth2.rs | 6 ++-- tests/test_with_oauth.rs | 4 +-- 7 files changed, 95 insertions(+), 79 deletions(-) diff --git a/rspotify-http/src/lib.rs b/rspotify-http/src/lib.rs index 477ed732..07147101 100644 --- a/rspotify-http/src/lib.rs +++ b/rspotify-http/src/lib.rs @@ -27,7 +27,7 @@ pub use self::ureq::UreqClient as HttpClient; #[cfg(any(feature = "client-reqwest", feature = "client-ureq"))] #[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] -pub use common::{Error, Result, BaseHttpClient, Form, Headers, Query}; +pub use common::{BaseHttpClient, Error, Form, Headers, Query, Result}; #[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] compile_error!( @@ -41,69 +41,3 @@ compile_error!( "You have to enable at least one of the available clients with the \ `client-reqwest` or `client-ureq` features." ); - -#[cfg(test)] -mod test { - use super::*; - use crate::client::SpotifyBuilder; - use crate::oauth2::TokenBuilder; - use crate::scopes; - use chrono::prelude::*; - use chrono::Duration; - - #[test] - fn test_bearer_auth() { - let access_token = "access_token"; - let tok = TokenBuilder::default() - .access_token(access_token) - .build() - .unwrap(); - let (auth, value) = headers::bearer_auth(&tok); - assert_eq!(auth, "authorization"); - assert_eq!(value, "Bearer access_token"); - } - - #[test] - fn test_basic_auth() { - let (auth, value) = headers::basic_auth("ramsay", "123456"); - assert_eq!(auth, "authorization"); - assert_eq!(value, "Basic cmFtc2F5OjEyMzQ1Ng=="); - } - - #[test] - fn test_endpoint_url() { - let spotify = SpotifyBuilder::default().build().unwrap(); - assert_eq!( - spotify.endpoint_url("me/player/play"), - "https://api.spotify.com/v1/me/player/play" - ); - assert_eq!( - spotify.endpoint_url("http://api.spotify.com/v1/me/player/play"), - "http://api.spotify.com/v1/me/player/play" - ); - assert_eq!( - spotify.endpoint_url("https://api.spotify.com/v1/me/player/play"), - "https://api.spotify.com/v1/me/player/play" - ); - } - - #[test] - fn test_auth_headers() { - let tok = TokenBuilder::default() - .access_token("test-access_token") - .expires_in(Duration::seconds(1)) - .expires_at(Utc::now()) - .scope(scopes!("playlist-read-private")) - .refresh_token("...") - .build() - .unwrap(); - - let spotify = SpotifyBuilder::default().token(tok).build().unwrap(); - - let headers = spotify.auth_headers().unwrap(); - assert_eq!( - headers.get("authorization"), - Some(&"Bearer test-access_token".to_owned()) - ); - } -} diff --git a/src/client_creds.rs b/src/client_creds.rs index 0349520e..1b37d891 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -59,6 +59,16 @@ impl ClientCredentialsSpotify { } } + /// Build a new `ClientCredentialsSpotify` from an already generated token. + /// Note that once the token expires this will fail to make requests, as the + /// client credentials aren't known. + pub fn from_token(token: Token) -> Self { + ClientCredentialsSpotify { + token: Some(token), + ..Default::default() + } + } + /// Obtains the client access token for the app without saving it into the /// cache file. The resulting token is saved internally. // TODO: handle with and without cache diff --git a/src/code_auth.rs b/src/code_auth.rs index 8a604ea7..798457cb 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -74,6 +74,16 @@ impl CodeAuthSpotify { } } + /// Build a new `CodeAuthSpotify` from an already generated token. Note that + /// once the token expires this will fail to make requests, as the client + /// credentials aren't known. + pub fn from_token(token: Token) -> Self { + CodeAuthSpotify { + token: Some(token), + ..Default::default() + } + } + /// Gets the required URL to authorize the current client to begin the /// authorization flow. pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 8e00ab16..c27d0ef9 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -15,7 +15,7 @@ pub struct CodeAuthPkceSpotify { pub creds: Credentials, pub oauth: OAuth, pub config: Config, - pub tok: Option, + pub token: Option, pub(in crate) http: HttpClient, } @@ -25,11 +25,11 @@ impl BaseClient for CodeAuthPkceSpotify { } fn get_token(&self) -> Option<&Token> { - self.tok.as_ref() + self.token.as_ref() } fn get_token_mut(&mut self) -> Option<&mut Token> { - self.tok.as_mut() + self.token.as_mut() } fn get_creds(&self) -> &Credentials { @@ -65,8 +65,19 @@ impl CodeAuthPkceSpotify { } } + /// Build a new `CodeAuthPkceSpotify` from an already generated token. Note + /// that once the token expires this will fail to make requests, as the + /// client credentials aren't known. + pub fn from_token(token: Token) -> Self { + CodeAuthPkceSpotify { + token: Some(token), + ..Default::default() + } + } + /// Gets the required URL to authorize the current client to begin the /// authorization flow. + // TODO pub fn get_authorize_url(&self) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); @@ -81,7 +92,6 @@ impl CodeAuthPkceSpotify { payload.insert(headers::REDIRECT_URI, &oauth.redirect_uri); payload.insert(headers::SCOPE, &scope); payload.insert(headers::STATE, &oauth.state); - // TODO // payload.insert(headers::CODE_CHALLENGE, todo!()); // payload.insert(headers::CODE_CHALLENGE_METHOD, "S256"); diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 7b97f4e5..498c1404 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -57,7 +57,9 @@ pub fn basic_auth(user: &str, password: &str) -> (String, String) { #[cfg(test)] mod test { - use super::append_device_id; + use super::*; + use crate::{scopes, ClientCredentialsSpotify, Token}; + use chrono::{prelude::*, Duration}; #[test] fn test_append_device_id_without_question_mark() { @@ -77,4 +79,58 @@ mod test { "me/player/shuffle?state=true&device_id=fdafdsadfa" ); } + + #[test] + fn test_bearer_auth() { + let tok = Token { + access_token: "access_token".to_string(), + ..Default::default() + }; + + let (auth, value) = bearer_auth(&tok); + assert_eq!(auth, "authorization"); + assert_eq!(value, "Bearer access_token"); + } + + #[test] + fn test_basic_auth() { + let (auth, value) = basic_auth("ramsay", "123456"); + assert_eq!(auth, "authorization"); + assert_eq!(value, "Basic cmFtc2F5OjEyMzQ1Ng=="); + } + + #[test] + fn test_endpoint_url() { + let spotify = ClientCredentialsSpotify::default(); + assert_eq!( + spotify.endpoint_url("me/player/play"), + "https://api.spotify.com/v1/me/player/play" + ); + assert_eq!( + spotify.endpoint_url("http://api.spotify.com/v1/me/player/play"), + "http://api.spotify.com/v1/me/player/play" + ); + assert_eq!( + spotify.endpoint_url("https://api.spotify.com/v1/me/player/play"), + "https://api.spotify.com/v1/me/player/play" + ); + } + + #[test] + fn test_auth_headers() { + let tok = Token { + access_token: "test-access_token".to_string(), + expires_in: Duration::seconds(1), + expires_at: Some(Utc::now()), + scope: scopes!("playlist-read-private"), + refresh_token: Some("...".to_string()), + }; + + let spotify = ClientCredentialsSpotify::default(); + let headers = spotify.auth_headers().unwrap(); + assert_eq!( + headers.get("authorization"), + Some(&"Bearer test-access_token".to_owned()) + ); + } } diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index 422dbc67..85172de1 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -56,9 +56,8 @@ async fn test_read_token_cache() { cache_path: PathBuf::from(".test_read_token_cache.json"), ..Default::default() }; - let mut predefined_spotify = ClientCredentialsSpotify::default(); + let mut predefined_spotify = ClientCredentialsSpotify::from_token(tok.clone()); predefined_spotify.config = config.clone(); - predefined_spotify.token = Some(tok.clone()); // write token data to cache_path predefined_spotify.write_token_cache().unwrap(); @@ -95,8 +94,7 @@ fn test_write_token() { cache_path: PathBuf::from(".test_write_token_cache.json"), ..Default::default() }; - let mut spotify = ClientCredentialsSpotify::default(); - spotify.token = Some(tok.clone()); + let mut spotify = ClientCredentialsSpotify::from_token(tok.clone()); spotify.config = config; let tok_str = serde_json::to_string(&tok).unwrap(); diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 9fd1849d..19d84556 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -40,9 +40,7 @@ pub async fn oauth_client() -> CodeAuthSpotify { ..Default::default() }; - let mut client = CodeAuthSpotify::default(); - client.token = Some(tok); - client + CodeAuthSpotify::from_token(tok) } else if let Ok(refresh_token) = env::var("RSPOTIFY_REFRESH_TOKEN") { // The credentials must be available in the environment. Enable // `env-file` in order to read them from an `.env` file. From 98c0fb311490e7fb85784bc73abf545073047141 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 01:48:57 +0200 Subject: [PATCH 29/56] fix most documentation --- rspotify-macros/src/lib.rs | 5 +- src/client_creds.rs | 14 +++-- src/code_auth.rs | 50 ++++++++++++++-- src/code_auth_pkce.rs | 9 +++ src/endpoints/base.rs | 41 +++++++------ src/endpoints/oauth.rs | 34 +++++------ src/lib.rs | 118 ++++++++++--------------------------- 7 files changed, 135 insertions(+), 136 deletions(-) diff --git a/rspotify-macros/src/lib.rs b/rspotify-macros/src/lib.rs index 8b641cce..d7612d7c 100644 --- a/rspotify-macros/src/lib.rs +++ b/rspotify-macros/src/lib.rs @@ -1,6 +1,5 @@ -/// Create a [`HashSet`](std::collections::HashSet) from a list of `&str` (which -/// will be converted to String internally), to easily create scopes for -/// [`Token`](rspotify::Token) or [`OAuth`](rspotify::OAuth). +/// Create a [`HashSet`](std::collections::HashSet) from a list of `&str` to +/// easily create scopes for `Token` or `OAuth`. /// /// Example: /// diff --git a/src/client_creds.rs b/src/client_creds.rs index 1b37d891..076f786b 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -7,12 +7,18 @@ use crate::{ use maybe_async::maybe_async; -/// The [Client Credentials -/// Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow) -/// client for the Spotify API. +/// The [Client Credentials Flow](reference) client for the Spotify API. +/// +/// This is the most basic flow. It requests a token to Spotify given some +/// client credentials, without user authorization. The only step to take is to +/// call [`Self::request_token`]. See [this example](example-main). /// /// Note: This flow does not include authorization and therefore cannot be used -/// to access or to manage endpoints related to user private data. +/// to access or to manage the endpoints related to user private data in +/// [`OAuthClient`](crate::endpoints::OAuthClient). +/// +/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow +/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/client_creds.rs #[derive(Clone, Debug, Default)] pub struct ClientCredentialsSpotify { pub config: Config, diff --git a/src/code_auth.rs b/src/code_auth.rs index 798457cb..c14d805c 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -11,13 +11,55 @@ use std::collections::HashMap; use maybe_async::maybe_async; use url::Url; -/// The [Authorization Code -/// Flow](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow) -/// client for the Spotify API. +/// The [Authorization Code Flow](reference) client for the Spotify API. /// /// This includes user authorization, and thus has access to endpoints related /// to user private data, unlike the [Client Credentials -/// Flow](crate::ClientCredentialsSpotify) client. +/// Flow](crate::ClientCredentialsSpotify) client. See [`BaseClient`] and +/// [`OAuthClient`] for the available endpoints. +/// +/// If you're developing a CLI application, you might be interested in the `cli` +/// feature. This brings the [`Self::prompt_for_token`] utility to automatically +/// follow the flow steps via user interaction. +/// +/// Otherwise, these are the steps to be followed to authenticate your app: +/// +/// 0. Generate a request URL with [`Self::get_authorize_url`]. +/// 1. The user logs in with the request URL. They will be redirected to the +/// given redirect URI, including a code in the URL parameters. This happens +/// on your side. +/// 2. The code obtained in the previous step is parsed with +/// [`Self::parse_response_code`]. +/// 3. The code is sent to Spotify in order to obtain an access token with +/// [`Self::request_token`]. +/// 4. Finally, this access token can be used internally for the requests. +/// It may expire relatively soon, so it can be refreshed with the refresh +/// token (obtained in the previous step as well) using +/// [`Self::refresh_token`]. Otherwise, a new access token may be generated +/// from scratch by repeating these steps, but the advantage of refreshing it +/// is that this doesn't require the user to log in, and that it's a simpler +/// procedure. +/// +/// See [this related example](example-refresh-token) to learn more about +/// refreshing tokens. +/// +/// There's a [webapp example](example-webapp) for more details on how you can +/// implement it for something like a web server, or [this one](example-main) +/// for a CLI use case. +/// +/// An example of the CLI authentication: +/// +/// ![demo](https://raw.githubusercontent.com/ramsayleung/rspotify/master/doc/images/rspotify.gif) +/// +/// Note: even if your script does not have an accessible URL, you will have to +/// specify a redirect URI. It doesn't need to work, you can use +/// `http://localhost:8888/callback` for example, which will also have the code +/// appended like so: `http://localhost/?code=...`. +/// +/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow +/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/code_auth.rs +/// [example-webapp]: https://github.com/ramsayleung/rspotify/tree/master/examples/webapp +/// [example-refresh-token]: https://github.com/ramsayleung/rspotify/blob/master/examples/with_refresh_token.rs #[derive(Clone, Debug, Default)] pub struct CodeAuthSpotify { pub creds: Credentials, diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index c27d0ef9..70f4d4b5 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -10,6 +10,15 @@ use crate::{ use std::collections::HashMap; +/// The [Authorization Code Flow with Proof Key for Code Exchange +/// (PKCE)](reference) client for the Spotify API. +/// +/// This flow is very similar to the regular Authorization Code Flow, so please +/// read [`CodeAuthSpotify`](crate::CodeAuthSpotify) for more information about +/// it. The main difference in this case is that you can avoid storing your +/// client secret by generating a *code verifier* and a *code challenge*. +/// +/// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce #[derive(Clone, Debug, Default)] pub struct CodeAuthPkceSpotify { pub creds: Credentials, diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 8fa8faa0..b0d63d7a 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -252,8 +252,8 @@ where /// - limit - the number of albums to return /// - offset - the index of the first album to return /// - /// See [`Spotify::artist_albums_manual`] for a manually paginated version - /// of this. + /// See [`Self::artist_albums_manual`] for a manually paginated version of + /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-artists-albums) fn artist_albums<'a>( @@ -270,7 +270,7 @@ where ) } - /// The manually paginated version of [`Spotify::artist_albums`]. + /// The manually paginated version of [`Self::artist_albums`]. async fn artist_albums_manual( &self, artist_id: &ArtistId, @@ -404,7 +404,7 @@ where /// - limit - the number of items to return /// - offset - the index of the first item to return /// - /// See [`Spotify::album_track_manual`] for a manually paginated version of + /// See [`Self::album_track_manual`] for a manually paginated version of /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-an-albums-tracks) @@ -418,7 +418,7 @@ where ) } - /// The manually paginated version of [`Spotify::album_track`]. + /// The manually paginated version of [`Self::album_track`]. async fn album_track_manual( &self, album_id: &AlbumId, @@ -525,8 +525,8 @@ where /// - offset: Optional. The index of the first episode to return. Default: 0 (the first object). Use with limit to get the next set of episodes. /// - market: Optional. An ISO 3166-1 alpha-2 country code or the string from_token. /// - /// See [`Spotify::get_shows_episodes_manual`] for a manually paginated - /// version of this. + /// See [`Self::get_shows_episodes_manual`] for a manually paginated version + /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-shows-episodes) fn get_shows_episodes<'a>( @@ -542,7 +542,7 @@ where ) } - /// The manually paginated version of [`Spotify::get_shows_episodes`]. + /// The manually paginated version of [`Self::get_shows_episodes`]. async fn get_shows_episodes_manual( &self, id: &ShowId, @@ -664,7 +664,7 @@ where /// - offset - The index of the first item to return. Default: 0 (the first /// object). Use with limit to get the next set of items. /// - /// See [`Spotify::categories_manual`] for a manually paginated version of + /// See [`Self::categories_manual`] for a manually paginated version of /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-categories) @@ -679,7 +679,7 @@ where ) } - /// The manually paginated version of [`Spotify::categories`]. + /// The manually paginated version of [`Self::categories`]. async fn categories_manual( &self, locale: Option<&str>, @@ -709,8 +709,8 @@ where /// - offset - The index of the first item to return. Default: 0 (the first /// object). Use with limit to get the next set of items. /// - /// See [`Spotify::category_playlists_manual`] for a manually paginated - /// version of this. + /// See [`Self::category_playlists_manual`] for a manually paginated version + /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-categories-playlists) fn category_playlists<'a>( @@ -726,7 +726,7 @@ where ) } - /// The manually paginated version of [`Spotify::category_playlists`]. + /// The manually paginated version of [`Self::category_playlists`]. async fn category_playlists_manual( &self, category_id: &str, @@ -798,7 +798,7 @@ where /// - offset - The index of the first item to return. Default: 0 (the first /// object). Use with limit to get the next set of items. /// - /// See [`Spotify::new_releases_manual`] for a manually paginated version of + /// See [`Self::new_releases_manual`] for a manually paginated version of /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-new-releases) @@ -812,7 +812,7 @@ where ) } - /// The manually paginated version of [`Spotify::new_releases`]. + /// The manually paginated version of [`Self::new_releases`]. async fn new_releases_manual( &self, country: Option<&Market>, @@ -919,8 +919,7 @@ where /// - offset - the index of the first track to return /// - market - an ISO 3166-1 alpha-2 country code or the string from_token. /// - /// See [`Spotify::playlist_tracks`] for a manually paginated version of - /// this. + /// See [`Self::playlist_tracks`] for a manually paginated version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-playlists-tracks) fn playlist_tracks<'a>( @@ -937,7 +936,7 @@ where ) } - /// The manually paginated version of [`Spotify::playlist_tracks`]. + /// The manually paginated version of [`Self::playlist_tracks`]. async fn playlist_tracks_manual( &self, playlist_id: &PlaylistId, @@ -967,8 +966,8 @@ where /// - limit - the number of items to return /// - offset - the index of the first item to return /// - /// See [`Spotify::user_playlists_manual`] for a manually paginated version - /// of this. + /// See [`Self::user_playlists_manual`] for a manually paginated version of + /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) fn user_playlists<'a>( @@ -981,7 +980,7 @@ where ) } - /// The manually paginated version of [`Spotify::user_playlists`]. + /// The manually paginated version of [`Self::user_playlists`]. async fn user_playlists_manual( &self, user_id: &UserId, diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 39de1ab7..a1d196c3 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -77,7 +77,7 @@ pub trait OAuthClient: BaseClient { /// - limit - the number of items to return /// - offset - the index of the first item to return /// - /// See [`Spotify::current_user_playlists_manual`] for a manually paginated + /// See [`Self::current_user_playlists_manual`] for a manually paginated /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-a-list-of-current-users-playlists) @@ -88,7 +88,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of [`Spotify::current_user_playlists`]. + /// The manually paginated version of [`Self::current_user_playlists`]. async fn current_user_playlists_manual( &self, limit: Option, @@ -463,8 +463,8 @@ pub trait OAuthClient: BaseClient { /// - offset - the index of the first album to return /// - market - Provide this parameter if you want to apply Track Relinking. /// - /// See [`Spotify::current_user_saved_albums`] for a manually paginated - /// version of this. + /// See [`Self::current_user_saved_albums`] for a manually paginated version + /// of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-albums) fn current_user_saved_albums(&self) -> Paginator<'_, ClientResult> { @@ -474,8 +474,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of - /// [`Spotify::current_user_saved_albums`]. + /// The manually paginated version of [`Self::current_user_saved_albums`]. async fn current_user_saved_albums_manual( &self, limit: Option, @@ -500,8 +499,8 @@ pub trait OAuthClient: BaseClient { /// - offset - the index of the first track to return /// - market - Provide this parameter if you want to apply Track Relinking. /// - /// See [`Spotify::current_user_saved_tracks_manual`] for a manually - /// paginated version of this. + /// See [`Self::current_user_saved_tracks_manual`] for a manually paginated + /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-tracks) fn current_user_saved_tracks(&self) -> Paginator<'_, ClientResult> { @@ -511,8 +510,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of - /// [`Spotify::current_user_saved_tracks`]. + /// The manually paginated version of [`Self::current_user_saved_tracks`]. async fn current_user_saved_tracks_manual( &self, limit: Option, @@ -607,8 +605,8 @@ pub trait OAuthClient: BaseClient { /// - offset - the index of the first entity to return /// - time_range - Over what time frame are the affinities computed /// - /// See [`Spotify::current_user_top_artists_manual`] for a manually - /// paginated version of this. + /// See [`Self::current_user_top_artists_manual`] for a manually paginated + /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) fn current_user_top_artists<'a>( @@ -623,7 +621,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of [`Spotify::current_user_top_artists`]. + /// The manually paginated version of [`Self::current_user_top_artists`]. async fn current_user_top_artists_manual( &self, time_range: Option<&TimeRange>, @@ -649,7 +647,7 @@ pub trait OAuthClient: BaseClient { /// - offset - the index of the first entity to return /// - time_range - Over what time frame are the affinities computed /// - /// See [`Spotify::current_user_top_tracks_manual`] for a manually paginated + /// See [`Self::current_user_top_tracks_manual`] for a manually paginated /// version of this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-top-artists-and-tracks) @@ -665,7 +663,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of [`Spotify::current_user_top_tracks`]. + /// The manually paginated version of [`Self::current_user_top_tracks`]. async fn current_user_top_tracks_manual( &self, time_range: Option<&TimeRange>, @@ -1147,8 +1145,8 @@ pub trait OAuthClient: BaseClient { /// - offset(Optional). The index of the first show to return. Default: 0 /// (the first object). Use with limit to get the next set of shows. /// - /// See [`Spotify::get_saved_show_manual`] for a manually paginated version - /// of this. + /// See [`Self::get_saved_show_manual`] for a manually paginated version of + /// this. /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-users-saved-shows) fn get_saved_show(&self) -> Paginator<'_, ClientResult> { @@ -1158,7 +1156,7 @@ pub trait OAuthClient: BaseClient { ) } - /// The manually paginated version of [`Spotify::get_saved_show`]. + /// The manually paginated version of [`Self::get_saved_show`]. async fn get_saved_show_manual( &self, limit: Option, diff --git a/src/lib.rs b/src/lib.rs index d3a42a03..7aaa3005 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ //! Rspotify is a wrapper for the [Spotify Web API](spotify-main), inspired by -//! [spotipy](spotipy). It includes support for all the [authorization +//! [spotipy](spotipy-github). It includes support for all the [authorization //! flows](spotify-auth-flows), and helper methods for [all available endpoints //! ](spotify-reference). //! @@ -7,17 +7,17 @@ //! //! ### HTTP Client //! -//! By default, Rspotify uses the [`reqwest`] asynchronous HTTP client with its -//! default TLS, but you can customize both the HTTP client and the TLS with the -//! following features: +//! By default, Rspotify uses the [reqwest](reqwest-docs) asynchronous HTTP +//! client with its default TLS, but you can customize both the HTTP client and +//! the TLS with the following features: //! -//! - [`reqwest`](reqwest): enabling +//! - [reqwest](reqwest-docs): enabling //! `client-reqwest`, TLS available: //! + `reqwest-default-tls` (reqwest's default) //! + `reqwest-rustls-tls` //! + `reqwest-native-tls` //! + `reqwest-native-tls-vendored` -//! - [`ureq`](ureq): enabling `client-ureq`, TLS +//! - [ureq](ureq-docs): enabling `client-ureq`, TLS //! available: //! + `ureq-rustls-tls` (ureq's default) //! @@ -49,9 +49,9 @@ //! //! ### Proxies //! -//! [`reqwest`](reqwest#proxies) supports system proxies by default. It reads -//! the environment variables `HTTP_PROXY` and `HTTPS_PROXY` environmental -//! variables to set HTTP and HTTPS proxies, respectively. +//! [reqwest supports system proxies by default](reqwest-proxies). It reads the +//! environment variables `HTTP_PROXY` and `HTTPS_PROXY` environmental variables +//! to set HTTP and HTTPS proxies, respectively. //! //! ### Environmental variables //! @@ -64,25 +64,28 @@ //! rspotify = { version = "...", features = ["env-file"] } //! ``` //! -//! ### Cli utilities +//! ### CLI utilities //! //! Rspotify includes basic support for Cli apps to obtain access tokens by -//! prompting the user, after enabling the `cli` feature. See the [Authorization -//! ](#authorization) section for more information. +//! prompting the user, after enabling the `cli` feature. See the +//! [Authorization](#authorization) section for more information. //! //! ## Getting Started //! //! ### Authorization //! -//! All endpoints require authorization. You will need to generate a token that -//! indicates that the client has been granted permission to perform requests. -//! You will need to [register your app to get the necessary client +//! All endpoints require app authorization; you will need to generate a token +//! that indicates that the client has been granted permission to perform +//! requests. You can start by [registering your app to get the necessary client //! credentials](spotify-register-app). Read the [official guide for a detailed //! explanation of the different authorization flows //! available](spotify-auth-flows). //! //! Rspotify has a different client for each of the available authentication -//! flows. Please refer to their documentation for more info: +//! flows. They may implement the endpoints in +//! [`BaseClient`](crate::endpoints::BaseClient) or +//! [`OAuthClient`](crate::endpoints::OAuthClient) according to what kind of +//! flow it is. Please refer to their documentation for more details: //! //! * [Client Credentials Flow](spotify-client-creds): see //! [`ClientCredentialsSpotify`]. @@ -93,79 +96,22 @@ //! has not been tested on a browser yet. If you'd like support for it, let us //! know in an issue! //! -//! The most basic authentication flow, named the [Client Credentials flow -//! ](client-creds), consists on requesting a token to Spotify given some client -//! credentials. This can be done with [`Spotify::request_client_token` -//! ](crate::client::Spotify::request_client_token), as seen in [this example -//! ](https://github.com/ramsayleung/rspotify/blob/master/examples/album.rs). -//! -//! Some of the available endpoints also require access to the user's personal -//! information, meaning that you have to follow the [Authorization Flow -//! ](https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow) -//! instead. In a nutshell, these are the steps you need to make for this: -//! -//! 0. Generate a request URL with [`Spotify::get_authorize_url` -//! ](crate::client::Spotify::get_authorize_url). -//! 1. The user logs in with the request URL, which redirects to the redirect -//! URI and provides a code in the parameters. This happens on your side. -//! 2. The code obtained in the previous step is parsed with -//! [`Spotify::parse_response_code` -//! ](crate::client::Spotify::parse_response_code). -//! 3. The code is sent to Spotify in order to obtain an access token with -//! [`Spotify::request_user_token` -//! ](crate::client::Spotify::request_user_token) or -//! [`Spotify::request_user_token_without_cache` -//! ](crate::client::Spotify::prompt_for_user_token_without_cache). -//! 4. Finally, this access token can be used internally for the requests. -//! This access token may expire relatively soon, so it can be refreshed -//! with the refresh token (obtained in the third step as well) using -//! [`Spotify::refresh_user_token` -//! ](crate::client::Spotify::refresh_user_token) or -//! [`Spotify::refresh_user_token_without_cache` -//! ](crate::client::Spotify::refresh_user_token_without_cache). -//! Otherwise, a new access token may be generated from scratch by repeating -//! these steps, but the advantage of refreshing it is that this doesn't -//! require the user to log in, and that it's a simpler procedure. -//! -//! See the [`webapp` -//! ](https://github.com/ramsayleung/rspotify/tree/master/examples/webapp) -//! example for more details on how you can implement it for something like a -//! web server. -//! -//! If you're developing a Cli application, you might be interested in the -//! `cli` feature, which brings the [`Spotify::prompt_for_user_token` -//! ](crate::client::Spotify::prompt_for_user_token) and -//! [`Spotify::prompt_for_user_token_without_cache` -//! ](crate::client::Spotify::prompt_for_user_token_without_cache) -//! methods. These will run all the authentication steps. The user wil log in -//! by opening the request URL in its default browser, and the requests will be -//! performed automatically. -//! -//! An example of the Cli authentication: -//! -//! ![demo](https://raw.githubusercontent.com/ramsayleung/rspotify/master/doc/images/rspotify.gif) -//! -//! Note: even if your script does not have an accessible URL, you will have to -//! specify a redirect URI. It doesn't need to work or be accessible, you can -//! use `http://localhost:8888/callback` for example, which will also have the -//! code appended like so: `http://localhost/?code=...`. -//! //! In order to help other developers to get used to `rspotify`, there are //! public credentials available for a dummy account. You can test `rspotify` -//! with this account's `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET` -//! inside the [`.env` file -//! ](https://github.com/ramsayleung/rspotify/blob/master/.env) for more -//! details. +//! with this account's `RSPOTIFY_CLIENT_ID` and `RSPOTIFY_CLIENT_SECRET` inside +//! the [`.env` file](https://github.com/ramsayleung/rspotify/blob/master/.env) +//! for more details. //! //! ### Examples //! -//! There are some [available examples](examples) which can serve as a learning -//! tool. +//! There are some [available examples on the GitHub +//! repository](examples-github) which can serve as a learning tool. //! -//! [spotipy]: https://github.com/plamere/spotipy -//! [reqwest]: https://github.com/seanmonstar/reqwest -//! [ureq]: https://github.com/algesten/ureq -//! [examples]: https://github.com/ramsayleung/rspotify/tree/master/examples +//! [spotipy-github]: https://github.com/plamere/spotipy +//! [reqwest-docs]: https://docs.rs/reqwest/ +//! [reqwest-proxies]: https://docs.rs/reqwest/#proxies +//! [ureq-docs]: https://docs.rs/ureq/ +//! [examples-github]: https://github.com/ramsayleung/rspotify/tree/master/examples //! [spotify-main]: https://developer.spotify.com/web-api/ //! [spotify-auth-flows]: https://developer.spotify.com/documentation/general/guides/authorization-guide //! [spotify-reference]: https://developer.spotify.com/documentation/web-api/reference/ @@ -273,9 +219,9 @@ pub struct Config { pub cache_path: PathBuf, /// The pagination chunk size used when performing automatically paginated - /// requests, like [`Spotify::artist_albums`]. This means that a request - /// will be performed every `pagination_chunks` items. By default this is - /// [`DEFAULT_PAGINATION_CHUNKS`]. + /// requests, like [`artist_albums`](crate::endpoints::BaseClient). This + /// means that a request will be performed every `pagination_chunks` items. + /// By default this is [`DEFAULT_PAGINATION_CHUNKS`]. /// /// Note that most endpoints set a maximum to the number of items per /// request, which most times is 50. From 2cadd88fdff552d0b6c6b855fc3cc30aaf174b29 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 01:53:22 +0200 Subject: [PATCH 30/56] mention pkce example --- src/code_auth_pkce.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 70f4d4b5..af221416 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -18,7 +18,11 @@ use std::collections::HashMap; /// it. The main difference in this case is that you can avoid storing your /// client secret by generating a *code verifier* and a *code challenge*. /// +/// There's an [example](example-main) available to learn how to use this +/// client. +/// /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce +/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/code_auth.rs #[derive(Clone, Debug, Default)] pub struct CodeAuthPkceSpotify { pub creds: Credentials, From 1eb5edf30be1417bacab151f741cebdd19ad4c4d Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Tue, 4 May 2021 01:54:45 +0200 Subject: [PATCH 31/56] fix tests --- src/endpoints/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index 498c1404..aecb03d8 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -126,7 +126,7 @@ mod test { refresh_token: Some("...".to_string()), }; - let spotify = ClientCredentialsSpotify::default(); + let spotify = ClientCredentialsSpotify::from_token(tok); let headers = spotify.auth_headers().unwrap(); assert_eq!( headers.get("authorization"), From d318b7f18507890fbb773e22f0d63c74d67c5921 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 20:39:52 +0200 Subject: [PATCH 32/56] handling more cached cases --- src/client_creds.rs | 7 +++---- src/code_auth.rs | 14 ++++++++------ src/endpoints/base.rs | 22 ++++++++++++++++++---- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/client_creds.rs b/src/client_creds.rs index 076f786b..07468b2d 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -75,9 +75,8 @@ impl ClientCredentialsSpotify { } } - /// Obtains the client access token for the app without saving it into the - /// cache file. The resulting token is saved internally. - // TODO: handle with and without cache + /// Obtains the client access token for the app. The resulting token is + /// saved internally. #[maybe_async] pub async fn request_token(&mut self) -> ClientResult<()> { let mut data = Form::new(); @@ -85,6 +84,6 @@ impl ClientCredentialsSpotify { self.token = Some(self.fetch_access_token(&data).await?); - Ok(()) + self.write_token_cache() } } diff --git a/src/code_auth.rs b/src/code_auth.rs index c14d805c..8d5e13f8 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -173,6 +173,10 @@ impl CodeAuthSpotify { self.token = Some(self.fetch_access_token(&data).await?); + if self.config.token_cached { + self.write_token_cache()?; + } + Ok(()) } @@ -199,15 +203,13 @@ impl CodeAuthSpotify { /// case it didn't exist/was invalid. /// /// Note: this method requires the `cli` feature. - // TODO: handle with and without cache #[cfg(feature = "cli")] #[maybe_async] pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { - match self.read_oauth_token_cache().await { + match self.read_token_cache().await { // TODO: shouldn't this also refresh the obtained token? - Some(mut new_token) => { - let mut cur_token = self.get_token_mut(); - cur_token.replace(&mut new_token); + Some(new_token) => { + self.token.replace(new_token); } // Otherwise following the usual procedure to get the token. None => { @@ -217,6 +219,6 @@ impl CodeAuthSpotify { } } - Ok(()) + self.write_token_cache() } } diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index b0d63d7a..cf943874 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -143,7 +143,15 @@ where } /// Updates the cache file at the internal cache path. + /// + /// This should be used whenever it's possible to, even if the cached token + /// isn't configured, because this will already check `Config::token_cached` + /// and do nothing in that case already. fn write_token_cache(&self) -> ClientResult<()> { + if !self.get_config().token_cached { + return Ok(()); + } + if let Some(tok) = self.get_token().as_ref() { tok.write_cache(&self.get_config().cache_path)?; } @@ -152,15 +160,21 @@ where } /// Tries to read the cache file's token, which may not exist. - async fn read_token_cache(&mut self) -> Option { - let tok = Token::from_cache(&self.get_config().cache_path)?; + /// + /// Similarly to [`Self::write_token_cache`], this will already check if the + /// cached token is enabled and return `None` in case it isn't. + async fn read_token_cache(&self) -> Option { + if !self.get_config().token_cached { + return None; + } - if tok.is_expired() { + let token = Token::from_cache(&self.get_config().cache_path)?; + if token.is_expired() { // Invalid token, since it doesn't have at least the currently // required scopes or it's expired. None } else { - Some(tok) + Some(token) } } From decd8c6aaeb0b98574565c295293e909aad73c17 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 20:56:53 +0200 Subject: [PATCH 33/56] add comments to the code auth implementation --- src/code_auth.rs | 71 ++++++++++++++++++++++++------------------------ 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/code_auth.rs b/src/code_auth.rs index 8d5e13f8..9796c831 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -69,6 +69,7 @@ pub struct CodeAuthSpotify { pub(in crate) http: HttpClient, } +/// This client has access to the base methods. impl BaseClient for CodeAuthSpotify { fn get_http(&self) -> &HttpClient { &self.http @@ -91,14 +92,18 @@ impl BaseClient for CodeAuthSpotify { } } -// This could also be a macro (less important) +/// This client includes user authorization, so it has access to the user +/// private endpoints in [`OAuthClient`]. impl OAuthClient for CodeAuthSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } } +/// Some client-specific implementations specific to the authorization flow. impl CodeAuthSpotify { + /// Builds a new [`CodeAuthSpotify`] given a pair of client credentials and + /// OAuth information. pub fn new(creds: Credentials, oauth: OAuth) -> Self { CodeAuthSpotify { creds, @@ -107,27 +112,29 @@ impl CodeAuthSpotify { } } - pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { + /// Build a new [`CodeAuthSpotify`] from an already generated token. Note + /// that once the token expires this will fail to make requests, as the + /// client credentials aren't known. + pub fn from_token(token: Token) -> Self { CodeAuthSpotify { - creds, - oauth, - config, + token: Some(token), ..Default::default() } } - /// Build a new `CodeAuthSpotify` from an already generated token. Note that - /// once the token expires this will fail to make requests, as the client - /// credentials aren't known. - pub fn from_token(token: Token) -> Self { + /// Same as [`Self::new`] but with an extra parameter to configure the + /// client. + pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { CodeAuthSpotify { - token: Some(token), + creds, + oauth, + config, ..Default::default() } } - /// Gets the required URL to authorize the current client to begin the - /// authorization flow. + /// Returns the URL needed to authorize the current client as the first step + /// in the authorization flow. pub fn get_authorize_url(&self, show_dialog: bool) -> ClientResult { let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); @@ -151,10 +158,8 @@ impl CodeAuthSpotify { Ok(parsed.into_string()) } - /// Obtains the user access token for the app with the given code without - /// saving it into the cache file, as part of the OAuth authentication. - /// The access token will be saved inside the Spotify instance. - // TODO: implement with and without cache. + /// Obtains a user access token given a code, as part of the OAuth + /// authentication. The access token will be saved internally. #[maybe_async] pub async fn request_token(&mut self, code: &str) -> ClientResult<()> { let mut data = Form::new(); @@ -171,36 +176,34 @@ impl CodeAuthSpotify { data.insert(headers::SCOPE, scopes.as_ref()); data.insert(headers::STATE, oauth.state.as_ref()); - self.token = Some(self.fetch_access_token(&data).await?); - - if self.config.token_cached { - self.write_token_cache()?; - } + let token = self.fetch_access_token(&data).await?; + self.token = Some(token); - Ok(()) + self.write_token_cache() } - /// Refreshes the access token with the refresh token provided by the - /// without saving it into the cache file. + /// Refreshes the current access token given a refresh token. /// /// The obtained token will be saved internally. - // TODO: implement with and without cache #[maybe_async] pub async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { let mut data = Form::new(); data.insert(headers::REFRESH_TOKEN, refresh_token); data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - let mut tok = self.fetch_access_token(&data).await?; - tok.refresh_token = Some(refresh_token.to_string()); - self.token = Some(tok); + let mut token = self.fetch_access_token(&data).await?; + token.refresh_token = Some(refresh_token.to_string()); + self.token = Some(token); - Ok(()) + self.write_token_cache() } - /// The same as the `prompt_for_user_token_without_cache` method, but it - /// will try to use the user token into the cache file, and save it in - /// case it didn't exist/was invalid. + /// Opens up the authorization URL in the user's browser so that it can + /// authenticate. It also reads from the standard input the redirect URI + /// in order to obtain the access token information. The resulting access + /// token will be saved internally once the operation is successful. + /// + /// The authorizaton URL can be obtained with [`Self::get_authorize_url`]. /// /// Note: this method requires the `cli` feature. #[cfg(feature = "cli")] @@ -208,9 +211,7 @@ impl CodeAuthSpotify { pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { match self.read_token_cache().await { // TODO: shouldn't this also refresh the obtained token? - Some(new_token) => { - self.token.replace(new_token); - } + Some(new_token) => self.token.replace(new_token), // Otherwise following the usual procedure to get the token. None => { let code = self.get_code_from_user(url)?; From 6603b1cc61799a8b821020c24ff08dd25326456a Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 21:02:17 +0200 Subject: [PATCH 34/56] comments for the PKCE client as well --- src/code_auth_pkce.rs | 107 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index af221416..be8f0714 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -1,15 +1,16 @@ -use url::Url; - use crate::{ auth_urls, endpoints::{BaseClient, OAuthClient}, headers, - http::HttpClient, + http::{HttpClient, Form}, ClientResult, Config, Credentials, OAuth, Token, }; use std::collections::HashMap; +use maybe_async::maybe_async; +use url::Url; + /// The [Authorization Code Flow with Proof Key for Code Exchange /// (PKCE)](reference) client for the Spotify API. /// @@ -32,6 +33,7 @@ pub struct CodeAuthPkceSpotify { pub(in crate) http: HttpClient, } +/// This client has access to the base methods. impl BaseClient for CodeAuthPkceSpotify { fn get_http(&self) -> &HttpClient { &self.http @@ -54,13 +56,18 @@ impl BaseClient for CodeAuthPkceSpotify { } } +/// This client includes user authorization, so it has access to the user +/// private endpoints in [`OAuthClient`]. impl OAuthClient for CodeAuthPkceSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } } +/// Some client-specific implementations specific to the authorization flow. impl CodeAuthPkceSpotify { + /// Builds a new [`CodeAuthPkceSpotify`] given a pair of client credentials + /// and OAuth information. pub fn new(creds: Credentials, oauth: OAuth) -> Self { CodeAuthPkceSpotify { creds, @@ -69,29 +76,31 @@ impl CodeAuthPkceSpotify { } } - pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { + /// Build a new [`CodeAuthPkceSpotify`] from an already generated token. + /// Note that once the token expires this will fail to make requests, as the + /// client credentials aren't known. + pub fn from_token(token: Token) -> Self { CodeAuthPkceSpotify { - creds, - oauth, - config, + token: Some(token), ..Default::default() } } - /// Build a new `CodeAuthPkceSpotify` from an already generated token. Note - /// that once the token expires this will fail to make requests, as the - /// client credentials aren't known. - pub fn from_token(token: Token) -> Self { + /// Same as [`Self::new`] but with an extra parameter to configure the + /// client. + pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { CodeAuthPkceSpotify { - token: Some(token), + creds, + oauth, + config, ..Default::default() } } - /// Gets the required URL to authorize the current client to begin the - /// authorization flow. - // TODO + /// Returns the URL needed to authorize the current client as the first step + /// in the authorization flow. pub fn get_authorize_url(&self) -> ClientResult { + // TODO let mut payload: HashMap<&str, &str> = HashMap::new(); let oauth = self.get_oauth(); let scope = oauth @@ -111,4 +120,72 @@ impl CodeAuthPkceSpotify { let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; Ok(parsed.into_string()) } + + /// Obtains a user access token given a code, as part of the OAuth + /// authentication. The access token will be saved internally. + #[maybe_async] + pub async fn request_token(&mut self, code: &str) -> ClientResult<()> { + // TODO + let mut data = Form::new(); + let oauth = self.get_oauth(); + let scopes = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); + data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); + data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); + data.insert(headers::CODE, code); + data.insert(headers::SCOPE, scopes.as_ref()); + data.insert(headers::STATE, oauth.state.as_ref()); + + let token = self.fetch_access_token(&data).await?; + self.token = Some(token); + + self.write_token_cache() + } + + /// Refreshes the current access token given a refresh token. + /// + /// The obtained token will be saved internally. + #[maybe_async] + pub async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { + // TODO + let mut data = Form::new(); + data.insert(headers::REFRESH_TOKEN, refresh_token); + data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); + + let mut token = self.fetch_access_token(&data).await?; + token.refresh_token = Some(refresh_token.to_string()); + self.token = Some(token); + + self.write_token_cache() + } + + /// Opens up the authorization URL in the user's browser so that it can + /// authenticate. It also reads from the standard input the redirect URI + /// in order to obtain the access token information. The resulting access + /// token will be saved internally once the operation is successful. + /// + /// The authorizaton URL can be obtained with [`Self::get_authorize_url`]. + /// + /// Note: this method requires the `cli` feature. + #[cfg(feature = "cli")] + #[maybe_async] + pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { + // TODO: this should go in OAuthClient + match self.read_token_cache().await { + // TODO: shouldn't this also refresh the obtained token? + Some(new_token) => self.token.replace(new_token), + // Otherwise following the usual procedure to get the token. + None => { + let code = self.get_code_from_user(url)?; + // Will write to the cache file if successful + self.request_token(&code).await?; + } + } + + self.write_token_cache() + } } From b9835e8a0d405fc0aff9506a3da5ad68632b8a3a Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 21:07:11 +0200 Subject: [PATCH 35/56] comments for client credentials --- src/client_creds.rs | 24 +++++++++++++++--------- src/code_auth_pkce.rs | 2 +- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/client_creds.rs b/src/client_creds.rs index 07468b2d..ff939d0f 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -27,6 +27,7 @@ pub struct ClientCredentialsSpotify { pub(in crate) http: HttpClient, } +/// This client has access to the base methods. impl BaseClient for ClientCredentialsSpotify { fn get_http(&self) -> &HttpClient { &self.http @@ -49,7 +50,10 @@ impl BaseClient for ClientCredentialsSpotify { } } +/// Some client-specific implementations specific to the authorization flow. impl ClientCredentialsSpotify { + /// Builds a new [`ClientCredentialsSpotify`] given a pair of client + /// credentials and OAuth information. pub fn new(creds: Credentials) -> Self { ClientCredentialsSpotify { creds, @@ -57,25 +61,27 @@ impl ClientCredentialsSpotify { } } - pub fn with_config(creds: Credentials, config: Config) -> Self { + /// Build a new [`ClientCredentialsSpotify`] from an already generated + /// token. Note that once the token expires this will fail to make requests, + /// as the client credentials aren't known. + pub fn from_token(token: Token) -> Self { ClientCredentialsSpotify { - config, - creds, + token: Some(token), ..Default::default() } } - /// Build a new `ClientCredentialsSpotify` from an already generated token. - /// Note that once the token expires this will fail to make requests, as the - /// client credentials aren't known. - pub fn from_token(token: Token) -> Self { + /// Same as [`Self::new`] but with an extra parameter to configure the + /// client. + pub fn with_config(creds: Credentials, config: Config) -> Self { ClientCredentialsSpotify { - token: Some(token), + config, + creds, ..Default::default() } } - /// Obtains the client access token for the app. The resulting token is + /// Obtains the client access token for the app. The resulting token will be /// saved internally. #[maybe_async] pub async fn request_token(&mut self) -> ClientResult<()> { diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index be8f0714..94c01780 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -2,7 +2,7 @@ use crate::{ auth_urls, endpoints::{BaseClient, OAuthClient}, headers, - http::{HttpClient, Form}, + http::{Form, HttpClient}, ClientResult, Config, Credentials, OAuth, Token, }; From 03fb05baf98152c5ec1f68a53a0e3d4e4b562873 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 21:50:51 +0200 Subject: [PATCH 36/56] move oauth utilities to the trait --- src/client_creds.rs | 1 - src/code_auth.rs | 106 +++++++++++++++------------------------- src/code_auth_pkce.rs | 107 ++++++++++++++--------------------------- src/endpoints/oauth.rs | 33 +++++++++++++ 4 files changed, 109 insertions(+), 138 deletions(-) diff --git a/src/client_creds.rs b/src/client_creds.rs index ff939d0f..9beee15a 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -50,7 +50,6 @@ impl BaseClient for ClientCredentialsSpotify { } } -/// Some client-specific implementations specific to the authorization flow. impl ClientCredentialsSpotify { /// Builds a new [`ClientCredentialsSpotify`] given a pair of client /// credentials and OAuth information. diff --git a/src/code_auth.rs b/src/code_auth.rs index 9796c831..5b6df221 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -70,6 +70,7 @@ pub struct CodeAuthSpotify { } /// This client has access to the base methods. +#[maybe_async(?Send)] impl BaseClient for CodeAuthSpotify { fn get_http(&self) -> &HttpClient { &self.http @@ -94,13 +95,51 @@ impl BaseClient for CodeAuthSpotify { /// This client includes user authorization, so it has access to the user /// private endpoints in [`OAuthClient`]. +#[maybe_async(?Send)] impl OAuthClient for CodeAuthSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } + + /// Obtains a user access token given a code, as part of the OAuth + /// authentication. The access token will be saved internally. + async fn request_token(&mut self, code: &str) -> ClientResult<()> { + let mut data = Form::new(); + let oauth = self.get_oauth(); + let scopes = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); + data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); + data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); + data.insert(headers::CODE, code); + data.insert(headers::SCOPE, scopes.as_ref()); + data.insert(headers::STATE, oauth.state.as_ref()); + + let token = self.fetch_access_token(&data).await?; + self.token = Some(token); + + self.write_token_cache() + } + + /// Refreshes the current access token given a refresh token. + /// + /// The obtained token will be saved internally. + async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { + let mut data = Form::new(); + data.insert(headers::REFRESH_TOKEN, refresh_token); + data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); + + let mut token = self.fetch_access_token(&data).await?; + token.refresh_token = Some(refresh_token.to_string()); + self.token = Some(token); + + self.write_token_cache() + } } -/// Some client-specific implementations specific to the authorization flow. impl CodeAuthSpotify { /// Builds a new [`CodeAuthSpotify`] given a pair of client credentials and /// OAuth information. @@ -157,69 +196,4 @@ impl CodeAuthSpotify { let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; Ok(parsed.into_string()) } - - /// Obtains a user access token given a code, as part of the OAuth - /// authentication. The access token will be saved internally. - #[maybe_async] - pub async fn request_token(&mut self, code: &str) -> ClientResult<()> { - let mut data = Form::new(); - let oauth = self.get_oauth(); - let scopes = oauth - .scope - .clone() - .into_iter() - .collect::>() - .join(" "); - data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); - data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); - data.insert(headers::CODE, code); - data.insert(headers::SCOPE, scopes.as_ref()); - data.insert(headers::STATE, oauth.state.as_ref()); - - let token = self.fetch_access_token(&data).await?; - self.token = Some(token); - - self.write_token_cache() - } - - /// Refreshes the current access token given a refresh token. - /// - /// The obtained token will be saved internally. - #[maybe_async] - pub async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { - let mut data = Form::new(); - data.insert(headers::REFRESH_TOKEN, refresh_token); - data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - - let mut token = self.fetch_access_token(&data).await?; - token.refresh_token = Some(refresh_token.to_string()); - self.token = Some(token); - - self.write_token_cache() - } - - /// Opens up the authorization URL in the user's browser so that it can - /// authenticate. It also reads from the standard input the redirect URI - /// in order to obtain the access token information. The resulting access - /// token will be saved internally once the operation is successful. - /// - /// The authorizaton URL can be obtained with [`Self::get_authorize_url`]. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - #[maybe_async] - pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { - match self.read_token_cache().await { - // TODO: shouldn't this also refresh the obtained token? - Some(new_token) => self.token.replace(new_token), - // Otherwise following the usual procedure to get the token. - None => { - let code = self.get_code_from_user(url)?; - // Will write to the cache file if successful - self.request_token(&code).await?; - } - } - - self.write_token_cache() - } } diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index 94c01780..bca43937 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -58,13 +58,48 @@ impl BaseClient for CodeAuthPkceSpotify { /// This client includes user authorization, so it has access to the user /// private endpoints in [`OAuthClient`]. +#[maybe_async(?Send)] impl OAuthClient for CodeAuthPkceSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } + + async fn request_token(&mut self, code: &str) -> ClientResult<()> { + // TODO + let mut data = Form::new(); + let oauth = self.get_oauth(); + let scopes = oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "); + data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); + data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); + data.insert(headers::CODE, code); + data.insert(headers::SCOPE, scopes.as_ref()); + data.insert(headers::STATE, oauth.state.as_ref()); + + let token = self.fetch_access_token(&data).await?; + self.token = Some(token); + + self.write_token_cache() + } + + async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { + // TODO + let mut data = Form::new(); + data.insert(headers::REFRESH_TOKEN, refresh_token); + data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); + + let mut token = self.fetch_access_token(&data).await?; + token.refresh_token = Some(refresh_token.to_string()); + self.token = Some(token); + + self.write_token_cache() + } } -/// Some client-specific implementations specific to the authorization flow. impl CodeAuthPkceSpotify { /// Builds a new [`CodeAuthPkceSpotify`] given a pair of client credentials /// and OAuth information. @@ -97,8 +132,6 @@ impl CodeAuthPkceSpotify { } } - /// Returns the URL needed to authorize the current client as the first step - /// in the authorization flow. pub fn get_authorize_url(&self) -> ClientResult { // TODO let mut payload: HashMap<&str, &str> = HashMap::new(); @@ -120,72 +153,4 @@ impl CodeAuthPkceSpotify { let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; Ok(parsed.into_string()) } - - /// Obtains a user access token given a code, as part of the OAuth - /// authentication. The access token will be saved internally. - #[maybe_async] - pub async fn request_token(&mut self, code: &str) -> ClientResult<()> { - // TODO - let mut data = Form::new(); - let oauth = self.get_oauth(); - let scopes = oauth - .scope - .clone() - .into_iter() - .collect::>() - .join(" "); - data.insert(headers::GRANT_TYPE, headers::GRANT_AUTH_CODE); - data.insert(headers::REDIRECT_URI, oauth.redirect_uri.as_ref()); - data.insert(headers::CODE, code); - data.insert(headers::SCOPE, scopes.as_ref()); - data.insert(headers::STATE, oauth.state.as_ref()); - - let token = self.fetch_access_token(&data).await?; - self.token = Some(token); - - self.write_token_cache() - } - - /// Refreshes the current access token given a refresh token. - /// - /// The obtained token will be saved internally. - #[maybe_async] - pub async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()> { - // TODO - let mut data = Form::new(); - data.insert(headers::REFRESH_TOKEN, refresh_token); - data.insert(headers::GRANT_TYPE, headers::GRANT_REFRESH_TOKEN); - - let mut token = self.fetch_access_token(&data).await?; - token.refresh_token = Some(refresh_token.to_string()); - self.token = Some(token); - - self.write_token_cache() - } - - /// Opens up the authorization URL in the user's browser so that it can - /// authenticate. It also reads from the standard input the redirect URI - /// in order to obtain the access token information. The resulting access - /// token will be saved internally once the operation is successful. - /// - /// The authorizaton URL can be obtained with [`Self::get_authorize_url`]. - /// - /// Note: this method requires the `cli` feature. - #[cfg(feature = "cli")] - #[maybe_async] - pub async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { - // TODO: this should go in OAuthClient - match self.read_token_cache().await { - // TODO: shouldn't this also refresh the obtained token? - Some(new_token) => self.token.replace(new_token), - // Otherwise following the usual procedure to get the token. - None => { - let code = self.get_code_from_user(url)?; - // Will write to the cache file if successful - self.request_token(&code).await?; - } - } - - self.write_token_cache() - } } diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index a1d196c3..8fd90ed8 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -22,6 +22,15 @@ use url::Url; pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; + /// Obtains a user access token given a code, as part of the OAuth + /// authentication. The access token will be saved internally. + async fn request_token(&mut self, code: &str) -> ClientResult<()>; + + /// Refreshes the current access token given a refresh token. + /// + /// The obtained token will be saved internally. + async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()>; + /// Tries to read the cache file's token, which may not exist. async fn read_oauth_token_cache(&mut self) -> Option { let tok = Token::from_cache(&self.get_config().cache_path)?; @@ -71,6 +80,30 @@ pub trait OAuthClient: BaseClient { Some(url.to_string()) } + /// Opens up the authorization URL in the user's browser so that it can + /// authenticate. It also reads from the standard input the redirect URI + /// in order to obtain the access token information. The resulting access + /// token will be saved internally once the operation is successful. + /// + /// Note: this method requires the `cli` feature. + #[cfg(feature = "cli")] + #[maybe_async] + async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { + // TODO: this should go in OAuthClient + match self.read_token_cache().await { + // TODO: shouldn't this also refresh the obtained token? + Some(new_token) => self.token.replace(new_token), + // Otherwise following the usual procedure to get the token. + None => { + let code = self.get_code_from_user(url)?; + // Will write to the cache file if successful + self.request_token(&code).await?; + } + } + + self.write_token_cache() + } + /// Get current user playlists without required getting his profile. /// /// Parameters: From 060cd72b366d0a79ce7e4b45f3e0c181af536d4a Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 21:52:34 +0200 Subject: [PATCH 37/56] fix comment --- src/code_auth_pkce.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index bca43937..b707810b 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -132,6 +132,8 @@ impl CodeAuthPkceSpotify { } } + /// Returns the URL needed to authorize the current client as the first step + /// in the authorization flow. pub fn get_authorize_url(&self) -> ClientResult { // TODO let mut payload: HashMap<&str, &str> = HashMap::new(); From 3c94d37259f2b1fb73eada797388fa81d00d066e Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 22:32:25 +0200 Subject: [PATCH 38/56] comments for the traits --- src/endpoints/base.rs | 58 ++++++++++++++++++++++++++++ src/endpoints/oauth.rs | 87 ++++++++++-------------------------------- 2 files changed, 78 insertions(+), 67 deletions(-) diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index cf943874..26e5065b 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -16,6 +16,9 @@ use chrono::Utc; use maybe_async::maybe_async; use serde_json::{Map, Value}; +/// This trait implements the basic endpoints from the Spotify API that may be +/// accessed without user authorization, including parts of the authentication +/// flow that are shared, and the endpoints. #[maybe_async(?Send)] pub trait BaseClient where @@ -486,6 +489,61 @@ where convert_result(&result) } + /// Gets playlist of a user. + /// + /// Parameters: + /// - user_id - the id of the user + /// - playlist_id - the id of the playlist + /// - fields - which fields to return + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) + async fn user_playlist( + &self, + user_id: &UserId, + playlist_id: Option<&PlaylistId>, + fields: Option<&str>, + ) -> ClientResult { + let params = build_map! { + optional "fields": fields, + }; + + let url = match playlist_id { + Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), + None => format!("users/{}/starred", user_id.id()), + }; + let result = self.endpoint_get(&url, ¶ms).await?; + convert_result(&result) + } + + /// Check to see if the given users are following the given playlist. + /// + /// Parameters: + /// - playlist_id - the id of the playlist + /// - user_ids - the ids of the users that you want to + /// check to see if they follow the playlist. Maximum: 5 ids. + /// + /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) + async fn playlist_check_follow( + &self, + playlist_id: &PlaylistId, + user_ids: &[&UserId], + ) -> ClientResult> { + if user_ids.len() > 5 { + error!("The maximum length of user ids is limited to 5 :-)"); + } + let url = format!( + "playlists/{}/followers/contains?ids={}", + playlist_id.id(), + user_ids + .iter() + .map(|id| id.id()) + .collect::>() + .join(","), + ); + let result = self.endpoint_get(&url, &Query::new()).await?; + convert_result(&result) + } + /// Get Spotify catalog information for a single show identified by its unique Spotify ID. /// /// Path Parameters: diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 8fd90ed8..f3210ffe 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -18,6 +18,15 @@ use rspotify_model::idtypes::PlayContextIdType; use serde_json::{json, Map}; use url::Url; +/// This trait implements the methods available strictly to clients with user +/// authorization, including some parts of the authentication flow that are +/// shared, and the endpoints. +/// +/// Note that the base trait [`BaseClient`](crate::endpoints::BaseClient) may +/// have endpoints that conditionally require authorization like +/// [`user_playlist`](crate::endpoints::BaseClient::user_playlist). This trait +/// only separates endpoints that *always* need authorization from the base +/// ones. #[maybe_async(?Send)] pub trait OAuthClient: BaseClient { fn get_oauth(&self) -> &OAuth; @@ -26,9 +35,8 @@ pub trait OAuthClient: BaseClient { /// authentication. The access token will be saved internally. async fn request_token(&mut self, code: &str) -> ClientResult<()>; - /// Refreshes the current access token given a refresh token. - /// - /// The obtained token will be saved internally. + /// Refreshes the current access token given a refresh token. The obtained + /// token will be saved internally. async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()>; /// Tries to read the cache file's token, which may not exist. @@ -44,6 +52,15 @@ pub trait OAuthClient: BaseClient { } } + /// Parse the response code in the given response url. If the URL cannot be + /// parsed or the `code` parameter is not present, this will return `None`. + fn parse_response_code(&self, url: &str) -> Option { + let url = Url::parse(url).ok()?; + let mut params = url.query_pairs(); + let (_, url) = params.find(|(key, _)| key == "code")?; + Some(url.to_string()) + } + /// Tries to open the authorization URL in the user's browser, and returns /// the obtained code. /// @@ -71,15 +88,6 @@ pub trait OAuthClient: BaseClient { Ok(code) } - /// Parse the response code in the given response url. If the URL cannot be - /// parsed or the `code` parameter is not present, this will return `None`. - fn parse_response_code(&self, url: &str) -> Option { - let url = Url::parse(url).ok()?; - let mut params = url.query_pairs(); - let (_, url) = params.find(|(key, _)| key == "code")?; - Some(url.to_string()) - } - /// Opens up the authorization URL in the user's browser so that it can /// authenticate. It also reads from the standard input the redirect URI /// in order to obtain the access token information. The resulting access @@ -138,32 +146,6 @@ pub trait OAuthClient: BaseClient { convert_result(&result) } - /// Gets playlist of a user. - /// - /// Parameters: - /// - user_id - the id of the user - /// - playlist_id - the id of the playlist - /// - fields - which fields to return - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-get-list-users-playlists) - async fn user_playlist( - &self, - user_id: &UserId, - playlist_id: Option<&PlaylistId>, - fields: Option<&str>, - ) -> ClientResult { - let params = build_map! { - optional "fields": fields, - }; - - let url = match playlist_id { - Some(playlist_id) => format!("users/{}/playlists/{}", user_id.id(), playlist_id.id()), - None => format!("users/{}/starred", user_id.id()), - }; - let result = self.endpoint_get(&url, ¶ms).await?; - convert_result(&result) - } - /// Creates a playlist for a user. /// /// Parameters: @@ -428,35 +410,6 @@ pub trait OAuthClient: BaseClient { Ok(()) } - /// Check to see if the given users are following the given playlist. - /// - /// Parameters: - /// - playlist_id - the id of the playlist - /// - user_ids - the ids of the users that you want to - /// check to see if they follow the playlist. Maximum: 5 ids. - /// - /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#endpoint-check-if-user-follows-playlist) - async fn playlist_check_follow( - &self, - playlist_id: &PlaylistId, - user_ids: &[&UserId], - ) -> ClientResult> { - if user_ids.len() > 5 { - error!("The maximum length of user ids is limited to 5 :-)"); - } - let url = format!( - "playlists/{}/followers/contains?ids={}", - playlist_id.id(), - user_ids - .iter() - .map(|id| id.id()) - .collect::>() - .join(","), - ); - let result = self.endpoint_get(&url, &Query::new()).await?; - convert_result(&result) - } - /// Get detailed profile information about the current user. /// An alias for the 'current_user' method. /// From 01e172c2b356b354add28a9982a528fba7832aad Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sat, 8 May 2021 22:36:51 +0200 Subject: [PATCH 39/56] remove TODO --- src/endpoints/oauth.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index f3210ffe..16a09ade 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -97,7 +97,6 @@ pub trait OAuthClient: BaseClient { #[cfg(feature = "cli")] #[maybe_async] async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { - // TODO: this should go in OAuthClient match self.read_token_cache().await { // TODO: shouldn't this also refresh the obtained token? Some(new_token) => self.token.replace(new_token), From f16e148a9868645328db25987b9ebe1feaa71842 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 00:38:35 +0200 Subject: [PATCH 40/56] fix webapp example --- examples/webapp/src/main.rs | 149 +++++++++++++++++------------------- src/endpoints/base.rs | 3 +- 2 files changed, 73 insertions(+), 79 deletions(-) diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index 79553de8..8fcb5c0f 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -1,7 +1,7 @@ //! In this example, the token is saved into a cache file. If you are building a //! real-world web app, you should store it in a database instead. In that case -//! you can use `Spotify::request_user_token_without_cache` and -//! `Spotify::refresh_user_token_without_cache` to avoid creating cache files. +//! you can disable `token_cached` in the `Config` struct passed to the client +//! when initializing it to avoid using cache files. #![feature(proc_macro_hygiene, decl_macro)] @@ -14,13 +14,11 @@ use rocket::response::Redirect; use rocket_contrib::json; use rocket_contrib::json::JsonValue; use rocket_contrib::templates::Template; -use rspotify::client::{ClientError, SpotifyBuilder}; -use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; -use rspotify::scopes; +use rspotify::{scopes, CodeAuthSpotify, OAuth, Credentials, Config, prelude::*, Token}; use std::fs; use std::{ - collections::{HashMap, HashSet}, + collections::HashMap, env, path::PathBuf, }; @@ -47,9 +45,18 @@ fn generate_random_uuid(length: usize) -> String { .collect() } +fn get_cache_path(cookies: &Cookies) -> PathBuf { + let project_dir_path = env::current_dir().unwrap(); + let mut cache_path = project_dir_path; + cache_path.push(CACHE_PATH); + cache_path.push(cookies.get("uuid").unwrap().value()); + + cache_path +} + fn create_cache_path_if_absent(cookies: &Cookies) -> PathBuf { - let (exist, cache_path) = check_cache_path_exists(cookies); - if !exist { + let cache_path = get_cache_path(cookies); + if !cache_path.exists() { let mut path = cache_path.clone(); path.pop(); fs::create_dir_all(path).unwrap(); @@ -58,54 +65,49 @@ fn create_cache_path_if_absent(cookies: &Cookies) -> PathBuf { } fn remove_cache_path(mut cookies: Cookies) { - let (exist, cache_path) = check_cache_path_exists(&cookies); - if exist { + let cache_path = get_cache_path(&cookies); + if cache_path.exists() { fs::remove_file(cache_path).unwrap() } cookies.remove(Cookie::named("uuid")) } -fn check_cache_path_exists(cookies: &Cookies) -> (bool, PathBuf) { - let project_dir_path = env::current_dir().unwrap(); - let mut cache_path = project_dir_path; - cache_path.push(CACHE_PATH); - cache_path.push(cookies.get("uuid").unwrap().value()); - (cache_path.exists(), cache_path) +fn check_cache_path_exists(cookies: &Cookies) -> bool { + let cache_path = get_cache_path(cookies); + cache_path.exists() } -fn init_spotify() -> SpotifyBuilder { +fn init_spotify(cookies: &Cookies) -> CodeAuthSpotify { + let config = Config { + token_cached: true, + cache_path: create_cache_path_if_absent(cookies), + ..Default::default() + }; + // Please notice that protocol of redirect_uri, make sure it's http // (or https). It will fail if you mix them up. - let scope = scopes!("user-read-currently-playing", "playlist-modify-private"); - let oauth = OAuthBuilder::default() - .redirect_uri("http://localhost:8000/callback") - .scope(scope) - .build() - .unwrap(); + let oauth = OAuth { + scope: scopes!("user-read-currently-playing", "playlist-modify-private"), + redirect_uri: "http://localhost:8000/callback".to_owned(), + ..Default::default() + }; // Replacing client_id and client_secret with yours. - let creds = CredentialsBuilder::default() - .id("e1dce60f1e274e20861ce5d96142a4d3") - .secret("0e4e03b9be8d465d87fc32857a4b5aa3") - .build() - .unwrap(); - - SpotifyBuilder::default() - .credentials(creds) - .oauth(oauth) - .clone() + let creds = Credentials::new( + "e1dce60f1e274e20861ce5d96142a4d3", + "0e4e03b9be8d465d87fc32857a4b5aa3" + ); + + CodeAuthSpotify::with_config(creds, oauth, config) } #[get("/callback?")] fn callback(cookies: Cookies, code: String) -> AppResponse { - let mut spotify = init_spotify(); - let mut spotify = spotify - .cache_path(create_cache_path_if_absent(&cookies)) - .build() - .unwrap(); - return match spotify.request_user_token(code.as_str()) { + let mut spotify = init_spotify(&cookies); + + match spotify.request_token(&code) { Ok(_) => { - println!("request user token successful"); + println!("Request user token successful"); AppResponse::Redirect(Redirect::to("/")) } Err(err) => { @@ -114,33 +116,28 @@ fn callback(cookies: Cookies, code: String) -> AppResponse { context.insert("err_msg", "Failed to get token!"); AppResponse::Template(Template::render("error", context)) } - }; + } } #[get("/")] fn index(mut cookies: Cookies) -> AppResponse { - let mut spotify_builder = init_spotify(); - let check_exists = |c| { - let (exists, _) = check_cache_path_exists(c); - exists - }; + let mut context = HashMap::new(); // The user is authenticated if their cookie is set and a cache exists for // them. - let authenticated = cookies.get("uuid").is_some() && check_exists(&cookies); - let spotify = if authenticated { - let (_, cache_path) = check_cache_path_exists(&cookies); - let token = TokenBuilder::from_cache(cache_path).build().unwrap(); - spotify_builder.token(token).build().unwrap() - } else { + let authenticated = cookies.get("uuid").is_some() && check_cache_path_exists(&cookies); + if !authenticated { cookies.add(Cookie::new("uuid", generate_random_uuid(64))); - spotify_builder - .cache_path(create_cache_path_if_absent(&cookies)) - .build() - .unwrap() - }; - let mut context = HashMap::new(); + let spotify = init_spotify(&cookies); + let auth_url = spotify.get_authorize_url(true).unwrap(); + context.insert("auth_url", auth_url); + return AppResponse::Template(Template::render("authorize", context)); + } + + let cache_path = get_cache_path(&cookies); + let token = Token::from_cache(cache_path).unwrap(); + let spotify = CodeAuthSpotify::from_token(token); match spotify.me() { Ok(user_info) => { context.insert( @@ -151,14 +148,7 @@ fn index(mut cookies: Cookies) -> AppResponse { ); AppResponse::Template(Template::render("index", context.clone())) } - Err(ClientError::InvalidAuth(msg)) => { - println!("InvalidAuth msg {:?}", msg); - let auth_url = spotify.get_authorize_url(true).unwrap(); - context.insert("auth_url", auth_url); - AppResponse::Template(Template::render("authorize", context)) - } Err(err) => { - let mut context = HashMap::new(); context.insert("err_msg", format!("Failed for {}!", err)); AppResponse::Template(Template::render("error", context)) } @@ -173,30 +163,33 @@ fn sign_out(cookies: Cookies) -> AppResponse { #[get("/playlists")] fn playlist(cookies: Cookies) -> AppResponse { - let mut spotify = init_spotify(); - let (exist, cache_path) = check_cache_path_exists(&cookies); - if !exist { + let mut spotify = init_spotify(&cookies); + if !spotify.config.cache_path.exists() { return AppResponse::Redirect(Redirect::to("/")); } - let token = TokenBuilder::from_cache(cache_path).build().unwrap(); - let spotify = spotify.token(token).build().unwrap(); - match spotify.current_user_playlists(Some(20), Some(0)) { - Ok(playlists) => AppResponse::Json(json!(playlists)), - Err(_) => AppResponse::Redirect(Redirect::to("/")), + let token = spotify.read_oauth_token_cache().unwrap(); + spotify.token = Some(token); + let playlists = spotify.current_user_playlists() + .take(50) + .filter_map(Result::ok) + .collect::>(); + + if playlists.is_empty() { + return AppResponse::Redirect(Redirect::to("/")); } + + AppResponse::Json(json!(playlists)) } #[get("/me")] fn me(cookies: Cookies) -> AppResponse { - let mut spotify = init_spotify(); - let (exist, cache_path) = check_cache_path_exists(&cookies); - if !exist { + let mut spotify = init_spotify(&cookies); + if !spotify.config.cache_path.exists() { return AppResponse::Redirect(Redirect::to("/")); } - let token = TokenBuilder::from_cache(cache_path).build().unwrap(); - let spotify = spotify.token(token).build().unwrap(); + spotify.token = Some(spotify.read_oauth_token_cache().unwrap()); match spotify.me() { Ok(user_info) => AppResponse::Json(json!(user_info)), Err(_) => AppResponse::Redirect(Redirect::to("/")), diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 26e5065b..463f4c6e 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -162,6 +162,7 @@ where Ok(()) } + // TODO: remove, this is confusing with `read_oauth_token_cache`. /// Tries to read the cache file's token, which may not exist. /// /// Similarly to [`Self::write_token_cache`], this will already check if the @@ -529,7 +530,7 @@ where user_ids: &[&UserId], ) -> ClientResult> { if user_ids.len() > 5 { - error!("The maximum length of user ids is limited to 5 :-)"); + log::error!("The maximum length of user ids is limited to 5 :-)"); } let url = format!( "playlists/{}/followers/contains?ids={}", From 1e96b5b2b00f3e1a20be5e0d95114ade6e40f6d2 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 00:46:09 +0200 Subject: [PATCH 41/56] move confusing read_token_cache method --- examples/webapp/src/main.rs | 4 ++-- src/client_creds.rs | 20 ++++++++++++++++++++ src/endpoints/base.rs | 20 -------------------- src/endpoints/oauth.rs | 2 +- 4 files changed, 23 insertions(+), 23 deletions(-) diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index 8fcb5c0f..30af4a4a 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -168,7 +168,7 @@ fn playlist(cookies: Cookies) -> AppResponse { return AppResponse::Redirect(Redirect::to("/")); } - let token = spotify.read_oauth_token_cache().unwrap(); + let token = spotify.read_token_cache().unwrap(); spotify.token = Some(token); let playlists = spotify.current_user_playlists() .take(50) @@ -189,7 +189,7 @@ fn me(cookies: Cookies) -> AppResponse { return AppResponse::Redirect(Redirect::to("/")); } - spotify.token = Some(spotify.read_oauth_token_cache().unwrap()); + spotify.token = Some(spotify.read_token_cache().unwrap()); match spotify.me() { Ok(user_info) => AppResponse::Json(json!(user_info)), Err(_) => AppResponse::Redirect(Redirect::to("/")), diff --git a/src/client_creds.rs b/src/client_creds.rs index 9beee15a..319d3f4d 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -80,6 +80,26 @@ impl ClientCredentialsSpotify { } } + /// Tries to read the cache file's token, which may not exist. + /// + /// Similarly to [`Self::write_token_cache`], this will already check if the + /// cached token is enabled and return `None` in case it isn't. + #[maybe_async] + pub async fn read_token_cache(&self) -> Option { + if !self.get_config().token_cached { + return None; + } + + let token = Token::from_cache(&self.get_config().cache_path)?; + if token.is_expired() { + // Invalid token, since it doesn't have at least the currently + // required scopes or it's expired. + None + } else { + Some(token) + } + } + /// Obtains the client access token for the app. The resulting token will be /// saved internally. #[maybe_async] diff --git a/src/endpoints/base.rs b/src/endpoints/base.rs index 463f4c6e..e8953a9f 100644 --- a/src/endpoints/base.rs +++ b/src/endpoints/base.rs @@ -162,26 +162,6 @@ where Ok(()) } - // TODO: remove, this is confusing with `read_oauth_token_cache`. - /// Tries to read the cache file's token, which may not exist. - /// - /// Similarly to [`Self::write_token_cache`], this will already check if the - /// cached token is enabled and return `None` in case it isn't. - async fn read_token_cache(&self) -> Option { - if !self.get_config().token_cached { - return None; - } - - let token = Token::from_cache(&self.get_config().cache_path)?; - if token.is_expired() { - // Invalid token, since it doesn't have at least the currently - // required scopes or it's expired. - None - } else { - Some(token) - } - } - /// Sends a request to Spotify for an access token. async fn fetch_access_token(&self, payload: &Form<'_>) -> ClientResult { // This request uses a specific content type, and the client ID/secret diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 16a09ade..4885112a 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -40,7 +40,7 @@ pub trait OAuthClient: BaseClient { async fn refresh_token(&mut self, refresh_token: &str) -> ClientResult<()>; /// Tries to read the cache file's token, which may not exist. - async fn read_oauth_token_cache(&mut self) -> Option { + async fn read_token_cache(&mut self) -> Option { let tok = Token::from_cache(&self.get_config().cache_path)?; if !self.get_oauth().scope.is_subset(&tok.scope) || tok.is_expired() { From 599cea87000aefb1082855c25d21e0b6eaf1c94a Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 00:56:10 +0200 Subject: [PATCH 42/56] fix cli compilation --- src/endpoints/oauth.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/endpoints/oauth.rs b/src/endpoints/oauth.rs index 4885112a..2a1cb466 100644 --- a/src/endpoints/oauth.rs +++ b/src/endpoints/oauth.rs @@ -99,7 +99,9 @@ pub trait OAuthClient: BaseClient { async fn prompt_for_token(&mut self, url: &str) -> ClientResult<()> { match self.read_token_cache().await { // TODO: shouldn't this also refresh the obtained token? - Some(new_token) => self.token.replace(new_token), + Some(ref mut new_token) => { + self.get_token_mut().replace(new_token); + } // Otherwise following the usual procedure to get the token. None => { let code = self.get_code_from_user(url)?; From 3409e60502e59555b0c70490ea8cb853c840188b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 01:14:29 +0200 Subject: [PATCH 43/56] bump url to fix warning --- Cargo.toml | 2 +- rspotify-http/Cargo.toml | 2 +- src/code_auth.rs | 2 +- src/code_auth_pkce.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 99cb47a9..5845fe74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,7 +42,7 @@ maybe-async = "0.2.1" serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0.57" thiserror = "1.0.20" -url = "2.1.1" +url = "2.2.2" webbrowser = { version = "0.5.5", optional = true } ### Auth ### diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml index 465a2d41..fa2d684d 100644 --- a/rspotify-http/Cargo.toml +++ b/rspotify-http/Cargo.toml @@ -24,7 +24,7 @@ reqwest = { version = "0.11.0", default-features = false, features = ["json", "s serde_json = "1.0.57" thiserror = "1.0.20" ureq = { version = "2.0", default-features = false, features = ["json", "cookies"], optional = true } -url = "2.1.1" +url = "2.2.2" [dev-dependencies] env_logger = "0.8.1" diff --git a/src/code_auth.rs b/src/code_auth.rs index 5b6df221..faad0ba5 100644 --- a/src/code_auth.rs +++ b/src/code_auth.rs @@ -194,6 +194,6 @@ impl CodeAuthSpotify { } let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; - Ok(parsed.into_string()) + Ok(parsed.into()) } } diff --git a/src/code_auth_pkce.rs b/src/code_auth_pkce.rs index b707810b..765674dc 100644 --- a/src/code_auth_pkce.rs +++ b/src/code_auth_pkce.rs @@ -153,6 +153,6 @@ impl CodeAuthPkceSpotify { // payload.insert(headers::CODE_CHALLENGE_METHOD, "S256"); let parsed = Url::parse_with_params(auth_urls::AUTHORIZE, payload)?; - Ok(parsed.into_string()) + Ok(parsed.into()) } } From fdafad887ef049d1a45b92addc2d495553c74dc9 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 01:19:42 +0200 Subject: [PATCH 44/56] fix tests --- tests/test_oauth2.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index 85172de1..4821393c 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -53,6 +53,7 @@ async fn test_read_token_cache() { }; let config = Config { + token_cached: true, cache_path: PathBuf::from(".test_read_token_cache.json"), ..Default::default() }; @@ -91,6 +92,7 @@ fn test_write_token() { }; let config = Config { + token_cached: true, cache_path: PathBuf::from(".test_write_token_cache.json"), ..Default::default() }; From 46993434dab174cf187213a30cc09fbb185c73f4 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 01:50:48 +0200 Subject: [PATCH 45/56] fix CI --- .github/workflows/ci.yml | 36 +++++++----------------------------- .travis.yml | 25 ------------------------- 2 files changed, 7 insertions(+), 54 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c55b0e..68b528c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,8 +43,8 @@ jobs: - arm-unknown-linux-gnueabihf - armv7-unknown-linux-gnueabihf client: - - client-ureq,ureq-rustls-tls - - client-reqwest,reqwest-rustls-tls + - rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-rustls-tls,rspotify-http/client-ureq,rspotify-http/ureq-rustls-tls + - rspotify/cli,rspotify/env-file,rspotify/client-reqwest,rspotify/reqwest-rustls-tls,rspotify-http/client-reqwest,rspotify-http/reqwest-rustls-tls steps: - name: Checkout sources uses: actions/checkout@v2 @@ -64,7 +64,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: build - args: --workspace --target ${{ matrix.target }} --no-default-features --features=cli,env-file,${{ matrix.client }} + args: -p rspotify -p rspotify-http -p rspotify-model -p rspotify-macros --no-default-features --features=${{ matrix.client }} --target ${{ matrix.target }} test-client: name: Test and Lint for each Client @@ -72,8 +72,8 @@ jobs: strategy: matrix: client: - - client-ureq,ureq-rustls-tls - - client-reqwest,reqwest-rustls-tls + - rspotify/cli,rspotify/env-file,rspotify/client-ureq,rspotify/ureq-rustls-tls,rspotify-http/client-ureq,rspotify-http/ureq-rustls-tls + - rspotify/cli,rspotify/env-file,rspotify/client-reqwest,rspotify/reqwest-rustls-tls,rspotify-http/client-reqwest,rspotify-http/reqwest-rustls-tls steps: - name: Checkout sources uses: actions/checkout@v2 @@ -90,32 +90,10 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --workspace --no-default-features --features=cli,env-file,${{ matrix.client }} -- -D warnings + args: -p rspotify -p rspotify-http -p rspotify-model -p rspotify-macros --no-default-features --features=${{ matrix.client }} -- -D warnings - name: Run cargo test uses: actions-rs/cargo@v1 with: command: test - args: --no-default-features --features=env-file,${{ matrix.client }} - - # The rest of the crates don't need to be tested with multiple feature - # combinations. - test-crates: - name: Simple Tests for Crates - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v2 - - - name: Install stable toolchain - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - - name: Run cargo test - uses: actions-rs/cargo@v1 - with: - command: test - args: -p rspotify-macros -p rspotify-model -p rspotify-http + args: -p rspotify -p rspotify-http -p rspotify-model -p rspotify-macros --no-default-features --features=${{ matrix.client }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e3a90395..00000000 --- a/.travis.yml +++ /dev/null @@ -1,25 +0,0 @@ -language: rust -rust: - - stable - - beta - - nightly -matrix: - allow_failures: - - rust: nightly - -script: - - cargo test - - cargo run --example artists_albums - - cargo run --example track - - cargo run --example artist_related_artists - - cargo run --example tracks - - cargo run --example artist_top_tracks - - cargo run --example user - - cargo run --example artists - - cargo run --example albums - - cargo run --example audios_features - - cargo run --example audio_analysis - - cargo run --example album_tracks - - cargo run --example audio_features - - cargo run --example artist - - cargo run --example album From 2c93222d7bcabf1bf0539fb3c445174538d72726 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 02:07:54 +0200 Subject: [PATCH 46/56] remove cyclic dependency --- rspotify-macros/Cargo.toml | 1 - rspotify-macros/src/lib.rs | 31 ++++++++++++++----------------- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/rspotify-macros/Cargo.toml b/rspotify-macros/Cargo.toml index 47c854fe..87a265b1 100644 --- a/rspotify-macros/Cargo.toml +++ b/rspotify-macros/Cargo.toml @@ -13,4 +13,3 @@ edition = "2018" [dev-dependencies] serde_json = "1.0.57" chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } -rspotify = { path = "..", version = "0.10.0" } diff --git a/rspotify-macros/src/lib.rs b/rspotify-macros/src/lib.rs index d7612d7c..069759f3 100644 --- a/rspotify-macros/src/lib.rs +++ b/rspotify-macros/src/lib.rs @@ -4,18 +4,14 @@ /// Example: /// /// ``` -/// use rspotify::{Token, scopes}; +/// use rspotify_macros::scopes; /// use std::collections::HashSet; -/// use chrono::{Duration, prelude::*}; /// -/// let scope = scopes!("playlist-read-private", "playlist-read-collaborative"); -/// let tok = Token { -/// scope, -/// access_token: "test-access_token".to_owned(), -/// expires_in: Duration::seconds(1), -/// expires_at: Some(Utc::now().to_owned()), -/// refresh_token: Some("...".to_owned()), -/// }; +/// let with_macro = scopes!("playlist-read-private", "playlist-read-collaborative"); +/// let mut manually = HashSet::new(); +/// manually.insert("playlist-read-private".to_owned()); +/// manually.insert("playlist-read-collaborative".to_owned()); +/// assert_eq!(with_macro, manually); /// ``` #[macro_export] macro_rules! scopes { @@ -134,7 +130,6 @@ macro_rules! build_json { #[cfg(test)] mod test { use crate::{build_json, build_map, scopes}; - use rspotify::model::Market; use serde_json::{json, Map, Value}; use std::collections::HashMap; @@ -153,23 +148,25 @@ mod test { // Passed as parameters, for example. let id = "Pink Lemonade"; let artist = Some("The Wombats"); - let market: Option<&Market> = None; + let market: Option = None; + let market_str = market.clone().map(|x| x.to_string()); let with_macro = build_map! { // Mandatory (not an `Option`) "id": id, // Can be used directly optional "artist": artist, // `Modality` needs to be converted to &str - optional "market": market.map(|x| x.as_ref()), + optional "market": market_str.as_deref(), }; let mut manually = HashMap::<&str, &str>::with_capacity(3); manually.insert("id", id); + let market_str = market.map(|x| x.to_string()); if let Some(val) = artist { manually.insert("artist", val); } - if let Some(val) = market.map(|x| x.as_ref()) { + if let Some(val) = market_str.as_deref() { manually.insert("market", val); } @@ -181,12 +178,12 @@ mod test { // Passed as parameters, for example. let id = "Pink Lemonade"; let artist = Some("The Wombats"); - let market: Option<&Market> = None; + let market: Option = None; let with_macro = build_json! { "id": id, optional "artist": artist, - optional "market": market.map(|x| x.as_ref()), + optional "market": market.map(|x| x.to_string()), }; let mut manually = Map::with_capacity(3); @@ -194,7 +191,7 @@ mod test { if let Some(val) = artist.map(|x| json!(x)) { manually.insert("artist".to_string(), val); } - if let Some(val) = market.map(|x| x.as_ref()).map(|x| json!(x)) { + if let Some(val) = market.map(|x| x.to_string()).map(|x| json!(x)) { manually.insert("market".to_string(), val); } From e6ed136c83b0fd21e2575b5b9fa5adb79ed26690 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Sun, 9 May 2021 02:13:11 +0200 Subject: [PATCH 47/56] fix last CI issue --- examples/code_auth_pkce.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/code_auth_pkce.rs b/examples/code_auth_pkce.rs index 90b8977f..a49c8a03 100644 --- a/examples/code_auth_pkce.rs +++ b/examples/code_auth_pkce.rs @@ -32,8 +32,7 @@ async fn main() { // ..Default::default(), // }; // ``` - let oauth = OAuth::from_env().unwrap(); - oauth.scope = scopes!("user-read-recently-played"); + let oauth = OAuth::from_env(scopes!("user-read-recently-played")).unwrap(); let mut spotify = CodeAuthPkceSpotify::new(creds, oauth); @@ -42,7 +41,7 @@ async fn main() { spotify.prompt_for_token(&url).await.unwrap(); // Running the requests - let history = spotify.current_playback(None, None).await; + let history = spotify.current_playback(None, None::>).await; println!("Response: {:?}", history); } From b45b0f3597086b611522bedcf1be4d6bd9d27563 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:42:08 +0200 Subject: [PATCH 48/56] add myself as an author! --- Cargo.toml | 5 ++++- rspotify-http/Cargo.toml | 5 ++++- rspotify-macros/Cargo.toml | 5 ++++- rspotify-model/Cargo.toml | 5 ++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5845fe74..2610baf8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,8 @@ [package] -authors = ["Ramsay Leung "] +authors = [ + "Ramsay Leung ", + "Mario Ortiz Manero " +] name = "rspotify" version = "0.10.0" license = "MIT" diff --git a/rspotify-http/Cargo.toml b/rspotify-http/Cargo.toml index fa2d684d..dafb313a 100644 --- a/rspotify-http/Cargo.toml +++ b/rspotify-http/Cargo.toml @@ -1,5 +1,8 @@ [package] -authors = ["Ramsay Leung "] +authors = [ + "Ramsay Leung ", + "Mario Ortiz Manero " +] name = "rspotify-http" version = "0.10.0" license = "MIT" diff --git a/rspotify-macros/Cargo.toml b/rspotify-macros/Cargo.toml index 87a265b1..4f12ef55 100644 --- a/rspotify-macros/Cargo.toml +++ b/rspotify-macros/Cargo.toml @@ -1,6 +1,9 @@ [package] name = "rspotify-macros" -authors = ["Ramsay Leung "] +authors = [ + "Ramsay Leung ", + "Mario Ortiz Manero " +] version = "0.10.0" license = "MIT" readme = "README.md" diff --git a/rspotify-model/Cargo.toml b/rspotify-model/Cargo.toml index 94050191..5f0690a6 100644 --- a/rspotify-model/Cargo.toml +++ b/rspotify-model/Cargo.toml @@ -1,7 +1,10 @@ [package] name = "rspotify-model" version = "0.10.0" -authors = ["Ramsay Leung "] +authors = [ + "Ramsay Leung ", + "Mario Ortiz Manero " +] license = "MIT" readme = "README.md" description = "Model for Rspotify" From 0c3faed48040373689d52b9f9e0f7cc216ff24d1 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:43:58 +0200 Subject: [PATCH 49/56] rename: code_auth -> auth_code --- Cargo.toml | 8 ++++---- src/{code_auth.rs => auth_code.rs} | 2 +- src/{code_auth_pkce.rs => auth_code_pkce.rs} | 2 +- src/lib.rs | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) rename src/{code_auth.rs => auth_code.rs} (99%) rename src/{code_auth_pkce.rs => auth_code_pkce.rs} (99%) diff --git a/Cargo.toml b/Cargo.toml index 2610baf8..bb2500c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,14 +94,14 @@ required-features = ["env-file", "cli", "client-reqwest"] path = "examples/client_credentials.rs" [[example]] -name = "code_auth" +name = "auth_code" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/code_auth.rs" +path = "examples/auth_code.rs" [[example]] -name = "code_auth_pkce" +name = "auth_code_pkce" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/code_auth_pkce.rs" +path = "examples/auth_code_pkce.rs" [[example]] name = "oauth_tokens" diff --git a/src/code_auth.rs b/src/auth_code.rs similarity index 99% rename from src/code_auth.rs rename to src/auth_code.rs index faad0ba5..1e23479c 100644 --- a/src/code_auth.rs +++ b/src/auth_code.rs @@ -57,7 +57,7 @@ use url::Url; /// appended like so: `http://localhost/?code=...`. /// /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow -/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/code_auth.rs +/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/auth_code.rs /// [example-webapp]: https://github.com/ramsayleung/rspotify/tree/master/examples/webapp /// [example-refresh-token]: https://github.com/ramsayleung/rspotify/blob/master/examples/with_refresh_token.rs #[derive(Clone, Debug, Default)] diff --git a/src/code_auth_pkce.rs b/src/auth_code_pkce.rs similarity index 99% rename from src/code_auth_pkce.rs rename to src/auth_code_pkce.rs index 765674dc..e1085b3b 100644 --- a/src/code_auth_pkce.rs +++ b/src/auth_code_pkce.rs @@ -23,7 +23,7 @@ use url::Url; /// client. /// /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce -/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/code_auth.rs +/// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/auth_code_pkce.rs #[derive(Clone, Debug, Default)] pub struct CodeAuthPkceSpotify { pub creds: Credentials, diff --git a/src/lib.rs b/src/lib.rs index 7aaa3005..b1aeff20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -123,8 +123,8 @@ //! [spotify-implicit-grant]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow pub mod client_creds; -pub mod code_auth; -pub mod code_auth_pkce; +pub mod auth_code; +pub mod auth_code_pkce; pub mod endpoints; // Subcrate re-exports @@ -133,8 +133,8 @@ pub use rspotify_macros as macros; pub use rspotify_model as model; // Top-level re-exports pub use client_creds::ClientCredentialsSpotify; -pub use code_auth::CodeAuthSpotify; -pub use code_auth_pkce::CodeAuthPkceSpotify; +pub use auth_code::CodeAuthSpotify; +pub use auth_code_pkce::CodeAuthPkceSpotify; pub use macros::scopes; use std::{ From 7523da4997983abeb25dd3bb9a099cf60695979e Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:46:06 +0200 Subject: [PATCH 50/56] rename: CodeAuthSpotify -> AuthCodeSpotify --- examples/code_auth.rs | 4 ++-- examples/oauth_tokens.rs | 4 ++-- examples/pagination_async.rs | 4 ++-- examples/pagination_manual.rs | 4 ++-- examples/pagination_sync.rs | 4 ++-- examples/ureq/device.rs | 4 ++-- examples/ureq/me.rs | 4 ++-- examples/ureq/seek_track.rs | 4 ++-- examples/webapp/src/main.rs | 8 ++++---- examples/with_refresh_token.rs | 10 +++++----- src/auth_code.rs | 18 +++++++++--------- src/auth_code_pkce.rs | 2 +- src/lib.rs | 4 ++-- tests/test_oauth2.rs | 6 +++--- tests/test_with_oauth.rs | 8 ++++---- 15 files changed, 44 insertions(+), 44 deletions(-) diff --git a/examples/code_auth.rs b/examples/code_auth.rs index e3113d77..a54443ae 100644 --- a/examples/code_auth.rs +++ b/examples/code_auth.rs @@ -1,7 +1,7 @@ use rspotify::{ model::{AdditionalType, Country, Market}, prelude::*, - scopes, CodeAuthSpotify, Credentials, OAuth, + scopes, AuthCodeSpotify, Credentials, OAuth, }; #[tokio::main] @@ -38,7 +38,7 @@ async fn main() { // ``` let oauth = OAuth::from_env(scopes!("user-read-currently-playing")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/oauth_tokens.rs b/examples/oauth_tokens.rs index 1339c17a..6a1baea3 100644 --- a/examples/oauth_tokens.rs +++ b/examples/oauth_tokens.rs @@ -5,7 +5,7 @@ //! an .env file or export them manually as environmental variables for this to //! work. -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -38,7 +38,7 @@ async fn main() { ); let oauth = OAuth::from_env(scopes).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); let url = spotify.get_authorize_url(false).unwrap(); spotify.prompt_for_token(&url).await.unwrap(); diff --git a/examples/pagination_async.rs b/examples/pagination_async.rs index 35824699..ac01bbbe 100644 --- a/examples/pagination_async.rs +++ b/examples/pagination_async.rs @@ -8,7 +8,7 @@ use futures::stream::TryStreamExt; use futures_util::pin_mut; -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -18,7 +18,7 @@ async fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/pagination_manual.rs b/examples/pagination_manual.rs index 50a7c3a5..a8c6ab0d 100644 --- a/examples/pagination_manual.rs +++ b/examples/pagination_manual.rs @@ -1,7 +1,7 @@ //! This example shows how manual pagination works. It's what the raw API //! returns, but harder to use than an iterator or stream. -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -11,7 +11,7 @@ async fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/pagination_sync.rs b/examples/pagination_sync.rs index 99a1fe2c..2dd8600e 100644 --- a/examples/pagination_sync.rs +++ b/examples/pagination_sync.rs @@ -9,7 +9,7 @@ //! } //! ``` -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. @@ -18,7 +18,7 @@ fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-library-read")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index 0e57c675..f4c66dd0 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. @@ -7,7 +7,7 @@ fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index e15b22bc..9258589b 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. @@ -7,7 +7,7 @@ fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/ureq/seek_track.rs b/examples/ureq/seek_track.rs index 62715c34..a4a98ae1 100644 --- a/examples/ureq/seek_track.rs +++ b/examples/ureq/seek_track.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; fn main() { // You can use any logger for debugging. @@ -7,7 +7,7 @@ fn main() { let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-read-playback-state")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url(false).unwrap(); diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index 30af4a4a..742c1e68 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -14,7 +14,7 @@ use rocket::response::Redirect; use rocket_contrib::json; use rocket_contrib::json::JsonValue; use rocket_contrib::templates::Template; -use rspotify::{scopes, CodeAuthSpotify, OAuth, Credentials, Config, prelude::*, Token}; +use rspotify::{scopes, AuthCodeSpotify, OAuth, Credentials, Config, prelude::*, Token}; use std::fs; use std::{ @@ -77,7 +77,7 @@ fn check_cache_path_exists(cookies: &Cookies) -> bool { cache_path.exists() } -fn init_spotify(cookies: &Cookies) -> CodeAuthSpotify { +fn init_spotify(cookies: &Cookies) -> AuthCodeSpotify { let config = Config { token_cached: true, cache_path: create_cache_path_if_absent(cookies), @@ -98,7 +98,7 @@ fn init_spotify(cookies: &Cookies) -> CodeAuthSpotify { "0e4e03b9be8d465d87fc32857a4b5aa3" ); - CodeAuthSpotify::with_config(creds, oauth, config) + AuthCodeSpotify::with_config(creds, oauth, config) } #[get("/callback?")] @@ -137,7 +137,7 @@ fn index(mut cookies: Cookies) -> AppResponse { let cache_path = get_cache_path(&cookies); let token = Token::from_cache(cache_path).unwrap(); - let spotify = CodeAuthSpotify::from_token(token); + let spotify = AuthCodeSpotify::from_token(token); match spotify.me() { Ok(user_info) => { context.insert( diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index fedab8fb..b4ce8fee 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -15,11 +15,11 @@ //! tokens](https://github.com/felix-hilden/tekore/issues/86), so in the case of //! Spotify it doesn't seem to revoke them at all. -use rspotify::{model::Id, prelude::*, scopes, CodeAuthSpotify, Credentials, OAuth}; +use rspotify::{model::Id, prelude::*, scopes, AuthCodeSpotify, Credentials, OAuth}; // Sample request that will follow some artists, print the user's // followed artists, and then unfollow the artists. -async fn do_things(spotify: CodeAuthSpotify) { +async fn do_things(spotify: AuthCodeSpotify) { let artists = vec![ Id::from_id("3RGLhK1IP9jnYFH4BRFJBS").unwrap(), // The Clash Id::from_id("0yNLKJebCb8Aueb54LYya3").unwrap(), // New Order @@ -56,7 +56,7 @@ async fn main() { // The default credentials from the `.env` file will be used by default. let creds = Credentials::from_env().unwrap(); let oauth = OAuth::from_env(scopes!("user-follow-read user-follow-modify")).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds.clone(), oauth.clone()); + let mut spotify = AuthCodeSpotify::new(creds.clone(), oauth.clone()); // In the first session of the application we authenticate and obtain the // refresh token. We can also do some requests here. @@ -79,7 +79,7 @@ async fn main() { // At a different time, the refresh token can be used to refresh an access // token directly and run requests: println!(">>> Session two, running some requests:"); - let mut spotify = CodeAuthSpotify::new(creds.clone(), oauth.clone()); + let mut spotify = AuthCodeSpotify::new(creds.clone(), oauth.clone()); // No `prompt_for_user_token_without_cache` needed. spotify .refresh_token(&refresh_token) @@ -90,7 +90,7 @@ async fn main() { // This process can now be repeated multiple times by using only the // refresh token that was obtained at the beginning. println!(">>> Session three, running some requests:"); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); spotify .refresh_token(&refresh_token) .await diff --git a/src/auth_code.rs b/src/auth_code.rs index 1e23479c..13632fa1 100644 --- a/src/auth_code.rs +++ b/src/auth_code.rs @@ -61,7 +61,7 @@ use url::Url; /// [example-webapp]: https://github.com/ramsayleung/rspotify/tree/master/examples/webapp /// [example-refresh-token]: https://github.com/ramsayleung/rspotify/blob/master/examples/with_refresh_token.rs #[derive(Clone, Debug, Default)] -pub struct CodeAuthSpotify { +pub struct AuthCodeSpotify { pub creds: Credentials, pub oauth: OAuth, pub config: Config, @@ -71,7 +71,7 @@ pub struct CodeAuthSpotify { /// This client has access to the base methods. #[maybe_async(?Send)] -impl BaseClient for CodeAuthSpotify { +impl BaseClient for AuthCodeSpotify { fn get_http(&self) -> &HttpClient { &self.http } @@ -96,7 +96,7 @@ impl BaseClient for CodeAuthSpotify { /// This client includes user authorization, so it has access to the user /// private endpoints in [`OAuthClient`]. #[maybe_async(?Send)] -impl OAuthClient for CodeAuthSpotify { +impl OAuthClient for AuthCodeSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } @@ -140,22 +140,22 @@ impl OAuthClient for CodeAuthSpotify { } } -impl CodeAuthSpotify { - /// Builds a new [`CodeAuthSpotify`] given a pair of client credentials and +impl AuthCodeSpotify { + /// Builds a new [`AuthCodeSpotify`] given a pair of client credentials and /// OAuth information. pub fn new(creds: Credentials, oauth: OAuth) -> Self { - CodeAuthSpotify { + AuthCodeSpotify { creds, oauth, ..Default::default() } } - /// Build a new [`CodeAuthSpotify`] from an already generated token. Note + /// Build a new [`AuthCodeSpotify`] from an already generated token. Note /// that once the token expires this will fail to make requests, as the /// client credentials aren't known. pub fn from_token(token: Token) -> Self { - CodeAuthSpotify { + AuthCodeSpotify { token: Some(token), ..Default::default() } @@ -164,7 +164,7 @@ impl CodeAuthSpotify { /// Same as [`Self::new`] but with an extra parameter to configure the /// client. pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { - CodeAuthSpotify { + AuthCodeSpotify { creds, oauth, config, diff --git a/src/auth_code_pkce.rs b/src/auth_code_pkce.rs index e1085b3b..906179db 100644 --- a/src/auth_code_pkce.rs +++ b/src/auth_code_pkce.rs @@ -15,7 +15,7 @@ use url::Url; /// (PKCE)](reference) client for the Spotify API. /// /// This flow is very similar to the regular Authorization Code Flow, so please -/// read [`CodeAuthSpotify`](crate::CodeAuthSpotify) for more information about +/// read [`AuthCodeSpotify`](crate::AuthCodeSpotify) for more information about /// it. The main difference in this case is that you can avoid storing your /// client secret by generating a *code verifier* and a *code challenge*. /// diff --git a/src/lib.rs b/src/lib.rs index b1aeff20..ebb04eba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -89,7 +89,7 @@ //! //! * [Client Credentials Flow](spotify-client-creds): see //! [`ClientCredentialsSpotify`]. -//! * [Authorization Code Flow](spotify-auth-code): see [`CodeAuthSpotify`]. +//! * [Authorization Code Flow](spotify-auth-code): see [`AuthCodeSpotify`]. //! * [Authorization Code Flow with Proof Key for Code Exchange //! (PKCE)](spotify-auth-code-pkce): see [`CodeAuthPkceSpotify`]. //! * [Implicit Grant Flow](spotify-implicit-grant): unimplemented, as Rspotify @@ -133,7 +133,7 @@ pub use rspotify_macros as macros; pub use rspotify_model as model; // Top-level re-exports pub use client_creds::ClientCredentialsSpotify; -pub use auth_code::CodeAuthSpotify; +pub use auth_code::AuthCodeSpotify; pub use auth_code_pkce::CodeAuthPkceSpotify; pub use macros::scopes; diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index 4821393c..8af9cf75 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -4,7 +4,7 @@ use chrono::prelude::*; use chrono::Duration; use maybe_async::maybe_async; use rspotify::{ - prelude::*, scopes, ClientCredentialsSpotify, CodeAuthSpotify, Config, Credentials, OAuth, + prelude::*, scopes, ClientCredentialsSpotify, AuthCodeSpotify, Config, Credentials, OAuth, Token, }; use std::{collections::HashMap, fs, io::Read, path::PathBuf, thread::sleep}; @@ -22,7 +22,7 @@ fn test_get_authorize_url() { }; let creds = Credentials::new("this-is-my-client-id", "this-is-my-client-secret"); - let spotify = CodeAuthSpotify::new(creds, oauth); + let spotify = AuthCodeSpotify::new(creds, oauth); let authorize_url = spotify.get_authorize_url(false).unwrap(); let hash_query: HashMap<_, _> = Url::parse(&authorize_url) @@ -134,7 +134,7 @@ fn test_token_is_expired() { #[test] fn test_parse_response_code() { - let spotify = CodeAuthSpotify::default(); + let spotify = AuthCodeSpotify::default(); let url = "http://localhost:8888/callback"; let code = spotify.parse_response_code(url); diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 19d84556..412dd7bd 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -23,7 +23,7 @@ use rspotify::{ TrackId, TrackPositions, }, prelude::*, - scopes, CodeAuthSpotify, Credentials, OAuth, Token, + scopes, AuthCodeSpotify, Credentials, OAuth, Token, }; use chrono::prelude::*; @@ -33,14 +33,14 @@ use std::env; /// Generating a new OAuth client for the requests. #[maybe_async] -pub async fn oauth_client() -> CodeAuthSpotify { +pub async fn oauth_client() -> AuthCodeSpotify { if let Ok(access_token) = env::var("RSPOTIFY_ACCESS_TOKEN") { let tok = Token { access_token, ..Default::default() }; - CodeAuthSpotify::from_token(tok) + AuthCodeSpotify::from_token(tok) } else if let Ok(refresh_token) = env::var("RSPOTIFY_REFRESH_TOKEN") { // The credentials must be available in the environment. Enable // `env-file` in order to read them from an `.env` file. @@ -75,7 +75,7 @@ pub async fn oauth_client() -> CodeAuthSpotify { // Using every possible scope let oauth = OAuth::from_env(scope).unwrap(); - let mut spotify = CodeAuthSpotify::new(creds, oauth); + let mut spotify = AuthCodeSpotify::new(creds, oauth); spotify.refresh_token(&refresh_token).await.unwrap(); spotify } else { From f44d9e8e185a2d86f858e2dc4f83396da3c0438f Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:47:09 +0200 Subject: [PATCH 51/56] rename: CodeAuthPkceSpotify -> AuthCodePkceSpotify --- examples/code_auth_pkce.rs | 4 ++-- src/auth_code_pkce.rs | 18 +++++++++--------- src/lib.rs | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/code_auth_pkce.rs b/examples/code_auth_pkce.rs index a49c8a03..c5e572e0 100644 --- a/examples/code_auth_pkce.rs +++ b/examples/code_auth_pkce.rs @@ -1,4 +1,4 @@ -use rspotify::{prelude::*, scopes, CodeAuthPkceSpotify, Credentials, OAuth}; +use rspotify::{prelude::*, scopes, AuthCodePkceSpotify, Credentials, OAuth}; #[tokio::main] async fn main() { @@ -34,7 +34,7 @@ async fn main() { // ``` let oauth = OAuth::from_env(scopes!("user-read-recently-played")).unwrap(); - let mut spotify = CodeAuthPkceSpotify::new(creds, oauth); + let mut spotify = AuthCodePkceSpotify::new(creds, oauth); // Obtaining the access token let url = spotify.get_authorize_url().unwrap(); diff --git a/src/auth_code_pkce.rs b/src/auth_code_pkce.rs index 906179db..24806ec9 100644 --- a/src/auth_code_pkce.rs +++ b/src/auth_code_pkce.rs @@ -25,7 +25,7 @@ use url::Url; /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce /// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/auth_code_pkce.rs #[derive(Clone, Debug, Default)] -pub struct CodeAuthPkceSpotify { +pub struct AuthCodePkceSpotify { pub creds: Credentials, pub oauth: OAuth, pub config: Config, @@ -34,7 +34,7 @@ pub struct CodeAuthPkceSpotify { } /// This client has access to the base methods. -impl BaseClient for CodeAuthPkceSpotify { +impl BaseClient for AuthCodePkceSpotify { fn get_http(&self) -> &HttpClient { &self.http } @@ -59,7 +59,7 @@ impl BaseClient for CodeAuthPkceSpotify { /// This client includes user authorization, so it has access to the user /// private endpoints in [`OAuthClient`]. #[maybe_async(?Send)] -impl OAuthClient for CodeAuthPkceSpotify { +impl OAuthClient for AuthCodePkceSpotify { fn get_oauth(&self) -> &OAuth { &self.oauth } @@ -100,22 +100,22 @@ impl OAuthClient for CodeAuthPkceSpotify { } } -impl CodeAuthPkceSpotify { - /// Builds a new [`CodeAuthPkceSpotify`] given a pair of client credentials +impl AuthCodePkceSpotify { + /// Builds a new [`AuthCodePkceSpotify`] given a pair of client credentials /// and OAuth information. pub fn new(creds: Credentials, oauth: OAuth) -> Self { - CodeAuthPkceSpotify { + AuthCodePkceSpotify { creds, oauth, ..Default::default() } } - /// Build a new [`CodeAuthPkceSpotify`] from an already generated token. + /// Build a new [`AuthCodePkceSpotify`] from an already generated token. /// Note that once the token expires this will fail to make requests, as the /// client credentials aren't known. pub fn from_token(token: Token) -> Self { - CodeAuthPkceSpotify { + AuthCodePkceSpotify { token: Some(token), ..Default::default() } @@ -124,7 +124,7 @@ impl CodeAuthPkceSpotify { /// Same as [`Self::new`] but with an extra parameter to configure the /// client. pub fn with_config(creds: Credentials, oauth: OAuth, config: Config) -> Self { - CodeAuthPkceSpotify { + AuthCodePkceSpotify { creds, oauth, config, diff --git a/src/lib.rs b/src/lib.rs index ebb04eba..c5d86484 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -91,7 +91,7 @@ //! [`ClientCredentialsSpotify`]. //! * [Authorization Code Flow](spotify-auth-code): see [`AuthCodeSpotify`]. //! * [Authorization Code Flow with Proof Key for Code Exchange -//! (PKCE)](spotify-auth-code-pkce): see [`CodeAuthPkceSpotify`]. +//! (PKCE)](spotify-auth-code-pkce): see [`AuthCodePkceSpotify`]. //! * [Implicit Grant Flow](spotify-implicit-grant): unimplemented, as Rspotify //! has not been tested on a browser yet. If you'd like support for it, let us //! know in an issue! @@ -134,7 +134,7 @@ pub use rspotify_model as model; // Top-level re-exports pub use client_creds::ClientCredentialsSpotify; pub use auth_code::AuthCodeSpotify; -pub use auth_code_pkce::CodeAuthPkceSpotify; +pub use auth_code_pkce::AuthCodePkceSpotify; pub use macros::scopes; use std::{ From 136793142aee24917f7f81c153229628992e18aa Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:49:28 +0200 Subject: [PATCH 52/56] rename: ClientCredentialsSpotify -> ClientCredsSpotify --- examples/client_credentials.rs | 4 ++-- examples/ureq/search.rs | 4 ++-- src/auth_code.rs | 2 +- src/client_creds.rs | 20 ++++++++++---------- src/endpoints/mod.rs | 6 +++--- src/lib.rs | 4 ++-- tests/test_oauth2.rs | 8 ++++---- tests/test_with_credential.rs | 6 +++--- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/examples/client_credentials.rs b/examples/client_credentials.rs index 08852ca9..d54faaac 100644 --- a/examples/client_credentials.rs +++ b/examples/client_credentials.rs @@ -1,4 +1,4 @@ -use rspotify::{model::Id, prelude::*, ClientCredentialsSpotify, Credentials}; +use rspotify::{model::Id, prelude::*, ClientCredsSpotify, Credentials}; #[tokio::main] async fn main() { @@ -23,7 +23,7 @@ async fn main() { // ``` let creds = Credentials::from_env().unwrap(); - let mut spotify = ClientCredentialsSpotify::new(creds); + let mut spotify = ClientCredsSpotify::new(creds); // Obtaining the access token. Requires to be mutable because the internal // token will be modified. We don't need OAuth for this specific endpoint, diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index abd8a979..caab1fae 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -1,7 +1,7 @@ use rspotify::{ model::{Country, Market, SearchType}, prelude::*, - ClientCredentialsSpotify, Credentials, + ClientCredsSpotify, Credentials, }; fn main() { @@ -9,7 +9,7 @@ fn main() { env_logger::init(); let creds = Credentials::from_env().unwrap(); - let mut spotify = ClientCredentialsSpotify::new(creds); + let mut spotify = ClientCredsSpotify::new(creds); // Obtaining the access token spotify.request_token().unwrap(); diff --git a/src/auth_code.rs b/src/auth_code.rs index 13632fa1..d3486e4e 100644 --- a/src/auth_code.rs +++ b/src/auth_code.rs @@ -15,7 +15,7 @@ use url::Url; /// /// This includes user authorization, and thus has access to endpoints related /// to user private data, unlike the [Client Credentials -/// Flow](crate::ClientCredentialsSpotify) client. See [`BaseClient`] and +/// Flow](crate::ClientCredsSpotify) client. See [`BaseClient`] and /// [`OAuthClient`] for the available endpoints. /// /// If you're developing a CLI application, you might be interested in the `cli` diff --git a/src/client_creds.rs b/src/client_creds.rs index 319d3f4d..db0b1632 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -20,7 +20,7 @@ use maybe_async::maybe_async; /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow /// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/client_creds.rs #[derive(Clone, Debug, Default)] -pub struct ClientCredentialsSpotify { +pub struct ClientCredsSpotify { pub config: Config, pub creds: Credentials, pub token: Option, @@ -28,7 +28,7 @@ pub struct ClientCredentialsSpotify { } /// This client has access to the base methods. -impl BaseClient for ClientCredentialsSpotify { +impl BaseClient for ClientCredsSpotify { fn get_http(&self) -> &HttpClient { &self.http } @@ -50,21 +50,21 @@ impl BaseClient for ClientCredentialsSpotify { } } -impl ClientCredentialsSpotify { - /// Builds a new [`ClientCredentialsSpotify`] given a pair of client - /// credentials and OAuth information. +impl ClientCredsSpotify { + /// Builds a new [`ClientCredsSpotify`] given a pair of client credentials + /// and OAuth information. pub fn new(creds: Credentials) -> Self { - ClientCredentialsSpotify { + ClientCredsSpotify { creds, ..Default::default() } } - /// Build a new [`ClientCredentialsSpotify`] from an already generated - /// token. Note that once the token expires this will fail to make requests, + /// Build a new [`ClientCredsSpotify`] from an already generated token. Note + /// that once the token expires this will fail to make requests, /// as the client credentials aren't known. pub fn from_token(token: Token) -> Self { - ClientCredentialsSpotify { + ClientCredsSpotify { token: Some(token), ..Default::default() } @@ -73,7 +73,7 @@ impl ClientCredentialsSpotify { /// Same as [`Self::new`] but with an extra parameter to configure the /// client. pub fn with_config(creds: Credentials, config: Config) -> Self { - ClientCredentialsSpotify { + ClientCredsSpotify { config, creds, ..Default::default() diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index aecb03d8..3d93763b 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -58,7 +58,7 @@ pub fn basic_auth(user: &str, password: &str) -> (String, String) { #[cfg(test)] mod test { use super::*; - use crate::{scopes, ClientCredentialsSpotify, Token}; + use crate::{scopes, ClientCredsSpotify, Token}; use chrono::{prelude::*, Duration}; #[test] @@ -101,7 +101,7 @@ mod test { #[test] fn test_endpoint_url() { - let spotify = ClientCredentialsSpotify::default(); + let spotify = ClientCredsSpotify::default(); assert_eq!( spotify.endpoint_url("me/player/play"), "https://api.spotify.com/v1/me/player/play" @@ -126,7 +126,7 @@ mod test { refresh_token: Some("...".to_string()), }; - let spotify = ClientCredentialsSpotify::from_token(tok); + let spotify = ClientCredsSpotify::from_token(tok); let headers = spotify.auth_headers().unwrap(); assert_eq!( headers.get("authorization"), diff --git a/src/lib.rs b/src/lib.rs index c5d86484..68bd1359 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -88,7 +88,7 @@ //! flow it is. Please refer to their documentation for more details: //! //! * [Client Credentials Flow](spotify-client-creds): see -//! [`ClientCredentialsSpotify`]. +//! [`ClientCredsSpotify`]. //! * [Authorization Code Flow](spotify-auth-code): see [`AuthCodeSpotify`]. //! * [Authorization Code Flow with Proof Key for Code Exchange //! (PKCE)](spotify-auth-code-pkce): see [`AuthCodePkceSpotify`]. @@ -132,7 +132,7 @@ pub use rspotify_http as http; pub use rspotify_macros as macros; pub use rspotify_model as model; // Top-level re-exports -pub use client_creds::ClientCredentialsSpotify; +pub use client_creds::ClientCredsSpotify; pub use auth_code::AuthCodeSpotify; pub use auth_code_pkce::AuthCodePkceSpotify; pub use macros::scopes; diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index 8af9cf75..b5945217 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -4,7 +4,7 @@ use chrono::prelude::*; use chrono::Duration; use maybe_async::maybe_async; use rspotify::{ - prelude::*, scopes, ClientCredentialsSpotify, AuthCodeSpotify, Config, Credentials, OAuth, + prelude::*, scopes, ClientCredsSpotify, AuthCodeSpotify, Config, Credentials, OAuth, Token, }; use std::{collections::HashMap, fs, io::Read, path::PathBuf, thread::sleep}; @@ -57,14 +57,14 @@ async fn test_read_token_cache() { cache_path: PathBuf::from(".test_read_token_cache.json"), ..Default::default() }; - let mut predefined_spotify = ClientCredentialsSpotify::from_token(tok.clone()); + let mut predefined_spotify = ClientCredsSpotify::from_token(tok.clone()); predefined_spotify.config = config.clone(); // write token data to cache_path predefined_spotify.write_token_cache().unwrap(); assert!(predefined_spotify.config.cache_path.exists()); - let mut spotify = ClientCredentialsSpotify::default(); + let mut spotify = ClientCredsSpotify::default(); spotify.config = config; // read token from cache file @@ -96,7 +96,7 @@ fn test_write_token() { cache_path: PathBuf::from(".test_write_token_cache.json"), ..Default::default() }; - let mut spotify = ClientCredentialsSpotify::from_token(tok.clone()); + let mut spotify = ClientCredsSpotify::from_token(tok.clone()); spotify.config = config; let tok_str = serde_json::to_string(&tok).unwrap(); diff --git a/tests/test_with_credential.rs b/tests/test_with_credential.rs index ad12a40d..dfb3afa1 100644 --- a/tests/test_with_credential.rs +++ b/tests/test_with_credential.rs @@ -4,14 +4,14 @@ use common::maybe_async_test; use rspotify::{ model::{AlbumType, Country, Id, Market}, prelude::*, - ClientCredentialsSpotify, Credentials, + ClientCredsSpotify, Credentials, }; use maybe_async::maybe_async; /// Generating a new basic client for the requests. #[maybe_async] -pub async fn creds_client() -> ClientCredentialsSpotify { +pub async fn creds_client() -> ClientCredsSpotify { // The credentials must be available in the environment. let creds = Credentials::from_env().unwrap_or_else(|| { panic!( @@ -21,7 +21,7 @@ pub async fn creds_client() -> ClientCredentialsSpotify { ) }); - let mut spotify = ClientCredentialsSpotify::new(creds); + let mut spotify = ClientCredsSpotify::new(creds); spotify.request_token().await.unwrap(); spotify } From 8c016bc122b56dd44748da3120bdb9b6e492abd8 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 01:51:39 +0200 Subject: [PATCH 53/56] rename: endpoints -> clients --- src/auth_code.rs | 2 +- src/auth_code_pkce.rs | 2 +- src/client_creds.rs | 4 ++-- src/{endpoints => clients}/base.rs | 2 +- src/{endpoints => clients}/mod.rs | 0 src/{endpoints => clients}/oauth.rs | 6 +++--- src/{endpoints => clients}/pagination/iter.rs | 0 src/{endpoints => clients}/pagination/mod.rs | 0 src/{endpoints => clients}/pagination/stream.rs | 0 src/lib.rs | 10 +++++----- 10 files changed, 13 insertions(+), 13 deletions(-) rename src/{endpoints => clients}/base.rs (99%) rename src/{endpoints => clients}/mod.rs (100%) rename src/{endpoints => clients}/oauth.rs (99%) rename src/{endpoints => clients}/pagination/iter.rs (100%) rename src/{endpoints => clients}/pagination/mod.rs (100%) rename src/{endpoints => clients}/pagination/stream.rs (100%) diff --git a/src/auth_code.rs b/src/auth_code.rs index d3486e4e..b37122f2 100644 --- a/src/auth_code.rs +++ b/src/auth_code.rs @@ -1,6 +1,6 @@ use crate::{ auth_urls, - endpoints::{BaseClient, OAuthClient}, + clients::{BaseClient, OAuthClient}, headers, http::{Form, HttpClient}, ClientResult, Config, Credentials, OAuth, Token, diff --git a/src/auth_code_pkce.rs b/src/auth_code_pkce.rs index 24806ec9..23b03024 100644 --- a/src/auth_code_pkce.rs +++ b/src/auth_code_pkce.rs @@ -1,6 +1,6 @@ use crate::{ auth_urls, - endpoints::{BaseClient, OAuthClient}, + clients::{BaseClient, OAuthClient}, headers, http::{Form, HttpClient}, ClientResult, Config, Credentials, OAuth, Token, diff --git a/src/client_creds.rs b/src/client_creds.rs index db0b1632..602e06a2 100644 --- a/src/client_creds.rs +++ b/src/client_creds.rs @@ -1,5 +1,5 @@ use crate::{ - endpoints::BaseClient, + clients::BaseClient, headers, http::{Form, HttpClient}, ClientResult, Config, Credentials, Token, @@ -15,7 +15,7 @@ use maybe_async::maybe_async; /// /// Note: This flow does not include authorization and therefore cannot be used /// to access or to manage the endpoints related to user private data in -/// [`OAuthClient`](crate::endpoints::OAuthClient). +/// [`OAuthClient`](crate::clients::OAuthClient). /// /// [reference]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#client-credentials-flow /// [example-main]: https://github.com/ramsayleung/rspotify/blob/master/examples/client_creds.rs diff --git a/src/endpoints/base.rs b/src/clients/base.rs similarity index 99% rename from src/endpoints/base.rs rename to src/clients/base.rs index e8953a9f..d711358c 100644 --- a/src/endpoints/base.rs +++ b/src/clients/base.rs @@ -1,6 +1,6 @@ use crate::{ auth_urls, - endpoints::{ + clients::{ basic_auth, bearer_auth, convert_result, join_ids, pagination::{paginate, Paginator}, }, diff --git a/src/endpoints/mod.rs b/src/clients/mod.rs similarity index 100% rename from src/endpoints/mod.rs rename to src/clients/mod.rs diff --git a/src/endpoints/oauth.rs b/src/clients/oauth.rs similarity index 99% rename from src/endpoints/oauth.rs rename to src/clients/oauth.rs index 2a1cb466..8086346a 100644 --- a/src/endpoints/oauth.rs +++ b/src/clients/oauth.rs @@ -1,5 +1,5 @@ use crate::{ - endpoints::{ + clients::{ append_device_id, convert_result, join_ids, pagination::{paginate, Paginator}, BaseClient, @@ -22,9 +22,9 @@ use url::Url; /// authorization, including some parts of the authentication flow that are /// shared, and the endpoints. /// -/// Note that the base trait [`BaseClient`](crate::endpoints::BaseClient) may +/// Note that the base trait [`BaseClient`](crate::clients::BaseClient) may /// have endpoints that conditionally require authorization like -/// [`user_playlist`](crate::endpoints::BaseClient::user_playlist). This trait +/// [`user_playlist`](crate::clients::BaseClient::user_playlist). This trait /// only separates endpoints that *always* need authorization from the base /// ones. #[maybe_async(?Send)] diff --git a/src/endpoints/pagination/iter.rs b/src/clients/pagination/iter.rs similarity index 100% rename from src/endpoints/pagination/iter.rs rename to src/clients/pagination/iter.rs diff --git a/src/endpoints/pagination/mod.rs b/src/clients/pagination/mod.rs similarity index 100% rename from src/endpoints/pagination/mod.rs rename to src/clients/pagination/mod.rs diff --git a/src/endpoints/pagination/stream.rs b/src/clients/pagination/stream.rs similarity index 100% rename from src/endpoints/pagination/stream.rs rename to src/clients/pagination/stream.rs diff --git a/src/lib.rs b/src/lib.rs index 68bd1359..4acf12e3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -83,8 +83,8 @@ //! //! Rspotify has a different client for each of the available authentication //! flows. They may implement the endpoints in -//! [`BaseClient`](crate::endpoints::BaseClient) or -//! [`OAuthClient`](crate::endpoints::OAuthClient) according to what kind of +//! [`BaseClient`](crate::clients::BaseClient) or +//! [`OAuthClient`](crate::clients::OAuthClient) according to what kind of //! flow it is. Please refer to their documentation for more details: //! //! * [Client Credentials Flow](spotify-client-creds): see @@ -125,7 +125,7 @@ pub mod client_creds; pub mod auth_code; pub mod auth_code_pkce; -pub mod endpoints; +pub mod clients; // Subcrate re-exports pub use rspotify_http as http; @@ -151,7 +151,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; pub mod prelude { - pub use crate::endpoints::{BaseClient, OAuthClient}; + pub use crate::clients::{BaseClient, OAuthClient}; } pub(in crate) mod headers { @@ -219,7 +219,7 @@ pub struct Config { pub cache_path: PathBuf, /// The pagination chunk size used when performing automatically paginated - /// requests, like [`artist_albums`](crate::endpoints::BaseClient). This + /// requests, like [`artist_albums`](crate::clients::BaseClient). This /// means that a request will be performed every `pagination_chunks` items. /// By default this is [`DEFAULT_PAGINATION_CHUNKS`]. /// From 5c1a7be9686a4ab43d6e37c6252932e4415fdb71 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 02:26:07 +0200 Subject: [PATCH 54/56] fix maybe-async/is_sync in cargo.toml --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bb2500c0..eeeb7582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ ureq-rustls-tls = ["rspotify-http/ureq-rustls-tls"] # Internal features for checking async or sync compilation __async = ["futures", "async-stream", "async-trait"] -__sync = [] +__sync = ["maybe-async/is_sync"] [package.metadata.docs.rs] # Also documenting the CLI methods From 56d5c52f44ae598530d204a363fcf72ecdfe6069 Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 02:30:26 +0200 Subject: [PATCH 55/56] fix renames --- Cargo.toml | 4 ++-- examples/{code_auth.rs => auth_code.rs} | 0 examples/{code_auth_pkce.rs => auth_code_pkce.rs} | 0 examples/{client_credentials.rs => client_creds.rs} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename examples/{code_auth.rs => auth_code.rs} (100%) rename examples/{code_auth_pkce.rs => auth_code_pkce.rs} (100%) rename examples/{client_credentials.rs => client_creds.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index eeeb7582..6df17351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,9 +89,9 @@ __sync = ["maybe-async/is_sync"] features = ["cli"] [[example]] -name = "client_credentials" +name = "client_creds" required-features = ["env-file", "cli", "client-reqwest"] -path = "examples/client_credentials.rs" +path = "examples/client_creds.rs" [[example]] name = "auth_code" diff --git a/examples/code_auth.rs b/examples/auth_code.rs similarity index 100% rename from examples/code_auth.rs rename to examples/auth_code.rs diff --git a/examples/code_auth_pkce.rs b/examples/auth_code_pkce.rs similarity index 100% rename from examples/code_auth_pkce.rs rename to examples/auth_code_pkce.rs diff --git a/examples/client_credentials.rs b/examples/client_creds.rs similarity index 100% rename from examples/client_credentials.rs rename to examples/client_creds.rs From bc0e0e457b61443c2869b675871183ad6c0e7b6b Mon Sep 17 00:00:00 2001 From: Mario Ortiz Manero Date: Fri, 14 May 2021 02:31:17 +0200 Subject: [PATCH 56/56] format --- src/lib.rs | 4 ++-- tests/test_oauth2.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 4acf12e3..fbcdfe19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -122,9 +122,9 @@ //! [spotify-auth-code-pkce]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce //! [spotify-implicit-grant]: https://developer.spotify.com/documentation/general/guides/authorization-guide/#implicit-grant-flow -pub mod client_creds; pub mod auth_code; pub mod auth_code_pkce; +pub mod client_creds; pub mod clients; // Subcrate re-exports @@ -132,9 +132,9 @@ pub use rspotify_http as http; pub use rspotify_macros as macros; pub use rspotify_model as model; // Top-level re-exports -pub use client_creds::ClientCredsSpotify; pub use auth_code::AuthCodeSpotify; pub use auth_code_pkce::AuthCodePkceSpotify; +pub use client_creds::ClientCredsSpotify; pub use macros::scopes; use std::{ diff --git a/tests/test_oauth2.rs b/tests/test_oauth2.rs index b5945217..bcaeedbf 100644 --- a/tests/test_oauth2.rs +++ b/tests/test_oauth2.rs @@ -4,8 +4,7 @@ use chrono::prelude::*; use chrono::Duration; use maybe_async::maybe_async; use rspotify::{ - prelude::*, scopes, ClientCredsSpotify, AuthCodeSpotify, Config, Credentials, OAuth, - Token, + prelude::*, scopes, AuthCodeSpotify, ClientCredsSpotify, Config, Credentials, OAuth, Token, }; use std::{collections::HashMap, fs, io::Read, path::PathBuf, thread::sleep}; use url::Url;