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