use reqwest::IntoUrl;
use reqwest::header::HeaderMap;
use serde::Serializer;
use std::string::FromUtf8Error;
use crate::{
BASE_URL, Category, NekosBestError, NekosBestResponse, NekosBestResponseSingle,
details::{GifDetails, ImageDetails},
};
#[cfg(feature = "blocking")]
use nb_blocking_util::blocking;
#[cfg(feature = "strong-types")]
#[path = "strong_types_impl.rs"]
mod strong_types_impl;
use crate::client::{Client, ClientConfig, ReqBuilder, ReqwestResponse};
#[cfg(feature = "strong-types")]
pub use strong_types_impl::{
get as st_get, get_amount as st_get_amount, get_with_client as st_get_with_client,
get_with_client_amount as st_get_with_client_amount, search as st_search,
search_with_client as st_search_with_client,
};
#[cfg_attr(feature = "blocking", blocking)]
async fn parse_from_response(
response: ReqwestResponse,
) -> Result<NekosBestResponse, NekosBestError> {
#[cfg(not(nekosbest_dbgjson))]
let v = response
.error_for_status()?
.json::<NekosBestResponse>()
.await?;
#[cfg(nekosbest_dbgjson)]
let v = {
let json = response.error_for_status()?.text().await?;
dbg!(&json);
serde_json::from_str::<NekosBestResponse>(&json)?
};
Ok(v)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_with_client(
client: &Client,
category: impl Into<Category>,
) -> Result<NekosBestResponseSingle, NekosBestError> {
let r = crate::prepare_request(client.client.get(format!("{BASE_URL}/{}", category.into())))
.send()
.await?;
let mut resp = parse_from_response(r).await?;
let resp = resp.0.pop().ok_or(NekosBestError::NotFound)?;
Ok(resp)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_with_client_amount(
client: &Client,
category: impl Into<Category>,
amount: impl Into<u8>,
) -> Result<NekosBestResponse, NekosBestError> {
let req = crate::prepare_request(client.client.get(format!("{BASE_URL}/{}", category.into())))
.query(&[("amount", amount.into())]);
let r = req.send().await?;
let v = parse_from_response(r).await?;
Ok(v)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get(category: impl Into<Category>) -> Result<NekosBestResponseSingle, NekosBestError> {
get_with_client(&Client::new(ClientConfig::default()), category).await
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_amount(
category: impl Into<Category>,
amount: impl Into<u8>,
) -> Result<NekosBestResponse, NekosBestError> {
get_with_client_amount(&Client::new(ClientConfig::default()), category, amount).await
}
#[derive(Debug, thiserror::Error)]
pub enum HeaderDeserializeUrlEncodedError {
#[error("Missing header")]
MissingHeader,
#[error("Not ASCII header")]
NotAsciiHeader(#[from] reqwest::header::ToStrError),
#[error("UTF8 error")]
Utf8(#[from] FromUtf8Error),
}
fn header_deserialize_urlencoded(
headers: &HeaderMap,
name: &str,
) -> Result<String, HeaderDeserializeUrlEncodedError> {
let s = headers
.get(name)
.ok_or(HeaderDeserializeUrlEncodedError::MissingHeader)?
.to_str()?;
let s = urlencoding::decode(s)?.replace("+", " ");
Ok(s)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_with_client_image_details(
client: &Client,
url: impl IntoUrl,
) -> Result<ImageDetails, NekosBestError> {
let resp = crate::prepare_request(client.client.get(url))
.send()
.await?
.error_for_status()?;
let headers = resp.headers();
let details = ImageDetails {
artist_name: header_deserialize_urlencoded(headers, "artist_name")?,
artist_href: header_deserialize_urlencoded(headers, "artist_href")?.parse()?,
source_url: header_deserialize_urlencoded(headers, "source_url")?.parse()?,
};
Ok(details)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_image_details(url: impl IntoUrl) -> Result<ImageDetails, NekosBestError> {
get_with_client_image_details(&Client::new(ClientConfig::default()), url).await
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_with_client_gif_details(
client: &Client,
url: impl IntoUrl,
) -> Result<GifDetails, NekosBestError> {
let resp = crate::prepare_request(client.client.get(url))
.send()
.await?
.error_for_status()?;
let headers = resp.headers();
let details = GifDetails {
anime_name: header_deserialize_urlencoded(headers, "anime_name")?,
};
Ok(details)
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn get_gif_details(url: impl IntoUrl) -> Result<GifDetails, NekosBestError> {
get_with_client_gif_details(&Client::new(ClientConfig::default()), url).await
}
#[derive(serde::Serialize)]
pub struct SearchQuery {
query: String,
#[serde(rename = "type")]
kind: SearchQueryKind,
#[serde(skip_serializing_if = "Option::is_none")]
category: Option<Category>,
#[serde(skip_serializing_if = "Option::is_none")]
amount: Option<usize>,
}
impl SearchQuery {
pub fn new(query: impl Into<String>, kind: impl Into<SearchQueryKind>) -> Self {
Self {
query: query.into(),
kind: kind.into(),
category: None,
amount: None,
}
}
pub fn category(mut self, category: impl Into<Category>) -> Self {
self.category = Some(category.into());
self
}
pub fn amount(mut self, amount: usize) -> Self {
self.amount = Some(amount);
self
}
fn apply_to(&self, r: ReqBuilder) -> ReqBuilder {
r.query(self)
}
}
#[derive(Copy, Clone)]
#[repr(u32)]
pub enum SearchQueryKind {
Image = 1,
Gif = 2,
}
impl serde::Serialize for SearchQueryKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u32(*self as u32)
}
}
#[cfg_attr(feature = "blocking", blocking)]
pub async fn search_with_client(
client: &Client,
query: SearchQuery,
) -> Result<NekosBestResponse, NekosBestError> {
let req = crate::prepare_request(client.client.get(format!("{BASE_URL}/search")));
#[cfg(not(feature = "blocking"))]
client.handle_search_ratelimit().await?;
let req = query.apply_to(req);
let res = req.send().await?;
#[cfg(not(feature = "blocking"))]
client.update_search_ratelimit_data(res.headers()).await;
parse_from_response(res).await
}
#[deprecated(
note = "Use `search_with_client` instead, and provide a client.",
since = "0.17.0"
)]
#[cfg_attr(feature = "blocking", blocking)]
pub async fn search(query: SearchQuery) -> Result<NekosBestResponse, NekosBestError> {
search_with_client(&Client::new(ClientConfig::default()), query).await
}