[youtube] Clarify ytplayer.config extraction rationale
[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 re
10 import time
11 import traceback
12
13 from .common import InfoExtractor, SearchInfoExtractor
14 from ..jsinterp import JSInterpreter
15 from ..swfinterp import SWFInterpreter
16 from ..compat import (
17     compat_chr,
18     compat_parse_qs,
19     compat_urllib_parse,
20     compat_urllib_parse_unquote,
21     compat_urllib_parse_unquote_plus,
22     compat_urllib_parse_urlparse,
23     compat_urllib_request,
24     compat_urlparse,
25     compat_str,
26 )
27 from ..utils import (
28     clean_html,
29     encode_dict,
30     ExtractorError,
31     float_or_none,
32     get_element_by_attribute,
33     get_element_by_id,
34     int_or_none,
35     orderedSet,
36     parse_duration,
37     remove_start,
38     smuggle_url,
39     str_to_int,
40     unescapeHTML,
41     unified_strdate,
42     unsmuggle_url,
43     uppercase_escape,
44     ISO3166Utils,
45 )
46
47
48 class YoutubeBaseInfoExtractor(InfoExtractor):
49     """Provide base functions for Youtube extractors"""
50     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
51     _TWOFACTOR_URL = 'https://accounts.google.com/signin/challenge'
52     _NETRC_MACHINE = 'youtube'
53     # If True it will raise an error if no login info is provided
54     _LOGIN_REQUIRED = False
55
56     def _set_language(self):
57         self._set_cookie(
58             '.youtube.com', 'PREF', 'f1=50000000&hl=en',
59             # YouTube sets the expire time to about two months
60             expire_time=time.time() + 2 * 30 * 24 * 3600)
61
62     def _ids_to_results(self, ids):
63         return [
64             self.url_result(vid_id, 'Youtube', video_id=vid_id)
65             for vid_id in ids]
66
67     def _login(self):
68         """
69         Attempt to log in to YouTube.
70         True is returned if successful or skipped.
71         False is returned if login failed.
72
73         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
74         """
75         (username, password) = self._get_login_info()
76         # No authentication to be performed
77         if username is None:
78             if self._LOGIN_REQUIRED:
79                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
80             return True
81
82         login_page = self._download_webpage(
83             self._LOGIN_URL, None,
84             note='Downloading login page',
85             errnote='unable to fetch login page', fatal=False)
86         if login_page is False:
87             return
88
89         galx = self._search_regex(r'(?s)<input.+?name="GALX".+?value="(.+?)"',
90                                   login_page, 'Login GALX parameter')
91
92         # Log in
93         login_form_strs = {
94             'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
95             'Email': username,
96             'GALX': galx,
97             'Passwd': password,
98
99             'PersistentCookie': 'yes',
100             '_utf8': '霱',
101             'bgresponse': 'js_disabled',
102             'checkConnection': '',
103             'checkedDomains': 'youtube',
104             'dnConn': '',
105             'pstMsg': '0',
106             'rmShown': '1',
107             'secTok': '',
108             'signIn': 'Sign in',
109             'timeStmp': '',
110             'service': 'youtube',
111             'uilel': '3',
112             'hl': 'en_US',
113         }
114
115         login_data = compat_urllib_parse.urlencode(encode_dict(login_form_strs)).encode('ascii')
116
117         req = compat_urllib_request.Request(self._LOGIN_URL, login_data)
118         login_results = self._download_webpage(
119             req, None,
120             note='Logging in', errnote='unable to log in', fatal=False)
121         if login_results is False:
122             return False
123
124         if re.search(r'id="errormsg_0_Passwd"', login_results) is not None:
125             raise ExtractorError('Please use your account password and a two-factor code instead of an application-specific password.', expected=True)
126
127         # Two-Factor
128         # TODO add SMS and phone call support - these require making a request and then prompting the user
129
130         if re.search(r'(?i)<form[^>]* id="challenge"', login_results) is not None:
131             tfa_code = self._get_tfa_info('2-step verification code')
132
133             if not tfa_code:
134                 self._downloader.report_warning(
135                     'Two-factor authentication required. Provide it either interactively or with --twofactor <code>'
136                     '(Note that only TOTP (Google Authenticator App) codes work at this time.)')
137                 return False
138
139             tfa_code = remove_start(tfa_code, 'G-')
140
141             tfa_form_strs = self._form_hidden_inputs('challenge', login_results)
142
143             tfa_form_strs.update({
144                 'Pin': tfa_code,
145                 'TrustDevice': 'on',
146             })
147
148             tfa_data = compat_urllib_parse.urlencode(encode_dict(tfa_form_strs)).encode('ascii')
149
150             tfa_req = compat_urllib_request.Request(self._TWOFACTOR_URL, tfa_data)
151             tfa_results = self._download_webpage(
152                 tfa_req, None,
153                 note='Submitting TFA code', errnote='unable to submit tfa', fatal=False)
154
155             if tfa_results is False:
156                 return False
157
158             if re.search(r'(?i)<form[^>]* id="challenge"', tfa_results) is not None:
159                 self._downloader.report_warning('Two-factor code expired or invalid. Please try again, or use a one-use backup code instead.')
160                 return False
161             if re.search(r'(?i)<form[^>]* id="gaia_loginform"', tfa_results) is not None:
162                 self._downloader.report_warning('unable to log in - did the page structure change?')
163                 return False
164             if re.search(r'smsauth-interstitial-reviewsettings', tfa_results) is not None:
165                 self._downloader.report_warning('Your Google account has a security notice. Please log in on your web browser, resolve the notice, and try again.')
166                 return False
167
168         if re.search(r'(?i)<form[^>]* id="gaia_loginform"', login_results) is not None:
169             self._downloader.report_warning('unable to log in: bad username or password')
170             return False
171         return True
172
173     def _real_initialize(self):
174         if self._downloader is None:
175             return
176         self._set_language()
177         if not self._login():
178             return
179
180
181 class YoutubeEntryListBaseInfoExtractor(InfoExtractor):
182     # Extract entries from page with "Load more" button
183     def _entries(self, page, playlist_id):
184         more_widget_html = content_html = page
185         for page_num in itertools.count(1):
186             for entry in self._process_page(content_html):
187                 yield entry
188
189             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
190             if not mobj:
191                 break
192
193             more = self._download_json(
194                 'https://youtube.com/%s' % mobj.group('more'), playlist_id,
195                 'Downloading page #%s' % page_num,
196                 transform_source=uppercase_escape)
197             content_html = more['content_html']
198             if not content_html.strip():
199                 # Some webpages show a "Load more" button but they don't
200                 # have more videos
201                 break
202             more_widget_html = more['load_more_widget_html']
203
204
205 class YoutubePlaylistBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
206     def _process_page(self, content):
207         for video_id, video_title in self.extract_videos_from_page(content):
208             yield self.url_result(video_id, 'Youtube', video_id, video_title)
209
210     def extract_videos_from_page(self, page):
211         ids_in_page = []
212         titles_in_page = []
213         for mobj in re.finditer(self._VIDEO_RE, page):
214             # The link with index 0 is not the first video of the playlist (not sure if still actual)
215             if 'index' in mobj.groupdict() and mobj.group('id') == '0':
216                 continue
217             video_id = mobj.group('id')
218             video_title = unescapeHTML(mobj.group('title'))
219             if video_title:
220                 video_title = video_title.strip()
221             try:
222                 idx = ids_in_page.index(video_id)
223                 if video_title and not titles_in_page[idx]:
224                     titles_in_page[idx] = video_title
225             except ValueError:
226                 ids_in_page.append(video_id)
227                 titles_in_page.append(video_title)
228         return zip(ids_in_page, titles_in_page)
229
230
231 class YoutubePlaylistsBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
232     def _process_page(self, content):
233         for playlist_id in re.findall(r'href="/?playlist\?list=(.+?)"', content):
234             yield self.url_result(
235                 'https://www.youtube.com/playlist?list=%s' % playlist_id, 'YoutubePlaylist')
236
237     def _real_extract(self, url):
238         playlist_id = self._match_id(url)
239         webpage = self._download_webpage(url, playlist_id)
240         title = self._og_search_title(webpage, fatal=False)
241         return self.playlist_result(self._entries(webpage, playlist_id), playlist_id, title)
242
243
244 class YoutubeIE(YoutubeBaseInfoExtractor):
245     IE_DESC = 'YouTube.com'
246     _VALID_URL = r"""(?x)^
247                      (
248                          (?:https?://|//)                                    # http(s):// or protocol-independent URL
249                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
250                             (?:www\.)?deturl\.com/www\.youtube\.com/|
251                             (?:www\.)?pwnyoutube\.com/|
252                             (?:www\.)?yourepeat\.com/|
253                             tube\.majestyc\.net/|
254                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains
255                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
256                          (?:                                                  # the various things that can precede the ID:
257                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/
258                              |(?:                                             # or the v= param in all its forms
259                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
260                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
261                                  (?:.*?&)??                                   # any other preceding param (like /?s=tuff&v=xxxx)
262                                  v=
263                              )
264                          ))
265                          |(?:
266                             youtu\.be|                                        # just youtu.be/xxxx
267                             vid\.plus                                         # or vid.plus/xxxx
268                          )/
269                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
270                          )
271                      )?                                                       # all until now is optional -> you can pass the naked ID
272                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID
273                      (?!.*?&list=)                                            # combined list/video URLs are handled by the playlist IE
274                      (?(1).+)?                                                # if we found the ID, everything can follow
275                      $"""
276     _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
277     _formats = {
278         '5': {'ext': 'flv', 'width': 400, 'height': 240},
279         '6': {'ext': 'flv', 'width': 450, 'height': 270},
280         '13': {'ext': '3gp'},
281         '17': {'ext': '3gp', 'width': 176, 'height': 144},
282         '18': {'ext': 'mp4', 'width': 640, 'height': 360},
283         '22': {'ext': 'mp4', 'width': 1280, 'height': 720},
284         '34': {'ext': 'flv', 'width': 640, 'height': 360},
285         '35': {'ext': 'flv', 'width': 854, 'height': 480},
286         '36': {'ext': '3gp', 'width': 320, 'height': 240},
287         '37': {'ext': 'mp4', 'width': 1920, 'height': 1080},
288         '38': {'ext': 'mp4', 'width': 4096, 'height': 3072},
289         '43': {'ext': 'webm', 'width': 640, 'height': 360},
290         '44': {'ext': 'webm', 'width': 854, 'height': 480},
291         '45': {'ext': 'webm', 'width': 1280, 'height': 720},
292         '46': {'ext': 'webm', 'width': 1920, 'height': 1080},
293         '59': {'ext': 'mp4', 'width': 854, 'height': 480},
294         '78': {'ext': 'mp4', 'width': 854, 'height': 480},
295
296
297         # 3d videos
298         '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'preference': -20},
299         '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'preference': -20},
300         '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'preference': -20},
301         '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'preference': -20},
302         '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'preference': -20},
303         '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'preference': -20},
304         '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'preference': -20},
305
306         # Apple HTTP Live Streaming
307         '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
308         '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'preference': -10},
309         '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'preference': -10},
310         '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'preference': -10},
311         '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'preference': -10},
312         '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
313         '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'preference': -10},
314
315         # DASH mp4 video
316         '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
317         '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
318         '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
319         '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
320         '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
321         '138': {'ext': 'mp4', 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},  # Height can vary (https://github.com/rg3/youtube-dl/issues/4559)
322         '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
323         '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
324         '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'h264'},
325         '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'h264'},
326         '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'vcodec': 'h264'},
327
328         # Dash mp4 audio
329         '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'vcodec': 'none', 'abr': 48, 'preference': -50, 'container': 'm4a_dash'},
330         '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'vcodec': 'none', 'abr': 128, 'preference': -50, 'container': 'm4a_dash'},
331         '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'vcodec': 'none', 'abr': 256, 'preference': -50, 'container': 'm4a_dash'},
332
333         # Dash webm
334         '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
335         '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
336         '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
337         '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
338         '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
339         '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
340         '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'container': 'webm', 'vcodec': 'vp9'},
341         '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
342         '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
343         '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
344         '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
345         '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
346         '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
347         '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
348         '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
349         '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
350         '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'vp9'},
351         '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'vp9'},
352         '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'vp9'},
353         '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'vcodec': 'vp9'},
354         '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'vp9'},
355
356         # Dash webm audio
357         '171': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 128, 'preference': -50},
358         '172': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 256, 'preference': -50},
359
360         # Dash webm audio with opus inside
361         '249': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50, 'preference': -50},
362         '250': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70, 'preference': -50},
363         '251': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160, 'preference': -50},
364
365         # RTMP (unnamed)
366         '_rtmp': {'protocol': 'rtmp'},
367     }
368
369     IE_NAME = 'youtube'
370     _TESTS = [
371         {
372             'url': 'http://www.youtube.com/watch?v=BaW_jenozKcj&t=1s&end=9',
373             'info_dict': {
374                 'id': 'BaW_jenozKc',
375                 'ext': 'mp4',
376                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
377                 'uploader': 'Philipp Hagemeister',
378                 'uploader_id': 'phihag',
379                 'upload_date': '20121002',
380                 '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 .',
381                 'categories': ['Science & Technology'],
382                 'tags': ['youtube-dl'],
383                 'like_count': int,
384                 'dislike_count': int,
385                 'start_time': 1,
386                 'end_time': 9,
387             }
388         },
389         {
390             'url': 'http://www.youtube.com/watch?v=UxxajLWwzqY',
391             'note': 'Test generic use_cipher_signature video (#897)',
392             'info_dict': {
393                 'id': 'UxxajLWwzqY',
394                 'ext': 'mp4',
395                 'upload_date': '20120506',
396                 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]',
397                 'description': 'md5:782e8651347686cba06e58f71ab51773',
398                 'tags': ['Icona Pop i love it', 'sweden', 'pop music', 'big beat records', 'big beat', 'charli',
399                          'xcx', 'charli xcx', 'girls', 'hbo', 'i love it', "i don't care", 'icona', 'pop',
400                          'iconic ep', 'iconic', 'love', 'it'],
401                 'uploader': 'Icona Pop',
402                 'uploader_id': 'IconaPop',
403             }
404         },
405         {
406             'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ',
407             'note': 'Test VEVO video with age protection (#956)',
408             'info_dict': {
409                 'id': '07FYdnEawAQ',
410                 'ext': 'mp4',
411                 'upload_date': '20130703',
412                 'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
413                 'description': 'md5:64249768eec3bc4276236606ea996373',
414                 'uploader': 'justintimberlakeVEVO',
415                 'uploader_id': 'justintimberlakeVEVO',
416                 'age_limit': 18,
417             }
418         },
419         {
420             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
421             'note': 'Embed-only video (#1746)',
422             'info_dict': {
423                 'id': 'yZIXLfi8CZQ',
424                 'ext': 'mp4',
425                 'upload_date': '20120608',
426                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
427                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
428                 'uploader': 'SET India',
429                 'uploader_id': 'setindia'
430             }
431         },
432         {
433             'url': 'http://www.youtube.com/watch?v=BaW_jenozKcj&v=UxxajLWwzqY',
434             'note': 'Use the first video ID in the URL',
435             'info_dict': {
436                 'id': 'BaW_jenozKc',
437                 'ext': 'mp4',
438                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
439                 'uploader': 'Philipp Hagemeister',
440                 'uploader_id': 'phihag',
441                 'upload_date': '20121002',
442                 '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 .',
443                 'categories': ['Science & Technology'],
444                 'tags': ['youtube-dl'],
445                 'like_count': int,
446                 'dislike_count': int,
447             },
448             'params': {
449                 'skip_download': True,
450             },
451         },
452         {
453             'url': 'http://www.youtube.com/watch?v=a9LDPn-MO4I',
454             'note': '256k DASH audio (format 141) via DASH manifest',
455             'info_dict': {
456                 'id': 'a9LDPn-MO4I',
457                 'ext': 'm4a',
458                 'upload_date': '20121002',
459                 'uploader_id': '8KVIDEO',
460                 'description': '',
461                 'uploader': '8KVIDEO',
462                 'title': 'UHDTV TEST 8K VIDEO.mp4'
463             },
464             'params': {
465                 'youtube_include_dash_manifest': True,
466                 'format': '141',
467             },
468         },
469         # DASH manifest with encrypted signature
470         {
471             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
472             'info_dict': {
473                 'id': 'IB3lcPjvWLA',
474                 'ext': 'm4a',
475                 'title': 'Afrojack, Spree Wilson - The Spark ft. Spree Wilson',
476                 'description': 'md5:12e7067fa6735a77bdcbb58cb1187d2d',
477                 'uploader': 'AfrojackVEVO',
478                 'uploader_id': 'AfrojackVEVO',
479                 'upload_date': '20131011',
480             },
481             'params': {
482                 'youtube_include_dash_manifest': True,
483                 'format': '141',
484             },
485         },
486         # JS player signature function name containing $
487         {
488             'url': 'https://www.youtube.com/watch?v=nfWlot6h_JM',
489             'info_dict': {
490                 'id': 'nfWlot6h_JM',
491                 'ext': 'm4a',
492                 'title': 'Taylor Swift - Shake It Off',
493                 'description': 'md5:95f66187cd7c8b2c13eb78e1223b63c3',
494                 'uploader': 'TaylorSwiftVEVO',
495                 'uploader_id': 'TaylorSwiftVEVO',
496                 'upload_date': '20140818',
497             },
498             'params': {
499                 'youtube_include_dash_manifest': True,
500                 'format': '141',
501             },
502         },
503         # Controversy video
504         {
505             'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
506             'info_dict': {
507                 'id': 'T4XJQO3qol8',
508                 'ext': 'mp4',
509                 'upload_date': '20100909',
510                 'uploader': 'The Amazing Atheist',
511                 'uploader_id': 'TheAmazingAtheist',
512                 'title': 'Burning Everyone\'s Koran',
513                 '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',
514             }
515         },
516         # Normal age-gate video (No vevo, embed allowed)
517         {
518             'url': 'http://youtube.com/watch?v=HtVdAasjOgU',
519             'info_dict': {
520                 'id': 'HtVdAasjOgU',
521                 'ext': 'mp4',
522                 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
523                 'description': 're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
524                 'uploader': 'The Witcher',
525                 'uploader_id': 'WitcherGame',
526                 'upload_date': '20140605',
527                 'age_limit': 18,
528             },
529         },
530         # Age-gate video with encrypted signature
531         {
532             'url': 'http://www.youtube.com/watch?v=6kLq3WMV1nU',
533             'info_dict': {
534                 'id': '6kLq3WMV1nU',
535                 'ext': 'mp4',
536                 'title': 'Dedication To My Ex (Miss That) (Lyric Video)',
537                 'description': 'md5:33765bb339e1b47e7e72b5490139bb41',
538                 'uploader': 'LloydVEVO',
539                 'uploader_id': 'LloydVEVO',
540                 'upload_date': '20110629',
541                 'age_limit': 18,
542             },
543         },
544         # video_info is None (https://github.com/rg3/youtube-dl/issues/4421)
545         {
546             'url': '__2ABJjxzNo',
547             'info_dict': {
548                 'id': '__2ABJjxzNo',
549                 'ext': 'mp4',
550                 'upload_date': '20100430',
551                 'uploader_id': 'deadmau5',
552                 'description': 'md5:12c56784b8032162bb936a5f76d55360',
553                 'uploader': 'deadmau5',
554                 'title': 'Deadmau5 - Some Chords (HD)',
555             },
556             'expected_warnings': [
557                 'DASH manifest missing',
558             ]
559         },
560         # Olympics (https://github.com/rg3/youtube-dl/issues/4431)
561         {
562             'url': 'lqQg6PlCWgI',
563             'info_dict': {
564                 'id': 'lqQg6PlCWgI',
565                 'ext': 'mp4',
566                 'upload_date': '20120724',
567                 'uploader_id': 'olympic',
568                 'description': 'HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games',
569                 'uploader': 'Olympics',
570                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games',
571             },
572             'params': {
573                 'skip_download': 'requires avconv',
574             }
575         },
576         # Non-square pixels
577         {
578             'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
579             'info_dict': {
580                 'id': '_b-2C3KPAM0',
581                 'ext': 'mp4',
582                 'stretched_ratio': 16 / 9.,
583                 'upload_date': '20110310',
584                 'uploader_id': 'AllenMeow',
585                 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
586                 'uploader': '孫艾倫',
587                 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
588             },
589         },
590         # url_encoded_fmt_stream_map is empty string
591         {
592             'url': 'qEJwOuvDf7I',
593             'info_dict': {
594                 'id': 'qEJwOuvDf7I',
595                 'ext': 'webm',
596                 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
597                 'description': '',
598                 'upload_date': '20150404',
599                 'uploader_id': 'spbelect',
600                 'uploader': 'Наблюдатели Петербурга',
601             },
602             'params': {
603                 'skip_download': 'requires avconv',
604             }
605         },
606         # Extraction from multiple DASH manifests (https://github.com/rg3/youtube-dl/pull/6097)
607         {
608             'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
609             'info_dict': {
610                 'id': 'FIl7x6_3R5Y',
611                 'ext': 'mp4',
612                 'title': 'md5:7b81415841e02ecd4313668cde88737a',
613                 'description': 'md5:116377fd2963b81ec4ce64b542173306',
614                 'upload_date': '20150625',
615                 'uploader_id': 'dorappi2000',
616                 'uploader': 'dorappi2000',
617                 'formats': 'mincount:33',
618             },
619         },
620         # DASH manifest with segment_list
621         {
622             'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
623             'md5': '8ce563a1d667b599d21064e982ab9e31',
624             'info_dict': {
625                 'id': 'CsmdDsKjzN8',
626                 'ext': 'mp4',
627                 'upload_date': '20150501',  # According to '<meta itemprop="datePublished"', but in other places it's 20150510
628                 'uploader': 'Airtek',
629                 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
630                 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
631                 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
632             },
633             'params': {
634                 'youtube_include_dash_manifest': True,
635                 'format': '135',  # bestvideo
636             }
637         },
638         {
639             # Multifeed videos (multiple cameras), URL is for Main Camera
640             'url': 'https://www.youtube.com/watch?v=jqWvoWXjCVs',
641             'info_dict': {
642                 'id': 'jqWvoWXjCVs',
643                 'title': 'teamPGP: Rocket League Noob Stream',
644                 'description': 'md5:dc7872fb300e143831327f1bae3af010',
645             },
646             'playlist': [{
647                 'info_dict': {
648                     'id': 'jqWvoWXjCVs',
649                     'ext': 'mp4',
650                     'title': 'teamPGP: Rocket League Noob Stream (Main Camera)',
651                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
652                     'upload_date': '20150721',
653                     'uploader': 'Beer Games Beer',
654                     'uploader_id': 'beergamesbeer',
655                 },
656             }, {
657                 'info_dict': {
658                     'id': '6h8e8xoXJzg',
659                     'ext': 'mp4',
660                     'title': 'teamPGP: Rocket League Noob Stream (kreestuh)',
661                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
662                     'upload_date': '20150721',
663                     'uploader': 'Beer Games Beer',
664                     'uploader_id': 'beergamesbeer',
665                 },
666             }, {
667                 'info_dict': {
668                     'id': 'PUOgX5z9xZw',
669                     'ext': 'mp4',
670                     'title': 'teamPGP: Rocket League Noob Stream (grizzle)',
671                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
672                     'upload_date': '20150721',
673                     'uploader': 'Beer Games Beer',
674                     'uploader_id': 'beergamesbeer',
675                 },
676             }, {
677                 'info_dict': {
678                     'id': 'teuwxikvS5k',
679                     'ext': 'mp4',
680                     'title': 'teamPGP: Rocket League Noob Stream (zim)',
681                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
682                     'upload_date': '20150721',
683                     'uploader': 'Beer Games Beer',
684                     'uploader_id': 'beergamesbeer',
685                 },
686             }],
687             'params': {
688                 'skip_download': True,
689             },
690         },
691         {
692             'url': 'http://vid.plus/FlRa-iH7PGw',
693             'only_matching': True,
694         },
695         {
696             # Title with JS-like syntax "};" (see https://github.com/rg3/youtube-dl/issues/7468)
697             'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
698             'info_dict': {
699                 'id': 'lsguqyKfVQg',
700                 'ext': 'mp4',
701                 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
702                 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
703                 'upload_date': '20151119',
704                 'uploader_id': 'IronSoulElf',
705                 'uploader': 'IronSoulElf',
706             },
707             'params': {
708                 'skip_download': True,
709             },
710         },
711         {
712             # Tags with '};' (see https://github.com/rg3/youtube-dl/issues/7468)
713             'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
714             'only_matching': True,
715         },
716     ]
717
718     def __init__(self, *args, **kwargs):
719         super(YoutubeIE, self).__init__(*args, **kwargs)
720         self._player_cache = {}
721
722     def report_video_info_webpage_download(self, video_id):
723         """Report attempt to download video info webpage."""
724         self.to_screen('%s: Downloading video info webpage' % video_id)
725
726     def report_information_extraction(self, video_id):
727         """Report attempt to extract video information."""
728         self.to_screen('%s: Extracting video information' % video_id)
729
730     def report_unavailable_format(self, video_id, format):
731         """Report extracted video URL."""
732         self.to_screen('%s: Format %s not available' % (video_id, format))
733
734     def report_rtmp_download(self):
735         """Indicate the download will use the RTMP protocol."""
736         self.to_screen('RTMP download detected')
737
738     def _signature_cache_id(self, example_sig):
739         """ Return a string representation of a signature """
740         return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
741
742     def _extract_signature_function(self, video_id, player_url, example_sig):
743         id_m = re.match(
744             r'.*?-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|/base)?\.(?P<ext>[a-z]+)$',
745             player_url)
746         if not id_m:
747             raise ExtractorError('Cannot identify player %r' % player_url)
748         player_type = id_m.group('ext')
749         player_id = id_m.group('id')
750
751         # Read from filesystem cache
752         func_id = '%s_%s_%s' % (
753             player_type, player_id, self._signature_cache_id(example_sig))
754         assert os.path.basename(func_id) == func_id
755
756         cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
757         if cache_spec is not None:
758             return lambda s: ''.join(s[i] for i in cache_spec)
759
760         download_note = (
761             'Downloading player %s' % player_url
762             if self._downloader.params.get('verbose') else
763             'Downloading %s player %s' % (player_type, player_id)
764         )
765         if player_type == 'js':
766             code = self._download_webpage(
767                 player_url, video_id,
768                 note=download_note,
769                 errnote='Download of %s failed' % player_url)
770             res = self._parse_sig_js(code)
771         elif player_type == 'swf':
772             urlh = self._request_webpage(
773                 player_url, video_id,
774                 note=download_note,
775                 errnote='Download of %s failed' % player_url)
776             code = urlh.read()
777             res = self._parse_sig_swf(code)
778         else:
779             assert False, 'Invalid player type %r' % player_type
780
781         test_string = ''.join(map(compat_chr, range(len(example_sig))))
782         cache_res = res(test_string)
783         cache_spec = [ord(c) for c in cache_res]
784
785         self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
786         return res
787
788     def _print_sig_code(self, func, example_sig):
789         def gen_sig_code(idxs):
790             def _genslice(start, end, step):
791                 starts = '' if start == 0 else str(start)
792                 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
793                 steps = '' if step == 1 else (':%d' % step)
794                 return 's[%s%s%s]' % (starts, ends, steps)
795
796             step = None
797             # Quelch pyflakes warnings - start will be set when step is set
798             start = '(Never used)'
799             for i, prev in zip(idxs[1:], idxs[:-1]):
800                 if step is not None:
801                     if i - prev == step:
802                         continue
803                     yield _genslice(start, prev, step)
804                     step = None
805                     continue
806                 if i - prev in [-1, 1]:
807                     step = i - prev
808                     start = prev
809                     continue
810                 else:
811                     yield 's[%d]' % prev
812             if step is None:
813                 yield 's[%d]' % i
814             else:
815                 yield _genslice(start, i, step)
816
817         test_string = ''.join(map(compat_chr, range(len(example_sig))))
818         cache_res = func(test_string)
819         cache_spec = [ord(c) for c in cache_res]
820         expr_code = ' + '.join(gen_sig_code(cache_spec))
821         signature_id_tuple = '(%s)' % (
822             ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
823         code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
824                 '    return %s\n') % (signature_id_tuple, expr_code)
825         self.to_screen('Extracted signature function:\n' + code)
826
827     def _parse_sig_js(self, jscode):
828         funcname = self._search_regex(
829             r'\.sig\|\|([a-zA-Z0-9$]+)\(', jscode,
830             'Initial JS player signature function name')
831
832         jsi = JSInterpreter(jscode)
833         initial_function = jsi.extract_function(funcname)
834         return lambda s: initial_function([s])
835
836     def _parse_sig_swf(self, file_contents):
837         swfi = SWFInterpreter(file_contents)
838         TARGET_CLASSNAME = 'SignatureDecipher'
839         searched_class = swfi.extract_class(TARGET_CLASSNAME)
840         initial_function = swfi.extract_function(searched_class, 'decipher')
841         return lambda s: initial_function([s])
842
843     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
844         """Turn the encrypted s field into a working signature"""
845
846         if player_url is None:
847             raise ExtractorError('Cannot decrypt signature without player_url')
848
849         if player_url.startswith('//'):
850             player_url = 'https:' + player_url
851         try:
852             player_id = (player_url, self._signature_cache_id(s))
853             if player_id not in self._player_cache:
854                 func = self._extract_signature_function(
855                     video_id, player_url, s
856                 )
857                 self._player_cache[player_id] = func
858             func = self._player_cache[player_id]
859             if self._downloader.params.get('youtube_print_sig_code'):
860                 self._print_sig_code(func, s)
861             return func(s)
862         except Exception as e:
863             tb = traceback.format_exc()
864             raise ExtractorError(
865                 'Signature extraction failed: ' + tb, cause=e)
866
867     def _get_subtitles(self, video_id, webpage):
868         try:
869             subs_doc = self._download_xml(
870                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
871                 video_id, note=False)
872         except ExtractorError as err:
873             self._downloader.report_warning('unable to download video subtitles: %s' % compat_str(err))
874             return {}
875
876         sub_lang_list = {}
877         for track in subs_doc.findall('track'):
878             lang = track.attrib['lang_code']
879             if lang in sub_lang_list:
880                 continue
881             sub_formats = []
882             for ext in ['sbv', 'vtt', 'srt']:
883                 params = compat_urllib_parse.urlencode({
884                     'lang': lang,
885                     'v': video_id,
886                     'fmt': ext,
887                     'name': track.attrib['name'].encode('utf-8'),
888                 })
889                 sub_formats.append({
890                     'url': 'https://www.youtube.com/api/timedtext?' + params,
891                     'ext': ext,
892                 })
893             sub_lang_list[lang] = sub_formats
894         if not sub_lang_list:
895             self._downloader.report_warning('video doesn\'t have subtitles')
896             return {}
897         return sub_lang_list
898
899     def _get_ytplayer_config(self, video_id, webpage):
900         patterns = (
901             # User data may contain arbitrary character sequences that may affect
902             # JSON extraction with regex, e.g. when '};' is contained the second
903             # regex won't capture the whole JSON. Yet working around by trying more
904             # concrete regex first keeping in mind proper quoted string handling
905             # to be implemented in future that will replace this workaround (see
906             # https://github.com/rg3/youtube-dl/issues/7468,
907             # https://github.com/rg3/youtube-dl/pull/7599)
908             r';ytplayer\.config\s*=\s*({.+?});ytplayer',
909             r';ytplayer\.config\s*=\s*({.+?});',
910         )
911         config = self._search_regex(
912             patterns, webpage, 'ytplayer.config', default=None)
913         if config:
914             return self._parse_json(
915                 uppercase_escape(config), video_id, fatal=False)
916
917     def _get_automatic_captions(self, video_id, webpage):
918         """We need the webpage for getting the captions url, pass it as an
919            argument to speed up the process."""
920         self.to_screen('%s: Looking for automatic captions' % video_id)
921         player_config = self._get_ytplayer_config(video_id, webpage)
922         err_msg = 'Couldn\'t find automatic captions for %s' % video_id
923         if not player_config:
924             self._downloader.report_warning(err_msg)
925             return {}
926         try:
927             args = player_config['args']
928             caption_url = args['ttsurl']
929             timestamp = args['timestamp']
930             # We get the available subtitles
931             list_params = compat_urllib_parse.urlencode({
932                 'type': 'list',
933                 'tlangs': 1,
934                 'asrs': 1,
935             })
936             list_url = caption_url + '&' + list_params
937             caption_list = self._download_xml(list_url, video_id)
938             original_lang_node = caption_list.find('track')
939             if original_lang_node is None:
940                 self._downloader.report_warning('Video doesn\'t have automatic captions')
941                 return {}
942             original_lang = original_lang_node.attrib['lang_code']
943             caption_kind = original_lang_node.attrib.get('kind', '')
944
945             sub_lang_list = {}
946             for lang_node in caption_list.findall('target'):
947                 sub_lang = lang_node.attrib['lang_code']
948                 sub_formats = []
949                 for ext in ['sbv', 'vtt', 'srt']:
950                     params = compat_urllib_parse.urlencode({
951                         'lang': original_lang,
952                         'tlang': sub_lang,
953                         'fmt': ext,
954                         'ts': timestamp,
955                         'kind': caption_kind,
956                     })
957                     sub_formats.append({
958                         'url': caption_url + '&' + params,
959                         'ext': ext,
960                     })
961                 sub_lang_list[sub_lang] = sub_formats
962             return sub_lang_list
963         # An extractor error can be raise by the download process if there are
964         # no automatic captions but there are subtitles
965         except (KeyError, ExtractorError):
966             self._downloader.report_warning(err_msg)
967             return {}
968
969     @classmethod
970     def extract_id(cls, url):
971         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
972         if mobj is None:
973             raise ExtractorError('Invalid URL: %s' % url)
974         video_id = mobj.group(2)
975         return video_id
976
977     def _extract_from_m3u8(self, manifest_url, video_id):
978         url_map = {}
979
980         def _get_urls(_manifest):
981             lines = _manifest.split('\n')
982             urls = filter(lambda l: l and not l.startswith('#'),
983                           lines)
984             return urls
985         manifest = self._download_webpage(manifest_url, video_id, 'Downloading formats manifest')
986         formats_urls = _get_urls(manifest)
987         for format_url in formats_urls:
988             itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
989             url_map[itag] = format_url
990         return url_map
991
992     def _extract_annotations(self, video_id):
993         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
994         return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
995
996     def _parse_dash_manifest(
997             self, video_id, dash_manifest_url, player_url, age_gate, fatal=True):
998         def decrypt_sig(mobj):
999             s = mobj.group(1)
1000             dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
1001             return '/signature/%s' % dec_s
1002         dash_manifest_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, dash_manifest_url)
1003         dash_doc = self._download_xml(
1004             dash_manifest_url, video_id,
1005             note='Downloading DASH manifest',
1006             errnote='Could not download DASH manifest',
1007             fatal=fatal)
1008
1009         if dash_doc is False:
1010             return []
1011
1012         formats = []
1013         for a in dash_doc.findall('.//{urn:mpeg:DASH:schema:MPD:2011}AdaptationSet'):
1014             mime_type = a.attrib.get('mimeType')
1015             for r in a.findall('{urn:mpeg:DASH:schema:MPD:2011}Representation'):
1016                 url_el = r.find('{urn:mpeg:DASH:schema:MPD:2011}BaseURL')
1017                 if url_el is None:
1018                     continue
1019                 if mime_type == 'text/vtt':
1020                     # TODO implement WebVTT downloading
1021                     pass
1022                 elif mime_type.startswith('audio/') or mime_type.startswith('video/'):
1023                     segment_list = r.find('{urn:mpeg:DASH:schema:MPD:2011}SegmentList')
1024                     format_id = r.attrib['id']
1025                     video_url = url_el.text
1026                     filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength'))
1027                     f = {
1028                         'format_id': format_id,
1029                         'url': video_url,
1030                         'width': int_or_none(r.attrib.get('width')),
1031                         'height': int_or_none(r.attrib.get('height')),
1032                         'tbr': int_or_none(r.attrib.get('bandwidth'), 1000),
1033                         'asr': int_or_none(r.attrib.get('audioSamplingRate')),
1034                         'filesize': filesize,
1035                         'fps': int_or_none(r.attrib.get('frameRate')),
1036                     }
1037                     if segment_list is not None:
1038                         f.update({
1039                             'initialization_url': segment_list.find('{urn:mpeg:DASH:schema:MPD:2011}Initialization').attrib['sourceURL'],
1040                             'segment_urls': [segment.attrib.get('media') for segment in segment_list.findall('{urn:mpeg:DASH:schema:MPD:2011}SegmentURL')],
1041                             'protocol': 'http_dash_segments',
1042                         })
1043                     try:
1044                         existing_format = next(
1045                             fo for fo in formats
1046                             if fo['format_id'] == format_id)
1047                     except StopIteration:
1048                         full_info = self._formats.get(format_id, {}).copy()
1049                         full_info.update(f)
1050                         codecs = r.attrib.get('codecs')
1051                         if codecs:
1052                             if full_info.get('acodec') == 'none' and 'vcodec' not in full_info:
1053                                 full_info['vcodec'] = codecs
1054                             elif full_info.get('vcodec') == 'none' and 'acodec' not in full_info:
1055                                 full_info['acodec'] = codecs
1056                         formats.append(full_info)
1057                     else:
1058                         existing_format.update(f)
1059                 else:
1060                     self.report_warning('Unknown MIME type %s in DASH manifest' % mime_type)
1061         return formats
1062
1063     def _real_extract(self, url):
1064         url, smuggled_data = unsmuggle_url(url, {})
1065
1066         proto = (
1067             'http' if self._downloader.params.get('prefer_insecure', False)
1068             else 'https')
1069
1070         start_time = None
1071         end_time = None
1072         parsed_url = compat_urllib_parse_urlparse(url)
1073         for component in [parsed_url.fragment, parsed_url.query]:
1074             query = compat_parse_qs(component)
1075             if start_time is None and 't' in query:
1076                 start_time = parse_duration(query['t'][0])
1077             if start_time is None and 'start' in query:
1078                 start_time = parse_duration(query['start'][0])
1079             if end_time is None and 'end' in query:
1080                 end_time = parse_duration(query['end'][0])
1081
1082         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1083         mobj = re.search(self._NEXT_URL_RE, url)
1084         if mobj:
1085             url = proto + '://www.youtube.com/' + compat_urllib_parse_unquote(mobj.group(1)).lstrip('/')
1086         video_id = self.extract_id(url)
1087
1088         # Get video webpage
1089         url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
1090         video_webpage = self._download_webpage(url, video_id)
1091
1092         # Attempt to extract SWF player URL
1093         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1094         if mobj is not None:
1095             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
1096         else:
1097             player_url = None
1098
1099         dash_mpds = []
1100
1101         def add_dash_mpd(video_info):
1102             dash_mpd = video_info.get('dashmpd')
1103             if dash_mpd and dash_mpd[0] not in dash_mpds:
1104                 dash_mpds.append(dash_mpd[0])
1105
1106         # Get video info
1107         embed_webpage = None
1108         is_live = None
1109         if re.search(r'player-age-gate-content">', video_webpage) is not None:
1110             age_gate = True
1111             # We simulate the access to the video from www.youtube.com/v/{video_id}
1112             # this can be viewed without login into Youtube
1113             url = proto + '://www.youtube.com/embed/%s' % video_id
1114             embed_webpage = self._download_webpage(url, video_id, 'Downloading embed webpage')
1115             data = compat_urllib_parse.urlencode({
1116                 'video_id': video_id,
1117                 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1118                 'sts': self._search_regex(
1119                     r'"sts"\s*:\s*(\d+)', embed_webpage, 'sts', default=''),
1120             })
1121             video_info_url = proto + '://www.youtube.com/get_video_info?' + data
1122             video_info_webpage = self._download_webpage(
1123                 video_info_url, video_id,
1124                 note='Refetching age-gated info webpage',
1125                 errnote='unable to download video info webpage')
1126             video_info = compat_parse_qs(video_info_webpage)
1127             add_dash_mpd(video_info)
1128         else:
1129             age_gate = False
1130             video_info = None
1131             # Try looking directly into the video webpage
1132             ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1133             if ytplayer_config:
1134                 args = ytplayer_config['args']
1135                 if args.get('url_encoded_fmt_stream_map'):
1136                     # Convert to the same format returned by compat_parse_qs
1137                     video_info = dict((k, [v]) for k, v in args.items())
1138                     add_dash_mpd(video_info)
1139                 if args.get('livestream') == '1' or args.get('live_playback') == 1:
1140                     is_live = True
1141             if not video_info or self._downloader.params.get('youtube_include_dash_manifest', True):
1142                 # We also try looking in get_video_info since it may contain different dashmpd
1143                 # URL that points to a DASH manifest with possibly different itag set (some itags
1144                 # are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
1145                 # manifest pointed by get_video_info's dashmpd).
1146                 # The general idea is to take a union of itags of both DASH manifests (for example
1147                 # video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
1148                 self.report_video_info_webpage_download(video_id)
1149                 for el_type in ['&el=info', '&el=embedded', '&el=detailpage', '&el=vevo', '']:
1150                     video_info_url = (
1151                         '%s://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
1152                         % (proto, video_id, el_type))
1153                     video_info_webpage = self._download_webpage(
1154                         video_info_url,
1155                         video_id, note=False,
1156                         errnote='unable to download video info webpage')
1157                     get_video_info = compat_parse_qs(video_info_webpage)
1158                     if get_video_info.get('use_cipher_signature') != ['True']:
1159                         add_dash_mpd(get_video_info)
1160                     if not video_info:
1161                         video_info = get_video_info
1162                     if 'token' in get_video_info:
1163                         # Different get_video_info requests may report different results, e.g.
1164                         # some may report video unavailability, but some may serve it without
1165                         # any complaint (see https://github.com/rg3/youtube-dl/issues/7362,
1166                         # the original webpage as well as el=info and el=embedded get_video_info
1167                         # requests report video unavailability due to geo restriction while
1168                         # el=detailpage succeeds and returns valid data). This is probably
1169                         # due to YouTube measures against IP ranges of hosting providers.
1170                         # Working around by preferring the first succeeded video_info containing
1171                         # the token if no such video_info yet was found.
1172                         if 'token' not in video_info:
1173                             video_info = get_video_info
1174                         break
1175         if 'token' not in video_info:
1176             if 'reason' in video_info:
1177                 if 'The uploader has not made this video available in your country.' in video_info['reason']:
1178                     regions_allowed = self._html_search_meta('regionsAllowed', video_webpage, default=None)
1179                     if regions_allowed:
1180                         raise ExtractorError('YouTube said: This video is available in %s only' % (
1181                             ', '.join(map(ISO3166Utils.short2full, regions_allowed.split(',')))),
1182                             expected=True)
1183                 raise ExtractorError(
1184                     'YouTube said: %s' % video_info['reason'][0],
1185                     expected=True, video_id=video_id)
1186             else:
1187                 raise ExtractorError(
1188                     '"token" parameter not in video info for unknown reason',
1189                     video_id=video_id)
1190
1191         # title
1192         if 'title' in video_info:
1193             video_title = video_info['title'][0]
1194         else:
1195             self._downloader.report_warning('Unable to extract video title')
1196             video_title = '_'
1197
1198         # description
1199         video_description = get_element_by_id("eow-description", video_webpage)
1200         if video_description:
1201             video_description = re.sub(r'''(?x)
1202                 <a\s+
1203                     (?:[a-zA-Z-]+="[^"]+"\s+)*?
1204                     title="([^"]+)"\s+
1205                     (?:[a-zA-Z-]+="[^"]+"\s+)*?
1206                     class="yt-uix-redirect-link"\s*>
1207                 [^<]+
1208                 </a>
1209             ''', r'\1', video_description)
1210             video_description = clean_html(video_description)
1211         else:
1212             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
1213             if fd_mobj:
1214                 video_description = unescapeHTML(fd_mobj.group(1))
1215             else:
1216                 video_description = ''
1217
1218         if 'multifeed_metadata_list' in video_info and not smuggled_data.get('force_singlefeed', False):
1219             if not self._downloader.params.get('noplaylist'):
1220                 entries = []
1221                 feed_ids = []
1222                 multifeed_metadata_list = compat_urllib_parse_unquote_plus(video_info['multifeed_metadata_list'][0])
1223                 for feed in multifeed_metadata_list.split(','):
1224                     feed_data = compat_parse_qs(feed)
1225                     entries.append({
1226                         '_type': 'url_transparent',
1227                         'ie_key': 'Youtube',
1228                         'url': smuggle_url(
1229                             '%s://www.youtube.com/watch?v=%s' % (proto, feed_data['id'][0]),
1230                             {'force_singlefeed': True}),
1231                         'title': '%s (%s)' % (video_title, feed_data['title'][0]),
1232                     })
1233                     feed_ids.append(feed_data['id'][0])
1234                 self.to_screen(
1235                     'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
1236                     % (', '.join(feed_ids), video_id))
1237                 return self.playlist_result(entries, video_id, video_title, video_description)
1238             self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1239
1240         if 'view_count' in video_info:
1241             view_count = int(video_info['view_count'][0])
1242         else:
1243             view_count = None
1244
1245         # Check for "rental" videos
1246         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
1247             raise ExtractorError('"rental" videos not supported')
1248
1249         # Start extracting information
1250         self.report_information_extraction(video_id)
1251
1252         # uploader
1253         if 'author' not in video_info:
1254             raise ExtractorError('Unable to extract uploader name')
1255         video_uploader = compat_urllib_parse_unquote_plus(video_info['author'][0])
1256
1257         # uploader_id
1258         video_uploader_id = None
1259         mobj = re.search(r'<link itemprop="url" href="http://www.youtube.com/(?:user|channel)/([^"]+)">', video_webpage)
1260         if mobj is not None:
1261             video_uploader_id = mobj.group(1)
1262         else:
1263             self._downloader.report_warning('unable to extract uploader nickname')
1264
1265         # thumbnail image
1266         # We try first to get a high quality image:
1267         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
1268                             video_webpage, re.DOTALL)
1269         if m_thumb is not None:
1270             video_thumbnail = m_thumb.group(1)
1271         elif 'thumbnail_url' not in video_info:
1272             self._downloader.report_warning('unable to extract video thumbnail')
1273             video_thumbnail = None
1274         else:   # don't panic if we can't find it
1275             video_thumbnail = compat_urllib_parse_unquote_plus(video_info['thumbnail_url'][0])
1276
1277         # upload date
1278         upload_date = self._html_search_meta(
1279             'datePublished', video_webpage, 'upload date', default=None)
1280         if not upload_date:
1281             upload_date = self._search_regex(
1282                 [r'(?s)id="eow-date.*?>(.*?)</span>',
1283                  r'id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live|Started) on (.+?)</strong>'],
1284                 video_webpage, 'upload date', default=None)
1285             if upload_date:
1286                 upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
1287         upload_date = unified_strdate(upload_date)
1288
1289         m_cat_container = self._search_regex(
1290             r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
1291             video_webpage, 'categories', default=None)
1292         if m_cat_container:
1293             category = self._html_search_regex(
1294                 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
1295                 default=None)
1296             video_categories = None if category is None else [category]
1297         else:
1298             video_categories = None
1299
1300         video_tags = [
1301             unescapeHTML(m.group('content'))
1302             for m in re.finditer(self._meta_regex('og:video:tag'), video_webpage)]
1303
1304         def _extract_count(count_name):
1305             return str_to_int(self._search_regex(
1306                 r'-%s-button[^>]+><span[^>]+class="yt-uix-button-content"[^>]*>([\d,]+)</span>'
1307                 % re.escape(count_name),
1308                 video_webpage, count_name, default=None))
1309
1310         like_count = _extract_count('like')
1311         dislike_count = _extract_count('dislike')
1312
1313         # subtitles
1314         video_subtitles = self.extract_subtitles(video_id, video_webpage)
1315         automatic_captions = self.extract_automatic_captions(video_id, video_webpage)
1316
1317         if 'length_seconds' not in video_info:
1318             self._downloader.report_warning('unable to extract video duration')
1319             video_duration = None
1320         else:
1321             video_duration = int(compat_urllib_parse_unquote_plus(video_info['length_seconds'][0]))
1322
1323         # annotations
1324         video_annotations = None
1325         if self._downloader.params.get('writeannotations', False):
1326             video_annotations = self._extract_annotations(video_id)
1327
1328         def _map_to_format_list(urlmap):
1329             formats = []
1330             for itag, video_real_url in urlmap.items():
1331                 dct = {
1332                     'format_id': itag,
1333                     'url': video_real_url,
1334                     'player_url': player_url,
1335                 }
1336                 if itag in self._formats:
1337                     dct.update(self._formats[itag])
1338                 formats.append(dct)
1339             return formats
1340
1341         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
1342             self.report_rtmp_download()
1343             formats = [{
1344                 'format_id': '_rtmp',
1345                 'protocol': 'rtmp',
1346                 'url': video_info['conn'][0],
1347                 'player_url': player_url,
1348             }]
1349         elif len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or len(video_info.get('adaptive_fmts', [''])[0]) >= 1:
1350             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts', [''])[0]
1351             if 'rtmpe%3Dyes' in encoded_url_map:
1352                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
1353             formats = []
1354             for url_data_str in encoded_url_map.split(','):
1355                 url_data = compat_parse_qs(url_data_str)
1356                 if 'itag' not in url_data or 'url' not in url_data:
1357                     continue
1358                 format_id = url_data['itag'][0]
1359                 url = url_data['url'][0]
1360
1361                 if 'sig' in url_data:
1362                     url += '&signature=' + url_data['sig'][0]
1363                 elif 's' in url_data:
1364                     encrypted_sig = url_data['s'][0]
1365                     ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
1366
1367                     jsplayer_url_json = self._search_regex(
1368                         ASSETS_RE,
1369                         embed_webpage if age_gate else video_webpage,
1370                         'JS player URL (1)', default=None)
1371                     if not jsplayer_url_json and not age_gate:
1372                         # We need the embed website after all
1373                         if embed_webpage is None:
1374                             embed_url = proto + '://www.youtube.com/embed/%s' % video_id
1375                             embed_webpage = self._download_webpage(
1376                                 embed_url, video_id, 'Downloading embed webpage')
1377                         jsplayer_url_json = self._search_regex(
1378                             ASSETS_RE, embed_webpage, 'JS player URL')
1379
1380                     player_url = json.loads(jsplayer_url_json)
1381                     if player_url is None:
1382                         player_url_json = self._search_regex(
1383                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
1384                             video_webpage, 'age gate player URL')
1385                         player_url = json.loads(player_url_json)
1386
1387                     if self._downloader.params.get('verbose'):
1388                         if player_url is None:
1389                             player_version = 'unknown'
1390                             player_desc = 'unknown'
1391                         else:
1392                             if player_url.endswith('swf'):
1393                                 player_version = self._search_regex(
1394                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
1395                                     'flash player', fatal=False)
1396                                 player_desc = 'flash player %s' % player_version
1397                             else:
1398                                 player_version = self._search_regex(
1399                                     [r'html5player-([^/]+?)(?:/html5player(?:-new)?)?\.js', r'(?:www|player)-([^/]+)/base\.js'],
1400                                     player_url,
1401                                     'html5 player', fatal=False)
1402                                 player_desc = 'html5 player %s' % player_version
1403
1404                         parts_sizes = self._signature_cache_id(encrypted_sig)
1405                         self.to_screen('{%s} signature length %s, %s' %
1406                                        (format_id, parts_sizes, player_desc))
1407
1408                     signature = self._decrypt_signature(
1409                         encrypted_sig, video_id, player_url, age_gate)
1410                     url += '&signature=' + signature
1411                 if 'ratebypass' not in url:
1412                     url += '&ratebypass=yes'
1413
1414                 # Some itags are not included in DASH manifest thus corresponding formats will
1415                 # lack metadata (see https://github.com/rg3/youtube-dl/pull/5993).
1416                 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
1417                 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
1418                 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
1419                 dct = {
1420                     'format_id': format_id,
1421                     'url': url,
1422                     'player_url': player_url,
1423                     'filesize': int_or_none(url_data.get('clen', [None])[0]),
1424                     'tbr': float_or_none(url_data.get('bitrate', [None])[0], 1000),
1425                     'width': width,
1426                     'height': height,
1427                     'fps': int_or_none(url_data.get('fps', [None])[0]),
1428                     'format_note': url_data.get('quality_label', [None])[0] or url_data.get('quality', [None])[0],
1429                 }
1430                 type_ = url_data.get('type', [None])[0]
1431                 if type_:
1432                     type_split = type_.split(';')
1433                     kind_ext = type_split[0].split('/')
1434                     if len(kind_ext) == 2:
1435                         kind, ext = kind_ext
1436                         dct['ext'] = ext
1437                         if kind in ('audio', 'video'):
1438                             codecs = None
1439                             for mobj in re.finditer(
1440                                     r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?)(?P<val>.+?)(?P=quote)(?:;|$)', type_):
1441                                 if mobj.group('key') == 'codecs':
1442                                     codecs = mobj.group('val')
1443                                     break
1444                             if codecs:
1445                                 codecs = codecs.split(',')
1446                                 if len(codecs) == 2:
1447                                     acodec, vcodec = codecs[0], codecs[1]
1448                                 else:
1449                                     acodec, vcodec = (codecs[0], 'none') if kind == 'audio' else ('none', codecs[0])
1450                                 dct.update({
1451                                     'acodec': acodec,
1452                                     'vcodec': vcodec,
1453                                 })
1454                 if format_id in self._formats:
1455                     dct.update(self._formats[format_id])
1456                 formats.append(dct)
1457         elif video_info.get('hlsvp'):
1458             manifest_url = video_info['hlsvp'][0]
1459             url_map = self._extract_from_m3u8(manifest_url, video_id)
1460             formats = _map_to_format_list(url_map)
1461         else:
1462             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1463
1464         # Look for the DASH manifest
1465         if self._downloader.params.get('youtube_include_dash_manifest', True):
1466             dash_mpd_fatal = True
1467             for dash_manifest_url in dash_mpds:
1468                 dash_formats = {}
1469                 try:
1470                     for df in self._parse_dash_manifest(
1471                             video_id, dash_manifest_url, player_url, age_gate, dash_mpd_fatal):
1472                         # Do not overwrite DASH format found in some previous DASH manifest
1473                         if df['format_id'] not in dash_formats:
1474                             dash_formats[df['format_id']] = df
1475                         # Additional DASH manifests may end up in HTTP Error 403 therefore
1476                         # allow them to fail without bug report message if we already have
1477                         # some DASH manifest succeeded. This is temporary workaround to reduce
1478                         # burst of bug reports until we figure out the reason and whether it
1479                         # can be fixed at all.
1480                         dash_mpd_fatal = False
1481                 except (ExtractorError, KeyError) as e:
1482                     self.report_warning(
1483                         'Skipping DASH manifest: %r' % e, video_id)
1484                 if dash_formats:
1485                     # Remove the formats we found through non-DASH, they
1486                     # contain less info and it can be wrong, because we use
1487                     # fixed values (for example the resolution). See
1488                     # https://github.com/rg3/youtube-dl/issues/5774 for an
1489                     # example.
1490                     formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
1491                     formats.extend(dash_formats.values())
1492
1493         # Check for malformed aspect ratio
1494         stretched_m = re.search(
1495             r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
1496             video_webpage)
1497         if stretched_m:
1498             ratio = float(stretched_m.group('w')) / float(stretched_m.group('h'))
1499             for f in formats:
1500                 if f.get('vcodec') != 'none':
1501                     f['stretched_ratio'] = ratio
1502
1503         self._sort_formats(formats)
1504
1505         return {
1506             'id': video_id,
1507             'uploader': video_uploader,
1508             'uploader_id': video_uploader_id,
1509             'upload_date': upload_date,
1510             'title': video_title,
1511             'thumbnail': video_thumbnail,
1512             'description': video_description,
1513             'categories': video_categories,
1514             'tags': video_tags,
1515             'subtitles': video_subtitles,
1516             'automatic_captions': automatic_captions,
1517             'duration': video_duration,
1518             'age_limit': 18 if age_gate else 0,
1519             'annotations': video_annotations,
1520             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
1521             'view_count': view_count,
1522             'like_count': like_count,
1523             'dislike_count': dislike_count,
1524             'average_rating': float_or_none(video_info.get('avg_rating', [None])[0]),
1525             'formats': formats,
1526             'is_live': is_live,
1527             'start_time': start_time,
1528             'end_time': end_time,
1529         }
1530
1531
1532 class YoutubePlaylistIE(YoutubeBaseInfoExtractor, YoutubePlaylistBaseInfoExtractor):
1533     IE_DESC = 'YouTube.com playlists'
1534     _VALID_URL = r"""(?x)(?:
1535                         (?:https?://)?
1536                         (?:\w+\.)?
1537                         youtube\.com/
1538                         (?:
1539                            (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
1540                            \? (?:.*?&)*? (?:p|a|list)=
1541                         |  p/
1542                         )
1543                         (
1544                             (?:PL|LL|EC|UU|FL|RD|UL)?[0-9A-Za-z-_]{10,}
1545                             # Top tracks, they can also include dots
1546                             |(?:MC)[\w\.]*
1547                         )
1548                         .*
1549                      |
1550                         ((?:PL|LL|EC|UU|FL|RD|UL)[0-9A-Za-z-_]{10,})
1551                      )"""
1552     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
1553     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)(?:[^>]+>(?P<title>[^<]+))?'
1554     IE_NAME = 'youtube:playlist'
1555     _TESTS = [{
1556         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1557         'info_dict': {
1558             'title': 'ytdl test PL',
1559             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1560         },
1561         'playlist_count': 3,
1562     }, {
1563         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1564         'info_dict': {
1565             'id': 'PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1566             'title': 'YDL_Empty_List',
1567         },
1568         'playlist_count': 0,
1569     }, {
1570         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1571         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1572         'info_dict': {
1573             'title': '29C3: Not my department',
1574             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1575         },
1576         'playlist_count': 95,
1577     }, {
1578         'note': 'issue #673',
1579         'url': 'PLBB231211A4F62143',
1580         'info_dict': {
1581             'title': '[OLD]Team Fortress 2 (Class-based LP)',
1582             'id': 'PLBB231211A4F62143',
1583         },
1584         'playlist_mincount': 26,
1585     }, {
1586         'note': 'Large playlist',
1587         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1588         'info_dict': {
1589             'title': 'Uploads from Cauchemar',
1590             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
1591         },
1592         'playlist_mincount': 799,
1593     }, {
1594         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1595         'info_dict': {
1596             'title': 'YDL_safe_search',
1597             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1598         },
1599         'playlist_count': 2,
1600     }, {
1601         'note': 'embedded',
1602         'url': 'http://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1603         'playlist_count': 4,
1604         'info_dict': {
1605             'title': 'JODA15',
1606             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1607         }
1608     }, {
1609         'note': 'Embedded SWF player',
1610         'url': 'http://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1611         'playlist_count': 4,
1612         'info_dict': {
1613             'title': 'JODA7',
1614             'id': 'YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ',
1615         }
1616     }, {
1617         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
1618         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
1619         'info_dict': {
1620             'title': 'Uploads from Interstellar Movie',
1621             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
1622         },
1623         'playlist_mincout': 21,
1624     }]
1625
1626     def _real_initialize(self):
1627         self._login()
1628
1629     def _extract_mix(self, playlist_id):
1630         # The mixes are generated from a single video
1631         # the id of the playlist is just 'RD' + video_id
1632         url = 'https://youtube.com/watch?v=%s&list=%s' % (playlist_id[-11:], playlist_id)
1633         webpage = self._download_webpage(
1634             url, playlist_id, 'Downloading Youtube mix')
1635         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
1636         title_span = (
1637             search_title('playlist-title') or
1638             search_title('title long-title') or
1639             search_title('title'))
1640         title = clean_html(title_span)
1641         ids = orderedSet(re.findall(
1642             r'''(?xs)data-video-username=".*?".*?
1643                        href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
1644             webpage))
1645         url_results = self._ids_to_results(ids)
1646
1647         return self.playlist_result(url_results, playlist_id, title)
1648
1649     def _extract_playlist(self, playlist_id):
1650         url = self._TEMPLATE_URL % playlist_id
1651         page = self._download_webpage(url, playlist_id)
1652
1653         for match in re.findall(r'<div class="yt-alert-message">([^<]+)</div>', page):
1654             match = match.strip()
1655             # Check if the playlist exists or is private
1656             if re.match(r'[^<]*(The|This) playlist (does not exist|is private)[^<]*', match):
1657                 raise ExtractorError(
1658                     'The playlist doesn\'t exist or is private, use --username or '
1659                     '--netrc to access it.',
1660                     expected=True)
1661             elif re.match(r'[^<]*Invalid parameters[^<]*', match):
1662                 raise ExtractorError(
1663                     'Invalid parameters. Maybe URL is incorrect.',
1664                     expected=True)
1665             elif re.match(r'[^<]*Choose your language[^<]*', match):
1666                 continue
1667             else:
1668                 self.report_warning('Youtube gives an alert message: ' + match)
1669
1670         playlist_title = self._html_search_regex(
1671             r'(?s)<h1 class="pl-header-title[^"]*"[^>]*>\s*(.*?)\s*</h1>',
1672             page, 'title')
1673
1674         return self.playlist_result(self._entries(page, playlist_id), playlist_id, playlist_title)
1675
1676     def _real_extract(self, url):
1677         # Extract playlist id
1678         mobj = re.match(self._VALID_URL, url)
1679         if mobj is None:
1680             raise ExtractorError('Invalid URL: %s' % url)
1681         playlist_id = mobj.group(1) or mobj.group(2)
1682
1683         # Check if it's a video-specific URL
1684         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
1685         if 'v' in query_dict:
1686             video_id = query_dict['v'][0]
1687             if self._downloader.params.get('noplaylist'):
1688                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1689                 return self.url_result(video_id, 'Youtube', video_id=video_id)
1690             else:
1691                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
1692
1693         if playlist_id.startswith('RD') or playlist_id.startswith('UL'):
1694             # Mixes require a custom extraction process
1695             return self._extract_mix(playlist_id)
1696
1697         return self._extract_playlist(playlist_id)
1698
1699
1700 class YoutubeChannelIE(YoutubePlaylistBaseInfoExtractor):
1701     IE_DESC = 'YouTube.com channels'
1702     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/(?P<id>[0-9A-Za-z_-]+)'
1703     _TEMPLATE_URL = 'https://www.youtube.com/channel/%s/videos'
1704     _VIDEO_RE = r'(?:title="(?P<title>[^"]+)"[^>]+)?href="/watch\?v=(?P<id>[0-9A-Za-z_-]+)&?'
1705     IE_NAME = 'youtube:channel'
1706     _TESTS = [{
1707         'note': 'paginated channel',
1708         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
1709         'playlist_mincount': 91,
1710         'info_dict': {
1711             'id': 'UUKfVa3S1e4PHvxWcwyMMg8w',
1712             'title': 'Uploads from lex will',
1713         }
1714     }, {
1715         'note': 'Age restricted channel',
1716         # from https://www.youtube.com/user/DeusExOfficial
1717         'url': 'https://www.youtube.com/channel/UCs0ifCMCm1icqRbqhUINa0w',
1718         'playlist_mincount': 64,
1719         'info_dict': {
1720             'id': 'UUs0ifCMCm1icqRbqhUINa0w',
1721             'title': 'Uploads from Deus Ex',
1722         },
1723     }]
1724
1725     def _real_extract(self, url):
1726         channel_id = self._match_id(url)
1727
1728         url = self._TEMPLATE_URL % channel_id
1729
1730         # Channel by page listing is restricted to 35 pages of 30 items, i.e. 1050 videos total (see #5778)
1731         # Workaround by extracting as a playlist if managed to obtain channel playlist URL
1732         # otherwise fallback on channel by page extraction
1733         channel_page = self._download_webpage(
1734             url + '?view=57', channel_id,
1735             'Downloading channel page', fatal=False)
1736         if channel_page is False:
1737             channel_playlist_id = False
1738         else:
1739             channel_playlist_id = self._html_search_meta(
1740                 'channelId', channel_page, 'channel id', default=None)
1741             if not channel_playlist_id:
1742                 channel_playlist_id = self._search_regex(
1743                     r'data-(?:channel-external-|yt)id="([^"]+)"',
1744                     channel_page, 'channel id', default=None)
1745         if channel_playlist_id and channel_playlist_id.startswith('UC'):
1746             playlist_id = 'UU' + channel_playlist_id[2:]
1747             return self.url_result(
1748                 compat_urlparse.urljoin(url, '/playlist?list=%s' % playlist_id), 'YoutubePlaylist')
1749
1750         channel_page = self._download_webpage(url, channel_id, 'Downloading page #1')
1751         autogenerated = re.search(r'''(?x)
1752                 class="[^"]*?(?:
1753                     channel-header-autogenerated-label|
1754                     yt-channel-title-autogenerated
1755                 )[^"]*"''', channel_page) is not None
1756
1757         if autogenerated:
1758             # The videos are contained in a single page
1759             # the ajax pages can't be used, they are empty
1760             entries = [
1761                 self.url_result(
1762                     video_id, 'Youtube', video_id=video_id,
1763                     video_title=video_title)
1764                 for video_id, video_title in self.extract_videos_from_page(channel_page)]
1765             return self.playlist_result(entries, channel_id)
1766
1767         return self.playlist_result(self._entries(channel_page, channel_id), channel_id)
1768
1769
1770 class YoutubeUserIE(YoutubeChannelIE):
1771     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
1772     _VALID_URL = r'(?:(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:user/)?(?!(?:attribution_link|watch|results)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)(?P<id>[A-Za-z0-9_-]+)'
1773     _TEMPLATE_URL = 'https://www.youtube.com/user/%s/videos'
1774     IE_NAME = 'youtube:user'
1775
1776     _TESTS = [{
1777         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
1778         'playlist_mincount': 320,
1779         'info_dict': {
1780             'title': 'TheLinuxFoundation',
1781         }
1782     }, {
1783         'url': 'ytuser:phihag',
1784         'only_matching': True,
1785     }]
1786
1787     @classmethod
1788     def suitable(cls, url):
1789         # Don't return True if the url can be extracted with other youtube
1790         # extractor, the regex would is too permissive and it would match.
1791         other_ies = iter(klass for (name, klass) in globals().items() if name.endswith('IE') and klass is not cls)
1792         if any(ie.suitable(url) for ie in other_ies):
1793             return False
1794         else:
1795             return super(YoutubeUserIE, cls).suitable(url)
1796
1797
1798 class YoutubeUserPlaylistsIE(YoutubePlaylistsBaseInfoExtractor):
1799     IE_DESC = 'YouTube.com user playlists'
1800     _VALID_URL = r'https?://(?:\w+\.)?youtube\.com/user/(?P<id>[^/]+)/playlists'
1801     IE_NAME = 'youtube:user:playlists'
1802
1803     _TESTS = [{
1804         'url': 'http://www.youtube.com/user/ThirstForScience/playlists',
1805         'playlist_mincount': 4,
1806         'info_dict': {
1807             'id': 'ThirstForScience',
1808             'title': 'Thirst for Science',
1809         },
1810     }, {
1811         # with "Load more" button
1812         'url': 'http://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
1813         'playlist_mincount': 70,
1814         'info_dict': {
1815             'id': 'igorkle1',
1816             'title': 'Игорь Клейнер',
1817         },
1818     }]
1819
1820
1821 class YoutubeSearchIE(SearchInfoExtractor, YoutubePlaylistIE):
1822     IE_DESC = 'YouTube.com searches'
1823     # there doesn't appear to be a real limit, for example if you search for
1824     # 'python' you get more than 8.000.000 results
1825     _MAX_RESULTS = float('inf')
1826     IE_NAME = 'youtube:search'
1827     _SEARCH_KEY = 'ytsearch'
1828     _EXTRA_QUERY_ARGS = {}
1829     _TESTS = []
1830
1831     def _get_n_results(self, query, n):
1832         """Get a specified number of results for a query"""
1833
1834         videos = []
1835         limit = n
1836
1837         for pagenum in itertools.count(1):
1838             url_query = {
1839                 'search_query': query.encode('utf-8'),
1840                 'page': pagenum,
1841                 'spf': 'navigate',
1842             }
1843             url_query.update(self._EXTRA_QUERY_ARGS)
1844             result_url = 'https://www.youtube.com/results?' + compat_urllib_parse.urlencode(url_query)
1845             data = self._download_json(
1846                 result_url, video_id='query "%s"' % query,
1847                 note='Downloading page %s' % pagenum,
1848                 errnote='Unable to download API page')
1849             html_content = data[1]['body']['content']
1850
1851             if 'class="search-message' in html_content:
1852                 raise ExtractorError(
1853                     '[youtube] No video results', expected=True)
1854
1855             new_videos = self._ids_to_results(orderedSet(re.findall(
1856                 r'href="/watch\?v=(.{11})', html_content)))
1857             videos += new_videos
1858             if not new_videos or len(videos) > limit:
1859                 break
1860
1861         if len(videos) > n:
1862             videos = videos[:n]
1863         return self.playlist_result(videos, query)
1864
1865
1866 class YoutubeSearchDateIE(YoutubeSearchIE):
1867     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
1868     _SEARCH_KEY = 'ytsearchdate'
1869     IE_DESC = 'YouTube.com searches, newest videos first'
1870     _EXTRA_QUERY_ARGS = {'search_sort': 'video_date_uploaded'}
1871
1872
1873 class YoutubeSearchURLIE(InfoExtractor):
1874     IE_DESC = 'YouTube.com search URLs'
1875     IE_NAME = 'youtube:search_url'
1876     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?search_query=(?P<query>[^&]+)(?:[&]|$)'
1877     _TESTS = [{
1878         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
1879         'playlist_mincount': 5,
1880         'info_dict': {
1881             'title': 'youtube-dl test video',
1882         }
1883     }]
1884
1885     def _real_extract(self, url):
1886         mobj = re.match(self._VALID_URL, url)
1887         query = compat_urllib_parse_unquote_plus(mobj.group('query'))
1888
1889         webpage = self._download_webpage(url, query)
1890         result_code = self._search_regex(
1891             r'(?s)<ol[^>]+class="item-section"(.*?)</ol>', webpage, 'result HTML')
1892
1893         part_codes = re.findall(
1894             r'(?s)<h3[^>]+class="[^"]*yt-lockup-title[^"]*"[^>]*>(.*?)</h3>', result_code)
1895         entries = []
1896         for part_code in part_codes:
1897             part_title = self._html_search_regex(
1898                 [r'(?s)title="([^"]+)"', r'>([^<]+)</a>'], part_code, 'item title', fatal=False)
1899             part_url_snippet = self._html_search_regex(
1900                 r'(?s)href="([^"]+)"', part_code, 'item URL')
1901             part_url = compat_urlparse.urljoin(
1902                 'https://www.youtube.com/', part_url_snippet)
1903             entries.append({
1904                 '_type': 'url',
1905                 'url': part_url,
1906                 'title': part_title,
1907             })
1908
1909         return {
1910             '_type': 'playlist',
1911             'entries': entries,
1912             'title': query,
1913         }
1914
1915
1916 class YoutubeShowIE(YoutubePlaylistsBaseInfoExtractor):
1917     IE_DESC = 'YouTube.com (multi-season) shows'
1918     _VALID_URL = r'https?://www\.youtube\.com/show/(?P<id>[^?#]*)'
1919     IE_NAME = 'youtube:show'
1920     _TESTS = [{
1921         'url': 'https://www.youtube.com/show/airdisasters',
1922         'playlist_mincount': 5,
1923         'info_dict': {
1924             'id': 'airdisasters',
1925             'title': 'Air Disasters',
1926         }
1927     }]
1928
1929     def _real_extract(self, url):
1930         playlist_id = self._match_id(url)
1931         return super(YoutubeShowIE, self)._real_extract(
1932             'https://www.youtube.com/show/%s/playlists' % playlist_id)
1933
1934
1935 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
1936     """
1937     Base class for feed extractors
1938     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
1939     """
1940     _LOGIN_REQUIRED = True
1941
1942     @property
1943     def IE_NAME(self):
1944         return 'youtube:%s' % self._FEED_NAME
1945
1946     def _real_initialize(self):
1947         self._login()
1948
1949     def _real_extract(self, url):
1950         page = self._download_webpage(
1951             'https://www.youtube.com/feed/%s' % self._FEED_NAME, self._PLAYLIST_TITLE)
1952
1953         # The extraction process is the same as for playlists, but the regex
1954         # for the video ids doesn't contain an index
1955         ids = []
1956         more_widget_html = content_html = page
1957         for page_num in itertools.count(1):
1958             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
1959
1960             # 'recommended' feed has infinite 'load more' and each new portion spins
1961             # the same videos in (sometimes) slightly different order, so we'll check
1962             # for unicity and break when portion has no new videos
1963             new_ids = filter(lambda video_id: video_id not in ids, orderedSet(matches))
1964             if not new_ids:
1965                 break
1966
1967             ids.extend(new_ids)
1968
1969             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
1970             if not mobj:
1971                 break
1972
1973             more = self._download_json(
1974                 'https://youtube.com/%s' % mobj.group('more'), self._PLAYLIST_TITLE,
1975                 'Downloading page #%s' % page_num,
1976                 transform_source=uppercase_escape)
1977             content_html = more['content_html']
1978             more_widget_html = more['load_more_widget_html']
1979
1980         return self.playlist_result(
1981             self._ids_to_results(ids), playlist_title=self._PLAYLIST_TITLE)
1982
1983
1984 class YoutubeWatchLaterIE(YoutubePlaylistIE):
1985     IE_NAME = 'youtube:watchlater'
1986     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
1987     _VALID_URL = r'https?://www\.youtube\.com/(?:feed/watch_later|playlist\?list=WL)|:ytwatchlater'
1988
1989     _TESTS = []  # override PlaylistIE tests
1990
1991     def _real_extract(self, url):
1992         return self._extract_playlist('WL')
1993
1994
1995 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
1996     IE_NAME = 'youtube:favorites'
1997     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
1998     _VALID_URL = r'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
1999     _LOGIN_REQUIRED = True
2000
2001     def _real_extract(self, url):
2002         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
2003         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
2004         return self.url_result(playlist_id, 'YoutubePlaylist')
2005
2006
2007 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
2008     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
2009     _VALID_URL = r'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?'
2010     _FEED_NAME = 'recommended'
2011     _PLAYLIST_TITLE = 'Youtube Recommended videos'
2012
2013
2014 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
2015     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
2016     _VALID_URL = r'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
2017     _FEED_NAME = 'subscriptions'
2018     _PLAYLIST_TITLE = 'Youtube Subscriptions'
2019
2020
2021 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
2022     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
2023     _VALID_URL = 'https?://www\.youtube\.com/feed/history|:ythistory'
2024     _FEED_NAME = 'history'
2025     _PLAYLIST_TITLE = 'Youtube History'
2026
2027
2028 class YoutubeTruncatedURLIE(InfoExtractor):
2029     IE_NAME = 'youtube:truncated_url'
2030     IE_DESC = False  # Do not list
2031     _VALID_URL = r'''(?x)
2032         (?:https?://)?
2033         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
2034         (?:watch\?(?:
2035             feature=[a-z_]+|
2036             annotation_id=annotation_[^&]+|
2037             x-yt-cl=[0-9]+|
2038             hl=[^&]*|
2039             t=[0-9]+
2040         )?
2041         |
2042             attribution_link\?a=[^&]+
2043         )
2044         $
2045     '''
2046
2047     _TESTS = [{
2048         'url': 'http://www.youtube.com/watch?annotation_id=annotation_3951667041',
2049         'only_matching': True,
2050     }, {
2051         'url': 'http://www.youtube.com/watch?',
2052         'only_matching': True,
2053     }, {
2054         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
2055         'only_matching': True,
2056     }, {
2057         'url': 'https://www.youtube.com/watch?feature=foo',
2058         'only_matching': True,
2059     }, {
2060         'url': 'https://www.youtube.com/watch?hl=en-GB',
2061         'only_matching': True,
2062     }, {
2063         'url': 'https://www.youtube.com/watch?t=2372',
2064         'only_matching': True,
2065     }]
2066
2067     def _real_extract(self, url):
2068         raise ExtractorError(
2069             'Did you forget to quote the URL? Remember that & is a meta '
2070             'character in most shells, so you want to put the URL in quotes, '
2071             'like  youtube-dl '
2072             '"http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
2073             ' or simply  youtube-dl BaW_jenozKc  .',
2074             expected=True)
2075
2076
2077 class YoutubeTruncatedIDIE(InfoExtractor):
2078     IE_NAME = 'youtube:truncated_id'
2079     IE_DESC = False  # Do not list
2080     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
2081
2082     _TESTS = [{
2083         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
2084         'only_matching': True,
2085     }]
2086
2087     def _real_extract(self, url):
2088         video_id = self._match_id(url)
2089         raise ExtractorError(
2090             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
2091             expected=True)