1use crate::errors::{Result, SpiderError};
8use crate::events::SpiderEventEmitter;
9use crate::protocol::protocol_adapter::ProtocolAdapter;
10use arc_swap::ArcSwap;
11use serde_json::Value;
12use std::sync::Arc;
13use std::time::{Duration, Instant};
14use tokio::time::sleep;
15
16#[cfg(feature = "ai")]
17use crate::ai::agent::{Agent, AgentOptions, AgentResult, AgentScope};
18#[cfg(feature = "ai")]
19use crate::ai::llm_provider::LLMProvider;
20
21pub struct SpiderPage {
28 adapter: ArcSwap<ProtocolAdapter>,
29 emitter: SpiderEventEmitter,
30 #[cfg(feature = "ai")]
31 llm: Option<Arc<dyn LLMProvider>>,
32}
33
34#[derive(Debug, Clone)]
44pub enum FieldSelector<'a> {
45 Text(&'a str),
48 Attr {
50 selector: &'a str,
51 attribute: &'a str,
52 },
53}
54
55impl<'a> From<&'a str> for FieldSelector<'a> {
56 fn from(s: &'a str) -> Self {
57 Self::Text(s)
58 }
59}
60
61impl SpiderPage {
62 pub fn new(adapter: ProtocolAdapter) -> Self {
68 Self {
69 adapter: ArcSwap::from_pointee(adapter),
70 emitter: SpiderEventEmitter::default(),
71 #[cfg(feature = "ai")]
72 llm: None,
73 }
74 }
75
76 pub fn from_arc(adapter: Arc<ProtocolAdapter>) -> Self {
78 Self {
79 adapter: ArcSwap::from(adapter),
80 emitter: SpiderEventEmitter::default(),
81 #[cfg(feature = "ai")]
82 llm: None,
83 }
84 }
85
86 #[cfg(feature = "ai")]
91 pub(crate) fn from_arc_with(
92 adapter: Arc<ProtocolAdapter>,
93 emitter: SpiderEventEmitter,
94 llm: Option<Arc<dyn LLMProvider>>,
95 ) -> Self {
96 Self {
97 adapter: ArcSwap::from(adapter),
98 emitter,
99 llm,
100 }
101 }
102
103 #[inline]
105 pub(crate) fn adapter(&self) -> arc_swap::Guard<Arc<ProtocolAdapter>> {
106 self.adapter.load()
107 }
108
109 #[cfg(feature = "ai")]
122 pub async fn agent(
123 &self,
124 instruction: &str,
125 options: Option<AgentOptions>,
126 ) -> Result<AgentResult> {
127 let mut opts = options.unwrap_or_default();
128 let provider: Arc<dyn LLMProvider> = if let Some(config) = opts.llm.take() {
129 Arc::from(crate::ai::llm_provider::create_provider(config))
130 } else {
131 self.llm.clone().ok_or_else(|| {
132 SpiderError::Llm(
133 "LLM not configured. Pass llm option to SpiderBrowser for AI methods.".into(),
134 )
135 })?
136 };
137 let adapter = self.adapter.load_full();
140 opts.scope = AgentScope::Page;
141 let agent = Agent::new(&adapter, provider.as_ref(), &self.emitter, Some(opts));
142 Ok(agent.execute(instruction).await)
143 }
144
145 pub async fn goto(&self, url: &str) -> Result<()> {
151 self.adapter().navigate(url).await
152 }
153
154 pub async fn goto_fast(&self, url: &str) -> Result<()> {
158 self.adapter().navigate_fast(url).await
159 }
160
161 pub async fn goto_dom(&self, url: &str) -> Result<()> {
166 self.adapter().navigate_dom(url).await
167 }
168
169 pub async fn go_back(&self) -> Result<()> {
171 self.adapter().evaluate("window.history.back()").await?;
172 Ok(())
173 }
174
175 pub async fn go_forward(&self) -> Result<()> {
177 self.adapter().evaluate("window.history.forward()").await?;
178 Ok(())
179 }
180
181 pub async fn reload(&self) -> Result<()> {
183 self.adapter().evaluate("window.location.reload()").await?;
184 Ok(())
185 }
186
187 pub async fn content(&self, wait_ms: u64, min_length: usize) -> Result<String> {
204 if wait_ms > 0 {
209 let early_html = self.adapter().get_html().await.unwrap_or_default();
210 if early_html.len() >= min_length
211 && !Self::is_interstitial_content(&early_html)
212 && !Self::is_rate_limit_content(&early_html)
213 {
214 return Ok(early_html);
215 }
216 self.wait_for_network_idle(wait_ms).await?;
217 }
218
219 let mut html = self.adapter().get_html().await.unwrap_or_default();
220
221 if wait_ms > 0 && Self::is_interstitial_content(&html) {
230 let interstitial_waits: &[u64] = &[2000, 2000, 3000, 4000, 5000, 7000, 7000];
231 for &wait in interstitial_waits {
232 sleep(Duration::from_millis(wait)).await;
233 html = self.adapter().get_html().await.unwrap_or_default();
234 if !Self::is_interstitial_content(&html) {
235 break;
236 }
237 if html.len() > 15_000 {
239 break;
240 }
241 }
242 if Self::is_interstitial_content(&html) {
245 return Err(SpiderError::Blocked(
246 "Page stuck on interstitial challenge".into(),
247 ));
248 }
249 }
250
251 if wait_ms > 0 && Self::is_rate_limit_content(&html) {
254 return Err(SpiderError::Blocked(
255 "Rate limit exceeded (site-level)".into(),
256 ));
257 }
258
259 if wait_ms > 0 && html.len() < min_length {
264 let increments: &[u64] = &[300, 500, 800, 1200];
265 for &extra in increments {
266 sleep(Duration::from_millis(extra)).await;
267 let updated = self.adapter().get_html().await.unwrap_or_default();
268 if updated.len() > html.len() {
269 html = updated;
270 }
271 if html.len() >= min_length {
272 break;
273 }
274 }
275 if html.len() < min_length {
279 let poll_deadline = Instant::now() + Duration::from_millis(3000);
280 while Instant::now() < poll_deadline {
281 sleep(Duration::from_millis(1000)).await;
282 let polled = self.adapter().get_html().await.unwrap_or_default();
283 if polled.len() > html.len() {
284 html = polled;
285 }
286 if html.len() >= min_length {
287 break;
288 }
289 }
290 }
291 }
292
293 Ok(html)
294 }
295
296 pub async fn raw_content(&self) -> Result<String> {
299 self.adapter().get_html().await
300 }
301
302 pub async fn content_with_early_return(
314 &self,
315 max_wait_ms: u64,
316 min_content_length: usize,
317 poll_interval_ms: u64,
318 ) -> Result<String> {
319 let deadline = Instant::now() + Duration::from_millis(max_wait_ms);
320 while Instant::now() < deadline {
321 let html = self.adapter().get_html().await.unwrap_or_default();
322 if html.len() >= min_content_length
323 && !Self::is_interstitial_content(&html)
324 && !Self::is_rate_limit_content(&html)
325 {
326 return Ok(html);
327 }
328 let remaining = deadline.saturating_duration_since(Instant::now());
329 if remaining.is_zero() {
330 break;
331 }
332 let wait = Duration::from_millis(poll_interval_ms).min(remaining);
333 sleep(wait).await;
334 }
335 Ok(self.adapter().get_html().await.unwrap_or_default())
337 }
338
339 pub async fn content_with_network_idle(
357 &self,
358 max_wait_ms: u64,
359 min_content_length: usize,
360 interstitial_budget_ms: u64,
361 ) -> Result<String> {
362 let deadline = Instant::now() + Duration::from_millis(max_wait_ms);
363
364 let mut html = self.adapter().get_html().await.unwrap_or_default();
366 if html.len() >= min_content_length
367 && !Self::is_interstitial_content(&html)
368 && !Self::is_rate_limit_content(&html)
369 {
370 return Ok(html);
371 }
372
373 let dom_deadline = deadline.min(Instant::now() + Duration::from_millis(5000));
375 while Instant::now() < dom_deadline {
376 let state = self.adapter().evaluate("document.readyState").await;
377 if let Ok(val) = state {
378 let s = val.as_str().unwrap_or("");
379 if s == "interactive" || s == "complete" {
380 break;
381 }
382 }
383 sleep(Duration::from_millis(200)).await;
384 }
385
386 let idle_ms: u64 = 400;
390 let idle_check_ms = {
391 let remaining = deadline.saturating_duration_since(Instant::now());
392 remaining.as_millis().min(8000) as u64
393 };
394 if idle_check_ms > 500 {
395 let js = format!(
396 r#"
397 new Promise((resolve) => {{
398 let lastActivity = Date.now();
399 const idleThreshold = {idle_ms};
400 const deadline = Date.now() + {idle_check_ms};
401 const perfObs = new PerformanceObserver(() => {{ lastActivity = Date.now(); }});
402 try {{ perfObs.observe({{ entryTypes: ['resource'] }}); }} catch(e) {{}}
403 const mutObs = new MutationObserver(() => {{ lastActivity = Date.now(); }});
404 mutObs.observe(document.documentElement, {{ childList: true, subtree: true, attributes: true }});
405 const check = () => {{
406 const now = Date.now();
407 if (now >= deadline || (now - lastActivity >= idleThreshold)) {{
408 perfObs.disconnect(); mutObs.disconnect(); resolve(true); return;
409 }}
410 setTimeout(check, 100);
411 }};
412 setTimeout(check, idleThreshold);
413 }})
414 "#
415 );
416 if self.adapter().evaluate(&js).await.is_err() {
417 sleep(Duration::from_millis(500)).await;
418 }
419 }
420
421 html = self.adapter().get_html().await.unwrap_or_default();
423 if html.len() >= min_content_length
424 && !Self::is_interstitial_content(&html)
425 && !Self::is_rate_limit_content(&html)
426 {
427 return Ok(html);
428 }
429
430 if Self::is_interstitial_content(&html) {
435 let i_deadline =
436 deadline.min(Instant::now() + Duration::from_millis(interstitial_budget_ms));
437 let waits: &[u64] = &[2000, 2000, 3000, 4000, 5000, 7000, 10000];
438 for &wait in waits {
439 if Instant::now() >= i_deadline {
440 break;
441 }
442 let remaining = i_deadline.saturating_duration_since(Instant::now());
443 let actual_wait = Duration::from_millis(wait).min(remaining);
444 sleep(actual_wait).await;
445 html = self.adapter().get_html().await.unwrap_or_default();
446 if !Self::is_interstitial_content(&html) {
447 break;
448 }
449 if html.len() > 15_000 {
450 break;
451 }
452 }
453 if Self::is_interstitial_content(&html) {
454 return Err(SpiderError::Blocked(
455 "Page stuck on interstitial challenge".into(),
456 ));
457 }
458 }
459
460 if Self::is_rate_limit_content(&html) {
461 return Err(SpiderError::Blocked(
462 "Rate limit exceeded (site-level)".into(),
463 ));
464 }
465
466 if html.len() < min_content_length {
468 while Instant::now() < deadline {
469 sleep(Duration::from_millis(1000)).await;
470 let polled = self.adapter().get_html().await.unwrap_or_default();
471 if polled.len() > html.len() {
472 html = polled;
473 }
474 if html.len() >= min_content_length {
475 break;
476 }
477 }
478 }
479
480 Ok(html)
481 }
482
483 pub async fn title(&self) -> Result<String> {
489 let val = self.adapter().evaluate("document.title").await?;
490 Ok(val.as_str().unwrap_or("").to_string())
491 }
492
493 pub async fn url(&self) -> Result<String> {
495 let val = self.adapter().evaluate("window.location.href").await?;
496 Ok(val.as_str().unwrap_or("").to_string())
497 }
498
499 pub async fn screenshot(&self) -> Result<String> {
501 self.adapter().capture_screenshot().await
502 }
503
504 pub async fn evaluate(&self, expression: &str) -> Result<Value> {
506 self.adapter().evaluate(expression).await
507 }
508
509 pub async fn save_snapshot(&self, snapshot_id: Option<&str>) -> Result<Value> {
518 let mut params = serde_json::Map::new();
519 if let Some(id) = snapshot_id {
520 params.insert("id".into(), Value::String(id.to_string()));
521 }
522 let resp = self
523 .adapter()
524 .send_command("Snapshot.capture", Value::Object(params))
525 .await?;
526 Ok(match resp.get("snapshot") {
528 Some(blob) => blob.clone(),
529 None => resp,
530 })
531 }
532
533 pub async fn restore_snapshot(&self, snapshot: Value) -> Result<Value> {
536 let blob = match snapshot.get("snapshot") {
537 Some(b) => b.clone(),
538 None => snapshot,
539 };
540 self.adapter()
541 .send_command("Snapshot.restore", serde_json::json!({ "snapshot": blob }))
542 .await
543 }
544
545 pub async fn delete_snapshot(&self, snapshot_id: &str) -> Result<Value> {
547 self.adapter()
548 .send_command("Snapshot.delete", serde_json::json!({ "id": snapshot_id }))
549 .await
550 }
551
552 pub async fn click(&self, selector: &str) -> Result<()> {
558 let (x, y) = self.get_element_center(selector).await?;
559 self.adapter().click_point(x, y).await
560 }
561
562 pub async fn click_at(&self, x: f64, y: f64) -> Result<()> {
564 self.adapter().click_point(x, y).await
565 }
566
567 pub async fn dblclick(&self, selector: &str) -> Result<()> {
569 let (x, y) = self.get_element_center(selector).await?;
570 self.adapter().double_click_point(x, y).await
571 }
572
573 pub async fn right_click(&self, selector: &str) -> Result<()> {
575 let (x, y) = self.get_element_center(selector).await?;
576 self.adapter().right_click_point(x, y).await
577 }
578
579 pub async fn click_and_hold(&self, selector: &str, hold_ms: u64) -> Result<()> {
587 let (x, y) = self.get_element_center(selector).await?;
588 self.adapter().click_hold_point(x, y, hold_ms).await
589 }
590
591 pub async fn click_and_hold_at(&self, x: f64, y: f64, hold_ms: u64) -> Result<()> {
597 self.adapter().click_hold_point(x, y, hold_ms).await
598 }
599
600 pub async fn click_all(&self, selector: &str) -> Result<()> {
602 let escaped = serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
603 let js = format!(
604 r#"
605 (function() {{
606 const els = document.querySelectorAll({escaped});
607 return Array.from(els).map(el => {{
608 const r = el.getBoundingClientRect();
609 return {{ x: r.x + r.width / 2, y: r.y + r.height / 2 }};
610 }});
611 }})()
612 "#
613 );
614 let result = self.adapter().evaluate(&js).await?;
615 if let Some(points) = result.as_array() {
616 for pt in points {
617 let x = pt.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0);
618 let y = pt.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0);
619 self.adapter().click_point(x, y).await?;
620 sleep(Duration::from_millis(100)).await;
621 }
622 }
623 Ok(())
624 }
625
626 pub async fn fill(&self, selector: &str, value: &str) -> Result<()> {
632 let escaped_sel =
633 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
634 let clear_js = format!(
636 r#"
637 (function() {{
638 const el = document.querySelector({escaped_sel});
639 if (el) {{ el.focus(); el.value = ''; }}
640 }})()
641 "#
642 );
643 self.adapter().evaluate(&clear_js).await?;
644
645 if let Ok((x, y)) = self.get_element_center(selector).await {
647 let _ = self.adapter().click_point(x, y).await;
648 }
649
650 self.adapter().insert_text(value).await?;
652
653 let dispatch_js = format!(
655 r#"
656 (function() {{
657 const el = document.querySelector({escaped_sel});
658 if (el) {{
659 el.dispatchEvent(new Event('input', {{ bubbles: true }}));
660 el.dispatchEvent(new Event('change', {{ bubbles: true }}));
661 }}
662 }})()
663 "#
664 );
665 self.adapter().evaluate(&dispatch_js).await?;
666 Ok(())
667 }
668
669 pub async fn type_text(&self, value: &str) -> Result<()> {
671 self.adapter().insert_text(value).await
672 }
673
674 pub async fn press(&self, key: &str) -> Result<()> {
676 self.adapter().press_key(key).await
677 }
678
679 pub async fn clear(&self, selector: &str) -> Result<()> {
681 let escaped =
682 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
683 let js = format!("document.querySelector({escaped}).value = ''");
684 self.adapter().evaluate(&js).await?;
685 Ok(())
686 }
687
688 pub async fn select(&self, selector: &str, value: &str) -> Result<()> {
690 let escaped_sel =
691 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
692 let escaped_val =
693 serde_json::to_string(value).unwrap_or_else(|_| format!("\"{}\"", value));
694 let js = format!(
695 r#"
696 (function() {{
697 const el = document.querySelector({escaped_sel});
698 if (el) {{
699 el.value = {escaped_val};
700 el.dispatchEvent(new Event('change', {{ bubbles: true }}));
701 }}
702 }})()
703 "#
704 );
705 self.adapter().evaluate(&js).await?;
706 Ok(())
707 }
708
709 pub async fn focus(&self, selector: &str) -> Result<()> {
715 let escaped =
716 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
717 let js = format!("document.querySelector({escaped})?.focus()");
718 self.adapter().evaluate(&js).await?;
719 Ok(())
720 }
721
722 pub async fn blur(&self, selector: &str) -> Result<()> {
724 let escaped =
725 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
726 let js = format!("document.querySelector({escaped})?.blur()");
727 self.adapter().evaluate(&js).await?;
728 Ok(())
729 }
730
731 pub async fn hover(&self, selector: &str) -> Result<()> {
733 let (x, y) = self.get_element_center(selector).await?;
734 self.adapter().hover_point(x, y).await
735 }
736
737 pub async fn drag(&self, from_selector: &str, to_selector: &str) -> Result<()> {
743 let (fx, fy) = self.get_element_center(from_selector).await?;
744 let (tx, ty) = self.get_element_center(to_selector).await?;
745 self.adapter().drag_point(fx, fy, tx, ty).await
746 }
747
748 pub async fn scroll_y(&self, pixels: i64) -> Result<()> {
754 let js = format!("window.scrollBy(0, {pixels})");
755 self.adapter().evaluate(&js).await?;
756 Ok(())
757 }
758
759 pub async fn scroll_x(&self, pixels: i64) -> Result<()> {
761 let js = format!("window.scrollBy({pixels}, 0)");
762 self.adapter().evaluate(&js).await?;
763 Ok(())
764 }
765
766 pub async fn scroll_to(&self, selector: &str) -> Result<()> {
768 let escaped =
769 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
770 let js = format!(
771 "document.querySelector({escaped})?.scrollIntoView({{ behavior: 'smooth', block: 'center' }})"
772 );
773 self.adapter().evaluate(&js).await?;
774 Ok(())
775 }
776
777 pub async fn scroll_to_point(&self, x: f64, y: f64) -> Result<()> {
779 let js = format!("window.scrollTo({x}, {y})");
780 self.adapter().evaluate(&js).await?;
781 Ok(())
782 }
783
784 pub async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<()> {
790 let interval: u64 = 100;
791 let max_iter = (timeout_ms + interval - 1) / interval; let escaped =
793 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
794 let check_js = format!("!!document.querySelector({escaped})");
795 for _ in 0..max_iter {
796 let found = self.adapter().evaluate(&check_js).await?;
797 if found.as_bool().unwrap_or(false) {
798 return Ok(());
799 }
800 sleep(Duration::from_millis(interval)).await;
801 }
802 Err(SpiderError::Timeout(format!(
803 "Timeout waiting for selector: {selector}"
804 )))
805 }
806
807 pub async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<()> {
809 let wait = timeout_ms.min(1000);
810 sleep(Duration::from_millis(wait)).await;
811 Ok(())
812 }
813
814 pub async fn wait_for_ready(&self, timeout_ms: u64) -> Result<()> {
823 let start = Instant::now();
824 let poll_interval: u64 = 200;
825 let stable_threshold = Duration::from_millis(500);
826 let timeout = Duration::from_millis(timeout_ms);
827
828 while start.elapsed() < timeout {
830 let state = self.adapter().evaluate("document.readyState").await;
831 if let Ok(val) = state {
832 if val.as_str() == Some("complete") {
833 break;
834 }
835 }
836 sleep(Duration::from_millis(poll_interval)).await;
837 }
838
839 let mut last_length: i64 = 0;
841 let mut stable_since = Instant::now();
842
843 while start.elapsed() < timeout {
844 let length = self
845 .adapter()
846 .evaluate("document.documentElement.innerHTML.length")
847 .await
848 .ok()
849 .and_then(|v| v.as_i64())
850 .unwrap_or(0);
851
852 if length != last_length {
853 last_length = length;
854 stable_since = Instant::now();
855 } else if stable_since.elapsed() >= stable_threshold {
856 return Ok(());
857 }
858
859 sleep(Duration::from_millis(poll_interval)).await;
860 }
861
862 Ok(())
863 }
864
865 pub async fn wait_for_content(&self, min_length: usize, timeout_ms: u64) -> Result<()> {
868 let start = Instant::now();
869 let timeout = Duration::from_millis(timeout_ms);
870 while start.elapsed() < timeout {
871 let length = self
872 .adapter()
873 .evaluate("document.documentElement.innerHTML.length")
874 .await
875 .ok()
876 .and_then(|v| v.as_u64())
877 .unwrap_or(0) as usize;
878 if length >= min_length {
879 return Ok(());
880 }
881 sleep(Duration::from_millis(200)).await;
882 }
883 Ok(())
884 }
885
886 pub async fn wait_for_network_idle(&self, timeout_ms: u64) -> Result<()> {
898 let start = Instant::now();
899 let poll_interval: u64 = 250;
900 let timeout = Duration::from_millis(timeout_ms);
901
902 while start.elapsed() < timeout {
904 let state = self.adapter().evaluate("document.readyState").await;
905 if let Ok(val) = state {
906 if val.as_str() == Some("complete") {
907 break;
908 }
909 }
910 sleep(Duration::from_millis(poll_interval)).await;
911 }
912
913 let idle_ms: u64 = 400;
918 let remaining = {
919 let elapsed = start.elapsed();
920 if timeout > elapsed {
921 (timeout - elapsed).as_millis().max(1000) as u64
922 } else {
923 1000
924 }
925 };
926 let js = format!(
927 r#"
928 new Promise((resolve) => {{
929 let lastActivity = Date.now();
930 const idleThreshold = {idle_ms};
931 const deadline = Date.now() + {remaining};
932
933 const perfObs = new PerformanceObserver(() => {{ lastActivity = Date.now(); }});
934 try {{ perfObs.observe({{ entryTypes: ['resource'] }}); }} catch(e) {{}}
935
936 const mutObs = new MutationObserver(() => {{ lastActivity = Date.now(); }});
937 mutObs.observe(document.documentElement, {{
938 childList: true, subtree: true, attributes: true
939 }});
940
941 const check = () => {{
942 const now = Date.now();
943 if (now >= deadline || (now - lastActivity >= idleThreshold)) {{
944 perfObs.disconnect();
945 mutObs.disconnect();
946 resolve(true);
947 return;
948 }}
949 setTimeout(check, 100);
950 }};
951 setTimeout(check, idleThreshold);
952 }})
953 "#
954 );
955 if self.adapter().evaluate(&js).await.is_err() {
956 sleep(Duration::from_millis(500)).await;
959 }
960
961 Ok(())
962 }
963
964 pub async fn set_viewport(
970 &self,
971 width: u32,
972 height: u32,
973 device_scale_factor: f64,
974 mobile: bool,
975 ) -> Result<()> {
976 self.adapter()
977 .set_viewport(width, height, device_scale_factor, mobile)
978 .await
979 }
980
981 pub async fn query_selector(&self, selector: &str) -> Result<Option<String>> {
987 let escaped =
988 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
989 let js = format!("document.querySelector({escaped})?.outerHTML ?? null");
990 let val = self.adapter().evaluate(&js).await?;
991 if val.is_null() {
992 Ok(None)
993 } else {
994 Ok(val.as_str().map(|s| s.to_string()))
995 }
996 }
997
998 pub async fn query_selector_all(&self, selector: &str) -> Result<Vec<String>> {
1000 let escaped =
1001 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
1002 let js = format!(
1003 "Array.from(document.querySelectorAll({escaped})).map(el => el.outerHTML)"
1004 );
1005 let val = self.adapter().evaluate(&js).await?;
1006 let items = val
1007 .as_array()
1008 .map(|arr| {
1009 arr.iter()
1010 .filter_map(|v| v.as_str().map(|s| s.to_string()))
1011 .collect()
1012 })
1013 .unwrap_or_default();
1014 Ok(items)
1015 }
1016
1017 pub async fn text_content(&self, selector: &str) -> Result<Option<String>> {
1019 let escaped =
1020 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
1021 let js = format!("document.querySelector({escaped})?.textContent ?? null");
1022 let val = self.adapter().evaluate(&js).await?;
1023 if val.is_null() {
1024 Ok(None)
1025 } else {
1026 Ok(val.as_str().map(|s| s.to_string()))
1027 }
1028 }
1029
1030 pub async fn extract_fields(
1052 &self,
1053 fields: &[(&str, FieldSelector<'_>)],
1054 ) -> Result<std::collections::HashMap<String, Option<String>>> {
1055 let field_map: Vec<Value> = fields
1057 .iter()
1058 .map(|(key, sel)| {
1059 let (css, attr) = match sel {
1060 FieldSelector::Text(s) => (*s, None),
1061 FieldSelector::Attr {
1062 selector,
1063 attribute,
1064 } => (*selector, Some(*attribute)),
1065 };
1066 serde_json::json!({
1067 "key": key,
1068 "selector": css,
1069 "attribute": attr,
1070 })
1071 })
1072 .collect();
1073
1074 let field_json = serde_json::to_string(&field_map)
1075 .unwrap_or_else(|_| "[]".to_string());
1076
1077 let js = format!(
1078 r#"
1079 (() => {{
1080 const fields = {field_json};
1081 const result = {{}};
1082 for (const f of fields) {{
1083 const el = document.querySelector(f.selector);
1084 result[f.key] = el
1085 ? (f.attribute ? el.getAttribute(f.attribute) : el.textContent?.trim()) ?? null
1086 : null;
1087 }}
1088 return JSON.stringify(result);
1089 }})()
1090 "#
1091 );
1092
1093 let val = self.adapter().evaluate(&js).await?;
1094 let raw = val.as_str().unwrap_or("{}");
1095 let parsed: std::collections::HashMap<String, Option<String>> =
1096 serde_json::from_str(raw).unwrap_or_default();
1097 Ok(parsed)
1098 }
1099
1100 async fn get_element_center(&self, selector: &str) -> Result<(f64, f64)> {
1107 let escaped =
1108 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
1109 let js = format!(
1110 r#"
1111 (function() {{
1112 const el = document.querySelector({escaped});
1113 if (!el) return null;
1114 el.scrollIntoView({{ block: 'center', behavior: 'instant' }});
1115 const r = el.getBoundingClientRect();
1116 return {{ x: r.x + r.width / 2, y: r.y + r.height / 2 }};
1117 }})()
1118 "#
1119 );
1120 let result = self.adapter().evaluate(&js).await?;
1121
1122 if result.is_null() {
1123 return Err(SpiderError::Other(format!(
1124 "Element not found: {selector}"
1125 )));
1126 }
1127
1128 let x = result
1129 .get("x")
1130 .and_then(|v| v.as_f64())
1131 .ok_or_else(|| SpiderError::Other(format!("Element not found: {selector}")))?;
1132 let y = result
1133 .get("y")
1134 .and_then(|v| v.as_f64())
1135 .ok_or_else(|| SpiderError::Other(format!("Element not found: {selector}")))?;
1136
1137 Ok((x, y))
1138 }
1139
1140 pub fn route_message(&self, data: &str) {
1142 self.adapter.load().route_message(data);
1143 }
1144
1145 pub fn destroy(&self) {
1147 self.adapter.load().destroy();
1148 }
1149
1150 pub fn set_adapter(&self, adapter: ProtocolAdapter) {
1156 self.adapter.store(Arc::new(adapter));
1157 }
1158
1159 pub fn set_adapter_arc(&self, adapter: Arc<ProtocolAdapter>) {
1161 self.adapter.store(adapter);
1162 }
1163
1164 pub fn is_interstitial_content(html: &str) -> bool {
1169 if html.len() > 15_000 {
1170 return false; }
1172 let lower = html.to_lowercase();
1173
1174 if lower.contains("just a moment")
1176 || lower.contains("checking your browser")
1177 || lower.contains("please wait while we verify")
1178 || lower.contains("verifying the device")
1179 || lower.contains("available after verification")
1180 || lower.contains("ddos-guard")
1181 || lower.contains("challenge-platform")
1182 || lower.contains("px-captcha")
1183 || lower.contains("_cf_chl_opt")
1184 || lower.contains("managed_challenge")
1185 || lower.contains("datadome")
1186 || lower.contains("ak_bmsc")
1187 || lower.contains("please enable cookies")
1188 {
1189 return true;
1190 }
1191
1192 if html.len() < 5_000 {
1197 if lower.contains("loading...") || lower.contains("loading results") {
1198 return true;
1199 }
1200 if lower.contains("please wait") && !lower.contains("article") {
1201 return true;
1202 }
1203 }
1204
1205 false
1206 }
1207
1208 pub fn is_rate_limit_content(html: &str) -> bool {
1213 if html.len() > 20_000 {
1214 return false; }
1216 let lower = html.to_lowercase();
1217 lower.contains("rate limit exceeded")
1218 || lower.contains("too many requests")
1219 || (lower.contains("rate limit") && lower.contains("please try again"))
1220 }
1221}
1222
1223#[cfg(test)]
1224mod tests {
1225 use super::*;
1226
1227 #[test]
1232 fn interstitial_cloudflare() {
1233 let html = "<html><body>Just a moment...</body></html>";
1234 assert!(SpiderPage::is_interstitial_content(html));
1235 }
1236
1237 #[test]
1238 fn interstitial_checking_browser() {
1239 let html = "<html><body>Checking your browser before accessing</body></html>";
1240 assert!(SpiderPage::is_interstitial_content(html));
1241 }
1242
1243 #[test]
1244 fn interstitial_perimeterx() {
1245 let html = "<html><body>Verifying the device...</body></html>";
1246 assert!(SpiderPage::is_interstitial_content(html));
1247 }
1248
1249 #[test]
1250 fn interstitial_ddos_guard() {
1251 let html = "<html><head></head><body>ddos-guard check</body></html>";
1252 assert!(SpiderPage::is_interstitial_content(html));
1253 }
1254
1255 #[test]
1256 fn interstitial_challenge_platform() {
1257 let html = "<html><body class='challenge-platform'>wait</body></html>";
1258 assert!(SpiderPage::is_interstitial_content(html));
1259 }
1260
1261 #[test]
1262 fn interstitial_px_captcha() {
1263 let html = "<html><body><div id='px-captcha'></div></body></html>";
1264 assert!(SpiderPage::is_interstitial_content(html));
1265 }
1266
1267 #[test]
1268 fn interstitial_cf_chl_opt() {
1269 let html = "<html><body><script>var _cf_chl_opt={}</script></body></html>";
1270 assert!(SpiderPage::is_interstitial_content(html));
1271 }
1272
1273 #[test]
1274 fn interstitial_managed_challenge() {
1275 let html = "<html><body>managed_challenge page</body></html>";
1276 assert!(SpiderPage::is_interstitial_content(html));
1277 }
1278
1279 #[test]
1280 fn interstitial_datadome() {
1281 let html = "<html><body>DataDome verification</body></html>";
1282 assert!(SpiderPage::is_interstitial_content(html));
1283 }
1284
1285 #[test]
1286 fn interstitial_akamai() {
1287 let html = "<html><body><script>ak_bmsc=cookie</script></body></html>";
1288 assert!(SpiderPage::is_interstitial_content(html));
1289 }
1290
1291 #[test]
1292 fn interstitial_enable_cookies() {
1293 let html = "<html><body>Please enable cookies to continue</body></html>";
1294 assert!(SpiderPage::is_interstitial_content(html));
1295 }
1296
1297 #[test]
1298 fn interstitial_loading_small() {
1299 let html = "<html><body>Loading...</body></html>";
1300 assert!(SpiderPage::is_interstitial_content(html));
1301 }
1302
1303 #[test]
1304 fn interstitial_loading_results() {
1305 let html = "<html><body>Loading results</body></html>";
1306 assert!(SpiderPage::is_interstitial_content(html));
1307 }
1308
1309 #[test]
1310 fn interstitial_please_wait_small() {
1311 let html = "<html><body>Please wait</body></html>";
1312 assert!(SpiderPage::is_interstitial_content(html));
1313 }
1314
1315 #[test]
1316 fn interstitial_please_wait_with_article_not_detected() {
1317 let html = "<html><body>Please wait for this article</body></html>";
1318 assert!(!SpiderPage::is_interstitial_content(html));
1319 }
1320
1321 #[test]
1322 fn interstitial_large_page_not_detected() {
1323 let html = "x".repeat(16_000);
1324 assert!(!SpiderPage::is_interstitial_content(&html));
1325 }
1326
1327 #[test]
1328 fn interstitial_normal_page_not_detected() {
1329 let html = "<html><body><h1>Welcome</h1><p>Normal content here.</p></body></html>";
1330 assert!(!SpiderPage::is_interstitial_content(html));
1331 }
1332
1333 #[test]
1338 fn rate_limit_exceeded() {
1339 let html = "<html><body>Rate limit exceeded</body></html>";
1340 assert!(SpiderPage::is_rate_limit_content(html));
1341 }
1342
1343 #[test]
1344 fn rate_limit_too_many_requests() {
1345 let html = "<html><body>Too many requests</body></html>";
1346 assert!(SpiderPage::is_rate_limit_content(html));
1347 }
1348
1349 #[test]
1350 fn rate_limit_try_again() {
1351 let html = "<html><body>Rate limit hit. Please try again later.</body></html>";
1352 assert!(SpiderPage::is_rate_limit_content(html));
1353 }
1354
1355 #[test]
1356 fn rate_limit_large_page_not_detected() {
1357 let html = format!(
1358 "<html><body>{}</body></html>",
1359 "x".repeat(21_000)
1360 );
1361 assert!(!SpiderPage::is_rate_limit_content(&html));
1362 }
1363
1364 #[test]
1365 fn rate_limit_normal_page_not_detected() {
1366 let html = "<html><body><h1>Normal page</h1></body></html>";
1367 assert!(!SpiderPage::is_rate_limit_content(html));
1368 }
1369}