parse_book_source/engine/
mod.rs1mod internal;
7#[cfg(test)]
8mod tests;
9
10use crate::error::{BookSourceError, Result};
11use crate::eval::{eval_list, eval_value};
12use crate::fetch::cookie::CookieJar;
13use crate::fetch::{Fetcher, ReqwestFetcher};
14use crate::model::{BookInfo, BookList, Chapter, ExploreEntry, Toc, Volume};
15use crate::source::BookSource;
16use std::collections::{BTreeMap, HashMap};
17use std::sync::{Arc, RwLock};
18
19#[derive(Clone, Copy, Default)]
23struct RenderArgs<'a> {
24 render: bool,
25 ready_for: Option<&'a str>,
26 intercept_api: Option<&'a str>,
27 page: u32,
29 page_by: Option<&'a str>,
31}
32
33#[derive(Clone)]
35pub struct Engine {
36 source: Arc<BookSource>,
37 fetcher: Arc<dyn Fetcher>,
38 login_header: BTreeMap<String, String>,
42 cookies: Arc<RwLock<CookieJar>>,
45 source_vars: Arc<RwLock<BTreeMap<String, String>>>,
48 book_vars: Arc<RwLock<BTreeMap<String, String>>>,
51 page_cache: Arc<RwLock<HashMap<String, BookList>>>,
57 entries_cache: Arc<RwLock<Option<Vec<ExploreEntry>>>>,
62}
63
64impl std::fmt::Debug for Engine {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 f.debug_struct("Engine")
67 .field("source", &self.source.name)
68 .finish_non_exhaustive()
69 }
70}
71
72impl Engine {
73 pub fn new(source: BookSource) -> Result<Self> {
75 let fetcher = Arc::new(ReqwestFetcher::new(&source)?);
76 Ok(Self::with_fetcher(source, fetcher))
77 }
78
79 pub fn with_fetcher(source: BookSource, fetcher: Arc<dyn Fetcher>) -> Self {
83 Self {
84 source: Arc::new(source),
85 fetcher,
86 login_header: BTreeMap::new(),
87 cookies: Arc::new(RwLock::new(CookieJar::default())),
88 source_vars: Arc::new(RwLock::new(BTreeMap::new())),
89 book_vars: Arc::new(RwLock::new(BTreeMap::new())),
90 page_cache: Arc::new(RwLock::new(HashMap::new())),
91 entries_cache: Arc::new(RwLock::new(None)),
92 }
93 }
94
95 fn cached_page(&self, key: &str) -> Option<BookList> {
97 self.page_cache
98 .read()
99 .ok()
100 .and_then(|c| c.get(key).cloned())
101 }
102
103 fn cache_page(&self, key: String, list: &BookList) {
105 if let Ok(mut c) = self.page_cache.write() {
106 c.insert(key, list.clone());
107 }
108 }
109
110 #[must_use]
113 pub fn with_login_header(mut self, login_header: BTreeMap<String, String>) -> Self {
114 self.login_header = login_header;
115 self
116 }
117
118 #[must_use]
120 pub fn with_cookies(self, persistent: &BTreeMap<String, String>) -> Self {
121 if let Ok(mut jar) = self.cookies.write() {
122 *jar = CookieJar::from_persistent(persistent);
123 }
124 self
125 }
126
127 pub fn persistent_cookies(&self) -> BTreeMap<String, String> {
130 self.cookies
131 .read()
132 .map(|j| j.persistent())
133 .unwrap_or_default()
134 }
135
136 #[must_use]
138 pub fn with_book_vars(self, book_vars: BTreeMap<String, String>) -> Self {
139 if let Ok(mut g) = self.book_vars.write() {
140 *g = book_vars;
141 }
142 self
143 }
144
145 #[must_use]
147 pub fn with_source_vars(self, source_vars: &BTreeMap<String, String>) -> Self {
148 if let Ok(mut g) = self.source_vars.write() {
149 for (k, v) in source_vars {
150 g.insert(k.clone(), v.clone());
151 }
152 }
153 self
154 }
155
156 pub fn book_vars(&self) -> BTreeMap<String, String> {
158 self.book_vars.read().map(|g| g.clone()).unwrap_or_default()
159 }
160
161 pub fn source_vars(&self) -> BTreeMap<String, String> {
163 self.source_vars
164 .read()
165 .map(|g| g.clone())
166 .unwrap_or_default()
167 }
168
169 #[cfg(feature = "browser")]
173 pub fn with_browser_assist(
174 source: BookSource,
175 browser: Option<crate::fetch::browser::BrowserFetcher>,
176 ) -> Result<Self> {
177 let fetcher = crate::fetch::browser::EscalatingFetcher::new(&source, browser)?;
178 Ok(Self::with_fetcher(source, Arc::new(fetcher)))
179 }
180
181 pub fn source(&self) -> &BookSource {
183 &self.source
184 }
185
186 pub fn source_url(&self) -> &str {
188 &self.source.url
189 }
190
191 pub async fn warmup(&self) {
195 for u in &self.source.http.warmup {
196 let _ = self.run_request(self.get_req(u.clone())).await;
197 }
198 }
199
200 pub async fn book_info(&self, book_url: &str) -> Result<BookInfo> {
202 let mut chapter = self.base_vars();
203 self.run_prelude(&self.source.book_info.prelude, &mut chapter)
204 .await?;
205 let html = self.fetch_checked(book_url).await?;
206 let rules = self.source.book_info.as_book_rules();
207 self.eval_book_info(&rules, &html, &self.flatten(&chapter))
208 }
209
210 pub async fn toc(&self, toc_url: &str) -> Result<Toc> {
212 let toc = &self.source.toc;
213 let mut chapter = self.base_vars();
214 self.run_prelude(&toc.prelude, &mut chapter).await?;
215 let vars = self.flatten(&chapter);
216 let pages = self
217 .fetch_pages(toc_url, toc.next_page.as_ref(), toc.max_pages, &vars)
218 .await?;
219
220 let mut chapters: Vec<Chapter> = Vec::new();
221 let mut volumes: Vec<Volume> = Vec::new();
222 for page in &pages {
223 for item in eval_list(&toc.list, page)? {
224 let title = eval_value(&toc.name, &item, &vars)?;
225 let is_volume = match &toc.is_volume {
226 Some(r) => !eval_value(r, &item, &vars)?.trim().is_empty(),
227 None => false,
228 };
229 if is_volume {
230 volumes.push(Volume {
231 title,
232 first_chapter_index: chapters.len(),
233 });
234 } else {
235 let url = eval_value(&toc.url, &item, &vars)?;
236 chapters.push(Chapter {
237 title,
238 url,
239 is_volume: false,
240 });
241 }
242 }
243 }
244 Ok(Toc { chapters, volumes })
245 }
246
247 pub async fn content(&self, chapter_url: &str) -> Result<String> {
249 let c = &self.source.content;
250 let mut chapter = self.base_vars();
251 self.run_prelude(&c.prelude, &mut chapter).await?;
252 let vars = self.flatten(&chapter);
253 let pages = self
254 .fetch_pages(chapter_url, c.next_page.as_ref(), c.max_pages, &vars)
255 .await?;
256 let mut parts = Vec::with_capacity(pages.len());
257 for page in &pages {
258 parts.push(eval_value(&c.value, page, &vars)?);
259 }
260 Ok(parts.join("\n"))
261 }
262
263 pub async fn search(&self, key: &str, page: u32, page_size: u32) -> Result<BookList> {
266 let op = self
267 .source
268 .search
269 .as_ref()
270 .ok_or(BookSourceError::Missing("search"))?;
271 let mut extra = BTreeMap::new();
272 extra.insert("key".to_string(), key.to_string());
273 self.run_list_page(op, 's', &extra, page, page_size).await
274 }
275
276 pub async fn explore(
279 &self,
280 entry: &ExploreEntry,
281 page: u32,
282 page_size: u32,
283 ) -> Result<BookList> {
284 let op = self
285 .source
286 .explore
287 .as_ref()
288 .ok_or(BookSourceError::Missing("explore"))?;
289 self.run_list_page(&op.page, 'e', &entry.vars, page, page_size)
290 .await
291 }
292
293 pub async fn explore_entries(&self) -> Result<Vec<ExploreEntry>> {
299 if let Some(cached) = self.entries_cache.read().ok().and_then(|g| g.clone()) {
300 return Ok(cached);
301 }
302 let Some(op) = self.source.explore.as_ref() else {
303 return Ok(Vec::new());
304 };
305 let mut out: Vec<ExploreEntry> = Vec::new();
306 let mut first_err: Option<BookSourceError> = None;
307 for src in &op.entries {
308 match self.load_entry_source(src).await {
309 Ok(mut entries) => out.append(&mut entries),
310 Err(e) => {
311 first_err.get_or_insert(e);
312 }
313 }
314 }
315 if out.is_empty()
317 && let Some(e) = first_err.take()
318 {
319 return Err(e);
320 }
321 if first_err.is_none()
323 && let Ok(mut g) = self.entries_cache.write()
324 {
325 *g = Some(out.clone());
326 }
327 Ok(out)
328 }
329}