use super::*;
pub struct AudioCaptchaSolver {
client: reqwest::Client,
pub(crate) stt_endpoint: String,
pub(crate) config: SolveConfig,
}
impl Default for AudioCaptchaSolver {
fn default() -> Self {
Self::new()
}
}
impl AudioCaptchaSolver {
pub fn new() -> Self {
let config = SolveConfig::default();
Self {
client: reqwest::Client::builder()
.timeout(Duration::from_millis(config.client_http_timeout_ms))
.build()
.unwrap_or_else(|_| reqwest::Client::new()),
stt_endpoint: "http://localhost:9000/asr".to_string(),
config,
}
}
pub fn with_stt_endpoint(mut self, url: impl Into<String>) -> Self {
self.stt_endpoint = url.into();
self
}
pub fn with_config(mut self, config: SolveConfig) -> Self {
self.config = config;
self
}
}
#[async_trait]
impl CaptchaSolver for AudioCaptchaSolver {
fn name(&self) -> &'static str {
"AudioCaptchaSolver"
}
fn method(&self) -> SolveMethod {
SolveMethod::AudioBypass
}
fn supports(&self, kind: &crate::captcha_detect::DetectedCaptcha) -> bool {
use crate::captcha_detect::DetectedCaptcha;
matches!(
kind,
DetectedCaptcha::RecaptchaV2 | DetectedCaptcha::AudioCaptcha
)
}
async fn solve(
&self,
page: &Page,
_captcha_info: &crate::captcha_detect::CaptchaInfo,
) -> Result<CaptchaSolveResult> {
let t0 = Instant::now();
let audio_btn = page
.find_element("#recaptcha-audio-button, .rc-button-audio")
.await?;
audio_btn.click().await?;
tokio::time::sleep(Duration::from_millis(self.config.audio_button_delay_ms)).await;
let audio_src = page
.evaluate(r#"document.querySelector('audio').getAttribute('src')"#)
.await?
.into_value::<String>()?;
if audio_src.is_empty() {
return Ok(CaptchaSolveResult::failure(
SolveMethod::AudioBypass,
t0.elapsed().as_millis() as u64,
));
}
let audio_bytes = self.client.get(&audio_src).send().await?.bytes().await?;
let stt_resp = self
.client
.post(&self.stt_endpoint)
.header("Content-Type", "audio/mpeg")
.body(audio_bytes)
.send()
.await?;
if !stt_resp.status().is_success() {
return Ok(CaptchaSolveResult::failure(
SolveMethod::AudioBypass,
t0.elapsed().as_millis() as u64,
));
}
let transcript = stt_resp.text().await?.trim().to_string();
if transcript.is_empty() {
return Ok(CaptchaSolveResult::failure(
SolveMethod::AudioBypass,
t0.elapsed().as_millis() as u64,
));
}
let input = page
.find_element("#audio-response, .rc-response-input")
.await?;
input.click().await?;
input.type_str(&transcript).await?;
tokio::time::sleep(Duration::from_millis(self.config.audio_submit_delay_ms)).await;
let verify = page
.find_element("#recaptcha-verify-button, .rc-button-default")
.await?;
verify.click().await?;
let has_token = page
.evaluate("!!document.querySelector('#g-recaptcha-response')?.value")
.await?
.into_value::<bool>()
.unwrap_or(false);
let cookies = if has_token {
crate::cookies::capture_from_page(page)
.await
.unwrap_or_default()
} else {
Vec::new()
};
Ok(CaptchaSolveResult {
solution: transcript.clone(),
confidence: if has_token { 0.9 } else { 0.5 },
method: SolveMethod::AudioBypass,
time_ms: t0.elapsed().as_millis() as u64,
success: has_token,
screenshot: None,
cookies,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_solver_custom_endpoint() {
let s = AudioCaptchaSolver::new().with_stt_endpoint("http://custom:9999/stt");
assert_eq!(s.stt_endpoint, "http://custom:9999/stt");
}
}