269 lines
7.8 KiB
Rust
269 lines
7.8 KiB
Rust
|
use std::fmt::Display;
|
||
|
use std::str::FromStr;
|
||
|
|
||
|
use reqwest::Method;
|
||
|
|
||
|
use serde::{Deserialize, Deserializer, de};
|
||
|
|
||
|
use std::time::Instant;
|
||
|
|
||
|
use crate::objects::*;
|
||
|
|
||
|
use md5::{Digest, Md5};
|
||
|
|
||
|
use crate::errors::{ErrorKind, DzErr};
|
||
|
|
||
|
const API_URL: &str = "https://api.deezer.com";
|
||
|
// client id and secret are from deezer's chromecast app
|
||
|
const CLIENT_ID: &str = "119915";
|
||
|
const CLIENT_SECRET: &str = "2f5b4c9785ddc367975b83d90dc46f5c";
|
||
|
|
||
|
fn from_str<'de, T, D>(deserializer: D) -> Result<T, D::Error>
|
||
|
where T: FromStr,
|
||
|
T::Err: Display,
|
||
|
D: Deserializer<'de>
|
||
|
{
|
||
|
let s = String::deserialize(deserializer)?;
|
||
|
T::from_str(&s).map_err(de::Error::custom)
|
||
|
}
|
||
|
|
||
|
#[derive(Deserialize, Debug)]
|
||
|
struct TokenResp {
|
||
|
access_token: String,
|
||
|
#[serde(deserialize_with = "from_str")]
|
||
|
expires: u64,
|
||
|
}
|
||
|
|
||
|
// needed because deezer's API returns an integer on "expires" instead of a string when logging in
|
||
|
// thanks baguette assholes
|
||
|
#[derive(Deserialize, Debug)]
|
||
|
struct LoginTokenResp {
|
||
|
access_token: String,
|
||
|
expires: u64,
|
||
|
}
|
||
|
|
||
|
pub struct AccessToken {
|
||
|
pub token: String,
|
||
|
expires: u64,
|
||
|
instant: Instant
|
||
|
}
|
||
|
|
||
|
impl AccessToken {
|
||
|
fn new(token: String, expires: u64) -> AccessToken {
|
||
|
AccessToken {
|
||
|
token,
|
||
|
expires,
|
||
|
instant: Instant::now(),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[derive(Deserialize, Debug)]
|
||
|
#[serde(untagged)]
|
||
|
enum DzRespType<T> {
|
||
|
Err { error: DzErr },
|
||
|
Ok(T),
|
||
|
}
|
||
|
|
||
|
pub struct Client {
|
||
|
client: reqwest::Client,
|
||
|
pub access_token: Option<AccessToken>,
|
||
|
}
|
||
|
|
||
|
impl Client {
|
||
|
pub fn new() -> Self {
|
||
|
Self {
|
||
|
access_token: None,
|
||
|
client: reqwest::Client::new(),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub async fn get_token(&mut self) -> Result<(), ErrorKind> {
|
||
|
let resp = self.client.request(Method::GET, "https://connect.deezer.com/oauth/access_token.php")
|
||
|
.query(&[
|
||
|
("grant_type", "client_credentials"),
|
||
|
("client_id", CLIENT_ID),
|
||
|
("client_secret", CLIENT_SECRET),
|
||
|
("output", "json"),
|
||
|
])
|
||
|
.send().await.map_err(|e| ErrorKind::Reqwest(e))?
|
||
|
.json::<DzRespType<TokenResp>>().await.map_err(|e| ErrorKind::Reqwest(e))?;
|
||
|
|
||
|
match resp {
|
||
|
DzRespType::Ok(token) => {
|
||
|
self.access_token = Some(AccessToken::new(token.access_token, token.expires));
|
||
|
Ok(())
|
||
|
},
|
||
|
DzRespType::Err { error } => Err(ErrorKind::API(error)),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async fn check_token(&mut self) -> Result<(), ErrorKind> {
|
||
|
match &self.access_token {
|
||
|
Some(token) => {
|
||
|
if token.instant.elapsed().as_secs() >= token.expires && token.expires != 0 {
|
||
|
self.get_token().await
|
||
|
} else {
|
||
|
Ok(())
|
||
|
}
|
||
|
},
|
||
|
None => self.get_token().await
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub async fn login(&mut self, email: &str, password: &str) -> Result<(), ErrorKind> {
|
||
|
let password = format!("{:x}", Md5::digest(password.as_bytes()));
|
||
|
|
||
|
let hash = [
|
||
|
CLIENT_ID.as_bytes(),
|
||
|
email.as_bytes(),
|
||
|
password.as_bytes(),
|
||
|
CLIENT_SECRET.as_bytes(),
|
||
|
].concat();
|
||
|
let hash = format!("{:x}", Md5::digest(&hash));
|
||
|
|
||
|
let url = format!("{}/auth/token", API_URL);
|
||
|
let resp = self.client.request(Method::GET, &url)
|
||
|
.query(&[
|
||
|
("app_id", CLIENT_ID),
|
||
|
("login", email),
|
||
|
("password", &password),
|
||
|
("hash", &hash),
|
||
|
])
|
||
|
.send().await.map_err(|e| ErrorKind::Reqwest(e))?
|
||
|
.json::<DzRespType<LoginTokenResp>>().await.map_err(|e| ErrorKind::Reqwest(e))?;
|
||
|
|
||
|
match resp {
|
||
|
DzRespType::Ok(token) => {
|
||
|
self.access_token = Some(AccessToken::new(token.access_token, token.expires));
|
||
|
Ok(())
|
||
|
},
|
||
|
DzRespType::Err { error } => Err(ErrorKind::API(error)),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub async fn login_token(&mut self, token: &str) -> Result<(), ErrorKind> {
|
||
|
self.access_token = Some(AccessToken::new(token.to_string(), 0));
|
||
|
match self.infos().await {
|
||
|
Err(e) => {
|
||
|
self.access_token = None;
|
||
|
Err(e)
|
||
|
},
|
||
|
Ok(i) => {
|
||
|
match i.user_token {
|
||
|
Some(_) => Ok(()),
|
||
|
None => {
|
||
|
self.access_token = None;
|
||
|
Err(ErrorKind::NotUserToken)
|
||
|
},
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub async fn api_call<T, Q>(&mut self, method: reqwest::Method, path: &str, query: &Q) -> Result<T, ErrorKind>
|
||
|
where T: de::DeserializeOwned,
|
||
|
Q: serde::ser::Serialize + ?Sized,
|
||
|
{
|
||
|
self.check_token().await.map_err(|e| ErrorKind::Token(Box::new(e)))?;
|
||
|
let url = format!("{}/{}", API_URL, path);
|
||
|
|
||
|
let resp = self.client.request(method, &url)
|
||
|
.query(&[("access_token", &self.access_token.as_ref().unwrap().token)])
|
||
|
.query(query)
|
||
|
.body("\n") // needed otherwise some POST requests fail
|
||
|
.send().await.map_err(|e| ErrorKind::Reqwest(e))?
|
||
|
.json::<DzRespType<T>>().await.map_err(|e| ErrorKind::Reqwest(e))?;
|
||
|
|
||
|
match resp {
|
||
|
DzRespType::Ok(r) => Ok(r),
|
||
|
DzRespType::Err { error } => Err(ErrorKind::API(error)),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub async fn track(&mut self, id: i64) -> Result<Track, ErrorKind> {
|
||
|
Track::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn album(&mut self, id: u64) -> Result<Album, ErrorKind> {
|
||
|
Album::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn artist(&mut self, id: u64) -> Result<Artist, ErrorKind> {
|
||
|
Artist::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn playlist(&mut self, id: u64) -> Result<Playlist, ErrorKind> {
|
||
|
Playlist::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn podcast(&mut self, id: u64) -> Result<Podcast, ErrorKind> {
|
||
|
Podcast::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn episode(&mut self, id: u64) -> Result<Episode, ErrorKind> {
|
||
|
Episode::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn chart(&mut self, id: u64) -> Result<Chart, ErrorKind> {
|
||
|
Chart::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn radio(&mut self, id: u64) -> Result<Radio, ErrorKind> {
|
||
|
Radio::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn radio_list(&mut self) -> Result<DzArray<Radio>, ErrorKind> {
|
||
|
Radio::list(self).await
|
||
|
}
|
||
|
|
||
|
pub async fn search_track(&mut self, query: &str) -> Result<DzArray<Track>, ErrorKind> {
|
||
|
search::track(self, query).await
|
||
|
}
|
||
|
|
||
|
pub async fn search_album(&mut self, query: &str) -> Result<DzArray<Album>, ErrorKind> {
|
||
|
search::album(self, query).await
|
||
|
}
|
||
|
|
||
|
pub async fn search_artist(&mut self, query: &str) -> Result<DzArray<Artist>, ErrorKind> {
|
||
|
search::artist(self, query).await
|
||
|
}
|
||
|
|
||
|
pub async fn search_playlist(&mut self, query: &str) -> Result<DzArray<Playlist>, ErrorKind> {
|
||
|
search::playlist(self, query).await
|
||
|
}
|
||
|
|
||
|
pub async fn genre(&mut self, id: u64) -> Result<Genre, ErrorKind> {
|
||
|
Genre::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn editorial(&mut self, id: u64) -> Result<Editorial, ErrorKind> {
|
||
|
Editorial::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn lyrics(&mut self, id: u64) -> Result<Lyrics, ErrorKind> {
|
||
|
Lyrics::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn user(&mut self, id: u64) -> Result<User, ErrorKind> {
|
||
|
User::get(self, id).await
|
||
|
}
|
||
|
|
||
|
pub async fn user_self(&mut self) -> Result<User, ErrorKind> {
|
||
|
User::get_self(self).await
|
||
|
}
|
||
|
|
||
|
pub async fn infos(&mut self) -> Result<Infos, ErrorKind> {
|
||
|
Infos::get(self).await
|
||
|
}
|
||
|
|
||
|
pub async fn options(&mut self) -> Result<Options, ErrorKind> {
|
||
|
Options::get(self).await
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Default for Client {
|
||
|
fn default() -> Self {
|
||
|
Client::new()
|
||
|
}
|
||
|
}
|