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