use std::sync::LazyLock;
use std::time::Duration;
use crate::error::{BooruError, Result};
#[cfg(feature = "danbooru")]
pub mod danbooru;
#[cfg(feature = "gelbooru")]
pub mod gelbooru;
pub mod generic;
#[cfg(feature = "rule34")]
pub mod rule34;
#[cfg(feature = "safebooru")]
pub mod safebooru;
static SHARED_CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.pool_max_idle_per_host(10)
.pool_idle_timeout(Duration::from_secs(30))
.build()
.expect("Failed to create HTTP client")
});
#[inline]
pub fn shared_client() -> &'static reqwest::Client {
&SHARED_CLIENT
}
#[derive(Debug)]
pub struct ClientBuilder<T: Client> {
pub(crate) client: reqwest::Client,
pub(crate) key: Option<String>,
pub(crate) user: Option<String>,
pub(crate) tags: Vec<String>,
pub(crate) limit: u32,
pub(crate) url: String,
pub(crate) page: u32,
_marker: std::marker::PhantomData<T>,
}
impl<T: Client> Clone for ClientBuilder<T> {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
key: self.key.clone(),
user: self.user.clone(),
tags: self.tags.clone(),
limit: self.limit,
url: self.url.clone(),
page: self.page,
_marker: std::marker::PhantomData,
}
}
}
pub trait Client: From<ClientBuilder<Self>> + Sized + Send + Sync {
type Post: Send;
type Rating: Into<String> + Send;
const URL: &'static str;
const SORT: &'static str;
const MAX_TAGS: Option<usize>;
#[must_use]
fn builder() -> ClientBuilder<Self> {
ClientBuilder::new()
}
fn get_by_id(&self, id: u32) -> impl std::future::Future<Output = Result<Self::Post>> + Send;
fn get(&self) -> impl std::future::Future<Output = Result<Vec<Self::Post>>> + Send;
}
impl<T: Client> ClientBuilder<T> {
#[must_use]
pub fn new() -> Self {
Self {
client: SHARED_CLIENT.clone(),
key: None,
user: None,
tags: Vec::new(),
limit: 100,
url: T::URL.to_string(),
page: 0,
_marker: std::marker::PhantomData,
}
}
#[must_use]
pub fn with_client(client: reqwest::Client) -> Self {
Self {
client,
key: None,
user: None,
tags: Vec::new(),
limit: 100,
url: T::URL.to_string(),
page: 0,
_marker: std::marker::PhantomData,
}
}
#[must_use]
pub fn with_custom_url(mut self, url: &str) -> Self {
self.url = url.to_string();
self
}
#[must_use]
pub fn set_credentials(mut self, key: impl Into<String>, user: impl Into<String>) -> Self {
self.key = Some(key.into());
self.user = Some(user.into());
self
}
pub fn tag(mut self, tag: impl Into<String>) -> Result<Self> {
if let Some(max) = T::MAX_TAGS
&& self.tags.len() >= max
{
return Err(BooruError::TagLimitExceeded {
client: std::any::type_name::<T>()
.rsplit("::")
.next()
.unwrap_or("Unknown"),
max,
actual: self.tags.len() + 1,
});
}
self.tags.push(tag.into());
Ok(self)
}
#[must_use]
pub fn rating(mut self, rating: T::Rating) -> Self {
self.tags.push(format!("rating:{}", rating.into()));
self
}
#[must_use]
pub fn limit(mut self, limit: u32) -> Self {
self.limit = limit;
self
}
#[must_use]
pub fn random(mut self) -> Self {
self.tags.push(format!("{}random", T::SORT));
self
}
#[must_use]
pub fn sort(mut self, order: generic::Sort) -> Self {
self.tags.push(format!("{}{}", T::SORT, order));
self
}
#[must_use]
pub fn blacklist_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(format!("-{}", tag.into()));
self
}
#[must_use]
pub fn default_url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
#[must_use]
pub fn page(mut self, page: u32) -> Self {
self.page = page;
self
}
pub fn tags<I, S>(mut self, tags: I) -> Result<Self>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for tag in tags {
self = self.tag(tag)?;
}
Ok(self)
}
#[must_use]
pub fn blacklist_tags<I, S>(mut self, tags: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
for tag in tags {
self = self.blacklist_tag(tag);
}
self
}
#[must_use]
pub fn tag_count(&self) -> usize {
self.tags.len()
}
#[must_use]
pub fn has_tags(&self) -> bool {
!self.tags.is_empty()
}
#[must_use]
pub fn build(self) -> T {
T::from(self)
}
}
impl<T: Client> Default for ClientBuilder<T> {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "danbooru")]
pub use danbooru::DanbooruClient;
#[cfg(feature = "gelbooru")]
pub use gelbooru::GelbooruClient;
#[cfg(feature = "rule34")]
pub use rule34::Rule34Client;
#[cfg(feature = "safebooru")]
pub use safebooru::SafebooruClient;