From 1e95102ec824522aec97f7a38c952ee9bbc8df98 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 1 Jun 2026 05:32:27 -0700 Subject: [PATCH] Fix "captcha challenge before playback" errors: drop player_client=web MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: mid-batch (e.g. item 34 of 40) channel scans failed with ERROR: [youtube] : Video unavailable. YouTube is requiring a captcha challenge before playback Root cause: we hardcoded `--extractor-args youtube:player_client=web` on every download. That flag dates from when `web` was the throttle-avoidance client, but YouTube has since made `web` the MOST aggressively bot-checked client — it's exactly the one that demands a captcha. Verified directly: forcing player_client=web throws the captcha error on a specific video that resolves fine without it. Fix: - Remove player_client=web from all three download builders (main, repair, music). Modern yt-dlp auto-selects better clients (android_vr, tv, web_safari) that aren't captcha-walled, and our POT provider already serves tokens for them (web_safari in the logs). - Add a randomized 2-6 s --sleep-interval between videos. The jitter (vs a fixed cadence) is what keeps a long channel scan from looking robotic and tripping the wall around request ~30. Verified: both video IDs from the bug report (4JJ434QQoE4, Gr4bTC7DZWA) that captcha'd under player_client=web now resolve cleanly. Co-Authored-By: Claude Opus 4.7 --- src/downloader.rs | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/downloader.rs b/src/downloader.rs index 2c6595b..c335fd5 100644 --- a/src/downloader.rs +++ b/src/downloader.rs @@ -18,7 +18,6 @@ //! | `--embed-metadata --embed-info-json --embed-chapters` | Embed rich metadata into the MKV | //! | `--xattrs` | Store metadata in filesystem extended attributes | //! | `--sponsorblock-mark all` | Mark (but don't remove) SponsorBlock segments | -//! | `--extractor-args youtube:player_client=web` | Use the web player API to avoid throttling | //! | `--impersonate ` | Browser TLS fingerprint per source platform (see [`crate::platform::Platform::impersonate_target`]) | //! | `--break-on-existing` | Stop when the archive file records the video as already downloaded | //! | `--download-archive archive.txt` | Record downloaded IDs to avoid re-downloading | @@ -268,14 +267,23 @@ impl Downloader { /// /// YouTube occasionally resets connections mid-transfer; with default /// settings yt-dlp gives up after 10 quick retries. We bump the retry - /// count and add a linear backoff so transient resets self-heal. The - /// sleep flags throttle per-IP request rate slightly so we don't trip - /// YouTube's rate limiter when many channels are being checked at once. + /// count and add a linear backoff so transient resets self-heal. + /// + /// The sleep flags throttle request rate so we don't trip YouTube's + /// bot-detection / captcha wall mid-batch (it tends to fire after + /// ~30 rapid requests): + /// - `--sleep-requests 1`: 1 s between metadata/API requests. + /// - `--sleep-interval` / `--max-sleep-interval`: a random 2-6 s + /// pause *between videos*. The jitter is what matters — a fixed + /// cadence looks robotic; a random one looks human and is far less + /// likely to trip the captcha on a long channel scan. fn apply_retry_flags(cmd: &mut Command) { cmd.arg("--retries").arg("30") .arg("--fragment-retries").arg("30") .arg("--retry-sleep").arg("linear=1:30:2") - .arg("--sleep-requests").arg("1"); + .arg("--sleep-requests").arg("1") + .arg("--sleep-interval").arg("2") + .arg("--max-sleep-interval").arg("6"); } /// Build a fresh `Command` invoking the currently configured yt-dlp binary. @@ -464,8 +472,6 @@ impl Downloader { .arg("--xattrs") .arg("--sponsorblock-mark") .arg("all") - .arg("--extractor-args") - .arg("youtube:player_client=web") .arg("--progress"); if let Some(fmt) = quality.format_spec() { cmd.arg("-f").arg(fmt); @@ -518,9 +524,7 @@ impl Downloader { .arg("--write-info-json") .arg("--write-description") .arg("--write-subs") - .arg("--write-auto-subs") - .arg("--extractor-args") - .arg("youtube:player_client=web"); + .arg("--write-auto-subs"); // `repair()` rebuilds a YouTube watch URL from a stored video ID, so // the source platform is always YouTube here regardless of where the // original video lives on disk. @@ -559,9 +563,7 @@ impl Downloader { .arg("--write-thumbnail") .arg("--write-info-json") .arg("--embed-metadata") - .arg("--xattrs") - .arg("--extractor-args") - .arg("youtube:player_client=web"); + .arg("--xattrs"); // Music downloads can come from any audio-first platform — classify // the URL once so SoundCloud/Bandcamp pulls get their appropriate // (typically no-op) impersonation profile.