]> asedeno.scripts.mit.edu Git - youtube-dl.git/blob - youtube_dl/extractor/youtube.py
ff6c7b0f8b148a474bc153ab03bc4abd2c980e9b
[youtube-dl.git] / youtube_dl / extractor / youtube.py
1 # coding: utf-8
2
3 from __future__ import unicode_literals
4
5 import itertools
6 import json
7 import os.path
8 import random
9 import re
10 import traceback
11
12 from .common import InfoExtractor, SearchInfoExtractor
13 from ..compat import (
14     compat_chr,
15     compat_HTTPError,
16     compat_map as map,
17     compat_parse_qs,
18     compat_str,
19     compat_urllib_parse_unquote_plus,
20     compat_urllib_parse_urlencode,
21     compat_urllib_parse_urlparse,
22     compat_urlparse,
23 )
24 from ..jsinterp import JSInterpreter
25 from ..utils import (
26     ExtractorError,
27     clean_html,
28     dict_get,
29     error_to_compat_str,
30     float_or_none,
31     int_or_none,
32     js_to_json,
33     mimetype2ext,
34     parse_codecs,
35     parse_duration,
36     qualities,
37     remove_start,
38     smuggle_url,
39     str_or_none,
40     str_to_int,
41     try_get,
42     unescapeHTML,
43     unified_strdate,
44     unsmuggle_url,
45     update_url_query,
46     url_or_none,
47     urlencode_postdata,
48     urljoin,
49 )
50
51
52 def parse_qs(url):
53     return compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
54
55
56 class YoutubeBaseInfoExtractor(InfoExtractor):
57     """Provide base functions for Youtube extractors"""
58     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
59     _TWOFACTOR_URL = 'https://accounts.google.com/signin/challenge'
60
61     _LOOKUP_URL = 'https://accounts.google.com/_/signin/sl/lookup'
62     _CHALLENGE_URL = 'https://accounts.google.com/_/signin/sl/challenge'
63     _TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
64
65     _NETRC_MACHINE = 'youtube'
66     # If True it will raise an error if no login info is provided
67     _LOGIN_REQUIRED = False
68
69     _PLAYLIST_ID_RE = r'(?:(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}|RDMM)'
70
71     def _login(self):
72         """
73         Attempt to log in to YouTube.
74         True is returned if successful or skipped.
75         False is returned if login failed.
76
77         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
78         """
79         username, password = self._get_login_info()
80         # No authentication to be performed
81         if username is None:
82             if self._LOGIN_REQUIRED and self._downloader.params.get('cookiefile') is None:
83                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
84             return True
85
86         login_page = self._download_webpage(
87             self._LOGIN_URL, None,
88             note='Downloading login page',
89             errnote='unable to fetch login page', fatal=False)
90         if login_page is False:
91             return
92
93         login_form = self._hidden_inputs(login_page)
94
95         def req(url, f_req, note, errnote):
96             data = login_form.copy()
97             data.update({
98                 'pstMsg': 1,
99                 'checkConnection': 'youtube',
100                 'checkedDomains': 'youtube',
101                 'hl': 'en',
102                 'deviceinfo': '[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]',
103                 'f.req': json.dumps(f_req),
104                 'flowName': 'GlifWebSignIn',
105                 'flowEntry': 'ServiceLogin',
106                 # TODO: reverse actual botguard identifier generation algo
107                 'bgRequest': '["identifier",""]',
108             })
109             return self._download_json(
110                 url, None, note=note, errnote=errnote,
111                 transform_source=lambda s: re.sub(r'^[^[]*', '', s),
112                 fatal=False,
113                 data=urlencode_postdata(data), headers={
114                     'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
115                     'Google-Accounts-XSRF': 1,
116                 })
117
118         def warn(message):
119             self._downloader.report_warning(message)
120
121         lookup_req = [
122             username,
123             None, [], None, 'US', None, None, 2, False, True,
124             [
125                 None, None,
126                 [2, 1, None, 1,
127                  'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn',
128                  None, [], 4],
129                 1, [None, None, []], None, None, None, True
130             ],
131             username,
132         ]
133
134         lookup_results = req(
135             self._LOOKUP_URL, lookup_req,
136             'Looking up account info', 'Unable to look up account info')
137
138         if lookup_results is False:
139             return False
140
141         user_hash = try_get(lookup_results, lambda x: x[0][2], compat_str)
142         if not user_hash:
143             warn('Unable to extract user hash')
144             return False
145
146         challenge_req = [
147             user_hash,
148             None, 1, None, [1, None, None, None, [password, None, True]],
149             [
150                 None, None, [2, 1, None, 1, 'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn', None, [], 4],
151                 1, [None, None, []], None, None, None, True
152             ]]
153
154         challenge_results = req(
155             self._CHALLENGE_URL, challenge_req,
156             'Logging in', 'Unable to log in')
157
158         if challenge_results is False:
159             return
160
161         login_res = try_get(challenge_results, lambda x: x[0][5], list)
162         if login_res:
163             login_msg = try_get(login_res, lambda x: x[5], compat_str)
164             warn(
165                 'Unable to login: %s' % 'Invalid password'
166                 if login_msg == 'INCORRECT_ANSWER_ENTERED' else login_msg)
167             return False
168
169         res = try_get(challenge_results, lambda x: x[0][-1], list)
170         if not res:
171             warn('Unable to extract result entry')
172             return False
173
174         login_challenge = try_get(res, lambda x: x[0][0], list)
175         if login_challenge:
176             challenge_str = try_get(login_challenge, lambda x: x[2], compat_str)
177             if challenge_str == 'TWO_STEP_VERIFICATION':
178                 # SEND_SUCCESS - TFA code has been successfully sent to phone
179                 # QUOTA_EXCEEDED - reached the limit of TFA codes
180                 status = try_get(login_challenge, lambda x: x[5], compat_str)
181                 if status == 'QUOTA_EXCEEDED':
182                     warn('Exceeded the limit of TFA codes, try later')
183                     return False
184
185                 tl = try_get(challenge_results, lambda x: x[1][2], compat_str)
186                 if not tl:
187                     warn('Unable to extract TL')
188                     return False
189
190                 tfa_code = self._get_tfa_info('2-step verification code')
191
192                 if not tfa_code:
193                     warn(
194                         'Two-factor authentication required. Provide it either interactively or with --twofactor <code>'
195                         '(Note that only TOTP (Google Authenticator App) codes work at this time.)')
196                     return False
197
198                 tfa_code = remove_start(tfa_code, 'G-')
199
200                 tfa_req = [
201                     user_hash, None, 2, None,
202                     [
203                         9, None, None, None, None, None, None, None,
204                         [None, tfa_code, True, 2]
205                     ]]
206
207                 tfa_results = req(
208                     self._TFA_URL.format(tl), tfa_req,
209                     'Submitting TFA code', 'Unable to submit TFA code')
210
211                 if tfa_results is False:
212                     return False
213
214                 tfa_res = try_get(tfa_results, lambda x: x[0][5], list)
215                 if tfa_res:
216                     tfa_msg = try_get(tfa_res, lambda x: x[5], compat_str)
217                     warn(
218                         'Unable to finish TFA: %s' % 'Invalid TFA code'
219                         if tfa_msg == 'INCORRECT_ANSWER_ENTERED' else tfa_msg)
220                     return False
221
222                 check_cookie_url = try_get(
223                     tfa_results, lambda x: x[0][-1][2], compat_str)
224             else:
225                 CHALLENGES = {
226                     'LOGIN_CHALLENGE': "This device isn't recognized. For your security, Google wants to make sure it's really you.",
227                     'USERNAME_RECOVERY': 'Please provide additional information to aid in the recovery process.',
228                     'REAUTH': "There is something unusual about your activity. For your security, Google wants to make sure it's really you.",
229                 }
230                 challenge = CHALLENGES.get(
231                     challenge_str,
232                     '%s returned error %s.' % (self.IE_NAME, challenge_str))
233                 warn('%s\nGo to https://accounts.google.com/, login and solve a challenge.' % challenge)
234                 return False
235         else:
236             check_cookie_url = try_get(res, lambda x: x[2], compat_str)
237
238         if not check_cookie_url:
239             warn('Unable to extract CheckCookie URL')
240             return False
241
242         check_cookie_results = self._download_webpage(
243             check_cookie_url, None, 'Checking cookie', fatal=False)
244
245         if check_cookie_results is False:
246             return False
247
248         if 'https://myaccount.google.com/' not in check_cookie_results:
249             warn('Unable to log in')
250             return False
251
252         return True
253
254     def _initialize_consent(self):
255         cookies = self._get_cookies('https://www.youtube.com/')
256         if cookies.get('__Secure-3PSID'):
257             return
258         consent_id = None
259         consent = cookies.get('CONSENT')
260         if consent:
261             if 'YES' in consent.value:
262                 return
263             consent_id = self._search_regex(
264                 r'PENDING\+(\d+)', consent.value, 'consent', default=None)
265         if not consent_id:
266             consent_id = random.randint(100, 999)
267         self._set_cookie('.youtube.com', 'CONSENT', 'YES+cb.20210328-17-p0.en+FX+%s' % consent_id)
268
269     def _real_initialize(self):
270         self._initialize_consent()
271         if self._downloader is None:
272             return
273         if not self._login():
274             return
275
276     _DEFAULT_API_DATA = {
277         'context': {
278             'client': {
279                 'clientName': 'WEB',
280                 'clientVersion': '2.20201021.03.00',
281             }
282         },
283     }
284
285     _YT_INITIAL_DATA_RE = r'(?:window\s*\[\s*["\']ytInitialData["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;'
286     _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*=\s*({.+?})\s*;'
287     _YT_INITIAL_BOUNDARY_RE = r'(?:var\s+meta|</script|\n)'
288
289     def _call_api(self, ep, query, video_id, fatal=True):
290         data = self._DEFAULT_API_DATA.copy()
291         data.update(query)
292
293         return self._download_json(
294             'https://www.youtube.com/youtubei/v1/%s' % ep, video_id=video_id,
295             note='Downloading API JSON', errnote='Unable to download API page',
296             data=json.dumps(data).encode('utf8'), fatal=fatal,
297             headers={'content-type': 'application/json'},
298             query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'})
299
300     def _extract_yt_initial_data(self, video_id, webpage):
301         return self._parse_json(
302             self._search_regex(
303                 (r'%s\s*%s' % (self._YT_INITIAL_DATA_RE, self._YT_INITIAL_BOUNDARY_RE),
304                  self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
305             video_id)
306
307     def _extract_ytcfg(self, video_id, webpage):
308         return self._parse_json(
309             self._search_regex(
310                 r'ytcfg\.set\s*\(\s*({.+?})\s*\)\s*;', webpage, 'ytcfg',
311                 default='{}'), video_id, fatal=False) or {}
312
313     def _extract_video(self, renderer):
314         video_id = renderer['videoId']
315         title = try_get(
316             renderer,
317             (lambda x: x['title']['runs'][0]['text'],
318              lambda x: x['title']['simpleText']), compat_str)
319         description = try_get(
320             renderer, lambda x: x['descriptionSnippet']['runs'][0]['text'],
321             compat_str)
322         duration = parse_duration(try_get(
323             renderer, lambda x: x['lengthText']['simpleText'], compat_str))
324         view_count_text = try_get(
325             renderer, lambda x: x['viewCountText']['simpleText'], compat_str) or ''
326         view_count = str_to_int(self._search_regex(
327             r'^([\d,]+)', re.sub(r'\s', '', view_count_text),
328             'view count', default=None))
329         uploader = try_get(
330             renderer,
331             (lambda x: x['ownerText']['runs'][0]['text'],
332              lambda x: x['shortBylineText']['runs'][0]['text']), compat_str)
333         return {
334             '_type': 'url',
335             'ie_key': YoutubeIE.ie_key(),
336             'id': video_id,
337             'url': video_id,
338             'title': title,
339             'description': description,
340             'duration': duration,
341             'view_count': view_count,
342             'uploader': uploader,
343         }
344
345     def _search_results(self, query, params):
346         data = {
347             'context': {
348                 'client': {
349                     'clientName': 'WEB',
350                     'clientVersion': '2.20201021.03.00',
351                 }
352             },
353             'query': query,
354         }
355         if params:
356             data['params'] = params
357         for page_num in itertools.count(1):
358             search = self._download_json(
359                 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
360                 video_id='query "%s"' % query,
361                 note='Downloading page %s' % page_num,
362                 errnote='Unable to download API page', fatal=False,
363                 data=json.dumps(data).encode('utf8'),
364                 headers={'content-type': 'application/json'})
365             if not search:
366                 break
367             slr_contents = try_get(
368                 search,
369                 (lambda x: x['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'],
370                  lambda x: x['onResponseReceivedCommands'][0]['appendContinuationItemsAction']['continuationItems']),
371                 list)
372             if not slr_contents:
373                 break
374             for slr_content in slr_contents:
375                 isr_contents = try_get(
376                     slr_content,
377                     lambda x: x['itemSectionRenderer']['contents'],
378                     list)
379                 if not isr_contents:
380                     continue
381                 for content in isr_contents:
382                     if not isinstance(content, dict):
383                         continue
384                     video = content.get('videoRenderer')
385                     if not isinstance(video, dict):
386                         continue
387                     video_id = video.get('videoId')
388                     if not video_id:
389                         continue
390                     yield self._extract_video(video)
391             token = try_get(
392                 slr_contents,
393                 lambda x: x[-1]['continuationItemRenderer']['continuationEndpoint']['continuationCommand']['token'],
394                 compat_str)
395             if not token:
396                 break
397             data['continuation'] = token
398
399
400 class YoutubeIE(YoutubeBaseInfoExtractor):
401     IE_DESC = 'YouTube.com'
402     _INVIDIOUS_SITES = (
403         # invidious-redirect websites
404         r'(?:www\.)?redirect\.invidious\.io',
405         r'(?:(?:www|dev)\.)?invidio\.us',
406         # Invidious instances taken from https://github.com/iv-org/documentation/blob/master/Invidious-Instances.md
407         r'(?:(?:www|no)\.)?invidiou\.sh',
408         r'(?:(?:www|fi)\.)?invidious\.snopyta\.org',
409         r'(?:www\.)?invidious\.kabi\.tk',
410         r'(?:www\.)?invidious\.13ad\.de',
411         r'(?:www\.)?invidious\.mastodon\.host',
412         r'(?:www\.)?invidious\.zapashcanon\.fr',
413         r'(?:www\.)?(?:invidious(?:-us)?|piped)\.kavin\.rocks',
414         r'(?:www\.)?invidious\.tinfoil-hat\.net',
415         r'(?:www\.)?invidious\.himiko\.cloud',
416         r'(?:www\.)?invidious\.reallyancient\.tech',
417         r'(?:www\.)?invidious\.tube',
418         r'(?:www\.)?invidiou\.site',
419         r'(?:www\.)?invidious\.site',
420         r'(?:www\.)?invidious\.xyz',
421         r'(?:www\.)?invidious\.nixnet\.xyz',
422         r'(?:www\.)?invidious\.048596\.xyz',
423         r'(?:www\.)?invidious\.drycat\.fr',
424         r'(?:www\.)?inv\.skyn3t\.in',
425         r'(?:www\.)?tube\.poal\.co',
426         r'(?:www\.)?tube\.connect\.cafe',
427         r'(?:www\.)?vid\.wxzm\.sx',
428         r'(?:www\.)?vid\.mint\.lgbt',
429         r'(?:www\.)?vid\.puffyan\.us',
430         r'(?:www\.)?yewtu\.be',
431         r'(?:www\.)?yt\.elukerio\.org',
432         r'(?:www\.)?yt\.lelux\.fi',
433         r'(?:www\.)?invidious\.ggc-project\.de',
434         r'(?:www\.)?yt\.maisputain\.ovh',
435         r'(?:www\.)?ytprivate\.com',
436         r'(?:www\.)?invidious\.13ad\.de',
437         r'(?:www\.)?invidious\.toot\.koeln',
438         r'(?:www\.)?invidious\.fdn\.fr',
439         r'(?:www\.)?watch\.nettohikari\.com',
440         r'(?:www\.)?invidious\.namazso\.eu',
441         r'(?:www\.)?invidious\.silkky\.cloud',
442         r'(?:www\.)?invidious\.exonip\.de',
443         r'(?:www\.)?invidious\.riverside\.rocks',
444         r'(?:www\.)?invidious\.blamefran\.net',
445         r'(?:www\.)?invidious\.moomoo\.de',
446         r'(?:www\.)?ytb\.trom\.tf',
447         r'(?:www\.)?yt\.cyberhost\.uk',
448         r'(?:www\.)?kgg2m7yk5aybusll\.onion',
449         r'(?:www\.)?qklhadlycap4cnod\.onion',
450         r'(?:www\.)?axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid\.onion',
451         r'(?:www\.)?c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid\.onion',
452         r'(?:www\.)?fz253lmuao3strwbfbmx46yu7acac2jz27iwtorgmbqlkurlclmancad\.onion',
453         r'(?:www\.)?invidious\.l4qlywnpwqsluw65ts7md3khrivpirse744un3x7mlskqauz5pyuzgqd\.onion',
454         r'(?:www\.)?owxfohz4kjyv25fvlqilyxast7inivgiktls3th44jhk3ej3i7ya\.b32\.i2p',
455         r'(?:www\.)?4l2dgddgsrkf2ous66i6seeyi6etzfgrue332grh2n7madpwopotugyd\.onion',
456         r'(?:www\.)?w6ijuptxiku4xpnnaetxvnkc5vqcdu7mgns2u77qefoixi63vbvnpnqd\.onion',
457         r'(?:www\.)?kbjggqkzv65ivcqj6bumvp337z6264huv5kpkwuv6gu5yjiskvan7fad\.onion',
458         r'(?:www\.)?grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad\.onion',
459         r'(?:www\.)?hpniueoejy4opn7bc4ftgazyqjoeqwlvh2uiku2xqku6zpoa4bf5ruid\.onion',
460     )
461     _VALID_URL = r"""(?x)^
462                      (
463                          (?:https?://|//)                                    # http(s):// or protocol-independent URL
464                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie|kids)?\.com|
465                             (?:www\.)?deturl\.com/www\.youtube\.com|
466                             (?:www\.)?pwnyoutube\.com|
467                             (?:www\.)?hooktube\.com|
468                             (?:www\.)?yourepeat\.com|
469                             tube\.majestyc\.net|
470                             %(invidious)s|
471                             youtube\.googleapis\.com)/                        # the various hostnames, with wildcard subdomains
472                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
473                          (?:                                                  # the various things that can precede the ID:
474                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/
475                              |shorts/
476                              |(?:                                             # or the v= param in all its forms
477                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
478                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
479                                  (?:.*?[&;])??                                # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&amp;v=V36LpHqtcDY)
480                                  v=
481                              )
482                          ))
483                          |(?:
484                             youtu\.be|                                        # just youtu.be/xxxx
485                             vid\.plus|                                        # or vid.plus/xxxx
486                             zwearz\.com/watch|                                # or zwearz.com/watch/xxxx
487                             %(invidious)s
488                          )/
489                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
490                          )
491                      )?                                                       # all until now is optional -> you can pass the naked ID
492                      (?P<id>[0-9A-Za-z_-]{11})                                # here is it! the YouTube video ID
493                      (?(1).+)?                                                # if we found the ID, everything can follow
494                      $""" % {
495         'invidious': '|'.join(_INVIDIOUS_SITES),
496     }
497     _PLAYER_INFO_RE = (
498         r'/s/player/(?P<id>[a-zA-Z0-9_-]{8,})/player',
499         r'/(?P<id>[a-zA-Z0-9_-]{8,})/player(?:_ias\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?|-plasma-ias-(?:phone|tablet)-[a-z]{2}_[A-Z]{2}\.vflset)/base\.js$',
500         r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
501     )
502     _SUBTITLE_FORMATS = ('srv1', 'srv2', 'srv3', 'ttml', 'vtt')
503
504     _GEO_BYPASS = False
505
506     IE_NAME = 'youtube'
507     _TESTS = [
508         {
509             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
510             'info_dict': {
511                 'id': 'BaW_jenozKc',
512                 'ext': 'mp4',
513                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
514                 'uploader': 'Philipp Hagemeister',
515                 'uploader_id': 'phihag',
516                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
517                 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
518                 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
519                 'upload_date': '20121002',
520                 'description': 'test chars:  "\'/\\ä↭𝕐\ntest URL: https://github.com/rg3/youtube-dl/issues/1892\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de .',
521                 'categories': ['Science & Technology'],
522                 'tags': ['youtube-dl'],
523                 'duration': 10,
524                 'view_count': int,
525                 'like_count': int,
526                 'dislike_count': int,
527                 'start_time': 1,
528                 'end_time': 9,
529             }
530         },
531         {
532             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
533             'note': 'Embed-only video (#1746)',
534             'info_dict': {
535                 'id': 'yZIXLfi8CZQ',
536                 'ext': 'mp4',
537                 'upload_date': '20120608',
538                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
539                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
540                 'uploader': 'SET India',
541                 'uploader_id': 'setindia',
542                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/setindia',
543                 'age_limit': 18,
544             },
545             'skip': 'Private video',
546         },
547         {
548             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=yZIXLfi8CZQ',
549             'note': 'Use the first video ID in the URL',
550             'info_dict': {
551                 'id': 'BaW_jenozKc',
552                 'ext': 'mp4',
553                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
554                 'uploader': 'Philipp Hagemeister',
555                 'uploader_id': 'phihag',
556                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
557                 'upload_date': '20121002',
558                 'description': 'test chars:  "\'/\\ä↭𝕐\ntest URL: https://github.com/rg3/youtube-dl/issues/1892\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de .',
559                 'categories': ['Science & Technology'],
560                 'tags': ['youtube-dl'],
561                 'duration': 10,
562                 'view_count': int,
563                 'like_count': int,
564                 'dislike_count': int,
565             },
566             'params': {
567                 'skip_download': True,
568             },
569         },
570         {
571             'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
572             'note': '256k DASH audio (format 141) via DASH manifest',
573             'info_dict': {
574                 'id': 'a9LDPn-MO4I',
575                 'ext': 'm4a',
576                 'upload_date': '20121002',
577                 'uploader_id': '8KVIDEO',
578                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
579                 'description': '',
580                 'uploader': '8KVIDEO',
581                 'title': 'UHDTV TEST 8K VIDEO.mp4'
582             },
583             'params': {
584                 'youtube_include_dash_manifest': True,
585                 'format': '141',
586             },
587             'skip': 'format 141 not served anymore',
588         },
589         # DASH manifest with encrypted signature
590         {
591             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
592             'info_dict': {
593                 'id': 'IB3lcPjvWLA',
594                 'ext': 'm4a',
595                 'title': 'Afrojack, Spree Wilson - The Spark (Official Music Video) ft. Spree Wilson',
596                 'description': 'md5:8f5e2b82460520b619ccac1f509d43bf',
597                 'duration': 244,
598                 'uploader': 'AfrojackVEVO',
599                 'uploader_id': 'AfrojackVEVO',
600                 'upload_date': '20131011',
601                 'abr': 129.495,
602             },
603             'params': {
604                 'youtube_include_dash_manifest': True,
605                 'format': '141/bestaudio[ext=m4a]',
606             },
607         },
608         # Controversy video
609         {
610             'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
611             'info_dict': {
612                 'id': 'T4XJQO3qol8',
613                 'ext': 'mp4',
614                 'duration': 219,
615                 'upload_date': '20100909',
616                 'uploader': 'Amazing Atheist',
617                 'uploader_id': 'TheAmazingAtheist',
618                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheAmazingAtheist',
619                 'title': 'Burning Everyone\'s Koran',
620                 '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',
621             }
622         },
623         # Normal age-gate video (No vevo, embed allowed), available via embed page
624         {
625             'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
626             'info_dict': {
627                 'id': 'HtVdAasjOgU',
628                 'ext': 'mp4',
629                 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
630                 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
631                 'duration': 142,
632                 'uploader': 'The Witcher',
633                 'uploader_id': 'WitcherGame',
634                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame',
635                 'upload_date': '20140605',
636                 'age_limit': 18,
637             },
638         },
639         {
640             # Age-gated video only available with authentication (unavailable
641             # via embed page workaround)
642             'url': 'XgnwCQzjau8',
643             'only_matching': True,
644         },
645         # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421)
646         # YouTube Red ad is not captured for creator
647         {
648             'url': '__2ABJjxzNo',
649             'info_dict': {
650                 'id': '__2ABJjxzNo',
651                 'ext': 'mp4',
652                 'duration': 266,
653                 'upload_date': '20100430',
654                 'uploader_id': 'deadmau5',
655                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/deadmau5',
656                 'creator': 'deadmau5',
657                 'description': 'md5:6cbcd3a92ce1bc676fc4d6ab4ace2336',
658                 'uploader': 'deadmau5',
659                 'title': 'Deadmau5 - Some Chords (HD)',
660                 'alt_title': 'Some Chords',
661             },
662             'expected_warnings': [
663                 'DASH manifest missing',
664             ]
665         },
666         # Olympics (https://github.com/ytdl-org/youtube-dl/issues/4431)
667         {
668             'url': 'lqQg6PlCWgI',
669             'info_dict': {
670                 'id': 'lqQg6PlCWgI',
671                 'ext': 'mp4',
672                 'duration': 6085,
673                 'upload_date': '20150827',
674                 'uploader_id': 'olympic',
675                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic',
676                 'description': 'HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games',
677                 'uploader': 'Olympic',
678                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games',
679             },
680             'params': {
681                 'skip_download': 'requires avconv',
682             }
683         },
684         # Non-square pixels
685         {
686             'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
687             'info_dict': {
688                 'id': '_b-2C3KPAM0',
689                 'ext': 'mp4',
690                 'stretched_ratio': 16 / 9.,
691                 'duration': 85,
692                 'upload_date': '20110310',
693                 'uploader_id': 'AllenMeow',
694                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/AllenMeow',
695                 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
696                 'uploader': '孫ᄋᄅ',
697                 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
698             },
699         },
700         # url_encoded_fmt_stream_map is empty string
701         {
702             'url': 'qEJwOuvDf7I',
703             'info_dict': {
704                 'id': 'qEJwOuvDf7I',
705                 'ext': 'webm',
706                 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
707                 'description': '',
708                 'upload_date': '20150404',
709                 'uploader_id': 'spbelect',
710                 'uploader': 'Наблюдатели Петербурга',
711             },
712             'params': {
713                 'skip_download': 'requires avconv',
714             },
715             'skip': 'This live event has ended.',
716         },
717         # Extraction from multiple DASH manifests (https://github.com/ytdl-org/youtube-dl/pull/6097)
718         {
719             'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
720             'info_dict': {
721                 'id': 'FIl7x6_3R5Y',
722                 'ext': 'webm',
723                 'title': 'md5:7b81415841e02ecd4313668cde88737a',
724                 'description': 'md5:116377fd2963b81ec4ce64b542173306',
725                 'duration': 220,
726                 'upload_date': '20150625',
727                 'uploader_id': 'dorappi2000',
728                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/dorappi2000',
729                 'uploader': 'dorappi2000',
730                 'formats': 'mincount:31',
731             },
732             'skip': 'not actual anymore',
733         },
734         # DASH manifest with segment_list
735         {
736             'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
737             'md5': '8ce563a1d667b599d21064e982ab9e31',
738             'info_dict': {
739                 'id': 'CsmdDsKjzN8',
740                 'ext': 'mp4',
741                 'upload_date': '20150501',  # According to '<meta itemprop="datePublished"', but in other places it's 20150510
742                 'uploader': 'Airtek',
743                 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
744                 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
745                 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
746             },
747             'params': {
748                 'youtube_include_dash_manifest': True,
749                 'format': '135',  # bestvideo
750             },
751             'skip': 'This live event has ended.',
752         },
753         {
754             # Multifeed videos (multiple cameras), URL is for Main Camera
755             'url': 'https://www.youtube.com/watch?v=jvGDaLqkpTg',
756             'info_dict': {
757                 'id': 'jvGDaLqkpTg',
758                 'title': 'Tom Clancy Free Weekend Rainbow Whatever',
759                 'description': 'md5:e03b909557865076822aa169218d6a5d',
760             },
761             'playlist': [{
762                 'info_dict': {
763                     'id': 'jvGDaLqkpTg',
764                     'ext': 'mp4',
765                     'title': 'Tom Clancy Free Weekend Rainbow Whatever (Main Camera)',
766                     'description': 'md5:e03b909557865076822aa169218d6a5d',
767                     'duration': 10643,
768                     'upload_date': '20161111',
769                     'uploader': 'Team PGP',
770                     'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
771                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
772                 },
773             }, {
774                 'info_dict': {
775                     'id': '3AKt1R1aDnw',
776                     'ext': 'mp4',
777                     'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 2)',
778                     'description': 'md5:e03b909557865076822aa169218d6a5d',
779                     'duration': 10991,
780                     'upload_date': '20161111',
781                     'uploader': 'Team PGP',
782                     'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
783                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
784                 },
785             }, {
786                 'info_dict': {
787                     'id': 'RtAMM00gpVc',
788                     'ext': 'mp4',
789                     'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 3)',
790                     'description': 'md5:e03b909557865076822aa169218d6a5d',
791                     'duration': 10995,
792                     'upload_date': '20161111',
793                     'uploader': 'Team PGP',
794                     'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
795                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
796                 },
797             }, {
798                 'info_dict': {
799                     'id': '6N2fdlP3C5U',
800                     'ext': 'mp4',
801                     'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 4)',
802                     'description': 'md5:e03b909557865076822aa169218d6a5d',
803                     'duration': 10990,
804                     'upload_date': '20161111',
805                     'uploader': 'Team PGP',
806                     'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
807                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
808                 },
809             }],
810             'params': {
811                 'skip_download': True,
812             },
813         },
814         {
815             # Multifeed video with comma in title (see https://github.com/ytdl-org/youtube-dl/issues/8536)
816             'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
817             'info_dict': {
818                 'id': 'gVfLd0zydlo',
819                 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
820             },
821             'playlist_count': 2,
822             'skip': 'Not multifeed anymore',
823         },
824         {
825             'url': 'https://vid.plus/FlRa-iH7PGw',
826             'only_matching': True,
827         },
828         {
829             'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
830             'only_matching': True,
831         },
832         {
833             # Title with JS-like syntax "};" (see https://github.com/ytdl-org/youtube-dl/issues/7468)
834             # Also tests cut-off URL expansion in video description (see
835             # https://github.com/ytdl-org/youtube-dl/issues/1892,
836             # https://github.com/ytdl-org/youtube-dl/issues/8164)
837             'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
838             'info_dict': {
839                 'id': 'lsguqyKfVQg',
840                 'ext': 'mp4',
841                 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
842                 'alt_title': 'Dark Walk - Position Music',
843                 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
844                 'duration': 133,
845                 'upload_date': '20151119',
846                 'uploader_id': 'IronSoulElf',
847                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IronSoulElf',
848                 'uploader': 'IronSoulElf',
849                 'creator': 'Todd Haberman,  Daniel Law Heath and Aaron Kaplan',
850                 'track': 'Dark Walk - Position Music',
851                 'artist': 'Todd Haberman,  Daniel Law Heath and Aaron Kaplan',
852                 'album': 'Position Music - Production Music Vol. 143 - Dark Walk',
853             },
854             'params': {
855                 'skip_download': True,
856             },
857         },
858         {
859             # Tags with '};' (see https://github.com/ytdl-org/youtube-dl/issues/7468)
860             'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
861             'only_matching': True,
862         },
863         {
864             # Video with yt:stretch=17:0
865             'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
866             'info_dict': {
867                 'id': 'Q39EVAstoRM',
868                 'ext': 'mp4',
869                 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
870                 'description': 'md5:ee18a25c350637c8faff806845bddee9',
871                 'upload_date': '20151107',
872                 'uploader_id': 'UCCr7TALkRbo3EtFzETQF1LA',
873                 'uploader': 'CH GAMER DROID',
874             },
875             'params': {
876                 'skip_download': True,
877             },
878             'skip': 'This video does not exist.',
879         },
880         {
881             # Video with incomplete 'yt:stretch=16:'
882             'url': 'https://www.youtube.com/watch?v=FRhJzUSJbGI',
883             'only_matching': True,
884         },
885         {
886             # Video licensed under Creative Commons
887             'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
888             'info_dict': {
889                 'id': 'M4gD1WSo5mA',
890                 'ext': 'mp4',
891                 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
892                 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
893                 'duration': 721,
894                 'upload_date': '20150127',
895                 'uploader_id': 'BerkmanCenter',
896                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/BerkmanCenter',
897                 'uploader': 'The Berkman Klein Center for Internet & Society',
898                 'license': 'Creative Commons Attribution license (reuse allowed)',
899             },
900             'params': {
901                 'skip_download': True,
902             },
903         },
904         {
905             # Channel-like uploader_url
906             'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
907             'info_dict': {
908                 'id': 'eQcmzGIKrzg',
909                 'ext': 'mp4',
910                 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
911                 'description': 'md5:13a2503d7b5904ef4b223aa101628f39',
912                 'duration': 4060,
913                 'upload_date': '20151119',
914                 'uploader': 'Bernie Sanders',
915                 'uploader_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
916                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
917                 'license': 'Creative Commons Attribution license (reuse allowed)',
918             },
919             'params': {
920                 'skip_download': True,
921             },
922         },
923         {
924             'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;amp;v=V36LpHqtcDY',
925             'only_matching': True,
926         },
927         {
928             # YouTube Red paid video (https://github.com/ytdl-org/youtube-dl/issues/10059)
929             'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
930             'only_matching': True,
931         },
932         {
933             # Rental video preview
934             'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
935             'info_dict': {
936                 'id': 'uGpuVWrhIzE',
937                 'ext': 'mp4',
938                 'title': 'Piku - Trailer',
939                 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
940                 'upload_date': '20150811',
941                 'uploader': 'FlixMatrix',
942                 'uploader_id': 'FlixMatrixKaravan',
943                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/FlixMatrixKaravan',
944                 'license': 'Standard YouTube License',
945             },
946             'params': {
947                 'skip_download': True,
948             },
949             'skip': 'This video is not available.',
950         },
951         {
952             # YouTube Red video with episode data
953             'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
954             'info_dict': {
955                 'id': 'iqKdEhx-dD4',
956                 'ext': 'mp4',
957                 'title': 'Isolation - Mind Field (Ep 1)',
958                 'description': 'md5:f540112edec5d09fc8cc752d3d4ba3cd',
959                 'duration': 2085,
960                 'upload_date': '20170118',
961                 'uploader': 'Vsauce',
962                 'uploader_id': 'Vsauce',
963                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Vsauce',
964                 'series': 'Mind Field',
965                 'season_number': 1,
966                 'episode_number': 1,
967             },
968             'params': {
969                 'skip_download': True,
970             },
971             'expected_warnings': [
972                 'Skipping DASH manifest',
973             ],
974         },
975         {
976             # The following content has been identified by the YouTube community
977             # as inappropriate or offensive to some audiences.
978             'url': 'https://www.youtube.com/watch?v=6SJNVb0GnPI',
979             'info_dict': {
980                 'id': '6SJNVb0GnPI',
981                 'ext': 'mp4',
982                 'title': 'Race Differences in Intelligence',
983                 'description': 'md5:5d161533167390427a1f8ee89a1fc6f1',
984                 'duration': 965,
985                 'upload_date': '20140124',
986                 'uploader': 'New Century Foundation',
987                 'uploader_id': 'UCEJYpZGqgUob0zVVEaLhvVg',
988                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCEJYpZGqgUob0zVVEaLhvVg',
989             },
990             'params': {
991                 'skip_download': True,
992             },
993             'skip': 'This video has been removed for violating YouTube\'s policy on hate speech.',
994         },
995         {
996             # itag 212
997             'url': '1t24XAntNCY',
998             'only_matching': True,
999         },
1000         {
1001             # geo restricted to JP
1002             'url': 'sJL6WA-aGkQ',
1003             'only_matching': True,
1004         },
1005         {
1006             'url': 'https://invidio.us/watch?v=BaW_jenozKc',
1007             'only_matching': True,
1008         },
1009         {
1010             'url': 'https://redirect.invidious.io/watch?v=BaW_jenozKc',
1011             'only_matching': True,
1012         },
1013         {
1014             # from https://nitter.pussthecat.org/YouTube/status/1360363141947944964#m
1015             'url': 'https://redirect.invidious.io/Yh0AhrY9GjA',
1016             'only_matching': True,
1017         },
1018         {
1019             # DRM protected
1020             'url': 'https://www.youtube.com/watch?v=s7_qI6_mIXc',
1021             'only_matching': True,
1022         },
1023         {
1024             # Video with unsupported adaptive stream type formats
1025             'url': 'https://www.youtube.com/watch?v=Z4Vy8R84T1U',
1026             'info_dict': {
1027                 'id': 'Z4Vy8R84T1U',
1028                 'ext': 'mp4',
1029                 'title': 'saman SMAN 53 Jakarta(Sancety) opening COFFEE4th at SMAN 53 Jakarta',
1030                 'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
1031                 'duration': 433,
1032                 'upload_date': '20130923',
1033                 'uploader': 'Amelia Putri Harwita',
1034                 'uploader_id': 'UCpOxM49HJxmC1qCalXyB3_Q',
1035                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCpOxM49HJxmC1qCalXyB3_Q',
1036                 'formats': 'maxcount:10',
1037             },
1038             'params': {
1039                 'skip_download': True,
1040                 'youtube_include_dash_manifest': False,
1041             },
1042             'skip': 'not actual anymore',
1043         },
1044         {
1045             # Youtube Music Auto-generated description
1046             'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
1047             'info_dict': {
1048                 'id': 'MgNrAu2pzNs',
1049                 'ext': 'mp4',
1050                 'title': 'Voyeur Girl',
1051                 'description': 'md5:7ae382a65843d6df2685993e90a8628f',
1052                 'upload_date': '20190312',
1053                 'uploader': 'Stephen - Topic',
1054                 'uploader_id': 'UC-pWHpBjdGG69N9mM2auIAA',
1055                 'artist': 'Stephen',
1056                 'track': 'Voyeur Girl',
1057                 'album': 'it\'s too much love to know my dear',
1058                 'release_date': '20190313',
1059                 'release_year': 2019,
1060             },
1061             'params': {
1062                 'skip_download': True,
1063             },
1064         },
1065         {
1066             'url': 'https://www.youtubekids.com/watch?v=3b8nCWDgZ6Q',
1067             'only_matching': True,
1068         },
1069         {
1070             # invalid -> valid video id redirection
1071             'url': 'DJztXj2GPfl',
1072             'info_dict': {
1073                 'id': 'DJztXj2GPfk',
1074                 'ext': 'mp4',
1075                 'title': 'Panjabi MC - Mundian To Bach Ke (The Dictator Soundtrack)',
1076                 'description': 'md5:bf577a41da97918e94fa9798d9228825',
1077                 'upload_date': '20090125',
1078                 'uploader': 'Prochorowka',
1079                 'uploader_id': 'Prochorowka',
1080                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Prochorowka',
1081                 'artist': 'Panjabi MC',
1082                 'track': 'Beware of the Boys (Mundian to Bach Ke) - Motivo Hi-Lectro Remix',
1083                 'album': 'Beware of the Boys (Mundian To Bach Ke)',
1084             },
1085             'params': {
1086                 'skip_download': True,
1087             },
1088             'skip': 'Video unavailable',
1089         },
1090         {
1091             # empty description results in an empty string
1092             'url': 'https://www.youtube.com/watch?v=x41yOUIvK2k',
1093             'info_dict': {
1094                 'id': 'x41yOUIvK2k',
1095                 'ext': 'mp4',
1096                 'title': 'IMG 3456',
1097                 'description': '',
1098                 'upload_date': '20170613',
1099                 'uploader_id': 'ElevageOrVert',
1100                 'uploader': 'ElevageOrVert',
1101             },
1102             'params': {
1103                 'skip_download': True,
1104             },
1105         },
1106         {
1107             # with '};' inside yt initial data (see [1])
1108             # see [2] for an example with '};' inside ytInitialPlayerResponse
1109             # 1. https://github.com/ytdl-org/youtube-dl/issues/27093
1110             # 2. https://github.com/ytdl-org/youtube-dl/issues/27216
1111             'url': 'https://www.youtube.com/watch?v=CHqg6qOn4no',
1112             'info_dict': {
1113                 'id': 'CHqg6qOn4no',
1114                 'ext': 'mp4',
1115                 'title': 'Part 77   Sort a list of simple types in c#',
1116                 'description': 'md5:b8746fa52e10cdbf47997903f13b20dc',
1117                 'upload_date': '20130831',
1118                 'uploader_id': 'kudvenkat',
1119                 'uploader': 'kudvenkat',
1120             },
1121             'params': {
1122                 'skip_download': True,
1123             },
1124         },
1125         {
1126             # another example of '};' in ytInitialData
1127             'url': 'https://www.youtube.com/watch?v=gVfgbahppCY',
1128             'only_matching': True,
1129         },
1130         {
1131             'url': 'https://www.youtube.com/watch_popup?v=63RmMXCd_bQ',
1132             'only_matching': True,
1133         },
1134         {
1135             # https://github.com/ytdl-org/youtube-dl/pull/28094
1136             'url': 'OtqTfy26tG0',
1137             'info_dict': {
1138                 'id': 'OtqTfy26tG0',
1139                 'ext': 'mp4',
1140                 'title': 'Burn Out',
1141                 'description': 'md5:8d07b84dcbcbfb34bc12a56d968b6131',
1142                 'upload_date': '20141120',
1143                 'uploader': 'The Cinematic Orchestra - Topic',
1144                 'uploader_id': 'UCIzsJBIyo8hhpFm1NK0uLgw',
1145                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCIzsJBIyo8hhpFm1NK0uLgw',
1146                 'artist': 'The Cinematic Orchestra',
1147                 'track': 'Burn Out',
1148                 'album': 'Every Day',
1149                 'release_data': None,
1150                 'release_year': None,
1151             },
1152             'params': {
1153                 'skip_download': True,
1154             },
1155         },
1156         {
1157             # controversial video, only works with bpctr when authenticated with cookies
1158             'url': 'https://www.youtube.com/watch?v=nGC3D_FkCmg',
1159             'only_matching': True,
1160         },
1161         {
1162             # restricted location, https://github.com/ytdl-org/youtube-dl/issues/28685
1163             'url': 'cBvYw8_A0vQ',
1164             'info_dict': {
1165                 'id': 'cBvYw8_A0vQ',
1166                 'ext': 'mp4',
1167                 'title': '4K Ueno Okachimachi  Street  Scenes  上野御徒町歩き',
1168                 'description': 'md5:ea770e474b7cd6722b4c95b833c03630',
1169                 'upload_date': '20201120',
1170                 'uploader': 'Walk around Japan',
1171                 'uploader_id': 'UC3o_t8PzBmXf5S9b7GLx1Mw',
1172                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UC3o_t8PzBmXf5S9b7GLx1Mw',
1173             },
1174             'params': {
1175                 'skip_download': True,
1176             },
1177         },
1178         {
1179             # YT 'Shorts'
1180             'url': 'https://youtube.com/shorts/4L2J27mJ3Dc',
1181             'info_dict': {
1182                 'id': '4L2J27mJ3Dc',
1183                 'ext': 'mp4',
1184                 'upload_date': '20211025',
1185                 'uploader': 'Charlie Berens',
1186                 'description': 'md5:976512b8a29269b93bbd8a61edc45a6d',
1187                 'uploader_id': 'fivedlrmilkshake',
1188                 'title': 'Midwest Squid Game #Shorts',
1189             },
1190             'params': {
1191                 'skip_download': True,
1192             },
1193         },
1194     ]
1195     _formats = {
1196         '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1197         '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1198         '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
1199         '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
1200         '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
1201         '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1202         '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1203         '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1204         # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
1205         '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
1206         '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1207         '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1208         '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1209         '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1210         '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1211         '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1212         '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1213         '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1214
1215
1216         # 3D videos
1217         '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1218         '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1219         '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1220         '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1221         '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
1222         '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1223         '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1224
1225         # Apple HTTP Live Streaming
1226         '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1227         '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1228         '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1229         '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1230         '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1231         '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1232         '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1233         '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
1234
1235         # DASH mp4 video
1236         '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
1237         '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
1238         '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1239         '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
1240         '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
1241         '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'},  # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
1242         '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
1243         '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1244         '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
1245         '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1246         '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1247         '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
1248
1249         # Dash mp4 audio
1250         '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
1251         '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
1252         '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
1253         '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1254         '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1255         '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
1256         '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
1257
1258         # Dash webm
1259         '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1260         '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1261         '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1262         '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1263         '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1264         '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1265         '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
1266         '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1267         '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1268         '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1269         '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1270         '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1271         '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1272         '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1273         '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1274         # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
1275         '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1276         '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1277         '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1278         '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1279         '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1280         '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1281
1282         # Dash webm audio
1283         '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
1284         '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
1285
1286         # Dash webm audio with opus inside
1287         '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
1288         '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
1289         '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
1290
1291         # RTMP (unnamed)
1292         '_rtmp': {'protocol': 'rtmp'},
1293
1294         # av01 video only formats sometimes served with "unknown" codecs
1295         '394': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
1296         '395': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
1297         '396': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
1298         '397': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
1299     }
1300
1301     @classmethod
1302     def suitable(cls, url):
1303         # Hack for lazy extractors until more generic solution is implemented
1304         # (see #28780)
1305         from .youtube import parse_qs
1306         qs = parse_qs(url)
1307         if qs.get('list', [None])[0]:
1308             return False
1309         return super(YoutubeIE, cls).suitable(url)
1310
1311     def __init__(self, *args, **kwargs):
1312         super(YoutubeIE, self).__init__(*args, **kwargs)
1313         self._code_cache = {}
1314         self._player_cache = {}
1315
1316     def _signature_cache_id(self, example_sig):
1317         """ Return a string representation of a signature """
1318         return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
1319
1320     @classmethod
1321     def _extract_player_info(cls, player_url):
1322         for player_re in cls._PLAYER_INFO_RE:
1323             id_m = re.search(player_re, player_url)
1324             if id_m:
1325                 break
1326         else:
1327             raise ExtractorError('Cannot identify player %r' % player_url)
1328         return id_m.group('id')
1329
1330     def _get_player_code(self, video_id, player_url, player_id=None):
1331         if not player_id:
1332             player_id = self._extract_player_info(player_url)
1333
1334         if player_id not in self._code_cache:
1335             self._code_cache[player_id] = self._download_webpage(
1336                 player_url, video_id,
1337                 note='Downloading player ' + player_id,
1338                 errnote='Download of %s failed' % player_url)
1339         return self._code_cache[player_id]
1340
1341     def _extract_signature_function(self, video_id, player_url, example_sig):
1342         player_id = self._extract_player_info(player_url)
1343
1344         # Read from filesystem cache
1345         func_id = 'js_%s_%s' % (
1346             player_id, self._signature_cache_id(example_sig))
1347         assert os.path.basename(func_id) == func_id
1348
1349         cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
1350         if cache_spec is not None:
1351             return lambda s: ''.join(s[i] for i in cache_spec)
1352
1353         code = self._get_player_code(video_id, player_url, player_id)
1354         res = self._parse_sig_js(code)
1355
1356         test_string = ''.join(map(compat_chr, range(len(example_sig))))
1357         cache_res = res(test_string)
1358         cache_spec = [ord(c) for c in cache_res]
1359
1360         self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
1361         return res
1362
1363     def _print_sig_code(self, func, example_sig):
1364         def gen_sig_code(idxs):
1365             def _genslice(start, end, step):
1366                 starts = '' if start == 0 else str(start)
1367                 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
1368                 steps = '' if step == 1 else (':%d' % step)
1369                 return 's[%s%s%s]' % (starts, ends, steps)
1370
1371             step = None
1372             # Quelch pyflakes warnings - start will be set when step is set
1373             start = '(Never used)'
1374             for i, prev in zip(idxs[1:], idxs[:-1]):
1375                 if step is not None:
1376                     if i - prev == step:
1377                         continue
1378                     yield _genslice(start, prev, step)
1379                     step = None
1380                     continue
1381                 if i - prev in [-1, 1]:
1382                     step = i - prev
1383                     start = prev
1384                     continue
1385                 else:
1386                     yield 's[%d]' % prev
1387             if step is None:
1388                 yield 's[%d]' % i
1389             else:
1390                 yield _genslice(start, i, step)
1391
1392         test_string = ''.join(map(compat_chr, range(len(example_sig))))
1393         cache_res = func(test_string)
1394         cache_spec = [ord(c) for c in cache_res]
1395         expr_code = ' + '.join(gen_sig_code(cache_spec))
1396         signature_id_tuple = '(%s)' % (
1397             ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
1398         code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
1399                 '    return %s\n') % (signature_id_tuple, expr_code)
1400         self.to_screen('Extracted signature function:\n' + code)
1401
1402     def _parse_sig_js(self, jscode):
1403         funcname = self._search_regex(
1404             (r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1405              r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1406              r'\bm=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)',
1407              r'\bc&&\(c=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(c\)\)',
1408              r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2,})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\);[a-zA-Z0-9$]{2}\.[a-zA-Z0-9$]{2}\(a,\d+\)',
1409              r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2,})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
1410              r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
1411              # Obsolete patterns
1412              r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1413              r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
1414              r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1415              r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1416              r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1417              r'\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1418              r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1419              r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\('),
1420             jscode, 'Initial JS player signature function name', group='sig')
1421
1422         jsi = JSInterpreter(jscode)
1423         initial_function = jsi.extract_function(funcname)
1424         return lambda s: initial_function([s])
1425
1426     def _decrypt_signature(self, s, video_id, player_url):
1427         """Turn the encrypted s field into a working signature"""
1428
1429         if player_url is None:
1430             raise ExtractorError('Cannot decrypt signature without player_url')
1431
1432         try:
1433             player_id = (player_url, self._signature_cache_id(s))
1434             if player_id not in self._player_cache:
1435                 func = self._extract_signature_function(
1436                     video_id, player_url, s
1437                 )
1438                 self._player_cache[player_id] = func
1439             func = self._player_cache[player_id]
1440             if self._downloader.params.get('youtube_print_sig_code'):
1441                 self._print_sig_code(func, s)
1442             return func(s)
1443         except Exception as e:
1444             tb = traceback.format_exc()
1445             raise ExtractorError(
1446                 'Signature extraction failed: ' + tb, cause=e)
1447
1448     def _extract_player_url(self, webpage):
1449         player_url = self._search_regex(
1450             r'"(?:PLAYER_JS_URL|jsUrl)"\s*:\s*"([^"]+)"',
1451             webpage or '', 'player URL', fatal=False)
1452         if not player_url:
1453             return
1454         if player_url.startswith('//'):
1455             player_url = 'https:' + player_url
1456         elif not re.match(r'https?://', player_url):
1457             player_url = compat_urlparse.urljoin(
1458                 'https://www.youtube.com', player_url)
1459         return player_url
1460
1461     # from yt-dlp
1462     # See also:
1463     # 1. https://github.com/ytdl-org/youtube-dl/issues/29326#issuecomment-894619419
1464     # 2. https://code.videolan.org/videolan/vlc/-/blob/4fb284e5af69aa9ac2100ccbdd3b88debec9987f/share/lua/playlist/youtube.lua#L116
1465     # 3. https://github.com/ytdl-org/youtube-dl/issues/30097#issuecomment-950157377
1466     def _extract_n_function_name(self, jscode):
1467         target = r'(?P<nfunc>[a-zA-Z_$][\w$]*)(?:\[(?P<idx>\d+)\])?'
1468         nfunc_and_idx = self._search_regex(
1469             r'\.get\("n"\)\)&&\(b=(%s)\([\w$]+\)' % (target, ),
1470             jscode, 'Initial JS player n function name')
1471         nfunc, idx = re.match(target, nfunc_and_idx).group('nfunc', 'idx')
1472         if not idx:
1473             return nfunc
1474         return self._parse_json(self._search_regex(
1475             r'var %s\s*=\s*(\[.+?\]);' % (re.escape(nfunc), ), jscode,
1476             'Initial JS player n function list ({nfunc}[{idx}])'.format(**locals())), nfunc, transform_source=js_to_json)[int(idx)]
1477
1478     def _extract_n_function(self, video_id, player_url):
1479         player_id = self._extract_player_info(player_url)
1480         func_code = self._downloader.cache.load('youtube-nsig', player_id)
1481
1482         if func_code:
1483             jsi = JSInterpreter(func_code)
1484         else:
1485             player_id = self._extract_player_info(player_url)
1486             jscode = self._get_player_code(video_id, player_url, player_id)
1487             funcname = self._extract_n_function_name(jscode)
1488             jsi = JSInterpreter(jscode)
1489             func_code = jsi.extract_function_code(funcname)
1490             self._downloader.cache.store('youtube-nsig', player_id, func_code)
1491
1492         if self._downloader.params.get('youtube_print_sig_code'):
1493             self.to_screen('Extracted nsig function from {0}:\n{1}\n'.format(player_id, func_code[1]))
1494
1495         return lambda s: jsi.extract_function_from_code(*func_code)([s])
1496
1497     def _n_descramble(self, n_param, player_url, video_id):
1498         """Compute the response to YT's "n" parameter challenge
1499
1500         Args:
1501         n_param     -- challenge string that is the value of the
1502                        URL's "n" query parameter
1503         player_url  -- URL of YT player JS
1504         video_id
1505         """
1506
1507         sig_id = ('nsig_value', n_param)
1508         if sig_id in self._player_cache:
1509             return self._player_cache[sig_id]
1510
1511         try:
1512             player_id = ('nsig', player_url)
1513             if player_id not in self._player_cache:
1514                 self._player_cache[player_id] = self._extract_n_function(video_id, player_url)
1515             func = self._player_cache[player_id]
1516             self._player_cache[sig_id] = func(n_param)
1517             if self._downloader.params.get('verbose', False):
1518                 self._downloader.to_screen('[debug] [%s] %s' % (self.IE_NAME, 'Decrypted nsig {0} => {1}'.format(n_param, self._player_cache[sig_id])))
1519             return self._player_cache[sig_id]
1520         except Exception as e:
1521             self._downloader.report_warning(
1522                 '[%s] %s (%s %s)' % (
1523                     self.IE_NAME,
1524                     'Unable to decode n-parameter: download likely to be throttled',
1525                     error_to_compat_str(e),
1526                     traceback.format_exc()))
1527
1528     def _unthrottle_format_urls(self, video_id, player_url, formats):
1529         for fmt in formats:
1530             parsed_fmt_url = compat_urlparse.urlparse(fmt['url'])
1531             qs = compat_urlparse.parse_qs(parsed_fmt_url.query)
1532             n_param = qs.get('n')
1533             if not n_param:
1534                 continue
1535             n_param = n_param[-1]
1536             n_response = self._n_descramble(n_param, player_url, video_id)
1537             if n_response:
1538                 qs['n'] = [n_response]
1539                 fmt['url'] = compat_urlparse.urlunparse(
1540                     parsed_fmt_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
1541
1542     def _mark_watched(self, video_id, player_response):
1543         playback_url = url_or_none(try_get(
1544             player_response,
1545             lambda x: x['playbackTracking']['videostatsPlaybackUrl']['baseUrl']))
1546         if not playback_url:
1547             return
1548         parsed_playback_url = compat_urlparse.urlparse(playback_url)
1549         qs = compat_urlparse.parse_qs(parsed_playback_url.query)
1550
1551         # cpn generation algorithm is reverse engineered from base.js.
1552         # In fact it works even with dummy cpn.
1553         CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
1554         cpn = ''.join((CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16)))
1555
1556         qs.update({
1557             'ver': ['2'],
1558             'cpn': [cpn],
1559         })
1560         playback_url = compat_urlparse.urlunparse(
1561             parsed_playback_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
1562
1563         self._download_webpage(
1564             playback_url, video_id, 'Marking watched',
1565             'Unable to mark watched', fatal=False)
1566
1567     @staticmethod
1568     def _extract_urls(webpage):
1569         # Embedded YouTube player
1570         entries = [
1571             unescapeHTML(mobj.group('url'))
1572             for mobj in re.finditer(r'''(?x)
1573             (?:
1574                 <iframe[^>]+?src=|
1575                 data-video-url=|
1576                 <embed[^>]+?src=|
1577                 embedSWF\(?:\s*|
1578                 <object[^>]+data=|
1579                 new\s+SWFObject\(
1580             )
1581             (["\'])
1582                 (?P<url>(?:https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/
1583                 (?:embed|v|p)/[0-9A-Za-z_-]{11}.*?)
1584             \1''', webpage)]
1585
1586         # lazyYT YouTube embed
1587         entries.extend(list(map(
1588             unescapeHTML,
1589             re.findall(r'class="lazyYT" data-youtube-id="([^"]+)"', webpage))))
1590
1591         # Wordpress "YouTube Video Importer" plugin
1592         matches = re.findall(r'''(?x)<div[^>]+
1593             class=(?P<q1>[\'"])[^\'"]*\byvii_single_video_player\b[^\'"]*(?P=q1)[^>]+
1594             data-video_id=(?P<q2>[\'"])([^\'"]+)(?P=q2)''', webpage)
1595         entries.extend(m[-1] for m in matches)
1596
1597         return entries
1598
1599     @staticmethod
1600     def _extract_url(webpage):
1601         urls = YoutubeIE._extract_urls(webpage)
1602         return urls[0] if urls else None
1603
1604     @classmethod
1605     def extract_id(cls, url):
1606         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
1607         if mobj is None:
1608             raise ExtractorError('Invalid URL: %s' % url)
1609         video_id = mobj.group(2)
1610         return video_id
1611
1612     def _extract_chapters_from_json(self, data, video_id, duration):
1613         chapters_list = try_get(
1614             data,
1615             lambda x: x['playerOverlays']
1616                        ['playerOverlayRenderer']
1617                        ['decoratedPlayerBarRenderer']
1618                        ['decoratedPlayerBarRenderer']
1619                        ['playerBar']
1620                        ['chapteredPlayerBarRenderer']
1621                        ['chapters'],
1622             list)
1623         if not chapters_list:
1624             return
1625
1626         def chapter_time(chapter):
1627             return float_or_none(
1628                 try_get(
1629                     chapter,
1630                     lambda x: x['chapterRenderer']['timeRangeStartMillis'],
1631                     int),
1632                 scale=1000)
1633         chapters = []
1634         for next_num, chapter in enumerate(chapters_list, start=1):
1635             start_time = chapter_time(chapter)
1636             if start_time is None:
1637                 continue
1638             end_time = (chapter_time(chapters_list[next_num])
1639                         if next_num < len(chapters_list) else duration)
1640             if end_time is None:
1641                 continue
1642             title = try_get(
1643                 chapter, lambda x: x['chapterRenderer']['title']['simpleText'],
1644                 compat_str)
1645             chapters.append({
1646                 'start_time': start_time,
1647                 'end_time': end_time,
1648                 'title': title,
1649             })
1650         return chapters
1651
1652     def _extract_yt_initial_variable(self, webpage, regex, video_id, name):
1653         return self._parse_json(self._search_regex(
1654             (r'%s\s*%s' % (regex, self._YT_INITIAL_BOUNDARY_RE),
1655              regex), webpage, name, default='{}'), video_id, fatal=False)
1656
1657     def _real_extract(self, url):
1658         url, smuggled_data = unsmuggle_url(url, {})
1659         video_id = self._match_id(url)
1660         base_url = self.http_scheme() + '//www.youtube.com/'
1661         webpage_url = base_url + 'watch?v=' + video_id
1662         webpage = self._download_webpage(
1663             webpage_url + '&bpctr=9999999999&has_verified=1', video_id, fatal=False)
1664
1665         player_response = None
1666         if webpage:
1667             player_response = self._extract_yt_initial_variable(
1668                 webpage, self._YT_INITIAL_PLAYER_RESPONSE_RE,
1669                 video_id, 'initial player response')
1670         if not player_response:
1671             player_response = self._call_api(
1672                 'player', {'videoId': video_id}, video_id)
1673
1674         playability_status = player_response.get('playabilityStatus') or {}
1675         if playability_status.get('reason') == 'Sign in to confirm your age':
1676             video_info = self._download_webpage(
1677                 base_url + 'get_video_info', video_id,
1678                 'Refetching age-gated info webpage',
1679                 'unable to download video info webpage', query={
1680                     'video_id': video_id,
1681                     'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1682                     'html5': 1,
1683                     # See https://github.com/ytdl-org/youtube-dl/issues/29333#issuecomment-864049544
1684                     'c': 'TVHTML5',
1685                     'cver': '6.20180913',
1686                 }, fatal=False)
1687             if video_info:
1688                 pr = self._parse_json(
1689                     try_get(
1690                         compat_parse_qs(video_info),
1691                         lambda x: x['player_response'][0], compat_str) or '{}',
1692                     video_id, fatal=False)
1693                 if pr and isinstance(pr, dict):
1694                     player_response = pr
1695
1696         trailer_video_id = try_get(
1697             playability_status,
1698             lambda x: x['errorScreen']['playerLegacyDesktopYpcTrailerRenderer']['trailerVideoId'],
1699             compat_str)
1700         if trailer_video_id:
1701             return self.url_result(
1702                 trailer_video_id, self.ie_key(), trailer_video_id)
1703
1704         def get_text(x):
1705             if not x:
1706                 return
1707             text = x.get('simpleText')
1708             if text and isinstance(text, compat_str):
1709                 return text
1710             runs = x.get('runs')
1711             if not isinstance(runs, list):
1712                 return
1713             return ''.join([r['text'] for r in runs if isinstance(r.get('text'), compat_str)])
1714
1715         search_meta = (
1716             lambda x: self._html_search_meta(x, webpage, default=None)) \
1717             if webpage else lambda x: None
1718
1719         video_details = player_response.get('videoDetails') or {}
1720         microformat = try_get(
1721             player_response,
1722             lambda x: x['microformat']['playerMicroformatRenderer'],
1723             dict) or {}
1724         video_title = video_details.get('title') \
1725             or get_text(microformat.get('title')) \
1726             or search_meta(['og:title', 'twitter:title', 'title'])
1727         video_description = video_details.get('shortDescription')
1728
1729         if not smuggled_data.get('force_singlefeed', False):
1730             if not self._downloader.params.get('noplaylist'):
1731                 multifeed_metadata_list = try_get(
1732                     player_response,
1733                     lambda x: x['multicamera']['playerLegacyMulticameraRenderer']['metadataList'],
1734                     compat_str)
1735                 if multifeed_metadata_list:
1736                     entries = []
1737                     feed_ids = []
1738                     for feed in multifeed_metadata_list.split(','):
1739                         # Unquote should take place before split on comma (,) since textual
1740                         # fields may contain comma as well (see
1741                         # https://github.com/ytdl-org/youtube-dl/issues/8536)
1742                         feed_data = compat_parse_qs(
1743                             compat_urllib_parse_unquote_plus(feed))
1744
1745                         def feed_entry(name):
1746                             return try_get(
1747                                 feed_data, lambda x: x[name][0], compat_str)
1748
1749                         feed_id = feed_entry('id')
1750                         if not feed_id:
1751                             continue
1752                         feed_title = feed_entry('title')
1753                         title = video_title
1754                         if feed_title:
1755                             title += ' (%s)' % feed_title
1756                         entries.append({
1757                             '_type': 'url_transparent',
1758                             'ie_key': 'Youtube',
1759                             'url': smuggle_url(
1760                                 base_url + 'watch?v=' + feed_data['id'][0],
1761                                 {'force_singlefeed': True}),
1762                             'title': title,
1763                         })
1764                         feed_ids.append(feed_id)
1765                     self.to_screen(
1766                         'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
1767                         % (', '.join(feed_ids), video_id))
1768                     return self.playlist_result(
1769                         entries, video_id, video_title, video_description)
1770             else:
1771                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1772
1773         formats = []
1774         itags = []
1775         itag_qualities = {}
1776         player_url = None
1777         q = qualities(['tiny', 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'])
1778         streaming_data = player_response.get('streamingData') or {}
1779         streaming_formats = streaming_data.get('formats') or []
1780         streaming_formats.extend(streaming_data.get('adaptiveFormats') or [])
1781         for fmt in streaming_formats:
1782             if fmt.get('targetDurationSec') or fmt.get('drmFamilies'):
1783                 continue
1784
1785             itag = str_or_none(fmt.get('itag'))
1786             quality = fmt.get('quality')
1787             if itag and quality:
1788                 itag_qualities[itag] = quality
1789             # FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
1790             # (adding `&sq=0` to the URL) and parsing emsg box to determine the
1791             # number of fragment that would subsequently requested with (`&sq=N`)
1792             if fmt.get('type') == 'FORMAT_STREAM_TYPE_OTF':
1793                 continue
1794
1795             fmt_url = fmt.get('url')
1796             if not fmt_url:
1797                 sc = compat_parse_qs(fmt.get('signatureCipher'))
1798                 fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0]))
1799                 encrypted_sig = try_get(sc, lambda x: x['s'][0])
1800                 if not (sc and fmt_url and encrypted_sig):
1801                     continue
1802                 if not player_url:
1803                     player_url = self._extract_player_url(webpage)
1804                 if not player_url:
1805                     continue
1806                 signature = self._decrypt_signature(sc['s'][0], video_id, player_url)
1807                 sp = try_get(sc, lambda x: x['sp'][0]) or 'signature'
1808                 fmt_url += '&' + sp + '=' + signature
1809
1810             if itag:
1811                 itags.append(itag)
1812             tbr = float_or_none(
1813                 fmt.get('averageBitrate') or fmt.get('bitrate'), 1000)
1814             dct = {
1815                 'asr': int_or_none(fmt.get('audioSampleRate')),
1816                 'filesize': int_or_none(fmt.get('contentLength')),
1817                 'format_id': itag,
1818                 'format_note': fmt.get('qualityLabel') or quality,
1819                 'fps': int_or_none(fmt.get('fps')),
1820                 'height': int_or_none(fmt.get('height')),
1821                 'quality': q(quality),
1822                 'tbr': tbr,
1823                 'url': fmt_url,
1824                 'width': fmt.get('width'),
1825             }
1826             mimetype = fmt.get('mimeType')
1827             if mimetype:
1828                 mobj = re.match(
1829                     r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^"]+)")?', mimetype)
1830                 if mobj:
1831                     dct['ext'] = mimetype2ext(mobj.group(1))
1832                     dct.update(parse_codecs(mobj.group(2)))
1833             no_audio = dct.get('acodec') == 'none'
1834             no_video = dct.get('vcodec') == 'none'
1835             if no_audio:
1836                 dct['vbr'] = tbr
1837             if no_video:
1838                 dct['abr'] = tbr
1839             if no_audio or no_video:
1840                 dct['downloader_options'] = {
1841                     # Youtube throttles chunks >~10M
1842                     'http_chunk_size': 10485760,
1843                 }
1844                 if dct.get('ext'):
1845                     dct['container'] = dct['ext'] + '_dash'
1846             formats.append(dct)
1847
1848         hls_manifest_url = streaming_data.get('hlsManifestUrl')
1849         if hls_manifest_url:
1850             for f in self._extract_m3u8_formats(
1851                     hls_manifest_url, video_id, 'mp4', fatal=False):
1852                 itag = self._search_regex(
1853                     r'/itag/(\d+)', f['url'], 'itag', default=None)
1854                 if itag:
1855                     f['format_id'] = itag
1856                 formats.append(f)
1857
1858         if self._downloader.params.get('youtube_include_dash_manifest', True):
1859             dash_manifest_url = streaming_data.get('dashManifestUrl')
1860             if dash_manifest_url:
1861                 for f in self._extract_mpd_formats(
1862                         dash_manifest_url, video_id, fatal=False):
1863                     itag = f['format_id']
1864                     if itag in itags:
1865                         continue
1866                     if itag in itag_qualities:
1867                         f['quality'] = q(itag_qualities[itag])
1868                     filesize = int_or_none(self._search_regex(
1869                         r'/clen/(\d+)', f.get('fragment_base_url')
1870                         or f['url'], 'file size', default=None))
1871                     if filesize:
1872                         f['filesize'] = filesize
1873                     formats.append(f)
1874
1875         if not formats:
1876             if streaming_data.get('licenseInfos'):
1877                 raise ExtractorError(
1878                     'This video is DRM protected.', expected=True)
1879             pemr = try_get(
1880                 playability_status,
1881                 lambda x: x['errorScreen']['playerErrorMessageRenderer'],
1882                 dict) or {}
1883             reason = get_text(pemr.get('reason')) or playability_status.get('reason')
1884             subreason = pemr.get('subreason')
1885             if subreason:
1886                 subreason = clean_html(get_text(subreason))
1887                 if subreason == 'The uploader has not made this video available in your country.':
1888                     countries = microformat.get('availableCountries')
1889                     if not countries:
1890                         regions_allowed = search_meta('regionsAllowed')
1891                         countries = regions_allowed.split(',') if regions_allowed else None
1892                     self.raise_geo_restricted(
1893                         subreason, countries)
1894                 reason += '\n' + subreason
1895             if reason:
1896                 raise ExtractorError(reason, expected=True)
1897
1898         self._sort_formats(formats)
1899
1900         keywords = video_details.get('keywords') or []
1901         if not keywords and webpage:
1902             keywords = [
1903                 unescapeHTML(m.group('content'))
1904                 for m in re.finditer(self._meta_regex('og:video:tag'), webpage)]
1905         for keyword in keywords:
1906             if keyword.startswith('yt:stretch='):
1907                 mobj = re.search(r'(\d+)\s*:\s*(\d+)', keyword)
1908                 if mobj:
1909                     # NB: float is intentional for forcing float division
1910                     w, h = (float(v) for v in mobj.groups())
1911                     if w > 0 and h > 0:
1912                         ratio = w / h
1913                         for f in formats:
1914                             if f.get('vcodec') != 'none':
1915                                 f['stretched_ratio'] = ratio
1916                         break
1917
1918         thumbnails = []
1919         for container in (video_details, microformat):
1920             for thumbnail in (try_get(
1921                     container,
1922                     lambda x: x['thumbnail']['thumbnails'], list) or []):
1923                 thumbnail_url = thumbnail.get('url')
1924                 if not thumbnail_url:
1925                     continue
1926                 thumbnails.append({
1927                     'height': int_or_none(thumbnail.get('height')),
1928                     'url': thumbnail_url,
1929                     'width': int_or_none(thumbnail.get('width')),
1930                 })
1931             if thumbnails:
1932                 break
1933         else:
1934             thumbnail = search_meta(['og:image', 'twitter:image'])
1935             if thumbnail:
1936                 thumbnails = [{'url': thumbnail}]
1937
1938         category = microformat.get('category') or search_meta('genre')
1939         channel_id = video_details.get('channelId') \
1940             or microformat.get('externalChannelId') \
1941             or search_meta('channelId')
1942         duration = int_or_none(
1943             video_details.get('lengthSeconds')
1944             or microformat.get('lengthSeconds')) \
1945             or parse_duration(search_meta('duration'))
1946         is_live = video_details.get('isLive')
1947         owner_profile_url = microformat.get('ownerProfileUrl')
1948
1949         if not player_url:
1950             player_url = self._extract_player_url(webpage)
1951         self._unthrottle_format_urls(video_id, player_url, formats)
1952
1953         info = {
1954             'id': video_id,
1955             'title': self._live_title(video_title) if is_live else video_title,
1956             'formats': formats,
1957             'thumbnails': thumbnails,
1958             'description': video_description,
1959             'upload_date': unified_strdate(
1960                 microformat.get('uploadDate')
1961                 or search_meta('uploadDate')),
1962             'uploader': video_details['author'],
1963             'uploader_id': self._search_regex(r'/(?:channel|user)/([^/?&#]+)', owner_profile_url, 'uploader id') if owner_profile_url else None,
1964             'uploader_url': owner_profile_url,
1965             'channel_id': channel_id,
1966             'channel_url': 'https://www.youtube.com/channel/' + channel_id if channel_id else None,
1967             'duration': duration,
1968             'view_count': int_or_none(
1969                 video_details.get('viewCount')
1970                 or microformat.get('viewCount')
1971                 or search_meta('interactionCount')),
1972             'average_rating': float_or_none(video_details.get('averageRating')),
1973             'age_limit': 18 if (
1974                 microformat.get('isFamilySafe') is False
1975                 or search_meta('isFamilyFriendly') == 'false'
1976                 or search_meta('og:restrictions:age') == '18+') else 0,
1977             'webpage_url': webpage_url,
1978             'categories': [category] if category else None,
1979             'tags': keywords,
1980             'is_live': is_live,
1981         }
1982
1983         pctr = try_get(
1984             player_response,
1985             lambda x: x['captions']['playerCaptionsTracklistRenderer'], dict)
1986         if pctr:
1987             def process_language(container, base_url, lang_code, query):
1988                 lang_subs = []
1989                 for fmt in self._SUBTITLE_FORMATS:
1990                     query.update({
1991                         'fmt': fmt,
1992                     })
1993                     lang_subs.append({
1994                         'ext': fmt,
1995                         'url': update_url_query(base_url, query),
1996                     })
1997                 container[lang_code] = lang_subs
1998
1999             subtitles = {}
2000             for caption_track in (pctr.get('captionTracks') or []):
2001                 base_url = caption_track.get('baseUrl')
2002                 if not base_url:
2003                     continue
2004                 if caption_track.get('kind') != 'asr':
2005                     lang_code = caption_track.get('languageCode')
2006                     if not lang_code:
2007                         continue
2008                     process_language(
2009                         subtitles, base_url, lang_code, {})
2010                     continue
2011                 automatic_captions = {}
2012                 for translation_language in (pctr.get('translationLanguages') or []):
2013                     translation_language_code = translation_language.get('languageCode')
2014                     if not translation_language_code:
2015                         continue
2016                     process_language(
2017                         automatic_captions, base_url, translation_language_code,
2018                         {'tlang': translation_language_code})
2019                 info['automatic_captions'] = automatic_captions
2020             info['subtitles'] = subtitles
2021
2022         parsed_url = compat_urllib_parse_urlparse(url)
2023         for component in [parsed_url.fragment, parsed_url.query]:
2024             query = compat_parse_qs(component)
2025             for k, v in query.items():
2026                 for d_k, s_ks in [('start', ('start', 't')), ('end', ('end',))]:
2027                     d_k += '_time'
2028                     if d_k not in info and k in s_ks:
2029                         info[d_k] = parse_duration(query[k][0])
2030
2031         if video_description:
2032             mobj = re.search(r'(?s)(?P<track>[^·\n]+)·(?P<artist>[^\n]+)\n+(?P<album>[^\n]+)(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?(.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?.+\nAuto-generated by YouTube\.\s*$', video_description)
2033             if mobj:
2034                 release_year = mobj.group('release_year')
2035                 release_date = mobj.group('release_date')
2036                 if release_date:
2037                     release_date = release_date.replace('-', '')
2038                     if not release_year:
2039                         release_year = release_date[:4]
2040                 info.update({
2041                     'album': mobj.group('album'.strip()),
2042                     'artist': mobj.group('clean_artist') or ', '.join(a.strip() for a in mobj.group('artist').split('·')),
2043                     'track': mobj.group('track').strip(),
2044                     'release_date': release_date,
2045                     'release_year': int_or_none(release_year),
2046                 })
2047
2048         initial_data = None
2049         if webpage:
2050             initial_data = self._extract_yt_initial_variable(
2051                 webpage, self._YT_INITIAL_DATA_RE, video_id,
2052                 'yt initial data')
2053         if not initial_data:
2054             initial_data = self._call_api(
2055                 'next', {'videoId': video_id}, video_id, fatal=False)
2056
2057         if initial_data:
2058             chapters = self._extract_chapters_from_json(
2059                 initial_data, video_id, duration)
2060             if not chapters:
2061                 for engagment_pannel in (initial_data.get('engagementPanels') or []):
2062                     contents = try_get(
2063                         engagment_pannel, lambda x: x['engagementPanelSectionListRenderer']['content']['macroMarkersListRenderer']['contents'],
2064                         list)
2065                     if not contents:
2066                         continue
2067
2068                     def chapter_time(mmlir):
2069                         return parse_duration(
2070                             get_text(mmlir.get('timeDescription')))
2071
2072                     chapters = []
2073                     for next_num, content in enumerate(contents, start=1):
2074                         mmlir = content.get('macroMarkersListItemRenderer') or {}
2075                         start_time = chapter_time(mmlir)
2076                         end_time = chapter_time(try_get(
2077                             contents, lambda x: x[next_num]['macroMarkersListItemRenderer'])) \
2078                             if next_num < len(contents) else duration
2079                         if start_time is None or end_time is None:
2080                             continue
2081                         chapters.append({
2082                             'start_time': start_time,
2083                             'end_time': end_time,
2084                             'title': get_text(mmlir.get('title')),
2085                         })
2086                     if chapters:
2087                         break
2088             if chapters:
2089                 info['chapters'] = chapters
2090
2091             contents = try_get(
2092                 initial_data,
2093                 lambda x: x['contents']['twoColumnWatchNextResults']['results']['results']['contents'],
2094                 list) or []
2095             for content in contents:
2096                 vpir = content.get('videoPrimaryInfoRenderer')
2097                 if vpir:
2098                     stl = vpir.get('superTitleLink')
2099                     if stl:
2100                         stl = get_text(stl)
2101                         if try_get(
2102                                 vpir,
2103                                 lambda x: x['superTitleIcon']['iconType']) == 'LOCATION_PIN':
2104                             info['location'] = stl
2105                         else:
2106                             mobj = re.search(r'(.+?)\s*S(\d+)\s*•\s*E(\d+)', stl)
2107                             if mobj:
2108                                 info.update({
2109                                     'series': mobj.group(1),
2110                                     'season_number': int(mobj.group(2)),
2111                                     'episode_number': int(mobj.group(3)),
2112                                 })
2113                     for tlb in (try_get(
2114                             vpir,
2115                             lambda x: x['videoActions']['menuRenderer']['topLevelButtons'],
2116                             list) or []):
2117                         tbr = tlb.get('toggleButtonRenderer') or {}
2118                         for getter, regex in [(
2119                                 lambda x: x['defaultText']['accessibility']['accessibilityData'],
2120                                 r'(?P<count>[\d,]+)\s*(?P<type>(?:dis)?like)'), ([
2121                                     lambda x: x['accessibility'],
2122                                     lambda x: x['accessibilityData']['accessibilityData'],
2123                                 ], r'(?P<type>(?:dis)?like) this video along with (?P<count>[\d,]+) other people')]:
2124                             label = (try_get(tbr, getter, dict) or {}).get('label')
2125                             if label:
2126                                 mobj = re.match(regex, label)
2127                                 if mobj:
2128                                     info[mobj.group('type') + '_count'] = str_to_int(mobj.group('count'))
2129                                     break
2130                     sbr_tooltip = try_get(
2131                         vpir, lambda x: x['sentimentBar']['sentimentBarRenderer']['tooltip'])
2132                     if sbr_tooltip:
2133                         like_count, dislike_count = sbr_tooltip.split(' / ')
2134                         info.update({
2135                             'like_count': str_to_int(like_count),
2136                             'dislike_count': str_to_int(dislike_count),
2137                         })
2138                 vsir = content.get('videoSecondaryInfoRenderer')
2139                 if vsir:
2140                     info['channel'] = get_text(try_get(
2141                         vsir,
2142                         lambda x: x['owner']['videoOwnerRenderer']['title'],
2143                         dict))
2144                     rows = try_get(
2145                         vsir,
2146                         lambda x: x['metadataRowContainer']['metadataRowContainerRenderer']['rows'],
2147                         list) or []
2148                     multiple_songs = False
2149                     for row in rows:
2150                         if try_get(row, lambda x: x['metadataRowRenderer']['hasDividerLine']) is True:
2151                             multiple_songs = True
2152                             break
2153                     for row in rows:
2154                         mrr = row.get('metadataRowRenderer') or {}
2155                         mrr_title = mrr.get('title')
2156                         if not mrr_title:
2157                             continue
2158                         mrr_title = get_text(mrr['title'])
2159                         mrr_contents_text = get_text(mrr['contents'][0])
2160                         if mrr_title == 'License':
2161                             info['license'] = mrr_contents_text
2162                         elif not multiple_songs:
2163                             if mrr_title == 'Album':
2164                                 info['album'] = mrr_contents_text
2165                             elif mrr_title == 'Artist':
2166                                 info['artist'] = mrr_contents_text
2167                             elif mrr_title == 'Song':
2168                                 info['track'] = mrr_contents_text
2169
2170         for s_k, d_k in [('artist', 'creator'), ('track', 'alt_title')]:
2171             v = info.get(s_k)
2172             if v:
2173                 info[d_k] = v
2174
2175         self.mark_watched(video_id, player_response)
2176
2177         return info
2178
2179
2180 class YoutubeTabIE(YoutubeBaseInfoExtractor):
2181     IE_DESC = 'YouTube.com tab'
2182     _VALID_URL = r'''(?x)
2183                     https?://
2184                         (?:\w+\.)?
2185                         (?:
2186                             youtube(?:kids)?\.com|
2187                             invidio\.us
2188                         )/
2189                         (?:
2190                             (?:channel|c|user|feed|hashtag)/|
2191                             (?:playlist|watch)\?.*?\blist=|
2192                             (?!(?:watch|embed|v|e|results)\b)
2193                         )
2194                         (?P<id>[^/?\#&]+)
2195                     '''
2196     IE_NAME = 'youtube:tab'
2197
2198     _TESTS = [{
2199         # playlists, multipage
2200         'url': 'https://www.youtube.com/c/ИгорьКлейнер/playlists?view=1&flow=grid',
2201         'playlist_mincount': 94,
2202         'info_dict': {
2203             'id': 'UCqj7Cz7revf5maW9g5pgNcg',
2204             'title': 'Игорь Клейнер - Playlists',
2205             'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
2206         },
2207     }, {
2208         # playlists, multipage, different order
2209         'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
2210         'playlist_mincount': 94,
2211         'info_dict': {
2212             'id': 'UCqj7Cz7revf5maW9g5pgNcg',
2213             'title': 'Игорь Клейнер - Playlists',
2214             'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
2215         },
2216     }, {
2217         # playlists, series
2218         'url': 'https://www.youtube.com/c/3blue1brown/playlists?view=50&sort=dd&shelf_id=3',
2219         'playlist_mincount': 5,
2220         'info_dict': {
2221             'id': 'UCYO_jab_esuFRV4b17AJtAw',
2222             'title': '3Blue1Brown - Playlists',
2223             'description': 'md5:e1384e8a133307dd10edee76e875d62f',
2224         },
2225     }, {
2226         # playlists, singlepage
2227         'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
2228         'playlist_mincount': 4,
2229         'info_dict': {
2230             'id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
2231             'title': 'ThirstForScience - Playlists',
2232             'description': 'md5:609399d937ea957b0f53cbffb747a14c',
2233         }
2234     }, {
2235         'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
2236         'only_matching': True,
2237     }, {
2238         # basic, single video playlist
2239         'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2240         'info_dict': {
2241             'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
2242             'uploader': 'Sergey M.',
2243             'id': 'PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2244             'title': 'youtube-dl public playlist',
2245         },
2246         'playlist_count': 1,
2247     }, {
2248         # empty playlist
2249         'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
2250         'info_dict': {
2251             'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
2252             'uploader': 'Sergey M.',
2253             'id': 'PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
2254             'title': 'youtube-dl empty playlist',
2255         },
2256         'playlist_count': 0,
2257     }, {
2258         # Home tab
2259         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/featured',
2260         'info_dict': {
2261             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2262             'title': 'lex will - Home',
2263             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2264         },
2265         'playlist_mincount': 2,
2266     }, {
2267         # Videos tab
2268         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos',
2269         'info_dict': {
2270             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2271             'title': 'lex will - Videos',
2272             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2273         },
2274         'playlist_mincount': 975,
2275     }, {
2276         # Videos tab, sorted by popular
2277         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos?view=0&sort=p&flow=grid',
2278         'info_dict': {
2279             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2280             'title': 'lex will - Videos',
2281             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2282         },
2283         'playlist_mincount': 199,
2284     }, {
2285         # Playlists tab
2286         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/playlists',
2287         'info_dict': {
2288             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2289             'title': 'lex will - Playlists',
2290             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2291         },
2292         'playlist_mincount': 17,
2293     }, {
2294         # Community tab
2295         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/community',
2296         'info_dict': {
2297             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2298             'title': 'lex will - Community',
2299             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2300         },
2301         'playlist_mincount': 18,
2302     }, {
2303         # Channels tab
2304         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/channels',
2305         'info_dict': {
2306             'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2307             'title': 'lex will - Channels',
2308             'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2309         },
2310         'playlist_mincount': 138,
2311     }, {
2312         'url': 'https://invidio.us/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2313         'only_matching': True,
2314     }, {
2315         'url': 'https://www.youtubekids.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2316         'only_matching': True,
2317     }, {
2318         'url': 'https://music.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2319         'only_matching': True,
2320     }, {
2321         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
2322         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2323         'info_dict': {
2324             'title': '29C3: Not my department',
2325             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2326             'uploader': 'Christiaan008',
2327             'uploader_id': 'UCEPzS1rYsrkqzSLNp76nrcg',
2328         },
2329         'playlist_count': 96,
2330     }, {
2331         'note': 'Large playlist',
2332         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
2333         'info_dict': {
2334             'title': 'Uploads from Cauchemar',
2335             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
2336             'uploader': 'Cauchemar',
2337             'uploader_id': 'UCBABnxM4Ar9ten8Mdjj1j0Q',
2338         },
2339         'playlist_mincount': 1123,
2340     }, {
2341         # even larger playlist, 8832 videos
2342         'url': 'http://www.youtube.com/user/NASAgovVideo/videos',
2343         'only_matching': True,
2344     }, {
2345         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
2346         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
2347         'info_dict': {
2348             'title': 'Uploads from Interstellar Movie',
2349             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
2350             'uploader': 'Interstellar Movie',
2351             'uploader_id': 'UCXw-G3eDE9trcvY2sBMM_aA',
2352         },
2353         'playlist_mincount': 21,
2354     }, {
2355         # https://github.com/ytdl-org/youtube-dl/issues/21844
2356         'url': 'https://www.youtube.com/playlist?list=PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
2357         'info_dict': {
2358             'title': 'Data Analysis with Dr Mike Pound',
2359             'id': 'PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
2360             'uploader_id': 'UC9-y-6csu5WGm29I7JiwpnA',
2361             'uploader': 'Computerphile',
2362         },
2363         'playlist_mincount': 11,
2364     }, {
2365         'url': 'https://invidio.us/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2366         'only_matching': True,
2367     }, {
2368         # Playlist URL that does not actually serve a playlist
2369         'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
2370         'info_dict': {
2371             'id': 'FqZTN594JQw',
2372             'ext': 'webm',
2373             'title': "Smiley's People 01 detective, Adventure Series, Action",
2374             'uploader': 'STREEM',
2375             'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
2376             'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
2377             'upload_date': '20150526',
2378             'license': 'Standard YouTube License',
2379             'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
2380             'categories': ['People & Blogs'],
2381             'tags': list,
2382             'view_count': int,
2383             'like_count': int,
2384             'dislike_count': int,
2385         },
2386         'params': {
2387             'skip_download': True,
2388         },
2389         'skip': 'This video is not available.',
2390         'add_ie': [YoutubeIE.ie_key()],
2391     }, {
2392         'url': 'https://www.youtubekids.com/watch?v=Agk7R8I8o5U&list=PUZ6jURNr1WQZCNHF0ao-c0g',
2393         'only_matching': True,
2394     }, {
2395         'url': 'https://www.youtube.com/watch?v=MuAGGZNfUkU&list=RDMM',
2396         'only_matching': True,
2397     }, {
2398         'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
2399         'info_dict': {
2400             'id': '9Auq9mYxFEE',
2401             'ext': 'mp4',
2402             'title': 'Watch Sky News live',
2403             'uploader': 'Sky News',
2404             'uploader_id': 'skynews',
2405             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/skynews',
2406             'upload_date': '20191102',
2407             'description': 'md5:78de4e1c2359d0ea3ed829678e38b662',
2408             'categories': ['News & Politics'],
2409             'tags': list,
2410             'like_count': int,
2411             'dislike_count': int,
2412         },
2413         'params': {
2414             'skip_download': True,
2415         },
2416     }, {
2417         'url': 'https://www.youtube.com/user/TheYoungTurks/live',
2418         'info_dict': {
2419             'id': 'a48o2S1cPoo',
2420             'ext': 'mp4',
2421             'title': 'The Young Turks - Live Main Show',
2422             'uploader': 'The Young Turks',
2423             'uploader_id': 'TheYoungTurks',
2424             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
2425             'upload_date': '20150715',
2426             'license': 'Standard YouTube License',
2427             'description': 'md5:438179573adcdff3c97ebb1ee632b891',
2428             'categories': ['News & Politics'],
2429             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
2430             'like_count': int,
2431             'dislike_count': int,
2432         },
2433         'params': {
2434             'skip_download': True,
2435         },
2436         'only_matching': True,
2437     }, {
2438         'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
2439         'only_matching': True,
2440     }, {
2441         'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
2442         'only_matching': True,
2443     }, {
2444         'url': 'https://www.youtube.com/feed/trending',
2445         'only_matching': True,
2446     }, {
2447         # needs auth
2448         'url': 'https://www.youtube.com/feed/library',
2449         'only_matching': True,
2450     }, {
2451         # needs auth
2452         'url': 'https://www.youtube.com/feed/history',
2453         'only_matching': True,
2454     }, {
2455         # needs auth
2456         'url': 'https://www.youtube.com/feed/subscriptions',
2457         'only_matching': True,
2458     }, {
2459         # needs auth
2460         'url': 'https://www.youtube.com/feed/watch_later',
2461         'only_matching': True,
2462     }, {
2463         # no longer available?
2464         'url': 'https://www.youtube.com/feed/recommended',
2465         'only_matching': True,
2466     }, {
2467         # inline playlist with not always working continuations
2468         'url': 'https://www.youtube.com/watch?v=UC6u0Tct-Fo&list=PL36D642111D65BE7C',
2469         'only_matching': True,
2470     }, {
2471         'url': 'https://www.youtube.com/course?list=ECUl4u3cNGP61MdtwGTqZA0MreSaDybji8',
2472         'only_matching': True,
2473     }, {
2474         'url': 'https://www.youtube.com/course',
2475         'only_matching': True,
2476     }, {
2477         'url': 'https://www.youtube.com/zsecurity',
2478         'only_matching': True,
2479     }, {
2480         'url': 'http://www.youtube.com/NASAgovVideo/videos',
2481         'only_matching': True,
2482     }, {
2483         'url': 'https://www.youtube.com/TheYoungTurks/live',
2484         'only_matching': True,
2485     }, {
2486         'url': 'https://www.youtube.com/hashtag/cctv9',
2487         'info_dict': {
2488             'id': 'cctv9',
2489             'title': '#cctv9',
2490         },
2491         'playlist_mincount': 350,
2492     }, {
2493         'url': 'https://www.youtube.com/watch?list=PLW4dVinRY435CBE_JD3t-0SRXKfnZHS1P&feature=youtu.be&v=M9cJMXmQ_ZU',
2494         'only_matching': True,
2495     }, {
2496         'note': 'Search tab',
2497         'url': 'https://www.youtube.com/c/3blue1brown/search?query=linear%20algebra',
2498         'playlist_mincount': 40,
2499         'info_dict': {
2500             'id': 'UCYO_jab_esuFRV4b17AJtAw',
2501             'title': '3Blue1Brown - Search - linear algebra',
2502             'description': 'md5:e1384e8a133307dd10edee76e875d62f',
2503             'uploader': '3Blue1Brown',
2504             'uploader_id': 'UCYO_jab_esuFRV4b17AJtAw',
2505         }
2506     }]
2507
2508     @classmethod
2509     def suitable(cls, url):
2510         return False if YoutubeIE.suitable(url) else super(
2511             YoutubeTabIE, cls).suitable(url)
2512
2513     def _extract_channel_id(self, webpage):
2514         channel_id = self._html_search_meta(
2515             'channelId', webpage, 'channel id', default=None)
2516         if channel_id:
2517             return channel_id
2518         channel_url = self._html_search_meta(
2519             ('og:url', 'al:ios:url', 'al:android:url', 'al:web:url',
2520              'twitter:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad',
2521              'twitter:app:url:googleplay'), webpage, 'channel url')
2522         return self._search_regex(
2523             r'https?://(?:www\.)?youtube\.com/channel/([^/?#&])+',
2524             channel_url, 'channel id')
2525
2526     @staticmethod
2527     def _extract_grid_item_renderer(item):
2528         assert isinstance(item, dict)
2529         for key, renderer in item.items():
2530             if not key.startswith('grid') or not key.endswith('Renderer'):
2531                 continue
2532             if not isinstance(renderer, dict):
2533                 continue
2534             return renderer
2535
2536     def _grid_entries(self, grid_renderer):
2537         for item in grid_renderer['items']:
2538             if not isinstance(item, dict):
2539                 continue
2540             renderer = self._extract_grid_item_renderer(item)
2541             if not isinstance(renderer, dict):
2542                 continue
2543             title = try_get(
2544                 renderer, (lambda x: x['title']['runs'][0]['text'],
2545                            lambda x: x['title']['simpleText']), compat_str)
2546             # playlist
2547             playlist_id = renderer.get('playlistId')
2548             if playlist_id:
2549                 yield self.url_result(
2550                     'https://www.youtube.com/playlist?list=%s' % playlist_id,
2551                     ie=YoutubeTabIE.ie_key(), video_id=playlist_id,
2552                     video_title=title)
2553                 continue
2554             # video
2555             video_id = renderer.get('videoId')
2556             if video_id:
2557                 yield self._extract_video(renderer)
2558                 continue
2559             # channel
2560             channel_id = renderer.get('channelId')
2561             if channel_id:
2562                 title = try_get(
2563                     renderer, lambda x: x['title']['simpleText'], compat_str)
2564                 yield self.url_result(
2565                     'https://www.youtube.com/channel/%s' % channel_id,
2566                     ie=YoutubeTabIE.ie_key(), video_title=title)
2567                 continue
2568             # generic endpoint URL support
2569             ep_url = urljoin('https://www.youtube.com/', try_get(
2570                 renderer, lambda x: x['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
2571                 compat_str))
2572             if ep_url:
2573                 for ie in (YoutubeTabIE, YoutubePlaylistIE, YoutubeIE):
2574                     if ie.suitable(ep_url):
2575                         yield self.url_result(
2576                             ep_url, ie=ie.ie_key(), video_id=ie._match_id(ep_url), video_title=title)
2577                         break
2578
2579     def _shelf_entries_from_content(self, shelf_renderer):
2580         content = shelf_renderer.get('content')
2581         if not isinstance(content, dict):
2582             return
2583         renderer = content.get('gridRenderer')
2584         if renderer:
2585             # TODO: add support for nested playlists so each shelf is processed
2586             # as separate playlist
2587             # TODO: this includes only first N items
2588             for entry in self._grid_entries(renderer):
2589                 yield entry
2590         renderer = content.get('horizontalListRenderer')
2591         if renderer:
2592             # TODO
2593             pass
2594
2595     def _shelf_entries(self, shelf_renderer, skip_channels=False):
2596         ep = try_get(
2597             shelf_renderer, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'],
2598             compat_str)
2599         shelf_url = urljoin('https://www.youtube.com', ep)
2600         if shelf_url:
2601             # Skipping links to another channels, note that checking for
2602             # endpoint.commandMetadata.webCommandMetadata.webPageTypwebPageType == WEB_PAGE_TYPE_CHANNEL
2603             # will not work
2604             if skip_channels and '/channels?' in shelf_url:
2605                 return
2606             title = try_get(
2607                 shelf_renderer, lambda x: x['title']['runs'][0]['text'], compat_str)
2608             yield self.url_result(shelf_url, video_title=title)
2609         # Shelf may not contain shelf URL, fallback to extraction from content
2610         for entry in self._shelf_entries_from_content(shelf_renderer):
2611             yield entry
2612
2613     def _playlist_entries(self, video_list_renderer):
2614         for content in video_list_renderer['contents']:
2615             if not isinstance(content, dict):
2616                 continue
2617             renderer = content.get('playlistVideoRenderer') or content.get('playlistPanelVideoRenderer')
2618             if not isinstance(renderer, dict):
2619                 continue
2620             video_id = renderer.get('videoId')
2621             if not video_id:
2622                 continue
2623             yield self._extract_video(renderer)
2624
2625     def _video_entry(self, video_renderer):
2626         video_id = video_renderer.get('videoId')
2627         if video_id:
2628             return self._extract_video(video_renderer)
2629
2630     def _post_thread_entries(self, post_thread_renderer):
2631         post_renderer = try_get(
2632             post_thread_renderer, lambda x: x['post']['backstagePostRenderer'], dict)
2633         if not post_renderer:
2634             return
2635         # video attachment
2636         video_renderer = try_get(
2637             post_renderer, lambda x: x['backstageAttachment']['videoRenderer'], dict)
2638         video_id = None
2639         if video_renderer:
2640             entry = self._video_entry(video_renderer)
2641             if entry:
2642                 yield entry
2643         # inline video links
2644         runs = try_get(post_renderer, lambda x: x['contentText']['runs'], list) or []
2645         for run in runs:
2646             if not isinstance(run, dict):
2647                 continue
2648             ep_url = try_get(
2649                 run, lambda x: x['navigationEndpoint']['urlEndpoint']['url'], compat_str)
2650             if not ep_url:
2651                 continue
2652             if not YoutubeIE.suitable(ep_url):
2653                 continue
2654             ep_video_id = YoutubeIE._match_id(ep_url)
2655             if video_id == ep_video_id:
2656                 continue
2657             yield self.url_result(ep_url, ie=YoutubeIE.ie_key(), video_id=video_id)
2658
2659     def _post_thread_continuation_entries(self, post_thread_continuation):
2660         contents = post_thread_continuation.get('contents')
2661         if not isinstance(contents, list):
2662             return
2663         for content in contents:
2664             renderer = content.get('backstagePostThreadRenderer')
2665             if not isinstance(renderer, dict):
2666                 continue
2667             for entry in self._post_thread_entries(renderer):
2668                 yield entry
2669
2670     def _rich_grid_entries(self, contents):
2671         for content in contents:
2672             video_renderer = try_get(content, lambda x: x['richItemRenderer']['content']['videoRenderer'], dict)
2673             if video_renderer:
2674                 entry = self._video_entry(video_renderer)
2675                 if entry:
2676                     yield entry
2677
2678     @staticmethod
2679     def _build_continuation_query(continuation, ctp=None):
2680         query = {
2681             'ctoken': continuation,
2682             'continuation': continuation,
2683         }
2684         if ctp:
2685             query['itct'] = ctp
2686         return query
2687
2688     @staticmethod
2689     def _extract_next_continuation_data(renderer):
2690         next_continuation = try_get(
2691             renderer, lambda x: x['continuations'][0]['nextContinuationData'], dict)
2692         if not next_continuation:
2693             return
2694         continuation = next_continuation.get('continuation')
2695         if not continuation:
2696             return
2697         ctp = next_continuation.get('clickTrackingParams')
2698         return YoutubeTabIE._build_continuation_query(continuation, ctp)
2699
2700     @classmethod
2701     def _extract_continuation(cls, renderer):
2702         next_continuation = cls._extract_next_continuation_data(renderer)
2703         if next_continuation:
2704             return next_continuation
2705         contents = []
2706         for key in ('contents', 'items'):
2707             contents.extend(try_get(renderer, lambda x: x[key], list) or [])
2708         for content in contents:
2709             if not isinstance(content, dict):
2710                 continue
2711             continuation_ep = try_get(
2712                 content, lambda x: x['continuationItemRenderer']['continuationEndpoint'],
2713                 dict)
2714             if not continuation_ep:
2715                 continue
2716             continuation = try_get(
2717                 continuation_ep, lambda x: x['continuationCommand']['token'], compat_str)
2718             if not continuation:
2719                 continue
2720             ctp = continuation_ep.get('clickTrackingParams')
2721             return YoutubeTabIE._build_continuation_query(continuation, ctp)
2722
2723     def _entries(self, tab, item_id, webpage):
2724         tab_content = try_get(tab, lambda x: x['content'], dict)
2725         if not tab_content:
2726             return
2727         slr_renderer = try_get(tab_content, lambda x: x['sectionListRenderer'], dict)
2728         if slr_renderer:
2729             is_channels_tab = tab.get('title') == 'Channels'
2730             continuation = None
2731             slr_contents = try_get(slr_renderer, lambda x: x['contents'], list) or []
2732             for slr_content in slr_contents:
2733                 if not isinstance(slr_content, dict):
2734                     continue
2735                 is_renderer = try_get(slr_content, lambda x: x['itemSectionRenderer'], dict)
2736                 if not is_renderer:
2737                     continue
2738                 isr_contents = try_get(is_renderer, lambda x: x['contents'], list) or []
2739                 for isr_content in isr_contents:
2740                     if not isinstance(isr_content, dict):
2741                         continue
2742                     renderer = isr_content.get('playlistVideoListRenderer')
2743                     if renderer:
2744                         for entry in self._playlist_entries(renderer):
2745                             yield entry
2746                         continuation = self._extract_continuation(renderer)
2747                         continue
2748                     renderer = isr_content.get('gridRenderer')
2749                     if renderer:
2750                         for entry in self._grid_entries(renderer):
2751                             yield entry
2752                         continuation = self._extract_continuation(renderer)
2753                         continue
2754                     renderer = isr_content.get('shelfRenderer')
2755                     if renderer:
2756                         for entry in self._shelf_entries(renderer, not is_channels_tab):
2757                             yield entry
2758                         continue
2759                     renderer = isr_content.get('backstagePostThreadRenderer')
2760                     if renderer:
2761                         for entry in self._post_thread_entries(renderer):
2762                             yield entry
2763                         continuation = self._extract_continuation(renderer)
2764                         continue
2765                     renderer = isr_content.get('videoRenderer')
2766                     if renderer:
2767                         entry = self._video_entry(renderer)
2768                         if entry:
2769                             yield entry
2770
2771                 if not continuation:
2772                     continuation = self._extract_continuation(is_renderer)
2773             if not continuation:
2774                 continuation = self._extract_continuation(slr_renderer)
2775         else:
2776             rich_grid_renderer = tab_content.get('richGridRenderer')
2777             if not rich_grid_renderer:
2778                 return
2779             for entry in self._rich_grid_entries(rich_grid_renderer.get('contents') or []):
2780                 yield entry
2781             continuation = self._extract_continuation(rich_grid_renderer)
2782
2783         ytcfg = self._extract_ytcfg(item_id, webpage)
2784         client_version = try_get(
2785             ytcfg, lambda x: x['INNERTUBE_CLIENT_VERSION'], compat_str) or '2.20210407.08.00'
2786
2787         headers = {
2788             'x-youtube-client-name': '1',
2789             'x-youtube-client-version': client_version,
2790             'content-type': 'application/json',
2791         }
2792
2793         context = try_get(ytcfg, lambda x: x['INNERTUBE_CONTEXT'], dict) or {
2794             'client': {
2795                 'clientName': 'WEB',
2796                 'clientVersion': client_version,
2797             }
2798         }
2799         visitor_data = try_get(context, lambda x: x['client']['visitorData'], compat_str)
2800
2801         identity_token = self._extract_identity_token(ytcfg, webpage)
2802         if identity_token:
2803             headers['x-youtube-identity-token'] = identity_token
2804
2805         data = {
2806             'context': context,
2807         }
2808
2809         for page_num in itertools.count(1):
2810             if not continuation:
2811                 break
2812             if visitor_data:
2813                 headers['x-goog-visitor-id'] = visitor_data
2814             data['continuation'] = continuation['continuation']
2815             data['clickTracking'] = {
2816                 'clickTrackingParams': continuation['itct']
2817             }
2818             count = 0
2819             retries = 3
2820             while count <= retries:
2821                 try:
2822                     # Downloading page may result in intermittent 5xx HTTP error
2823                     # that is usually worked around with a retry
2824                     response = self._download_json(
2825                         'https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
2826                         None, 'Downloading page %d%s' % (page_num, ' (retry #%d)' % count if count else ''),
2827                         headers=headers, data=json.dumps(data).encode('utf8'))
2828                     break
2829                 except ExtractorError as e:
2830                     if isinstance(e.cause, compat_HTTPError) and e.cause.code in (500, 503):
2831                         count += 1
2832                         if count <= retries:
2833                             continue
2834                     raise
2835             if not response:
2836                 break
2837
2838             visitor_data = try_get(
2839                 response, lambda x: x['responseContext']['visitorData'], compat_str) or visitor_data
2840
2841             continuation_contents = try_get(
2842                 response, lambda x: x['continuationContents'], dict)
2843             if continuation_contents:
2844                 continuation_renderer = continuation_contents.get('playlistVideoListContinuation')
2845                 if continuation_renderer:
2846                     for entry in self._playlist_entries(continuation_renderer):
2847                         yield entry
2848                     continuation = self._extract_continuation(continuation_renderer)
2849                     continue
2850                 continuation_renderer = continuation_contents.get('gridContinuation')
2851                 if continuation_renderer:
2852                     for entry in self._grid_entries(continuation_renderer):
2853                         yield entry
2854                     continuation = self._extract_continuation(continuation_renderer)
2855                     continue
2856                 continuation_renderer = continuation_contents.get('itemSectionContinuation')
2857                 if continuation_renderer:
2858                     for entry in self._post_thread_continuation_entries(continuation_renderer):
2859                         yield entry
2860                     continuation = self._extract_continuation(continuation_renderer)
2861                     continue
2862
2863             on_response_received = dict_get(response, ('onResponseReceivedActions', 'onResponseReceivedEndpoints'))
2864             continuation_items = try_get(
2865                 on_response_received, lambda x: x[0]['appendContinuationItemsAction']['continuationItems'], list)
2866             if continuation_items:
2867                 continuation_item = continuation_items[0]
2868                 if not isinstance(continuation_item, dict):
2869                     continue
2870                 renderer = self._extract_grid_item_renderer(continuation_item)
2871                 if renderer:
2872                     grid_renderer = {'items': continuation_items}
2873                     for entry in self._grid_entries(grid_renderer):
2874                         yield entry
2875                     continuation = self._extract_continuation(grid_renderer)
2876                     continue
2877                 renderer = continuation_item.get('playlistVideoRenderer') or continuation_item.get('itemSectionRenderer')
2878                 if renderer:
2879                     video_list_renderer = {'contents': continuation_items}
2880                     for entry in self._playlist_entries(video_list_renderer):
2881                         yield entry
2882                     continuation = self._extract_continuation(video_list_renderer)
2883                     continue
2884                 renderer = continuation_item.get('backstagePostThreadRenderer')
2885                 if renderer:
2886                     continuation_renderer = {'contents': continuation_items}
2887                     for entry in self._post_thread_continuation_entries(continuation_renderer):
2888                         yield entry
2889                     continuation = self._extract_continuation(continuation_renderer)
2890                     continue
2891                 renderer = continuation_item.get('richItemRenderer')
2892                 if renderer:
2893                     for entry in self._rich_grid_entries(continuation_items):
2894                         yield entry
2895                     continuation = self._extract_continuation({'contents': continuation_items})
2896                     continue
2897
2898             break
2899
2900     @staticmethod
2901     def _extract_selected_tab(tabs):
2902         for tab in tabs:
2903             renderer = dict_get(tab, ('tabRenderer', 'expandableTabRenderer')) or {}
2904             if renderer.get('selected') is True:
2905                 return renderer
2906         else:
2907             raise ExtractorError('Unable to find selected tab')
2908
2909     @staticmethod
2910     def _extract_uploader(data):
2911         uploader = {}
2912         sidebar_renderer = try_get(
2913             data, lambda x: x['sidebar']['playlistSidebarRenderer']['items'], list)
2914         if sidebar_renderer:
2915             for item in sidebar_renderer:
2916                 if not isinstance(item, dict):
2917                     continue
2918                 renderer = item.get('playlistSidebarSecondaryInfoRenderer')
2919                 if not isinstance(renderer, dict):
2920                     continue
2921                 owner = try_get(
2922                     renderer, lambda x: x['videoOwner']['videoOwnerRenderer']['title']['runs'][0], dict)
2923                 if owner:
2924                     uploader['uploader'] = owner.get('text')
2925                     uploader['uploader_id'] = try_get(
2926                         owner, lambda x: x['navigationEndpoint']['browseEndpoint']['browseId'], compat_str)
2927                     uploader['uploader_url'] = urljoin(
2928                         'https://www.youtube.com/',
2929                         try_get(owner, lambda x: x['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str))
2930         return uploader
2931
2932     @staticmethod
2933     def _extract_alert(data):
2934         alerts = []
2935         for alert in try_get(data, lambda x: x['alerts'], list) or []:
2936             if not isinstance(alert, dict):
2937                 continue
2938             alert_text = try_get(
2939                 alert, lambda x: x['alertRenderer']['text'], dict)
2940             if not alert_text:
2941                 continue
2942             text = try_get(
2943                 alert_text,
2944                 (lambda x: x['simpleText'], lambda x: x['runs'][0]['text']),
2945                 compat_str)
2946             if text:
2947                 alerts.append(text)
2948         return '\n'.join(alerts)
2949
2950     def _extract_from_tabs(self, item_id, webpage, data, tabs):
2951         selected_tab = self._extract_selected_tab(tabs)
2952         renderer = try_get(
2953             data, lambda x: x['metadata']['channelMetadataRenderer'], dict)
2954         playlist_id = item_id
2955         title = description = None
2956         if renderer:
2957             channel_title = renderer.get('title') or item_id
2958             tab_title = selected_tab.get('title')
2959             title = channel_title or item_id
2960             if tab_title:
2961                 title += ' - %s' % tab_title
2962             if selected_tab.get('expandedText'):
2963                 title += ' - %s' % selected_tab['expandedText']
2964             description = renderer.get('description')
2965             playlist_id = renderer.get('externalId')
2966         else:
2967             renderer = try_get(
2968                 data, lambda x: x['metadata']['playlistMetadataRenderer'], dict)
2969             if renderer:
2970                 title = renderer.get('title')
2971             else:
2972                 renderer = try_get(
2973                     data, lambda x: x['header']['hashtagHeaderRenderer'], dict)
2974                 if renderer:
2975                     title = try_get(renderer, lambda x: x['hashtag']['simpleText'])
2976         playlist = self.playlist_result(
2977             self._entries(selected_tab, item_id, webpage),
2978             playlist_id=playlist_id, playlist_title=title,
2979             playlist_description=description)
2980         playlist.update(self._extract_uploader(data))
2981         return playlist
2982
2983     def _extract_from_playlist(self, item_id, url, data, playlist):
2984         title = playlist.get('title') or try_get(
2985             data, lambda x: x['titleText']['simpleText'], compat_str)
2986         playlist_id = playlist.get('playlistId') or item_id
2987         # Inline playlist rendition continuation does not always work
2988         # at Youtube side, so delegating regular tab-based playlist URL
2989         # processing whenever possible.
2990         playlist_url = urljoin(url, try_get(
2991             playlist, lambda x: x['endpoint']['commandMetadata']['webCommandMetadata']['url'],
2992             compat_str))
2993         if playlist_url and playlist_url != url:
2994             return self.url_result(
2995                 playlist_url, ie=YoutubeTabIE.ie_key(), video_id=playlist_id,
2996                 video_title=title)
2997         return self.playlist_result(
2998             self._playlist_entries(playlist), playlist_id=playlist_id,
2999             playlist_title=title)
3000
3001     def _extract_identity_token(self, ytcfg, webpage):
3002         if ytcfg:
3003             token = try_get(ytcfg, lambda x: x['ID_TOKEN'], compat_str)
3004             if token:
3005                 return token
3006         return self._search_regex(
3007             r'\bID_TOKEN["\']\s*:\s*["\'](.+?)["\']', webpage,
3008             'identity token', default=None)
3009
3010     def _real_extract(self, url):
3011         item_id = self._match_id(url)
3012         url = compat_urlparse.urlunparse(
3013             compat_urlparse.urlparse(url)._replace(netloc='www.youtube.com'))
3014         # Handle both video/playlist URLs
3015         qs = parse_qs(url)
3016         video_id = qs.get('v', [None])[0]
3017         playlist_id = qs.get('list', [None])[0]
3018         if video_id and playlist_id:
3019             if self._downloader.params.get('noplaylist'):
3020                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
3021                 return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
3022             self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
3023         webpage = self._download_webpage(url, item_id)
3024         data = self._extract_yt_initial_data(item_id, webpage)
3025         tabs = try_get(
3026             data, lambda x: x['contents']['twoColumnBrowseResultsRenderer']['tabs'], list)
3027         if tabs:
3028             return self._extract_from_tabs(item_id, webpage, data, tabs)
3029         playlist = try_get(
3030             data, lambda x: x['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
3031         if playlist:
3032             return self._extract_from_playlist(item_id, url, data, playlist)
3033         # Fallback to video extraction if no playlist alike page is recognized.
3034         # First check for the current video then try the v attribute of URL query.
3035         video_id = try_get(
3036             data, lambda x: x['currentVideoEndpoint']['watchEndpoint']['videoId'],
3037             compat_str) or video_id
3038         if video_id:
3039             return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
3040         # Capture and output alerts
3041         alert = self._extract_alert(data)
3042         if alert:
3043             raise ExtractorError(alert, expected=True)
3044         # Failed to recognize
3045         raise ExtractorError('Unable to recognize tab page')
3046
3047
3048 class YoutubePlaylistIE(InfoExtractor):
3049     IE_DESC = 'YouTube.com playlists'
3050     _VALID_URL = r'''(?x)(?:
3051                         (?:https?://)?
3052                         (?:\w+\.)?
3053                         (?:
3054                             (?:
3055                                 youtube(?:kids)?\.com|
3056                                 invidio\.us
3057                             )
3058                             /.*?\?.*?\blist=
3059                         )?
3060                         (?P<id>%(playlist_id)s)
3061                      )''' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
3062     IE_NAME = 'youtube:playlist'
3063     _TESTS = [{
3064         'note': 'issue #673',
3065         'url': 'PLBB231211A4F62143',
3066         'info_dict': {
3067             'title': '[OLD]Team Fortress 2 (Class-based LP)',
3068             'id': 'PLBB231211A4F62143',
3069             'uploader': 'Wickydoo',
3070             'uploader_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
3071         },
3072         'playlist_mincount': 29,
3073     }, {
3074         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
3075         'info_dict': {
3076             'title': 'YDL_safe_search',
3077             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
3078         },
3079         'playlist_count': 2,
3080         'skip': 'This playlist is private',
3081     }, {
3082         'note': 'embedded',
3083         'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
3084         'playlist_count': 4,
3085         'info_dict': {
3086             'title': 'JODA15',
3087             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
3088             'uploader': 'milan',
3089             'uploader_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
3090         }
3091     }, {
3092         'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
3093         'playlist_mincount': 982,
3094         'info_dict': {
3095             'title': '2018 Chinese New Singles (11/6 updated)',
3096             'id': 'PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
3097             'uploader': 'LBK',
3098             'uploader_id': 'UC21nz3_MesPLqtDqwdvnoxA',
3099         }
3100     }, {
3101         'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
3102         'only_matching': True,
3103     }, {
3104         # music album playlist
3105         'url': 'OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM',
3106         'only_matching': True,
3107     }]
3108
3109     @classmethod
3110     def suitable(cls, url):
3111         if YoutubeTabIE.suitable(url):
3112             return False
3113         # Hack for lazy extractors until more generic solution is implemented
3114         # (see #28780)
3115         from .youtube import parse_qs
3116         qs = parse_qs(url)
3117         if qs.get('v', [None])[0]:
3118             return False
3119         return super(YoutubePlaylistIE, cls).suitable(url)
3120
3121     def _real_extract(self, url):
3122         playlist_id = self._match_id(url)
3123         qs = parse_qs(url)
3124         if not qs:
3125             qs = {'list': playlist_id}
3126         return self.url_result(
3127             update_url_query('https://www.youtube.com/playlist', qs),
3128             ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
3129
3130
3131 class YoutubeYtBeIE(InfoExtractor):
3132     _VALID_URL = r'https?://youtu\.be/(?P<id>[0-9A-Za-z_-]{11})/*?.*?\blist=(?P<playlist_id>%(playlist_id)s)' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
3133     _TESTS = [{
3134         'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
3135         'info_dict': {
3136             'id': 'yeWKywCrFtk',
3137             'ext': 'mp4',
3138             'title': 'Small Scale Baler and Braiding Rugs',
3139             'uploader': 'Backus-Page House Museum',
3140             'uploader_id': 'backuspagemuseum',
3141             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
3142             'upload_date': '20161008',
3143             'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
3144             'categories': ['Nonprofits & Activism'],
3145             'tags': list,
3146             'like_count': int,
3147             'dislike_count': int,
3148         },
3149         'params': {
3150             'noplaylist': True,
3151             'skip_download': True,
3152         },
3153     }, {
3154         'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
3155         'only_matching': True,
3156     }]
3157
3158     def _real_extract(self, url):
3159         mobj = re.match(self._VALID_URL, url)
3160         video_id = mobj.group('id')
3161         playlist_id = mobj.group('playlist_id')
3162         return self.url_result(
3163             update_url_query('https://www.youtube.com/watch', {
3164                 'v': video_id,
3165                 'list': playlist_id,
3166                 'feature': 'youtu.be',
3167             }), ie=YoutubeTabIE.ie_key(), video_id=playlist_id)
3168
3169
3170 class YoutubeYtUserIE(InfoExtractor):
3171     _VALID_URL = r'ytuser:(?P<id>.+)'
3172     _TESTS = [{
3173         'url': 'ytuser:phihag',
3174         'only_matching': True,
3175     }]
3176
3177     def _real_extract(self, url):
3178         user_id = self._match_id(url)
3179         return self.url_result(
3180             'https://www.youtube.com/user/%s' % user_id,
3181             ie=YoutubeTabIE.ie_key(), video_id=user_id)
3182
3183
3184 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
3185     IE_NAME = 'youtube:favorites'
3186     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
3187     _VALID_URL = r'https?://(?:www\.)?youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
3188     _LOGIN_REQUIRED = True
3189     _TESTS = [{
3190         'url': ':ytfav',
3191         'only_matching': True,
3192     }, {
3193         'url': ':ytfavorites',
3194         'only_matching': True,
3195     }]
3196
3197     def _real_extract(self, url):
3198         return self.url_result(
3199             'https://www.youtube.com/playlist?list=LL',
3200             ie=YoutubeTabIE.ie_key())
3201
3202
3203 class YoutubeSearchIE(SearchInfoExtractor, YoutubeBaseInfoExtractor):
3204     IE_DESC = 'YouTube.com searches'
3205     IE_NAME = 'youtube:search'
3206     _SEARCH_KEY = 'ytsearch'
3207     _SEARCH_PARAMS = 'EgIQAQ%3D%3D'  # Videos only
3208     _MAX_RESULTS = float('inf')
3209     _TESTS = [{
3210         'url': 'ytsearch10:youtube-dl test video',
3211         'playlist_count': 10,
3212         'info_dict': {
3213             'id': 'youtube-dl test video',
3214             'title': 'youtube-dl test video',
3215         }
3216     }]
3217
3218     def _get_n_results(self, query, n):
3219         """Get a specified number of results for a query"""
3220         entries = itertools.islice(self._search_results(query, self._SEARCH_PARAMS), 0, None if n == float('inf') else n)
3221         return self.playlist_result(entries, query, query)
3222
3223
3224 class YoutubeSearchDateIE(YoutubeSearchIE):
3225     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
3226     _SEARCH_KEY = 'ytsearchdate'
3227     IE_DESC = 'YouTube.com searches, newest videos first'
3228     _SEARCH_PARAMS = 'CAISAhAB'  # Videos only, sorted by date
3229     _TESTS = [{
3230         'url': 'ytsearchdate10:youtube-dl test video',
3231         'playlist_count': 10,
3232         'info_dict': {
3233             'id': 'youtube-dl test video',
3234             'title': 'youtube-dl test video',
3235         }
3236     }]
3237
3238
3239 class YoutubeSearchURLIE(YoutubeBaseInfoExtractor):
3240     IE_DESC = 'YouTube search URLs with sorting and filter support'
3241     IE_NAME = YoutubeSearchIE.IE_NAME + '_url'
3242     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?:[^&]+)(?:[&]|$)'
3243     _TESTS = [{
3244         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
3245         'playlist_mincount': 5,
3246         'info_dict': {
3247             'id': 'youtube-dl test video',
3248             'title': 'youtube-dl test video',
3249         },
3250         'params': {'playlistend': 5}
3251     }, {
3252         'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
3253         'only_matching': True,
3254     }]
3255
3256     def _real_extract(self, url):
3257         qs = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
3258         query = (qs.get('search_query') or qs.get('q'))[0]
3259         params = qs.get('sp', ('',))[0]
3260         return self.playlist_result(self._search_results(query, params), query, query)
3261
3262
3263 class YoutubeFeedsInfoExtractor(YoutubeTabIE):
3264     """
3265     Base class for feed extractors
3266     Subclasses must define the _FEED_NAME property.
3267     """
3268     _LOGIN_REQUIRED = True
3269
3270     @property
3271     def IE_NAME(self):
3272         return 'youtube:%s' % self._FEED_NAME
3273
3274     def _real_initialize(self):
3275         self._login()
3276
3277     def _real_extract(self, url):
3278         return self.url_result(
3279             'https://www.youtube.com/feed/%s' % self._FEED_NAME,
3280             ie=YoutubeTabIE.ie_key())
3281
3282
3283 class YoutubeWatchLaterIE(InfoExtractor):
3284     IE_NAME = 'youtube:watchlater'
3285     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
3286     _VALID_URL = r':ytwatchlater'
3287     _TESTS = [{
3288         'url': ':ytwatchlater',
3289         'only_matching': True,
3290     }]
3291
3292     def _real_extract(self, url):
3293         return self.url_result(
3294             'https://www.youtube.com/playlist?list=WL', ie=YoutubeTabIE.ie_key())
3295
3296
3297 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
3298     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
3299     _VALID_URL = r':ytrec(?:ommended)?'
3300     _FEED_NAME = 'recommended'
3301     _TESTS = [{
3302         'url': ':ytrec',
3303         'only_matching': True,
3304     }, {
3305         'url': ':ytrecommended',
3306         'only_matching': True,
3307     }]
3308
3309
3310 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
3311     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
3312     _VALID_URL = r':ytsubs(?:criptions)?'
3313     _FEED_NAME = 'subscriptions'
3314     _TESTS = [{
3315         'url': ':ytsubs',
3316         'only_matching': True,
3317     }, {
3318         'url': ':ytsubscriptions',
3319         'only_matching': True,
3320     }]
3321
3322
3323 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
3324     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
3325     _VALID_URL = r':ythistory'
3326     _FEED_NAME = 'history'
3327     _TESTS = [{
3328         'url': ':ythistory',
3329         'only_matching': True,
3330     }]
3331
3332
3333 class YoutubeTruncatedURLIE(InfoExtractor):
3334     IE_NAME = 'youtube:truncated_url'
3335     IE_DESC = False  # Do not list
3336     _VALID_URL = r'''(?x)
3337         (?:https?://)?
3338         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
3339         (?:watch\?(?:
3340             feature=[a-z_]+|
3341             annotation_id=annotation_[^&]+|
3342             x-yt-cl=[0-9]+|
3343             hl=[^&]*|
3344             t=[0-9]+
3345         )?
3346         |
3347             attribution_link\?a=[^&]+
3348         )
3349         $
3350     '''
3351
3352     _TESTS = [{
3353         'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
3354         'only_matching': True,
3355     }, {
3356         'url': 'https://www.youtube.com/watch?',
3357         'only_matching': True,
3358     }, {
3359         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
3360         'only_matching': True,
3361     }, {
3362         'url': 'https://www.youtube.com/watch?feature=foo',
3363         'only_matching': True,
3364     }, {
3365         'url': 'https://www.youtube.com/watch?hl=en-GB',
3366         'only_matching': True,
3367     }, {
3368         'url': 'https://www.youtube.com/watch?t=2372',
3369         'only_matching': True,
3370     }]
3371
3372     def _real_extract(self, url):
3373         raise ExtractorError(
3374             'Did you forget to quote the URL? Remember that & is a meta '
3375             'character in most shells, so you want to put the URL in quotes, '
3376             'like  youtube-dl '
3377             '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
3378             ' or simply  youtube-dl BaW_jenozKc  .',
3379             expected=True)
3380
3381
3382 class YoutubeTruncatedIDIE(InfoExtractor):
3383     IE_NAME = 'youtube:truncated_id'
3384     IE_DESC = False  # Do not list
3385     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
3386
3387     _TESTS = [{
3388         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
3389         'only_matching': True,
3390     }]
3391
3392     def _real_extract(self, url):
3393         video_id = self._match_id(url)
3394         raise ExtractorError(
3395             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
3396             expected=True)