use crate::http::PerfilBrowser;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ResultadoBusca {
pub posicao: u32,
pub titulo: String,
pub url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub url_exibicao: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub titulo_original: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub conteudo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tamanho_conteudo: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metodo_extracao_conteudo: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MetadadosBusca {
pub tempo_execucao_ms: u64,
pub hash_seletores: String,
pub retentativas: u32,
pub usou_endpoint_fallback: bool,
pub fetches_simultaneos: u32,
pub sucessos_fetch: u32,
pub falhas_fetch: u32,
pub usou_chrome: bool,
pub user_agent: String,
pub usou_proxy: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaidaBusca {
pub query: String,
pub motor: String,
pub endpoint: String,
pub timestamp: String,
pub regiao: String,
pub quantidade_resultados: u32,
pub resultados: Vec<ResultadoBusca>,
pub paginas_buscadas: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub erro: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mensagem: Option<String>,
pub metadados: MetadadosBusca,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SaidaBuscaMultipla {
pub quantidade_queries: u32,
pub timestamp: String,
pub paralelismo: u32,
pub buscas: Vec<SaidaBusca>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ConfiguracaoSeletores {
pub html_endpoint: SeletoresHtml,
#[serde(default)]
pub lite_endpoint: SeletoresLite,
#[serde(default)]
pub pagination: SeletoresPaginacao,
#[serde(default)]
pub related_searches: SeletoresRelacionadas,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeletoresHtml {
pub results_container: String,
pub result_item: String,
pub title_and_url: String,
pub snippet: String,
pub display_url: String,
pub ads_filter: FiltroAnuncios,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct FiltroAnuncios {
pub ad_classes: Vec<String>,
pub ad_attributes: Vec<String>,
pub ad_url_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeletoresLite {
pub results_table: String,
pub result_link: String,
pub result_snippet: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeletoresPaginacao {
pub vqd_input: String,
pub s_input: String,
pub dc_input: String,
pub next_form: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SeletoresRelacionadas {
pub container: String,
pub links: String,
}
impl Default for SeletoresHtml {
fn default() -> Self {
Self {
results_container: "#links".to_string(),
result_item:
"#links .result:not(.result--ad), #links .results_links, div.result:not(.result--ad)"
.to_string(),
title_and_url: ".result__a, a.result__a, .result__title a".to_string(),
snippet: ".result__snippet, a.result__snippet".to_string(),
display_url: ".result__url, span.result__url".to_string(),
ads_filter: FiltroAnuncios::default(),
}
}
}
impl Default for FiltroAnuncios {
fn default() -> Self {
Self {
ad_classes: vec![".result--ad".to_string(), ".badge--ad".to_string()],
ad_attributes: vec!["data-nrn=ad".to_string()],
ad_url_patterns: vec!["duckduckgo.com/y.js".to_string()],
}
}
}
impl Default for SeletoresLite {
fn default() -> Self {
Self {
results_table: "table, body table".to_string(),
result_link: "a.result-link, td a[href]".to_string(),
result_snippet: "td.result-snippet, tr.result-snippet td".to_string(),
}
}
}
impl Default for SeletoresPaginacao {
fn default() -> Self {
Self {
vqd_input: "input[name='vqd'], input[type='hidden'][name='vqd']".to_string(),
s_input: "input[name='s']".to_string(),
dc_input: "input[name='dc']".to_string(),
next_form: "form.result--more__btn, form[action='/html/']".to_string(),
}
}
}
impl Default for SeletoresRelacionadas {
fn default() -> Self {
Self {
container: ".result--more__btn, .result--sep".to_string(),
links: "a".to_string(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Endpoint {
Html,
Lite,
}
impl Endpoint {
pub fn como_str(&self) -> &'static str {
match self {
Endpoint::Html => "html",
Endpoint::Lite => "lite",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FiltroTemporal {
Dia,
Semana,
Mes,
Ano,
}
impl FiltroTemporal {
pub fn como_parametro(&self) -> &'static str {
match self {
FiltroTemporal::Dia => "d",
FiltroTemporal::Semana => "w",
FiltroTemporal::Mes => "m",
FiltroTemporal::Ano => "y",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SafeSearch {
Off,
Moderate,
Strict,
}
impl SafeSearch {
pub fn como_parametro(&self) -> Option<&'static str> {
match self {
SafeSearch::Off => Some("-1"),
SafeSearch::Moderate => None,
SafeSearch::Strict => Some("1"),
}
}
}
#[derive(Debug, Clone)]
pub struct Configuracoes {
pub query: String,
pub queries: Vec<String>,
pub num_resultados: Option<u32>,
pub formato: FormatoSaida,
pub timeout_segundos: u64,
pub idioma: String,
pub pais: String,
pub modo_verboso: bool,
pub modo_silencioso: bool,
pub user_agent: String,
pub perfil_browser: PerfilBrowser,
pub paralelismo: u32,
pub paginas: u32,
pub retries: u32,
pub endpoint: Endpoint,
pub filtro_temporal: Option<FiltroTemporal>,
pub safe_search: SafeSearch,
pub modo_stream: bool,
pub arquivo_saida: Option<std::path::PathBuf>,
pub buscar_conteudo: bool,
pub max_tamanho_conteudo: usize,
pub proxy: Option<String>,
pub sem_proxy: bool,
pub timeout_global_segundos: u64,
pub corresponde_plataforma_ua: bool,
pub limite_por_host: usize,
pub caminho_chrome: Option<std::path::PathBuf>,
pub seletores: std::sync::Arc<ConfiguracaoSeletores>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormatoSaida {
Json,
Text,
Markdown,
Auto,
}
impl FormatoSaida {
pub fn a_partir_de_str(valor: &str) -> Option<Self> {
match valor.to_ascii_lowercase().as_str() {
"json" => Some(Self::Json),
"text" => Some(Self::Text),
"markdown" | "md" => Some(Self::Markdown),
"auto" => Some(Self::Auto),
_ => None,
}
}
}
#[cfg(test)]
mod testes {
use super::*;
#[test]
fn configuracao_seletores_default_contem_result_container() {
let cfg = ConfiguracaoSeletores::default();
assert_eq!(cfg.html_endpoint.results_container, "#links");
assert!(cfg
.html_endpoint
.ads_filter
.ad_url_patterns
.contains(&"duckduckgo.com/y.js".to_string()));
}
#[test]
fn formato_saida_parseia_variantes_validas() {
assert_eq!(
FormatoSaida::a_partir_de_str("json"),
Some(FormatoSaida::Json)
);
assert_eq!(
FormatoSaida::a_partir_de_str("TEXT"),
Some(FormatoSaida::Text)
);
assert_eq!(
FormatoSaida::a_partir_de_str("markdown"),
Some(FormatoSaida::Markdown)
);
assert_eq!(
FormatoSaida::a_partir_de_str("md"),
Some(FormatoSaida::Markdown)
);
assert_eq!(
FormatoSaida::a_partir_de_str("Auto"),
Some(FormatoSaida::Auto)
);
assert_eq!(FormatoSaida::a_partir_de_str("xml"), None);
}
#[test]
fn saida_busca_serializa_campos_em_portugues_no_json() {
let saida = SaidaBusca {
query: "teste".to_string(),
motor: "duckduckgo".to_string(),
endpoint: "html".to_string(),
timestamp: "2026-04-14T00:00:00Z".to_string(),
regiao: "br-pt".to_string(),
quantidade_resultados: 0,
resultados: vec![],
paginas_buscadas: 1,
erro: None,
mensagem: None,
metadados: MetadadosBusca {
tempo_execucao_ms: 0,
hash_seletores: "abc123".to_string(),
retentativas: 0,
usou_endpoint_fallback: false,
fetches_simultaneos: 0,
sucessos_fetch: 0,
falhas_fetch: 0,
usou_chrome: false,
user_agent: "Mozilla/5.0".to_string(),
usou_proxy: false,
},
};
let json = serde_json::to_string(&saida).expect("serialização deve funcionar");
assert!(json.contains("\"query\""));
assert!(json.contains("\"quantidade_resultados\""));
assert!(json.contains("\"tempo_execucao_ms\""));
assert!(json.contains("\"resultados\""));
assert!(json.contains("\"metadados\""));
assert!(!json.contains("\"buscas_relacionadas\""));
assert!(!json.contains("\"results_count\""));
assert!(!json.contains("\"results\":"));
assert!(!json.contains("\"metadata\""));
assert!(!json.contains("\"related_searches\""));
}
#[test]
fn saida_busca_multipla_serializa_campos_em_portugues() {
let saida = SaidaBuscaMultipla {
quantidade_queries: 2,
timestamp: "2026-04-14T00:00:00Z".to_string(),
paralelismo: 5,
buscas: vec![],
};
let json = serde_json::to_string(&saida).expect("serialização deve funcionar");
assert!(json.contains("\"quantidade_queries\":2"));
assert!(json.contains("\"paralelismo\":5"));
assert!(json.contains("\"buscas\":[]"));
assert!(!json.contains("\"queries_count\""));
assert!(!json.contains("\"parallel\""));
assert!(!json.contains("\"searches\""));
}
}