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