Classify YouTube captcha wall as rate-limited, not "not found"

The captcha/bot-challenge error reads:
  Video unavailable. YouTube is requiring a captcha challenge before playback

Because it contains "Video unavailable", the NotFound rule grabbed it
and showed the misleading "removed by the uploader — nothing to
download" hint. But the upload isn't gone; it's a bot-detection wall.

Add captcha / "not a bot" fingerprints to the RateLimited rule, which
runs before NotFound so it wins the "Video unavailable" overlap. Also
broadened the RateLimited hint to mention captchas + the POT provider
as remedies alongside cookies + waiting.

New test pins the captcha line → RateLimited; the genuine
"removed by the uploader" line still maps to NotFound. 85 pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Luna 2026-06-01 08:47:14 -07:00
parent a8f59fe854
commit 71466b21bd

View file

@ -64,7 +64,7 @@ impl ErrorClass {
pub fn hint(self) -> &'static str { pub fn hint(self) -> &'static str {
match self { match self {
ErrorClass::RateLimited => ErrorClass::RateLimited =>
"YouTube is rate-limiting you. Add cookies from a logged-in browser session (Settings → Cookies), or wait 1060 minutes and retry.", "YouTube is rate-limiting you or demanding a captcha (bot detection). Refresh your cookies.txt from a logged-in browser session (Settings → Cookies), enable the POT token provider, and/or wait 1060 minutes before retrying.",
ErrorClass::MembersOnly => ErrorClass::MembersOnly =>
"This video requires a logged-in session with access (members-only, private, or paid). Update cookies.txt from an account that can view it.", "This video requires a logged-in session with access (members-only, private, or paid). Update cookies.txt from an account that can view it.",
ErrorClass::Geoblocked => ErrorClass::Geoblocked =>
@ -102,12 +102,18 @@ where
let l = line.to_ascii_lowercase(); let l = line.to_ascii_lowercase();
// ── Rate limits / bot challenges ───────────────────────────── // ── Rate limits / bot challenges ─────────────────────────────
// Checked first so a bot-challenge phrasing that also contains
// "video unavailable" (e.g. YouTube's captcha wall) is classified
// as rate-limited rather than NotFound below.
if l.contains("http error 429") if l.contains("http error 429")
|| l.contains("too many requests") || l.contains("too many requests")
|| l.contains("sign in to confirm you") || l.contains("sign in to confirm you")
|| l.contains("sign in to confirm your age") || l.contains("sign in to confirm your age")
|| l.contains("ratelimit") || l.contains("ratelimit")
|| l.contains("rate limit") || l.contains("rate limit")
|| l.contains("captcha")
|| l.contains("requiring a captcha challenge")
|| l.contains("not a bot")
{ {
return ErrorClass::RateLimited; return ErrorClass::RateLimited;
} }
@ -229,6 +235,18 @@ mod tests {
); );
} }
#[test]
fn captcha_wall_is_rate_limited_not_not_found() {
// The captcha line also contains "Video unavailable", which the
// NotFound rule would otherwise grab. The RateLimited rule must
// win because the fix is "refresh cookies / wait", not "the
// upload is gone".
assert_eq!(
classify_str("ERROR: [youtube] tfoprUBw0H0: Video unavailable. YouTube is requiring a captcha challenge before playback"),
ErrorClass::RateLimited
);
}
#[test] #[test]
fn detects_geo_block() { fn detects_geo_block() {
assert_eq!( assert_eq!(