use std::sync::Arc;
use crate::browser::ContextOverride;
use crate::launcher::{Geolocation, OsType};
use super::rotate::{RotateStrategy, Rotator};
#[derive(Debug, Clone, Default)]
pub struct FingerprintProfile {
pub user_agent: Option<String>,
pub locale: Option<String>,
pub timezone_id: Option<String>,
pub platform: Option<String>,
pub os: Option<OsType>,
pub geolocation: Option<Geolocation>,
pub window_size: Option<(u32, u32)>,
}
impl FingerprintProfile {
pub fn new() -> Self {
Self::default()
}
pub fn user_agent(mut self, ua: impl Into<String>) -> Self {
self.user_agent = Some(ua.into());
self
}
pub fn locale(mut self, locale: impl Into<String>) -> Self {
self.locale = Some(locale.into());
self
}
pub fn timezone(mut self, tz: impl Into<String>) -> Self {
self.timezone_id = Some(tz.into());
self
}
pub fn platform(mut self, platform: impl Into<String>) -> Self {
self.platform = Some(platform.into());
self
}
pub fn os(mut self, os: OsType) -> Self {
self.os = Some(os);
self
}
pub fn geolocation(mut self, latitude: f64, longitude: f64) -> Self {
self.geolocation = Some(Geolocation {
latitude,
longitude,
accuracy: None,
});
self
}
pub fn window_size(mut self, width: u32, height: u32) -> Self {
self.window_size = Some((width, height));
self
}
pub fn apply_to(&self, mut ov: ContextOverride) -> ContextOverride {
if let Some(ua) = &self.user_agent {
ov.user_agent = Some(ua.clone());
}
if let Some(l) = &self.locale {
ov.locale = Some(l.clone());
}
if let Some(tz) = &self.timezone_id {
ov.timezone_id = Some(tz.clone());
}
if let Some(p) = &self.platform {
ov.platform = Some(p.clone());
}
if let Some(os) = self.os {
ov.os = Some(os);
}
if let Some(g) = self.geolocation {
ov.geolocation = Some(g);
}
if let Some(ws) = self.window_size {
ov.window_size = Some(ws);
}
ov
}
}
#[derive(Clone)]
pub struct FingerprintPool {
profiles: Arc<Vec<FingerprintProfile>>,
rotator: Arc<Rotator>,
}
impl FingerprintPool {
pub fn new(profiles: Vec<FingerprintProfile>) -> Self {
Self::with_strategy(profiles, RotateStrategy::RoundRobin)
}
pub fn with_strategy(profiles: Vec<FingerprintProfile>, strategy: RotateStrategy) -> Self {
Self {
profiles: Arc::new(profiles),
rotator: Arc::new(Rotator::new(strategy)),
}
}
pub fn presets() -> Self {
Self::new(vec![
FingerprintProfile::new()
.locale("zh-CN")
.timezone("Asia/Shanghai")
.window_size(1920, 1080),
FingerprintProfile::new()
.locale("en-US")
.timezone("America/New_York")
.window_size(1536, 864),
FingerprintProfile::new()
.locale("en-GB")
.timezone("Europe/London")
.window_size(1366, 768),
FingerprintProfile::new()
.locale("ja-JP")
.timezone("Asia/Tokyo")
.window_size(1440, 900),
FingerprintProfile::new()
.locale("de-DE")
.timezone("Europe/Berlin")
.window_size(1920, 1080),
])
}
pub fn strategy(self, strategy: RotateStrategy) -> Self {
Self {
profiles: self.profiles,
rotator: Arc::new(Rotator::new(strategy)),
}
}
pub fn len(&self) -> usize {
self.profiles.len()
}
pub fn is_empty(&self) -> bool {
self.profiles.is_empty()
}
#[allow(clippy::should_implement_trait)]
pub fn next(&self) -> Option<FingerprintProfile> {
self.rotator
.pick(self.profiles.len(), None)
.map(|i| self.profiles[i].clone())
}
pub fn for_key(&self, key: &str) -> Option<FingerprintProfile> {
self.rotator
.pick(self.profiles.len(), Some(key))
.map(|i| self.profiles[i].clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn apply_to_sets_fields_and_keeps_proxy() {
use crate::launcher::Proxy;
let ov = ContextOverride::new().proxy(Proxy::new("http://x:1"));
let prof = FingerprintProfile::new()
.locale("zh-CN")
.timezone("Asia/Shanghai")
.window_size(1280, 800);
let merged = prof.apply_to(ov);
assert_eq!(merged.locale.as_deref(), Some("zh-CN"));
assert_eq!(merged.timezone_id.as_deref(), Some("Asia/Shanghai"));
assert_eq!(merged.window_size, Some((1280, 800)));
assert!(merged.proxy.is_some(), "应保留已设的代理");
}
#[test]
fn presets_rotate_locales() {
let p = FingerprintPool::presets();
assert_eq!(p.len(), 5);
let first = p.next().unwrap().locale;
let second = p.next().unwrap().locale;
assert_ne!(first, second, "轮询应取到不同画像");
}
#[test]
fn empty_pool_returns_none() {
let p = FingerprintPool::new(vec![]);
assert!(p.is_empty());
assert!(p.next().is_none());
}
}