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:
@@ -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]
|
||||
Reference in New Issue
Block a user