feat(downloads): a Facebook cookie download stuck at "Cannot parse data" is retried as Chrome

Measured on dev: with the user's cookie some reels still answer "Cannot parse
data" and only extract when the request impersonates Chrome. Impersonation is
a second attempt behind that one error, only when a cookie is attached — links
that already download with the cookie keep their plain path, and without a
cookie the same error is the login wall.

curl_cffi is pinned inside yt-dlp's supported range (0.10-0.15; outside it
every target silently reports "unavailable"); the lock only gains the new
packages. A test fails the gate if the locked pair can no longer impersonate.
This commit is contained in:
2026-09-13 22:48:21 +02:00
parent 98ec1ffcab
commit 1354f5b503
6 changed files with 141 additions and 7 deletions
+10 -1
View File
@@ -103,12 +103,21 @@ def test_generic_source_url_is_passthrough_youtube_reconstructs():
)
def test_client_sets_and_retriable_are_youtube_only():
def test_client_sets_and_retriable_are_per_source():
# Generic sources make a single attempt and never retry on the YouTube per-client markers.
assert SourceAdapter().client_sets() == [None]
assert SourceAdapter().client_sets(authed=True) == [None]
assert SourceAdapter().retriable("Sign in to confirm you're not a bot") is False
assert YouTubeAdapter().retriable("Sign in to confirm you're not a bot") is True
assert YouTubeAdapter().retriable("some unrelated network blip") is False
# Facebook: a second, Chrome-impersonating attempt only WITH the user's cookie, and only behind
# the parse wall (without a cookie that wall is the login wall — no disguise gets past it).
fb = FacebookAdapter()
assert fb.client_sets() == [None]
assert len(fb.client_sets(authed=True)) == 2
assert fb.ydl_opts_extra(fb.client_sets(authed=True)[0]) == {}
assert fb.retriable("ERROR: [facebook] 1: Cannot parse data; please report this issue") is True
assert fb.retriable("ERROR: [facebook] 1: No video formats found!") is False
def test_youtube_ydl_opts_extra_carries_player_client_and_pot():
@@ -0,0 +1,92 @@
"""E11.5 S3 — a Facebook cookie download stuck at "Cannot parse data" is retried as Chrome.
Measured on dev (yt-dlp 2026.07.04, the owner's cookie): reel 806379462330494 fails with the cookie
alone and extracts 2560p with `--impersonate chrome`; the reels that already download with the cookie
keep their plain first attempt. These pin the attempt order through the real download loop
(`pipeline._extract_with_fallback`, with yt-dlp faked at the `YoutubeDL` boundary), and that the
LOCKED curl_cffi can actually impersonate — the pair only works inside yt-dlp's own version range, and
outside it every target silently reports "unavailable".
"""
import pytest
import yt_dlp
from yt_dlp.networking.impersonate import ImpersonateTarget
from app.downloads import adapters, pipeline
_URL = "https://www.facebook.com/reel/806379462330494/?fs=e&fs=e"
_WALL = (
"ERROR: [facebook] 806379462330494: Cannot parse data; please report this issue on "
"https://github.com/yt-dlp/yt-dlp/issues?q= , filling out the appropriate issue template."
)
_NO_FORMATS = "ERROR: [facebook] 2541212983053999: No video formats found!"
def test_the_locked_curl_cffi_can_impersonate_chrome():
# YoutubeDL refuses to start when the target is unavailable — the same check a download makes.
with yt_dlp.YoutubeDL({"impersonate": ImpersonateTarget.from_str("chrome"), "quiet": True}):
pass
@pytest.fixture
def ydl(monkeypatch):
"""Fake `yt_dlp.YoutubeDL`: records each attempt's options and plays back `outcomes` in order."""
seen: list[dict] = []
outcomes: list = []
class FakeYDL:
def __init__(self, opts):
seen.append(opts)
def __enter__(self):
return self
def __exit__(self, *exc):
return False
def extract_info(self, url, download):
outcome = outcomes.pop(0)
if isinstance(outcome, str):
raise yt_dlp.utils.DownloadError(outcome)
return outcome
monkeypatch.setattr(pipeline.yt_dlp, "YoutubeDL", FakeYDL)
monkeypatch.setattr(pipeline, "_touch_job", lambda job_id: None)
monkeypatch.setattr(pipeline, "_set_job", lambda job_id, **fields: None)
return seen, outcomes
def _run(tmp_path, cookiefile):
return pipeline._extract_with_fallback(
1, 1, _URL, {}, tmp_path, adapters.FacebookAdapter(), cookiefile=cookiefile
)
def test_a_cookie_download_at_the_wall_is_retried_as_chrome(ydl, tmp_path):
seen, outcomes = ydl
outcomes += [_WALL, {"id": "806379462330494"}]
assert _run(tmp_path, "/cookies.txt") == {"id": "806379462330494"}
assert [o.get("impersonate") for o in seen] == [None, ImpersonateTarget.from_str("chrome")]
assert all(o["cookiefile"] == "/cookies.txt" for o in seen) # the retry stays signed in
def test_without_a_cookie_the_wall_is_the_login_wall_and_final(ydl, tmp_path):
seen, outcomes = ydl
outcomes += [_WALL]
with pytest.raises(yt_dlp.utils.DownloadError):
_run(tmp_path, None)
assert len(seen) == 1 and "impersonate" not in seen[0]
def test_other_facebook_failures_are_not_retried_as_chrome(ydl, tmp_path):
seen, outcomes = ydl
outcomes += [_NO_FORMATS]
with pytest.raises(yt_dlp.utils.DownloadError):
_run(tmp_path, "/cookies.txt")
assert len(seen) == 1 and "impersonate" not in seen[0]