pub use headless_chrome::protocol::cdp::CSS::CSSComputedStyleProperty;
use headless_chrome::{Browser as HeadlessBrowser, Element, LaunchOptions};
use image::DynamicImage;
use once_cell::sync::Lazy;
use scraper::Html;
use serde::de::DeserializeOwned;
use std::sync::Arc;
use url::Url;
use super::{extract_article, NodeRef};
use crate::context::document::Document;
static BROWSER: Browser = Browser::new();
pub(crate) struct Browser {
headless_client: Lazy<Result<HeadlessBrowser, String>>,
headfull_client: Lazy<Result<HeadlessBrowser, String>>,
}
impl std::fmt::Debug for Browser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Browser").finish()
}
}
impl Default for Browser {
fn default() -> Self {
Self::new()
}
}
impl Browser {
pub const fn new() -> Self {
Self {
headless_client: Lazy::new(|| {
let browser = HeadlessBrowser::new(
LaunchOptions::default_builder()
.headless(true)
.build()
.expect("Could not find chrome-executable"),
)
.map_err(|err| err.to_string())?;
Ok(browser)
}),
headfull_client: Lazy::new(|| {
let browser = HeadlessBrowser::new(
LaunchOptions::default_builder()
.headless(false)
.build()
.expect("Could not find chrome-executable"),
)
.map_err(|err| err.to_string())?;
Ok(browser)
}),
}
}
#[tracing::instrument]
pub fn new_tab(&self, headless: bool) -> Result<Tab, anyhow::Error> {
let client = if headless {
&self.headless_client
} else {
&self.headfull_client
};
let browser = client.as_ref().map_err(|err| anyhow::anyhow!("{}", err))?;
let tab = browser.new_tab()?;
Ok(Tab { inner: tab })
}
}
#[derive(Clone)]
pub struct Tab {
pub(crate) inner: Arc<headless_chrome::Tab>,
}
impl Tab {
pub fn new(url: Url, headless: bool) -> Result<Self, anyhow::Error> {
let tab = BROWSER.new_tab(headless)?;
tab.goto(url.as_ref())?;
Ok(tab)
}
pub fn inner(&self) -> Arc<headless_chrome::Tab> {
self.inner.clone()
}
#[tracing::instrument]
pub fn goto(&self, url: &str) -> Result<(), anyhow::Error> {
self.inner.navigate_to(url)?.wait_until_navigated()?;
Ok(())
}
#[tracing::instrument]
pub fn find(&self, selector: &str) -> Result<Node, anyhow::Error> {
let element = self.inner.wait_for_element(selector)?;
Ok(Node { inner: element })
}
#[tracing::instrument]
pub fn screenshot(&self) -> Result<DynamicImage, anyhow::Error> {
let bytes = self.inner.capture_screenshot(
headless_chrome::protocol::cdp::Page::CaptureScreenshotFormatOption::Jpeg,
None,
None,
false,
)?;
let image = image::load_from_memory(&bytes)?;
Ok(image)
}
pub fn url(&self) -> Url {
self.inner.get_url().parse().unwrap()
}
pub fn article(&self) -> anyhow::Result<Document> {
let html = self.inner.get_content()?;
extract_article(&html)
}
pub fn title(&self) -> Option<String> {
self.inner.get_title().ok()
}
pub fn html(&self) -> anyhow::Result<Html> {
Ok(Html::parse_document(&self.inner.get_content()?))
}
pub fn node(&self, node_ref: NodeRef) -> anyhow::Result<Node<'_>> {
if let NodeRef::Dynamic(node_id) = node_ref {
Ok(Element::new(&self.inner, node_id)?.into())
} else {
anyhow::bail!("NodeRef is not a DynamicNodeRef")
}
}
}
impl Drop for Tab {
#[tracing::instrument]
fn drop(&mut self) {
let _ = self.inner.close(true);
}
}
impl std::fmt::Debug for Tab {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Tab").finish()
}
}
#[derive(Debug)]
pub struct Node<'a> {
inner: Element<'a>,
}
impl<'a> From<Element<'a>> for Node<'a> {
fn from(inner: Element<'a>) -> Self {
Self { inner }
}
}
impl<'a> Node<'a> {
pub fn id(&self) -> NodeRef {
NodeRef::Dynamic(self.inner.node_id)
}
#[tracing::instrument]
pub fn get_text(&self) -> Result<String, anyhow::Error> {
let text = self.inner.get_inner_text()?;
Ok(text)
}
#[tracing::instrument]
pub fn click(&self) -> Result<(), anyhow::Error> {
self.inner.click()?;
Ok(())
}
#[tracing::instrument]
pub fn send_keys(&self, keys: &str) -> Result<(), anyhow::Error> {
self.inner.type_into(keys)?;
Ok(())
}
#[tracing::instrument]
pub fn outer_html(&self) -> Result<String, anyhow::Error> {
let html = self.inner.get_content()?;
Ok(html)
}
#[tracing::instrument]
pub fn outer_html_visible(&self) -> Result<String, anyhow::Error> {
self.call_js_fn(
r#"
function(...args) {
function node_to_html(node) {
// If this is a text node, return the text content
if (node instanceof Text) {
return node.textContent;
}
if (node instanceof HTMLElement && node.checkVisibility()) {
let tag = node.tagName.toLowerCase();
let text = "<" + tag;
for (const attr of node.attributes) {
text += " " + attr.name + "=\"" + attr.value + "\"";
}
text += ">";
if (node.hasChildNodes()) {
for (const child of node.childNodes) {
text += node_to_html(child);
}
}
text += "</" + tag + ">";
return text;
}
return "";
}
return node_to_html(this);
}
"#,
vec![],
false,
)
}
#[tracing::instrument]
pub fn screenshot(&self) -> Result<DynamicImage, anyhow::Error> {
let bytes = self.inner.capture_screenshot(
headless_chrome::protocol::cdp::Page::CaptureScreenshotFormatOption::Jpeg,
)?;
let image = image::load_from_memory(&bytes)?;
Ok(image)
}
#[tracing::instrument]
pub fn find_child(&self, selector: &str) -> Result<Self, anyhow::Error> {
let child = self.inner.find_element(selector)?;
Ok(Self { inner: child })
}
#[tracing::instrument]
pub fn name(&self) -> &str {
&self.inner.tag_name
}
#[tracing::instrument]
pub fn attributes(&self) -> Result<Vec<(String, String)>, anyhow::Error> {
let Some(attributes) = self.inner.get_attributes()? else {
return Ok(Vec::new());
};
Ok(attributes
.into_iter()
.filter_map(|attribute| {
let value = self.inner.get_attribute_value(&attribute).ok()??;
Some((attribute, value))
})
.collect())
}
pub fn computed_style(&self) -> anyhow::Result<Vec<CSSComputedStyleProperty>> {
self.inner.get_computed_styles()
}
pub fn is_visible(&self) -> anyhow::Result<bool> {
self.call_js_fn(
"function(...args) { return this.checkVisibility(); }",
vec![],
false,
)
}
pub fn call_js_fn<R: DeserializeOwned>(
&self,
function: &str,
args: Vec<serde_json::Value>,
async_function: bool,
) -> anyhow::Result<R> {
let result = self.inner.call_js_fn(function, args, async_function)?;
match result.value {
Some(value) => Ok(serde_json::from_value(value)?),
None => Err(anyhow::anyhow!("No value returned from function")),
}
}
#[tracing::instrument]
pub fn hover(&self) -> anyhow::Result<()> {
self.inner.move_mouse_over()?;
Ok(())
}
pub fn children(&self) -> anyhow::Result<Vec<NodeRef>> {
let node_info = self.inner.get_description()?;
let children = node_info.children.unwrap_or_default();
let children = children.iter().map(|child| {
let child_id = child.node_id;
NodeRef::Dynamic(child_id)
});
Ok(children.collect())
}
}