diff --git a/CHANGELOG.md b/CHANGELOG.md index 161a3f6a..05c239a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - `dotenv` support is now optional. You can enable it with the `env-file` feature to have the same behavior as before ([#108](https://github.com/ramsayleung/rspotify/issues/108)). - Renamed environmental variables to `RSPOTIFY_CLIENT_ID`, `RSPOTIFY_CLIENT_SECRET` and `RSPOTIFY_REDIRECT_URI` to avoid name collisions with other libraries that use OAuth2 ([#118](https://github.com/ramsayleung/rspotify/issues/118)). - Fix typo in `user_playlist_remove_specific_occurrenes_of_tracks`, now it's `user_playlist_remove_specific_occurrences_of_tracks`. +- All fallible calls in the client return a `ClientError` rather than using `failure`. ## 0.10 (2020/07/01) diff --git a/Cargo.toml b/Cargo.toml index 2b633513..5d18c144 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ edition = "2018" [dependencies] chrono = { version = "0.4.13", features = ["serde", "rustc-serialize"] } dotenv = { version = "0.15.0", optional = true } -failure = "0.1.8" lazy_static = "1.4.0" log = "0.4.11" percent-encoding = "2.1.0" @@ -22,6 +21,7 @@ reqwest = { version = "0.10.7", features = ["json", "socks"], default-features = serde = { version = "1.0.115", features = ["derive"] } serde_json = "1.0.57" webbrowser = { version = "0.5.5", optional = true } +thiserror = "1.0.20" [dev-dependencies] async_once = { version = "0.1.0", features = ["tokio"] } diff --git a/src/blocking/client.rs b/src/blocking/client.rs index a902fee8..f099efdc 100644 --- a/src/blocking/client.rs +++ b/src/blocking/client.rs @@ -1,20 +1,18 @@ //! Client to Spotify API endpoint // 3rd-part library use chrono::prelude::*; -use failure::format_err; use log::{error, trace}; -use reqwest::blocking::Client; +use reqwest::blocking::{Client, Response}; use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}; -use reqwest::Method; -use reqwest::StatusCode; +use reqwest::{Method, StatusCode}; use serde::Deserialize; use serde_json::map::Map; use serde_json::{json, Value}; +use thiserror::Error; // Built-in battery use std::borrow::Cow; use std::collections::HashMap; -use std::fmt; use std::io::Read; use std::string::String; @@ -41,74 +39,70 @@ use crate::senum::{ AdditionalType, AlbumType, Country, IncludeExternal, RepeatState, SearchType, TimeRange, Type, }; -/// Describes API errors -#[derive(Debug, Deserialize)] -pub enum ApiError { +/// Possible errors returned from the `rspotify` client. +#[derive(Debug, Error)] +pub enum ClientError { + #[error("request unauthorized")] Unauthorized, + #[error("exceeded request limit")] RateLimited(Option), - #[serde(alias = "error")] - RegularError { - status: u16, - message: String, - }, - #[serde(alias = "error")] - PlayerError { - status: u16, - message: String, - reason: String, - }, - Other(u16), + #[error("spotify error: {0}")] + Api(#[from] ApiError), + #[error("json parse error: {0}")] + ParseJSON(#[from] serde_json::Error), + #[error("request error: {0}")] + Request(#[from] reqwest::Error), + #[error("status code: {0}")] + StatusCode(StatusCode), } -impl failure::Fail for ApiError {} -impl fmt::Display for ApiError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ApiError::Unauthorized => write!(f, "Unauthorized request to API"), - ApiError::RateLimited(e) => { - if let Some(d) = e { - write!(f, "Exceeded API request limit - please wait {} seconds", d) - } else { - write!(f, "Exceeded API request limit") - } - } - ApiError::RegularError { status, message } => { - write!(f, "Spotify API error code {}: {}", status, message) - } - ApiError::PlayerError { - status, - message, - reason, - } => write!( - f, - "Spotify API error code {} {}: {}", - status, reason, message + +impl From for ClientError { + fn from(response: Response) -> Self { + match response.status() { + StatusCode::UNAUTHORIZED => Self::Unauthorized, + StatusCode::TOO_MANY_REQUESTS => Self::RateLimited( + response + .headers() + .get(reqwest::header::RETRY_AFTER) + .and_then(|header| header.to_str().ok()) + .and_then(|duration| duration.parse().ok()), ), - ApiError::Other(s) => write!(f, "Spotify API reported error code {}", s), + status @ StatusCode::FORBIDDEN | status @ StatusCode::NOT_FOUND => response + .json::() + .map(Into::into) + .unwrap_or_else(|_| status.into()), + status => status.into(), } } } -impl From for ApiError { - fn from(response: reqwest::blocking::Response) -> Self { - match response.status() { - StatusCode::UNAUTHORIZED => ApiError::Unauthorized, - StatusCode::TOO_MANY_REQUESTS => { - if let Ok(duration) = response.headers()[reqwest::header::RETRY_AFTER].to_str() { - ApiError::RateLimited(duration.parse::().ok()) - } else { - ApiError::RateLimited(None) - } - } - status @ StatusCode::FORBIDDEN | status @ StatusCode::NOT_FOUND => { - if let Ok(reason) = response.json::() { - reason - } else { - ApiError::Other(status.as_u16()) - } - } - status => ApiError::Other(status.as_u16()), - } + +impl From for ClientError { + fn from(code: StatusCode) -> Self { + Self::StatusCode(code) } } + +/// Matches errors that are returned from the Spotfiy +/// API as part of the JSON response object. +#[derive(Debug, Error, Deserialize)] +pub enum ApiError { + /// See https://developer.spotify.com/documentation/web-api/reference/object-model/#error-object + #[error("{status}: {message}")] + #[serde(alias = "error")] + Regular { status: u16, message: String }, + + /// See https://developer.spotify.com/documentation/web-api/reference/object-model/#player-error-object + #[error("{status} ({reason}): {message}")] + #[serde(alias = "error")] + Player { + status: u16, + message: String, + reason: String, + }, +} + +type ClientResult = Result; + /// Spotify API object #[derive(Debug, Clone)] pub struct Spotify { @@ -173,7 +167,7 @@ impl Spotify { method: Method, url: &str, payload: Option<&Value>, - ) -> Result { + ) -> ClientResult { let mut url: Cow = url.into(); if !url.starts_with("http") { url = ["https://api.spotify.com/v1/", &url].concat().into(); @@ -207,15 +201,11 @@ impl Spotify { if response.status().is_success() { Ok(buf) } else { - Err(failure::Error::from(ApiError::from(response))) + Err(response.into()) } } ///send get request - fn get( - &self, - url: &str, - params: &mut HashMap, - ) -> Result { + fn get(&self, url: &str, params: &mut HashMap) -> ClientResult { if !params.is_empty() { let param: String = convert_map_to_string(params); let mut url_with_params = url.to_owned(); @@ -228,16 +218,16 @@ impl Spotify { } /// Send post request - fn post(&self, url: &str, payload: &Value) -> Result { + fn post(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::POST, url, Some(payload)) } /// Send put request - fn put(&self, url: &str, payload: &Value) -> Result { + fn put(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::PUT, url, Some(payload)) } /// Send delete request - fn delete(&self, url: &str, payload: &Value) -> Result { + fn delete(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::DELETE, url, Some(payload)) } @@ -245,7 +235,7 @@ impl Spotify { /// returns a single track given the track's ID, URI or URL /// Parameters: /// - track_id - a spotify URI, URL or ID - pub fn track(&self, track_id: &str) -> Result { + pub fn track(&self, track_id: &str) -> ClientResult { let trid = self.get_id(Type::Track, track_id); let url = format!("tracks/{}", trid); let result = self.get(&url, &mut HashMap::new())?; @@ -261,7 +251,7 @@ impl Spotify { &self, track_ids: Vec<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut ids: Vec = vec![]; for track_id in track_ids { ids.push(self.get_id(Type::Track, track_id)); @@ -281,7 +271,7 @@ impl Spotify { /// returns a single artist given the artist's ID, URI or URL /// Parameters: /// - artist_id - an artist ID, URI or URL - pub fn artist(&self, artist_id: &str) -> Result { + pub fn artist(&self, artist_id: &str) -> ClientResult { let trid = self.get_id(Type::Artist, artist_id); let url = format!("artists/{}", trid); // url.push_str(&trid); @@ -293,7 +283,7 @@ impl Spotify { /// returns a list of artists given the artist IDs, URIs, or URLs /// Parameters: /// - artist_ids - a list of artist IDs, URIs or URLs - pub fn artists(&self, artist_ids: Vec) -> Result { + pub fn artists(&self, artist_ids: Vec) -> ClientResult { let mut ids: Vec = vec![]; for artist_id in artist_ids { ids.push(self.get_id(Type::Artist, &artist_id)); @@ -318,7 +308,7 @@ impl Spotify { country: Option, limit: Option, offset: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params: HashMap = HashMap::new(); if let Some(_limit) = limit { params.insert("limit".to_owned(), _limit.to_string()); @@ -349,7 +339,7 @@ impl Spotify { &self, artist_id: &str, country: T, - ) -> Result { + ) -> ClientResult { let mut params: HashMap = HashMap::new(); let country = country .into() @@ -383,7 +373,7 @@ impl Spotify { /// Spotify community's listening history. /// Parameters: /// - artist_id - the artist ID, URI or URL - pub fn artist_related_artists(&self, artist_id: &str) -> Result { + pub fn artist_related_artists(&self, artist_id: &str) -> ClientResult { let trid = self.get_id(Type::Artist, artist_id); let url = format!("artists/{}/related-artists", trid); // url.push_str(&trid); @@ -396,7 +386,7 @@ impl Spotify { /// returns a single album given the album's ID, URIs or URL /// Parameters: /// - album_id - the album ID, URI or URL - pub fn album(&self, album_id: &str) -> Result { + pub fn album(&self, album_id: &str) -> ClientResult { let trid = self.get_id(Type::Album, album_id); let url = format!("albums/{}", trid); // url.push_str(&trid); @@ -408,7 +398,7 @@ impl Spotify { /// returns a list of albums given the album IDs, URIs, or URLs /// Parameters: /// - albums_ids - a list of album IDs, URIs or URLs - pub fn albums(&self, album_ids: Vec) -> Result { + pub fn albums(&self, album_ids: Vec) -> ClientResult { let mut ids: Vec = vec![]; for album_id in album_ids { ids.push(self.get_id(Type::Album, &album_id)); @@ -438,7 +428,7 @@ impl Spotify { offset: O, market: Option, include_external: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(10); let offset = offset.into().unwrap_or(0); @@ -471,7 +461,7 @@ impl Spotify { album_id: &str, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); let trid = self.get_id(Type::Album, album_id); let url = format!("albums/{}/tracks", trid); @@ -487,7 +477,7 @@ impl Spotify { /// Gets basic profile information about a Spotify User /// Parameters: /// - user - the id of the usr - pub fn user(&self, user_id: &str) -> Result { + pub fn user(&self, user_id: &str) -> ClientResult { let url = format!("users/{}", user_id); let result = self.get(&url, &mut HashMap::new())?; self.convert_result::(&result) @@ -503,7 +493,7 @@ impl Spotify { playlist_id: &str, fields: Option<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); if let Some(_fields) = fields { params.insert("fields".to_owned(), _fields.to_string()); @@ -527,7 +517,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -548,7 +538,7 @@ impl Spotify { user_id: &str, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -569,7 +559,7 @@ impl Spotify { playlist_id: Option<&mut str>, fields: Option<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); if let Some(_fields) = fields { params.insert("fields".to_owned(), _fields.to_string()); @@ -609,7 +599,7 @@ impl Spotify { limit: L, offset: O, market: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -638,7 +628,7 @@ impl Spotify { name: &str, public: P, description: D, - ) -> Result { + ) -> ClientResult { let public = public.into().unwrap_or(true); let description = description.into().unwrap_or_else(|| "".to_owned()); let params = json!({ @@ -668,7 +658,7 @@ impl Spotify { public: Option, description: Option, collaborative: Option, - ) -> Result { + ) -> ClientResult { let mut params = Map::new(); if let Some(_name) = name { params.insert("name".to_owned(), _name.into()); @@ -691,11 +681,7 @@ impl Spotify { /// Parameters: /// - user_id - the id of the user /// - playlist_id - the id of the playlist - pub fn user_playlist_unfollow( - &self, - user_id: &str, - playlist_id: &str, - ) -> Result { + pub fn user_playlist_unfollow(&self, user_id: &str, playlist_id: &str) -> ClientResult { let url = format!("users/{}/playlists/{}/followers", user_id, playlist_id); self.delete(&url, &json!({})) } @@ -713,7 +699,7 @@ impl Spotify { playlist_id: &str, track_ids: &[String], position: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -740,7 +726,7 @@ impl Spotify { user_id: &str, playlist_id: &str, track_ids: &[String], - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -773,7 +759,7 @@ impl Spotify { range_length: R, insert_before: i32, snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let range_length = range_length.into().unwrap_or(1); let mut params = Map::new(); @@ -801,7 +787,7 @@ impl Spotify { playlist_id: &str, track_ids: &[String], snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -860,7 +846,7 @@ impl Spotify { playlist_id: &str, tracks: Vec>, snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let mut params = Map::new(); let plid = self.get_id(Type::Playlist, playlist_id); let mut ftracks: Vec> = vec![]; @@ -894,7 +880,7 @@ impl Spotify { playlist_owner_id: &str, playlist_id: &str, public: P, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let mut map = Map::new(); let public = public.into().unwrap_or(true); map.insert("public".to_owned(), public.into()); @@ -920,7 +906,7 @@ impl Spotify { playlist_owner_id: &str, playlist_id: &str, user_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); } @@ -937,7 +923,7 @@ impl Spotify { /// [get current users profile](https://developer.spotify.com/web-api/get-current-users-profile/) /// Get detailed profile information about the current user. /// An alias for the 'current_user' method. - pub fn me(&self) -> Result { + pub fn me(&self) -> ClientResult { let mut dumb: HashMap = HashMap::new(); let url = String::from("me/"); let result = self.get(&url, &mut dumb)?; @@ -945,13 +931,13 @@ impl Spotify { } /// Get detailed profile information about the current user. /// An alias for the 'me' method. - pub fn current_user(&self) -> Result { + pub fn current_user(&self) -> ClientResult { self.me() } /// [get the users currently playing track](https://developer.spotify.com/web-api/get-the-users-currently-playing-track/) /// Get information about the current users currently playing track. - pub fn current_user_playing_track(&self) -> Result, failure::Error> { + pub fn current_user_playing_track(&self) -> ClientResult> { let mut dumb = HashMap::new(); let url = String::from("me/player/currently-playing"); match self.get(&url, &mut dumb) { @@ -977,7 +963,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let mut params = HashMap::new(); @@ -996,7 +982,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let mut params = HashMap::new(); @@ -1015,7 +1001,7 @@ impl Spotify { &self, limit: L, after: Option, - ) -> Result { + ) -> ClientResult { let limit = limit.into().unwrap_or(20); let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.to_string()); @@ -1033,10 +1019,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - track_ids - a list of track URIs, URLs or IDs - pub fn current_user_saved_tracks_delete( - &self, - track_ids: &[String], - ) -> Result<(), failure::Error> { + pub fn current_user_saved_tracks_delete(&self, track_ids: &[String]) -> ClientResult<()> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1056,7 +1039,7 @@ impl Spotify { pub fn current_user_saved_tracks_contains( &self, track_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1072,10 +1055,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - track_ids - a list of track URIs, URLs or IDs - pub fn current_user_saved_tracks_add( - &self, - track_ids: &[String], - ) -> Result<(), failure::Error> { + pub fn current_user_saved_tracks_add(&self, track_ids: &[String]) -> ClientResult<()> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1102,7 +1082,7 @@ impl Spotify { limit: L, offset: O, time_range: T, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let time_range = time_range.into().unwrap_or(TimeRange::MediumTerm); @@ -1130,7 +1110,7 @@ impl Spotify { limit: L, offset: O, time_range: T, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let time_range = time_range.into().unwrap_or(TimeRange::MediumTerm); @@ -1150,7 +1130,7 @@ impl Spotify { pub fn current_user_recently_played>>( &self, limit: L, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(50); let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.to_string()); @@ -1164,10 +1144,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - album_ids - a list of album URIs, URLs or IDs - pub fn current_user_saved_albums_add( - &self, - album_ids: &[String], - ) -> Result<(), failure::Error> { + pub fn current_user_saved_albums_add(&self, album_ids: &[String]) -> ClientResult<()> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1184,10 +1161,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - album_ids - a list of album URIs, URLs or IDs - pub fn current_user_saved_albums_delete( - &self, - album_ids: &[String], - ) -> Result<(), failure::Error> { + pub fn current_user_saved_albums_delete(&self, album_ids: &[String]) -> ClientResult<()> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1207,7 +1181,7 @@ impl Spotify { pub fn current_user_saved_albums_contains( &self, album_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1222,7 +1196,7 @@ impl Spotify { /// Follow one or more artists /// Parameters: /// - artist_ids - a list of artist IDs - pub fn user_follow_artists(&self, artist_ids: &[String]) -> Result<(), failure::Error> { + pub fn user_follow_artists(&self, artist_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", artist_ids.join(",")); match self.put(&url, &json!({})) { Ok(_) => Ok(()), @@ -1234,7 +1208,7 @@ impl Spotify { /// Unfollow one or more artists /// Parameters: /// - artist_ids - a list of artist IDs - pub fn user_unfollow_artists(&self, artist_ids: &[String]) -> Result<(), failure::Error> { + pub fn user_unfollow_artists(&self, artist_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", artist_ids.join(",")); match self.delete(&url, &json!({})) { Ok(_) => Ok(()), @@ -1247,10 +1221,7 @@ impl Spotify { /// Check to see if the given users are following the given artists /// Parameters: /// - artist_ids - the ids of the users that you want to - pub fn user_artist_check_follow( - &self, - artsit_ids: &[String], - ) -> Result, failure::Error> { + pub fn user_artist_check_follow(&self, artsit_ids: &[String]) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", artsit_ids.join(",") @@ -1264,7 +1235,7 @@ impl Spotify { /// Follow one or more users /// Parameters: /// - user_ids - a list of artist IDs - pub fn user_follow_users(&self, user_ids: &[String]) -> Result<(), failure::Error> { + pub fn user_follow_users(&self, user_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", user_ids.join(",")); match self.put(&url, &json!({})) { Ok(_) => Ok(()), @@ -1276,7 +1247,7 @@ impl Spotify { /// Unfollow one or more users /// Parameters: /// - user_ids - a list of artist IDs - pub fn user_unfollow_users(&self, user_ids: &[String]) -> Result<(), failure::Error> { + pub fn user_unfollow_users(&self, user_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", user_ids.join(",")); match self.delete(&url, &json!({})) { Ok(_) => Ok(()), @@ -1307,7 +1278,7 @@ impl Spotify { timestamp: Option>, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1341,7 +1312,7 @@ impl Spotify { country: Option, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1373,7 +1344,7 @@ impl Spotify { country: Option, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1411,7 +1382,7 @@ impl Spotify { limit: L, country: Option, payload: &Map, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); params.insert("limit".to_owned(), limit.to_string()); @@ -1467,7 +1438,7 @@ impl Spotify { /// [get audio features](https://developer.spotify.com/web-api/get-audio-features/) /// Get audio features for a track /// - track - track URI, URL or ID - pub fn audio_features(&self, track: &str) -> Result { + pub fn audio_features(&self, track: &str) -> ClientResult { let track_id = self.get_id(Type::Track, track); let url = format!("audio-features/{}", track_id); let mut dumb = HashMap::new(); @@ -1478,10 +1449,7 @@ impl Spotify { /// [get several audio features](https://developer.spotify.com/web-api/get-several-audio-features/) /// Get Audio Features for Several Tracks /// -tracks a list of track URIs, URLs or IDs - pub fn audios_features( - &self, - tracks: &[String], - ) -> Result, failure::Error> { + pub fn audios_features(&self, tracks: &[String]) -> ClientResult> { let ids: Vec = tracks .iter() .map(|track| self.get_id(Type::Track, track)) @@ -1504,7 +1472,7 @@ impl Spotify { /// Get Audio Analysis for a Track /// Parameters: /// - track_id - a track URI, URL or ID - pub fn audio_analysis(&self, track: &str) -> Result { + pub fn audio_analysis(&self, track: &str) -> ClientResult { let trid = self.get_id(Type::Track, track); let url = format!("audio-analysis/{}", trid); let mut dumb = HashMap::new(); @@ -1514,7 +1482,7 @@ impl Spotify { /// [get a users available devices](https://developer.spotify.com/web-api/get-a-users-available-devices/) /// Get a User’s Available Devices - pub fn device(&self) -> Result { + pub fn device(&self) -> ClientResult { let url = String::from("me/player/devices"); let mut dumb = HashMap::new(); let result = self.get(&url, &mut dumb)?; @@ -1530,7 +1498,7 @@ impl Spotify { &self, market: Option, additional_types: Option>, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = String::from("me/player"); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1567,7 +1535,7 @@ impl Spotify { &self, market: Option, additional_types: Option>, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = String::from("me/player/currently-playing"); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1606,7 +1574,7 @@ impl Spotify { &self, device_id: &str, force_play: T, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let device_ids = vec![device_id.to_owned()]; let force_play = force_play.into().unwrap_or(true); let mut payload = Map::new(); @@ -1643,7 +1611,7 @@ impl Spotify { uris: Option>, offset: Option, position_ms: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { if context_uri.is_some() && uris.is_some() { error!("specify either contexxt uri or uris, not both"); } @@ -1679,7 +1647,7 @@ impl Spotify { /// Pause a User’s Playback /// Parameters: /// - device_id - device target for playback - pub fn pause_playback(&self, device_id: Option) -> Result<(), failure::Error> { + pub fn pause_playback(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/pause", device_id); match self.put(&url, &json!({})) { Ok(_) => Ok(()), @@ -1691,7 +1659,7 @@ impl Spotify { /// Skip User’s Playback To Next Track /// Parameters: /// - device_id - device target for playback - pub fn next_track(&self, device_id: Option) -> Result<(), failure::Error> { + pub fn next_track(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/next", device_id); match self.post(&url, &json!({})) { Ok(_) => Ok(()), @@ -1703,7 +1671,7 @@ impl Spotify { ///Skip User’s Playback To Previous Track /// Parameters: /// - device_id - device target for playback - pub fn previous_track(&self, device_id: Option) -> Result<(), failure::Error> { + pub fn previous_track(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/previous", device_id); match self.post(&url, &json!({})) { Ok(_) => Ok(()), @@ -1716,11 +1684,7 @@ impl Spotify { /// Parameters: /// - position_ms - position in milliseconds to seek to /// - device_id - device target for playback - pub fn seek_track( - &self, - position_ms: u32, - device_id: Option, - ) -> Result<(), failure::Error> { + pub fn seek_track(&self, position_ms: u32, device_id: Option) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/seek?position_ms={}", position_ms), device_id, @@ -1736,11 +1700,7 @@ impl Spotify { /// Parameters: /// - state - `track`, `context`, or `off` /// - device_id - device target for playback - pub fn repeat( - &self, - state: RepeatState, - device_id: Option, - ) -> Result<(), failure::Error> { + pub fn repeat(&self, state: RepeatState, device_id: Option) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/repeat?state={}", state.as_str()), device_id, @@ -1756,11 +1716,7 @@ impl Spotify { /// Parameters: /// - volume_percent - volume between 0 and 100 /// - device_id - device target for playback - pub fn volume( - &self, - volume_percent: u8, - device_id: Option, - ) -> Result<(), failure::Error> { + pub fn volume(&self, volume_percent: u8, device_id: Option) -> ClientResult<()> { if volume_percent > 100u8 { error!("volume must be between 0 and 100, inclusive"); } @@ -1779,7 +1735,7 @@ impl Spotify { /// Parameters: /// - state - true or false /// - device_id - device target for playback - pub fn shuffle(&self, state: bool, device_id: Option) -> Result<(), failure::Error> { + pub fn shuffle(&self, state: bool, device_id: Option) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); match self.put(&url, &json!({})) { Ok(_) => Ok(()), @@ -1793,11 +1749,7 @@ impl Spotify { /// - 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 - pub fn add_item_to_queue( - &self, - item: String, - device_id: Option, - ) -> Result<(), failure::Error> { + pub fn add_item_to_queue(&self, item: String, device_id: Option) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/queue?uri={}", &item), device_id); match self.post(&url, &json!({})) { Ok(_) => Ok(()), @@ -1808,7 +1760,7 @@ impl Spotify { /// 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. - pub fn save_shows(&self, ids: Vec) -> Result<(), failure::Error> { + pub fn save_shows(&self, ids: Vec) -> ClientResult<()> { let joined_ids = ids.join(","); let url = format!("me/shows/?ids={}", joined_ids); match self.put(&url, &json!({})) { @@ -1825,7 +1777,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1842,11 +1794,7 @@ impl Spotify { /// - id: The Spotify ID for the show. /// Query Parameters /// - market(Optional): An ISO 3166-1 alpha-2 country code. - pub fn get_a_show( - &self, - id: String, - market: Option, - ) -> Result { + pub fn get_a_show(&self, id: String, market: Option) -> ClientResult { let url = format!("shows/{}", id); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1865,7 +1813,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result { + ) -> ClientResult { let joined_ids = ids.join(","); let url = "shows"; let mut params = HashMap::new(); @@ -1877,18 +1825,8 @@ impl Spotify { self.convert_result::(&result) } - pub fn convert_result<'a, T: Deserialize<'a>>( - &self, - input: &'a str, - ) -> Result { - let result = serde_json::from_str::(input).map_err(|e| { - format_err!( - "convert result failed, reason: {:?}; content: [{:?}]", - e, - input - ) - })?; - Ok(result) + pub fn convert_result<'a, T: Deserialize<'a>>(&self, input: &'a str) -> ClientResult { + serde_json::from_str::(input).map_err(Into::into) } /// Get Spotify catalog information about an show’s episodes. Optional parameters can be used to limit the number of episodes returned. @@ -1905,7 +1843,7 @@ impl Spotify { limit: L, offset: O, market: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = format!("shows/{}/episodes", id); let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); @@ -1925,11 +1863,7 @@ impl Spotify { /// - id: The Spotify ID for the episode. /// Query Parameters /// - market: Optional. An ISO 3166-1 alpha-2 country code. - pub fn get_an_episode( - &self, - id: String, - market: Option, - ) -> Result { + pub fn get_an_episode(&self, id: String, market: Option) -> ClientResult { let url = format!("episodes/{}", id); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1948,7 +1882,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result { + ) -> ClientResult { let url = "episodes"; let joined_ids = ids.join(","); let mut params = HashMap::new(); @@ -1964,7 +1898,7 @@ impl Spotify { /// [Check users saved shows](https://developer.spotify.com/documentation/web-api/reference/library/check-users-saved-shows/) /// Query Parameters /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - pub fn check_users_saved_shows(&self, ids: Vec) -> Result, failure::Error> { + pub fn check_users_saved_shows(&self, ids: Vec) -> ClientResult> { let url = "me/shows/contains"; let joined_ids = ids.join(","); let mut params = HashMap::new(); @@ -1983,7 +1917,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let joined_ids = ids.join(","); let url = format!("me/shows?ids={}", joined_ids); let mut payload = Map::new(); diff --git a/src/client.rs b/src/client.rs index a2786c53..da622434 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,20 +1,17 @@ //! Client to Spotify API endpoint // 3rd-part library use chrono::prelude::*; -use failure::format_err; use log::{error, trace}; use reqwest::header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}; -use reqwest::Client; -use reqwest::Method; -use reqwest::StatusCode; +use reqwest::{Client, Method, Response, StatusCode}; use serde::Deserialize; use serde_json::map::Map; use serde_json::{json, Value}; +use thiserror::Error; // Built-in battery use std::borrow::Cow; use std::collections::HashMap; -use std::fmt; use std::string::String; use super::model::album::{FullAlbum, FullAlbums, PageSimpliedAlbums, SavedAlbum, SimplifiedAlbum}; @@ -40,74 +37,71 @@ use super::senum::{ }; use super::util::convert_map_to_string; -/// Describes API errors -#[derive(Debug, Deserialize)] -pub enum ApiError { +/// Possible errors returned from the `rspotify` client. +#[derive(Debug, Error)] +pub enum ClientError { + #[error("request unauthorized")] Unauthorized, + #[error("exceeded request limit")] RateLimited(Option), - #[serde(alias = "error")] - RegularError { - status: u16, - message: String, - }, - #[serde(alias = "error")] - PlayerError { - status: u16, - message: String, - reason: String, - }, - Other(u16), + #[error("spotify error: {0}")] + Api(#[from] ApiError), + #[error("json parse error: {0}")] + ParseJSON(#[from] serde_json::Error), + #[error("request error: {0}")] + Request(#[from] reqwest::Error), + #[error("status code: {0}")] + StatusCode(StatusCode), } -impl failure::Fail for ApiError {} -impl fmt::Display for ApiError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - ApiError::Unauthorized => write!(f, "Unauthorized request to API"), - ApiError::RateLimited(e) => { - if let Some(d) = e { - write!(f, "Exceeded API request limit - please wait {} seconds", d) - } else { - write!(f, "Exceeded API request limit") - } - } - ApiError::RegularError { status, message } => { - write!(f, "Spotify API error code {}: {}", status, message) - } - ApiError::PlayerError { - status, - message, - reason, - } => write!( - f, - "Spotify API error code {} {}: {}", - status, reason, message + +impl ClientError { + async fn from_response(response: Response) -> Self { + match response.status() { + StatusCode::UNAUTHORIZED => Self::Unauthorized, + StatusCode::TOO_MANY_REQUESTS => Self::RateLimited( + response + .headers() + .get(reqwest::header::RETRY_AFTER) + .and_then(|header| header.to_str().ok()) + .and_then(|duration| duration.parse().ok()), ), - ApiError::Other(s) => write!(f, "Spotify API reported error code {}", s), + status @ StatusCode::FORBIDDEN | status @ StatusCode::NOT_FOUND => response + .json::() + .await + .map(Into::into) + .unwrap_or_else(|_| status.into()), + status => status.into(), } } } -impl ApiError { - async fn from_response(response: reqwest::Response) -> Self { - match response.status() { - StatusCode::UNAUTHORIZED => ApiError::Unauthorized, - StatusCode::TOO_MANY_REQUESTS => { - if let Ok(duration) = response.headers()[reqwest::header::RETRY_AFTER].to_str() { - ApiError::RateLimited(duration.parse::().ok()) - } else { - ApiError::RateLimited(None) - } - } - status @ StatusCode::FORBIDDEN | status @ StatusCode::NOT_FOUND => { - if let Ok(reason) = response.json::().await { - reason - } else { - ApiError::Other(status.as_u16()) - } - } - status => ApiError::Other(status.as_u16()), - } + +impl From for ClientError { + fn from(code: StatusCode) -> Self { + Self::StatusCode(code) } } + +/// Matches errors that are returned from the Spotfiy +/// API as part of the JSON response object. +#[derive(Debug, Error, Deserialize)] +pub enum ApiError { + /// See https://developer.spotify.com/documentation/web-api/reference/object-model/#error-object + #[error("{status}: {message}")] + #[serde(alias = "error")] + Regular { status: u16, message: String }, + + /// See https://developer.spotify.com/documentation/web-api/reference/object-model/#player-error-object + #[error("{status} ({reason}): {message}")] + #[serde(alias = "error")] + Player { + status: u16, + message: String, + reason: String, + }, +} + +type ClientResult = Result; + /// Spotify API object #[derive(Debug, Clone)] pub struct Spotify { @@ -172,7 +166,7 @@ impl Spotify { method: Method, url: &str, payload: Option<&Value>, - ) -> Result { + ) -> ClientResult { let mut url: Cow = url.into(); if !url.starts_with("http") { url = ["https://api.spotify.com/v1/", &url].concat().into(); @@ -196,29 +190,17 @@ impl Spotify { builder }; - builder.send().await? + builder.send().await.map_err(ClientError::from)? }; if response.status().is_success() { - match response.text().await { - Ok(text) => Ok(text), - Err(e) => Err(failure::err_msg(format!( - "Error getting text out of response {}", - e - ))), - } + response.text().await.map_err(Into::into) } else { - Err(failure::Error::from( - ApiError::from_response(response).await, - )) + Err(ClientError::from_response(response).await) } } /// Send get request - async fn get( - &self, - url: &str, - params: &mut HashMap, - ) -> Result { + async fn get(&self, url: &str, params: &mut HashMap) -> ClientResult { if !params.is_empty() { let param: String = convert_map_to_string(params); let mut url_with_params = url.to_owned(); @@ -232,15 +214,15 @@ impl Spotify { } /// Send post request - async fn post(&self, url: &str, payload: &Value) -> Result { + async fn post(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::POST, url, Some(payload)).await } /// Send put request - async fn put(&self, url: &str, payload: &Value) -> Result { + async fn put(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::PUT, url, Some(payload)).await } /// send delete request - async fn delete(&self, url: &str, payload: &Value) -> Result { + async fn delete(&self, url: &str, payload: &Value) -> ClientResult { self.internal_call(Method::DELETE, url, Some(payload)).await } @@ -248,7 +230,7 @@ impl Spotify { /// returns a single track given the track's ID, URI or URL /// Parameters: /// - track_id - a spotify URI, URL or ID - pub async fn track(&self, track_id: &str) -> Result { + pub async fn track(&self, track_id: &str) -> ClientResult { let trid = self.get_id(Type::Track, track_id); let url = format!("tracks/{}", trid); let result = self.get(&url, &mut HashMap::new()).await?; @@ -264,7 +246,7 @@ impl Spotify { &self, track_ids: Vec<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut ids: Vec = vec![]; for track_id in track_ids { ids.push(self.get_id(Type::Track, track_id)); @@ -283,7 +265,7 @@ impl Spotify { /// returns a single artist given the artist's ID, URI or URL /// Parameters: /// - artist_id - an artist ID, URI or URL - pub async fn artist(&self, artist_id: &str) -> Result { + pub async fn artist(&self, artist_id: &str) -> ClientResult { let trid = self.get_id(Type::Artist, artist_id); let url = format!("artists/{}", trid); let result = self.get(&url, &mut HashMap::new()).await?; @@ -294,7 +276,7 @@ impl Spotify { /// returns a list of artists given the artist IDs, URIs, or URLs /// Parameters: /// - artist_ids - a list of artist IDs, URIs or URLs - pub async fn artists(&self, artist_ids: Vec) -> Result { + pub async fn artists(&self, artist_ids: Vec) -> ClientResult { let mut ids: Vec = vec![]; for artist_id in artist_ids { ids.push(self.get_id(Type::Artist, &artist_id)); @@ -318,7 +300,7 @@ impl Spotify { country: Option, limit: Option, offset: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params: HashMap = HashMap::new(); if let Some(_limit) = limit { params.insert("limit".to_owned(), _limit.to_string()); @@ -347,7 +329,7 @@ impl Spotify { &self, artist_id: &str, country: T, - ) -> Result { + ) -> ClientResult { let mut params: HashMap = HashMap::new(); let country = country .into() @@ -368,10 +350,7 @@ impl Spotify { /// Spotify community's listening history. /// Parameters: /// - artist_id - the artist ID, URI or URL - pub async fn artist_related_artists( - &self, - artist_id: &str, - ) -> Result { + pub async fn artist_related_artists(&self, artist_id: &str) -> ClientResult { let trid = self.get_id(Type::Artist, artist_id); let url = format!("artists/{}/related-artists", trid); let result = self.get(&url, &mut HashMap::new()).await?; @@ -382,7 +361,7 @@ impl Spotify { /// returns a single album given the album's ID, URIs or URL /// Parameters: /// - album_id - the album ID, URI or URL - pub async fn album(&self, album_id: &str) -> Result { + pub async fn album(&self, album_id: &str) -> ClientResult { let trid = self.get_id(Type::Album, album_id); let url = format!("albums/{}", trid); let result = self.get(&url, &mut HashMap::new()).await?; @@ -393,7 +372,7 @@ impl Spotify { /// returns a list of albums given the album IDs, URIs, or URLs /// Parameters: /// - albums_ids - a list of album IDs, URIs or URLs - pub async fn albums(&self, album_ids: Vec) -> Result { + pub async fn albums(&self, album_ids: Vec) -> ClientResult { let mut ids: Vec = vec![]; for album_id in album_ids { ids.push(self.get_id(Type::Album, &album_id)); @@ -423,7 +402,7 @@ impl Spotify { offset: O, market: Option, include_external: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(10); let offset = offset.into().unwrap_or(0); @@ -456,7 +435,7 @@ impl Spotify { album_id: &str, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); let trid = self.get_id(Type::Album, album_id); let url = format!("albums/{}/tracks", trid); @@ -470,7 +449,7 @@ impl Spotify { ///Gets basic profile information about a Spotify User ///Parameters: ///- user - the id of the usr - pub async fn user(&self, user_id: &str) -> Result { + pub async fn user(&self, user_id: &str) -> ClientResult { let url = format!("users/{}", user_id); let result = self.get(&url, &mut HashMap::new()).await?; self.convert_result::(&result) @@ -486,7 +465,7 @@ impl Spotify { playlist_id: &str, fields: Option<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); if let Some(_fields) = fields { params.insert("fields".to_owned(), _fields.to_string()); @@ -510,7 +489,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -531,7 +510,7 @@ impl Spotify { user_id: &str, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -552,7 +531,7 @@ impl Spotify { playlist_id: Option<&mut str>, fields: Option<&str>, market: Option, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); if let Some(_fields) = fields { params.insert("fields".to_owned(), _fields.to_string()); @@ -592,7 +571,7 @@ impl Spotify { limit: L, offset: O, market: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.into().unwrap_or(50).to_string()); params.insert("offset".to_owned(), offset.into().unwrap_or(0).to_string()); @@ -621,7 +600,7 @@ impl Spotify { name: &str, public: P, description: D, - ) -> Result { + ) -> ClientResult { let public = public.into().unwrap_or(true); let description = description.into().unwrap_or_else(|| "".to_owned()); let params = json!({ @@ -651,7 +630,7 @@ impl Spotify { public: Option, description: Option, collaborative: Option, - ) -> Result { + ) -> ClientResult { let mut params = Map::new(); if let Some(_name) = name { params.insert("name".to_owned(), _name.into()); @@ -678,7 +657,7 @@ impl Spotify { &self, user_id: &str, playlist_id: &str, - ) -> Result { + ) -> ClientResult { let url = format!("users/{}/playlists/{}/followers", user_id, playlist_id); self.delete(&url, &json!({})).await } @@ -696,7 +675,7 @@ impl Spotify { playlist_id: &str, track_ids: &[String], position: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -722,7 +701,7 @@ impl Spotify { user_id: &str, playlist_id: &str, track_ids: &[String], - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -755,7 +734,7 @@ impl Spotify { range_length: R, insert_before: i32, snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let range_length = range_length.into().unwrap_or(1); let mut params = Map::new(); @@ -783,7 +762,7 @@ impl Spotify { playlist_id: &str, track_ids: &[String], snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let plid = self.get_id(Type::Playlist, playlist_id); let uris: Vec = track_ids .iter() @@ -839,7 +818,7 @@ impl Spotify { playlist_id: &str, tracks: Vec>, snapshot_id: Option, - ) -> Result { + ) -> ClientResult { let mut params = Map::new(); let plid = self.get_id(Type::Playlist, playlist_id); let mut ftracks: Vec> = vec![]; @@ -873,7 +852,7 @@ impl Spotify { playlist_owner_id: &str, playlist_id: &str, public: P, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let mut map = Map::new(); let public = public.into().unwrap_or(true); map.insert("public".to_owned(), public.into()); @@ -899,7 +878,7 @@ impl Spotify { playlist_owner_id: &str, playlist_id: &str, user_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { if user_ids.len() > 5 { error!("The maximum length of user ids is limited to 5 :-)"); } @@ -916,7 +895,7 @@ impl Spotify { /// [get current users profile](https://developer.spotify.com/web-api/get-current-users-profile/) /// Get detailed profile information about the current user. /// An alias for the 'current_user' method. - pub async fn me(&self) -> Result { + pub async fn me(&self) -> ClientResult { let mut dumb: HashMap = HashMap::new(); let url = String::from("me/"); let result = self.get(&url, &mut dumb).await?; @@ -924,13 +903,13 @@ impl Spotify { } /// Get detailed profile information about the current user. /// An alias for the 'me' method. - pub async fn current_user(&self) -> Result { + pub async fn current_user(&self) -> ClientResult { self.me().await } /// [get the users currently playing track](https://developer.spotify.com/web-api/get-the-users-currently-playing-track/) /// Get information about the current users currently playing track. - pub async fn current_user_playing_track(&self) -> Result, failure::Error> { + pub async fn current_user_playing_track(&self) -> ClientResult> { let mut dumb = HashMap::new(); let url = String::from("me/player/currently-playing"); match self.get(&url, &mut dumb).await { @@ -956,7 +935,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let mut params = HashMap::new(); @@ -975,7 +954,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let mut params = HashMap::new(); @@ -994,7 +973,7 @@ impl Spotify { &self, limit: L, after: Option, - ) -> Result { + ) -> ClientResult { let limit = limit.into().unwrap_or(20); let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.to_string()); @@ -1012,10 +991,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - track_ids - a list of track URIs, URLs or IDs - pub async fn current_user_saved_tracks_delete( - &self, - track_ids: &[String], - ) -> Result<(), failure::Error> { + pub async fn current_user_saved_tracks_delete(&self, track_ids: &[String]) -> ClientResult<()> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1035,7 +1011,7 @@ impl Spotify { pub async fn current_user_saved_tracks_contains( &self, track_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1051,10 +1027,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - track_ids - a list of track URIs, URLs or IDs - pub async fn current_user_saved_tracks_add( - &self, - track_ids: &[String], - ) -> Result<(), failure::Error> { + pub async fn current_user_saved_tracks_add(&self, track_ids: &[String]) -> ClientResult<()> { let uris: Vec = track_ids .iter() .map(|id| self.get_id(Type::Track, id)) @@ -1081,7 +1054,7 @@ impl Spotify { limit: L, offset: O, time_range: T, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let time_range = time_range.into().unwrap_or(TimeRange::MediumTerm); @@ -1109,7 +1082,7 @@ impl Spotify { limit: L, offset: O, time_range: T, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); let time_range = time_range.into().unwrap_or(TimeRange::MediumTerm); @@ -1129,7 +1102,7 @@ impl Spotify { pub async fn current_user_recently_played>>( &self, limit: L, - ) -> Result, failure::Error> { + ) -> ClientResult> { let limit = limit.into().unwrap_or(50); let mut params = HashMap::new(); params.insert("limit".to_owned(), limit.to_string()); @@ -1143,10 +1116,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - album_ids - a list of album URIs, URLs or IDs - pub async fn current_user_saved_albums_add( - &self, - album_ids: &[String], - ) -> Result<(), failure::Error> { + pub async fn current_user_saved_albums_add(&self, album_ids: &[String]) -> ClientResult<()> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1163,10 +1133,7 @@ impl Spotify { /// "Your Music" library. /// Parameters: /// - album_ids - a list of album URIs, URLs or IDs - pub async fn current_user_saved_albums_delete( - &self, - album_ids: &[String], - ) -> Result<(), failure::Error> { + pub async fn current_user_saved_albums_delete(&self, album_ids: &[String]) -> ClientResult<()> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1186,7 +1153,7 @@ impl Spotify { pub async fn current_user_saved_albums_contains( &self, album_ids: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { let uris: Vec = album_ids .iter() .map(|id| self.get_id(Type::Album, id)) @@ -1201,7 +1168,7 @@ impl Spotify { /// Follow one or more artists /// Parameters: /// - artist_ids - a list of artist IDs - pub async fn user_follow_artists(&self, artist_ids: &[String]) -> Result<(), failure::Error> { + pub async fn user_follow_artists(&self, artist_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", artist_ids.join(",")); match self.put(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1213,7 +1180,7 @@ impl Spotify { /// Unfollow one or more artists /// Parameters: /// - artist_ids - a list of artist IDs - pub async fn user_unfollow_artists(&self, artist_ids: &[String]) -> Result<(), failure::Error> { + pub async fn user_unfollow_artists(&self, artist_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=artist&ids={}", artist_ids.join(",")); match self.delete(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1226,10 +1193,7 @@ impl Spotify { /// Check to see if the given users are following the given artists /// Parameters: /// - artist_ids - the ids of the users that you want to - pub async fn user_artist_check_follow( - &self, - artsit_ids: &[String], - ) -> Result, failure::Error> { + pub async fn user_artist_check_follow(&self, artsit_ids: &[String]) -> ClientResult> { let url = format!( "me/following/contains?type=artist&ids={}", artsit_ids.join(",") @@ -1243,7 +1207,7 @@ impl Spotify { /// Follow one or more users /// Parameters: /// - user_ids - a list of artist IDs - pub async fn user_follow_users(&self, user_ids: &[String]) -> Result<(), failure::Error> { + pub async fn user_follow_users(&self, user_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", user_ids.join(",")); match self.put(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1255,7 +1219,7 @@ impl Spotify { /// Unfollow one or more users /// Parameters: /// - user_ids - a list of artist IDs - pub async fn user_unfollow_users(&self, user_ids: &[String]) -> Result<(), failure::Error> { + pub async fn user_unfollow_users(&self, user_ids: &[String]) -> ClientResult<()> { let url = format!("me/following?type=user&ids={}", user_ids.join(",")); match self.delete(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1286,7 +1250,7 @@ impl Spotify { timestamp: Option>, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1320,7 +1284,7 @@ impl Spotify { country: Option, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1352,7 +1316,7 @@ impl Spotify { country: Option, limit: L, offset: O, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1390,7 +1354,7 @@ impl Spotify { limit: L, country: Option, payload: &Map, - ) -> Result { + ) -> ClientResult { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); params.insert("limit".to_owned(), limit.to_string()); @@ -1446,7 +1410,7 @@ impl Spotify { /// [get audio features](https://developer.spotify.com/web-api/get-audio-features/) /// Get audio features for a track /// - track - track URI, URL or ID - pub async fn audio_features(&self, track: &str) -> Result { + pub async fn audio_features(&self, track: &str) -> ClientResult { let track_id = self.get_id(Type::Track, track); let url = format!("audio-features/{}", track_id); let mut dumb = HashMap::new(); @@ -1460,7 +1424,7 @@ impl Spotify { pub async fn audios_features( &self, tracks: &[String], - ) -> Result, failure::Error> { + ) -> ClientResult> { let ids: Vec = tracks .iter() .map(|track| self.get_id(Type::Track, track)) @@ -1483,7 +1447,7 @@ impl Spotify { /// Get Audio Analysis for a Track /// Parameters: /// - track_id - a track URI, URL or ID - pub async fn audio_analysis(&self, track: &str) -> Result { + pub async fn audio_analysis(&self, track: &str) -> ClientResult { let trid = self.get_id(Type::Track, track); let url = format!("audio-analysis/{}", trid); let mut dumb = HashMap::new(); @@ -1493,7 +1457,7 @@ impl Spotify { /// [get a users available devices](https://developer.spotify.com/web-api/get-a-users-available-devices/) /// Get a User’s Available Devices - pub async fn device(&self) -> Result { + pub async fn device(&self) -> ClientResult { let url = String::from("me/player/devices"); let mut dumb = HashMap::new(); let result = self.get(&url, &mut dumb).await?; @@ -1509,7 +1473,7 @@ impl Spotify { &self, market: Option, additional_types: Option>, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = String::from("me/player"); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1546,7 +1510,7 @@ impl Spotify { &self, market: Option, additional_types: Option>, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = String::from("me/player/currently-playing"); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1585,7 +1549,7 @@ impl Spotify { &self, device_id: &str, force_play: T, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let device_ids = vec![device_id.to_owned()]; let force_play = force_play.into().unwrap_or(true); let mut payload = Map::new(); @@ -1622,7 +1586,7 @@ impl Spotify { uris: Option>, offset: Option, position_ms: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { if context_uri.is_some() && uris.is_some() { error!("specify either contexxt uri or uris, not both"); } @@ -1658,7 +1622,7 @@ impl Spotify { /// Pause a User’s Playback /// Parameters: /// - device_id - device target for playback - pub async fn pause_playback(&self, device_id: Option) -> Result<(), failure::Error> { + pub async fn pause_playback(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/pause", device_id); match self.put(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1670,7 +1634,7 @@ impl Spotify { /// Skip User’s Playback To Next Track /// Parameters: /// - device_id - device target for playback - pub async fn next_track(&self, device_id: Option) -> Result<(), failure::Error> { + pub async fn next_track(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/next", device_id); match self.post(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1682,7 +1646,7 @@ impl Spotify { /// Skip User’s Playback To Previous Track /// Parameters: /// - device_id - device target for playback - pub async fn previous_track(&self, device_id: Option) -> Result<(), failure::Error> { + pub async fn previous_track(&self, device_id: Option) -> ClientResult<()> { let url = self.append_device_id("me/player/previous", device_id); match self.post(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1699,7 +1663,7 @@ impl Spotify { &self, position_ms: u32, device_id: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/seek?position_ms={}", position_ms), device_id, @@ -1715,11 +1679,7 @@ impl Spotify { /// Parameters: /// - state - `track`, `context`, or `off` /// - device_id - device target for playback - pub async fn repeat( - &self, - state: RepeatState, - device_id: Option, - ) -> Result<(), failure::Error> { + pub async fn repeat(&self, state: RepeatState, device_id: Option) -> ClientResult<()> { let url = self.append_device_id( &format!("me/player/repeat?state={}", state.as_str()), device_id, @@ -1735,11 +1695,7 @@ impl Spotify { /// Parameters: /// - volume_percent - volume between 0 and 100 /// - device_id - device target for playback - pub async fn volume( - &self, - volume_percent: u8, - device_id: Option, - ) -> Result<(), failure::Error> { + pub async fn volume(&self, volume_percent: u8, device_id: Option) -> ClientResult<()> { if volume_percent > 100u8 { error!("volume must be between 0 and 100, inclusive"); } @@ -1758,11 +1714,7 @@ impl Spotify { /// Parameters: /// - state - true or false /// - device_id - device target for playback - pub async fn shuffle( - &self, - state: bool, - device_id: Option, - ) -> Result<(), failure::Error> { + pub async fn shuffle(&self, state: bool, device_id: Option) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/shuffle?state={}", state), device_id); match self.put(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1780,7 +1732,7 @@ impl Spotify { &self, item: String, device_id: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let url = self.append_device_id(&format!("me/player/queue?uri={}", &item), device_id); match self.post(&url, &json!({})).await { Ok(_) => Ok(()), @@ -1792,7 +1744,7 @@ impl Spotify { /// 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. - pub async fn save_shows(&self, ids: Vec) -> Result<(), failure::Error> { + pub async fn save_shows(&self, ids: Vec) -> ClientResult<()> { let joined_ids = ids.join(","); let url = format!("me/shows/?ids={}", joined_ids); match self.put(&url, &json!({})).await { @@ -1809,7 +1761,7 @@ impl Spotify { &self, limit: L, offset: O, - ) -> Result, failure::Error> { + ) -> ClientResult> { let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); let offset = offset.into().unwrap_or(0); @@ -1826,11 +1778,7 @@ impl Spotify { /// - id: The Spotify ID for the show. /// Query Parameters /// - market(Optional): An ISO 3166-1 alpha-2 country code. - pub async fn get_a_show( - &self, - id: String, - market: Option, - ) -> Result { + pub async fn get_a_show(&self, id: String, market: Option) -> ClientResult { let url = format!("shows/{}", id); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1849,7 +1797,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result { + ) -> ClientResult { let joined_ids = ids.join(","); let url = "shows"; let mut params = HashMap::new(); @@ -1874,7 +1822,7 @@ impl Spotify { limit: L, offset: O, market: Option, - ) -> Result, failure::Error> { + ) -> ClientResult> { let url = format!("shows/{}/episodes", id); let mut params = HashMap::new(); let limit = limit.into().unwrap_or(20); @@ -1898,7 +1846,7 @@ impl Spotify { &self, id: String, market: Option, - ) -> Result { + ) -> ClientResult { let url = format!("episodes/{}", id); let mut params = HashMap::new(); if let Some(_market) = market { @@ -1917,7 +1865,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result { + ) -> ClientResult { let url = "episodes"; let joined_ids = ids.join(","); let mut params = HashMap::new(); @@ -1933,10 +1881,7 @@ impl Spotify { /// [Check users saved shows](https://developer.spotify.com/documentation/web-api/reference/library/check-users-saved-shows/) /// Query Parameters /// - ids: Required. A comma-separated list of the Spotify IDs for the shows. Maximum: 50 IDs. - pub async fn check_users_saved_shows( - &self, - ids: Vec, - ) -> Result, failure::Error> { + pub async fn check_users_saved_shows(&self, ids: Vec) -> ClientResult> { let url = "me/shows/contains"; let joined_ids = ids.join(","); let mut params = HashMap::new(); @@ -1955,7 +1900,7 @@ impl Spotify { &self, ids: Vec, market: Option, - ) -> Result<(), failure::Error> { + ) -> ClientResult<()> { let joined_ids = ids.join(","); let url = format!("me/shows?ids={}", joined_ids); let mut payload = Map::new(); @@ -1971,18 +1916,8 @@ impl Spotify { } } - pub fn convert_result<'a, T: Deserialize<'a>>( - &self, - input: &'a str, - ) -> Result { - let result = serde_json::from_str::(input).map_err(|e| { - format_err!( - "convert result failed, reason: {:?}; content: [{:?}]", - e, - input - ) - })?; - Ok(result) + pub 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 API path.