use crate::error::FetchError;
use crate::fetch::{FetchRequest, FetchResponse, Fetcher, ReqwestFetcher};
use crate::source::BookSource;
use async_trait::async_trait;
use chromiumoxide::cdp::browser_protocol::network::Cookie;
use chromiumoxide::cdp::browser_protocol::page::BringToFrontParams;
use chromiumoxide::{Browser, BrowserConfig, Page};
use futures_util::StreamExt;
use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use tokio::sync::Mutex;
static SOLVE_FAILED: AtomicBool = AtomicBool::new(false);
static RENDER_FAILED: AtomicBool = AtomicBool::new(false);
static BROWSER_LOCK: Mutex<()> = Mutex::const_new(());
const RENDER_RETRY: u32 = 1;
#[derive(Debug, Clone)]
pub struct Clearance {
pub cookie_header: String,
pub user_agent: String,
}
#[derive(Debug, Clone)]
pub struct BrowserCookie {
pub domain: String,
pub name: String,
pub value: String,
}
#[derive(Debug, Clone, Default)]
pub struct LoginOutcome {
pub cookies: Vec<BrowserCookie>,
pub local_storage: BTreeMap<String, String>,
pub html: String,
pub url: String,
}
impl LoginOutcome {
pub fn cookies_by_registrable_domain(&self) -> BTreeMap<String, String> {
use crate::fetch::cookie::{pairs_to_str, registrable_domain};
let mut by: BTreeMap<String, BTreeMap<String, String>> = BTreeMap::new();
for c in &self.cookies {
let dom = registrable_domain(c.domain.trim_start_matches('.'));
by.entry(dom)
.or_default()
.insert(c.name.clone(), c.value.clone());
}
by.into_iter()
.map(|(d, kv)| (d, pairs_to_str(&kv)))
.collect()
}
}
#[derive(Debug, Clone, Default)]
pub struct LoginCriteria {
pub cookie_names: Vec<String>,
pub local_storage_keys: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct LoginSignal {
pub done: Arc<AtomicBool>,
pub cancel: Arc<AtomicBool>,
}
impl LoginSignal {
pub fn reset(&self) {
self.done.store(false, Ordering::Relaxed);
self.cancel.store(false, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AuthDecision {
Once,
Always,
Deny,
}
#[async_trait]
pub trait BrowserUi: Send + Sync {
async fn authorize(&self, source_name: &str) -> AuthDecision;
fn prompt_click(&self, url: &str, cancel: Arc<AtomicBool>);
fn notice(&self, _message: &str) {}
fn done(&self);
}
#[derive(Clone)]
pub struct BrowserOptions {
pub profile_dir: PathBuf,
pub grace: Duration,
pub total_timeout: Duration,
pub login_timeout: Duration,
pub poll_interval: Duration,
pub ui: Option<Arc<dyn BrowserUi>>,
}
impl Default for BrowserOptions {
fn default() -> Self {
Self {
profile_dir: default_profile_dir(),
grace: Duration::from_secs(5),
total_timeout: Duration::from_secs(60),
login_timeout: Duration::from_secs(300),
poll_interval: Duration::from_millis(800),
ui: None,
}
}
}
fn default_profile_dir() -> PathBuf {
match std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE")) {
Some(home) => PathBuf::from(home).join(".novel").join("browser-profile"),
None => std::env::temp_dir().join("trnovel-browser-profile"),
}
}
pub fn detect_browser() -> Option<PathBuf> {
detect_browser_impl()
}
#[cfg(target_os = "macos")]
fn detect_browser_impl() -> Option<PathBuf> {
const CANDIDATES: &[&str] = &[
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
"/Applications/Chromium.app/Contents/MacOS/Chromium",
"/Applications/Vivaldi.app/Contents/MacOS/Vivaldi",
];
CANDIDATES.iter().map(PathBuf::from).find(|p| p.is_file())
}
#[cfg(target_os = "windows")]
fn detect_browser_impl() -> Option<PathBuf> {
const REL: &[&str] = &[
r"Google\Chrome\Application\chrome.exe",
r"Microsoft\Edge\Application\msedge.exe",
r"BraveSoftware\Brave-Browser\Application\brave.exe",
r"Chromium\Application\chrome.exe",
];
for var in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
let Some(root) = std::env::var_os(var).map(PathBuf::from) else {
continue;
};
for rel in REL {
let p = root.join(rel);
if p.is_file() {
return Some(p);
}
}
}
None
}
#[cfg(target_os = "linux")]
fn detect_browser_impl() -> Option<PathBuf> {
const NAMES: &[&str] = &[
"google-chrome",
"google-chrome-stable",
"chromium",
"chromium-browser",
"microsoft-edge",
"brave-browser",
];
let paths = std::env::var_os("PATH")?;
for dir in std::env::split_paths(&paths) {
for name in NAMES {
let p = dir.join(name);
if p.is_file() {
return Some(p);
}
}
}
None
}
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
fn detect_browser_impl() -> Option<PathBuf> {
None
}
mod escalating;
mod fetcher;
pub use escalating::EscalatingFetcher;
pub use fetcher::{BrowserFetcher, shutdown_render_pool};