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