mod internal;
#[cfg(test)]
mod tests;
use crate::error::{BookSourceError, Result};
use crate::eval::{eval_list, eval_value};
use crate::fetch::cookie::CookieJar;
use crate::fetch::{Fetcher, ReqwestFetcher};
use crate::model::{BookInfo, BookList, Chapter, ExploreEntry, Toc, Volume};
use crate::source::BookSource;
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, RwLock};
#[derive(Clone, Copy, Default)]
struct RenderArgs<'a> {
render: bool,
ready_for: Option<&'a str>,
intercept_api: Option<&'a str>,
page: u32,
page_by: Option<&'a str>,
}
#[derive(Clone)]
pub struct Engine {
source: Arc<BookSource>,
fetcher: Arc<dyn Fetcher>,
login_header: BTreeMap<String, String>,
cookies: Arc<RwLock<CookieJar>>,
source_vars: Arc<RwLock<BTreeMap<String, String>>>,
book_vars: Arc<RwLock<BTreeMap<String, String>>>,
page_cache: Arc<RwLock<HashMap<String, BookList>>>,
entries_cache: Arc<RwLock<Option<Vec<ExploreEntry>>>>,
}
impl std::fmt::Debug for Engine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Engine")
.field("source", &self.source.name)
.finish_non_exhaustive()
}
}
impl Engine {
pub fn new(source: BookSource) -> Result<Self> {
let fetcher = Arc::new(ReqwestFetcher::new(&source)?);
Ok(Self::with_fetcher(source, fetcher))
}
pub fn with_fetcher(source: BookSource, fetcher: Arc<dyn Fetcher>) -> Self {
Self {
source: Arc::new(source),
fetcher,
login_header: BTreeMap::new(),
cookies: Arc::new(RwLock::new(CookieJar::default())),
source_vars: Arc::new(RwLock::new(BTreeMap::new())),
book_vars: Arc::new(RwLock::new(BTreeMap::new())),
page_cache: Arc::new(RwLock::new(HashMap::new())),
entries_cache: Arc::new(RwLock::new(None)),
}
}
fn cached_page(&self, key: &str) -> Option<BookList> {
self.page_cache
.read()
.ok()
.and_then(|c| c.get(key).cloned())
}
fn cache_page(&self, key: String, list: &BookList) {
if let Ok(mut c) = self.page_cache.write() {
c.insert(key, list.clone());
}
}
#[must_use]
pub fn with_login_header(mut self, login_header: BTreeMap<String, String>) -> Self {
self.login_header = login_header;
self
}
#[must_use]
pub fn with_cookies(self, persistent: &BTreeMap<String, String>) -> Self {
if let Ok(mut jar) = self.cookies.write() {
*jar = CookieJar::from_persistent(persistent);
}
self
}
pub fn persistent_cookies(&self) -> BTreeMap<String, String> {
self.cookies
.read()
.map(|j| j.persistent())
.unwrap_or_default()
}
#[must_use]
pub fn with_book_vars(self, book_vars: BTreeMap<String, String>) -> Self {
if let Ok(mut g) = self.book_vars.write() {
*g = book_vars;
}
self
}
#[must_use]
pub fn with_source_vars(self, source_vars: &BTreeMap<String, String>) -> Self {
if let Ok(mut g) = self.source_vars.write() {
for (k, v) in source_vars {
g.insert(k.clone(), v.clone());
}
}
self
}
pub fn book_vars(&self) -> BTreeMap<String, String> {
self.book_vars.read().map(|g| g.clone()).unwrap_or_default()
}
pub fn source_vars(&self) -> BTreeMap<String, String> {
self.source_vars
.read()
.map(|g| g.clone())
.unwrap_or_default()
}
#[cfg(feature = "browser")]
pub fn with_browser_assist(
source: BookSource,
browser: Option<crate::fetch::browser::BrowserFetcher>,
) -> Result<Self> {
let fetcher = crate::fetch::browser::EscalatingFetcher::new(&source, browser)?;
Ok(Self::with_fetcher(source, Arc::new(fetcher)))
}
pub fn source(&self) -> &BookSource {
&self.source
}
pub fn source_url(&self) -> &str {
&self.source.url
}
pub async fn warmup(&self) {
for u in &self.source.http.warmup {
let _ = self.run_request(self.get_req(u.clone())).await;
}
}
pub async fn book_info(&self, book_url: &str) -> Result<BookInfo> {
let mut chapter = self.base_vars();
self.run_prelude(&self.source.book_info.prelude, &mut chapter)
.await?;
let html = self.fetch_checked(book_url).await?;
let rules = self.source.book_info.as_book_rules();
self.eval_book_info(&rules, &html, &self.flatten(&chapter))
}
pub async fn toc(&self, toc_url: &str) -> Result<Toc> {
let toc = &self.source.toc;
let mut chapter = self.base_vars();
self.run_prelude(&toc.prelude, &mut chapter).await?;
let vars = self.flatten(&chapter);
let pages = self
.fetch_pages(toc_url, toc.next_page.as_ref(), toc.max_pages, &vars)
.await?;
let mut chapters: Vec<Chapter> = Vec::new();
let mut volumes: Vec<Volume> = Vec::new();
for page in &pages {
for item in eval_list(&toc.list, page)? {
let title = eval_value(&toc.name, &item, &vars)?;
let is_volume = match &toc.is_volume {
Some(r) => !eval_value(r, &item, &vars)?.trim().is_empty(),
None => false,
};
if is_volume {
volumes.push(Volume {
title,
first_chapter_index: chapters.len(),
});
} else {
let url = eval_value(&toc.url, &item, &vars)?;
chapters.push(Chapter {
title,
url,
is_volume: false,
});
}
}
}
Ok(Toc { chapters, volumes })
}
pub async fn content(&self, chapter_url: &str) -> Result<String> {
let c = &self.source.content;
let mut chapter = self.base_vars();
self.run_prelude(&c.prelude, &mut chapter).await?;
let vars = self.flatten(&chapter);
let pages = self
.fetch_pages(chapter_url, c.next_page.as_ref(), c.max_pages, &vars)
.await?;
let mut parts = Vec::with_capacity(pages.len());
for page in &pages {
parts.push(eval_value(&c.value, page, &vars)?);
}
Ok(parts.join("\n"))
}
pub async fn search(&self, key: &str, page: u32, page_size: u32) -> Result<BookList> {
let op = self
.source
.search
.as_ref()
.ok_or(BookSourceError::Missing("search"))?;
let mut extra = BTreeMap::new();
extra.insert("key".to_string(), key.to_string());
self.run_list_page(op, 's', &extra, page, page_size).await
}
pub async fn explore(
&self,
entry: &ExploreEntry,
page: u32,
page_size: u32,
) -> Result<BookList> {
let op = self
.source
.explore
.as_ref()
.ok_or(BookSourceError::Missing("explore"))?;
self.run_list_page(&op.page, 'e', &entry.vars, page, page_size)
.await
}
pub async fn explore_entries(&self) -> Result<Vec<ExploreEntry>> {
if let Some(cached) = self.entries_cache.read().ok().and_then(|g| g.clone()) {
return Ok(cached);
}
let Some(op) = self.source.explore.as_ref() else {
return Ok(Vec::new());
};
let mut out: Vec<ExploreEntry> = Vec::new();
let mut first_err: Option<BookSourceError> = None;
for src in &op.entries {
match self.load_entry_source(src).await {
Ok(mut entries) => out.append(&mut entries),
Err(e) => {
first_err.get_or_insert(e);
}
}
}
if out.is_empty()
&& let Some(e) = first_err.take()
{
return Err(e);
}
if first_err.is_none()
&& let Ok(mut g) = self.entries_cache.write()
{
*g = Some(out.clone());
}
Ok(out)
}
}