use crate::browser::cdp::{CdpClient, CdpSession};
use crate::browser::navigation::NavigationManager;
use crate::browser::profile::BrowserProfile;
use crate::browser::screenshot::ScreenshotManager;
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 {
profile: BrowserProfile,
cdp_client: Option<Arc<CdpClient>>,
cdp_url: Option<String>,
tab_manager: TabManager,
navigation_manager: NavigationManager,
screenshot_manager: ScreenshotManager,
launcher: Option<crate::browser::launcher::BrowserLauncher>,
}
impl Browser {
pub fn new(profile: BrowserProfile) -> Self {
Self {
profile,
cdp_client: None,
cdp_url: None,
tab_manager: TabManager::new(),
navigation_manager: NavigationManager::new(),
screenshot_manager: ScreenshotManager::new(),
launcher: None,
}
}
pub fn with_cdp_url(mut self, cdp_url: String) -> Self {
self.cdp_url = Some(cdp_url);
self
}
pub async fn start(&mut self) -> Result<()> {
let cdp_url = if let Some(ref cdp_url) = self.cdp_url {
cdp_url.clone()
} else {
use crate::browser::launcher::BrowserLauncher;
let mut launcher = BrowserLauncher::new(self.profile.clone());
let cdp_url = launcher.launch().await?;
self.launcher = Some(launcher);
self.cdp_url = Some(cdp_url.clone());
cdp_url
};
let mut client = CdpClient::new(cdp_url);
client.start().await?;
let client_arc = Arc::new(client);
self.cdp_client = Some(Arc::clone(&client_arc));
let targets = client_arc
.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 {
if let Some(target_id) = target["targetId"].as_str() {
let session = CdpSession::for_target(
Arc::clone(&client_arc),
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();
if let Some(client) = self.cdp_client.take() {
client.close().await;
drop(client); }
tokio::task::yield_now().await;
if let Some(ref mut launcher) = self.launcher {
launcher.stop().await?;
}
self.launcher = None;
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() {
if let Some(session) = self.tab_manager.get_session(target_id) {
return Ok(Arc::clone(&session.client));
}
}
Err(BrowsingError::Browser("No active session".to_string()))
}
pub fn get_session_id(&self) -> Result<String> {
if let Some(target_id) = self.tab_manager.current_target_id() {
if 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(),
}
};
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()
}
}