mirror of
				https://github.com/ytdl-org/youtube-dl.git
				synced 2025-10-29 09:26:20 -07:00 
			
		
		
		
	[YouTube] Bypass age-gating for certain restricted videos
* Use TVHTML5_SIMPLY_EMBEDDED_PLAYER client * Also add and fix tests * Introduce and use new utility function `update_url()`
This commit is contained in:
		| @@ -42,6 +42,7 @@ from ..utils import ( | |||||||
|     unescapeHTML, |     unescapeHTML, | ||||||
|     unified_strdate, |     unified_strdate, | ||||||
|     unsmuggle_url, |     unsmuggle_url, | ||||||
|  |     update_url, | ||||||
|     update_url_query, |     update_url_query, | ||||||
|     url_or_none, |     url_or_none, | ||||||
|     urlencode_postdata, |     urlencode_postdata, | ||||||
| @@ -286,15 +287,18 @@ class YoutubeBaseInfoExtractor(InfoExtractor): | |||||||
|     _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;' |     _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;' | ||||||
|     _YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)' |     _YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)' | ||||||
|  |  | ||||||
|     def _call_api(self, ep, query, video_id, fatal=True): |     def _call_api(self, ep, query, video_id, fatal=True, headers=None): | ||||||
|         data = self._DEFAULT_API_DATA.copy() |         data = self._DEFAULT_API_DATA.copy() | ||||||
|         data.update(query) |         data.update(query) | ||||||
|  |         real_headers = {'content-type': 'application/json'} | ||||||
|  |         if headers: | ||||||
|  |             real_headers.update(headers) | ||||||
|  |  | ||||||
|         return self._download_json( |         return self._download_json( | ||||||
|             'https://www.youtube.com/youtubei/v1/%s' % ep, video_id=video_id, |             'https://www.youtube.com/youtubei/v1/%s' % ep, video_id=video_id, | ||||||
|             note='Downloading API JSON', errnote='Unable to download API page', |             note='Downloading API JSON', errnote='Unable to download API page', | ||||||
|             data=json.dumps(data).encode('utf8'), fatal=fatal, |             data=json.dumps(data).encode('utf8'), fatal=fatal, | ||||||
|             headers={'content-type': 'application/json'}, |             headers=real_headers, | ||||||
|             query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'}) |             query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'}) | ||||||
|  |  | ||||||
|     def _extract_yt_initial_data(self, video_id, webpage): |     def _extract_yt_initial_data(self, video_id, webpage): | ||||||
| @@ -515,6 +519,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                 'uploader': 'Philipp Hagemeister', |                 'uploader': 'Philipp Hagemeister', | ||||||
|                 'uploader_id': 'phihag', |                 'uploader_id': 'phihag', | ||||||
|                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag', |                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag', | ||||||
|  |                 'channel': 'Philipp Hagemeister', | ||||||
|                 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q', |                 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q', | ||||||
|                 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q', |                 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q', | ||||||
|                 'upload_date': '20121002', |                 'upload_date': '20121002', | ||||||
| @@ -524,10 +529,10 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                 'duration': 10, |                 'duration': 10, | ||||||
|                 'view_count': int, |                 'view_count': int, | ||||||
|                 'like_count': int, |                 'like_count': int, | ||||||
|                 'dislike_count': int, |                 'thumbnail': 'https://i.ytimg.com/vi/BaW_jenozKc/maxresdefault.jpg', | ||||||
|                 'start_time': 1, |                 'start_time': 1, | ||||||
|                 'end_time': 9, |                 'end_time': 9, | ||||||
|             } |             }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ', |             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ', | ||||||
| @@ -562,7 +567,6 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                 'duration': 10, |                 'duration': 10, | ||||||
|                 'view_count': int, |                 'view_count': int, | ||||||
|                 'like_count': int, |                 'like_count': int, | ||||||
|                 'dislike_count': int, |  | ||||||
|             }, |             }, | ||||||
|             'params': { |             'params': { | ||||||
|                 'skip_download': True, |                 'skip_download': True, | ||||||
| @@ -621,8 +625,9 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                 'description': 'SUBSCRIBE: http://www.youtube.com/saturninefilms \r\n\r\nEven Obama has taken a stand against freedom on this issue: http://www.huffingtonpost.com/2010/09/09/obama-gma-interview-quran_n_710282.html', |                 'description': 'SUBSCRIBE: http://www.youtube.com/saturninefilms \r\n\r\nEven Obama has taken a stand against freedom on this issue: http://www.huffingtonpost.com/2010/09/09/obama-gma-interview-quran_n_710282.html', | ||||||
|             } |             } | ||||||
|         }, |         }, | ||||||
|         # Normal age-gate video (No vevo, embed allowed), available via embed page |         # Age-gated videos | ||||||
|         { |         { | ||||||
|  |             'note': 'Age-gated video (No vevo, embed allowed)', | ||||||
|             'url': 'https://youtube.com/watch?v=HtVdAasjOgU', |             'url': 'https://youtube.com/watch?v=HtVdAasjOgU', | ||||||
|             'info_dict': { |             'info_dict': { | ||||||
|                 'id': 'HtVdAasjOgU', |                 'id': 'HtVdAasjOgU', | ||||||
| @@ -631,17 +636,97 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}', |                 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}', | ||||||
|                 'duration': 142, |                 'duration': 142, | ||||||
|                 'uploader': 'The Witcher', |                 'uploader': 'The Witcher', | ||||||
|                 'uploader_id': 'WitcherGame', |  | ||||||
|                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame', |  | ||||||
|                 'upload_date': '20140605', |                 'upload_date': '20140605', | ||||||
|  |                 'thumbnail': 'https://i.ytimg.com/vi/HtVdAasjOgU/maxresdefault.jpg', | ||||||
|                 'age_limit': 18, |                 'age_limit': 18, | ||||||
|  |                 'categories': ['Gaming'], | ||||||
|  |                 'tags': 'count:17', | ||||||
|  |                 'channel': 'The Witcher', | ||||||
|  |                 'channel_url': 'https://www.youtube.com/channel/UCzybXLxv08IApdjdN0mJhEg', | ||||||
|  |                 'channel_id': 'UCzybXLxv08IApdjdN0mJhEg', | ||||||
|  |                 'view_count': int, | ||||||
|  |                 'like_count': int, | ||||||
|             }, |             }, | ||||||
|         }, |         }, | ||||||
|         { |         { | ||||||
|             # Age-gated video only available with authentication (unavailable |             'note': 'Age-gated video with embed allowed in public site', | ||||||
|             # via embed page workaround) |             'url': 'https://youtube.com/watch?v=HsUATh_Nc2U', | ||||||
|  |             'info_dict': { | ||||||
|  |                 'id': 'HsUATh_Nc2U', | ||||||
|  |                 'ext': 'mp4', | ||||||
|  |                 'title': 'Godzilla 2 (Official Video)', | ||||||
|  |                 'description': 'md5:bf77e03fcae5529475e500129b05668a', | ||||||
|  |                 'duration': 177, | ||||||
|  |                 'uploader': 'FlyingKitty', | ||||||
|  |                 'upload_date': '20200408', | ||||||
|  |                 'thumbnail': 'https://i.ytimg.com/vi/HsUATh_Nc2U/maxresdefault.jpg', | ||||||
|  |                 'age_limit': 18, | ||||||
|  |                 'categories': ['Entertainment'], | ||||||
|  |                 'tags': ['Flyingkitty', 'godzilla 2'], | ||||||
|  |                 'channel': 'FlyingKitty', | ||||||
|  |                 'channel_url': 'https://www.youtube.com/channel/UCYQT13AtrJC0gsM1far_zJg', | ||||||
|  |                 'channel_id': 'UCYQT13AtrJC0gsM1far_zJg', | ||||||
|  |                 'view_count': int, | ||||||
|  |                 'like_count': int, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             'note': 'Age-gated video embedable only with clientScreen=EMBED', | ||||||
|  |             'url': 'https://youtube.com/watch?v=Tq92D6wQ1mg', | ||||||
|  |             'info_dict': { | ||||||
|  |                 'id': 'Tq92D6wQ1mg', | ||||||
|  |                 'ext': 'mp4', | ||||||
|  |                 'title': '[MMD] Adios - EVERGLOW [+Motion DL]', | ||||||
|  |                 'description': 'md5:17eccca93a786d51bc67646756894066', | ||||||
|  |                 'duration': 106, | ||||||
|  |                 'uploader': 'Projekt Melody', | ||||||
|  |                 'upload_date': '20191227', | ||||||
|  |                 'age_limit': 18, | ||||||
|  |                 'thumbnail': 'https://i.ytimg.com/vi/Tq92D6wQ1mg/sddefault.jpg', | ||||||
|  |                 'tags': ['mmd', 'dance', 'mikumikudance', 'kpop', 'vtuber'], | ||||||
|  |                 'categories': ['Entertainment'], | ||||||
|  |                 'channel': 'Projekt Melody', | ||||||
|  |                 'channel_url': 'https://www.youtube.com/channel/UC1yoRdFoFJaCY-AGfD9W0wQ', | ||||||
|  |                 'channel_id': 'UC1yoRdFoFJaCY-AGfD9W0wQ', | ||||||
|  |                 'view_count': int, | ||||||
|  |                 'like_count': int, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             'note': 'Non-Age-gated non-embeddable video', | ||||||
|  |             'url': 'https://youtube.com/watch?v=MeJVWBSsPAY', | ||||||
|  |             'info_dict': { | ||||||
|  |                 'id': 'MeJVWBSsPAY', | ||||||
|  |                 'ext': 'mp4', | ||||||
|  |                 'title': 'OOMPH! - Such Mich Find Mich (Lyrics)', | ||||||
|  |                 'description': 'Fan Video. Music & Lyrics by OOMPH!.', | ||||||
|  |                 'duration': 210, | ||||||
|  |                 'uploader': 'Herr Lurik', | ||||||
|  |                 'uploader_id': 'st3in234', | ||||||
|  |                 'upload_date': '20130730', | ||||||
|  |                 'uploader_url': 'http://www.youtube.com/user/st3in234', | ||||||
|  |                 'age_limit': 0, | ||||||
|  |                 'thumbnail': 'https://i.ytimg.com/vi/MeJVWBSsPAY/hqdefault.jpg', | ||||||
|  |                 'tags': ['oomph', 'such mich find mich', 'lyrics', 'german industrial', 'musica industrial'], | ||||||
|  |                 'categories': ['Music'], | ||||||
|  |                 'channel': 'Herr Lurik', | ||||||
|  |                 'channel_url': 'https://www.youtube.com/channel/UCdR3RSDPqub28LjZx0v9-aA', | ||||||
|  |                 'channel_id': 'UCdR3RSDPqub28LjZx0v9-aA', | ||||||
|  |                 'artist': 'OOMPH!', | ||||||
|  |                 'view_count': int, | ||||||
|  |                 'like_count': int, | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             'note': 'Non-bypassable age-gated video', | ||||||
|  |             'url': 'https://youtube.com/watch?v=Cr381pDsSsA', | ||||||
|  |             'only_matching': True, | ||||||
|  |         }, | ||||||
|  |         { | ||||||
|  |             'note': 'Age-gated video only available with authentication (not via embed workaround)', | ||||||
|             'url': 'XgnwCQzjau8', |             'url': 'XgnwCQzjau8', | ||||||
|             'only_matching': True, |             'only_matching': True, | ||||||
|  |             'skip': '''This video has been removed for violating YouTube's Community Guidelines''', | ||||||
|         }, |         }, | ||||||
|         # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421) |         # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421) | ||||||
|         # YouTube Red ad is not captured for creator |         # YouTube Red ad is not captured for creator | ||||||
| @@ -670,17 +755,23 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|             'info_dict': { |             'info_dict': { | ||||||
|                 'id': 'lqQg6PlCWgI', |                 'id': 'lqQg6PlCWgI', | ||||||
|                 'ext': 'mp4', |                 'ext': 'mp4', | ||||||
|  |                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games', | ||||||
|  |                 'description': r're:(?s)(?:.+\s)?HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games\s*', | ||||||
|                 'duration': 6085, |                 'duration': 6085, | ||||||
|                 'upload_date': '20150827', |                 'upload_date': '20150827', | ||||||
|                 'uploader_id': 'olympic', |                 'uploader_id': 'olympic', | ||||||
|                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic', |                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic', | ||||||
|                 'description': 'HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games', |                 'uploader': r're:Olympics?', | ||||||
|                 'uploader': 'Olympic', |                 'age_limit': 0, | ||||||
|                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games', |                 'thumbnail': 'https://i.ytimg.com/vi/lqQg6PlCWgI/maxresdefault.jpg', | ||||||
|  |                 'categories': ['Sports'], | ||||||
|  |                 'tags': ['Hockey', '2012-07-31', '31 July 2012', 'Riverbank Arena', 'Session', 'Olympics', 'Olympic Games', 'London 2012', '2012 Summer Olympics', 'Summer Games'], | ||||||
|  |                 'channel': 'Olympics', | ||||||
|  |                 'channel_url': 'https://www.youtube.com/channel/UCTl3QQTvqHFjurroKxexy2Q', | ||||||
|  |                 'channel_id': 'UCTl3QQTvqHFjurroKxexy2Q', | ||||||
|  |                 'view_count': int, | ||||||
|  |                 'like_count': int, | ||||||
|             }, |             }, | ||||||
|             'params': { |  | ||||||
|                 'skip_download': 'requires avconv', |  | ||||||
|             } |  | ||||||
|         }, |         }, | ||||||
|         # Non-square pixels |         # Non-square pixels | ||||||
|         { |         { | ||||||
| @@ -1683,27 +1774,52 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|             player_response = self._call_api( |             player_response = self._call_api( | ||||||
|                 'player', {'videoId': video_id}, video_id) |                 'player', {'videoId': video_id}, video_id) | ||||||
|  |  | ||||||
|         playability_status = player_response.get('playabilityStatus') or {} |         def is_agegated(playability): | ||||||
|         if playability_status.get('reason') == 'Sign in to confirm your age': |             if not isinstance(playability, dict): | ||||||
|             video_info = self._download_webpage( |                 return | ||||||
|                 base_url + 'get_video_info', video_id, |  | ||||||
|                 'Refetching age-gated info webpage', |             if playability.get('desktopLegacyAgeGateReason'): | ||||||
|                 'unable to download video info webpage', query={ |                 return True | ||||||
|                     'video_id': video_id, |  | ||||||
|                     'eurl': 'https://youtube.googleapis.com/v/' + video_id, |             reasons = filter(None, (playability.get(r) for r in ('status', 'reason'))) | ||||||
|                     'html5': 1, |             AGE_GATE_REASONS = ( | ||||||
|                     # See https://github.com/ytdl-org/youtube-dl/issues/29333#issuecomment-864049544 |                 'confirm your age', 'age-restricted', 'inappropriate',  # reason | ||||||
|                     'c': 'TVHTML5', |                 'age_verification_required', 'age_check_required',  # status | ||||||
|                     'cver': '6.20180913', |             ) | ||||||
|                 }, fatal=False) |             return any(expected in reason for expected in AGE_GATE_REASONS for reason in reasons) | ||||||
|             if video_info: |  | ||||||
|                 pr = self._parse_json( |         def get_playability_status(response): | ||||||
|                     try_get( |             return try_get(response, lambda x: x['playabilityStatus'], dict) or {} | ||||||
|                         compat_parse_qs(video_info), |  | ||||||
|                         lambda x: x['player_response'][0], compat_str) or '{}', |         playability_status = get_playability_status(player_response) | ||||||
|                     video_id, fatal=False) |         if (is_agegated(playability_status) | ||||||
|                 if pr and isinstance(pr, dict): |                 and int_or_none(self._downloader.params.get('age_limit'), default=18) >= 18): | ||||||
|                     player_response = pr |  | ||||||
|  |             self.report_age_confirmation() | ||||||
|  |  | ||||||
|  |             # Thanks: https://github.com/yt-dlp/yt-dlp/pull/3233 | ||||||
|  |             pb_context = {'html5Preference': 'HTML5_PREF_WANTS'} | ||||||
|  |             query = { | ||||||
|  |                 'playbackContext': {'contentPlaybackContext': {'html5Preference': 'HTML5_PREF_WANTS'}}, | ||||||
|  |                 'contentCheckOk': True, | ||||||
|  |                 'racyCheckOk': True, | ||||||
|  |                 'context': { | ||||||
|  |                     'client': {'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER', 'clientVersion': '2.0', 'hl': 'en', 'clientScreen': 'EMBED'}, | ||||||
|  |                     'thirdParty': {'embedUrl': 'https://google.com'}, | ||||||
|  |                 }, | ||||||
|  |                 'videoId': video_id, | ||||||
|  |             } | ||||||
|  |             headers = { | ||||||
|  |                 'X-YouTube-Client-Name': '85', | ||||||
|  |                 'X-YouTube-Client-Version': '2.0', | ||||||
|  |                 'Origin': 'https://www.youtube.com' | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             video_info = self._call_api('player', query, video_id, fatal=False, headers=headers) | ||||||
|  |             age_gate_status = get_playability_status(video_info) | ||||||
|  |             if age_gate_status.get('status') == 'OK': | ||||||
|  |                 player_response = video_info | ||||||
|  |                 playability_status = age_gate_status | ||||||
|  |  | ||||||
|         trailer_video_id = try_get( |         trailer_video_id = try_get( | ||||||
|             playability_status, |             playability_status, | ||||||
| @@ -1932,12 +2048,12 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|             for thumbnail in (try_get( |             for thumbnail in (try_get( | ||||||
|                     container, |                     container, | ||||||
|                     lambda x: x['thumbnail']['thumbnails'], list) or []): |                     lambda x: x['thumbnail']['thumbnails'], list) or []): | ||||||
|                 thumbnail_url = thumbnail.get('url') |                 thumbnail_url = url_or_none(thumbnail.get('url')) | ||||||
|                 if not thumbnail_url: |                 if not thumbnail_url: | ||||||
|                     continue |                     continue | ||||||
|                 thumbnails.append({ |                 thumbnails.append({ | ||||||
|                     'height': int_or_none(thumbnail.get('height')), |                     'height': int_or_none(thumbnail.get('height')), | ||||||
|                     'url': thumbnail_url, |                     'url': update_url(thumbnail_url, query=None, fragment=None), | ||||||
|                     'width': int_or_none(thumbnail.get('width')), |                     'width': int_or_none(thumbnail.get('width')), | ||||||
|                 }) |                 }) | ||||||
|             if thumbnails: |             if thumbnails: | ||||||
| @@ -2142,6 +2258,7 @@ class YoutubeIE(YoutubeBaseInfoExtractor): | |||||||
|                     sbr_tooltip = try_get( |                     sbr_tooltip = try_get( | ||||||
|                         vpir, lambda x: x['sentimentBar']['sentimentBarRenderer']['tooltip']) |                         vpir, lambda x: x['sentimentBar']['sentimentBarRenderer']['tooltip']) | ||||||
|                     if sbr_tooltip: |                     if sbr_tooltip: | ||||||
|  |                         # however dislike_count was hidden by YT, as if there could ever be dislikable content on YT | ||||||
|                         like_count, dislike_count = sbr_tooltip.split(' / ') |                         like_count, dislike_count = sbr_tooltip.split(' / ') | ||||||
|                         info.update({ |                         info.update({ | ||||||
|                             'like_count': str_to_int(like_count), |                             'like_count': str_to_int(like_count), | ||||||
| @@ -2411,7 +2528,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor): | |||||||
|             'tags': list, |             'tags': list, | ||||||
|             'view_count': int, |             'view_count': int, | ||||||
|             'like_count': int, |             'like_count': int, | ||||||
|             'dislike_count': int, |  | ||||||
|         }, |         }, | ||||||
|         'params': { |         'params': { | ||||||
|             'skip_download': True, |             'skip_download': True, | ||||||
| @@ -2438,7 +2554,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor): | |||||||
|             'categories': ['News & Politics'], |             'categories': ['News & Politics'], | ||||||
|             'tags': list, |             'tags': list, | ||||||
|             'like_count': int, |             'like_count': int, | ||||||
|             'dislike_count': int, |  | ||||||
|         }, |         }, | ||||||
|         'params': { |         'params': { | ||||||
|             'skip_download': True, |             'skip_download': True, | ||||||
| @@ -2458,7 +2573,6 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor): | |||||||
|             'categories': ['News & Politics'], |             'categories': ['News & Politics'], | ||||||
|             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'], |             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'], | ||||||
|             'like_count': int, |             'like_count': int, | ||||||
|             'dislike_count': int, |  | ||||||
|         }, |         }, | ||||||
|         'params': { |         'params': { | ||||||
|             'skip_download': True, |             'skip_download': True, | ||||||
| @@ -3043,8 +3157,7 @@ class YoutubeTabIE(YoutubeBaseInfoExtractor): | |||||||
|  |  | ||||||
|     def _real_extract(self, url): |     def _real_extract(self, url): | ||||||
|         item_id = self._match_id(url) |         item_id = self._match_id(url) | ||||||
|         url = compat_urlparse.urlunparse( |         url = update_url(url, netloc='www.youtube.com') | ||||||
|             compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com')) |  | ||||||
|         # Handle both video/playlist URLs |         # Handle both video/playlist URLs | ||||||
|         qs = parse_qs(url) |         qs = parse_qs(url) | ||||||
|         video_id = qs.get('v', [None])[0] |         video_id = qs.get('v', [None])[0] | ||||||
| @@ -3178,7 +3291,6 @@ class YoutubeYtBeIE(InfoExtractor): | |||||||
|             'categories': ['Nonprofits & Activism'], |             'categories': ['Nonprofits & Activism'], | ||||||
|             'tags': list, |             'tags': list, | ||||||
|             'like_count': int, |             'like_count': int, | ||||||
|             'dislike_count': int, |  | ||||||
|         }, |         }, | ||||||
|         'params': { |         'params': { | ||||||
|             'noplaylist': True, |             'noplaylist': True, | ||||||
|   | |||||||
| @@ -4121,6 +4121,17 @@ def update_url_query(url, query): | |||||||
|         query=compat_urllib_parse_urlencode(qs, True))) |         query=compat_urllib_parse_urlencode(qs, True))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def update_url(url, **kwargs): | ||||||
|  |     """Replace URL components specified by kwargs | ||||||
|  |        url: compat_str or parsed URL tuple | ||||||
|  |        returns: compat_str""" | ||||||
|  |     if not kwargs: | ||||||
|  |         return compat_urlparse.urlunparse(url) if isinstance(url, tuple) else url | ||||||
|  |     if not isinstance(url, tuple): | ||||||
|  |         url = compat_urlparse.urlparse(url) | ||||||
|  |     return compat_urlparse.urlunparse(url._replace(**kwargs)) | ||||||
|  |  | ||||||
|  |  | ||||||
| def update_Request(req, url=None, data=None, headers={}, query={}): | def update_Request(req, url=None, data=None, headers={}, query={}): | ||||||
|     req_headers = req.headers.copy() |     req_headers = req.headers.copy() | ||||||
|     req_headers.update(headers) |     req_headers.update(headers) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user