diff options
| -rw-r--r-- | Cargo.toml | 3 | ||||
| -rw-r--r-- | examples/playlists/main.rs | 15 | ||||
| -rw-r--r-- | src/auth/mod.rs | 44 | ||||
| -rw-r--r-- | src/client.rs | 113 | ||||
| -rw-r--r-- | src/errors/auth.rs | 13 | 
5 files changed, 110 insertions, 78 deletions
| @@ -8,9 +8,8 @@ actix-web = { version = "4.1.0", default-features = false, features = ["macros"]  tokio = { version = "1.21.0", features = ["macros", "rt-multi-thread"] }  serde = { version = "1.0.144", features = ["derive"] }  serde_json = "1.0.85" -reqwest = { version = "0.11.11", features = ["json"] }  thiserror = "1.0.33" -hyper = "0.14.20" +hyper = { version = "0.14.20", features = ["client", "http2", "tcp"] }  [dev_dependencies]  config = "0.13.2" diff --git a/examples/playlists/main.rs b/examples/playlists/main.rs index fed5b73..81d3cf2 100644 --- a/examples/playlists/main.rs +++ b/examples/playlists/main.rs @@ -2,7 +2,7 @@ use std::error::Error;  use anyhow::Context;  use config::Config; -use deez::auth::{request_access_token, AuthPromptHandler}; +use deez::auth::AuthPromptHandler;  use deez::client::DeezerClient;  use serde::Deserialize; @@ -40,8 +40,11 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>>      println!("Retrieved authentication code '{}'", auth_code); -    let access_token = -        request_access_token(settings.app_id, settings.secret_key, auth_code).await?; +    let client = DeezerClient::new(); + +    let access_token = client +        .get_access_token(settings.app_id, settings.secret_key, auth_code) +        .await?;      println!(          "Retrieved access token '{}' which expires in {} seconds", @@ -49,13 +52,11 @@ async fn main() -> Result<(), Box<dyn Error + Send + Sync>>          access_token.expires.as_secs()      ); -    let client = DeezerClient::new(access_token); - -    let me = client.get_me().await?; +    let me = client.get_me(access_token.clone()).await?;      println!("{:#?}", me); -    let playlists = client.get_user_playlists(me.id).await?; +    let playlists = client.get_user_playlists(me.id, access_token).await?;      println!("Playlists: {:#?}", playlists); diff --git a/src/auth/mod.rs b/src/auth/mod.rs index fae0383..ce550e1 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -11,12 +11,11 @@ use tokio::task::JoinHandle;  use tokio::{select, spawn};  use crate::auth::service::retrieve_auth_code; -use crate::errors::auth::{AccessTokenRequestError, AuthPromptHandlerError}; +use crate::errors::auth::AuthPromptHandlerError;  mod service;  const AUTH_URL: &str = "https://connect.deezer.com/oauth/auth.php"; -const ACCESS_TOKEN_URL: &str = "https://connect.deezer.com/oauth/access_token.php";  /// A Deezer authentication code.  #[derive(Debug, Deserialize, Clone)] @@ -96,7 +95,7 @@ impl AuthPromptHandler  }  /// A Deezer access token. -#[derive(Debug)] +#[derive(Debug, Clone)]  pub struct AccessToken  {      /// The access token. @@ -105,42 +104,3 @@ pub struct AccessToken      /// The duration until the access token expires.      pub expires: Duration,  } - -/// Sends a request for a access token. -/// -/// # Errors -/// Will return Err if either sending the request fails or parsing the response fails. -pub async fn request_access_token( -    app_id: u32, -    secret_key: String, -    auth_code: AuthCode, -) -> Result<AccessToken, AccessTokenRequestError> -{ -    let response = reqwest::get(format!( -        "{}?app_id={}&secret={}&code={}&output=json", -        ACCESS_TOKEN_URL, app_id, secret_key, auth_code -    )) -    .await? -    .json::<AccessTokenResponse>() -    .await?; - -    Ok(response.into()) -} - -#[derive(Debug, Deserialize)] -struct AccessTokenResponse -{ -    pub access_token: String, -    pub expires: u64, -} - -impl From<AccessTokenResponse> for AccessToken -{ -    fn from(response: AccessTokenResponse) -> Self -    { -        Self { -            access_token: response.access_token, -            expires: Duration::from_secs(response.expires), -        } -    } -} diff --git a/src/client.rs b/src/client.rs index 0699545..179377f 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,11 +1,12 @@  //! Deezer client.  use std::fmt::Debug; +use std::time::Duration;  use hyper::client::{Client, HttpConnector};  use hyper::Uri;  use serde::Deserialize; -use crate::auth::AccessToken; +use crate::auth::{AccessToken, AuthCode};  use crate::errors::client::DeezerClientError;  use crate::playlist::Playlist;  use crate::user::{User, UserPlaylist, UserPlaylists}; @@ -31,24 +32,45 @@ struct ErrorResponseBody      error: DeezerError,  } +#[derive(Debug, Deserialize)] +struct AccessTokenResponse +{ +    pub access_token: String, +    pub expires: u64, +} + +impl From<AccessTokenResponse> for AccessToken +{ +    fn from(response: AccessTokenResponse) -> Self +    { +        Self { +            access_token: response.access_token, +            expires: Duration::from_secs(response.expires), +        } +    } +} +  /// Deezer client. +#[derive(Default)]  pub struct DeezerClient  {      client: Client<HttpConnector>, -    api_url: &'static str, -    access_token: AccessToken, +    api_uri_authority: &'static str, +    access_token_uri_authority: &'static str, +    access_token_uri_path: &'static str,  }  impl DeezerClient  {      /// Creates a new Deezer client.      #[must_use] -    pub fn new(access_token: AccessToken) -> Self +    pub fn new() -> Self      {          Self {              client: Client::new(), -            api_url: "api.deezer.com", -            access_token, +            api_uri_authority: "api.deezer.com", +            access_token_uri_authority: "connect.deezer.com", +            access_token_uri_path: "/oauth/access_token.php",          }      } @@ -56,11 +78,17 @@ impl DeezerClient      ///      /// # Errors      /// Will return Err if either sending the request or parsing the response fails. -    pub async fn get_me(&self) -> Result<User, DeezerClientError> +    pub async fn get_me( +        &self, +        access_token: AccessToken, +    ) -> Result<User, DeezerClientError>      {          let response = self              .client -            .get(self.build_endpoint_uri(&"user/me".to_string())?) +            .get(self.build_endpoint_uri( +                &"user/me".to_string(), +                &[("access_token", access_token.access_token)], +            )?)              .await?;          let body_buf = &*hyper::body::to_bytes(response).await?; @@ -82,11 +110,15 @@ impl DeezerClient      pub async fn get_user_playlists(          &self,          user_id: u64, +        access_token: AccessToken,      ) -> Result<Vec<UserPlaylist>, DeezerClientError>      {          let response = self              .client -            .get(self.build_endpoint_uri(&format!("user/{}/playlists", user_id))?) +            .get(self.build_endpoint_uri( +                &format!("user/{}/playlists", user_id), +                &[("access_token", access_token.access_token)], +            )?)              .await?;          let body_buf = &*hyper::body::to_bytes(response).await?; @@ -111,11 +143,15 @@ impl DeezerClient      pub async fn get_playlist(          &self,          playlist_id: u64, +        access_token: AccessToken,      ) -> Result<Playlist, DeezerClientError>      {          let response = self              .client -            .get(self.build_endpoint_uri(&format!("playlist/{}", playlist_id))?) +            .get(self.build_endpoint_uri( +                &format!("playlist/{}", playlist_id), +                &[("access_token", access_token.access_token)], +            )?)              .await?;          let body_buf = &*hyper::body::to_bytes(response).await?; @@ -130,14 +166,63 @@ impl DeezerClient          serde_json::from_slice(body_buf).map_err(DeezerClientError::ParseResponseFailed)      } -    fn build_endpoint_uri(&self, endpoint: &String) -> Result<Uri, DeezerClientError> +    /// Returns a access token. +    /// +    /// # Errors +    /// Will return Err if either sending the request or parsing the response fails. +    pub async fn get_access_token( +        &self, +        app_id: u32, +        secret_key: String, +        auth_code: AuthCode, +    ) -> Result<AccessToken, DeezerClientError> +    { +        let uri = Uri::builder() +            .scheme("http") +            .authority(self.access_token_uri_authority) +            .path_and_query(format!( +                "{}?app_id={}&secret={}&code={}&output=json", +                self.access_token_uri_path, app_id, secret_key, auth_code +            )) +            .build() +            .map_err(|_| DeezerClientError::BuildAPIEndpointURIFailed)?; + +        let response = self.client.get(uri).await?; + +        let body_buf = &*hyper::body::to_bytes(response).await?; + +        let err_body_result: Result<ErrorResponseBody, _> = +            serde_json::from_slice(body_buf); + +        if let Ok(err_body) = err_body_result { +            return Err(DeezerClientError::ReceivedErrorResponse(err_body.error)); +        } + +        let access_token_response: AccessTokenResponse = serde_json::from_slice(body_buf) +            .map_err(DeezerClientError::ParseResponseFailed)?; + +        Ok(access_token_response.into()) +    } + +    fn build_endpoint_uri( +        &self, +        endpoint: &String, +        query_params: &[(&'static str, String)], +    ) -> Result<Uri, DeezerClientError>      {          Uri::builder()              .scheme("http") -            .authority(self.api_url) +            .authority(self.api_uri_authority)              .path_and_query(format!( -                "/{}?access_token={}", -                endpoint, self.access_token.access_token +                "/{}?{}", +                endpoint, +                query_params +                    .iter() +                    .map(|(key, value)| format!("{}={}", key, value)) +                    .fold(String::new(), |acc, query_param| format!( +                        "{}&{}", +                        acc, query_param +                    ))              ))              .build()              .map_err(|_| DeezerClientError::BuildAPIEndpointURIFailed) diff --git a/src/errors/auth.rs b/src/errors/auth.rs index cd4741d..656673e 100644 --- a/src/errors/auth.rs +++ b/src/errors/auth.rs @@ -8,16 +8,3 @@ pub enum AuthPromptHandlerError      #[error("HTTP server failed to bind to address")]      BindAddressFailed,  } - -/// Access token request error. -#[derive(Debug, thiserror::Error)] -pub enum AccessTokenRequestError -{ -    /// Sending access token request failed. -    #[error("Sending access token request failed")] -    SendFailed(#[from] reqwest::Error), - -    /// Parsing access token respone failed. -    #[error("Parsing access token respone failed")] -    ResponseParseFailed, -} | 
