use crate::Page;
use anyhow::Result;
use async_trait::async_trait;
use super::{parse_detection_result, CaptchaInfo, Detector};
pub struct AudioCaptchaDetector;
impl AudioCaptchaDetector {
pub const JS: &'static str = r#"(function() {
const vendor = document.querySelector(
'audio[src*="captcha"], .audio-captcha, #audio-captcha, #audio-player'
);
if (vendor) {
return { kind: 'audio', site_key: null, container: '.audio-captcha, #audio-captcha, #audio-player' };
}
// Generic: any audio + a nearby input with a matching prompt.
const audio = document.querySelector('audio');
if (audio) {
const txt = (document.body.innerText || '').toLowerCase();
const promptHit = txt.includes('listen and type')
|| txt.includes('type the numbers')
|| txt.includes('type the digits')
|| txt.includes('audio captcha')
|| txt.includes('audio challenge');
const input = document.querySelector('input[type="text"], input[name="captcha"], input[name="answer"]');
if (promptHit && input) {
return { kind: 'audio', site_key: null, container: 'audio' };
}
}
return { kind: 'none', site_key: null, container: null };
})()"#;
}
#[async_trait]
impl Detector for AudioCaptchaDetector {
fn name(&self) -> &'static str {
"audio_captcha"
}
fn priority(&self) -> i32 {
50
}
async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
let raw = page.evaluate(Self::JS).await?;
let val = raw.into_value::<serde_json::Value>()?;
Ok(parse_detection_result(val))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_captcha_detector_js_has_selectors() {
assert!(AudioCaptchaDetector::JS.contains("audio[src*=\"captcha\"]"));
assert!(AudioCaptchaDetector::JS.contains(".audio-captcha"));
}
#[test]
fn audio_captcha_detector_js_has_generic_heuristic() {
let js = AudioCaptchaDetector::JS;
assert!(js.contains("listen and type"));
assert!(js.contains("type the numbers"));
}
#[test]
fn audio_captcha_detector_priority_below_vendor_specific() {
assert_eq!(AudioCaptchaDetector.priority(), 50);
}
}