use crate::browser::cdp::CdpClient;
use crate::browser::navigation::NavigationManager;
use crate::browser::profile::BrowserProfile;
use crate::browser::screenshot::ScreenshotManager;
use crate::browser::session_manager::SessionManager;
use crate::browser::tab_manager::TabManager;
use crate::browser::views::SessionInfo;
use crate::error::{BrowsingError, Result};
use crate::traits::BrowserClient;
use async_trait::async_trait;
use std::sync::Arc;
pub struct Browser {
session_manager: SessionManager,
tab_manager: TabManager,
navigation_manager: NavigationManager,
screenshot_manager: ScreenshotManager,
stealth: bool,
}
impl Browser {
pub fn new(profile: BrowserProfile) -> Self {
let stealth = profile.stealth;
Self {
session_manager: SessionManager::new(profile),
tab_manager: TabManager::new(),
navigation_manager: NavigationManager::new(),
screenshot_manager: ScreenshotManager::new(),
stealth,
}
}
pub fn with_cdp_url(mut self, cdp_url: String) -> Self {
self.session_manager = self.session_manager.with_cdp_url(cdp_url);
self
}
pub async fn start(&mut self) -> Result<()> {
let _cdp_url = self.session_manager.start().await?;
let client = self.session_manager.get_cdp_client()?;
if self.stealth {
crate::browser::stealth::enable_stealth(&client).await?;
}
let targets = client
.send_command("Target.getTargets", serde_json::json!({}))
.await?;
if let Some(target_infos) = targets["targetInfos"].as_array() {
let page_target = target_infos
.iter()
.find(|t| t["type"].as_str() == Some("page"))
.or_else(|| target_infos.first());
if let Some(target) = page_target
&& let Some(target_id) = target["targetId"].as_str() {
let session = self
.session_manager
.create_session(target_id.to_string(), None)
.await?;
self.tab_manager
.set_current_target_id(target_id.to_string());
self.tab_manager
.insert_session(target_id.to_string(), session);
}
}
Ok(())
}
pub async fn navigate(&mut self, url: &str) -> Result<()> {
let page = self.get_page()?;
self.navigation_manager.navigate(&page, url).await
}
pub async fn get_current_url(&self) -> Result<String> {
let client = self.get_cdp_client()?;
let session_id = self.get_session_id()?;
let params = serde_json::json!({
"expression": "window.location.href",
"returnByValue": true,
});
let result = client
.send_command_with_session("Runtime.evaluate", params, Some(&session_id))
.await?;
result
.get("result")
.and_then(|v| v.get("value"))
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| BrowsingError::Browser("Failed to get current URL".to_string()))
}
pub async fn stop(&mut self) -> Result<()> {
self.tab_manager = TabManager::new();
self.session_manager.stop().await?;
Ok(())
}
pub fn get_cdp_client(&self) -> Result<std::sync::Arc<crate::browser::cdp::CdpClient>> {
if let Some(target_id) = self.tab_manager.current_target_id()
&& let Some(session) = self.tab_manager.get_session(target_id) {
return Ok(Arc::clone(&session.client));
}
self.session_manager.get_cdp_client()
}
pub fn get_session_id(&self) -> Result<String> {
if let Some(target_id) = self.tab_manager.current_target_id()
&& let Some(session) = self.tab_manager.get_session(target_id) {
return Ok(session.session_id.clone());
}
Err(BrowsingError::Browser("No active session".to_string()))
}
pub fn get_page(&self) -> Result<crate::actor::Page> {
let client = self.get_cdp_client()?;
let session_id = self.get_session_id()?;
Ok(crate::actor::Page::new(client, session_id))
}
pub fn get_current_target_id(&self) -> Result<String> {
self.tab_manager
.current_target_id()
.map(|id| id.to_string())
.ok_or_else(|| BrowsingError::Browser("No current target ID".to_string()))
}
pub async fn take_screenshot(
&self,
path: Option<&str>,
full_page: bool,
format: Option<&str>,
quality: Option<u32>,
) -> Result<Vec<u8>> {
let page = self.get_page()?;
self.screenshot_manager
.take_screenshot(&page, path, full_page, format, quality)
.await
}
pub async fn get_tabs(&self) -> Result<Vec<crate::browser::views::TabInfo>> {
let client = self.get_cdp_client()?;
self.tab_manager.get_tabs(&client).await
}
pub async fn create_new_tab(&mut self, url: Option<&str>) -> Result<String> {
let client = self.get_cdp_client()?;
self.tab_manager.create_tab(&client, url).await
}
pub async fn switch_to_tab(&mut self, target_id: &str) -> Result<()> {
let client = self.get_cdp_client()?;
self.tab_manager.switch_to_tab(&client, target_id).await
}
pub async fn close_tab(&mut self, target_id: &str) -> Result<()> {
let client = self.get_cdp_client()?;
self.tab_manager.close_tab(&client, target_id).await
}
pub async fn get_target_id_from_tab_id(&self, tab_id: &str) -> Result<String> {
let tabs = self.get_tabs().await?;
for tab in tabs {
if tab.target_id.ends_with(tab_id) {
return Ok(tab.target_id);
}
}
Err(BrowsingError::Browser(format!(
"No target ID found ending with {tab_id}"
)))
}
pub async fn get_current_page_title(&self) -> Result<String> {
let client = self.get_cdp_client()?;
let target_id = self.get_current_target_id()?;
let params = serde_json::json!({
"targetId": target_id
});
let target_info = client.send_command("Target.getTargetInfo", params).await?;
let title = target_info
.get("targetInfo")
.and_then(|v| v.get("title"))
.and_then(|v| v.as_str())
.unwrap_or("Unknown")
.to_string();
Ok(title)
}
pub async fn get_session_info(&self) -> Result<SessionInfo> {
Ok(SessionInfo {
url: self.get_current_url().await?,
title: self.get_current_page_title().await?,
target_id: self.get_current_target_id()?,
session_id: self.get_session_id()?,
})
}
pub async fn get_browser_state_summary(
&self,
include_screenshot: bool,
dom_service: Option<&crate::dom::DomService>,
) -> Result<crate::browser::views::BrowserStateSummary> {
let url = self
.get_current_url()
.await
.unwrap_or_else(|_| "about:blank".to_string());
let title = self
.get_current_page_title()
.await
.unwrap_or_else(|_| "Unknown".to_string());
let tabs = self.get_tabs().await.unwrap_or_default();
let dom_state = if let Some(dom_service) = dom_service {
let (state, _, _) = dom_service.get_serialized_dom_tree(None).await?;
state
} else {
crate::dom::views::SerializedDOMState {
html: None,
text: Some("No DOM state available".to_string()),
markdown: None,
elements: vec![],
selector_map: std::collections::HashMap::new(),
page_intent: None,
page_intent_confidence: 0.0,
}
};
let screenshot = if include_screenshot {
match self.take_screenshot(None, false, Some("png"), None).await {
Ok(data) => {
use base64::{Engine as _, engine::general_purpose};
Some(general_purpose::STANDARD.encode(&data))
}
Err(_) => None,
}
} else {
None
};
let is_pdf_viewer = url.ends_with(".pdf") || title.contains("PDF");
Ok(crate::browser::views::BrowserStateSummary {
dom_state,
url,
title,
tabs,
screenshot,
page_info: None, pixels_above: 0,
pixels_below: 0,
browser_errors: vec![],
is_pdf_viewer,
recent_events: None,
pending_network_requests: vec![],
pagination_buttons: vec![],
closed_popup_messages: vec![],
})
}
}
#[async_trait]
impl BrowserClient for Browser {
async fn start(&mut self) -> Result<()> {
self.start().await
}
async fn stop(&mut self) -> Result<()> {
self.stop().await
}
async fn navigate(&mut self, url: &str) -> Result<()> {
let page = self.get_page()?;
self.navigation_manager.navigate(&page, url).await
}
async fn get_current_url(&self) -> Result<String> {
self.get_current_url().await
}
async fn create_tab(&mut self, url: Option<&str>) -> Result<String> {
let client = self.get_cdp_client()?;
self.tab_manager.create_tab(&client, url).await
}
async fn switch_to_tab(&mut self, target_id: &str) -> Result<()> {
let client = self.get_cdp_client()?;
self.tab_manager.switch_to_tab(&client, target_id).await
}
async fn close_tab(&mut self, target_id: &str) -> Result<()> {
let client = self.get_cdp_client()?;
self.tab_manager.close_tab(&client, target_id).await
}
async fn get_tabs(&self) -> Result<Vec<crate::browser::views::TabInfo>> {
let client = self.get_cdp_client()?;
self.tab_manager.get_tabs(&client).await
}
async fn get_target_id_from_tab_id(&self, tab_id: &str) -> Result<String> {
self.get_target_id_from_tab_id(tab_id).await
}
fn get_page(&self) -> Result<crate::actor::Page> {
let client = self.get_cdp_client()?;
let session_id = self.get_session_id()?;
Ok(crate::actor::Page::new(client, session_id))
}
async fn take_screenshot(&self, path: Option<&str>, full_page: bool) -> Result<Vec<u8>> {
let page = self.get_page()?;
self.screenshot_manager
.take_screenshot(&page, path, full_page, None, None)
.await
}
async fn get_session_info(&self) -> Result<SessionInfo> {
self.get_session_info().await
}
async fn get_current_page_title(&self) -> Result<String> {
self.get_current_page_title().await
}
fn get_cdp_client(&self) -> Result<Arc<CdpClient>> {
self.get_cdp_client()
}
fn get_session_id(&self) -> Result<String> {
self.get_session_id()
}
fn get_current_target_id(&self) -> Result<String> {
self.get_current_target_id()
}
}