Fix "captcha challenge before playback" errors: drop player_client=web

Symptom: mid-batch (e.g. item 34 of 40) channel scans failed with
  ERROR: [youtube] <id>: 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 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-01 05:32:27 -07:00
parent 6cefc0c0e5
commit 1e95102ec8

View file

@ -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 <target>` | 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.