use crate::cache::{load_source_state, save_source_state};
use anyhow::anyhow;
use parse_book_source::cookie::{merge_cookie_str, registrable_domain};
use parse_book_source::{
BookSource, BrowserFetcher, BrowserOptions, Fetcher, LoginCriteria, LoginSignal, ReqwestFetcher,
};
use std::collections::BTreeMap;
use std::sync::Arc;
pub fn is_script_login(source: &BookSource) -> bool {
source.get_login_js().is_some()
}
pub fn is_logged_in(source_url: &str) -> bool {
let state = load_source_state(source_url);
!state.cookies.is_empty() || !state.login_header.is_empty()
}
pub fn login_info_json(fields: &[(String, String)]) -> String {
let map: BTreeMap<&str, &str> = fields
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect();
serde_json::to_string(&map).unwrap_or_else(|_| "{}".into())
}
pub async fn script_login(mut source: BookSource, login_info: Option<String>) -> crate::Result<()> {
let login_js = source
.get_login_js()
.ok_or_else(|| anyhow!("该书源 loginUrl 不是登录脚本(@js:/<js>),应走浏览器登录"))?
.to_string();
if source.http.timeout.is_none() {
source.http.timeout = Some(30_000);
}
let url = source.url.clone();
let mut state = load_source_state(&url);
if let Some(plain) = login_info {
state
.set_login_info(&plain)
.map_err(|e| anyhow!("凭据加密失败: {e}"))?;
}
let fetcher: Arc<dyn Fetcher> =
Arc::new(ReqwestFetcher::new(&source).map_err(|e| anyhow!("构建取页器失败: {e}"))?);
let url2 = url.clone();
let (new_state, _dirty) = tokio::task::spawn_blocking(move || {
parse_book_source::host::run_login(&login_js, &url2, state, fetcher)
})
.await
.map_err(|e| anyhow!("登录任务异常: {e}"))?
.map_err(|e| anyhow!("登录脚本执行失败: {e}"))?;
save_source_state(&url, &new_state)?;
Ok(())
}
pub async fn browser_login(source: BookSource, signal: LoginSignal) -> crate::Result<()> {
let target = source.login_url.trim();
if target.is_empty() {
return Err(anyhow!("该书源未配置 loginUrl,无法浏览器登录").into());
}
let browser = BrowserFetcher::detect(BrowserOptions::default())
.ok_or_else(|| anyhow!("未探测到系统浏览器(Chrome/Edge/Brave/…),无法浏览器登录"))?;
signal.reset();
let criteria = LoginCriteria::default();
let outcome = browser
.login(target, &criteria, &signal)
.await
.map_err(|e| anyhow!("浏览器登录失败/取消: {e}"))?;
let url = source.url.clone();
let mut state = load_source_state(&url);
let allowed = [
registrable_domain(target),
registrable_domain(&url),
registrable_domain(&outcome.url),
];
for (dom, c) in outcome.cookies_by_registrable_domain() {
if !allowed.contains(&dom) {
continue;
}
let merged = merge_cookie_str(
state.cookies.get(&dom).map(String::as_str).unwrap_or(""),
&c,
);
state.cookies.insert(dom, merged);
}
if let Some(tok) = ["access_token", "token", "authorization", "jwt"]
.iter()
.find_map(|k| outcome.local_storage.get(*k))
.filter(|t| !t.is_empty())
{
let bearer = if tok.starts_with("Bearer ") {
tok.clone()
} else {
format!("Bearer {tok}")
};
state.login_header.insert("Authorization".into(), bearer);
}
save_source_state(&url, &state)?;
Ok(())
}