#![doc = include_str!("../README.md")]
#![warn(
missing_docs,
missing_debug_implementations,
missing_copy_implementations,
trivial_casts,
trivial_numeric_casts,
unsafe_code,
unused_import_braces,
unused_qualifications
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use log::info;
use reqwest::{
header::{HeaderMap, HeaderValue, InvalidHeaderValue, AUTHORIZATION},
Client as ReqwestClient, Error as ReqwestError, StatusCode,
};
use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;
pub mod models;
use models::*;
pub const BASE_URL: &str = "https://api.genius.com";
#[derive(Debug, Error)]
pub enum ClientError {
#[error("megamind client error: {0}")]
General(#[from] ReqwestError),
#[error("megamind rate limit error")]
RateLimited,
}
#[derive(Debug, Clone)]
pub struct Client {
internal: ReqwestClient,
}
impl Client {
async fn get<T: DeserializeOwned, S: AsRef<str>, P: Serialize + AsRef<str>>(
&self,
endpoint: S,
query: &[(&str, P)],
) -> Result<Response<T>, ClientError> {
info!(
target: "megamind::get",
"endpoint: \"{}\", queries: \"{}\"",
endpoint.as_ref(),
query
.iter()
.map(|q| format!("{}={}", q.0, q.1.as_ref()))
.collect::<Vec<String>>()
.join(",")
);
let response = self
.internal
.get(format!("{}{}", BASE_URL, endpoint.as_ref()))
.query(query)
.send()
.await?;
let resp_url = response.url().clone();
if response.status() == StatusCode::TOO_MANY_REQUESTS {
return Err(ClientError::RateLimited);
}
Ok(response
.json::<Response<T>>()
.await
.map_err(|e| e.with_url(resp_url))?)
}
pub async fn account(&self) -> Result<Response<AccountResponse>, ClientError> {
self.get("/account", &[("text_format", "html,plain")]).await
}
pub async fn annotation(
&self,
id: u32,
) -> Result<Response<AnnotationResponse>, ClientError> {
self.get(
format!("/annotations/{}", id),
&[("text_format", "html,plain")],
)
.await
}
pub async fn artist(
&self,
id: u32,
) -> Result<Response<ArtistResponse>, ClientError> {
self.get(format!("/artists/{}", id), &[("text_format", "html,plain")])
.await
}
pub async fn referents(
&self,
created_by: Option<u32>,
associated: Option<ReferentAssociation>,
per_page: Option<u8>,
page: Option<u8>,
) -> Result<Response<ReferentsResponse>, ClientError> {
let mut queries = vec![("text_format", String::from("html,plain"))];
if let Some(created_by_id) = created_by {
queries.push(("created_by_id", created_by_id.to_string()));
}
if let Some(association) = associated {
let params = match association {
ReferentAssociation::SongId(id) => ("song_id", id.to_string()),
ReferentAssociation::WebPageId(id) => ("web_page_id", id.to_string()),
};
queries.push(params);
}
if let Some(per_page) = per_page {
queries.push(("per_page", per_page.to_string()));
}
if let Some(page) = page {
queries.push(("page", page.to_string()));
}
self.get("/referents", &queries).await
}
pub async fn search<S: AsRef<str>>(
&self,
query: S,
) -> Result<Response<SearchResponse>, ClientError> {
self.get("/search", &[("q", query.as_ref())]).await
}
pub async fn song(&self, id: u32) -> Result<Response<SongResponse>, ClientError> {
self.get(format!("/songs/{}", id), &[("text_format", "html,plain")])
.await
}
pub async fn user(&self, id: u32) -> Result<Response<UserResponse>, ClientError> {
self.get(format!("/users/{}", id), &[("text_format", "html,plain")])
.await
}
pub async fn web_pages(
&self,
raw_annotatable_url: Option<&str>,
canonical_url: Option<&str>,
og_url: Option<&str>,
) -> Result<Response<WebPageResponse>, ClientError> {
let mut queries = Vec::new();
if let Some(rau) = raw_annotatable_url {
queries.push(("raw_annotatable_url", rau));
}
if let Some(cu) = canonical_url {
queries.push(("canonical_url", cu));
}
if let Some(ou) = og_url {
queries.push(("og_url", ou));
}
self.get("/web_pages/lookup", &queries).await
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ReferentAssociation {
SongId(u32),
WebPageId(u32),
}
#[derive(Default, Debug, Clone)]
pub struct ClientBuilder {
auth_token: Option<String>,
}
impl ClientBuilder {
pub fn new() -> Self {
ClientBuilder { auth_token: None }
}
pub fn auth_token<S: Into<String>>(mut self, auth_token: S) -> Self {
self.auth_token = Some(auth_token.into());
self
}
pub fn build(self) -> Result<Client, ClientBuilderError> {
if let Some(auth_token) = self.auth_token {
let mut headers = HeaderMap::new();
let mut header_val =
HeaderValue::from_str(&format!("Bearer {}", auth_token))?;
header_val.set_sensitive(true);
headers.insert(AUTHORIZATION, header_val);
Ok(Client {
internal: ReqwestClient::builder().default_headers(headers).build()?,
})
} else {
Err(ClientBuilderError::MissingAuthToken)
}
}
}
#[derive(Debug, Error)]
pub enum ClientBuilderError {
#[error("missing auth token")]
MissingAuthToken,
#[error("internal client build error: {0}")]
ReqwestBuilder(#[from] ReqwestError),
#[error("invalid auth header value: {0}")]
AuthHeaderValue(#[from] InvalidHeaderValue),
}