[youtube] Add fallback metadata extraction from videoDetails (closes #18052)
[youtube-dl] / youtube_dl / extractor / youtube.py
1 # coding: utf-8
2
3 from __future__ import unicode_literals
4
5
6 import itertools
7 import json
8 import os.path
9 import random
10 import re
11 import time
12 import traceback
13
14 from .common import InfoExtractor, SearchInfoExtractor
15 from ..jsinterp import JSInterpreter
16 from ..swfinterp import SWFInterpreter
17 from ..compat import (
18     compat_chr,
19     compat_kwargs,
20     compat_parse_qs,
21     compat_urllib_parse_unquote,
22     compat_urllib_parse_unquote_plus,
23     compat_urllib_parse_urlencode,
24     compat_urllib_parse_urlparse,
25     compat_urlparse,
26     compat_str,
27 )
28 from ..utils import (
29     clean_html,
30     error_to_compat_str,
31     ExtractorError,
32     float_or_none,
33     get_element_by_attribute,
34     get_element_by_id,
35     int_or_none,
36     mimetype2ext,
37     orderedSet,
38     parse_codecs,
39     parse_duration,
40     qualities,
41     remove_quotes,
42     remove_start,
43     smuggle_url,
44     str_or_none,
45     str_to_int,
46     try_get,
47     unescapeHTML,
48     unified_strdate,
49     unsmuggle_url,
50     uppercase_escape,
51     urlencode_postdata,
52 )
53
54
55 class YoutubeBaseInfoExtractor(InfoExtractor):
56     """Provide base functions for Youtube extractors"""
57     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
58     _TWOFACTOR_URL = 'https://accounts.google.com/signin/challenge'
59
60     _LOOKUP_URL = 'https://accounts.google.com/_/signin/sl/lookup'
61     _CHALLENGE_URL = 'https://accounts.google.com/_/signin/sl/challenge'
62     _TFA_URL = 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
63
64     _NETRC_MACHINE = 'youtube'
65     # If True it will raise an error if no login info is provided
66     _LOGIN_REQUIRED = False
67
68     _PLAYLIST_ID_RE = r'(?:PL|LL|EC|UU|FL|RD|UL|TL|OLAK5uy_)[0-9A-Za-z-_]{10,}'
69
70     def _set_language(self):
71         self._set_cookie(
72             '.youtube.com', 'PREF', 'f1=50000000&hl=en',
73             # YouTube sets the expire time to about two months
74             expire_time=time.time() + 2 * 30 * 24 * 3600)
75
76     def _ids_to_results(self, ids):
77         return [
78             self.url_result(vid_id, 'Youtube', video_id=vid_id)
79             for vid_id in ids]
80
81     def _login(self):
82         """
83         Attempt to log in to YouTube.
84         True is returned if successful or skipped.
85         False is returned if login failed.
86
87         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
88         """
89         username, password = self._get_login_info()
90         # No authentication to be performed
91         if username is None:
92             if self._LOGIN_REQUIRED and self._downloader.params.get('cookiefile') is None:
93                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
94             return True
95
96         login_page = self._download_webpage(
97             self._LOGIN_URL, None,
98             note='Downloading login page',
99             errnote='unable to fetch login page', fatal=False)
100         if login_page is False:
101             return
102
103         login_form = self._hidden_inputs(login_page)
104
105         def req(url, f_req, note, errnote):
106             data = login_form.copy()
107             data.update({
108                 'pstMsg': 1,
109                 'checkConnection': 'youtube',
110                 'checkedDomains': 'youtube',
111                 'hl': 'en',
112                 'deviceinfo': '[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]',
113                 'f.req': json.dumps(f_req),
114                 'flowName': 'GlifWebSignIn',
115                 'flowEntry': 'ServiceLogin',
116             })
117             return self._download_json(
118                 url, None, note=note, errnote=errnote,
119                 transform_source=lambda s: re.sub(r'^[^[]*', '', s),
120                 fatal=False,
121                 data=urlencode_postdata(data), headers={
122                     'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
123                     'Google-Accounts-XSRF': 1,
124                 })
125
126         def warn(message):
127             self._downloader.report_warning(message)
128
129         lookup_req = [
130             username,
131             None, [], None, 'US', None, None, 2, False, True,
132             [
133                 None, None,
134                 [2, 1, None, 1,
135                  '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',
136                  None, [], 4],
137                 1, [None, None, []], None, None, None, True
138             ],
139             username,
140         ]
141
142         lookup_results = req(
143             self._LOOKUP_URL, lookup_req,
144             'Looking up account info', 'Unable to look up account info')
145
146         if lookup_results is False:
147             return False
148
149         user_hash = try_get(lookup_results, lambda x: x[0][2], compat_str)
150         if not user_hash:
151             warn('Unable to extract user hash')
152             return False
153
154         challenge_req = [
155             user_hash,
156             None, 1, None, [1, None, None, None, [password, None, True]],
157             [
158                 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],
159                 1, [None, None, []], None, None, None, True
160             ]]
161
162         challenge_results = req(
163             self._CHALLENGE_URL, challenge_req,
164             'Logging in', 'Unable to log in')
165
166         if challenge_results is False:
167             return
168
169         login_res = try_get(challenge_results, lambda x: x[0][5], list)
170         if login_res:
171             login_msg = try_get(login_res, lambda x: x[5], compat_str)
172             warn(
173                 'Unable to login: %s' % 'Invalid password'
174                 if login_msg == 'INCORRECT_ANSWER_ENTERED' else login_msg)
175             return False
176
177         res = try_get(challenge_results, lambda x: x[0][-1], list)
178         if not res:
179             warn('Unable to extract result entry')
180             return False
181
182         login_challenge = try_get(res, lambda x: x[0][0], list)
183         if login_challenge:
184             challenge_str = try_get(login_challenge, lambda x: x[2], compat_str)
185             if challenge_str == 'TWO_STEP_VERIFICATION':
186                 # SEND_SUCCESS - TFA code has been successfully sent to phone
187                 # QUOTA_EXCEEDED - reached the limit of TFA codes
188                 status = try_get(login_challenge, lambda x: x[5], compat_str)
189                 if status == 'QUOTA_EXCEEDED':
190                     warn('Exceeded the limit of TFA codes, try later')
191                     return False
192
193                 tl = try_get(challenge_results, lambda x: x[1][2], compat_str)
194                 if not tl:
195                     warn('Unable to extract TL')
196                     return False
197
198                 tfa_code = self._get_tfa_info('2-step verification code')
199
200                 if not tfa_code:
201                     warn(
202                         'Two-factor authentication required. Provide it either interactively or with --twofactor <code>'
203                         '(Note that only TOTP (Google Authenticator App) codes work at this time.)')
204                     return False
205
206                 tfa_code = remove_start(tfa_code, 'G-')
207
208                 tfa_req = [
209                     user_hash, None, 2, None,
210                     [
211                         9, None, None, None, None, None, None, None,
212                         [None, tfa_code, True, 2]
213                     ]]
214
215                 tfa_results = req(
216                     self._TFA_URL.format(tl), tfa_req,
217                     'Submitting TFA code', 'Unable to submit TFA code')
218
219                 if tfa_results is False:
220                     return False
221
222                 tfa_res = try_get(tfa_results, lambda x: x[0][5], list)
223                 if tfa_res:
224                     tfa_msg = try_get(tfa_res, lambda x: x[5], compat_str)
225                     warn(
226                         'Unable to finish TFA: %s' % 'Invalid TFA code'
227                         if tfa_msg == 'INCORRECT_ANSWER_ENTERED' else tfa_msg)
228                     return False
229
230                 check_cookie_url = try_get(
231                     tfa_results, lambda x: x[0][-1][2], compat_str)
232             else:
233                 CHALLENGES = {
234                     'LOGIN_CHALLENGE': "This device isn't recognized. For your security, Google wants to make sure it's really you.",
235                     'USERNAME_RECOVERY': 'Please provide additional information to aid in the recovery process.',
236                     'REAUTH': "There is something unusual about your activity. For your security, Google wants to make sure it's really you.",
237                 }
238                 challenge = CHALLENGES.get(
239                     challenge_str,
240                     '%s returned error %s.' % (self.IE_NAME, challenge_str))
241                 warn('%s\nGo to https://accounts.google.com/, login and solve a challenge.' % challenge)
242                 return False
243         else:
244             check_cookie_url = try_get(res, lambda x: x[2], compat_str)
245
246         if not check_cookie_url:
247             warn('Unable to extract CheckCookie URL')
248             return False
249
250         check_cookie_results = self._download_webpage(
251             check_cookie_url, None, 'Checking cookie', fatal=False)
252
253         if check_cookie_results is False:
254             return False
255
256         if 'https://myaccount.google.com/' not in check_cookie_results:
257             warn('Unable to log in')
258             return False
259
260         return True
261
262     def _download_webpage_handle(self, *args, **kwargs):
263         query = kwargs.get('query', {}).copy()
264         query['disable_polymer'] = 'true'
265         kwargs['query'] = query
266         return super(YoutubeBaseInfoExtractor, self)._download_webpage_handle(
267             *args, **compat_kwargs(kwargs))
268
269     def _real_initialize(self):
270         if self._downloader is None:
271             return
272         self._set_language()
273         if not self._login():
274             return
275
276
277 class YoutubeEntryListBaseInfoExtractor(YoutubeBaseInfoExtractor):
278     # Extract entries from page with "Load more" button
279     def _entries(self, page, playlist_id):
280         more_widget_html = content_html = page
281         for page_num in itertools.count(1):
282             for entry in self._process_page(content_html):
283                 yield entry
284
285             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
286             if not mobj:
287                 break
288
289             more = self._download_json(
290                 'https://youtube.com/%s' % mobj.group('more'), playlist_id,
291                 'Downloading page #%s' % page_num,
292                 transform_source=uppercase_escape)
293             content_html = more['content_html']
294             if not content_html.strip():
295                 # Some webpages show a "Load more" button but they don't
296                 # have more videos
297                 break
298             more_widget_html = more['load_more_widget_html']
299
300
301 class YoutubePlaylistBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
302     def _process_page(self, content):
303         for video_id, video_title in self.extract_videos_from_page(content):
304             yield self.url_result(video_id, 'Youtube', video_id, video_title)
305
306     def extract_videos_from_page(self, page):
307         ids_in_page = []
308         titles_in_page = []
309         for mobj in re.finditer(self._VIDEO_RE, page):
310             # The link with index 0 is not the first video of the playlist (not sure if still actual)
311             if 'index' in mobj.groupdict() and mobj.group('id') == '0':
312                 continue
313             video_id = mobj.group('id')
314             video_title = unescapeHTML(mobj.group('title'))
315             if video_title:
316                 video_title = video_title.strip()
317             try:
318                 idx = ids_in_page.index(video_id)
319                 if video_title and not titles_in_page[idx]:
320                     titles_in_page[idx] = video_title
321             except ValueError:
322                 ids_in_page.append(video_id)
323                 titles_in_page.append(video_title)
324         return zip(ids_in_page, titles_in_page)
325
326
327 class YoutubePlaylistsBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
328     def _process_page(self, content):
329         for playlist_id in orderedSet(re.findall(
330                 r'<h3[^>]+class="[^"]*yt-lockup-title[^"]*"[^>]*><a[^>]+href="/?playlist\?list=([0-9A-Za-z-_]{10,})"',
331                 content)):
332             yield self.url_result(
333                 'https://www.youtube.com/playlist?list=%s' % playlist_id, 'YoutubePlaylist')
334
335     def _real_extract(self, url):
336         playlist_id = self._match_id(url)
337         webpage = self._download_webpage(url, playlist_id)
338         title = self._og_search_title(webpage, fatal=False)
339         return self.playlist_result(self._entries(webpage, playlist_id), playlist_id, title)
340
341
342 class YoutubeIE(YoutubeBaseInfoExtractor):
343     IE_DESC = 'YouTube.com'
344     _VALID_URL = r"""(?x)^
345                      (
346                          (?:https?://|//)                                    # http(s):// or protocol-independent URL
347                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
348                             (?:www\.)?deturl\.com/www\.youtube\.com/|
349                             (?:www\.)?pwnyoutube\.com/|
350                             (?:www\.)?hooktube\.com/|
351                             (?:www\.)?yourepeat\.com/|
352                             tube\.majestyc\.net/|
353                             (?:www\.)?invidio\.us/|
354                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains
355                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
356                          (?:                                                  # the various things that can precede the ID:
357                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/
358                              |(?:                                             # or the v= param in all its forms
359                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
360                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
361                                  (?:.*?[&;])??                                # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&amp;v=V36LpHqtcDY)
362                                  v=
363                              )
364                          ))
365                          |(?:
366                             youtu\.be|                                        # just youtu.be/xxxx
367                             vid\.plus|                                        # or vid.plus/xxxx
368                             zwearz\.com/watch|                                # or zwearz.com/watch/xxxx
369                          )/
370                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
371                          )
372                      )?                                                       # all until now is optional -> you can pass the naked ID
373                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID
374                      (?!.*?\blist=
375                         (?:
376                             %(playlist_id)s|                                  # combined list/video URLs are handled by the playlist IE
377                             WL                                                # WL are handled by the watch later IE
378                         )
379                      )
380                      (?(1).+)?                                                # if we found the ID, everything can follow
381                      $""" % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
382     _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
383     _formats = {
384         '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
385         '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
386         '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
387         '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
388         '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
389         '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
390         '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
391         '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
392         # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
393         '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
394         '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
395         '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
396         '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
397         '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
398         '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
399         '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
400         '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
401         '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
402
403
404         # 3D videos
405         '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
406         '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
407         '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
408         '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
409         '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
410         '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
411         '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
412
413         # Apple HTTP Live Streaming
414         '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
415         '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
416         '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
417         '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
418         '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
419         '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
420         '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
421         '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
422
423         # DASH mp4 video
424         '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
425         '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
426         '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
427         '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
428         '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
429         '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'},  # Height can vary (https://github.com/rg3/youtube-dl/issues/4559)
430         '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
431         '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
432         '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
433         '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
434         '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
435         '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
436
437         # Dash mp4 audio
438         '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
439         '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
440         '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
441         '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
442         '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
443         '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
444         '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
445
446         # Dash webm
447         '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
448         '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
449         '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
450         '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
451         '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
452         '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
453         '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
454         '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
455         '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
456         '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
457         '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
458         '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
459         '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
460         '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
461         '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
462         # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
463         '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
464         '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
465         '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
466         '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
467         '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
468         '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
469
470         # Dash webm audio
471         '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
472         '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
473
474         # Dash webm audio with opus inside
475         '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
476         '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
477         '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
478
479         # RTMP (unnamed)
480         '_rtmp': {'protocol': 'rtmp'},
481     }
482     _SUBTITLE_FORMATS = ('ttml', 'vtt')
483
484     _GEO_BYPASS = False
485
486     IE_NAME = 'youtube'
487     _TESTS = [
488         {
489             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
490             'info_dict': {
491                 'id': 'BaW_jenozKc',
492                 'ext': 'mp4',
493                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
494                 'uploader': 'Philipp Hagemeister',
495                 'uploader_id': 'phihag',
496                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
497                 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
498                 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
499                 'upload_date': '20121002',
500                 'license': 'Standard YouTube License',
501                 '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 .',
502                 'categories': ['Science & Technology'],
503                 'tags': ['youtube-dl'],
504                 'duration': 10,
505                 'view_count': int,
506                 'like_count': int,
507                 'dislike_count': int,
508                 'start_time': 1,
509                 'end_time': 9,
510             }
511         },
512         {
513             'url': 'https://www.youtube.com/watch?v=UxxajLWwzqY',
514             'note': 'Test generic use_cipher_signature video (#897)',
515             'info_dict': {
516                 'id': 'UxxajLWwzqY',
517                 'ext': 'mp4',
518                 'upload_date': '20120506',
519                 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]',
520                 'alt_title': 'I Love It (feat. Charli XCX)',
521                 'description': 'md5:f3ceb5ef83a08d95b9d146f973157cc8',
522                 'tags': ['Icona Pop i love it', 'sweden', 'pop music', 'big beat records', 'big beat', 'charli',
523                          'xcx', 'charli xcx', 'girls', 'hbo', 'i love it', "i don't care", 'icona', 'pop',
524                          'iconic ep', 'iconic', 'love', 'it'],
525                 'duration': 180,
526                 'uploader': 'Icona Pop',
527                 'uploader_id': 'IconaPop',
528                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IconaPop',
529                 'license': 'Standard YouTube License',
530                 'creator': 'Icona Pop',
531                 'track': 'I Love It (feat. Charli XCX)',
532                 'artist': 'Icona Pop',
533             }
534         },
535         {
536             'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ',
537             'note': 'Test VEVO video with age protection (#956)',
538             'info_dict': {
539                 'id': '07FYdnEawAQ',
540                 'ext': 'mp4',
541                 'upload_date': '20130703',
542                 'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
543                 'alt_title': 'Tunnel Vision',
544                 'description': 'md5:64249768eec3bc4276236606ea996373',
545                 'duration': 419,
546                 'uploader': 'justintimberlakeVEVO',
547                 'uploader_id': 'justintimberlakeVEVO',
548                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/justintimberlakeVEVO',
549                 'license': 'Standard YouTube License',
550                 'creator': 'Justin Timberlake',
551                 'track': 'Tunnel Vision',
552                 'artist': 'Justin Timberlake',
553                 'age_limit': 18,
554             }
555         },
556         {
557             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
558             'note': 'Embed-only video (#1746)',
559             'info_dict': {
560                 'id': 'yZIXLfi8CZQ',
561                 'ext': 'mp4',
562                 'upload_date': '20120608',
563                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
564                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
565                 'uploader': 'SET India',
566                 'uploader_id': 'setindia',
567                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/setindia',
568                 'license': 'Standard YouTube License',
569                 'age_limit': 18,
570             }
571         },
572         {
573             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=UxxajLWwzqY',
574             'note': 'Use the first video ID in the URL',
575             'info_dict': {
576                 'id': 'BaW_jenozKc',
577                 'ext': 'mp4',
578                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
579                 'uploader': 'Philipp Hagemeister',
580                 'uploader_id': 'phihag',
581                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
582                 'upload_date': '20121002',
583                 'license': 'Standard YouTube License',
584                 '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 .',
585                 'categories': ['Science & Technology'],
586                 'tags': ['youtube-dl'],
587                 'duration': 10,
588                 'view_count': int,
589                 'like_count': int,
590                 'dislike_count': int,
591             },
592             'params': {
593                 'skip_download': True,
594             },
595         },
596         {
597             'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
598             'note': '256k DASH audio (format 141) via DASH manifest',
599             'info_dict': {
600                 'id': 'a9LDPn-MO4I',
601                 'ext': 'm4a',
602                 'upload_date': '20121002',
603                 'uploader_id': '8KVIDEO',
604                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
605                 'description': '',
606                 'uploader': '8KVIDEO',
607                 'license': 'Standard YouTube License',
608                 'title': 'UHDTV TEST 8K VIDEO.mp4'
609             },
610             'params': {
611                 'youtube_include_dash_manifest': True,
612                 'format': '141',
613             },
614             'skip': 'format 141 not served anymore',
615         },
616         # DASH manifest with encrypted signature
617         {
618             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
619             'info_dict': {
620                 'id': 'IB3lcPjvWLA',
621                 'ext': 'm4a',
622                 'title': 'Afrojack, Spree Wilson - The Spark ft. Spree Wilson',
623                 'description': 'md5:1900ed86ee514927b9e00fbead6969a5',
624                 'duration': 244,
625                 'uploader': 'AfrojackVEVO',
626                 'uploader_id': 'AfrojackVEVO',
627                 'upload_date': '20131011',
628                 'license': 'Standard YouTube License',
629             },
630             'params': {
631                 'youtube_include_dash_manifest': True,
632                 'format': '141/bestaudio[ext=m4a]',
633             },
634         },
635         # JS player signature function name containing $
636         {
637             'url': 'https://www.youtube.com/watch?v=nfWlot6h_JM',
638             'info_dict': {
639                 'id': 'nfWlot6h_JM',
640                 'ext': 'm4a',
641                 'title': 'Taylor Swift - Shake It Off',
642                 'alt_title': 'Shake It Off',
643                 'description': 'md5:95f66187cd7c8b2c13eb78e1223b63c3',
644                 'duration': 242,
645                 'uploader': 'TaylorSwiftVEVO',
646                 'uploader_id': 'TaylorSwiftVEVO',
647                 'upload_date': '20140818',
648                 'license': 'Standard YouTube License',
649                 'creator': 'Taylor Swift',
650             },
651             'params': {
652                 'youtube_include_dash_manifest': True,
653                 'format': '141/bestaudio[ext=m4a]',
654             },
655         },
656         # Controversy video
657         {
658             'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
659             'info_dict': {
660                 'id': 'T4XJQO3qol8',
661                 'ext': 'mp4',
662                 'duration': 219,
663                 'upload_date': '20100909',
664                 'uploader': 'TJ Kirk',
665                 'uploader_id': 'TheAmazingAtheist',
666                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheAmazingAtheist',
667                 'license': 'Standard YouTube License',
668                 'title': 'Burning Everyone\'s Koran',
669                 'description': 'SUBSCRIBE: http://www.youtube.com/saturninefilms\n\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',
670             }
671         },
672         # Normal age-gate video (No vevo, embed allowed)
673         {
674             'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
675             'info_dict': {
676                 'id': 'HtVdAasjOgU',
677                 'ext': 'mp4',
678                 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
679                 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
680                 'duration': 142,
681                 'uploader': 'The Witcher',
682                 'uploader_id': 'WitcherGame',
683                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame',
684                 'upload_date': '20140605',
685                 'license': 'Standard YouTube License',
686                 'age_limit': 18,
687             },
688         },
689         # Age-gate video with encrypted signature
690         {
691             'url': 'https://www.youtube.com/watch?v=6kLq3WMV1nU',
692             'info_dict': {
693                 'id': '6kLq3WMV1nU',
694                 'ext': 'webm',
695                 'title': 'Dedication To My Ex (Miss That) (Lyric Video)',
696                 'description': 'md5:33765bb339e1b47e7e72b5490139bb41',
697                 'duration': 246,
698                 'uploader': 'LloydVEVO',
699                 'uploader_id': 'LloydVEVO',
700                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/LloydVEVO',
701                 'upload_date': '20110629',
702                 'license': 'Standard YouTube License',
703                 'age_limit': 18,
704             },
705         },
706         # video_info is None (https://github.com/rg3/youtube-dl/issues/4421)
707         # YouTube Red ad is not captured for creator
708         {
709             'url': '__2ABJjxzNo',
710             'info_dict': {
711                 'id': '__2ABJjxzNo',
712                 'ext': 'mp4',
713                 'duration': 266,
714                 'upload_date': '20100430',
715                 'uploader_id': 'deadmau5',
716                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/deadmau5',
717                 'creator': 'deadmau5',
718                 'description': 'md5:12c56784b8032162bb936a5f76d55360',
719                 'uploader': 'deadmau5',
720                 'license': 'Standard YouTube License',
721                 'title': 'Deadmau5 - Some Chords (HD)',
722                 'alt_title': 'Some Chords',
723             },
724             'expected_warnings': [
725                 'DASH manifest missing',
726             ]
727         },
728         # Olympics (https://github.com/rg3/youtube-dl/issues/4431)
729         {
730             'url': 'lqQg6PlCWgI',
731             'info_dict': {
732                 'id': 'lqQg6PlCWgI',
733                 'ext': 'mp4',
734                 'duration': 6085,
735                 'upload_date': '20150827',
736                 'uploader_id': 'olympic',
737                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic',
738                 'license': 'Standard YouTube License',
739                 'description': 'HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games',
740                 'uploader': 'Olympic',
741                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games',
742             },
743             'params': {
744                 'skip_download': 'requires avconv',
745             }
746         },
747         # Non-square pixels
748         {
749             'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
750             'info_dict': {
751                 'id': '_b-2C3KPAM0',
752                 'ext': 'mp4',
753                 'stretched_ratio': 16 / 9.,
754                 'duration': 85,
755                 'upload_date': '20110310',
756                 'uploader_id': 'AllenMeow',
757                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/AllenMeow',
758                 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
759                 'uploader': '孫ᄋᄅ',
760                 'license': 'Standard YouTube License',
761                 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
762             },
763         },
764         # url_encoded_fmt_stream_map is empty string
765         {
766             'url': 'qEJwOuvDf7I',
767             'info_dict': {
768                 'id': 'qEJwOuvDf7I',
769                 'ext': 'webm',
770                 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
771                 'description': '',
772                 'upload_date': '20150404',
773                 'uploader_id': 'spbelect',
774                 'uploader': 'Наблюдатели Петербурга',
775             },
776             'params': {
777                 'skip_download': 'requires avconv',
778             },
779             'skip': 'This live event has ended.',
780         },
781         # Extraction from multiple DASH manifests (https://github.com/rg3/youtube-dl/pull/6097)
782         {
783             'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
784             'info_dict': {
785                 'id': 'FIl7x6_3R5Y',
786                 'ext': 'webm',
787                 'title': 'md5:7b81415841e02ecd4313668cde88737a',
788                 'description': 'md5:116377fd2963b81ec4ce64b542173306',
789                 'duration': 220,
790                 'upload_date': '20150625',
791                 'uploader_id': 'dorappi2000',
792                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/dorappi2000',
793                 'uploader': 'dorappi2000',
794                 'license': 'Standard YouTube License',
795                 'formats': 'mincount:31',
796             },
797             'skip': 'not actual anymore',
798         },
799         # DASH manifest with segment_list
800         {
801             'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
802             'md5': '8ce563a1d667b599d21064e982ab9e31',
803             'info_dict': {
804                 'id': 'CsmdDsKjzN8',
805                 'ext': 'mp4',
806                 'upload_date': '20150501',  # According to '<meta itemprop="datePublished"', but in other places it's 20150510
807                 'uploader': 'Airtek',
808                 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
809                 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
810                 'license': 'Standard YouTube License',
811                 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
812             },
813             'params': {
814                 'youtube_include_dash_manifest': True,
815                 'format': '135',  # bestvideo
816             },
817             'skip': 'This live event has ended.',
818         },
819         {
820             # Multifeed videos (multiple cameras), URL is for Main Camera
821             'url': 'https://www.youtube.com/watch?v=jqWvoWXjCVs',
822             'info_dict': {
823                 'id': 'jqWvoWXjCVs',
824                 'title': 'teamPGP: Rocket League Noob Stream',
825                 'description': 'md5:dc7872fb300e143831327f1bae3af010',
826             },
827             'playlist': [{
828                 'info_dict': {
829                     'id': 'jqWvoWXjCVs',
830                     'ext': 'mp4',
831                     'title': 'teamPGP: Rocket League Noob Stream (Main Camera)',
832                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
833                     'duration': 7335,
834                     'upload_date': '20150721',
835                     'uploader': 'Beer Games Beer',
836                     'uploader_id': 'beergamesbeer',
837                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
838                     'license': 'Standard YouTube License',
839                 },
840             }, {
841                 'info_dict': {
842                     'id': '6h8e8xoXJzg',
843                     'ext': 'mp4',
844                     'title': 'teamPGP: Rocket League Noob Stream (kreestuh)',
845                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
846                     'duration': 7337,
847                     'upload_date': '20150721',
848                     'uploader': 'Beer Games Beer',
849                     'uploader_id': 'beergamesbeer',
850                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
851                     'license': 'Standard YouTube License',
852                 },
853             }, {
854                 'info_dict': {
855                     'id': 'PUOgX5z9xZw',
856                     'ext': 'mp4',
857                     'title': 'teamPGP: Rocket League Noob Stream (grizzle)',
858                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
859                     'duration': 7337,
860                     'upload_date': '20150721',
861                     'uploader': 'Beer Games Beer',
862                     'uploader_id': 'beergamesbeer',
863                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
864                     'license': 'Standard YouTube License',
865                 },
866             }, {
867                 'info_dict': {
868                     'id': 'teuwxikvS5k',
869                     'ext': 'mp4',
870                     'title': 'teamPGP: Rocket League Noob Stream (zim)',
871                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
872                     'duration': 7334,
873                     'upload_date': '20150721',
874                     'uploader': 'Beer Games Beer',
875                     'uploader_id': 'beergamesbeer',
876                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
877                     'license': 'Standard YouTube License',
878                 },
879             }],
880             'params': {
881                 'skip_download': True,
882             },
883         },
884         {
885             # Multifeed video with comma in title (see https://github.com/rg3/youtube-dl/issues/8536)
886             'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
887             'info_dict': {
888                 'id': 'gVfLd0zydlo',
889                 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
890             },
891             'playlist_count': 2,
892             'skip': 'Not multifeed anymore',
893         },
894         {
895             'url': 'https://vid.plus/FlRa-iH7PGw',
896             'only_matching': True,
897         },
898         {
899             'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
900             'only_matching': True,
901         },
902         {
903             # Title with JS-like syntax "};" (see https://github.com/rg3/youtube-dl/issues/7468)
904             # Also tests cut-off URL expansion in video description (see
905             # https://github.com/rg3/youtube-dl/issues/1892,
906             # https://github.com/rg3/youtube-dl/issues/8164)
907             'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
908             'info_dict': {
909                 'id': 'lsguqyKfVQg',
910                 'ext': 'mp4',
911                 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
912                 'alt_title': 'Dark Walk - Position Music',
913                 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
914                 'duration': 133,
915                 'upload_date': '20151119',
916                 'uploader_id': 'IronSoulElf',
917                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IronSoulElf',
918                 'uploader': 'IronSoulElf',
919                 'license': 'Standard YouTube License',
920                 'creator': 'Todd Haberman,  Daniel Law Heath and Aaron Kaplan',
921                 'track': 'Dark Walk - Position Music',
922                 'artist': 'Todd Haberman,  Daniel Law Heath and Aaron Kaplan',
923             },
924             'params': {
925                 'skip_download': True,
926             },
927         },
928         {
929             # Tags with '};' (see https://github.com/rg3/youtube-dl/issues/7468)
930             'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
931             'only_matching': True,
932         },
933         {
934             # Video with yt:stretch=17:0
935             'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
936             'info_dict': {
937                 'id': 'Q39EVAstoRM',
938                 'ext': 'mp4',
939                 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
940                 'description': 'md5:ee18a25c350637c8faff806845bddee9',
941                 'upload_date': '20151107',
942                 'uploader_id': 'UCCr7TALkRbo3EtFzETQF1LA',
943                 'uploader': 'CH GAMER DROID',
944             },
945             'params': {
946                 'skip_download': True,
947             },
948             'skip': 'This video does not exist.',
949         },
950         {
951             # Video licensed under Creative Commons
952             'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
953             'info_dict': {
954                 'id': 'M4gD1WSo5mA',
955                 'ext': 'mp4',
956                 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
957                 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
958                 'duration': 721,
959                 'upload_date': '20150127',
960                 'uploader_id': 'BerkmanCenter',
961                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/BerkmanCenter',
962                 'uploader': 'The Berkman Klein Center for Internet & Society',
963                 'license': 'Creative Commons Attribution license (reuse allowed)',
964             },
965             'params': {
966                 'skip_download': True,
967             },
968         },
969         {
970             # Channel-like uploader_url
971             'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
972             'info_dict': {
973                 'id': 'eQcmzGIKrzg',
974                 'ext': 'mp4',
975                 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
976                 'description': 'md5:dda0d780d5a6e120758d1711d062a867',
977                 'duration': 4060,
978                 'upload_date': '20151119',
979                 'uploader': 'Bernie Sanders',
980                 'uploader_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
981                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
982                 'license': 'Creative Commons Attribution license (reuse allowed)',
983             },
984             'params': {
985                 'skip_download': True,
986             },
987         },
988         {
989             'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;amp;v=V36LpHqtcDY',
990             'only_matching': True,
991         },
992         {
993             # YouTube Red paid video (https://github.com/rg3/youtube-dl/issues/10059)
994             'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
995             'only_matching': True,
996         },
997         {
998             # Rental video preview
999             'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
1000             'info_dict': {
1001                 'id': 'uGpuVWrhIzE',
1002                 'ext': 'mp4',
1003                 'title': 'Piku - Trailer',
1004                 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
1005                 'upload_date': '20150811',
1006                 'uploader': 'FlixMatrix',
1007                 'uploader_id': 'FlixMatrixKaravan',
1008                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/FlixMatrixKaravan',
1009                 'license': 'Standard YouTube License',
1010             },
1011             'params': {
1012                 'skip_download': True,
1013             },
1014             'skip': 'This video is not available.',
1015         },
1016         {
1017             # YouTube Red video with episode data
1018             'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
1019             'info_dict': {
1020                 'id': 'iqKdEhx-dD4',
1021                 'ext': 'mp4',
1022                 'title': 'Isolation - Mind Field (Ep 1)',
1023                 'description': 'md5:25b78d2f64ae81719f5c96319889b736',
1024                 'duration': 2085,
1025                 'upload_date': '20170118',
1026                 'uploader': 'Vsauce',
1027                 'uploader_id': 'Vsauce',
1028                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Vsauce',
1029                 'license': 'Standard YouTube License',
1030                 'series': 'Mind Field',
1031                 'season_number': 1,
1032                 'episode_number': 1,
1033             },
1034             'params': {
1035                 'skip_download': True,
1036             },
1037             'expected_warnings': [
1038                 'Skipping DASH manifest',
1039             ],
1040         },
1041         {
1042             # The following content has been identified by the YouTube community
1043             # as inappropriate or offensive to some audiences.
1044             'url': 'https://www.youtube.com/watch?v=6SJNVb0GnPI',
1045             'info_dict': {
1046                 'id': '6SJNVb0GnPI',
1047                 'ext': 'mp4',
1048                 'title': 'Race Differences in Intelligence',
1049                 'description': 'md5:5d161533167390427a1f8ee89a1fc6f1',
1050                 'duration': 965,
1051                 'upload_date': '20140124',
1052                 'uploader': 'New Century Foundation',
1053                 'uploader_id': 'UCEJYpZGqgUob0zVVEaLhvVg',
1054                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCEJYpZGqgUob0zVVEaLhvVg',
1055                 'license': 'Standard YouTube License',
1056             },
1057             'params': {
1058                 'skip_download': True,
1059             },
1060         },
1061         {
1062             # itag 212
1063             'url': '1t24XAntNCY',
1064             'only_matching': True,
1065         },
1066         {
1067             # geo restricted to JP
1068             'url': 'sJL6WA-aGkQ',
1069             'only_matching': True,
1070         },
1071         {
1072             'url': 'https://www.youtube.com/watch?v=MuAGGZNfUkU&list=RDMM',
1073             'only_matching': True,
1074         },
1075         {
1076             'url': 'https://invidio.us/watch?v=BaW_jenozKc',
1077             'only_matching': True,
1078         },
1079     ]
1080
1081     def __init__(self, *args, **kwargs):
1082         super(YoutubeIE, self).__init__(*args, **kwargs)
1083         self._player_cache = {}
1084
1085     def report_video_info_webpage_download(self, video_id):
1086         """Report attempt to download video info webpage."""
1087         self.to_screen('%s: Downloading video info webpage' % video_id)
1088
1089     def report_information_extraction(self, video_id):
1090         """Report attempt to extract video information."""
1091         self.to_screen('%s: Extracting video information' % video_id)
1092
1093     def report_unavailable_format(self, video_id, format):
1094         """Report extracted video URL."""
1095         self.to_screen('%s: Format %s not available' % (video_id, format))
1096
1097     def report_rtmp_download(self):
1098         """Indicate the download will use the RTMP protocol."""
1099         self.to_screen('RTMP download detected')
1100
1101     def _signature_cache_id(self, example_sig):
1102         """ Return a string representation of a signature """
1103         return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
1104
1105     def _extract_signature_function(self, video_id, player_url, example_sig):
1106         id_m = re.match(
1107             r'.*?-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|(?:/[a-z]{2}_[A-Z]{2})?/base)?\.(?P<ext>[a-z]+)$',
1108             player_url)
1109         if not id_m:
1110             raise ExtractorError('Cannot identify player %r' % player_url)
1111         player_type = id_m.group('ext')
1112         player_id = id_m.group('id')
1113
1114         # Read from filesystem cache
1115         func_id = '%s_%s_%s' % (
1116             player_type, player_id, self._signature_cache_id(example_sig))
1117         assert os.path.basename(func_id) == func_id
1118
1119         cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
1120         if cache_spec is not None:
1121             return lambda s: ''.join(s[i] for i in cache_spec)
1122
1123         download_note = (
1124             'Downloading player %s' % player_url
1125             if self._downloader.params.get('verbose') else
1126             'Downloading %s player %s' % (player_type, player_id)
1127         )
1128         if player_type == 'js':
1129             code = self._download_webpage(
1130                 player_url, video_id,
1131                 note=download_note,
1132                 errnote='Download of %s failed' % player_url)
1133             res = self._parse_sig_js(code)
1134         elif player_type == 'swf':
1135             urlh = self._request_webpage(
1136                 player_url, video_id,
1137                 note=download_note,
1138                 errnote='Download of %s failed' % player_url)
1139             code = urlh.read()
1140             res = self._parse_sig_swf(code)
1141         else:
1142             assert False, 'Invalid player type %r' % player_type
1143
1144         test_string = ''.join(map(compat_chr, range(len(example_sig))))
1145         cache_res = res(test_string)
1146         cache_spec = [ord(c) for c in cache_res]
1147
1148         self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
1149         return res
1150
1151     def _print_sig_code(self, func, example_sig):
1152         def gen_sig_code(idxs):
1153             def _genslice(start, end, step):
1154                 starts = '' if start == 0 else str(start)
1155                 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
1156                 steps = '' if step == 1 else (':%d' % step)
1157                 return 's[%s%s%s]' % (starts, ends, steps)
1158
1159             step = None
1160             # Quelch pyflakes warnings - start will be set when step is set
1161             start = '(Never used)'
1162             for i, prev in zip(idxs[1:], idxs[:-1]):
1163                 if step is not None:
1164                     if i - prev == step:
1165                         continue
1166                     yield _genslice(start, prev, step)
1167                     step = None
1168                     continue
1169                 if i - prev in [-1, 1]:
1170                     step = i - prev
1171                     start = prev
1172                     continue
1173                 else:
1174                     yield 's[%d]' % prev
1175             if step is None:
1176                 yield 's[%d]' % i
1177             else:
1178                 yield _genslice(start, i, step)
1179
1180         test_string = ''.join(map(compat_chr, range(len(example_sig))))
1181         cache_res = func(test_string)
1182         cache_spec = [ord(c) for c in cache_res]
1183         expr_code = ' + '.join(gen_sig_code(cache_spec))
1184         signature_id_tuple = '(%s)' % (
1185             ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
1186         code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
1187                 '    return %s\n') % (signature_id_tuple, expr_code)
1188         self.to_screen('Extracted signature function:\n' + code)
1189
1190     def _parse_sig_js(self, jscode):
1191         funcname = self._search_regex(
1192             (r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1193              r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
1194              r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*c\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1195              r'\bc\s*&&\s*d\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\('),
1196             jscode, 'Initial JS player signature function name', group='sig')
1197
1198         jsi = JSInterpreter(jscode)
1199         initial_function = jsi.extract_function(funcname)
1200         return lambda s: initial_function([s])
1201
1202     def _parse_sig_swf(self, file_contents):
1203         swfi = SWFInterpreter(file_contents)
1204         TARGET_CLASSNAME = 'SignatureDecipher'
1205         searched_class = swfi.extract_class(TARGET_CLASSNAME)
1206         initial_function = swfi.extract_function(searched_class, 'decipher')
1207         return lambda s: initial_function([s])
1208
1209     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
1210         """Turn the encrypted s field into a working signature"""
1211
1212         if player_url is None:
1213             raise ExtractorError('Cannot decrypt signature without player_url')
1214
1215         if player_url.startswith('//'):
1216             player_url = 'https:' + player_url
1217         elif not re.match(r'https?://', player_url):
1218             player_url = compat_urlparse.urljoin(
1219                 'https://www.youtube.com', player_url)
1220         try:
1221             player_id = (player_url, self._signature_cache_id(s))
1222             if player_id not in self._player_cache:
1223                 func = self._extract_signature_function(
1224                     video_id, player_url, s
1225                 )
1226                 self._player_cache[player_id] = func
1227             func = self._player_cache[player_id]
1228             if self._downloader.params.get('youtube_print_sig_code'):
1229                 self._print_sig_code(func, s)
1230             return func(s)
1231         except Exception as e:
1232             tb = traceback.format_exc()
1233             raise ExtractorError(
1234                 'Signature extraction failed: ' + tb, cause=e)
1235
1236     def _get_subtitles(self, video_id, webpage):
1237         try:
1238             subs_doc = self._download_xml(
1239                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
1240                 video_id, note=False)
1241         except ExtractorError as err:
1242             self._downloader.report_warning('unable to download video subtitles: %s' % error_to_compat_str(err))
1243             return {}
1244
1245         sub_lang_list = {}
1246         for track in subs_doc.findall('track'):
1247             lang = track.attrib['lang_code']
1248             if lang in sub_lang_list:
1249                 continue
1250             sub_formats = []
1251             for ext in self._SUBTITLE_FORMATS:
1252                 params = compat_urllib_parse_urlencode({
1253                     'lang': lang,
1254                     'v': video_id,
1255                     'fmt': ext,
1256                     'name': track.attrib['name'].encode('utf-8'),
1257                 })
1258                 sub_formats.append({
1259                     'url': 'https://www.youtube.com/api/timedtext?' + params,
1260                     'ext': ext,
1261                 })
1262             sub_lang_list[lang] = sub_formats
1263         if not sub_lang_list:
1264             self._downloader.report_warning('video doesn\'t have subtitles')
1265             return {}
1266         return sub_lang_list
1267
1268     def _get_ytplayer_config(self, video_id, webpage):
1269         patterns = (
1270             # User data may contain arbitrary character sequences that may affect
1271             # JSON extraction with regex, e.g. when '};' is contained the second
1272             # regex won't capture the whole JSON. Yet working around by trying more
1273             # concrete regex first keeping in mind proper quoted string handling
1274             # to be implemented in future that will replace this workaround (see
1275             # https://github.com/rg3/youtube-dl/issues/7468,
1276             # https://github.com/rg3/youtube-dl/pull/7599)
1277             r';ytplayer\.config\s*=\s*({.+?});ytplayer',
1278             r';ytplayer\.config\s*=\s*({.+?});',
1279         )
1280         config = self._search_regex(
1281             patterns, webpage, 'ytplayer.config', default=None)
1282         if config:
1283             return self._parse_json(
1284                 uppercase_escape(config), video_id, fatal=False)
1285
1286     def _get_automatic_captions(self, video_id, webpage):
1287         """We need the webpage for getting the captions url, pass it as an
1288            argument to speed up the process."""
1289         self.to_screen('%s: Looking for automatic captions' % video_id)
1290         player_config = self._get_ytplayer_config(video_id, webpage)
1291         err_msg = 'Couldn\'t find automatic captions for %s' % video_id
1292         if not player_config:
1293             self._downloader.report_warning(err_msg)
1294             return {}
1295         try:
1296             args = player_config['args']
1297             caption_url = args.get('ttsurl')
1298             if caption_url:
1299                 timestamp = args['timestamp']
1300                 # We get the available subtitles
1301                 list_params = compat_urllib_parse_urlencode({
1302                     'type': 'list',
1303                     'tlangs': 1,
1304                     'asrs': 1,
1305                 })
1306                 list_url = caption_url + '&' + list_params
1307                 caption_list = self._download_xml(list_url, video_id)
1308                 original_lang_node = caption_list.find('track')
1309                 if original_lang_node is None:
1310                     self._downloader.report_warning('Video doesn\'t have automatic captions')
1311                     return {}
1312                 original_lang = original_lang_node.attrib['lang_code']
1313                 caption_kind = original_lang_node.attrib.get('kind', '')
1314
1315                 sub_lang_list = {}
1316                 for lang_node in caption_list.findall('target'):
1317                     sub_lang = lang_node.attrib['lang_code']
1318                     sub_formats = []
1319                     for ext in self._SUBTITLE_FORMATS:
1320                         params = compat_urllib_parse_urlencode({
1321                             'lang': original_lang,
1322                             'tlang': sub_lang,
1323                             'fmt': ext,
1324                             'ts': timestamp,
1325                             'kind': caption_kind,
1326                         })
1327                         sub_formats.append({
1328                             'url': caption_url + '&' + params,
1329                             'ext': ext,
1330                         })
1331                     sub_lang_list[sub_lang] = sub_formats
1332                 return sub_lang_list
1333
1334             def make_captions(sub_url, sub_langs):
1335                 parsed_sub_url = compat_urllib_parse_urlparse(sub_url)
1336                 caption_qs = compat_parse_qs(parsed_sub_url.query)
1337                 captions = {}
1338                 for sub_lang in sub_langs:
1339                     sub_formats = []
1340                     for ext in self._SUBTITLE_FORMATS:
1341                         caption_qs.update({
1342                             'tlang': [sub_lang],
1343                             'fmt': [ext],
1344                         })
1345                         sub_url = compat_urlparse.urlunparse(parsed_sub_url._replace(
1346                             query=compat_urllib_parse_urlencode(caption_qs, True)))
1347                         sub_formats.append({
1348                             'url': sub_url,
1349                             'ext': ext,
1350                         })
1351                     captions[sub_lang] = sub_formats
1352                 return captions
1353
1354             # New captions format as of 22.06.2017
1355             player_response = args.get('player_response')
1356             if player_response and isinstance(player_response, compat_str):
1357                 player_response = self._parse_json(
1358                     player_response, video_id, fatal=False)
1359                 if player_response:
1360                     renderer = player_response['captions']['playerCaptionsTracklistRenderer']
1361                     base_url = renderer['captionTracks'][0]['baseUrl']
1362                     sub_lang_list = []
1363                     for lang in renderer['translationLanguages']:
1364                         lang_code = lang.get('languageCode')
1365                         if lang_code:
1366                             sub_lang_list.append(lang_code)
1367                     return make_captions(base_url, sub_lang_list)
1368
1369             # Some videos don't provide ttsurl but rather caption_tracks and
1370             # caption_translation_languages (e.g. 20LmZk1hakA)
1371             # Does not used anymore as of 22.06.2017
1372             caption_tracks = args['caption_tracks']
1373             caption_translation_languages = args['caption_translation_languages']
1374             caption_url = compat_parse_qs(caption_tracks.split(',')[0])['u'][0]
1375             sub_lang_list = []
1376             for lang in caption_translation_languages.split(','):
1377                 lang_qs = compat_parse_qs(compat_urllib_parse_unquote_plus(lang))
1378                 sub_lang = lang_qs.get('lc', [None])[0]
1379                 if sub_lang:
1380                     sub_lang_list.append(sub_lang)
1381             return make_captions(caption_url, sub_lang_list)
1382         # An extractor error can be raise by the download process if there are
1383         # no automatic captions but there are subtitles
1384         except (KeyError, IndexError, ExtractorError):
1385             self._downloader.report_warning(err_msg)
1386             return {}
1387
1388     def _mark_watched(self, video_id, video_info):
1389         playback_url = video_info.get('videostats_playback_base_url', [None])[0]
1390         if not playback_url:
1391             return
1392         parsed_playback_url = compat_urlparse.urlparse(playback_url)
1393         qs = compat_urlparse.parse_qs(parsed_playback_url.query)
1394
1395         # cpn generation algorithm is reverse engineered from base.js.
1396         # In fact it works even with dummy cpn.
1397         CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
1398         cpn = ''.join((CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16)))
1399
1400         qs.update({
1401             'ver': ['2'],
1402             'cpn': [cpn],
1403         })
1404         playback_url = compat_urlparse.urlunparse(
1405             parsed_playback_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
1406
1407         self._download_webpage(
1408             playback_url, video_id, 'Marking watched',
1409             'Unable to mark watched', fatal=False)
1410
1411     @staticmethod
1412     def _extract_urls(webpage):
1413         # Embedded YouTube player
1414         entries = [
1415             unescapeHTML(mobj.group('url'))
1416             for mobj in re.finditer(r'''(?x)
1417             (?:
1418                 <iframe[^>]+?src=|
1419                 data-video-url=|
1420                 <embed[^>]+?src=|
1421                 embedSWF\(?:\s*|
1422                 <object[^>]+data=|
1423                 new\s+SWFObject\(
1424             )
1425             (["\'])
1426                 (?P<url>(?:https?:)?//(?:www\.)?youtube(?:-nocookie)?\.com/
1427                 (?:embed|v|p)/[0-9A-Za-z_-]{11}.*?)
1428             \1''', webpage)]
1429
1430         # lazyYT YouTube embed
1431         entries.extend(list(map(
1432             unescapeHTML,
1433             re.findall(r'class="lazyYT" data-youtube-id="([^"]+)"', webpage))))
1434
1435         # Wordpress "YouTube Video Importer" plugin
1436         matches = re.findall(r'''(?x)<div[^>]+
1437             class=(?P<q1>[\'"])[^\'"]*\byvii_single_video_player\b[^\'"]*(?P=q1)[^>]+
1438             data-video_id=(?P<q2>[\'"])([^\'"]+)(?P=q2)''', webpage)
1439         entries.extend(m[-1] for m in matches)
1440
1441         return entries
1442
1443     @staticmethod
1444     def _extract_url(webpage):
1445         urls = YoutubeIE._extract_urls(webpage)
1446         return urls[0] if urls else None
1447
1448     @classmethod
1449     def extract_id(cls, url):
1450         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
1451         if mobj is None:
1452             raise ExtractorError('Invalid URL: %s' % url)
1453         video_id = mobj.group(2)
1454         return video_id
1455
1456     def _extract_annotations(self, video_id):
1457         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
1458         return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
1459
1460     @staticmethod
1461     def _extract_chapters(description, duration):
1462         if not description:
1463             return None
1464         chapter_lines = re.findall(
1465             r'(?:^|<br\s*/>)([^<]*<a[^>]+onclick=["\']yt\.www\.watch\.player\.seekTo[^>]+>(\d{1,2}:\d{1,2}(?::\d{1,2})?)</a>[^>]*)(?=$|<br\s*/>)',
1466             description)
1467         if not chapter_lines:
1468             return None
1469         chapters = []
1470         for next_num, (chapter_line, time_point) in enumerate(
1471                 chapter_lines, start=1):
1472             start_time = parse_duration(time_point)
1473             if start_time is None:
1474                 continue
1475             if start_time > duration:
1476                 break
1477             end_time = (duration if next_num == len(chapter_lines)
1478                         else parse_duration(chapter_lines[next_num][1]))
1479             if end_time is None:
1480                 continue
1481             if end_time > duration:
1482                 end_time = duration
1483             if start_time > end_time:
1484                 break
1485             chapter_title = re.sub(
1486                 r'<a[^>]+>[^<]+</a>', '', chapter_line).strip(' \t-')
1487             chapter_title = re.sub(r'\s+', ' ', chapter_title)
1488             chapters.append({
1489                 'start_time': start_time,
1490                 'end_time': end_time,
1491                 'title': chapter_title,
1492             })
1493         return chapters
1494
1495     def _real_extract(self, url):
1496         url, smuggled_data = unsmuggle_url(url, {})
1497
1498         proto = (
1499             'http' if self._downloader.params.get('prefer_insecure', False)
1500             else 'https')
1501
1502         start_time = None
1503         end_time = None
1504         parsed_url = compat_urllib_parse_urlparse(url)
1505         for component in [parsed_url.fragment, parsed_url.query]:
1506             query = compat_parse_qs(component)
1507             if start_time is None and 't' in query:
1508                 start_time = parse_duration(query['t'][0])
1509             if start_time is None and 'start' in query:
1510                 start_time = parse_duration(query['start'][0])
1511             if end_time is None and 'end' in query:
1512                 end_time = parse_duration(query['end'][0])
1513
1514         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1515         mobj = re.search(self._NEXT_URL_RE, url)
1516         if mobj:
1517             url = proto + '://www.youtube.com/' + compat_urllib_parse_unquote(mobj.group(1)).lstrip('/')
1518         video_id = self.extract_id(url)
1519
1520         # Get video webpage
1521         url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
1522         video_webpage = self._download_webpage(url, video_id)
1523
1524         # Attempt to extract SWF player URL
1525         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1526         if mobj is not None:
1527             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
1528         else:
1529             player_url = None
1530
1531         dash_mpds = []
1532
1533         def add_dash_mpd(video_info):
1534             dash_mpd = video_info.get('dashmpd')
1535             if dash_mpd and dash_mpd[0] not in dash_mpds:
1536                 dash_mpds.append(dash_mpd[0])
1537
1538         is_live = None
1539         view_count = None
1540
1541         def extract_view_count(v_info):
1542             return int_or_none(try_get(v_info, lambda x: x['view_count'][0]))
1543
1544         player_response = {}
1545
1546         # Get video info
1547         embed_webpage = None
1548         if re.search(r'player-age-gate-content">', video_webpage) is not None:
1549             age_gate = True
1550             # We simulate the access to the video from www.youtube.com/v/{video_id}
1551             # this can be viewed without login into Youtube
1552             url = proto + '://www.youtube.com/embed/%s' % video_id
1553             embed_webpage = self._download_webpage(url, video_id, 'Downloading embed webpage')
1554             data = compat_urllib_parse_urlencode({
1555                 'video_id': video_id,
1556                 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1557                 'sts': self._search_regex(
1558                     r'"sts"\s*:\s*(\d+)', embed_webpage, 'sts', default=''),
1559             })
1560             video_info_url = proto + '://www.youtube.com/get_video_info?' + data
1561             video_info_webpage = self._download_webpage(
1562                 video_info_url, video_id,
1563                 note='Refetching age-gated info webpage',
1564                 errnote='unable to download video info webpage')
1565             video_info = compat_parse_qs(video_info_webpage)
1566             add_dash_mpd(video_info)
1567         else:
1568             age_gate = False
1569             video_info = None
1570             sts = None
1571             # Try looking directly into the video webpage
1572             ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1573             if ytplayer_config:
1574                 args = ytplayer_config['args']
1575                 if args.get('url_encoded_fmt_stream_map') or args.get('hlsvp'):
1576                     # Convert to the same format returned by compat_parse_qs
1577                     video_info = dict((k, [v]) for k, v in args.items())
1578                     add_dash_mpd(video_info)
1579                 # Rental video is not rented but preview is available (e.g.
1580                 # https://www.youtube.com/watch?v=yYr8q0y5Jfg,
1581                 # https://github.com/rg3/youtube-dl/issues/10532)
1582                 if not video_info and args.get('ypc_vid'):
1583                     return self.url_result(
1584                         args['ypc_vid'], YoutubeIE.ie_key(), video_id=args['ypc_vid'])
1585                 if args.get('livestream') == '1' or args.get('live_playback') == 1:
1586                     is_live = True
1587                 sts = ytplayer_config.get('sts')
1588                 if not player_response:
1589                     pl_response = str_or_none(args.get('player_response'))
1590                     if pl_response:
1591                         pl_response = self._parse_json(pl_response, video_id, fatal=False)
1592                         if isinstance(pl_response, dict):
1593                             player_response = pl_response
1594             if not video_info or self._downloader.params.get('youtube_include_dash_manifest', True):
1595                 # We also try looking in get_video_info since it may contain different dashmpd
1596                 # URL that points to a DASH manifest with possibly different itag set (some itags
1597                 # are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
1598                 # manifest pointed by get_video_info's dashmpd).
1599                 # The general idea is to take a union of itags of both DASH manifests (for example
1600                 # video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
1601                 self.report_video_info_webpage_download(video_id)
1602                 for el in ('info', 'embedded', 'detailpage', 'vevo', ''):
1603                     query = {
1604                         'video_id': video_id,
1605                         'ps': 'default',
1606                         'eurl': '',
1607                         'gl': 'US',
1608                         'hl': 'en',
1609                     }
1610                     if el:
1611                         query['el'] = el
1612                     if sts:
1613                         query['sts'] = sts
1614                     video_info_webpage = self._download_webpage(
1615                         '%s://www.youtube.com/get_video_info' % proto,
1616                         video_id, note=False,
1617                         errnote='unable to download video info webpage',
1618                         fatal=False, query=query)
1619                     if not video_info_webpage:
1620                         continue
1621                     get_video_info = compat_parse_qs(video_info_webpage)
1622                     if not player_response:
1623                         pl_response = get_video_info.get('player_response', [None])[0]
1624                         if isinstance(pl_response, dict):
1625                             player_response = pl_response
1626                     add_dash_mpd(get_video_info)
1627                     if view_count is None:
1628                         view_count = extract_view_count(get_video_info)
1629                     if not video_info:
1630                         video_info = get_video_info
1631                     if 'token' in get_video_info:
1632                         # Different get_video_info requests may report different results, e.g.
1633                         # some may report video unavailability, but some may serve it without
1634                         # any complaint (see https://github.com/rg3/youtube-dl/issues/7362,
1635                         # the original webpage as well as el=info and el=embedded get_video_info
1636                         # requests report video unavailability due to geo restriction while
1637                         # el=detailpage succeeds and returns valid data). This is probably
1638                         # due to YouTube measures against IP ranges of hosting providers.
1639                         # Working around by preferring the first succeeded video_info containing
1640                         # the token if no such video_info yet was found.
1641                         if 'token' not in video_info:
1642                             video_info = get_video_info
1643                         break
1644
1645         def extract_unavailable_message():
1646             return self._html_search_regex(
1647                 r'(?s)<h1[^>]+id="unavailable-message"[^>]*>(.+?)</h1>',
1648                 video_webpage, 'unavailable message', default=None)
1649
1650         if 'token' not in video_info:
1651             if 'reason' in video_info:
1652                 if 'The uploader has not made this video available in your country.' in video_info['reason']:
1653                     regions_allowed = self._html_search_meta(
1654                         'regionsAllowed', video_webpage, default=None)
1655                     countries = regions_allowed.split(',') if regions_allowed else None
1656                     self.raise_geo_restricted(
1657                         msg=video_info['reason'][0], countries=countries)
1658                 reason = video_info['reason'][0]
1659                 if 'Invalid parameters' in reason:
1660                     unavailable_message = extract_unavailable_message()
1661                     if unavailable_message:
1662                         reason = unavailable_message
1663                 raise ExtractorError(
1664                     'YouTube said: %s' % reason,
1665                     expected=True, video_id=video_id)
1666             else:
1667                 raise ExtractorError(
1668                     '"token" parameter not in video info for unknown reason',
1669                     video_id=video_id)
1670
1671         video_details = try_get(
1672             player_response, lambda x: x['videoDetails'], dict) or {}
1673
1674         # title
1675         if 'title' in video_info:
1676             video_title = video_info['title'][0]
1677         elif 'title' in player_response:
1678             video_title = video_details['title']
1679         else:
1680             self._downloader.report_warning('Unable to extract video title')
1681             video_title = '_'
1682
1683         # description
1684         description_original = video_description = get_element_by_id("eow-description", video_webpage)
1685         if video_description:
1686
1687             def replace_url(m):
1688                 redir_url = compat_urlparse.urljoin(url, m.group(1))
1689                 parsed_redir_url = compat_urllib_parse_urlparse(redir_url)
1690                 if re.search(r'^(?:www\.)?(?:youtube(?:-nocookie)?\.com|youtu\.be)$', parsed_redir_url.netloc) and parsed_redir_url.path == '/redirect':
1691                     qs = compat_parse_qs(parsed_redir_url.query)
1692                     q = qs.get('q')
1693                     if q and q[0]:
1694                         return q[0]
1695                 return redir_url
1696
1697             description_original = video_description = re.sub(r'''(?x)
1698                 <a\s+
1699                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1700                     (?:title|href)="([^"]+)"\s+
1701                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1702                     class="[^"]*"[^>]*>
1703                 [^<]+\.{3}\s*
1704                 </a>
1705             ''', replace_url, video_description)
1706             video_description = clean_html(video_description)
1707         else:
1708             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
1709             if fd_mobj:
1710                 video_description = unescapeHTML(fd_mobj.group(1))
1711             else:
1712                 video_description = ''
1713
1714         if 'multifeed_metadata_list' in video_info and not smuggled_data.get('force_singlefeed', False):
1715             if not self._downloader.params.get('noplaylist'):
1716                 entries = []
1717                 feed_ids = []
1718                 multifeed_metadata_list = video_info['multifeed_metadata_list'][0]
1719                 for feed in multifeed_metadata_list.split(','):
1720                     # Unquote should take place before split on comma (,) since textual
1721                     # fields may contain comma as well (see
1722                     # https://github.com/rg3/youtube-dl/issues/8536)
1723                     feed_data = compat_parse_qs(compat_urllib_parse_unquote_plus(feed))
1724                     entries.append({
1725                         '_type': 'url_transparent',
1726                         'ie_key': 'Youtube',
1727                         'url': smuggle_url(
1728                             '%s://www.youtube.com/watch?v=%s' % (proto, feed_data['id'][0]),
1729                             {'force_singlefeed': True}),
1730                         'title': '%s (%s)' % (video_title, feed_data['title'][0]),
1731                     })
1732                     feed_ids.append(feed_data['id'][0])
1733                 self.to_screen(
1734                     'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
1735                     % (', '.join(feed_ids), video_id))
1736                 return self.playlist_result(entries, video_id, video_title, video_description)
1737             self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1738
1739         if view_count is None:
1740             view_count = extract_view_count(video_info)
1741         if view_count is None and video_details:
1742             view_count = int_or_none(video_details.get('viewCount'))
1743
1744         # Check for "rental" videos
1745         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
1746             raise ExtractorError('"rental" videos not supported. See https://github.com/rg3/youtube-dl/issues/359 for more information.', expected=True)
1747
1748         def _extract_filesize(media_url):
1749             return int_or_none(self._search_regex(
1750                 r'\bclen[=/](\d+)', media_url, 'filesize', default=None))
1751
1752         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
1753             self.report_rtmp_download()
1754             formats = [{
1755                 'format_id': '_rtmp',
1756                 'protocol': 'rtmp',
1757                 'url': video_info['conn'][0],
1758                 'player_url': player_url,
1759             }]
1760         elif not is_live and (len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or len(video_info.get('adaptive_fmts', [''])[0]) >= 1):
1761             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts', [''])[0]
1762             if 'rtmpe%3Dyes' in encoded_url_map:
1763                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
1764             formats_spec = {}
1765             fmt_list = video_info.get('fmt_list', [''])[0]
1766             if fmt_list:
1767                 for fmt in fmt_list.split(','):
1768                     spec = fmt.split('/')
1769                     if len(spec) > 1:
1770                         width_height = spec[1].split('x')
1771                         if len(width_height) == 2:
1772                             formats_spec[spec[0]] = {
1773                                 'resolution': spec[1],
1774                                 'width': int_or_none(width_height[0]),
1775                                 'height': int_or_none(width_height[1]),
1776                             }
1777             q = qualities(['small', 'medium', 'hd720'])
1778             formats = []
1779             for url_data_str in encoded_url_map.split(','):
1780                 url_data = compat_parse_qs(url_data_str)
1781                 if 'itag' not in url_data or 'url' not in url_data:
1782                     continue
1783                 format_id = url_data['itag'][0]
1784                 url = url_data['url'][0]
1785
1786                 if 's' in url_data or self._downloader.params.get('youtube_include_dash_manifest', True):
1787                     ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
1788                     jsplayer_url_json = self._search_regex(
1789                         ASSETS_RE,
1790                         embed_webpage if age_gate else video_webpage,
1791                         'JS player URL (1)', default=None)
1792                     if not jsplayer_url_json and not age_gate:
1793                         # We need the embed website after all
1794                         if embed_webpage is None:
1795                             embed_url = proto + '://www.youtube.com/embed/%s' % video_id
1796                             embed_webpage = self._download_webpage(
1797                                 embed_url, video_id, 'Downloading embed webpage')
1798                         jsplayer_url_json = self._search_regex(
1799                             ASSETS_RE, embed_webpage, 'JS player URL')
1800
1801                     player_url = json.loads(jsplayer_url_json)
1802                     if player_url is None:
1803                         player_url_json = self._search_regex(
1804                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
1805                             video_webpage, 'age gate player URL')
1806                         player_url = json.loads(player_url_json)
1807
1808                 if 'sig' in url_data:
1809                     url += '&signature=' + url_data['sig'][0]
1810                 elif 's' in url_data:
1811                     encrypted_sig = url_data['s'][0]
1812
1813                     if self._downloader.params.get('verbose'):
1814                         if player_url is None:
1815                             player_version = 'unknown'
1816                             player_desc = 'unknown'
1817                         else:
1818                             if player_url.endswith('swf'):
1819                                 player_version = self._search_regex(
1820                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
1821                                     'flash player', fatal=False)
1822                                 player_desc = 'flash player %s' % player_version
1823                             else:
1824                                 player_version = self._search_regex(
1825                                     [r'html5player-([^/]+?)(?:/html5player(?:-new)?)?\.js',
1826                                      r'(?:www|player)-([^/]+)(?:/[a-z]{2}_[A-Z]{2})?/base\.js'],
1827                                     player_url,
1828                                     'html5 player', fatal=False)
1829                                 player_desc = 'html5 player %s' % player_version
1830
1831                         parts_sizes = self._signature_cache_id(encrypted_sig)
1832                         self.to_screen('{%s} signature length %s, %s' %
1833                                        (format_id, parts_sizes, player_desc))
1834
1835                     signature = self._decrypt_signature(
1836                         encrypted_sig, video_id, player_url, age_gate)
1837                     url += '&signature=' + signature
1838                 if 'ratebypass' not in url:
1839                     url += '&ratebypass=yes'
1840
1841                 dct = {
1842                     'format_id': format_id,
1843                     'url': url,
1844                     'player_url': player_url,
1845                 }
1846                 if format_id in self._formats:
1847                     dct.update(self._formats[format_id])
1848                 if format_id in formats_spec:
1849                     dct.update(formats_spec[format_id])
1850
1851                 # Some itags are not included in DASH manifest thus corresponding formats will
1852                 # lack metadata (see https://github.com/rg3/youtube-dl/pull/5993).
1853                 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
1854                 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
1855                 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
1856
1857                 filesize = int_or_none(url_data.get(
1858                     'clen', [None])[0]) or _extract_filesize(url)
1859
1860                 quality = url_data.get('quality_label', [None])[0] or url_data.get('quality', [None])[0]
1861
1862                 more_fields = {
1863                     'filesize': filesize,
1864                     'tbr': float_or_none(url_data.get('bitrate', [None])[0], 1000),
1865                     'width': width,
1866                     'height': height,
1867                     'fps': int_or_none(url_data.get('fps', [None])[0]),
1868                     'format_note': quality,
1869                     'quality': q(quality),
1870                 }
1871                 for key, value in more_fields.items():
1872                     if value:
1873                         dct[key] = value
1874                 type_ = url_data.get('type', [None])[0]
1875                 if type_:
1876                     type_split = type_.split(';')
1877                     kind_ext = type_split[0].split('/')
1878                     if len(kind_ext) == 2:
1879                         kind, _ = kind_ext
1880                         dct['ext'] = mimetype2ext(type_split[0])
1881                         if kind in ('audio', 'video'):
1882                             codecs = None
1883                             for mobj in re.finditer(
1884                                     r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?)(?P<val>.+?)(?P=quote)(?:;|$)', type_):
1885                                 if mobj.group('key') == 'codecs':
1886                                     codecs = mobj.group('val')
1887                                     break
1888                             if codecs:
1889                                 dct.update(parse_codecs(codecs))
1890                 if dct.get('acodec') == 'none' or dct.get('vcodec') == 'none':
1891                     dct['downloader_options'] = {
1892                         # Youtube throttles chunks >~10M
1893                         'http_chunk_size': 10485760,
1894                     }
1895                 formats.append(dct)
1896         elif video_info.get('hlsvp'):
1897             manifest_url = video_info['hlsvp'][0]
1898             formats = []
1899             m3u8_formats = self._extract_m3u8_formats(
1900                 manifest_url, video_id, 'mp4', fatal=False)
1901             for a_format in m3u8_formats:
1902                 itag = self._search_regex(
1903                     r'/itag/(\d+)/', a_format['url'], 'itag', default=None)
1904                 if itag:
1905                     a_format['format_id'] = itag
1906                     if itag in self._formats:
1907                         dct = self._formats[itag].copy()
1908                         dct.update(a_format)
1909                         a_format = dct
1910                 a_format['player_url'] = player_url
1911                 # Accept-Encoding header causes failures in live streams on Youtube and Youtube Gaming
1912                 a_format.setdefault('http_headers', {})['Youtubedl-no-compression'] = 'True'
1913                 formats.append(a_format)
1914         else:
1915             error_message = clean_html(video_info.get('reason', [None])[0])
1916             if not error_message:
1917                 error_message = extract_unavailable_message()
1918             if error_message:
1919                 raise ExtractorError(error_message, expected=True)
1920             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1921
1922         # uploader
1923         video_uploader = try_get(
1924             video_info, lambda x: x['author'][0],
1925             compat_str) or str_or_none(video_details.get('author'))
1926         if video_uploader:
1927             video_uploader = compat_urllib_parse_unquote_plus(video_uploader)
1928         else:
1929             self._downloader.report_warning('unable to extract uploader name')
1930
1931         # uploader_id
1932         video_uploader_id = None
1933         video_uploader_url = None
1934         mobj = re.search(
1935             r'<link itemprop="url" href="(?P<uploader_url>https?://www\.youtube\.com/(?:user|channel)/(?P<uploader_id>[^"]+))">',
1936             video_webpage)
1937         if mobj is not None:
1938             video_uploader_id = mobj.group('uploader_id')
1939             video_uploader_url = mobj.group('uploader_url')
1940         else:
1941             self._downloader.report_warning('unable to extract uploader nickname')
1942
1943         channel_id = self._html_search_meta(
1944             'channelId', video_webpage, 'channel id')
1945         channel_url = 'http://www.youtube.com/channel/%s' % channel_id if channel_id else None
1946
1947         # thumbnail image
1948         # We try first to get a high quality image:
1949         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
1950                             video_webpage, re.DOTALL)
1951         if m_thumb is not None:
1952             video_thumbnail = m_thumb.group(1)
1953         elif 'thumbnail_url' not in video_info:
1954             self._downloader.report_warning('unable to extract video thumbnail')
1955             video_thumbnail = None
1956         else:   # don't panic if we can't find it
1957             video_thumbnail = compat_urllib_parse_unquote_plus(video_info['thumbnail_url'][0])
1958
1959         # upload date
1960         upload_date = self._html_search_meta(
1961             'datePublished', video_webpage, 'upload date', default=None)
1962         if not upload_date:
1963             upload_date = self._search_regex(
1964                 [r'(?s)id="eow-date.*?>(.*?)</span>',
1965                  r'(?:id="watch-uploader-info".*?>.*?|["\']simpleText["\']\s*:\s*["\'])(?:Published|Uploaded|Streamed live|Started) on (.+?)[<"\']'],
1966                 video_webpage, 'upload date', default=None)
1967         upload_date = unified_strdate(upload_date)
1968
1969         video_license = self._html_search_regex(
1970             r'<h4[^>]+class="title"[^>]*>\s*License\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li',
1971             video_webpage, 'license', default=None)
1972
1973         m_music = re.search(
1974             r'''(?x)
1975                 <h4[^>]+class="title"[^>]*>\s*Music\s*</h4>\s*
1976                 <ul[^>]*>\s*
1977                 <li>(?P<title>.+?)
1978                 by (?P<creator>.+?)
1979                 (?:
1980                     \(.+?\)|
1981                     <a[^>]*
1982                         (?:
1983                             \bhref=["\']/red[^>]*>|             # drop possible
1984                             >\s*Listen ad-free with YouTube Red # YouTube Red ad
1985                         )
1986                     .*?
1987                 )?</li
1988             ''',
1989             video_webpage)
1990         if m_music:
1991             video_alt_title = remove_quotes(unescapeHTML(m_music.group('title')))
1992             video_creator = clean_html(m_music.group('creator'))
1993         else:
1994             video_alt_title = video_creator = None
1995
1996         def extract_meta(field):
1997             return self._html_search_regex(
1998                 r'<h4[^>]+class="title"[^>]*>\s*%s\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li>\s*' % field,
1999                 video_webpage, field, default=None)
2000
2001         track = extract_meta('Song')
2002         artist = extract_meta('Artist')
2003
2004         m_episode = re.search(
2005             r'<div[^>]+id="watch7-headline"[^>]*>\s*<span[^>]*>.*?>(?P<series>[^<]+)</a></b>\s*S(?P<season>\d+)\s*•\s*E(?P<episode>\d+)</span>',
2006             video_webpage)
2007         if m_episode:
2008             series = m_episode.group('series')
2009             season_number = int(m_episode.group('season'))
2010             episode_number = int(m_episode.group('episode'))
2011         else:
2012             series = season_number = episode_number = None
2013
2014         m_cat_container = self._search_regex(
2015             r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
2016             video_webpage, 'categories', default=None)
2017         if m_cat_container:
2018             category = self._html_search_regex(
2019                 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
2020                 default=None)
2021             video_categories = None if category is None else [category]
2022         else:
2023             video_categories = None
2024
2025         video_tags = [
2026             unescapeHTML(m.group('content'))
2027             for m in re.finditer(self._meta_regex('og:video:tag'), video_webpage)]
2028
2029         def _extract_count(count_name):
2030             return str_to_int(self._search_regex(
2031                 r'-%s-button[^>]+><span[^>]+class="yt-uix-button-content"[^>]*>([\d,]+)</span>'
2032                 % re.escape(count_name),
2033                 video_webpage, count_name, default=None))
2034
2035         like_count = _extract_count('like')
2036         dislike_count = _extract_count('dislike')
2037
2038         if view_count is None:
2039             view_count = str_to_int(self._search_regex(
2040                 r'<[^>]+class=["\']watch-view-count[^>]+>\s*([\d,\s]+)', video_webpage,
2041                 'view count', default=None))
2042
2043         # subtitles
2044         video_subtitles = self.extract_subtitles(video_id, video_webpage)
2045         automatic_captions = self.extract_automatic_captions(video_id, video_webpage)
2046
2047         video_duration = try_get(
2048             video_info, lambda x: int_or_none(x['length_seconds'][0]))
2049         if not video_duration:
2050             video_duration = int_or_none(video_details.get('lengthSeconds'))
2051         if not video_duration:
2052             video_duration = parse_duration(self._html_search_meta(
2053                 'duration', video_webpage, 'video duration'))
2054
2055         # annotations
2056         video_annotations = None
2057         if self._downloader.params.get('writeannotations', False):
2058             video_annotations = self._extract_annotations(video_id)
2059
2060         chapters = self._extract_chapters(description_original, video_duration)
2061
2062         # Look for the DASH manifest
2063         if self._downloader.params.get('youtube_include_dash_manifest', True):
2064             dash_mpd_fatal = True
2065             for mpd_url in dash_mpds:
2066                 dash_formats = {}
2067                 try:
2068                     def decrypt_sig(mobj):
2069                         s = mobj.group(1)
2070                         dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
2071                         return '/signature/%s' % dec_s
2072
2073                     mpd_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, mpd_url)
2074
2075                     for df in self._extract_mpd_formats(
2076                             mpd_url, video_id, fatal=dash_mpd_fatal,
2077                             formats_dict=self._formats):
2078                         if not df.get('filesize'):
2079                             df['filesize'] = _extract_filesize(df['url'])
2080                         # Do not overwrite DASH format found in some previous DASH manifest
2081                         if df['format_id'] not in dash_formats:
2082                             dash_formats[df['format_id']] = df
2083                         # Additional DASH manifests may end up in HTTP Error 403 therefore
2084                         # allow them to fail without bug report message if we already have
2085                         # some DASH manifest succeeded. This is temporary workaround to reduce
2086                         # burst of bug reports until we figure out the reason and whether it
2087                         # can be fixed at all.
2088                         dash_mpd_fatal = False
2089                 except (ExtractorError, KeyError) as e:
2090                     self.report_warning(
2091                         'Skipping DASH manifest: %r' % e, video_id)
2092                 if dash_formats:
2093                     # Remove the formats we found through non-DASH, they
2094                     # contain less info and it can be wrong, because we use
2095                     # fixed values (for example the resolution). See
2096                     # https://github.com/rg3/youtube-dl/issues/5774 for an
2097                     # example.
2098                     formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
2099                     formats.extend(dash_formats.values())
2100
2101         # Check for malformed aspect ratio
2102         stretched_m = re.search(
2103             r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
2104             video_webpage)
2105         if stretched_m:
2106             w = float(stretched_m.group('w'))
2107             h = float(stretched_m.group('h'))
2108             # yt:stretch may hold invalid ratio data (e.g. for Q39EVAstoRM ratio is 17:0).
2109             # We will only process correct ratios.
2110             if w > 0 and h > 0:
2111                 ratio = w / h
2112                 for f in formats:
2113                     if f.get('vcodec') != 'none':
2114                         f['stretched_ratio'] = ratio
2115
2116         self._sort_formats(formats)
2117
2118         self.mark_watched(video_id, video_info)
2119
2120         return {
2121             'id': video_id,
2122             'uploader': video_uploader,
2123             'uploader_id': video_uploader_id,
2124             'uploader_url': video_uploader_url,
2125             'channel_id': channel_id,
2126             'channel_url': channel_url,
2127             'upload_date': upload_date,
2128             'license': video_license,
2129             'creator': video_creator or artist,
2130             'title': video_title,
2131             'alt_title': video_alt_title or track,
2132             'thumbnail': video_thumbnail,
2133             'description': video_description,
2134             'categories': video_categories,
2135             'tags': video_tags,
2136             'subtitles': video_subtitles,
2137             'automatic_captions': automatic_captions,
2138             'duration': video_duration,
2139             'age_limit': 18 if age_gate else 0,
2140             'annotations': video_annotations,
2141             'chapters': chapters,
2142             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
2143             'view_count': view_count,
2144             'like_count': like_count,
2145             'dislike_count': dislike_count,
2146             'average_rating': float_or_none(video_info.get('avg_rating', [None])[0]),
2147             'formats': formats,
2148             'is_live': is_live,
2149             'start_time': start_time,
2150             'end_time': end_time,
2151             'series': series,
2152             'season_number': season_number,
2153             'episode_number': episode_number,
2154             'track': track,
2155             'artist': artist,
2156         }
2157
2158
2159 class YoutubePlaylistIE(YoutubePlaylistBaseInfoExtractor):
2160     IE_DESC = 'YouTube.com playlists'
2161     _VALID_URL = r"""(?x)(?:
2162                         (?:https?://)?
2163                         (?:\w+\.)?
2164                         (?:
2165                             youtube\.com/
2166                             (?:
2167                                (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/(?:videoseries|[0-9A-Za-z_-]{11}))
2168                                \? (?:.*?[&;])*? (?:p|a|list)=
2169                             |  p/
2170                             )|
2171                             youtu\.be/[0-9A-Za-z_-]{11}\?.*?\blist=
2172                         )
2173                         (
2174                             (?:PL|LL|EC|UU|FL|RD|UL|TL|OLAK5uy_)?[0-9A-Za-z-_]{10,}
2175                             # Top tracks, they can also include dots
2176                             |(?:MC)[\w\.]*
2177                         )
2178                         .*
2179                      |
2180                         (%(playlist_id)s)
2181                      )""" % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
2182     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
2183     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)(?:[^>]+>(?P<title>[^<]+))?'
2184     IE_NAME = 'youtube:playlist'
2185     _TESTS = [{
2186         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
2187         'info_dict': {
2188             'title': 'ytdl test PL',
2189             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
2190         },
2191         'playlist_count': 3,
2192     }, {
2193         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
2194         'info_dict': {
2195             'id': 'PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
2196             'title': 'YDL_Empty_List',
2197         },
2198         'playlist_count': 0,
2199         'skip': 'This playlist is private',
2200     }, {
2201         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
2202         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2203         'info_dict': {
2204             'title': '29C3: Not my department',
2205             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2206         },
2207         'playlist_count': 95,
2208     }, {
2209         'note': 'issue #673',
2210         'url': 'PLBB231211A4F62143',
2211         'info_dict': {
2212             'title': '[OLD]Team Fortress 2 (Class-based LP)',
2213             'id': 'PLBB231211A4F62143',
2214         },
2215         'playlist_mincount': 26,
2216     }, {
2217         'note': 'Large playlist',
2218         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
2219         'info_dict': {
2220             'title': 'Uploads from Cauchemar',
2221             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
2222         },
2223         'playlist_mincount': 799,
2224     }, {
2225         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
2226         'info_dict': {
2227             'title': 'YDL_safe_search',
2228             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
2229         },
2230         'playlist_count': 2,
2231         'skip': 'This playlist is private',
2232     }, {
2233         'note': 'embedded',
2234         'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
2235         'playlist_count': 4,
2236         'info_dict': {
2237             'title': 'JODA15',
2238             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
2239         }
2240     }, {
2241         'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
2242         'playlist_mincount': 485,
2243         'info_dict': {
2244             'title': '2017 華語最新單曲 (2/24更新)',
2245             'id': 'PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
2246         }
2247     }, {
2248         'note': 'Embedded SWF player',
2249         'url': 'https://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
2250         'playlist_count': 4,
2251         'info_dict': {
2252             'title': 'JODA7',
2253             'id': 'YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ',
2254         }
2255     }, {
2256         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
2257         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
2258         'info_dict': {
2259             'title': 'Uploads from Interstellar Movie',
2260             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
2261         },
2262         'playlist_mincount': 21,
2263     }, {
2264         # Playlist URL that does not actually serve a playlist
2265         'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
2266         'info_dict': {
2267             'id': 'FqZTN594JQw',
2268             'ext': 'webm',
2269             'title': "Smiley's People 01 detective, Adventure Series, Action",
2270             'uploader': 'STREEM',
2271             'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
2272             'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
2273             'upload_date': '20150526',
2274             'license': 'Standard YouTube License',
2275             'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
2276             'categories': ['People & Blogs'],
2277             'tags': list,
2278             'view_count': int,
2279             'like_count': int,
2280             'dislike_count': int,
2281         },
2282         'params': {
2283             'skip_download': True,
2284         },
2285         'add_ie': [YoutubeIE.ie_key()],
2286     }, {
2287         'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
2288         'info_dict': {
2289             'id': 'yeWKywCrFtk',
2290             'ext': 'mp4',
2291             'title': 'Small Scale Baler and Braiding Rugs',
2292             'uploader': 'Backus-Page House Museum',
2293             'uploader_id': 'backuspagemuseum',
2294             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
2295             'upload_date': '20161008',
2296             'license': 'Standard YouTube License',
2297             'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
2298             'categories': ['Nonprofits & Activism'],
2299             'tags': list,
2300             'like_count': int,
2301             'dislike_count': int,
2302         },
2303         'params': {
2304             'noplaylist': True,
2305             'skip_download': True,
2306         },
2307     }, {
2308         'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
2309         'only_matching': True,
2310     }, {
2311         'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
2312         'only_matching': True,
2313     }, {
2314         # music album playlist
2315         'url': 'OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM',
2316         'only_matching': True,
2317     }]
2318
2319     def _real_initialize(self):
2320         self._login()
2321
2322     def _extract_mix(self, playlist_id):
2323         # The mixes are generated from a single video
2324         # the id of the playlist is just 'RD' + video_id
2325         ids = []
2326         last_id = playlist_id[-11:]
2327         for n in itertools.count(1):
2328             url = 'https://youtube.com/watch?v=%s&list=%s' % (last_id, playlist_id)
2329             webpage = self._download_webpage(
2330                 url, playlist_id, 'Downloading page {0} of Youtube mix'.format(n))
2331             new_ids = orderedSet(re.findall(
2332                 r'''(?xs)data-video-username=".*?".*?
2333                            href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
2334                 webpage))
2335             # Fetch new pages until all the videos are repeated, it seems that
2336             # there are always 51 unique videos.
2337             new_ids = [_id for _id in new_ids if _id not in ids]
2338             if not new_ids:
2339                 break
2340             ids.extend(new_ids)
2341             last_id = ids[-1]
2342
2343         url_results = self._ids_to_results(ids)
2344
2345         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
2346         title_span = (
2347             search_title('playlist-title') or
2348             search_title('title long-title') or
2349             search_title('title'))
2350         title = clean_html(title_span)
2351
2352         return self.playlist_result(url_results, playlist_id, title)
2353
2354     def _extract_playlist(self, playlist_id):
2355         url = self._TEMPLATE_URL % playlist_id
2356         page = self._download_webpage(url, playlist_id)
2357
2358         # the yt-alert-message now has tabindex attribute (see https://github.com/rg3/youtube-dl/issues/11604)
2359         for match in re.findall(r'<div class="yt-alert-message"[^>]*>([^<]+)</div>', page):
2360             match = match.strip()
2361             # Check if the playlist exists or is private
2362             mobj = re.match(r'[^<]*(?:The|This) playlist (?P<reason>does not exist|is private)[^<]*', match)
2363             if mobj:
2364                 reason = mobj.group('reason')
2365                 message = 'This playlist %s' % reason
2366                 if 'private' in reason:
2367                     message += ', use --username or --netrc to access it'
2368                 message += '.'
2369                 raise ExtractorError(message, expected=True)
2370             elif re.match(r'[^<]*Invalid parameters[^<]*', match):
2371                 raise ExtractorError(
2372                     'Invalid parameters. Maybe URL is incorrect.',
2373                     expected=True)
2374             elif re.match(r'[^<]*Choose your language[^<]*', match):
2375                 continue
2376             else:
2377                 self.report_warning('Youtube gives an alert message: ' + match)
2378
2379         playlist_title = self._html_search_regex(
2380             r'(?s)<h1 class="pl-header-title[^"]*"[^>]*>\s*(.*?)\s*</h1>',
2381             page, 'title', default=None)
2382
2383         _UPLOADER_BASE = r'class=["\']pl-header-details[^>]+>\s*<li>\s*<a[^>]+\bhref='
2384         uploader = self._search_regex(
2385             r'%s["\']/(?:user|channel)/[^>]+>([^<]+)' % _UPLOADER_BASE,
2386             page, 'uploader', default=None)
2387         mobj = re.search(
2388             r'%s(["\'])(?P<path>/(?:user|channel)/(?P<uploader_id>.+?))\1' % _UPLOADER_BASE,
2389             page)
2390         if mobj:
2391             uploader_id = mobj.group('uploader_id')
2392             uploader_url = compat_urlparse.urljoin(url, mobj.group('path'))
2393         else:
2394             uploader_id = uploader_url = None
2395
2396         has_videos = True
2397
2398         if not playlist_title:
2399             try:
2400                 # Some playlist URLs don't actually serve a playlist (e.g.
2401                 # https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4)
2402                 next(self._entries(page, playlist_id))
2403             except StopIteration:
2404                 has_videos = False
2405
2406         playlist = self.playlist_result(
2407             self._entries(page, playlist_id), playlist_id, playlist_title)
2408         playlist.update({
2409             'uploader': uploader,
2410             'uploader_id': uploader_id,
2411             'uploader_url': uploader_url,
2412         })
2413
2414         return has_videos, playlist
2415
2416     def _check_download_just_video(self, url, playlist_id):
2417         # Check if it's a video-specific URL
2418         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
2419         video_id = query_dict.get('v', [None])[0] or self._search_regex(
2420             r'(?:(?:^|//)youtu\.be/|youtube\.com/embed/(?!videoseries))([0-9A-Za-z_-]{11})', url,
2421             'video id', default=None)
2422         if video_id:
2423             if self._downloader.params.get('noplaylist'):
2424                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
2425                 return video_id, self.url_result(video_id, 'Youtube', video_id=video_id)
2426             else:
2427                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
2428                 return video_id, None
2429         return None, None
2430
2431     def _real_extract(self, url):
2432         # Extract playlist id
2433         mobj = re.match(self._VALID_URL, url)
2434         if mobj is None:
2435             raise ExtractorError('Invalid URL: %s' % url)
2436         playlist_id = mobj.group(1) or mobj.group(2)
2437
2438         video_id, video = self._check_download_just_video(url, playlist_id)
2439         if video:
2440             return video
2441
2442         if playlist_id.startswith(('RD', 'UL', 'PU')):
2443             # Mixes require a custom extraction process
2444             return self._extract_mix(playlist_id)
2445
2446         has_videos, playlist = self._extract_playlist(playlist_id)
2447         if has_videos or not video_id:
2448             return playlist
2449
2450         # Some playlist URLs don't actually serve a playlist (see
2451         # https://github.com/rg3/youtube-dl/issues/10537).
2452         # Fallback to plain video extraction if there is a video id
2453         # along with playlist id.
2454         return self.url_result(video_id, 'Youtube', video_id=video_id)
2455
2456
2457 class YoutubeChannelIE(YoutubePlaylistBaseInfoExtractor):
2458     IE_DESC = 'YouTube.com channels'
2459     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com|(?:www\.)?invidio\.us)/channel/(?P<id>[0-9A-Za-z_-]+)'
2460     _TEMPLATE_URL = 'https://www.youtube.com/channel/%s/videos'
2461     _VIDEO_RE = r'(?:title="(?P<title>[^"]+)"[^>]+)?href="/watch\?v=(?P<id>[0-9A-Za-z_-]+)&?'
2462     IE_NAME = 'youtube:channel'
2463     _TESTS = [{
2464         'note': 'paginated channel',
2465         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
2466         'playlist_mincount': 91,
2467         'info_dict': {
2468             'id': 'UUKfVa3S1e4PHvxWcwyMMg8w',
2469             'title': 'Uploads from lex will',
2470         }
2471     }, {
2472         'note': 'Age restricted channel',
2473         # from https://www.youtube.com/user/DeusExOfficial
2474         'url': 'https://www.youtube.com/channel/UCs0ifCMCm1icqRbqhUINa0w',
2475         'playlist_mincount': 64,
2476         'info_dict': {
2477             'id': 'UUs0ifCMCm1icqRbqhUINa0w',
2478             'title': 'Uploads from Deus Ex',
2479         },
2480     }, {
2481         'url': 'https://invidio.us/channel/UC23qupoDRn9YOAVzeoxjOQA',
2482         'only_matching': True,
2483     }]
2484
2485     @classmethod
2486     def suitable(cls, url):
2487         return (False if YoutubePlaylistsIE.suitable(url) or YoutubeLiveIE.suitable(url)
2488                 else super(YoutubeChannelIE, cls).suitable(url))
2489
2490     def _build_template_url(self, url, channel_id):
2491         return self._TEMPLATE_URL % channel_id
2492
2493     def _real_extract(self, url):
2494         channel_id = self._match_id(url)
2495
2496         url = self._build_template_url(url, channel_id)
2497
2498         # Channel by page listing is restricted to 35 pages of 30 items, i.e. 1050 videos total (see #5778)
2499         # Workaround by extracting as a playlist if managed to obtain channel playlist URL
2500         # otherwise fallback on channel by page extraction
2501         channel_page = self._download_webpage(
2502             url + '?view=57', channel_id,
2503             'Downloading channel page', fatal=False)
2504         if channel_page is False:
2505             channel_playlist_id = False
2506         else:
2507             channel_playlist_id = self._html_search_meta(
2508                 'channelId', channel_page, 'channel id', default=None)
2509             if not channel_playlist_id:
2510                 channel_url = self._html_search_meta(
2511                     ('al:ios:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad'),
2512                     channel_page, 'channel url', default=None)
2513                 if channel_url:
2514                     channel_playlist_id = self._search_regex(
2515                         r'vnd\.youtube://user/([0-9A-Za-z_-]+)',
2516                         channel_url, 'channel id', default=None)
2517         if channel_playlist_id and channel_playlist_id.startswith('UC'):
2518             playlist_id = 'UU' + channel_playlist_id[2:]
2519             return self.url_result(
2520                 compat_urlparse.urljoin(url, '/playlist?list=%s' % playlist_id), 'YoutubePlaylist')
2521
2522         channel_page = self._download_webpage(url, channel_id, 'Downloading page #1')
2523         autogenerated = re.search(r'''(?x)
2524                 class="[^"]*?(?:
2525                     channel-header-autogenerated-label|
2526                     yt-channel-title-autogenerated
2527                 )[^"]*"''', channel_page) is not None
2528
2529         if autogenerated:
2530             # The videos are contained in a single page
2531             # the ajax pages can't be used, they are empty
2532             entries = [
2533                 self.url_result(
2534                     video_id, 'Youtube', video_id=video_id,
2535                     video_title=video_title)
2536                 for video_id, video_title in self.extract_videos_from_page(channel_page)]
2537             return self.playlist_result(entries, channel_id)
2538
2539         try:
2540             next(self._entries(channel_page, channel_id))
2541         except StopIteration:
2542             alert_message = self._html_search_regex(
2543                 r'(?s)<div[^>]+class=(["\']).*?\byt-alert-message\b.*?\1[^>]*>(?P<alert>[^<]+)</div>',
2544                 channel_page, 'alert', default=None, group='alert')
2545             if alert_message:
2546                 raise ExtractorError('Youtube said: %s' % alert_message, expected=True)
2547
2548         return self.playlist_result(self._entries(channel_page, channel_id), channel_id)
2549
2550
2551 class YoutubeUserIE(YoutubeChannelIE):
2552     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
2553     _VALID_URL = r'(?:(?:https?://(?:\w+\.)?youtube\.com/(?:(?P<user>user|c)/)?(?!(?:attribution_link|watch|results|shared)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)(?P<id>[A-Za-z0-9_-]+)'
2554     _TEMPLATE_URL = 'https://www.youtube.com/%s/%s/videos'
2555     IE_NAME = 'youtube:user'
2556
2557     _TESTS = [{
2558         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
2559         'playlist_mincount': 320,
2560         'info_dict': {
2561             'id': 'UUfX55Sx5hEFjoC3cNs6mCUQ',
2562             'title': 'Uploads from The Linux Foundation',
2563         }
2564     }, {
2565         # Only available via https://www.youtube.com/c/12minuteathlete/videos
2566         # but not https://www.youtube.com/user/12minuteathlete/videos
2567         'url': 'https://www.youtube.com/c/12minuteathlete/videos',
2568         'playlist_mincount': 249,
2569         'info_dict': {
2570             'id': 'UUVjM-zV6_opMDx7WYxnjZiQ',
2571             'title': 'Uploads from 12 Minute Athlete',
2572         }
2573     }, {
2574         'url': 'ytuser:phihag',
2575         'only_matching': True,
2576     }, {
2577         'url': 'https://www.youtube.com/c/gametrailers',
2578         'only_matching': True,
2579     }, {
2580         'url': 'https://www.youtube.com/gametrailers',
2581         'only_matching': True,
2582     }, {
2583         # This channel is not available, geo restricted to JP
2584         'url': 'https://www.youtube.com/user/kananishinoSMEJ/videos',
2585         'only_matching': True,
2586     }]
2587
2588     @classmethod
2589     def suitable(cls, url):
2590         # Don't return True if the url can be extracted with other youtube
2591         # extractor, the regex would is too permissive and it would match.
2592         other_yt_ies = iter(klass for (name, klass) in globals().items() if name.startswith('Youtube') and name.endswith('IE') and klass is not cls)
2593         if any(ie.suitable(url) for ie in other_yt_ies):
2594             return False
2595         else:
2596             return super(YoutubeUserIE, cls).suitable(url)
2597
2598     def _build_template_url(self, url, channel_id):
2599         mobj = re.match(self._VALID_URL, url)
2600         return self._TEMPLATE_URL % (mobj.group('user') or 'user', mobj.group('id'))
2601
2602
2603 class YoutubeLiveIE(YoutubeBaseInfoExtractor):
2604     IE_DESC = 'YouTube.com live streams'
2605     _VALID_URL = r'(?P<base_url>https?://(?:\w+\.)?youtube\.com/(?:(?:user|channel|c)/)?(?P<id>[^/]+))/live'
2606     IE_NAME = 'youtube:live'
2607
2608     _TESTS = [{
2609         'url': 'https://www.youtube.com/user/TheYoungTurks/live',
2610         'info_dict': {
2611             'id': 'a48o2S1cPoo',
2612             'ext': 'mp4',
2613             'title': 'The Young Turks - Live Main Show',
2614             'uploader': 'The Young Turks',
2615             'uploader_id': 'TheYoungTurks',
2616             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
2617             'upload_date': '20150715',
2618             'license': 'Standard YouTube License',
2619             'description': 'md5:438179573adcdff3c97ebb1ee632b891',
2620             'categories': ['News & Politics'],
2621             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
2622             'like_count': int,
2623             'dislike_count': int,
2624         },
2625         'params': {
2626             'skip_download': True,
2627         },
2628     }, {
2629         'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
2630         'only_matching': True,
2631     }, {
2632         'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
2633         'only_matching': True,
2634     }, {
2635         'url': 'https://www.youtube.com/TheYoungTurks/live',
2636         'only_matching': True,
2637     }]
2638
2639     def _real_extract(self, url):
2640         mobj = re.match(self._VALID_URL, url)
2641         channel_id = mobj.group('id')
2642         base_url = mobj.group('base_url')
2643         webpage = self._download_webpage(url, channel_id, fatal=False)
2644         if webpage:
2645             page_type = self._og_search_property(
2646                 'type', webpage, 'page type', default='')
2647             video_id = self._html_search_meta(
2648                 'videoId', webpage, 'video id', default=None)
2649             if page_type.startswith('video') and video_id and re.match(
2650                     r'^[0-9A-Za-z_-]{11}$', video_id):
2651                 return self.url_result(video_id, YoutubeIE.ie_key())
2652         return self.url_result(base_url)
2653
2654
2655 class YoutubePlaylistsIE(YoutubePlaylistsBaseInfoExtractor):
2656     IE_DESC = 'YouTube.com user/channel playlists'
2657     _VALID_URL = r'https?://(?:\w+\.)?youtube\.com/(?:user|channel)/(?P<id>[^/]+)/playlists'
2658     IE_NAME = 'youtube:playlists'
2659
2660     _TESTS = [{
2661         'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
2662         'playlist_mincount': 4,
2663         'info_dict': {
2664             'id': 'ThirstForScience',
2665             'title': 'Thirst for Science',
2666         },
2667     }, {
2668         # with "Load more" button
2669         'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
2670         'playlist_mincount': 70,
2671         'info_dict': {
2672             'id': 'igorkle1',
2673             'title': 'Игорь Клейнер',
2674         },
2675     }, {
2676         'url': 'https://www.youtube.com/channel/UCiU1dHvZObB2iP6xkJ__Icw/playlists',
2677         'playlist_mincount': 17,
2678         'info_dict': {
2679             'id': 'UCiU1dHvZObB2iP6xkJ__Icw',
2680             'title': 'Chem Player',
2681         },
2682     }]
2683
2684
2685 class YoutubeSearchBaseInfoExtractor(YoutubePlaylistBaseInfoExtractor):
2686     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})(?:[^"]*"[^>]+\btitle="(?P<title>[^"]+))?'
2687
2688
2689 class YoutubeSearchIE(SearchInfoExtractor, YoutubeSearchBaseInfoExtractor):
2690     IE_DESC = 'YouTube.com searches'
2691     # there doesn't appear to be a real limit, for example if you search for
2692     # 'python' you get more than 8.000.000 results
2693     _MAX_RESULTS = float('inf')
2694     IE_NAME = 'youtube:search'
2695     _SEARCH_KEY = 'ytsearch'
2696     _EXTRA_QUERY_ARGS = {}
2697     _TESTS = []
2698
2699     def _get_n_results(self, query, n):
2700         """Get a specified number of results for a query"""
2701
2702         videos = []
2703         limit = n
2704
2705         url_query = {
2706             'search_query': query.encode('utf-8'),
2707         }
2708         url_query.update(self._EXTRA_QUERY_ARGS)
2709         result_url = 'https://www.youtube.com/results?' + compat_urllib_parse_urlencode(url_query)
2710
2711         for pagenum in itertools.count(1):
2712             data = self._download_json(
2713                 result_url, video_id='query "%s"' % query,
2714                 note='Downloading page %s' % pagenum,
2715                 errnote='Unable to download API page',
2716                 query={'spf': 'navigate'})
2717             html_content = data[1]['body']['content']
2718
2719             if 'class="search-message' in html_content:
2720                 raise ExtractorError(
2721                     '[youtube] No video results', expected=True)
2722
2723             new_videos = list(self._process_page(html_content))
2724             videos += new_videos
2725             if not new_videos or len(videos) > limit:
2726                 break
2727             next_link = self._html_search_regex(
2728                 r'href="(/results\?[^"]*\bsp=[^"]+)"[^>]*>\s*<span[^>]+class="[^"]*\byt-uix-button-content\b[^"]*"[^>]*>Next',
2729                 html_content, 'next link', default=None)
2730             if next_link is None:
2731                 break
2732             result_url = compat_urlparse.urljoin('https://www.youtube.com/', next_link)
2733
2734         if len(videos) > n:
2735             videos = videos[:n]
2736         return self.playlist_result(videos, query)
2737
2738
2739 class YoutubeSearchDateIE(YoutubeSearchIE):
2740     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
2741     _SEARCH_KEY = 'ytsearchdate'
2742     IE_DESC = 'YouTube.com searches, newest videos first'
2743     _EXTRA_QUERY_ARGS = {'search_sort': 'video_date_uploaded'}
2744
2745
2746 class YoutubeSearchURLIE(YoutubeSearchBaseInfoExtractor):
2747     IE_DESC = 'YouTube.com search URLs'
2748     IE_NAME = 'youtube:search_url'
2749     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?P<query>[^&]+)(?:[&]|$)'
2750     _TESTS = [{
2751         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
2752         'playlist_mincount': 5,
2753         'info_dict': {
2754             'title': 'youtube-dl test video',
2755         }
2756     }, {
2757         'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
2758         'only_matching': True,
2759     }]
2760
2761     def _real_extract(self, url):
2762         mobj = re.match(self._VALID_URL, url)
2763         query = compat_urllib_parse_unquote_plus(mobj.group('query'))
2764         webpage = self._download_webpage(url, query)
2765         return self.playlist_result(self._process_page(webpage), playlist_title=query)
2766
2767
2768 class YoutubeShowIE(YoutubePlaylistsBaseInfoExtractor):
2769     IE_DESC = 'YouTube.com (multi-season) shows'
2770     _VALID_URL = r'https?://(?:www\.)?youtube\.com/show/(?P<id>[^?#]*)'
2771     IE_NAME = 'youtube:show'
2772     _TESTS = [{
2773         'url': 'https://www.youtube.com/show/airdisasters',
2774         'playlist_mincount': 5,
2775         'info_dict': {
2776             'id': 'airdisasters',
2777             'title': 'Air Disasters',
2778         }
2779     }]
2780
2781     def _real_extract(self, url):
2782         playlist_id = self._match_id(url)
2783         return super(YoutubeShowIE, self)._real_extract(
2784             'https://www.youtube.com/show/%s/playlists' % playlist_id)
2785
2786
2787 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
2788     """
2789     Base class for feed extractors
2790     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
2791     """
2792     _LOGIN_REQUIRED = True
2793
2794     @property
2795     def IE_NAME(self):
2796         return 'youtube:%s' % self._FEED_NAME
2797
2798     def _real_initialize(self):
2799         self._login()
2800
2801     def _entries(self, page):
2802         # The extraction process is the same as for playlists, but the regex
2803         # for the video ids doesn't contain an index
2804         ids = []
2805         more_widget_html = content_html = page
2806         for page_num in itertools.count(1):
2807             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
2808
2809             # 'recommended' feed has infinite 'load more' and each new portion spins
2810             # the same videos in (sometimes) slightly different order, so we'll check
2811             # for unicity and break when portion has no new videos
2812             new_ids = list(filter(lambda video_id: video_id not in ids, orderedSet(matches)))
2813             if not new_ids:
2814                 break
2815
2816             ids.extend(new_ids)
2817
2818             for entry in self._ids_to_results(new_ids):
2819                 yield entry
2820
2821             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
2822             if not mobj:
2823                 break
2824
2825             more = self._download_json(
2826                 'https://youtube.com/%s' % mobj.group('more'), self._PLAYLIST_TITLE,
2827                 'Downloading page #%s' % page_num,
2828                 transform_source=uppercase_escape)
2829             content_html = more['content_html']
2830             more_widget_html = more['load_more_widget_html']
2831
2832     def _real_extract(self, url):
2833         page = self._download_webpage(
2834             'https://www.youtube.com/feed/%s' % self._FEED_NAME,
2835             self._PLAYLIST_TITLE)
2836         return self.playlist_result(
2837             self._entries(page), playlist_title=self._PLAYLIST_TITLE)
2838
2839
2840 class YoutubeWatchLaterIE(YoutubePlaylistIE):
2841     IE_NAME = 'youtube:watchlater'
2842     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
2843     _VALID_URL = r'https?://(?:www\.)?youtube\.com/(?:feed/watch_later|(?:playlist|watch)\?(?:.+&)?list=WL)|:ytwatchlater'
2844
2845     _TESTS = [{
2846         'url': 'https://www.youtube.com/playlist?list=WL',
2847         'only_matching': True,
2848     }, {
2849         'url': 'https://www.youtube.com/watch?v=bCNU9TrbiRk&index=1&list=WL',
2850         'only_matching': True,
2851     }]
2852
2853     def _real_extract(self, url):
2854         _, video = self._check_download_just_video(url, 'WL')
2855         if video:
2856             return video
2857         _, playlist = self._extract_playlist('WL')
2858         return playlist
2859
2860
2861 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
2862     IE_NAME = 'youtube:favorites'
2863     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
2864     _VALID_URL = r'https?://(?:www\.)?youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
2865     _LOGIN_REQUIRED = True
2866
2867     def _real_extract(self, url):
2868         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
2869         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
2870         return self.url_result(playlist_id, 'YoutubePlaylist')
2871
2872
2873 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
2874     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
2875     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/recommended|:ytrec(?:ommended)?'
2876     _FEED_NAME = 'recommended'
2877     _PLAYLIST_TITLE = 'Youtube Recommended videos'
2878
2879
2880 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
2881     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
2882     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
2883     _FEED_NAME = 'subscriptions'
2884     _PLAYLIST_TITLE = 'Youtube Subscriptions'
2885
2886
2887 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
2888     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
2889     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/history|:ythistory'
2890     _FEED_NAME = 'history'
2891     _PLAYLIST_TITLE = 'Youtube History'
2892
2893
2894 class YoutubeTruncatedURLIE(InfoExtractor):
2895     IE_NAME = 'youtube:truncated_url'
2896     IE_DESC = False  # Do not list
2897     _VALID_URL = r'''(?x)
2898         (?:https?://)?
2899         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
2900         (?:watch\?(?:
2901             feature=[a-z_]+|
2902             annotation_id=annotation_[^&]+|
2903             x-yt-cl=[0-9]+|
2904             hl=[^&]*|
2905             t=[0-9]+
2906         )?
2907         |
2908             attribution_link\?a=[^&]+
2909         )
2910         $
2911     '''
2912
2913     _TESTS = [{
2914         'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
2915         'only_matching': True,
2916     }, {
2917         'url': 'https://www.youtube.com/watch?',
2918         'only_matching': True,
2919     }, {
2920         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
2921         'only_matching': True,
2922     }, {
2923         'url': 'https://www.youtube.com/watch?feature=foo',
2924         'only_matching': True,
2925     }, {
2926         'url': 'https://www.youtube.com/watch?hl=en-GB',
2927         'only_matching': True,
2928     }, {
2929         'url': 'https://www.youtube.com/watch?t=2372',
2930         'only_matching': True,
2931     }]
2932
2933     def _real_extract(self, url):
2934         raise ExtractorError(
2935             'Did you forget to quote the URL? Remember that & is a meta '
2936             'character in most shells, so you want to put the URL in quotes, '
2937             'like  youtube-dl '
2938             '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
2939             ' or simply  youtube-dl BaW_jenozKc  .',
2940             expected=True)
2941
2942
2943 class YoutubeTruncatedIDIE(InfoExtractor):
2944     IE_NAME = 'youtube:truncated_id'
2945     IE_DESC = False  # Do not list
2946     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
2947
2948     _TESTS = [{
2949         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
2950         'only_matching': True,
2951     }]
2952
2953     def _real_extract(self, url):
2954         video_id = self._match_id(url)
2955         raise ExtractorError(
2956             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
2957             expected=True)