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.
93 lines
3.3 KiB
Python
93 lines
3.3 KiB
Python
"""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]
|