diff --git a/backend/core/failure.py b/backend/core/failure.py index c1345a4f..b66a673e 100644 --- a/backend/core/failure.py +++ b/backend/core/failure.py @@ -107,6 +107,9 @@ _HINTS: dict[str, str] = { "SOCKS_PROXY_SUPPORT_MISSING": "A SOCKS proxy is configured in your environment (ALL_PROXY/HTTPS_PROXY=socks5://…) and the backend's HTTP client is missing SOCKS support. Newer VoiceStudio builds ship SOCKS support (the socksio package) — update the app. If you still see this, unset ALL_PROXY/HTTPS_PROXY for VoiceStudio, or run `uv pip install 'httpx[socks]'` in the backend venv, then restart.", "SSL_HANDSHAKE_FAILURE": "A corporate or antivirus proxy is intercepting HTTPS traffic and re-signing certificates with its own CA — your OS trusts that CA, but Python's bundled certifi CA list doesn't, so the TLS handshake fails even though the connection reached the server. Newer VoiceStudio builds trust the OS certificate store at startup (the truststore package), which should already fix this — update the app and retry. If you still see this, add an HTTPS-scanning exclusion for VoiceStudio/Python in your antivirus, or ask IT for the proxy's CA bundle and set SSL_CERT_FILE to it, then restart.", "UNSUPPORTED_VIDEO_URL": "This link isn't a directly downloadable video. Paste a direct video page (e.g. a youtube.com/watch?v=… or douyin.com/video/ link), not a share/profile/feed link — or download the file and drop it in directly.", + # #2034: yt-dlp's own advice names CLI flags (--cookies-from-browser) + # that a VoiceStudio user has no way to pass. + "VIDEO_DOWNLOAD_BOT_CHECK": "YouTube refused this download until it can confirm a signed-in person is asking (its “Sign in to confirm you’re not a bot” check), so retrying the same link won’t help. Sign in to YouTube in your browser, export its cookies as a Netscape cookies.txt file (a cookies.txt browser extension does this), and attach it in Dub with “Choose a cookies.txt export” before importing the link again. The desktop app accepts cookie exports; a browser connection needs HTTPS. Or download the video yourself and upload the file.", "VIDEO_DRM_PROTECTED": "The video host only offered VoiceStudio a DRM-protected copy, which can't be downloaded. This is often not a property of the video itself — the host serves a different format set to different clients, and VoiceStudio already retried through every client it has. Try the link again in a minute, or download the video with a browser extension / the host's own download button and drop the file into Dubbing directly.", # #1301: distinct from SSL_HANDSHAKE_FAILURE. The handshake did not fail on # trust — the connection was CUT while TLS was in progress, so the certifi / @@ -569,6 +572,11 @@ def classify(reason: str) -> str: # download path now escalates the client the way it does for a 403. If # every client still says DRM, the video genuinely can't be fetched and the # user needs to hear that rather than retry a fourth time. + # #2034: YouTube's anti-automation wall. Not transient and not a + # player-client format set, so it must reach neither the network retry + # nor the 403 client escalation; the remedy is signed-in cookies. + if "not a bot" in low and ("sign in" in low or "cookies" in low): + return "VIDEO_DOWNLOAD_BOT_CHECK" if "drm protected" in low or "drm-protected" in low: return "VIDEO_DRM_PROTECTED" if ( diff --git a/backend/worker/errors.py b/backend/worker/errors.py index 17081568..ce204f83 100644 --- a/backend/worker/errors.py +++ b/backend/worker/errors.py @@ -94,6 +94,7 @@ _TAXONOMY: dict[str, ErrorClass] = { "PYANNOTE_LICENSE_REQUIRED": ErrorClass.TERMINAL, "UNSUPPORTED_VIDEO_URL": ErrorClass.TERMINAL, "VIDEO_DRM_PROTECTED": ErrorClass.TERMINAL, + "VIDEO_DOWNLOAD_BOT_CHECK": ErrorClass.TERMINAL, } # Protocol-level codes raised by the worker layer itself (no docs taxonomy). diff --git a/tests/test_yt_bot_check_2034.py b/tests/test_yt_bot_check_2034.py new file mode 100644 index 00000000..42de57da --- /dev/null +++ b/tests/test_yt_bot_check_2034.py @@ -0,0 +1,40 @@ +"""#2034 — YouTube's "Sign in to confirm you're not a bot" wall. + +The reporter got yt-dlp's raw advice ("Use --cookies-from-browser or --cookies"), +which names CLI flags nobody using VoiceStudio can pass. The app's own answer is +the Dub tab's cookies.txt import, so the failure gets its own class and hint, +and it is never retried as a network blip or escalated like a 403. +""" +from core.failure import _HINTS, classify +from services.dub_pipeline import _is_forbidden_download_error, _is_transient_download_error + +REPORTED = ( + "download: ERROR: [youtube] TJAfLE39ZZ8: Sign in to confirm you\u2019re not a bot. " + "Use --cookies-from-browser or --cookies for the authentication. See " + "https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp for how " + "to manually pass cookies. Also see " + "https://github.com/yt-dlp/yt-dlp/wiki/Extractors#exporting-youtube-cookies for tips " + "on effectively exporting YouTube cookies" +) + + +def test_the_reported_message_is_the_bot_check_class(): + assert classify(REPORTED) == "VIDEO_DOWNLOAD_BOT_CHECK" + # yt-dlp has shipped both apostrophes. + assert classify(REPORTED.replace("\u2019", "'")) == "VIDEO_DOWNLOAD_BOT_CHECK" + + +def test_the_hint_points_at_the_apps_own_cookie_import(): + hint = _HINTS["VIDEO_DOWNLOAD_BOT_CHECK"] + assert "cookies.txt" in hint and "Dub" in hint + assert "--cookies" not in hint + + +def test_it_is_neither_retried_nor_escalated(): + exc = RuntimeError(REPORTED) + assert not _is_transient_download_error(exc) + assert not _is_forbidden_download_error(exc) + + +def test_a_plain_network_drop_is_still_the_network_class(): + assert classify("Unable to download video: Connection reset by peer") == "VIDEO_DOWNLOAD_NETWORK"