From ab2df904e0d0341b3f0d799ab77019b9fbf83983 Mon Sep 17 00:00:00 2001 From: kikuyan Date: Sun, 15 Aug 2021 14:14:30 +0900 Subject: [PATCH 1/4] [extractor/facebook] Improve metadata extraction * add another data structure for video extraction * modify metadata extraction due to site change --- youtube_dl/extractor/facebook.py | 147 ++++++++++++++++++++++--------- 1 file changed, 106 insertions(+), 41 deletions(-) diff --git a/youtube_dl/extractor/facebook.py b/youtube_dl/extractor/facebook.py index 04650af39..786a95849 100644 --- a/youtube_dl/extractor/facebook.py +++ b/youtube_dl/extractor/facebook.py @@ -23,6 +23,7 @@ from ..utils import ( int_or_none, js_to_json, limit_length, + merge_dicts, parse_count, qualities, sanitized_Request, @@ -82,8 +83,9 @@ class FacebookIE(InfoExtractor): 'info_dict': { 'id': '274175099429670', 'ext': 'mp4', - 'title': 're:^Asif Nawab Butt posted a video', + 'title': 'Asif Nawab Butt', 'uploader': 'Asif Nawab Butt', + 'uploader_id': '100005115464446', 'upload_date': '20140506', 'timestamp': 1399398998, 'thumbnail': r're:^https?://.*', @@ -137,18 +139,20 @@ class FacebookIE(InfoExtractor): 'upload_date': '20160223', 'uploader': 'Barack Obama', }, + 'skip': 'Gif on giphy.com gone', }, { # have 1080P, but only up to 720p in swf params # data.video.story.attachments[].media 'url': 'https://www.facebook.com/cnn/videos/10155529876156509/', - 'md5': '9571fae53d4165bbbadb17a94651dcdc', 'info_dict': { 'id': '10155529876156509', 'ext': 'mp4', - 'title': 'She survived the holocaust — and years later, she’s getting her citizenship s...', + 'title': 'Holocaust survivor becomes US citizen', + 'description': 'She survived the holocaust — and years later, she’s getting her citizenship so she can vote for Hillary Clinton http://cnn.it/2eERh5f', 'timestamp': 1477818095, 'upload_date': '20161030', 'uploader': 'CNN', + 'uploader_id': '5550296508', 'thumbnail': r're:^https?://.*', 'view_count': int, }, @@ -159,10 +163,12 @@ class FacebookIE(InfoExtractor): 'info_dict': { 'id': '1417995061575415', 'ext': 'mp4', - 'title': 'md5:1db063d6a8c13faa8da727817339c857', + 'title': 'Ukrainian Scientists Worldwide | Довгоочікуване відео', + 'description': 'Довгоочікуване відео', 'timestamp': 1486648217, 'upload_date': '20170209', 'uploader': 'Yaroslav Korpan', + 'uploader_id': '100000948048708', }, 'params': { 'skip_download': True, @@ -181,16 +187,20 @@ class FacebookIE(InfoExtractor): 'params': { 'skip_download': True, }, + # TODO: parse this webpage + 'skip': 'Cannot parse webpage well', }, { # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media 'url': 'https://www.facebook.com/groups/1024490957622648/permalink/1396382447100162/', 'info_dict': { - 'id': '1396382447100162', + 'id': '202882990186699', 'ext': 'mp4', - 'title': 'md5:19a428bbde91364e3de815383b54a235', + 'title': 'birb (O v O\") | Hello? Yes your uber ride is here', + 'description': 'md5:963dee8a667a2b49f2059cf7ab54fe55', 'timestamp': 1486035494, 'upload_date': '20170202', 'uploader': 'Elisabeth Ahtn', + 'uploader_id': '100013949973717', }, 'params': { 'skip_download': True, @@ -231,8 +241,12 @@ class FacebookIE(InfoExtractor): 'info_dict': { 'id': '359649331226507', 'ext': 'mp4', - 'title': '#ESLOne VoD - Birmingham Finals Day#1 Fnatic vs. @Evil Geniuses', + 'title': 'Fnatic vs. EG - Group A - Opening Match - ESL One Birmingham Day 1', + 'description': '#ESLOne VoD - Birmingham Finals Day#1 Fnatic vs. @Evil Geniuses', + 'timestamp': 1527084179, + 'upload_date': '20180523', 'uploader': 'ESL One Dota 2', + 'uploader_id': '234218833769558', }, 'params': { 'skip_download': True, @@ -255,6 +269,7 @@ class FacebookIE(InfoExtractor): 'id': '10157667649866271', }, 'playlist_count': 3, + 'skip': 'Requires logging in', }, { # data.nodes[].comet_sections.content.story.attachments[].style_type_renderer.attachment.media 'url': 'https://m.facebook.com/Alliance.Police.Department/posts/4048563708499330', @@ -279,6 +294,7 @@ class FacebookIE(InfoExtractor): 'upload_date': '20161122', 'timestamp': 1479793574, }, + 'skip': 'No video', }, { # data.video.creation_story.attachments[].media 'url': 'https://www.facebook.com/watch/live/?v=1823658634322275', @@ -290,6 +306,18 @@ class FacebookIE(InfoExtractor): }, 'playlist_count': 1, 'skip': 'Requires logging in', + }, { + # data.node.comet_sections.content.story.attachments[].styles.attachment.media + 'url': 'https://www.facebook.com/groups/352666925484718/permalink/1112505706167499/', + 'info_dict': { + 'id': '1080405282779948', + 'ext': 'mp4', + 'title': 'Best Chinese Song', + 'timestamp': 1641225430, + 'upload_date': '20220103', + 'uploader_id': '1847991063', + 'uploader': 'Best Chinese Song', + }, }] _SUPPORTED_PAGLETS_REGEX = r'(?:pagelet_group_mall|permalink_video_pagelet|hyperfeed_story_id_[0-9a-f]+)' _api_config = { @@ -381,6 +409,63 @@ class FacebookIE(InfoExtractor): webpage = self._download_webpage( url.replace('://m.facebook.com/', '://www.facebook.com/'), video_id) + def extract_metadata(webpage): + video_title = self._html_search_regex( + r']*class="uiHeaderTitle"[^>]*>([^<]*)', webpage, + 'title', default=None) + if not video_title: + video_title = self._html_search_regex( + r'(?s)(.*?)', + webpage, 'alternative title', default=None) + if not video_title: + video_title = self._html_search_meta( + ['og:title', 'twitter:title', 'description'], + webpage, 'title', default=None) + if video_title: + video_title = limit_length(video_title, 80) + else: + video_title = 'Facebook video #%s' % video_id + description = self._html_search_meta( + ['description', 'og:description', 'twitter:description'], + webpage, 'description', default=None) + uploader = clean_html(get_element_by_id( + 'fbPhotoPageAuthorName', webpage)) or self._search_regex( + r'ownerName\s*:\s*"([^"]+)"', webpage, 'uploader', + default=None) or self._og_search_title(webpage, default=None) + timestamp = int_or_none(self._search_regex( + r']+data-utime=["\'](\d+)', webpage, + 'timestamp', default=None)) + thumbnail = self._html_search_meta( + ['og:image', 'twitter:image'], webpage, 'thumbnail', default=None) + # some webpages contain unretrievable thumbnail urls + # like https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=10155168902769113&get_thumbnail=1 + # in https://www.facebook.com/yaroslav.korpan/videos/1417995061575415/ + if thumbnail and not re.search(r'\.(?:jpg|png)', thumbnail): + thumbnail = None + view_count = parse_count(self._search_regex( + [r'\bviewCount\s*:\s*["\']([\d,.]+)', r'"video_view_count"\s*:\s*([\d,.]+)'], + webpage, 'view count', default=None)) + info_dict = { + 'title': video_title, + 'description': description, + 'uploader': uploader, + 'timestamp': timestamp, + 'thumbnail': thumbnail, + 'view_count': view_count, + } + info_json_ld = self._search_json_ld(webpage, video_id, default={}) + if not info_json_ld: + # expecting title, description, uploader, comment_count and like_count + info_json_ld = self._search_json_ld( + webpage, video_id, expected_type='SocialMediaPosting', default={}) + # if we already have a non-generic title, honor that + if info_dict['title'] != 'Facebook video #%s' % video_id: + info_json_ld['title'] = info_dict['title'] + if info_json_ld.get('title'): + info_json_ld['title'] = limit_length( + re.sub(r'\s*\|\s*Facebook$', '', info_json_ld['title']), 80) + return merge_dicts(info_json_ld, info_dict) + video_data = None def extract_video_data(instances): @@ -492,7 +577,10 @@ class FacebookIE(InfoExtractor): lambda x: x['attachments'] ], list) or [] for attachment in attachments: - attachment = try_get(attachment, lambda x: x['style_type_renderer']['attachment'], dict) + attachment = try_get(attachment, [ + lambda x: x['style_type_renderer']['attachment'], + lambda x: x['styles']['attachment'] + ], dict) or {} ns = try_get(attachment, lambda x: x['all_subattachments']['nodes'], list) or [] for n in ns: parse_attachment(n) @@ -513,7 +601,15 @@ class FacebookIE(InfoExtractor): if not entries: parse_graphql_video(video) - return self.playlist_result(entries, video_id) + if len(entries) > 1: + return self.playlist_result(entries, video_id) + + video_info = entries[0] + webpage_info = extract_metadata(webpage) + # honor precise duration in video info + if video_info.get('duration'): + webpage_info['duration'] = video_info['duration'] + return merge_dicts(webpage_info, video_info) if not video_data: m_msg = re.search(r'class="[^"]*uiInterstitialContent[^"]*">
(.*?)
', webpage) @@ -633,43 +729,12 @@ class FacebookIE(InfoExtractor): process_formats(formats) - video_title = self._html_search_regex( - r']*class="uiHeaderTitle"[^>]*>([^<]*)', webpage, - 'title', default=None) - if not video_title: - video_title = self._html_search_regex( - r'(?s)(.*?)', - webpage, 'alternative title', default=None) - if not video_title: - video_title = self._html_search_meta( - 'description', webpage, 'title', default=None) - if video_title: - video_title = limit_length(video_title, 80) - else: - video_title = 'Facebook video #%s' % video_id - uploader = clean_html(get_element_by_id( - 'fbPhotoPageAuthorName', webpage)) or self._search_regex( - r'ownerName\s*:\s*"([^"]+)"', webpage, 'uploader', - default=None) or self._og_search_title(webpage, fatal=False) - timestamp = int_or_none(self._search_regex( - r']+data-utime=["\'](\d+)', webpage, - 'timestamp', default=None)) - thumbnail = self._html_search_meta(['og:image', 'twitter:image'], webpage) - - view_count = parse_count(self._search_regex( - r'\bviewCount\s*:\s*["\']([\d,.]+)', webpage, 'view count', - default=None)) - info_dict = { 'id': video_id, - 'title': video_title, 'formats': formats, - 'uploader': uploader, - 'timestamp': timestamp, - 'thumbnail': thumbnail, - 'view_count': view_count, 'subtitles': subtitles, } + info_dict.update(extract_metadata(webpage)) return info_dict From 078f26831711ef9b73aba6cadf6947187cc479a5 Mon Sep 17 00:00:00 2001 From: dirkf Date: Sun, 27 Feb 2022 13:31:42 +0000 Subject: [PATCH 2/4] [common] Extract author from SocialMediaPosting in ld+json --- youtube_dl/extractor/common.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/youtube_dl/extractor/common.py b/youtube_dl/extractor/common.py index 797c35fd5..7ea45d4a7 100644 --- a/youtube_dl/extractor/common.py +++ b/youtube_dl/extractor/common.py @@ -1273,9 +1273,21 @@ class InfoExtractor(object): continue info[count_key] = interaction_count + def extract_author(e): + author = e.get('author') + if not author: + return + info.update({ + # author can be an instance of 'Organization' or 'Person' types. + # both types can have 'name' property(inherited from 'Thing' type). [1] + # however some websites are using 'Text' type instead. + # 1. https://schema.org/VideoObject + 'uploader': author.get('name') if isinstance(author, dict) else author if isinstance(author, compat_str) else None, + }) + def extract_video_object(e): assert e['@type'] == 'VideoObject' - author = e.get('author') + extract_author(e) info.update({ 'url': url_or_none(e.get('contentUrl')), 'title': unescapeHTML(e.get('name')), @@ -1283,11 +1295,6 @@ class InfoExtractor(object): 'thumbnail': url_or_none(e.get('thumbnailUrl') or e.get('thumbnailURL')), 'duration': parse_duration(e.get('duration')), 'timestamp': unified_timestamp(e.get('uploadDate')), - # author can be an instance of 'Organization' or 'Person' types. - # both types can have 'name' property(inherited from 'Thing' type). [1] - # however some websites are using 'Text' type instead. - # 1. https://schema.org/VideoObject - 'uploader': author.get('name') if isinstance(author, dict) else author if isinstance(author, compat_str) else None, 'filesize': float_or_none(e.get('contentSize')), 'tbr': int_or_none(e.get('bitrate')), 'width': int_or_none(e.get('width')), @@ -1332,6 +1339,8 @@ class InfoExtractor(object): 'title': unescapeHTML(e.get('headline')), 'description': unescapeHTML(e.get('articleBody')), }) + elif item_type == 'SocialMediaPosting': + extract_author(e) elif item_type == 'VideoObject': extract_video_object(e) if expected_type is None: From ecd14c832de1663a2b4d06827ba584be30167bda Mon Sep 17 00:00:00 2001 From: dirkf Date: Sun, 27 Feb 2022 13:33:28 +0000 Subject: [PATCH 3/4] [extractor/facebook] Update extraction improvements and fix tests * avoid crashing in parse_attachment() on invalid attachment * ignore empty results in search --- youtube_dl/extractor/facebook.py | 40 +++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/youtube_dl/extractor/facebook.py b/youtube_dl/extractor/facebook.py index 786a95849..a3f1bd325 100644 --- a/youtube_dl/extractor/facebook.py +++ b/youtube_dl/extractor/facebook.py @@ -83,7 +83,7 @@ class FacebookIE(InfoExtractor): 'info_dict': { 'id': '274175099429670', 'ext': 'mp4', - 'title': 'Asif Nawab Butt', + 'title': 'Asif', 'uploader': 'Asif Nawab Butt', 'uploader_id': '100005115464446', 'upload_date': '20140506', @@ -173,6 +173,7 @@ class FacebookIE(InfoExtractor): 'params': { 'skip_download': True, }, + 'skip': 'This video is only available for registered users', }, { 'url': 'https://www.facebook.com/LaGuiaDelVaron/posts/1072691702860471', 'info_dict': { @@ -188,15 +189,16 @@ class FacebookIE(InfoExtractor): 'skip_download': True, }, # TODO: parse this webpage - 'skip': 'Cannot parse webpage well', + 'skip': 'This video is only available for registered users (and cannot parse webpage well)', }, { # data.node.comet_sections.content.story.attachments[].style_type_renderer.attachment.media 'url': 'https://www.facebook.com/groups/1024490957622648/permalink/1396382447100162/', 'info_dict': { 'id': '202882990186699', 'ext': 'mp4', + 'title': 'birb (O v O\") | Hello? Yes your uber ride is here', - 'description': 'md5:963dee8a667a2b49f2059cf7ab54fe55', + 'description': 'md5:f122681cf504d04be12bb2bdc66c81fd', 'timestamp': 1486035494, 'upload_date': '20170202', 'uploader': 'Elisabeth Ahtn', @@ -308,15 +310,17 @@ class FacebookIE(InfoExtractor): 'skip': 'Requires logging in', }, { # data.node.comet_sections.content.story.attachments[].styles.attachment.media - 'url': 'https://www.facebook.com/groups/352666925484718/permalink/1112505706167499/', + # gone : 'https://www.facebook.com/groups/352666925484718/permalink/1112505706167499/', + 'url': 'https://www.facebook.com/groups/471738070722101/permalink/471799617382613/', 'info_dict': { - 'id': '1080405282779948', + 'id': '101173872166725', 'ext': 'mp4', - 'title': 'Best Chinese Song', - 'timestamp': 1641225430, - 'upload_date': '20220103', - 'uploader_id': '1847991063', - 'uploader': 'Best Chinese Song', + 'title': 'md5:0ac174e60ed9d9a5eec588343f6a889c', + 'description': 'md5:80efdff1371fa5d92758df2fb1b97bfd', + 'upload_date': '20210517', + 'timestamp': 1621247381, + 'uploader': 'md5:34165f897c692f92b13c2cc1eddd8245', + 'uploader_id': '100957655521680', }, }] _SUPPORTED_PAGLETS_REGEX = r'(?:pagelet_group_mall|permalink_video_pagelet|hyperfeed_story_id_[0-9a-f]+)' @@ -417,15 +421,23 @@ class FacebookIE(InfoExtractor): video_title = self._html_search_regex( r'(?s)(.*?)', webpage, 'alternative title', default=None) + + # _html_search_meta() finds '' ahead of later non-empty results + def html_search_meta_non_empty(patterns, html, name, **kwargs): + for p in patterns: + result = self._html_search_meta(p, html, name, **kwargs) + if result: + return result + if not video_title: - video_title = self._html_search_meta( + video_title = html_search_meta_non_empty( ['og:title', 'twitter:title', 'description'], webpage, 'title', default=None) if video_title: video_title = limit_length(video_title, 80) else: video_title = 'Facebook video #%s' % video_id - description = self._html_search_meta( + description = html_search_meta_non_empty( ['description', 'og:description', 'twitter:description'], webpage, 'description', default=None) uploader = clean_html(get_element_by_id( @@ -435,7 +447,7 @@ class FacebookIE(InfoExtractor): timestamp = int_or_none(self._search_regex( r']+data-utime=["\'](\d+)', webpage, 'timestamp', default=None)) - thumbnail = self._html_search_meta( + thumbnail = html_search_meta_non_empty( ['og:image', 'twitter:image'], webpage, 'thumbnail', default=None) # some webpages contain unretrievable thumbnail urls # like https://lookaside.fbsbx.com/lookaside/crawler/media/?media_id=10155168902769113&get_thumbnail=1 @@ -562,7 +574,7 @@ class FacebookIE(InfoExtractor): entries.append(info) def parse_attachment(attachment, key='media'): - media = attachment.get(key) or {} + media = try_get(attachment, lambda x: x[key], dict) or {} if media.get('__typename') == 'Video': return parse_graphql_video(media) From c555e5e478b3d9bc5861bcbd4a9cfe5d61958758 Mon Sep 17 00:00:00 2001 From: dirkf Date: Tue, 19 Apr 2022 12:57:22 +0100 Subject: [PATCH 4/4] [Facebook] Check for redirection to URL with `login`, or not a permalink, or not including the ID --- youtube_dl/extractor/facebook.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/youtube_dl/extractor/facebook.py b/youtube_dl/extractor/facebook.py index a3f1bd325..389d1f7bf 100644 --- a/youtube_dl/extractor/facebook.py +++ b/youtube_dl/extractor/facebook.py @@ -410,8 +410,15 @@ class FacebookIE(InfoExtractor): self._login() def _extract_from_url(self, url, video_id): - webpage = self._download_webpage( + webpage, urlh = self._download_webpage_handle( url.replace('://m.facebook.com/', '://www.facebook.com/'), video_id) + login_or_id_check = re.search(r'(?P\blogin\b)|/permalink/|[/=?]' + video_id, urlh.geturl()) + if login_or_id_check and login_or_id_check.group('login'): + self.raise_login_required() + elif not login_or_id_check: + raise ExtractorError( + 'Facebook redirected to a generic page (%s): the video may be unavailable or you may need to log in.' % urlh.geturl(), + expected=True) def extract_metadata(webpage): video_title = self._html_search_regex(