diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ea62e7a..aefca721 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -181,6 +181,12 @@ If we missed any change or there's something you'd like to discuss about this ve + Remove `SimplifiedPlayingContext`, since it's useless. - ([#177](https://github.com/ramsayleung/rspotify/pull/157)) Change `mode` from `f32` to `enum Modality`: + Change `AudioAnalysisSection::mode`, `AudioAnalysisTrack::mode` and `AudioFeatures::mode` from `f32` to `enum Modality`. +- ([#185](https://github.com/ramsayleung/rspotify/pull/185)) Polish the `Token.expires_at`, `Token.expires_in` fields + + Change `Token.expires_in` from u32 to `chrono::Duration` + + Change `Token.expires_at` from i64 to `chrono::DateTime` + + Change `Token.scope` from `String` to `HashSet`. + + Change `OAuth.scope` from `String` to `HashSet`. + + Change `SimplifiedPlaylist::tracks` from `HashMap` to `PlaylistTracksRef` ## 0.10 (2020/07/01) diff --git a/examples/current_user_recently_played.rs b/examples/current_user_recently_played.rs index 3ec3d9ed..c227e5a1 100644 --- a/examples/current_user_recently_played.rs +++ b/examples/current_user_recently_played.rs @@ -1,6 +1,8 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use std::collections::HashSet; + #[tokio::main] async fn main() { // You can use any logger for debugging. @@ -29,10 +31,9 @@ async fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope("user-read-recently-played") - .build() - .unwrap(); + let mut scopes = HashSet::new(); + scopes.insert("user-read-recently-played".to_owned()); + let oauth = OAuthBuilder::from_env().scope(scopes).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/oauth_tokens.rs b/examples/oauth_tokens.rs index cd4fc17e..fa806032 100644 --- a/examples/oauth_tokens.rs +++ b/examples/oauth_tokens.rs @@ -18,16 +18,15 @@ async fn main() { let creds = CredentialsBuilder::from_env().build().unwrap(); // Using every possible scope + let scope = "user-read-email user-read-private user-top-read \ + user-read-recently-played user-follow-read user-library-read \ + user-read-currently-playing user-read-playback-state \ + user-read-playback-position playlist-read-collaborative \ + playlist-read-private user-follow-modify user-library-modify \ + user-modify-playback-state playlist-modify-public \ + playlist-modify-private ugc-image-upload"; let oauth = OAuthBuilder::from_env() - .scope( - "user-read-email user-read-private user-top-read \ - user-read-recently-played user-follow-read user-library-read \ - user-read-currently-playing user-read-playback-state \ - user-read-playback-position playlist-read-collaborative \ - playlist-read-private user-follow-modify user-library-modify \ - user-modify-playback-state playlist-modify-public \ - playlist-modify-private ugc-image-upload", - ) + .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) .build() .unwrap(); diff --git a/examples/ureq/device.rs b/examples/ureq/device.rs index 9504456e..c68b046b 100644 --- a/examples/ureq/device.rs +++ b/examples/ureq/device.rs @@ -1,6 +1,8 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use std::collections::HashSet; + fn main() { // You can use any logger for debugging. env_logger::init(); @@ -28,10 +30,9 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope("user-read-playback-state") - .build() - .unwrap(); + let mut scope = HashSet::new(); + scope.insert("user-read-playback-state".to_owned()); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/me.rs b/examples/ureq/me.rs index bce1f38a..d6e39a3c 100644 --- a/examples/ureq/me.rs +++ b/examples/ureq/me.rs @@ -1,6 +1,8 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use std::collections::HashSet; + fn main() { // You can use any logger for debugging. env_logger::init(); @@ -28,10 +30,9 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope("user-read-playback-state") - .build() - .unwrap(); + let mut scope = HashSet::new(); + scope.insert("user-read-playback-state".to_owned()); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/ureq/search.rs b/examples/ureq/search.rs index cd2b7113..e5851da6 100644 --- a/examples/ureq/search.rs +++ b/examples/ureq/search.rs @@ -1,7 +1,9 @@ use rspotify::client::SpotifyBuilder; -use rspotify::model::{Country, SearchType}; +use rspotify::model::{Country, Market, SearchType}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use std::collections::HashSet; + fn main() { // You can use any logger for debugging. env_logger::init(); @@ -29,10 +31,9 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope("user-read-playback-state") - .build() - .unwrap(); + let mut scope = HashSet::new(); + scope.insert("user-read-playback-state".to_owned()); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) @@ -56,7 +57,7 @@ fn main() { SearchType::Artist, 10, 0, - Some(Country::UnitedStates), + Some(Market::Country(Country::UnitedStates)), None, ); match result { @@ -70,7 +71,7 @@ fn main() { SearchType::Playlist, 10, 0, - Some(Country::UnitedStates), + Some(Market::Country(Country::UnitedStates)), None, ); match result { @@ -84,7 +85,7 @@ fn main() { SearchType::Track, 10, 0, - Some(Country::UnitedStates), + Some(Market::Country(Country::UnitedStates)), None, ); match result { diff --git a/examples/ureq/seek_track.rs b/examples/ureq/seek_track.rs index 1519ac5c..76e680b9 100644 --- a/examples/ureq/seek_track.rs +++ b/examples/ureq/seek_track.rs @@ -1,6 +1,8 @@ use rspotify::client::SpotifyBuilder; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder}; +use std::collections::HashSet; + fn main() { // You can use any logger for debugging. env_logger::init(); @@ -28,10 +30,9 @@ fn main() { // .redirect_uri("http://localhost:8888/callback") // .build() // .unwrap(); - let oauth = OAuthBuilder::from_env() - .scope("user-read-playback-state") - .build() - .unwrap(); + let mut scope = HashSet::new(); + scope.insert("user-read-playback-state".to_owned()); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds) diff --git a/examples/webapp/src/main.rs b/examples/webapp/src/main.rs index f9e8bc47..078532ce 100644 --- a/examples/webapp/src/main.rs +++ b/examples/webapp/src/main.rs @@ -17,10 +17,12 @@ use rocket_contrib::templates::Template; use rspotify::client::{ClientError, SpotifyBuilder}; use rspotify::oauth2::{CredentialsBuilder, OAuthBuilder, TokenBuilder}; -use std::collections::HashMap; -use std::env; use std::fs; -use std::path::PathBuf; +use std::{ + collections::{HashMap, HashSet}, + env, + path::PathBuf, +}; #[derive(Debug, Responder)] pub enum AppResponse { @@ -51,7 +53,7 @@ fn create_cache_path_if_absent(cookies: &Cookies) -> PathBuf { path.pop(); fs::create_dir_all(path).unwrap(); } - cache_path.clone() + cache_path } fn remove_cache_path(mut cookies: Cookies) { @@ -73,9 +75,10 @@ fn check_cache_path_exists(cookies: &Cookies) -> (bool, PathBuf) { fn init_spotify() -> SpotifyBuilder { // Please notice that protocol of redirect_uri, make sure it's http // (or https). It will fail if you mix them up. + let scope = "user-read-currently-playing playlist-modify-private"; let oauth = OAuthBuilder::default() .redirect_uri("http://localhost:8000/callback") - .scope("user-read-currently-playing playlist-modify-private") + .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) .build() .unwrap(); diff --git a/examples/with_refresh_token.rs b/examples/with_refresh_token.rs index 1537d3f8..479765fc 100644 --- a/examples/with_refresh_token.rs +++ b/examples/with_refresh_token.rs @@ -56,8 +56,9 @@ async fn main() { // The default credentials from the `.env` file will be used by default. let creds = CredentialsBuilder::from_env().build().unwrap(); + let scope = "user-follow-read user-follow-modify"; let oauth = OAuthBuilder::from_env() - .scope("user-follow-read user-follow-modify") + .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) .build() .unwrap(); let mut spotify = SpotifyBuilder::default() diff --git a/src/lib.rs b/src/lib.rs index 6f802aa0..f16f910a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -153,6 +153,8 @@ //! ](https://github.com/ramsayleung/rspotify/tree/master/examples) //! which can serve as a learning tool. +use getrandom::getrandom; + // disable all modules when both client features are enabled, // this way only the compile error below gets show // instead of showing a whole list of confusing errors @@ -164,8 +166,6 @@ mod http; pub mod model; #[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] pub mod oauth2; -#[cfg(not(all(feature = "client-reqwest", feature = "client-ureq")))] -pub mod util; #[cfg(all(feature = "client-reqwest", feature = "client-ureq"))] compile_error!( @@ -186,3 +186,16 @@ mod macros { }; } } + +/// 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() +} diff --git a/src/model/playlist.rs b/src/model/playlist.rs index d40ebdbd..a494215b 100644 --- a/src/model/playlist.rs +++ b/src/model/playlist.rs @@ -1,7 +1,6 @@ //! All kinds of playlists objects use chrono::prelude::*; use serde::{Deserialize, Serialize}; -use serde_json::Value; use std::collections::HashMap; use super::image::Image; @@ -18,6 +17,15 @@ pub struct PlaylistResult { pub snapshot_id: String, } +/// Playlist Track Reference Object +/// +/// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-playlisttracksrefobject) +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlaylistTracksRef { + pub href: String, + pub total: u32, +} + /// Simplified playlist object /// /// [Reference](https://developer.spotify.com/documentation/web-api/reference/#object-simplifiedplaylistobject) @@ -32,7 +40,7 @@ pub struct SimplifiedPlaylist { pub owner: PublicUser, pub public: Option, pub snapshot_id: String, - pub tracks: HashMap, + pub tracks: PlaylistTracksRef, #[serde(rename = "type")] pub _type: Type, pub uri: String, diff --git a/src/oauth2.rs b/src/oauth2.rs index 3c010daa..b15f92c0 100644 --- a/src/oauth2.rs +++ b/src/oauth2.rs @@ -6,45 +6,90 @@ use maybe_async::maybe_async; use serde::{Deserialize, Serialize}; use url::Url; +use chrono::Duration; use std::collections::{HashMap, HashSet}; -use std::env; -use std::fs; -use std::io::{Read, Write}; -use std::iter::FromIterator; -use std::path::Path; +use std::{ + env, fs, + io::{Read, Write}, + path::Path, +}; use super::client::{ClientResult, Spotify}; use super::http::{headers, BaseClient, Form, Headers}; -use super::util::{datetime_to_timestamp, generate_random_string}; +use crate::generate_random_string; mod auth_urls { pub const AUTHORIZE: &str = "https://accounts.spotify.com/authorize"; pub const TOKEN: &str = "https://accounts.spotify.com/api/token"; } -// TODO this should be removed after making a custom type for scopes -// or handling them as a vector of strings. -fn is_scope_subset(needle_scope: &str, haystack_scope: &str) -> bool { - let needle_vec: Vec<&str> = needle_scope.split_whitespace().collect(); - let haystack_vec: Vec<&str> = haystack_scope.split_whitespace().collect(); - let needle_set: HashSet<&str> = HashSet::from_iter(needle_vec); - let haystack_set: HashSet<&str> = HashSet::from_iter(haystack_vec); - // needle_set - haystack_set - needle_set.is_subset(&haystack_set) +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. +/// 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, - pub expires_in: u32, - #[builder(setter(strip_option), default)] - pub expires_at: Option, + /// 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, - #[builder(setter(into))] - pub scope: String, + /// A list of [scopes](https://developer.spotify.com/documentation/general/guides/scopes/) + /// which have been granted for this `access_token` + #[builder(default = "HashSet::new()")] + #[serde(with = "space_separated_scope")] + pub scope: HashSet, } impl TokenBuilder { @@ -81,16 +126,10 @@ impl Token { Ok(()) } - // TODO: we should use `Instant` for expiration dates, which requires this - // to be modified. + /// Check if the token is expired pub fn is_expired(&self) -> bool { - let now: DateTime = Utc::now(); - - // 10s as buffer time - match self.expires_at { - Some(expires_at) => now.timestamp() > expires_at - 10, - None => true, - } + self.expires_at + .map_or(true, |x| Utc::now().timestamp() > x.timestamp()) } } @@ -130,8 +169,8 @@ pub struct OAuth { /// https://tools.ietf.org/html/rfc6749#section-10.12 #[builder(setter(into), default = "generate_random_string(16)")] pub state: String, - #[builder(setter(into))] - pub scope: String, + #[builder(default)] + pub scope: HashSet, #[builder(setter(into, strip_option), default)] pub proxies: Option, } @@ -169,10 +208,16 @@ impl Spotify { 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, &oauth.scope); + payload.insert(headers::SCOPE, &scope); payload.insert(headers::STATE, &oauth.state); if show_dialog { @@ -188,7 +233,7 @@ impl Spotify { pub async fn read_token_cache(&mut self) -> Option { let tok = TokenBuilder::from_cache(&self.cache_path).build().ok()?; - if !is_scope_subset(&self.get_oauth().ok()?.scope, &tok.scope) || tok.is_expired() { + 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 @@ -210,8 +255,7 @@ impl Spotify { .post_form(auth_urls::TOKEN, Some(&head), payload) .await?; let mut tok = serde_json::from_str::(&response)?; - tok.expires_at = Some(datetime_to_timestamp(tok.expires_in)); - + tok.expires_at = Utc::now().checked_add_signed(tok.expires_in); Ok(tok) } @@ -298,7 +342,15 @@ impl Spotify { ); data.insert(headers::REDIRECT_URI.to_owned(), oauth.redirect_uri.clone()); data.insert(headers::CODE.to_owned(), code.to_owned()); - data.insert(headers::SCOPE.to_owned(), oauth.scope.clone()); + data.insert( + headers::SCOPE.to_owned(), + oauth + .scope + .clone() + .into_iter() + .collect::>() + .join(" "), + ); data.insert(headers::STATE.to_owned(), oauth.state.clone()); self.token = Some(self.fetch_access_token(&data).await?); @@ -384,26 +436,70 @@ impl Spotify { mod tests { use super::*; use crate::client::SpotifyBuilder; + use url::Url; + use chrono::Duration; + use std::collections::HashSet; use std::fs; use std::io::Read; + use std::thread::sleep; #[test] - fn test_is_scope_subset() { - let mut needle_scope = String::from("1 2 3"); - let mut haystack_scope = String::from("1 2 3 4"); - let mut broken_scope = String::from("5 2 4"); - assert!(is_scope_subset(&mut needle_scope, &mut haystack_scope)); - assert!(!is_scope_subset(&mut broken_scope, &mut haystack_scope)); - } + fn test_get_authorize_url() { + let scope = "playlist-read-private"; + + let oauth = OAuthBuilder::default() + .state("fdsafdsfa") + .redirect_uri("localhost") + .scope(scope.split_whitespace().map(|x| x.to_owned()).collect()) + .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 authorize_url = spotify.get_authorize_url(false).unwrap(); + let hash_query: HashMap<_, _> = Url::parse(&authorize_url) + .unwrap() + .query_pairs() + .into_owned() + .collect(); + + assert_eq!(hash_query.get("client_id").unwrap(), "this-is-my-client-id"); + assert_eq!(hash_query.get("response_type").unwrap(), "code"); + assert_eq!(hash_query.get("redirect_uri").unwrap(), "localhost"); + assert_eq!(hash_query.get("scope").unwrap(), "playlist-read-private"); + assert_eq!(hash_query.get("state").unwrap(), "fdsafdsfa"); + } #[test] fn test_write_token() { + let now: DateTime = Utc::now(); + let scope = "playlist-read-private playlist-read-collaborative \ + playlist-modify-public playlist-modify-private \ + streaming ugc-image-upload user-follow-modify \ + user-follow-read user-library-read user-library-modify \ + user-read-private user-read-birthdate user-read-email \ + user-top-read user-read-playback-state user-modify-playback-state \ + user-read-currently-playing user-read-recently-played"; + let scope = scope + .split_whitespace() + .map(|x| x.to_owned()) + .collect::>(); + let tok = TokenBuilder::default() .access_token("test-access_token") - .expires_in(3600) - .expires_at(1515841743) - .scope("playlist-read-private playlist-read-collaborative playlist-modify-public playlist-modify-private streaming ugc-image-upload user-follow-modify user-follow-read user-library-read user-library-modify user-read-private user-read-birthdate user-read-email user-top-read user-read-playback-state user-modify-playback-state user-read-currently-playing user-read-recently-played") + .expires_in(Duration::seconds(3600)) + .expires_at(now) + .scope(scope.clone()) .refresh_token("...") .build() .unwrap(); @@ -421,6 +517,34 @@ mod tests { file.read_to_string(&mut tok_str_file).unwrap(); assert_eq!(tok_str, tok_str_file); + let tok_from_file: Token = serde_json::from_str(&tok_str_file).unwrap(); + assert_eq!(tok_from_file.scope, scope); + assert_eq!(tok_from_file.expires_in, Duration::seconds(3600)); + assert_eq!(tok_from_file.expires_at.unwrap(), now); + } + + #[test] + fn test_token_is_expired() { + let scope = "playlist-read-private playlist-read-collaborative \ + playlist-modify-public playlist-modify-private streaming \ + ugc-image-upload user-follow-modify user-follow-read \ + user-library-read user-library-modify user-read-private \ + user-read-birthdate user-read-email user-top-read \ + user-read-playback-state user-modify-playback-state \ + user-read-currently-playing user-read-recently-played"; + let scope = scope.split_whitespace().map(|x| x.to_owned()).collect(); + + let tok = TokenBuilder::default() + .access_token("test-access_token") + .expires_in(Duration::seconds(1)) + .expires_at(Utc::now()) + .scope(scope) + .refresh_token("...") + .build() + .unwrap(); + assert!(!tok.is_expired()); + sleep(std::time::Duration::from_secs(2)); + assert!(tok.is_expired()); } #[test] diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 43ccf9b5..00000000 --- a/src/util.rs +++ /dev/null @@ -1,21 +0,0 @@ -use chrono::prelude::*; -use getrandom::getrandom; - -/// Convert datetime to unix timestamp -pub(in crate) fn datetime_to_timestamp(elapsed: u32) -> i64 { - let utc: DateTime = Utc::now(); - utc.timestamp() + i64::from(elapsed) -} - -/// 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() -} diff --git a/tests/test_models.rs b/tests/test_models.rs index 2bd7cf1d..9d4c59a2 100644 --- a/tests/test_models.rs +++ b/tests/test_models.rs @@ -871,3 +871,51 @@ fn test_audio_analysis_track() { let audio_analysis_track: AudioAnalysisTrack = serde_json::from_str(&json).unwrap(); assert_eq!(audio_analysis_track.mode, Modality::Minor); } + +#[test] +fn test_simplified_playlist() { + let json = r#" + { + "collaborative": false, + "description": "Chegou o grande dia, aperte o play e partiu fim de semana!", + "external_urls": { + "spotify": "https://open.spotify.com/playlist/37i9dQZF1DX8mBRYewE6or" + }, + "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX8mBRYewE6or", + "id": "37i9dQZF1DX8mBRYewE6or", + "images": [ + { + "height": null, + "url": "https://i.scdn.co/image/ab67706f00000003206a95fa5badbe1d33b65e14", + "width": null + } + ], + "name": "Sexta", + "owner": { + "display_name": "Spotify", + "external_urls": { + "spotify": "https://open.spotify.com/user/spotify" + }, + "href": "https://api.spotify.com/v1/users/spotify", + "id": "spotify", + "type": "user", + "uri": "spotify:user:spotify" + }, + "primary_color": null, + "public": null, + "snapshot_id": "MTYxMzM5MzIyMywwMDAwMDAwMGQ0MWQ4Y2Q5OGYwMGIyMDRlOTgwMDk5OGVjZjg0Mjdl", + "tracks": { + "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX8mBRYewE6or/tracks", + "total": 62 + }, + "type": "playlist", + "uri": "spotify:playlist:37i9dQZF1DX8mBRYewE6or" + } + "#; + let simplified_playlist: SimplifiedPlaylist = serde_json::from_str(&json).unwrap(); + assert_eq!( + simplified_playlist.tracks.href, + "https://api.spotify.com/v1/playlists/37i9dQZF1DX8mBRYewE6or/tracks" + ); + assert_eq!(simplified_playlist.tracks.total, 62); +} diff --git a/tests/test_with_oauth.rs b/tests/test_with_oauth.rs index 5db660ac..6806ddfd 100644 --- a/tests/test_with_oauth.rs +++ b/tests/test_with_oauth.rs @@ -25,11 +25,10 @@ use rspotify::{ model::Market, }; -use std::env; - use chrono::prelude::*; use maybe_async::maybe_async; use serde_json::map::Map; +use std::env; /// Generating a new OAuth client for the requests. #[maybe_async] @@ -53,19 +52,18 @@ pub async fn oauth_client() -> Spotify { ) }); + let scope = "user-read-email user-read-private user-top-read \ + user-read-recently-played user-follow-read user-library-read \ + user-read-currently-playing user-read-playback-state \ + user-read-playback-position playlist-read-collaborative \ + playlist-read-private user-follow-modify user-library-modify \ + user-modify-playback-state playlist-modify-public \ + playlist-modify-private ugc-image-upload" + .split_whitespace() + .map(|x| x.to_owned()) + .collect(); // Using every possible scope - let oauth = OAuthBuilder::from_env() - .scope( - "user-read-email user-read-private user-top-read \ - user-read-recently-played user-follow-read user-library-read \ - user-read-currently-playing user-read-playback-state \ - user-read-playback-position playlist-read-collaborative \ - playlist-read-private user-follow-modify user-library-modify \ - user-modify-playback-state playlist-modify-public \ - playlist-modify-private ugc-image-upload", - ) - .build() - .unwrap(); + let oauth = OAuthBuilder::from_env().scope(scope).build().unwrap(); let mut spotify = SpotifyBuilder::default() .credentials(creds)