[youtube] Handle missing DASH manifest (Fixes #4421, fixes #4420)
[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 .subtitles import SubtitlesInfoExtractor
15 from ..jsinterp import JSInterpreter
16 from ..swfinterp import SWFInterpreter
17 from ..utils import (
18     compat_chr,
19     compat_parse_qs,
20     compat_urllib_parse,
21     compat_urllib_request,
22     compat_urlparse,
23     compat_str,
24
25     clean_html,
26     get_element_by_id,
27     get_element_by_attribute,
28     ExtractorError,
29     int_or_none,
30     OnDemandPagedList,
31     unescapeHTML,
32     unified_strdate,
33     orderedSet,
34     uppercase_escape,
35 )
36
37
38 class YoutubeBaseInfoExtractor(InfoExtractor):
39     """Provide base functions for Youtube extractors"""
40     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
41     _TWOFACTOR_URL = 'https://accounts.google.com/SecondFactor'
42     _NETRC_MACHINE = 'youtube'
43     # If True it will raise an error if no login info is provided
44     _LOGIN_REQUIRED = False
45
46     def _set_language(self):
47         self._set_cookie(
48             '.youtube.com', 'PREF', 'f1=50000000&hl=en',
49             # YouTube sets the expire time to about two months
50             expire_time=time.time() + 2 * 30 * 24 * 3600)
51
52     def _login(self):
53         """
54         Attempt to log in to YouTube.
55         True is returned if successful or skipped.
56         False is returned if login failed.
57
58         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
59         """
60         (username, password) = self._get_login_info()
61         # No authentication to be performed
62         if username is None:
63             if self._LOGIN_REQUIRED:
64                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
65             return True
66
67         login_page = self._download_webpage(
68             self._LOGIN_URL, None,
69             note='Downloading login page',
70             errnote='unable to fetch login page', fatal=False)
71         if login_page is False:
72             return
73
74         galx = self._search_regex(r'(?s)<input.+?name="GALX".+?value="(.+?)"',
75                                   login_page, 'Login GALX parameter')
76
77         # Log in
78         login_form_strs = {
79             'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
80             'Email': username,
81             'GALX': galx,
82             'Passwd': password,
83
84             'PersistentCookie': 'yes',
85             '_utf8': '霱',
86             'bgresponse': 'js_disabled',
87             'checkConnection': '',
88             'checkedDomains': 'youtube',
89             'dnConn': '',
90             'pstMsg': '0',
91             'rmShown': '1',
92             'secTok': '',
93             'signIn': 'Sign in',
94             'timeStmp': '',
95             'service': 'youtube',
96             'uilel': '3',
97             'hl': 'en_US',
98         }
99
100         # Convert to UTF-8 *before* urlencode because Python 2.x's urlencode
101         # chokes on unicode
102         login_form = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in login_form_strs.items())
103         login_data = compat_urllib_parse.urlencode(login_form).encode('ascii')
104
105         req = compat_urllib_request.Request(self._LOGIN_URL, login_data)
106         login_results = self._download_webpage(
107             req, None,
108             note='Logging in', errnote='unable to log in', fatal=False)
109         if login_results is False:
110             return False
111
112         if re.search(r'id="errormsg_0_Passwd"', login_results) is not None:
113             raise ExtractorError('Please use your account password and a two-factor code instead of an application-specific password.', expected=True)
114
115         # Two-Factor
116         # TODO add SMS and phone call support - these require making a request and then prompting the user
117
118         if re.search(r'(?i)<form[^>]* id="gaia_secondfactorform"', login_results) is not None:
119             tfa_code = self._get_tfa_info()
120
121             if tfa_code is None:
122                 self._downloader.report_warning('Two-factor authentication required. Provide it with --twofactor <code>')
123                 self._downloader.report_warning('(Note that only TOTP (Google Authenticator App) codes work at this time.)')
124                 return False
125
126             # Unlike the first login form, secTok and timeStmp are both required for the TFA form
127
128             match = re.search(r'id="secTok"\n\s+value=\'(.+)\'/>', login_results, re.M | re.U)
129             if match is None:
130                 self._downloader.report_warning('Failed to get secTok - did the page structure change?')
131             secTok = match.group(1)
132             match = re.search(r'id="timeStmp"\n\s+value=\'(.+)\'/>', login_results, re.M | re.U)
133             if match is None:
134                 self._downloader.report_warning('Failed to get timeStmp - did the page structure change?')
135             timeStmp = match.group(1)
136
137             tfa_form_strs = {
138                 'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
139                 'smsToken': '',
140                 'smsUserPin': tfa_code,
141                 'smsVerifyPin': 'Verify',
142
143                 'PersistentCookie': 'yes',
144                 'checkConnection': '',
145                 'checkedDomains': 'youtube',
146                 'pstMsg': '1',
147                 'secTok': secTok,
148                 'timeStmp': timeStmp,
149                 'service': 'youtube',
150                 'hl': 'en_US',
151             }
152             tfa_form = dict((k.encode('utf-8'), v.encode('utf-8')) for k, v in tfa_form_strs.items())
153             tfa_data = compat_urllib_parse.urlencode(tfa_form).encode('ascii')
154
155             tfa_req = compat_urllib_request.Request(self._TWOFACTOR_URL, tfa_data)
156             tfa_results = self._download_webpage(
157                 tfa_req, None,
158                 note='Submitting TFA code', errnote='unable to submit tfa', fatal=False)
159
160             if tfa_results is False:
161                 return False
162
163             if re.search(r'(?i)<form[^>]* id="gaia_secondfactorform"', tfa_results) is not None:
164                 self._downloader.report_warning('Two-factor code expired. Please try again, or use a one-use backup code instead.')
165                 return False
166             if re.search(r'(?i)<form[^>]* id="gaia_loginform"', tfa_results) is not None:
167                 self._downloader.report_warning('unable to log in - did the page structure change?')
168                 return False
169             if re.search(r'smsauth-interstitial-reviewsettings', tfa_results) is not None:
170                 self._downloader.report_warning('Your Google account has a security notice. Please log in on your web browser, resolve the notice, and try again.')
171                 return False
172
173         if re.search(r'(?i)<form[^>]* id="gaia_loginform"', login_results) is not None:
174             self._downloader.report_warning('unable to log in: bad username or password')
175             return False
176         return True
177
178     def _real_initialize(self):
179         if self._downloader is None:
180             return
181         self._set_language()
182         if not self._login():
183             return
184
185
186 class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
187     IE_DESC = 'YouTube.com'
188     _VALID_URL = r"""(?x)^
189                      (
190                          (?:https?://|//)                                    # http(s):// or protocol-independent URL
191                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
192                             (?:www\.)?deturl\.com/www\.youtube\.com/|
193                             (?:www\.)?pwnyoutube\.com/|
194                             (?:www\.)?yourepeat\.com/|
195                             tube\.majestyc\.net/|
196                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains
197                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
198                          (?:                                                  # the various things that can precede the ID:
199                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/
200                              |(?:                                             # or the v= param in all its forms
201                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
202                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
203                                  (?:.*?&)?                                    # any other preceding param (like /?s=tuff&v=xxxx)
204                                  v=
205                              )
206                          ))
207                          |youtu\.be/                                          # just youtu.be/xxxx
208                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
209                          )
210                      )?                                                       # all until now is optional -> you can pass the naked ID
211                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID
212                      (?!.*?&list=)                                            # combined list/video URLs are handled by the playlist IE
213                      (?(1).+)?                                                # if we found the ID, everything can follow
214                      $"""
215     _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
216     _formats = {
217         '5': {'ext': 'flv', 'width': 400, 'height': 240},
218         '6': {'ext': 'flv', 'width': 450, 'height': 270},
219         '13': {'ext': '3gp'},
220         '17': {'ext': '3gp', 'width': 176, 'height': 144},
221         '18': {'ext': 'mp4', 'width': 640, 'height': 360},
222         '22': {'ext': 'mp4', 'width': 1280, 'height': 720},
223         '34': {'ext': 'flv', 'width': 640, 'height': 360},
224         '35': {'ext': 'flv', 'width': 854, 'height': 480},
225         '36': {'ext': '3gp', 'width': 320, 'height': 240},
226         '37': {'ext': 'mp4', 'width': 1920, 'height': 1080},
227         '38': {'ext': 'mp4', 'width': 4096, 'height': 3072},
228         '43': {'ext': 'webm', 'width': 640, 'height': 360},
229         '44': {'ext': 'webm', 'width': 854, 'height': 480},
230         '45': {'ext': 'webm', 'width': 1280, 'height': 720},
231         '46': {'ext': 'webm', 'width': 1920, 'height': 1080},
232
233
234         # 3d videos
235         '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'preference': -20},
236         '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'preference': -20},
237         '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'preference': -20},
238         '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'preference': -20},
239         '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'preference': -20},
240         '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'preference': -20},
241         '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'preference': -20},
242
243         # Apple HTTP Live Streaming
244         '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
245         '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'preference': -10},
246         '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'preference': -10},
247         '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'preference': -10},
248         '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'preference': -10},
249         '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
250         '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'preference': -10},
251
252         # DASH mp4 video
253         '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
254         '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
255         '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
256         '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
257         '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
258         '138': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
259         '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
260         '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
261         '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'h264'},
262         '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'h264'},
263         '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'vcodec': 'h264'},
264
265         # Dash mp4 audio
266         '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 48, 'preference': -50},
267         '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 128, 'preference': -50},
268         '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 256, 'preference': -50},
269
270         # Dash webm
271         '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
272         '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
273         '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
274         '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
275         '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
276         '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
277         '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'container': 'webm', 'vcodec': 'VP9'},
278         '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
279         '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
280         '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
281         '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
282         '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
283         '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
284         '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
285         '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
286         '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
287         '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'VP9'},
288         '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'fps': 60, 'vcodec': 'VP9'},
289         '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40, 'vcodec': 'VP9'},
290
291         # Dash webm audio
292         '171': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 128, 'preference': -50},
293         '172': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 256, 'preference': -50},
294
295         # Dash webm audio with opus inside
296         '249': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50, 'preference': -50},
297         '250': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70, 'preference': -50},
298         '251': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160, 'preference': -50},
299
300         # RTMP (unnamed)
301         '_rtmp': {'protocol': 'rtmp'},
302     }
303
304     IE_NAME = 'youtube'
305     _TESTS = [
306         {
307             'url': 'http://www.youtube.com/watch?v=BaW_jenozKc',
308             'info_dict': {
309                 'id': 'BaW_jenozKc',
310                 'ext': 'mp4',
311                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
312                 'uploader': 'Philipp Hagemeister',
313                 'uploader_id': 'phihag',
314                 'upload_date': '20121002',
315                 '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 .',
316                 'categories': ['Science & Technology'],
317                 'like_count': int,
318                 'dislike_count': int,
319             }
320         },
321         {
322             'url': 'http://www.youtube.com/watch?v=UxxajLWwzqY',
323             'note': 'Test generic use_cipher_signature video (#897)',
324             'info_dict': {
325                 'id': 'UxxajLWwzqY',
326                 'ext': 'mp4',
327                 'upload_date': '20120506',
328                 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]',
329                 'description': 'md5:fea86fda2d5a5784273df5c7cc994d9f',
330                 'uploader': 'Icona Pop',
331                 'uploader_id': 'IconaPop',
332             }
333         },
334         {
335             'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ',
336             'note': 'Test VEVO video with age protection (#956)',
337             'info_dict': {
338                 'id': '07FYdnEawAQ',
339                 'ext': 'mp4',
340                 'upload_date': '20130703',
341                 'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
342                 'description': 'md5:64249768eec3bc4276236606ea996373',
343                 'uploader': 'justintimberlakeVEVO',
344                 'uploader_id': 'justintimberlakeVEVO',
345             }
346         },
347         {
348             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
349             'note': 'Embed-only video (#1746)',
350             'info_dict': {
351                 'id': 'yZIXLfi8CZQ',
352                 'ext': 'mp4',
353                 'upload_date': '20120608',
354                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
355                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
356                 'uploader': 'SET India',
357                 'uploader_id': 'setindia'
358             }
359         },
360         {
361             'url': 'http://www.youtube.com/watch?v=a9LDPn-MO4I',
362             'note': '256k DASH audio (format 141) via DASH manifest',
363             'info_dict': {
364                 'id': 'a9LDPn-MO4I',
365                 'ext': 'm4a',
366                 'upload_date': '20121002',
367                 'uploader_id': '8KVIDEO',
368                 'description': '',
369                 'uploader': '8KVIDEO',
370                 'title': 'UHDTV TEST 8K VIDEO.mp4'
371             },
372             'params': {
373                 'youtube_include_dash_manifest': True,
374                 'format': '141',
375             },
376         },
377         # DASH manifest with encrypted signature
378         {
379             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
380             'info_dict': {
381                 'id': 'IB3lcPjvWLA',
382                 'ext': 'm4a',
383                 'title': 'Afrojack, Spree Wilson - The Spark ft. Spree Wilson',
384                 'description': 'md5:12e7067fa6735a77bdcbb58cb1187d2d',
385                 'uploader': 'AfrojackVEVO',
386                 'uploader_id': 'AfrojackVEVO',
387                 'upload_date': '20131011',
388             },
389             'params': {
390                 'youtube_include_dash_manifest': True,
391                 'format': '141',
392             },
393         },
394         # Controversy video
395         {
396             'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
397             'info_dict': {
398                 'id': 'T4XJQO3qol8',
399                 'ext': 'mp4',
400                 'upload_date': '20100909',
401                 'uploader': 'The Amazing Atheist',
402                 'uploader_id': 'TheAmazingAtheist',
403                 'title': 'Burning Everyone\'s Koran',
404                 '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',
405             }
406         },
407         # Normal age-gate video (No vevo, embed allowed)
408         {
409             'url': 'http://youtube.com/watch?v=HtVdAasjOgU',
410             'info_dict': {
411                 'id': 'HtVdAasjOgU',
412                 'ext': 'mp4',
413                 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
414                 'description': 'md5:eca57043abae25130f58f655ad9a7771',
415                 'uploader': 'The Witcher',
416                 'uploader_id': 'WitcherGame',
417                 'upload_date': '20140605',
418             },
419         },
420         # video_info is None (https://github.com/rg3/youtube-dl/issues/4421)
421         {
422             'url': '__2ABJjxzNo',
423             'info_dict': {
424                 'id': '__2ABJjxzNo',
425                 'ext': 'mp4',
426                 'upload_date': '20100430',
427                 'uploader_id': 'deadmau5',
428                 'description': 'md5:12c56784b8032162bb936a5f76d55360',
429                 'uploader': 'deadmau5',
430                 'title': 'Deadmau5 - Some Chords (HD)',
431             },
432             'expected_warnings': [
433                 'DASH manifest missing',
434             ]
435         }
436     ]
437
438     def __init__(self, *args, **kwargs):
439         super(YoutubeIE, self).__init__(*args, **kwargs)
440         self._player_cache = {}
441
442     def report_video_info_webpage_download(self, video_id):
443         """Report attempt to download video info webpage."""
444         self.to_screen('%s: Downloading video info webpage' % video_id)
445
446     def report_information_extraction(self, video_id):
447         """Report attempt to extract video information."""
448         self.to_screen('%s: Extracting video information' % video_id)
449
450     def report_unavailable_format(self, video_id, format):
451         """Report extracted video URL."""
452         self.to_screen('%s: Format %s not available' % (video_id, format))
453
454     def report_rtmp_download(self):
455         """Indicate the download will use the RTMP protocol."""
456         self.to_screen('RTMP download detected')
457
458     def _signature_cache_id(self, example_sig):
459         """ Return a string representation of a signature """
460         return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
461
462     def _extract_signature_function(self, video_id, player_url, example_sig):
463         id_m = re.match(
464             r'.*-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.(?P<ext>[a-z]+)$',
465             player_url)
466         if not id_m:
467             raise ExtractorError('Cannot identify player %r' % player_url)
468         player_type = id_m.group('ext')
469         player_id = id_m.group('id')
470
471         # Read from filesystem cache
472         func_id = '%s_%s_%s' % (
473             player_type, player_id, self._signature_cache_id(example_sig))
474         assert os.path.basename(func_id) == func_id
475
476         cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
477         if cache_spec is not None:
478             return lambda s: ''.join(s[i] for i in cache_spec)
479
480         if player_type == 'js':
481             code = self._download_webpage(
482                 player_url, video_id,
483                 note='Downloading %s player %s' % (player_type, player_id),
484                 errnote='Download of %s failed' % player_url)
485             res = self._parse_sig_js(code)
486         elif player_type == 'swf':
487             urlh = self._request_webpage(
488                 player_url, video_id,
489                 note='Downloading %s player %s' % (player_type, player_id),
490                 errnote='Download of %s failed' % player_url)
491             code = urlh.read()
492             res = self._parse_sig_swf(code)
493         else:
494             assert False, 'Invalid player type %r' % player_type
495
496         if cache_spec is None:
497             test_string = ''.join(map(compat_chr, range(len(example_sig))))
498             cache_res = res(test_string)
499             cache_spec = [ord(c) for c in cache_res]
500
501         self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
502         return res
503
504     def _print_sig_code(self, func, example_sig):
505         def gen_sig_code(idxs):
506             def _genslice(start, end, step):
507                 starts = '' if start == 0 else str(start)
508                 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
509                 steps = '' if step == 1 else (':%d' % step)
510                 return 's[%s%s%s]' % (starts, ends, steps)
511
512             step = None
513             start = '(Never used)'  # Quelch pyflakes warnings - start will be
514                                     # set as soon as step is set
515             for i, prev in zip(idxs[1:], idxs[:-1]):
516                 if step is not None:
517                     if i - prev == step:
518                         continue
519                     yield _genslice(start, prev, step)
520                     step = None
521                     continue
522                 if i - prev in [-1, 1]:
523                     step = i - prev
524                     start = prev
525                     continue
526                 else:
527                     yield 's[%d]' % prev
528             if step is None:
529                 yield 's[%d]' % i
530             else:
531                 yield _genslice(start, i, step)
532
533         test_string = ''.join(map(compat_chr, range(len(example_sig))))
534         cache_res = func(test_string)
535         cache_spec = [ord(c) for c in cache_res]
536         expr_code = ' + '.join(gen_sig_code(cache_spec))
537         signature_id_tuple = '(%s)' % (
538             ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
539         code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
540                 '    return %s\n') % (signature_id_tuple, expr_code)
541         self.to_screen('Extracted signature function:\n' + code)
542
543     def _parse_sig_js(self, jscode):
544         funcname = self._search_regex(
545             r'\.sig\|\|([a-zA-Z0-9]+)\(', jscode,
546             'Initial JS player signature function name')
547
548         jsi = JSInterpreter(jscode)
549         initial_function = jsi.extract_function(funcname)
550         return lambda s: initial_function([s])
551
552     def _parse_sig_swf(self, file_contents):
553         swfi = SWFInterpreter(file_contents)
554         TARGET_CLASSNAME = 'SignatureDecipher'
555         searched_class = swfi.extract_class(TARGET_CLASSNAME)
556         initial_function = swfi.extract_function(searched_class, 'decipher')
557         return lambda s: initial_function([s])
558
559     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
560         """Turn the encrypted s field into a working signature"""
561
562         if player_url is None:
563             raise ExtractorError('Cannot decrypt signature without player_url')
564
565         if player_url.startswith('//'):
566             player_url = 'https:' + player_url
567         try:
568             player_id = (player_url, self._signature_cache_id(s))
569             if player_id not in self._player_cache:
570                 func = self._extract_signature_function(
571                     video_id, player_url, s
572                 )
573                 self._player_cache[player_id] = func
574             func = self._player_cache[player_id]
575             if self._downloader.params.get('youtube_print_sig_code'):
576                 self._print_sig_code(func, s)
577             return func(s)
578         except Exception as e:
579             tb = traceback.format_exc()
580             raise ExtractorError(
581                 'Signature extraction failed: ' + tb, cause=e)
582
583     def _get_available_subtitles(self, video_id, webpage):
584         try:
585             sub_list = self._download_webpage(
586                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
587                 video_id, note=False)
588         except ExtractorError as err:
589             self._downloader.report_warning('unable to download video subtitles: %s' % compat_str(err))
590             return {}
591         lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list)
592
593         sub_lang_list = {}
594         for l in lang_list:
595             lang = l[1]
596             if lang in sub_lang_list:
597                 continue
598             params = compat_urllib_parse.urlencode({
599                 'lang': lang,
600                 'v': video_id,
601                 'fmt': self._downloader.params.get('subtitlesformat', 'srt'),
602                 'name': unescapeHTML(l[0]).encode('utf-8'),
603             })
604             url = 'https://www.youtube.com/api/timedtext?' + params
605             sub_lang_list[lang] = url
606         if not sub_lang_list:
607             self._downloader.report_warning('video doesn\'t have subtitles')
608             return {}
609         return sub_lang_list
610
611     def _get_available_automatic_caption(self, video_id, webpage):
612         """We need the webpage for getting the captions url, pass it as an
613            argument to speed up the process."""
614         sub_format = self._downloader.params.get('subtitlesformat', 'srt')
615         self.to_screen('%s: Looking for automatic captions' % video_id)
616         mobj = re.search(r';ytplayer.config = ({.*?});', webpage)
617         err_msg = 'Couldn\'t find automatic captions for %s' % video_id
618         if mobj is None:
619             self._downloader.report_warning(err_msg)
620             return {}
621         player_config = json.loads(mobj.group(1))
622         try:
623             args = player_config['args']
624             caption_url = args['ttsurl']
625             timestamp = args['timestamp']
626             # We get the available subtitles
627             list_params = compat_urllib_parse.urlencode({
628                 'type': 'list',
629                 'tlangs': 1,
630                 'asrs': 1,
631             })
632             list_url = caption_url + '&' + list_params
633             caption_list = self._download_xml(list_url, video_id)
634             original_lang_node = caption_list.find('track')
635             if original_lang_node is None or original_lang_node.attrib.get('kind') != 'asr':
636                 self._downloader.report_warning('Video doesn\'t have automatic captions')
637                 return {}
638             original_lang = original_lang_node.attrib['lang_code']
639
640             sub_lang_list = {}
641             for lang_node in caption_list.findall('target'):
642                 sub_lang = lang_node.attrib['lang_code']
643                 params = compat_urllib_parse.urlencode({
644                     'lang': original_lang,
645                     'tlang': sub_lang,
646                     'fmt': sub_format,
647                     'ts': timestamp,
648                     'kind': 'asr',
649                 })
650                 sub_lang_list[sub_lang] = caption_url + '&' + params
651             return sub_lang_list
652         # An extractor error can be raise by the download process if there are
653         # no automatic captions but there are subtitles
654         except (KeyError, ExtractorError):
655             self._downloader.report_warning(err_msg)
656             return {}
657
658     @classmethod
659     def extract_id(cls, url):
660         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
661         if mobj is None:
662             raise ExtractorError('Invalid URL: %s' % url)
663         video_id = mobj.group(2)
664         return video_id
665
666     def _extract_from_m3u8(self, manifest_url, video_id):
667         url_map = {}
668
669         def _get_urls(_manifest):
670             lines = _manifest.split('\n')
671             urls = filter(lambda l: l and not l.startswith('#'),
672                           lines)
673             return urls
674         manifest = self._download_webpage(manifest_url, video_id, 'Downloading formats manifest')
675         formats_urls = _get_urls(manifest)
676         for format_url in formats_urls:
677             itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
678             url_map[itag] = format_url
679         return url_map
680
681     def _extract_annotations(self, video_id):
682         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
683         return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
684
685     def _parse_dash_manifest(self, video_id, dash_manifest_url):
686         def decrypt_sig(mobj):
687             s = mobj.group(1)
688             dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
689             return '/signature/%s' % dec_s
690         dash_manifest_url = re.sub(r'/s/([\w\.]+)', decrypt_sig, dash_manifest_url)
691         dash_doc = self._download_xml(
692             dash_manifest_url, video_id,
693             note='Downloading DASH manifest',
694             errnote='Could not download DASH manifest')
695
696         formats = []
697         for r in dash_doc.findall('.//{urn:mpeg:DASH:schema:MPD:2011}Representation'):
698             url_el = r.find('{urn:mpeg:DASH:schema:MPD:2011}BaseURL')
699             if url_el is None:
700                 continue
701             format_id = r.attrib['id']
702             video_url = url_el.text
703             filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength'))
704             f = {
705                 'format_id': format_id,
706                 'url': video_url,
707                 'width': int_or_none(r.attrib.get('width')),
708                 'tbr': int_or_none(r.attrib.get('bandwidth'), 1000),
709                 'asr': int_or_none(r.attrib.get('audioSamplingRate')),
710                 'filesize': filesize,
711                 'fps': int_or_none(r.attrib.get('frameRate')),
712             }
713             try:
714                 existing_format = next(
715                     fo for fo in formats
716                     if fo['format_id'] == format_id)
717             except StopIteration:
718                 f.update(self._formats.get(format_id, {}))
719                 formats.append(f)
720             else:
721                 existing_format.update(f)
722         return formats
723
724     def _real_extract(self, url):
725         proto = (
726             'http' if self._downloader.params.get('prefer_insecure', False)
727             else 'https')
728
729         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
730         mobj = re.search(self._NEXT_URL_RE, url)
731         if mobj:
732             url = proto + '://www.youtube.com/' + compat_urllib_parse.unquote(mobj.group(1)).lstrip('/')
733         video_id = self.extract_id(url)
734
735         # Get video webpage
736         url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
737         video_webpage = self._download_webpage(url, video_id)
738
739         # Attempt to extract SWF player URL
740         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
741         if mobj is not None:
742             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
743         else:
744             player_url = None
745
746         # Get video info
747         if re.search(r'player-age-gate-content">', video_webpage) is not None:
748             age_gate = True
749             # We simulate the access to the video from www.youtube.com/v/{video_id}
750             # this can be viewed without login into Youtube
751             data = compat_urllib_parse.urlencode({
752                 'video_id': video_id,
753                 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
754                 'sts': self._search_regex(
755                     r'"sts"\s*:\s*(\d+)', video_webpage, 'sts', default=''),
756             })
757             video_info_url = proto + '://www.youtube.com/get_video_info?' + data
758             video_info_webpage = self._download_webpage(
759                 video_info_url, video_id,
760                 note='Refetching age-gated info webpage',
761                 errnote='unable to download video info webpage')
762             video_info = compat_parse_qs(video_info_webpage)
763         else:
764             age_gate = False
765             try:
766                 # Try looking directly into the video webpage
767                 mobj = re.search(r';ytplayer\.config\s*=\s*({.*?});', video_webpage)
768                 if not mobj:
769                     raise ValueError('Could not find ytplayer.config')  # caught below
770                 json_code = uppercase_escape(mobj.group(1))
771                 ytplayer_config = json.loads(json_code)
772                 args = ytplayer_config['args']
773                 # Convert to the same format returned by compat_parse_qs
774                 video_info = dict((k, [v]) for k, v in args.items())
775                 if 'url_encoded_fmt_stream_map' not in args:
776                     raise ValueError('No stream_map present')  # caught below
777             except ValueError:
778                 # We fallback to the get_video_info pages (used by the embed page)
779                 self.report_video_info_webpage_download(video_id)
780                 for el_type in ['&el=embedded', '&el=detailpage', '&el=vevo', '']:
781                     video_info_url = (
782                         '%s://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
783                         % (proto, video_id, el_type))
784                     video_info_webpage = self._download_webpage(
785                         video_info_url,
786                         video_id, note=False,
787                         errnote='unable to download video info webpage')
788                     video_info = compat_parse_qs(video_info_webpage)
789                     if 'token' in video_info:
790                         break
791         if 'token' not in video_info:
792             if 'reason' in video_info:
793                 raise ExtractorError(
794                     'YouTube said: %s' % video_info['reason'][0],
795                     expected=True, video_id=video_id)
796             else:
797                 raise ExtractorError(
798                     '"token" parameter not in video info for unknown reason',
799                     video_id=video_id)
800
801         if 'view_count' in video_info:
802             view_count = int(video_info['view_count'][0])
803         else:
804             view_count = None
805
806         # Check for "rental" videos
807         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
808             raise ExtractorError('"rental" videos not supported')
809
810         # Start extracting information
811         self.report_information_extraction(video_id)
812
813         # uploader
814         if 'author' not in video_info:
815             raise ExtractorError('Unable to extract uploader name')
816         video_uploader = compat_urllib_parse.unquote_plus(video_info['author'][0])
817
818         # uploader_id
819         video_uploader_id = None
820         mobj = re.search(r'<link itemprop="url" href="http://www.youtube.com/(?:user|channel)/([^"]+)">', video_webpage)
821         if mobj is not None:
822             video_uploader_id = mobj.group(1)
823         else:
824             self._downloader.report_warning('unable to extract uploader nickname')
825
826         # title
827         if 'title' in video_info:
828             video_title = video_info['title'][0]
829         else:
830             self._downloader.report_warning('Unable to extract video title')
831             video_title = '_'
832
833         # thumbnail image
834         # We try first to get a high quality image:
835         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
836                             video_webpage, re.DOTALL)
837         if m_thumb is not None:
838             video_thumbnail = m_thumb.group(1)
839         elif 'thumbnail_url' not in video_info:
840             self._downloader.report_warning('unable to extract video thumbnail')
841             video_thumbnail = None
842         else:   # don't panic if we can't find it
843             video_thumbnail = compat_urllib_parse.unquote_plus(video_info['thumbnail_url'][0])
844
845         # upload date
846         upload_date = None
847         mobj = re.search(r'(?s)id="eow-date.*?>(.*?)</span>', video_webpage)
848         if mobj is None:
849             mobj = re.search(
850                 r'(?s)id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live) on (.*?)</strong>',
851                 video_webpage)
852         if mobj is not None:
853             upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
854             upload_date = unified_strdate(upload_date)
855
856         m_cat_container = self._search_regex(
857             r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
858             video_webpage, 'categories', fatal=False)
859         if m_cat_container:
860             category = self._html_search_regex(
861                 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
862                 default=None)
863             video_categories = None if category is None else [category]
864         else:
865             video_categories = None
866
867         # description
868         video_description = get_element_by_id("eow-description", video_webpage)
869         if video_description:
870             video_description = re.sub(r'''(?x)
871                 <a\s+
872                     (?:[a-zA-Z-]+="[^"]+"\s+)*?
873                     title="([^"]+)"\s+
874                     (?:[a-zA-Z-]+="[^"]+"\s+)*?
875                     class="yt-uix-redirect-link"\s*>
876                 [^<]+
877                 </a>
878             ''', r'\1', video_description)
879             video_description = clean_html(video_description)
880         else:
881             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
882             if fd_mobj:
883                 video_description = unescapeHTML(fd_mobj.group(1))
884             else:
885                 video_description = ''
886
887         def _extract_count(count_name):
888             count = self._search_regex(
889                 r'id="watch-%s"[^>]*>.*?([\d,]+)\s*</span>' % re.escape(count_name),
890                 video_webpage, count_name, default=None)
891             if count is not None:
892                 return int(count.replace(',', ''))
893             return None
894         like_count = _extract_count('like')
895         dislike_count = _extract_count('dislike')
896
897         # subtitles
898         video_subtitles = self.extract_subtitles(video_id, video_webpage)
899
900         if self._downloader.params.get('listsubtitles', False):
901             self._list_available_subtitles(video_id, video_webpage)
902             return
903
904         if 'length_seconds' not in video_info:
905             self._downloader.report_warning('unable to extract video duration')
906             video_duration = None
907         else:
908             video_duration = int(compat_urllib_parse.unquote_plus(video_info['length_seconds'][0]))
909
910         # annotations
911         video_annotations = None
912         if self._downloader.params.get('writeannotations', False):
913             video_annotations = self._extract_annotations(video_id)
914
915         def _map_to_format_list(urlmap):
916             formats = []
917             for itag, video_real_url in urlmap.items():
918                 dct = {
919                     'format_id': itag,
920                     'url': video_real_url,
921                     'player_url': player_url,
922                 }
923                 if itag in self._formats:
924                     dct.update(self._formats[itag])
925                 formats.append(dct)
926             return formats
927
928         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
929             self.report_rtmp_download()
930             formats = [{
931                 'format_id': '_rtmp',
932                 'protocol': 'rtmp',
933                 'url': video_info['conn'][0],
934                 'player_url': player_url,
935             }]
936         elif len(video_info.get('url_encoded_fmt_stream_map', [])) >= 1 or len(video_info.get('adaptive_fmts', [])) >= 1:
937             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts', [''])[0]
938             if 'rtmpe%3Dyes' in encoded_url_map:
939                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
940             url_map = {}
941             for url_data_str in encoded_url_map.split(','):
942                 url_data = compat_parse_qs(url_data_str)
943                 if 'itag' not in url_data or 'url' not in url_data:
944                     continue
945                 format_id = url_data['itag'][0]
946                 url = url_data['url'][0]
947
948                 if 'sig' in url_data:
949                     url += '&signature=' + url_data['sig'][0]
950                 elif 's' in url_data:
951                     encrypted_sig = url_data['s'][0]
952
953                     if not age_gate:
954                         jsplayer_url_json = self._search_regex(
955                             r'"assets":.+?"js":\s*("[^"]+")',
956                             video_webpage, 'JS player URL')
957                         player_url = json.loads(jsplayer_url_json)
958                     if player_url is None:
959                         player_url_json = self._search_regex(
960                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
961                             video_webpage, 'age gate player URL')
962                         player_url = json.loads(player_url_json)
963
964                     if self._downloader.params.get('verbose'):
965                         if player_url is None:
966                             player_version = 'unknown'
967                             player_desc = 'unknown'
968                         else:
969                             if player_url.endswith('swf'):
970                                 player_version = self._search_regex(
971                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
972                                     'flash player', fatal=False)
973                                 player_desc = 'flash player %s' % player_version
974                             else:
975                                 player_version = self._search_regex(
976                                     r'html5player-([^/]+?)(?:/html5player)?\.js',
977                                     player_url,
978                                     'html5 player', fatal=False)
979                                 player_desc = 'html5 player %s' % player_version
980
981                         parts_sizes = self._signature_cache_id(encrypted_sig)
982                         self.to_screen('{%s} signature length %s, %s' %
983                                        (format_id, parts_sizes, player_desc))
984
985                     signature = self._decrypt_signature(
986                         encrypted_sig, video_id, player_url, age_gate)
987                     url += '&signature=' + signature
988                 if 'ratebypass' not in url:
989                     url += '&ratebypass=yes'
990                 url_map[format_id] = url
991             formats = _map_to_format_list(url_map)
992         elif video_info.get('hlsvp'):
993             manifest_url = video_info['hlsvp'][0]
994             url_map = self._extract_from_m3u8(manifest_url, video_id)
995             formats = _map_to_format_list(url_map)
996         else:
997             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
998
999         # Look for the DASH manifest
1000         if self._downloader.params.get('youtube_include_dash_manifest', True):
1001             dash_mpd = video_info.get('dashmpd')
1002             if not dash_mpd:
1003                 self.report_warning('%s: DASH manifest missing' % video_id)
1004             else:
1005                 dash_manifest_url = dash_mpd[0]
1006                 try:
1007                     dash_formats = self._parse_dash_manifest(
1008                         video_id, dash_manifest_url)
1009                 except (ExtractorError, KeyError) as e:
1010                     self.report_warning(
1011                         'Skipping DASH manifest: %r' % e, video_id)
1012                 else:
1013                     formats.extend(dash_formats)
1014
1015         self._sort_formats(formats)
1016
1017         return {
1018             'id': video_id,
1019             'uploader': video_uploader,
1020             'uploader_id': video_uploader_id,
1021             'upload_date': upload_date,
1022             'title': video_title,
1023             'thumbnail': video_thumbnail,
1024             'description': video_description,
1025             'categories': video_categories,
1026             'subtitles': video_subtitles,
1027             'duration': video_duration,
1028             'age_limit': 18 if age_gate else 0,
1029             'annotations': video_annotations,
1030             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
1031             'view_count': view_count,
1032             'like_count': like_count,
1033             'dislike_count': dislike_count,
1034             'formats': formats,
1035         }
1036
1037
1038 class YoutubePlaylistIE(YoutubeBaseInfoExtractor):
1039     IE_DESC = 'YouTube.com playlists'
1040     _VALID_URL = r"""(?x)(?:
1041                         (?:https?://)?
1042                         (?:\w+\.)?
1043                         youtube\.com/
1044                         (?:
1045                            (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
1046                            \? (?:.*?&)*? (?:p|a|list)=
1047                         |  p/
1048                         )
1049                         (
1050                             (?:PL|LL|EC|UU|FL|RD)?[0-9A-Za-z-_]{10,}
1051                             # Top tracks, they can also include dots
1052                             |(?:MC)[\w\.]*
1053                         )
1054                         .*
1055                      |
1056                         ((?:PL|LL|EC|UU|FL|RD)[0-9A-Za-z-_]{10,})
1057                      )"""
1058     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
1059     _MORE_PAGES_INDICATOR = r'data-link-type="next"'
1060     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)'
1061     IE_NAME = 'youtube:playlist'
1062     _TESTS = [{
1063         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1064         'info_dict': {
1065             'title': 'ytdl test PL',
1066             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1067         },
1068         'playlist_count': 3,
1069     }, {
1070         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1071         'info_dict': {
1072             'title': 'YDL_Empty_List',
1073         },
1074         'playlist_count': 0,
1075     }, {
1076         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1077         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1078         'info_dict': {
1079             'title': '29C3: Not my department',
1080         },
1081         'playlist_count': 95,
1082     }, {
1083         'note': 'issue #673',
1084         'url': 'PLBB231211A4F62143',
1085         'info_dict': {
1086             'title': '[OLD]Team Fortress 2 (Class-based LP)',
1087         },
1088         'playlist_mincount': 26,
1089     }, {
1090         'note': 'Large playlist',
1091         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1092         'info_dict': {
1093             'title': 'Uploads from Cauchemar',
1094         },
1095         'playlist_mincount': 799,
1096     }, {
1097         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1098         'info_dict': {
1099             'title': 'YDL_safe_search',
1100         },
1101         'playlist_count': 2,
1102     }, {
1103         'note': 'embedded',
1104         'url': 'http://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1105         'playlist_count': 4,
1106         'info_dict': {
1107             'title': 'JODA15',
1108         }
1109     }, {
1110         'note': 'Embedded SWF player',
1111         'url': 'http://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1112         'playlist_count': 4,
1113         'info_dict': {
1114             'title': 'JODA7',
1115         }
1116     }]
1117
1118     def _real_initialize(self):
1119         self._login()
1120
1121     def _ids_to_results(self, ids):
1122         return [
1123             self.url_result(vid_id, 'Youtube', video_id=vid_id)
1124             for vid_id in ids]
1125
1126     def _extract_mix(self, playlist_id):
1127         # The mixes are generated from a a single video
1128         # the id of the playlist is just 'RD' + video_id
1129         url = 'https://youtube.com/watch?v=%s&list=%s' % (playlist_id[-11:], playlist_id)
1130         webpage = self._download_webpage(
1131             url, playlist_id, 'Downloading Youtube mix')
1132         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
1133         title_span = (
1134             search_title('playlist-title') or
1135             search_title('title long-title') or
1136             search_title('title'))
1137         title = clean_html(title_span)
1138         ids = orderedSet(re.findall(
1139             r'''(?xs)data-video-username=".*?".*?
1140                        href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
1141             webpage))
1142         url_results = self._ids_to_results(ids)
1143
1144         return self.playlist_result(url_results, playlist_id, title)
1145
1146     def _real_extract(self, url):
1147         # Extract playlist id
1148         mobj = re.match(self._VALID_URL, url)
1149         if mobj is None:
1150             raise ExtractorError('Invalid URL: %s' % url)
1151         playlist_id = mobj.group(1) or mobj.group(2)
1152
1153         # Check if it's a video-specific URL
1154         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
1155         if 'v' in query_dict:
1156             video_id = query_dict['v'][0]
1157             if self._downloader.params.get('noplaylist'):
1158                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1159                 return self.url_result(video_id, 'Youtube', video_id=video_id)
1160             else:
1161                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
1162
1163         if playlist_id.startswith('RD'):
1164             # Mixes require a custom extraction process
1165             return self._extract_mix(playlist_id)
1166         if playlist_id.startswith('TL'):
1167             raise ExtractorError('For downloading YouTube.com top lists, use '
1168                                  'the "yttoplist" keyword, for example "youtube-dl \'yttoplist:music:Top Tracks\'"', expected=True)
1169
1170         url = self._TEMPLATE_URL % playlist_id
1171         page = self._download_webpage(url, playlist_id)
1172         more_widget_html = content_html = page
1173
1174         # Check if the playlist exists or is private
1175         if re.search(r'<div class="yt-alert-message">[^<]*?(The|This) playlist (does not exist|is private)[^<]*?</div>', page) is not None:
1176             raise ExtractorError(
1177                 'The playlist doesn\'t exist or is private, use --username or '
1178                 '--netrc to access it.',
1179                 expected=True)
1180
1181         # Extract the video ids from the playlist pages
1182         ids = []
1183
1184         for page_num in itertools.count(1):
1185             matches = re.finditer(self._VIDEO_RE, content_html)
1186             # We remove the duplicates and the link with index 0
1187             # (it's not the first video of the playlist)
1188             new_ids = orderedSet(m.group('id') for m in matches if m.group('index') != '0')
1189             ids.extend(new_ids)
1190
1191             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
1192             if not mobj:
1193                 break
1194
1195             more = self._download_json(
1196                 'https://youtube.com/%s' % mobj.group('more'), playlist_id,
1197                 'Downloading page #%s' % page_num,
1198                 transform_source=uppercase_escape)
1199             content_html = more['content_html']
1200             more_widget_html = more['load_more_widget_html']
1201
1202         playlist_title = self._html_search_regex(
1203             r'(?s)<h1 class="pl-header-title[^"]*">\s*(.*?)\s*</h1>',
1204             page, 'title')
1205
1206         url_results = self._ids_to_results(ids)
1207         return self.playlist_result(url_results, playlist_id, playlist_title)
1208
1209
1210 class YoutubeTopListIE(YoutubePlaylistIE):
1211     IE_NAME = 'youtube:toplist'
1212     IE_DESC = ('YouTube.com top lists, "yttoplist:{channel}:{list title}"'
1213                ' (Example: "yttoplist:music:Top Tracks")')
1214     _VALID_URL = r'yttoplist:(?P<chann>.*?):(?P<title>.*?)$'
1215     _TESTS = [{
1216         'url': 'yttoplist:music:Trending',
1217         'playlist_mincount': 5,
1218         'skip': 'Only works for logged-in users',
1219     }]
1220
1221     def _real_extract(self, url):
1222         mobj = re.match(self._VALID_URL, url)
1223         channel = mobj.group('chann')
1224         title = mobj.group('title')
1225         query = compat_urllib_parse.urlencode({'title': title})
1226         channel_page = self._download_webpage(
1227             'https://www.youtube.com/%s' % channel, title)
1228         link = self._html_search_regex(
1229             r'''(?x)
1230                 <a\s+href="([^"]+)".*?>\s*
1231                 <span\s+class="branded-page-module-title-text">\s*
1232                 <span[^>]*>.*?%s.*?</span>''' % re.escape(query),
1233             channel_page, 'list')
1234         url = compat_urlparse.urljoin('https://www.youtube.com/', link)
1235
1236         video_re = r'data-index="\d+".*?data-video-id="([0-9A-Za-z_-]{11})"'
1237         ids = []
1238         # sometimes the webpage doesn't contain the videos
1239         # retry until we get them
1240         for i in itertools.count(0):
1241             msg = 'Downloading Youtube mix'
1242             if i > 0:
1243                 msg += ', retry #%d' % i
1244
1245             webpage = self._download_webpage(url, title, msg)
1246             ids = orderedSet(re.findall(video_re, webpage))
1247             if ids:
1248                 break
1249         url_results = self._ids_to_results(ids)
1250         return self.playlist_result(url_results, playlist_title=title)
1251
1252
1253 class YoutubeChannelIE(InfoExtractor):
1254     IE_DESC = 'YouTube.com channels'
1255     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/(?P<id>[0-9A-Za-z_-]+)'
1256     _MORE_PAGES_INDICATOR = 'yt-uix-load-more'
1257     _MORE_PAGES_URL = 'https://www.youtube.com/c4_browse_ajax?action_load_more_videos=1&flow=list&paging=%s&view=0&sort=da&channel_id=%s'
1258     IE_NAME = 'youtube:channel'
1259     _TESTS = [{
1260         'note': 'paginated channel',
1261         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
1262         'playlist_mincount': 91,
1263     }]
1264
1265     def extract_videos_from_page(self, page):
1266         ids_in_page = []
1267         for mobj in re.finditer(r'href="/watch\?v=([0-9A-Za-z_-]+)&?', page):
1268             if mobj.group(1) not in ids_in_page:
1269                 ids_in_page.append(mobj.group(1))
1270         return ids_in_page
1271
1272     def _real_extract(self, url):
1273         channel_id = self._match_id(url)
1274
1275         video_ids = []
1276         url = 'https://www.youtube.com/channel/%s/videos' % channel_id
1277         channel_page = self._download_webpage(url, channel_id)
1278         autogenerated = re.search(r'''(?x)
1279                 class="[^"]*?(?:
1280                     channel-header-autogenerated-label|
1281                     yt-channel-title-autogenerated
1282                 )[^"]*"''', channel_page) is not None
1283
1284         if autogenerated:
1285             # The videos are contained in a single page
1286             # the ajax pages can't be used, they are empty
1287             video_ids = self.extract_videos_from_page(channel_page)
1288             entries = [
1289                 self.url_result(video_id, 'Youtube', video_id=video_id)
1290                 for video_id in video_ids]
1291             return self.playlist_result(entries, channel_id)
1292
1293         def _entries():
1294             for pagenum in itertools.count(1):
1295                 url = self._MORE_PAGES_URL % (pagenum, channel_id)
1296                 page = self._download_json(
1297                     url, channel_id, note='Downloading page #%s' % pagenum,
1298                     transform_source=uppercase_escape)
1299
1300                 ids_in_page = self.extract_videos_from_page(page['content_html'])
1301                 for video_id in ids_in_page:
1302                     yield self.url_result(
1303                         video_id, 'Youtube', video_id=video_id)
1304
1305                 if self._MORE_PAGES_INDICATOR not in page['load_more_widget_html']:
1306                     break
1307
1308         return self.playlist_result(_entries(), channel_id)
1309
1310
1311 class YoutubeUserIE(InfoExtractor):
1312     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
1313     _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_-]+)'
1314     _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/users/%s'
1315     _GDATA_PAGE_SIZE = 50
1316     _GDATA_URL = 'https://gdata.youtube.com/feeds/api/users/%s/uploads?max-results=%d&start-index=%d&alt=json'
1317     IE_NAME = 'youtube:user'
1318
1319     _TESTS = [{
1320         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
1321         'playlist_mincount': 320,
1322         'info_dict': {
1323             'title': 'TheLinuxFoundation',
1324         }
1325     }, {
1326         'url': 'ytuser:phihag',
1327         'only_matching': True,
1328     }]
1329
1330     @classmethod
1331     def suitable(cls, url):
1332         # Don't return True if the url can be extracted with other youtube
1333         # extractor, the regex would is too permissive and it would match.
1334         other_ies = iter(klass for (name, klass) in globals().items() if name.endswith('IE') and klass is not cls)
1335         if any(ie.suitable(url) for ie in other_ies):
1336             return False
1337         else:
1338             return super(YoutubeUserIE, cls).suitable(url)
1339
1340     def _real_extract(self, url):
1341         username = self._match_id(url)
1342
1343         # Download video ids using YouTube Data API. Result size per
1344         # query is limited (currently to 50 videos) so we need to query
1345         # page by page until there are no video ids - it means we got
1346         # all of them.
1347
1348         def download_page(pagenum):
1349             start_index = pagenum * self._GDATA_PAGE_SIZE + 1
1350
1351             gdata_url = self._GDATA_URL % (username, self._GDATA_PAGE_SIZE, start_index)
1352             page = self._download_webpage(
1353                 gdata_url, username,
1354                 'Downloading video ids from %d to %d' % (
1355                     start_index, start_index + self._GDATA_PAGE_SIZE))
1356
1357             try:
1358                 response = json.loads(page)
1359             except ValueError as err:
1360                 raise ExtractorError('Invalid JSON in API response: ' + compat_str(err))
1361             if 'entry' not in response['feed']:
1362                 return
1363
1364             # Extract video identifiers
1365             entries = response['feed']['entry']
1366             for entry in entries:
1367                 title = entry['title']['$t']
1368                 video_id = entry['id']['$t'].split('/')[-1]
1369                 yield {
1370                     '_type': 'url',
1371                     'url': video_id,
1372                     'ie_key': 'Youtube',
1373                     'id': video_id,
1374                     'title': title,
1375                 }
1376         url_results = OnDemandPagedList(download_page, self._GDATA_PAGE_SIZE)
1377
1378         return self.playlist_result(url_results, playlist_title=username)
1379
1380
1381 class YoutubeSearchIE(SearchInfoExtractor):
1382     IE_DESC = 'YouTube.com searches'
1383     _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc'
1384     _MAX_RESULTS = 1000
1385     IE_NAME = 'youtube:search'
1386     _SEARCH_KEY = 'ytsearch'
1387
1388     def _get_n_results(self, query, n):
1389         """Get a specified number of results for a query"""
1390
1391         video_ids = []
1392         pagenum = 0
1393         limit = n
1394         PAGE_SIZE = 50
1395
1396         while (PAGE_SIZE * pagenum) < limit:
1397             result_url = self._API_URL % (
1398                 compat_urllib_parse.quote_plus(query.encode('utf-8')),
1399                 (PAGE_SIZE * pagenum) + 1)
1400             data_json = self._download_webpage(
1401                 result_url, video_id='query "%s"' % query,
1402                 note='Downloading page %s' % (pagenum + 1),
1403                 errnote='Unable to download API page')
1404             data = json.loads(data_json)
1405             api_response = data['data']
1406
1407             if 'items' not in api_response:
1408                 raise ExtractorError(
1409                     '[youtube] No video results', expected=True)
1410
1411             new_ids = list(video['id'] for video in api_response['items'])
1412             video_ids += new_ids
1413
1414             limit = min(n, api_response['totalItems'])
1415             pagenum += 1
1416
1417         if len(video_ids) > n:
1418             video_ids = video_ids[:n]
1419         videos = [self.url_result(video_id, 'Youtube', video_id=video_id)
1420                   for video_id in video_ids]
1421         return self.playlist_result(videos, query)
1422
1423
1424 class YoutubeSearchDateIE(YoutubeSearchIE):
1425     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
1426     _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc&orderby=published'
1427     _SEARCH_KEY = 'ytsearchdate'
1428     IE_DESC = 'YouTube.com searches, newest videos first'
1429
1430
1431 class YoutubeSearchURLIE(InfoExtractor):
1432     IE_DESC = 'YouTube.com search URLs'
1433     IE_NAME = 'youtube:search_url'
1434     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?search_query=(?P<query>[^&]+)(?:[&]|$)'
1435     _TESTS = [{
1436         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
1437         'playlist_mincount': 5,
1438         'info_dict': {
1439             'title': 'youtube-dl test video',
1440         }
1441     }]
1442
1443     def _real_extract(self, url):
1444         mobj = re.match(self._VALID_URL, url)
1445         query = compat_urllib_parse.unquote_plus(mobj.group('query'))
1446
1447         webpage = self._download_webpage(url, query)
1448         result_code = self._search_regex(
1449             r'(?s)<ol class="item-section"(.*?)</ol>', webpage, 'result HTML')
1450
1451         part_codes = re.findall(
1452             r'(?s)<h3 class="yt-lockup-title">(.*?)</h3>', result_code)
1453         entries = []
1454         for part_code in part_codes:
1455             part_title = self._html_search_regex(
1456                 [r'(?s)title="([^"]+)"', r'>([^<]+)</a>'], part_code, 'item title', fatal=False)
1457             part_url_snippet = self._html_search_regex(
1458                 r'(?s)href="([^"]+)"', part_code, 'item URL')
1459             part_url = compat_urlparse.urljoin(
1460                 'https://www.youtube.com/', part_url_snippet)
1461             entries.append({
1462                 '_type': 'url',
1463                 'url': part_url,
1464                 'title': part_title,
1465             })
1466
1467         return {
1468             '_type': 'playlist',
1469             'entries': entries,
1470             'title': query,
1471         }
1472
1473
1474 class YoutubeShowIE(InfoExtractor):
1475     IE_DESC = 'YouTube.com (multi-season) shows'
1476     _VALID_URL = r'https?://www\.youtube\.com/show/(?P<id>[^?#]*)'
1477     IE_NAME = 'youtube:show'
1478     _TESTS = [{
1479         'url': 'http://www.youtube.com/show/airdisasters',
1480         'playlist_mincount': 3,
1481         'info_dict': {
1482             'id': 'airdisasters',
1483             'title': 'Air Disasters',
1484         }
1485     }]
1486
1487     def _real_extract(self, url):
1488         mobj = re.match(self._VALID_URL, url)
1489         playlist_id = mobj.group('id')
1490         webpage = self._download_webpage(
1491             url, playlist_id, 'Downloading show webpage')
1492         # There's one playlist for each season of the show
1493         m_seasons = list(re.finditer(r'href="(/playlist\?list=.*?)"', webpage))
1494         self.to_screen('%s: Found %s seasons' % (playlist_id, len(m_seasons)))
1495         entries = [
1496             self.url_result(
1497                 'https://www.youtube.com' + season.group(1), 'YoutubePlaylist')
1498             for season in m_seasons
1499         ]
1500         title = self._og_search_title(webpage, fatal=False)
1501
1502         return {
1503             '_type': 'playlist',
1504             'id': playlist_id,
1505             'title': title,
1506             'entries': entries,
1507         }
1508
1509
1510 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
1511     """
1512     Base class for extractors that fetch info from
1513     http://www.youtube.com/feed_ajax
1514     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
1515     """
1516     _LOGIN_REQUIRED = True
1517     # use action_load_personal_feed instead of action_load_system_feed
1518     _PERSONAL_FEED = False
1519
1520     @property
1521     def _FEED_TEMPLATE(self):
1522         action = 'action_load_system_feed'
1523         if self._PERSONAL_FEED:
1524             action = 'action_load_personal_feed'
1525         return 'https://www.youtube.com/feed_ajax?%s=1&feed_name=%s&paging=%%s' % (action, self._FEED_NAME)
1526
1527     @property
1528     def IE_NAME(self):
1529         return 'youtube:%s' % self._FEED_NAME
1530
1531     def _real_initialize(self):
1532         self._login()
1533
1534     def _real_extract(self, url):
1535         feed_entries = []
1536         paging = 0
1537         for i in itertools.count(1):
1538             info = self._download_json(self._FEED_TEMPLATE % paging,
1539                                        '%s feed' % self._FEED_NAME,
1540                                        'Downloading page %s' % i)
1541             feed_html = info.get('feed_html') or info.get('content_html')
1542             load_more_widget_html = info.get('load_more_widget_html') or feed_html
1543             m_ids = re.finditer(r'"/watch\?v=(.*?)["&]', feed_html)
1544             ids = orderedSet(m.group(1) for m in m_ids)
1545             feed_entries.extend(
1546                 self.url_result(video_id, 'Youtube', video_id=video_id)
1547                 for video_id in ids)
1548             mobj = re.search(
1549                 r'data-uix-load-more-href="/?[^"]+paging=(?P<paging>\d+)',
1550                 load_more_widget_html)
1551             if mobj is None:
1552                 break
1553             paging = mobj.group('paging')
1554         return self.playlist_result(feed_entries, playlist_title=self._PLAYLIST_TITLE)
1555
1556
1557 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
1558     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
1559     _VALID_URL = r'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?'
1560     _FEED_NAME = 'recommended'
1561     _PLAYLIST_TITLE = 'Youtube Recommended videos'
1562
1563
1564 class YoutubeWatchLaterIE(YoutubeFeedsInfoExtractor):
1565     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
1566     _VALID_URL = r'https?://www\.youtube\.com/feed/watch_later|:ytwatchlater'
1567     _FEED_NAME = 'watch_later'
1568     _PLAYLIST_TITLE = 'Youtube Watch Later'
1569     _PERSONAL_FEED = True
1570
1571
1572 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
1573     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
1574     _VALID_URL = 'https?://www\.youtube\.com/feed/history|:ythistory'
1575     _FEED_NAME = 'history'
1576     _PERSONAL_FEED = True
1577     _PLAYLIST_TITLE = 'Youtube Watch History'
1578
1579
1580 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
1581     IE_NAME = 'youtube:favorites'
1582     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
1583     _VALID_URL = r'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
1584     _LOGIN_REQUIRED = True
1585
1586     def _real_extract(self, url):
1587         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
1588         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
1589         return self.url_result(playlist_id, 'YoutubePlaylist')
1590
1591
1592 class YoutubeSubscriptionsIE(YoutubePlaylistIE):
1593     IE_NAME = 'youtube:subscriptions'
1594     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
1595     _VALID_URL = r'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
1596     _TESTS = []
1597
1598     def _real_extract(self, url):
1599         title = 'Youtube Subscriptions'
1600         page = self._download_webpage('https://www.youtube.com/feed/subscriptions', title)
1601
1602         # The extraction process is the same as for playlists, but the regex
1603         # for the video ids doesn't contain an index
1604         ids = []
1605         more_widget_html = content_html = page
1606
1607         for page_num in itertools.count(1):
1608             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
1609             new_ids = orderedSet(matches)
1610             ids.extend(new_ids)
1611
1612             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
1613             if not mobj:
1614                 break
1615
1616             more = self._download_json(
1617                 'https://youtube.com/%s' % mobj.group('more'), title,
1618                 'Downloading page #%s' % page_num,
1619                 transform_source=uppercase_escape)
1620             content_html = more['content_html']
1621             more_widget_html = more['load_more_widget_html']
1622
1623         return {
1624             '_type': 'playlist',
1625             'title': title,
1626             'entries': self._ids_to_results(ids),
1627         }
1628
1629
1630 class YoutubeTruncatedURLIE(InfoExtractor):
1631     IE_NAME = 'youtube:truncated_url'
1632     IE_DESC = False  # Do not list
1633     _VALID_URL = r'''(?x)
1634         (?:https?://)?[^/]+/watch\?(?:
1635             feature=[a-z_]+|
1636             annotation_id=annotation_[^&]+
1637         )?$|
1638         (?:https?://)?(?:www\.)?youtube\.com/attribution_link\?a=[^&]+$
1639     '''
1640
1641     _TESTS = [{
1642         'url': 'http://www.youtube.com/watch?annotation_id=annotation_3951667041',
1643         'only_matching': True,
1644     }, {
1645         'url': 'http://www.youtube.com/watch?',
1646         'only_matching': True,
1647     }]
1648
1649     def _real_extract(self, url):
1650         raise ExtractorError(
1651             'Did you forget to quote the URL? Remember that & is a meta '
1652             'character in most shells, so you want to put the URL in quotes, '
1653             'like  youtube-dl '
1654             '"http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
1655             ' or simply  youtube-dl BaW_jenozKc  .',
1656             expected=True)