fix(downloads): a shared or feed-opened Facebook group post downloads its own video
A Facebook share link (/share/v/…) redirects to a group post, but only the permalink form went through the post-page read, so yt-dlp followed the share link itself and downloaded an ad from the signed-in group feed (prod job 471). The link of a post opened from the group's feed (/groups/<g>/?multi_permalinks=<id>) was not recognised at all, and yt-dlp answers it with "Unsupported URL" (prod job 472). With a cookie, a share link is now followed in the same request that reads the post page, and a feed link is read as the post it names. Without one, the feed link goes to yt-dlp in the permalink form it knows. A post page's "no single video" verdict now names the post for a share link too.
This commit is contained in:
@@ -12,7 +12,7 @@ Resolution works both before extraction (from source_kind / URL host) and after
|
||||
"""
|
||||
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from yt_dlp.networking.impersonate import ImpersonateTarget
|
||||
|
||||
@@ -183,10 +183,23 @@ class FacebookAdapter(SourceAdapter):
|
||||
# in the page scripts that carry the post id; its watch link downloads (plain first, then the
|
||||
# Chrome retry above). Needs the cookie: signed out, the page is the login wall.
|
||||
post_id = facebook_group_post_id(url)
|
||||
if post_id is None or not ydl_opts.get("cookiefile"):
|
||||
if not ydl_opts.get("cookiefile"):
|
||||
# yt-dlp knows the permalink form and answers the feed's form with "Unsupported URL".
|
||||
if post_id is not None and not _FB_GROUP_POST_PATH.match(urlparse(url).path):
|
||||
return _group_permalink(url, post_id)
|
||||
return url
|
||||
# A share link is a post only once followed: handed to yt-dlp, it was followed to the group
|
||||
# page and an ad from the signed-in feed came down (prod job 471). One request does both — the
|
||||
# redirect lands on the post page, which is then read as any post link's.
|
||||
if post_id is None and not _is_share_link(url):
|
||||
return url
|
||||
with egress.open_ydl(ydl_opts) as ydl:
|
||||
raw = ydl.urlopen(url).read(self._POST_PAGE_MAX_BYTES)
|
||||
response = ydl.urlopen(url)
|
||||
if post_id is None:
|
||||
post_id = facebook_group_post_id(response.url)
|
||||
if post_id is None:
|
||||
return url # a shared reel or video: yt-dlp follows the link as it always has
|
||||
raw = response.read(self._POST_PAGE_MAX_BYTES)
|
||||
page = raw.decode("utf-8", "replace")
|
||||
# Measured: a refused or expired cookie gets the login page (~450 KB, a login
|
||||
# form, no post id at all), which is not a post without a video — the user's cookie is the cause.
|
||||
@@ -246,6 +259,7 @@ class FacebookAdapter(SourceAdapter):
|
||||
|
||||
|
||||
_FB_GROUP_POST_PATH = re.compile(r"^/groups/[^/]+/(?:permalink|posts)/(\d+)/?$")
|
||||
_FB_GROUP_PATH = re.compile(r"^/groups/[^/]+/?$")
|
||||
_SCRIPT_BLOCK = re.compile(r"<script[^>]*>(.*?)</script>", re.S)
|
||||
_VIDEO_NODE = re.compile(r'"__typename":"Video","id":"(\d+)"')
|
||||
|
||||
@@ -256,12 +270,37 @@ def _quoted(post_id: str) -> str:
|
||||
|
||||
|
||||
def facebook_group_post_id(url: str) -> str | None:
|
||||
"""The post id of a Facebook group post link (`/groups/<group>/permalink|posts/<id>/`), else None."""
|
||||
"""The post id of a Facebook group post link, else None: `/groups/<group>/permalink|posts/<id>/`,
|
||||
or a post opened from the group's feed, `/groups/<group>/?multi_permalinks=<id>` (one id only —
|
||||
with several, which one was meant is not ours to guess)."""
|
||||
parsed = urlparse(url)
|
||||
if not _host_is(parsed.netloc.lower(), "facebook.com"):
|
||||
return None
|
||||
match = _FB_GROUP_POST_PATH.match(parsed.path)
|
||||
return match.group(1) if match else None
|
||||
if match:
|
||||
return match.group(1)
|
||||
if not _FB_GROUP_PATH.match(parsed.path):
|
||||
return None
|
||||
ids = parse_qs(parsed.query).get("multi_permalinks", [])
|
||||
return ids[0] if len(ids) == 1 and ids[0].isdigit() else None
|
||||
|
||||
|
||||
def _group_permalink(url: str, post_id: str) -> str:
|
||||
"""The permalink form of a group post link — the one form yt-dlp itself accepts."""
|
||||
group = urlparse(url).path.split("/")[2]
|
||||
return f"https://www.facebook.com/groups/{group}/permalink/{post_id}/"
|
||||
|
||||
|
||||
def facebook_post_link(url: str) -> bool:
|
||||
"""A link that `resolve_url` may read as a group post: a post link, or a share link (which may
|
||||
lead to one) — what the failure catalog accepts for the post page's own verdicts."""
|
||||
return facebook_group_post_id(url) is not None or _is_share_link(url)
|
||||
|
||||
|
||||
def _is_share_link(url: str) -> bool:
|
||||
# facebook.com/share/<code>/ and /share/v|r|p/<code>/ — a redirect to the shared object.
|
||||
parsed = urlparse(url)
|
||||
return _host_is(parsed.netloc.lower(), "facebook.com") and parsed.path.startswith("/share/")
|
||||
|
||||
|
||||
def facebook_post_video_id(page: str, post_id: str) -> str | None:
|
||||
|
||||
@@ -32,6 +32,7 @@ from app.downloads.adapters import (
|
||||
POST_NOT_SHOWN_MESSAGE,
|
||||
POST_WITHOUT_VIDEO_MESSAGE,
|
||||
facebook_group_post_id,
|
||||
facebook_post_link,
|
||||
)
|
||||
from app.downloads.credentials import host_key_for_url
|
||||
|
||||
@@ -227,13 +228,14 @@ _CATALOG: tuple[_Entry, ...] = (
|
||||
# answers "No video formats found" (measured on prod jobs 66 and 145, whose videos are reshared
|
||||
# clips). With a cookie yt-dlp only ever gets the post's VIDEO link, so the same text there is about
|
||||
# that video, not the post — the category says it. Only for group post links (the adapter's own
|
||||
# test of one): a reel with no formats is a different failure.
|
||||
# test of one): a reel with no formats is a different failure. The adapter's own text also comes
|
||||
# from a share link that led to a post, so its test takes those too.
|
||||
_Entry(
|
||||
host="facebook",
|
||||
markers=(POST_WITHOUT_VIDEO_MESSAGE.lower(),),
|
||||
code="format_unavailable",
|
||||
detail="facebook.group_post_video",
|
||||
link=facebook_group_post_id,
|
||||
link=facebook_post_link,
|
||||
),
|
||||
_Entry(
|
||||
host="facebook",
|
||||
|
||||
Reference in New Issue
Block a user