#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
pub mod chapter;
pub mod directory;
pub mod directory_list;
pub mod recent_chapter;
#[cfg(feature = "search")]
pub mod search;
pub(crate) mod tag;
pub(crate) mod utils;
use anyhow::{Context, Result};
use once_cell::sync::Lazy;
use reqwest::Url;
use serde::de::DeserializeOwned;
use chapter::Chapter;
use directory::Directory;
use directory_list::DirectoryList;
use recent_chapter::RecentChapter;
pub use chapter::ChapterConfig;
pub use directory::DirectoryConfig;
pub use directory_list::DirectoryListConfig;
pub use recent_chapter::RecentChapterConfig;
pub use tag::TagItem;
#[cfg(feature = "search")]
pub use search::{suggestion::SearchSuggestionConfig, SearchConfig};
static USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
pub static DYNASTY_READER_BASE: Lazy<Url> = Lazy::new(|| {
let s = std::env::var("DYNASTY_READER_BASE")
.unwrap_or_else(|_| "https://dynasty-scans.com".to_string());
let url = Url::parse(&s)
.expect("failed to parse `DYNASTY_READER_BASE` environment variable as `reqwest::Url`");
if url.cannot_be_a_base() {
panic!("`DYNASTY_READER_BASE` environment variable has invalid `reqwest::Url`")
}
url
});
#[derive(Debug, Clone)]
pub struct DynastyApi {
client: reqwest::Client,
}
impl Default for DynastyApi {
fn default() -> Self {
Self::new()
}
}
impl DynastyApi {
pub fn new() -> DynastyApi {
let client = reqwest::ClientBuilder::new()
.user_agent(USER_AGENT)
.build()
.unwrap();
DynastyApi::with_client(client)
}
pub fn with_client(client: reqwest::Client) -> DynastyApi {
DynastyApi { client }
}
pub async fn chapter(&self, config: ChapterConfig) -> Result<Chapter> {
self.execute_request_into_json(config).await
}
pub fn clone_reqwest_client(&self) -> reqwest::Client {
self.client.clone()
}
pub async fn directory(&self, config: DirectoryConfig) -> Result<Directory> {
use directory::UntaggedDirectory;
let directory_kind = config.kind;
self.execute_request_into_json(config)
.await
.map(|untagged: UntaggedDirectory| untagged.into_tagged(directory_kind))
}
pub async fn directory_list(&self, config: DirectoryListConfig) -> Result<DirectoryList> {
self.execute_request_into_json(config).await
}
pub async fn recent(&self, config: RecentChapterConfig) -> Result<RecentChapter> {
self.execute_request_into_json(config).await
}
#[cfg(feature = "search")]
pub async fn search(&self, config: SearchConfig) -> Result<search::Search> {
self.execute_request(config).await?.text().await?.parse()
}
#[cfg(feature = "search")]
pub async fn search_suggestions(
&self,
config: SearchSuggestionConfig,
) -> Result<Vec<search::suggestion::SearchSuggestion>> {
self.execute_request_into_json(config).await
}
async fn execute_request_into_json<R, T>(&self, route: R) -> Result<T>
where
R: DynastyReaderRoute,
T: DeserializeOwned,
{
let request_url = route.request_url();
self.execute_request(route)
.await?
.json::<T>()
.await
.with_context(|| format!("unable to parse `{}` response", request_url,))
}
async fn execute_request<R: DynastyReaderRoute>(&self, route: R) -> Result<reqwest::Response> {
let request_url = route.request_url();
route
.request_builder(&self.client, request_url.clone())
.send()
.await
.with_context(|| format!("failed to send request to `{}`", request_url))?
.error_for_status()
.map_err(|reqwest_error| {
anyhow::anyhow!(
"request to `{}` returns an unexpected status code `{}`",
request_url,
reqwest_error
.status()
.map(|status| status.as_u16())
.unwrap_or(500)
)
})
}
}
trait DynastyReaderRoute {
fn request_builder(&self, client: &reqwest::Client, url: Url) -> reqwest::RequestBuilder;
fn request_url(&self) -> reqwest::Url;
}
#[cfg(test)]
mod test_utils {
use std::{future::Future, time::Duration};
use anyhow::Result;
use once_cell::sync::Lazy;
use tryhard::{backoff_strategies::ExponentialBackoff, NoOnRetry, RetryFutureConfig};
use super::DynastyApi;
pub(crate) static DEFAULT_CLIENT: Lazy<DynastyApi> = Lazy::new(DynastyApi::default);
pub(crate) static TRYHARD_CONFIG: Lazy<RetryFutureConfig<ExponentialBackoff, NoOnRetry>> =
Lazy::new(|| RetryFutureConfig::new(7).exponential_backoff(Duration::from_millis(100)));
pub async fn tryhard_configs<I, F, FF, T>(
configs: impl IntoIterator<Item = I>,
future: F,
) -> Result<()>
where
I: Clone + Send + Sync + 'static,
F: Fn(&'static DynastyApi, I) -> FF + Send + Sync + Copy + 'static,
FF: Future<Output = Result<T>> + Send + 'static,
T: Send + 'static,
{
let mut handles = Vec::new();
for config in configs.into_iter() {
handles.push(tokio::spawn({
tryhard::retry_fn(move || future(&DEFAULT_CLIENT, config.clone()))
.with_config(*TRYHARD_CONFIG)
}))
}
for handle in handles {
handle.await??;
}
Ok(())
}
}