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