use std::collections::BTreeMap;
use std::time::Duration;
use reqwest::{Client as HttpClient, Url};
use serde::de::DeserializeOwned;
use tracing::instrument;
use crate::error::{Error, ErrorResponse};
use crate::sig::create_sig;
const LASTFM_API_BASE: &str = "https://ws.audioscrobbler.com/2.0/";
const DEFAULT_USER_AGENT: &str = concat!(
"soniq/",
env!("CARGO_PKG_VERSION"),
" (https://github.com/CleeSim/soniq)"
);
#[derive(Clone, Debug)]
pub struct Client {
api_key: String,
api_secret: Option<String>,
http: HttpClient,
base_url: Url,
}
impl Client {
pub fn builder(api_key: impl Into<String>) -> ClientBuilder {
ClientBuilder::new(api_key)
}
#[instrument(skip(self, params))]
pub async fn unsigned_get<T: DeserializeOwned>(
&self,
method: &str,
mut params: BTreeMap<String, String>,
) -> Result<T, Error> {
params.insert("method".into(), method.into());
params.insert("api_key".into(), self.api_key.clone());
params.insert("format".into(), "json".into());
self.get(params).await
}
#[instrument(skip(self, params))]
pub async fn signed_post<T: DeserializeOwned>(
&self,
method: &str,
params: BTreeMap<String, String>,
) -> Result<T, Error> {
self.signed_post_with_session(method, None, params).await
}
#[instrument(skip(self, session_key, params))]
pub async fn signed_post_with_session<T: DeserializeOwned>(
&self,
method: &str,
session_key: Option<&str>,
mut params: BTreeMap<String, String>,
) -> Result<T, Error> {
let api_secret = self.api_secret.as_deref().ok_or(Error::MissingApiSecret)?;
params.insert("method".into(), method.into());
params.insert("api_key".into(), self.api_key.clone());
if let Some(sk) = session_key {
params.insert("sk".into(), sk.into());
}
let sig = create_sig(¶ms, api_secret);
params.insert("api_sig".into(), sig);
params.insert("format".into(), "json".into());
self.post(params).await
}
async fn get<T: DeserializeOwned>(&self, params: BTreeMap<String, String>) -> Result<T, Error> {
let res = self
.http
.get(self.base_url.clone())
.query(¶ms)
.send()
.await?;
Self::handle_response(res).await
}
async fn post<T: DeserializeOwned>(
&self,
params: BTreeMap<String, String>,
) -> Result<T, Error> {
let res = self
.http
.post(self.base_url.clone())
.form(¶ms)
.send()
.await?;
Self::handle_response(res).await
}
async fn handle_response<T: DeserializeOwned>(res: reqwest::Response) -> Result<T, Error> {
let status = res.status();
let text = res.text().await?;
if status.is_success() {
serde_json::from_str(&text).map_err(Error::from)
} else {
match serde_json::from_str::<ErrorResponse>(&text) {
Ok(err) => Err(Error::LastFm(err)),
Err(_) => Err(Error::Http { status, text }),
}
}
}
}
impl Client {
pub fn user(&self) -> crate::endpoints::user::UserHandler<'_> {
crate::endpoints::user::UserEndpointExt::user(self)
}
}
#[derive(Debug)]
pub struct ClientBuilder {
api_key: String,
api_secret: Option<String>,
timeout: Duration,
user_agent: String,
base_url: Url,
}
impl ClientBuilder {
pub fn new(api_key: impl Into<String>) -> Self {
Self {
api_key: api_key.into(),
api_secret: None,
timeout: Duration::from_secs(10),
user_agent: DEFAULT_USER_AGENT.to_string(),
base_url: Url::parse(LASTFM_API_BASE).expect("Default base URL is invalid?"),
}
}
pub fn api_secret(mut self, secret: impl Into<String>) -> Self {
self.api_secret = Some(secret.into());
self
}
pub fn timeout(mut self, duration: Duration) -> Self {
self.timeout = duration;
self
}
pub fn base_url(mut self, url: impl AsRef<str>) -> Result<Self, url::ParseError> {
self.base_url = Url::parse(url.as_ref())?;
Ok(self)
}
pub fn build(self) -> Result<Client, Error> {
let http = HttpClient::builder()
.timeout(self.timeout)
.user_agent(self.user_agent)
.build()?;
Ok(Client {
api_key: self.api_key,
api_secret: self.api_secret,
http,
base_url: self.base_url,
})
}
}