Don't assume the 'subtitlesformat' is set in the params dict (fixes #1750)
[youtube-dl] / youtube_dl / extractor / youtube.py
1 # coding: utf-8
2
3 import collections
4 import errno
5 import io
6 import itertools
7 import json
8 import os.path
9 import re
10 import socket
11 import string
12 import struct
13 import traceback
14 import xml.etree.ElementTree
15 import zlib
16
17 from .common import InfoExtractor, SearchInfoExtractor
18 from .subtitles import SubtitlesInfoExtractor
19 from ..utils import (
20     compat_chr,
21     compat_http_client,
22     compat_parse_qs,
23     compat_urllib_error,
24     compat_urllib_parse,
25     compat_urllib_request,
26     compat_urlparse,
27     compat_str,
28
29     clean_html,
30     get_cachedir,
31     get_element_by_id,
32     ExtractorError,
33     unescapeHTML,
34     unified_strdate,
35     orderedSet,
36     write_json_file,
37 )
38
39 class YoutubeBaseInfoExtractor(InfoExtractor):
40     """Provide base functions for Youtube extractors"""
41     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
42     _LANG_URL = r'https://www.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1'
43     _AGE_URL = 'http://www.youtube.com/verify_age?next_url=/&gl=US&hl=en'
44     _NETRC_MACHINE = 'youtube'
45     # If True it will raise an error if no login info is provided
46     _LOGIN_REQUIRED = False
47
48     def report_lang(self):
49         """Report attempt to set language."""
50         self.to_screen(u'Setting language')
51
52     def _set_language(self):
53         request = compat_urllib_request.Request(self._LANG_URL)
54         try:
55             self.report_lang()
56             compat_urllib_request.urlopen(request).read()
57         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
58             self._downloader.report_warning(u'unable to set language: %s' % compat_str(err))
59             return False
60         return True
61
62     def _login(self):
63         (username, password) = self._get_login_info()
64         # No authentication to be performed
65         if username is None:
66             if self._LOGIN_REQUIRED:
67                 raise ExtractorError(u'No login info available, needed for using %s.' % self.IE_NAME, expected=True)
68             return False
69
70         request = compat_urllib_request.Request(self._LOGIN_URL)
71         try:
72             login_page = compat_urllib_request.urlopen(request).read().decode('utf-8')
73         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
74             self._downloader.report_warning(u'unable to fetch login page: %s' % compat_str(err))
75             return False
76
77         galx = self._search_regex(r'(?s)<input.+?name="GALX".+?value="(.+?)"',
78                                   login_page, u'Login GALX parameter')
79
80         # Log in
81         login_form_strs = {
82                 u'continue': u'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
83                 u'Email': username,
84                 u'GALX': galx,
85                 u'Passwd': password,
86                 u'PersistentCookie': u'yes',
87                 u'_utf8': u'霱',
88                 u'bgresponse': u'js_disabled',
89                 u'checkConnection': u'',
90                 u'checkedDomains': u'youtube',
91                 u'dnConn': u'',
92                 u'pstMsg': u'0',
93                 u'rmShown': u'1',
94                 u'secTok': u'',
95                 u'signIn': u'Sign in',
96                 u'timeStmp': u'',
97                 u'service': u'youtube',
98                 u'uilel': u'3',
99                 u'hl': u'en_US',
100         }
101         # Convert to UTF-8 *before* urlencode because Python 2.x's urlencode
102         # chokes on unicode
103         login_form = dict((k.encode('utf-8'), v.encode('utf-8')) for k,v in login_form_strs.items())
104         login_data = compat_urllib_parse.urlencode(login_form).encode('ascii')
105         request = compat_urllib_request.Request(self._LOGIN_URL, login_data)
106         try:
107             self.report_login()
108             login_results = compat_urllib_request.urlopen(request).read().decode('utf-8')
109             if re.search(r'(?i)<form[^>]* id="gaia_loginform"', login_results) is not None:
110                 self._downloader.report_warning(u'unable to log in: bad username or password')
111                 return False
112         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
113             self._downloader.report_warning(u'unable to log in: %s' % compat_str(err))
114             return False
115         return True
116
117     def _confirm_age(self):
118         age_form = {
119                 'next_url':     '/',
120                 'action_confirm':   'Confirm',
121                 }
122         request = compat_urllib_request.Request(self._AGE_URL, compat_urllib_parse.urlencode(age_form))
123         try:
124             self.report_age_confirmation()
125             compat_urllib_request.urlopen(request).read().decode('utf-8')
126         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
127             raise ExtractorError(u'Unable to confirm age: %s' % compat_str(err))
128         return True
129
130     def _real_initialize(self):
131         if self._downloader is None:
132             return
133         if not self._set_language():
134             return
135         if not self._login():
136             return
137         self._confirm_age()
138
139
140 class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
141     IE_DESC = u'YouTube.com'
142     _VALID_URL = r"""^
143                      (
144                          (?:https?://)?                                       # http(s):// (optional)
145                          (?:(?:(?:(?:\w+\.)?youtube(?:-nocookie)?\.com/|
146                             tube\.majestyc\.net/|
147                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains
148                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
149                          (?:                                                  # the various things that can precede the ID:
150                              (?:(?:v|embed|e)/)                               # v/ or embed/ or e/
151                              |(?:                                             # or the v= param in all its forms
152                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?)?    # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
153                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
154                                  (?:.*?&)?                                    # any other preceding param (like /?s=tuff&v=xxxx)
155                                  v=
156                              )
157                          ))
158                          |youtu\.be/                                          # just youtu.be/xxxx
159                          )
160                      )?                                                       # all until now is optional -> you can pass the naked ID
161                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID
162                      (?(1).+)?                                                # if we found the ID, everything can follow
163                      $"""
164     _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
165     # Listed in order of quality
166     _available_formats = ['38', '37', '46', '22', '45', '35', '44', '34', '18', '43', '6', '5', '36', '17', '13',
167                           # Apple HTTP Live Streaming
168                           '96', '95', '94', '93', '92', '132', '151',
169                           # 3D
170                           '85', '84', '102', '83', '101', '82', '100',
171                           # Dash video
172                           '138', '137', '248', '136', '247', '135', '246',
173                           '245', '244', '134', '243', '133', '242', '160',
174                           # Dash audio
175                           '141', '172', '140', '171', '139',
176                           ]
177     _available_formats_prefer_free = ['38', '46', '37', '45', '22', '44', '35', '43', '34', '18', '6', '5', '36', '17', '13',
178                                       # Apple HTTP Live Streaming
179                                       '96', '95', '94', '93', '92', '132', '151',
180                                       # 3D
181                                       '85', '102', '84', '101', '83', '100', '82',
182                                       # Dash video
183                                       '138', '248', '137', '247', '136', '246', '245',
184                                       '244', '135', '243', '134', '242', '133', '160',
185                                       # Dash audio
186                                       '172', '141', '171', '140', '139',
187                                       ]
188     _video_formats_map = {
189         'flv': ['35', '34', '6', '5'],
190         '3gp': ['36', '17', '13'],
191         'mp4': ['38', '37', '22', '18'],
192         'webm': ['46', '45', '44', '43'],
193     }
194     _video_extensions = {
195         '13': '3gp',
196         '17': '3gp',
197         '18': 'mp4',
198         '22': 'mp4',
199         '36': '3gp',
200         '37': 'mp4',
201         '38': 'mp4',
202         '43': 'webm',
203         '44': 'webm',
204         '45': 'webm',
205         '46': 'webm',
206
207         # 3d videos
208         '82': 'mp4',
209         '83': 'mp4',
210         '84': 'mp4',
211         '85': 'mp4',
212         '100': 'webm',
213         '101': 'webm',
214         '102': 'webm',
215
216         # Apple HTTP Live Streaming
217         '92': 'mp4',
218         '93': 'mp4',
219         '94': 'mp4',
220         '95': 'mp4',
221         '96': 'mp4',
222         '132': 'mp4',
223         '151': 'mp4',
224
225         # Dash mp4
226         '133': 'mp4',
227         '134': 'mp4',
228         '135': 'mp4',
229         '136': 'mp4',
230         '137': 'mp4',
231         '138': 'mp4',
232         '160': 'mp4',
233
234         # Dash mp4 audio
235         '139': 'm4a',
236         '140': 'm4a',
237         '141': 'm4a',
238
239         # Dash webm
240         '171': 'webm',
241         '172': 'webm',
242         '242': 'webm',
243         '243': 'webm',
244         '244': 'webm',
245         '245': 'webm',
246         '246': 'webm',
247         '247': 'webm',
248         '248': 'webm',
249     }
250     _video_dimensions = {
251         '5': '240x400',
252         '6': '???',
253         '13': '???',
254         '17': '144x176',
255         '18': '360x640',
256         '22': '720x1280',
257         '34': '360x640',
258         '35': '480x854',
259         '36': '240x320',
260         '37': '1080x1920',
261         '38': '3072x4096',
262         '43': '360x640',
263         '44': '480x854',
264         '45': '720x1280',
265         '46': '1080x1920',
266         '82': '360p',
267         '83': '480p',
268         '84': '720p',
269         '85': '1080p',
270         '92': '240p',
271         '93': '360p',
272         '94': '480p',
273         '95': '720p',
274         '96': '1080p',
275         '100': '360p',
276         '101': '480p',
277         '102': '720p',
278         '132': '240p',
279         '151': '72p',
280         '133': '240p',
281         '134': '360p',
282         '135': '480p',
283         '136': '720p',
284         '137': '1080p',
285         '138': '>1080p',
286         '139': '48k',
287         '140': '128k',
288         '141': '256k',
289         '160': '192p',
290         '171': '128k',
291         '172': '256k',
292         '242': '240p',
293         '243': '360p',
294         '244': '480p',
295         '245': '480p',
296         '246': '480p',
297         '247': '720p',
298         '248': '1080p',
299     }
300     _special_itags = {
301         '82': '3D',
302         '83': '3D',
303         '84': '3D',
304         '85': '3D',
305         '100': '3D',
306         '101': '3D',
307         '102': '3D',
308         '133': 'DASH Video',
309         '134': 'DASH Video',
310         '135': 'DASH Video',
311         '136': 'DASH Video',
312         '137': 'DASH Video',
313         '138': 'DASH Video',
314         '139': 'DASH Audio',
315         '140': 'DASH Audio',
316         '141': 'DASH Audio',
317         '160': 'DASH Video',
318         '171': 'DASH Audio',
319         '172': 'DASH Audio',
320         '242': 'DASH Video',
321         '243': 'DASH Video',
322         '244': 'DASH Video',
323         '245': 'DASH Video',
324         '246': 'DASH Video',
325         '247': 'DASH Video',
326         '248': 'DASH Video',
327     }
328
329     IE_NAME = u'youtube'
330     _TESTS = [
331         {
332             u"url":  u"http://www.youtube.com/watch?v=BaW_jenozKc",
333             u"file":  u"BaW_jenozKc.mp4",
334             u"info_dict": {
335                 u"title": u"youtube-dl test video \"'/\\ä↭𝕐",
336                 u"uploader": u"Philipp Hagemeister",
337                 u"uploader_id": u"phihag",
338                 u"upload_date": u"20121002",
339                 u"description": u"test chars:  \"'/\\ä↭𝕐\n\nThis is a test video for youtube-dl.\n\nFor more information, contact phihag@phihag.de ."
340             }
341         },
342         {
343             u"url":  u"http://www.youtube.com/watch?v=UxxajLWwzqY",
344             u"file":  u"UxxajLWwzqY.mp4",
345             u"note": u"Test generic use_cipher_signature video (#897)",
346             u"info_dict": {
347                 u"upload_date": u"20120506",
348                 u"title": u"Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]",
349                 u"description": u"md5:5b292926389560516e384ac437c0ec07",
350                 u"uploader": u"Icona Pop",
351                 u"uploader_id": u"IconaPop"
352             }
353         },
354         {
355             u"url":  u"https://www.youtube.com/watch?v=07FYdnEawAQ",
356             u"file":  u"07FYdnEawAQ.mp4",
357             u"note": u"Test VEVO video with age protection (#956)",
358             u"info_dict": {
359                 u"upload_date": u"20130703",
360                 u"title": u"Justin Timberlake - Tunnel Vision (Explicit)",
361                 u"description": u"md5:64249768eec3bc4276236606ea996373",
362                 u"uploader": u"justintimberlakeVEVO",
363                 u"uploader_id": u"justintimberlakeVEVO"
364             }
365         },
366     ]
367
368
369     @classmethod
370     def suitable(cls, url):
371         """Receives a URL and returns True if suitable for this IE."""
372         if YoutubePlaylistIE.suitable(url): return False
373         return re.match(cls._VALID_URL, url, re.VERBOSE) is not None
374
375     def __init__(self, *args, **kwargs):
376         super(YoutubeIE, self).__init__(*args, **kwargs)
377         self._player_cache = {}
378
379     def report_video_webpage_download(self, video_id):
380         """Report attempt to download video webpage."""
381         self.to_screen(u'%s: Downloading video webpage' % video_id)
382
383     def report_video_info_webpage_download(self, video_id):
384         """Report attempt to download video info webpage."""
385         self.to_screen(u'%s: Downloading video info webpage' % video_id)
386
387     def report_information_extraction(self, video_id):
388         """Report attempt to extract video information."""
389         self.to_screen(u'%s: Extracting video information' % video_id)
390
391     def report_unavailable_format(self, video_id, format):
392         """Report extracted video URL."""
393         self.to_screen(u'%s: Format %s not available' % (video_id, format))
394
395     def report_rtmp_download(self):
396         """Indicate the download will use the RTMP protocol."""
397         self.to_screen(u'RTMP download detected')
398
399     def _extract_signature_function(self, video_id, player_url, slen):
400         id_m = re.match(r'.*-(?P<id>[a-zA-Z0-9_-]+)\.(?P<ext>[a-z]+)$',
401                         player_url)
402         player_type = id_m.group('ext')
403         player_id = id_m.group('id')
404
405         # Read from filesystem cache
406         func_id = '%s_%s_%d' % (player_type, player_id, slen)
407         assert os.path.basename(func_id) == func_id
408         cache_dir = get_cachedir(self._downloader.params)
409
410         cache_enabled = cache_dir is not None
411         if cache_enabled:
412             cache_fn = os.path.join(os.path.expanduser(cache_dir),
413                                     u'youtube-sigfuncs',
414                                     func_id + '.json')
415             try:
416                 with io.open(cache_fn, 'r', encoding='utf-8') as cachef:
417                     cache_spec = json.load(cachef)
418                 return lambda s: u''.join(s[i] for i in cache_spec)
419             except IOError:
420                 pass  # No cache available
421
422         if player_type == 'js':
423             code = self._download_webpage(
424                 player_url, video_id,
425                 note=u'Downloading %s player %s' % (player_type, player_id),
426                 errnote=u'Download of %s failed' % player_url)
427             res = self._parse_sig_js(code)
428         elif player_type == 'swf':
429             urlh = self._request_webpage(
430                 player_url, video_id,
431                 note=u'Downloading %s player %s' % (player_type, player_id),
432                 errnote=u'Download of %s failed' % player_url)
433             code = urlh.read()
434             res = self._parse_sig_swf(code)
435         else:
436             assert False, 'Invalid player type %r' % player_type
437
438         if cache_enabled:
439             try:
440                 test_string = u''.join(map(compat_chr, range(slen)))
441                 cache_res = res(test_string)
442                 cache_spec = [ord(c) for c in cache_res]
443                 try:
444                     os.makedirs(os.path.dirname(cache_fn))
445                 except OSError as ose:
446                     if ose.errno != errno.EEXIST:
447                         raise
448                 write_json_file(cache_spec, cache_fn)
449             except Exception:
450                 tb = traceback.format_exc()
451                 self._downloader.report_warning(
452                     u'Writing cache to %r failed: %s' % (cache_fn, tb))
453
454         return res
455
456     def _print_sig_code(self, func, slen):
457         def gen_sig_code(idxs):
458             def _genslice(start, end, step):
459                 starts = u'' if start == 0 else str(start)
460                 ends = (u':%d' % (end+step)) if end + step >= 0 else u':'
461                 steps = u'' if step == 1 else (u':%d' % step)
462                 return u's[%s%s%s]' % (starts, ends, steps)
463
464             step = None
465             start = '(Never used)'  # Quelch pyflakes warnings - start will be
466                                     # set as soon as step is set
467             for i, prev in zip(idxs[1:], idxs[:-1]):
468                 if step is not None:
469                     if i - prev == step:
470                         continue
471                     yield _genslice(start, prev, step)
472                     step = None
473                     continue
474                 if i - prev in [-1, 1]:
475                     step = i - prev
476                     start = prev
477                     continue
478                 else:
479                     yield u's[%d]' % prev
480             if step is None:
481                 yield u's[%d]' % i
482             else:
483                 yield _genslice(start, i, step)
484
485         test_string = u''.join(map(compat_chr, range(slen)))
486         cache_res = func(test_string)
487         cache_spec = [ord(c) for c in cache_res]
488         expr_code = u' + '.join(gen_sig_code(cache_spec))
489         code = u'if len(s) == %d:\n    return %s\n' % (slen, expr_code)
490         self.to_screen(u'Extracted signature function:\n' + code)
491
492     def _parse_sig_js(self, jscode):
493         funcname = self._search_regex(
494             r'signature=([a-zA-Z]+)', jscode,
495             u'Initial JS player signature function name')
496
497         functions = {}
498
499         def argidx(varname):
500             return string.lowercase.index(varname)
501
502         def interpret_statement(stmt, local_vars, allow_recursion=20):
503             if allow_recursion < 0:
504                 raise ExtractorError(u'Recursion limit reached')
505
506             if stmt.startswith(u'var '):
507                 stmt = stmt[len(u'var '):]
508             ass_m = re.match(r'^(?P<out>[a-z]+)(?:\[(?P<index>[^\]]+)\])?' +
509                              r'=(?P<expr>.*)$', stmt)
510             if ass_m:
511                 if ass_m.groupdict().get('index'):
512                     def assign(val):
513                         lvar = local_vars[ass_m.group('out')]
514                         idx = interpret_expression(ass_m.group('index'),
515                                                    local_vars, allow_recursion)
516                         assert isinstance(idx, int)
517                         lvar[idx] = val
518                         return val
519                     expr = ass_m.group('expr')
520                 else:
521                     def assign(val):
522                         local_vars[ass_m.group('out')] = val
523                         return val
524                     expr = ass_m.group('expr')
525             elif stmt.startswith(u'return '):
526                 assign = lambda v: v
527                 expr = stmt[len(u'return '):]
528             else:
529                 raise ExtractorError(
530                     u'Cannot determine left side of statement in %r' % stmt)
531
532             v = interpret_expression(expr, local_vars, allow_recursion)
533             return assign(v)
534
535         def interpret_expression(expr, local_vars, allow_recursion):
536             if expr.isdigit():
537                 return int(expr)
538
539             if expr.isalpha():
540                 return local_vars[expr]
541
542             m = re.match(r'^(?P<in>[a-z]+)\.(?P<member>.*)$', expr)
543             if m:
544                 member = m.group('member')
545                 val = local_vars[m.group('in')]
546                 if member == 'split("")':
547                     return list(val)
548                 if member == 'join("")':
549                     return u''.join(val)
550                 if member == 'length':
551                     return len(val)
552                 if member == 'reverse()':
553                     return val[::-1]
554                 slice_m = re.match(r'slice\((?P<idx>.*)\)', member)
555                 if slice_m:
556                     idx = interpret_expression(
557                         slice_m.group('idx'), local_vars, allow_recursion-1)
558                     return val[idx:]
559
560             m = re.match(
561                 r'^(?P<in>[a-z]+)\[(?P<idx>.+)\]$', expr)
562             if m:
563                 val = local_vars[m.group('in')]
564                 idx = interpret_expression(m.group('idx'), local_vars,
565                                            allow_recursion-1)
566                 return val[idx]
567
568             m = re.match(r'^(?P<a>.+?)(?P<op>[%])(?P<b>.+?)$', expr)
569             if m:
570                 a = interpret_expression(m.group('a'),
571                                          local_vars, allow_recursion)
572                 b = interpret_expression(m.group('b'),
573                                          local_vars, allow_recursion)
574                 return a % b
575
576             m = re.match(
577                 r'^(?P<func>[a-zA-Z]+)\((?P<args>[a-z0-9,]+)\)$', expr)
578             if m:
579                 fname = m.group('func')
580                 if fname not in functions:
581                     functions[fname] = extract_function(fname)
582                 argvals = [int(v) if v.isdigit() else local_vars[v]
583                            for v in m.group('args').split(',')]
584                 return functions[fname](argvals)
585             raise ExtractorError(u'Unsupported JS expression %r' % expr)
586
587         def extract_function(funcname):
588             func_m = re.search(
589                 r'function ' + re.escape(funcname) +
590                 r'\((?P<args>[a-z,]+)\){(?P<code>[^}]+)}',
591                 jscode)
592             argnames = func_m.group('args').split(',')
593
594             def resf(args):
595                 local_vars = dict(zip(argnames, args))
596                 for stmt in func_m.group('code').split(';'):
597                     res = interpret_statement(stmt, local_vars)
598                 return res
599             return resf
600
601         initial_function = extract_function(funcname)
602         return lambda s: initial_function([s])
603
604     def _parse_sig_swf(self, file_contents):
605         if file_contents[1:3] != b'WS':
606             raise ExtractorError(
607                 u'Not an SWF file; header is %r' % file_contents[:3])
608         if file_contents[:1] == b'C':
609             content = zlib.decompress(file_contents[8:])
610         else:
611             raise NotImplementedError(u'Unsupported compression format %r' %
612                                       file_contents[:1])
613
614         def extract_tags(content):
615             pos = 0
616             while pos < len(content):
617                 header16 = struct.unpack('<H', content[pos:pos+2])[0]
618                 pos += 2
619                 tag_code = header16 >> 6
620                 tag_len = header16 & 0x3f
621                 if tag_len == 0x3f:
622                     tag_len = struct.unpack('<I', content[pos:pos+4])[0]
623                     pos += 4
624                 assert pos+tag_len <= len(content)
625                 yield (tag_code, content[pos:pos+tag_len])
626                 pos += tag_len
627
628         code_tag = next(tag
629                         for tag_code, tag in extract_tags(content)
630                         if tag_code == 82)
631         p = code_tag.index(b'\0', 4) + 1
632         code_reader = io.BytesIO(code_tag[p:])
633
634         # Parse ABC (AVM2 ByteCode)
635         def read_int(reader=None):
636             if reader is None:
637                 reader = code_reader
638             res = 0
639             shift = 0
640             for _ in range(5):
641                 buf = reader.read(1)
642                 assert len(buf) == 1
643                 b = struct.unpack('<B', buf)[0]
644                 res = res | ((b & 0x7f) << shift)
645                 if b & 0x80 == 0:
646                     break
647                 shift += 7
648             return res
649
650         def u30(reader=None):
651             res = read_int(reader)
652             assert res & 0xf0000000 == 0
653             return res
654         u32 = read_int
655
656         def s32(reader=None):
657             v = read_int(reader)
658             if v & 0x80000000 != 0:
659                 v = - ((v ^ 0xffffffff) + 1)
660             return v
661
662         def read_string(reader=None):
663             if reader is None:
664                 reader = code_reader
665             slen = u30(reader)
666             resb = reader.read(slen)
667             assert len(resb) == slen
668             return resb.decode('utf-8')
669
670         def read_bytes(count, reader=None):
671             if reader is None:
672                 reader = code_reader
673             resb = reader.read(count)
674             assert len(resb) == count
675             return resb
676
677         def read_byte(reader=None):
678             resb = read_bytes(1, reader=reader)
679             res = struct.unpack('<B', resb)[0]
680             return res
681
682         # minor_version + major_version
683         read_bytes(2 + 2)
684
685         # Constant pool
686         int_count = u30()
687         for _c in range(1, int_count):
688             s32()
689         uint_count = u30()
690         for _c in range(1, uint_count):
691             u32()
692         double_count = u30()
693         read_bytes((double_count-1) * 8)
694         string_count = u30()
695         constant_strings = [u'']
696         for _c in range(1, string_count):
697             s = read_string()
698             constant_strings.append(s)
699         namespace_count = u30()
700         for _c in range(1, namespace_count):
701             read_bytes(1)  # kind
702             u30()  # name
703         ns_set_count = u30()
704         for _c in range(1, ns_set_count):
705             count = u30()
706             for _c2 in range(count):
707                 u30()
708         multiname_count = u30()
709         MULTINAME_SIZES = {
710             0x07: 2,  # QName
711             0x0d: 2,  # QNameA
712             0x0f: 1,  # RTQName
713             0x10: 1,  # RTQNameA
714             0x11: 0,  # RTQNameL
715             0x12: 0,  # RTQNameLA
716             0x09: 2,  # Multiname
717             0x0e: 2,  # MultinameA
718             0x1b: 1,  # MultinameL
719             0x1c: 1,  # MultinameLA
720         }
721         multinames = [u'']
722         for _c in range(1, multiname_count):
723             kind = u30()
724             assert kind in MULTINAME_SIZES, u'Invalid multiname kind %r' % kind
725             if kind == 0x07:
726                 u30()  # namespace_idx
727                 name_idx = u30()
728                 multinames.append(constant_strings[name_idx])
729             else:
730                 multinames.append('[MULTINAME kind: %d]' % kind)
731                 for _c2 in range(MULTINAME_SIZES[kind]):
732                     u30()
733
734         # Methods
735         method_count = u30()
736         MethodInfo = collections.namedtuple(
737             'MethodInfo',
738             ['NEED_ARGUMENTS', 'NEED_REST'])
739         method_infos = []
740         for method_id in range(method_count):
741             param_count = u30()
742             u30()  # return type
743             for _ in range(param_count):
744                 u30()  # param type
745             u30()  # name index (always 0 for youtube)
746             flags = read_byte()
747             if flags & 0x08 != 0:
748                 # Options present
749                 option_count = u30()
750                 for c in range(option_count):
751                     u30()  # val
752                     read_bytes(1)  # kind
753             if flags & 0x80 != 0:
754                 # Param names present
755                 for _ in range(param_count):
756                     u30()  # param name
757             mi = MethodInfo(flags & 0x01 != 0, flags & 0x04 != 0)
758             method_infos.append(mi)
759
760         # Metadata
761         metadata_count = u30()
762         for _c in range(metadata_count):
763             u30()  # name
764             item_count = u30()
765             for _c2 in range(item_count):
766                 u30()  # key
767                 u30()  # value
768
769         def parse_traits_info():
770             trait_name_idx = u30()
771             kind_full = read_byte()
772             kind = kind_full & 0x0f
773             attrs = kind_full >> 4
774             methods = {}
775             if kind in [0x00, 0x06]:  # Slot or Const
776                 u30()  # Slot id
777                 u30()  # type_name_idx
778                 vindex = u30()
779                 if vindex != 0:
780                     read_byte()  # vkind
781             elif kind in [0x01, 0x02, 0x03]:  # Method / Getter / Setter
782                 u30()  # disp_id
783                 method_idx = u30()
784                 methods[multinames[trait_name_idx]] = method_idx
785             elif kind == 0x04:  # Class
786                 u30()  # slot_id
787                 u30()  # classi
788             elif kind == 0x05:  # Function
789                 u30()  # slot_id
790                 function_idx = u30()
791                 methods[function_idx] = multinames[trait_name_idx]
792             else:
793                 raise ExtractorError(u'Unsupported trait kind %d' % kind)
794
795             if attrs & 0x4 != 0:  # Metadata present
796                 metadata_count = u30()
797                 for _c3 in range(metadata_count):
798                     u30()  # metadata index
799
800             return methods
801
802         # Classes
803         TARGET_CLASSNAME = u'SignatureDecipher'
804         searched_idx = multinames.index(TARGET_CLASSNAME)
805         searched_class_id = None
806         class_count = u30()
807         for class_id in range(class_count):
808             name_idx = u30()
809             if name_idx == searched_idx:
810                 # We found the class we're looking for!
811                 searched_class_id = class_id
812             u30()  # super_name idx
813             flags = read_byte()
814             if flags & 0x08 != 0:  # Protected namespace is present
815                 u30()  # protected_ns_idx
816             intrf_count = u30()
817             for _c2 in range(intrf_count):
818                 u30()
819             u30()  # iinit
820             trait_count = u30()
821             for _c2 in range(trait_count):
822                 parse_traits_info()
823
824         if searched_class_id is None:
825             raise ExtractorError(u'Target class %r not found' %
826                                  TARGET_CLASSNAME)
827
828         method_names = {}
829         method_idxs = {}
830         for class_id in range(class_count):
831             u30()  # cinit
832             trait_count = u30()
833             for _c2 in range(trait_count):
834                 trait_methods = parse_traits_info()
835                 if class_id == searched_class_id:
836                     method_names.update(trait_methods.items())
837                     method_idxs.update(dict(
838                         (idx, name)
839                         for name, idx in trait_methods.items()))
840
841         # Scripts
842         script_count = u30()
843         for _c in range(script_count):
844             u30()  # init
845             trait_count = u30()
846             for _c2 in range(trait_count):
847                 parse_traits_info()
848
849         # Method bodies
850         method_body_count = u30()
851         Method = collections.namedtuple('Method', ['code', 'local_count'])
852         methods = {}
853         for _c in range(method_body_count):
854             method_idx = u30()
855             u30()  # max_stack
856             local_count = u30()
857             u30()  # init_scope_depth
858             u30()  # max_scope_depth
859             code_length = u30()
860             code = read_bytes(code_length)
861             if method_idx in method_idxs:
862                 m = Method(code, local_count)
863                 methods[method_idxs[method_idx]] = m
864             exception_count = u30()
865             for _c2 in range(exception_count):
866                 u30()  # from
867                 u30()  # to
868                 u30()  # target
869                 u30()  # exc_type
870                 u30()  # var_name
871             trait_count = u30()
872             for _c2 in range(trait_count):
873                 parse_traits_info()
874
875         assert p + code_reader.tell() == len(code_tag)
876         assert len(methods) == len(method_idxs)
877
878         method_pyfunctions = {}
879
880         def extract_function(func_name):
881             if func_name in method_pyfunctions:
882                 return method_pyfunctions[func_name]
883             if func_name not in methods:
884                 raise ExtractorError(u'Cannot find function %r' % func_name)
885             m = methods[func_name]
886
887             def resfunc(args):
888                 registers = ['(this)'] + list(args) + [None] * m.local_count
889                 stack = []
890                 coder = io.BytesIO(m.code)
891                 while True:
892                     opcode = struct.unpack('!B', coder.read(1))[0]
893                     if opcode == 36:  # pushbyte
894                         v = struct.unpack('!B', coder.read(1))[0]
895                         stack.append(v)
896                     elif opcode == 44:  # pushstring
897                         idx = u30(coder)
898                         stack.append(constant_strings[idx])
899                     elif opcode == 48:  # pushscope
900                         # We don't implement the scope register, so we'll just
901                         # ignore the popped value
902                         stack.pop()
903                     elif opcode == 70:  # callproperty
904                         index = u30(coder)
905                         mname = multinames[index]
906                         arg_count = u30(coder)
907                         args = list(reversed(
908                             [stack.pop() for _ in range(arg_count)]))
909                         obj = stack.pop()
910                         if mname == u'split':
911                             assert len(args) == 1
912                             assert isinstance(args[0], compat_str)
913                             assert isinstance(obj, compat_str)
914                             if args[0] == u'':
915                                 res = list(obj)
916                             else:
917                                 res = obj.split(args[0])
918                             stack.append(res)
919                         elif mname == u'slice':
920                             assert len(args) == 1
921                             assert isinstance(args[0], int)
922                             assert isinstance(obj, list)
923                             res = obj[args[0]:]
924                             stack.append(res)
925                         elif mname == u'join':
926                             assert len(args) == 1
927                             assert isinstance(args[0], compat_str)
928                             assert isinstance(obj, list)
929                             res = args[0].join(obj)
930                             stack.append(res)
931                         elif mname in method_pyfunctions:
932                             stack.append(method_pyfunctions[mname](args))
933                         else:
934                             raise NotImplementedError(
935                                 u'Unsupported property %r on %r'
936                                 % (mname, obj))
937                     elif opcode == 72:  # returnvalue
938                         res = stack.pop()
939                         return res
940                     elif opcode == 79:  # callpropvoid
941                         index = u30(coder)
942                         mname = multinames[index]
943                         arg_count = u30(coder)
944                         args = list(reversed(
945                             [stack.pop() for _ in range(arg_count)]))
946                         obj = stack.pop()
947                         if mname == u'reverse':
948                             assert isinstance(obj, list)
949                             obj.reverse()
950                         else:
951                             raise NotImplementedError(
952                                 u'Unsupported (void) property %r on %r'
953                                 % (mname, obj))
954                     elif opcode == 93:  # findpropstrict
955                         index = u30(coder)
956                         mname = multinames[index]
957                         res = extract_function(mname)
958                         stack.append(res)
959                     elif opcode == 97:  # setproperty
960                         index = u30(coder)
961                         value = stack.pop()
962                         idx = stack.pop()
963                         obj = stack.pop()
964                         assert isinstance(obj, list)
965                         assert isinstance(idx, int)
966                         obj[idx] = value
967                     elif opcode == 98:  # getlocal
968                         index = u30(coder)
969                         stack.append(registers[index])
970                     elif opcode == 99:  # setlocal
971                         index = u30(coder)
972                         value = stack.pop()
973                         registers[index] = value
974                     elif opcode == 102:  # getproperty
975                         index = u30(coder)
976                         pname = multinames[index]
977                         if pname == u'length':
978                             obj = stack.pop()
979                             assert isinstance(obj, list)
980                             stack.append(len(obj))
981                         else:  # Assume attribute access
982                             idx = stack.pop()
983                             assert isinstance(idx, int)
984                             obj = stack.pop()
985                             assert isinstance(obj, list)
986                             stack.append(obj[idx])
987                     elif opcode == 128:  # coerce
988                         u30(coder)
989                     elif opcode == 133:  # coerce_s
990                         assert isinstance(stack[-1], (type(None), compat_str))
991                     elif opcode == 164:  # modulo
992                         value2 = stack.pop()
993                         value1 = stack.pop()
994                         res = value1 % value2
995                         stack.append(res)
996                     elif opcode == 208:  # getlocal_0
997                         stack.append(registers[0])
998                     elif opcode == 209:  # getlocal_1
999                         stack.append(registers[1])
1000                     elif opcode == 210:  # getlocal_2
1001                         stack.append(registers[2])
1002                     elif opcode == 211:  # getlocal_3
1003                         stack.append(registers[3])
1004                     elif opcode == 214:  # setlocal_2
1005                         registers[2] = stack.pop()
1006                     elif opcode == 215:  # setlocal_3
1007                         registers[3] = stack.pop()
1008                     else:
1009                         raise NotImplementedError(
1010                             u'Unsupported opcode %d' % opcode)
1011
1012             method_pyfunctions[func_name] = resfunc
1013             return resfunc
1014
1015         initial_function = extract_function(u'decipher')
1016         return lambda s: initial_function([s])
1017
1018     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
1019         """Turn the encrypted s field into a working signature"""
1020
1021         if player_url is not None:
1022             try:
1023                 player_id = (player_url, len(s))
1024                 if player_id not in self._player_cache:
1025                     func = self._extract_signature_function(
1026                         video_id, player_url, len(s)
1027                     )
1028                     self._player_cache[player_id] = func
1029                 func = self._player_cache[player_id]
1030                 if self._downloader.params.get('youtube_print_sig_code'):
1031                     self._print_sig_code(func, len(s))
1032                 return func(s)
1033             except Exception:
1034                 tb = traceback.format_exc()
1035                 self._downloader.report_warning(
1036                     u'Automatic signature extraction failed: ' + tb)
1037
1038             self._downloader.report_warning(
1039                 u'Warning: Falling back to static signature algorithm')
1040
1041         return self._static_decrypt_signature(
1042             s, video_id, player_url, age_gate)
1043
1044     def _static_decrypt_signature(self, s, video_id, player_url, age_gate):
1045         if age_gate:
1046             # The videos with age protection use another player, so the
1047             # algorithms can be different.
1048             if len(s) == 86:
1049                 return s[2:63] + s[82] + s[64:82] + s[63]
1050
1051         if len(s) == 93:
1052             return s[86:29:-1] + s[88] + s[28:5:-1]
1053         elif len(s) == 92:
1054             return s[25] + s[3:25] + s[0] + s[26:42] + s[79] + s[43:79] + s[91] + s[80:83]
1055         elif len(s) == 91:
1056             return s[84:27:-1] + s[86] + s[26:5:-1]
1057         elif len(s) == 90:
1058             return s[25] + s[3:25] + s[2] + s[26:40] + s[77] + s[41:77] + s[89] + s[78:81]
1059         elif len(s) == 89:
1060             return s[84:78:-1] + s[87] + s[77:60:-1] + s[0] + s[59:3:-1]
1061         elif len(s) == 88:
1062             return s[7:28] + s[87] + s[29:45] + s[55] + s[46:55] + s[2] + s[56:87] + s[28]
1063         elif len(s) == 87:
1064             return s[6:27] + s[4] + s[28:39] + s[27] + s[40:59] + s[2] + s[60:]
1065         elif len(s) == 86:
1066             return s[80:72:-1] + s[16] + s[71:39:-1] + s[72] + s[38:16:-1] + s[82] + s[15::-1]
1067         elif len(s) == 85:
1068             return s[3:11] + s[0] + s[12:55] + s[84] + s[56:84]
1069         elif len(s) == 84:
1070             return s[78:70:-1] + s[14] + s[69:37:-1] + s[70] + s[36:14:-1] + s[80] + s[:14][::-1]
1071         elif len(s) == 83:
1072             return s[80:63:-1] + s[0] + s[62:0:-1] + s[63]
1073         elif len(s) == 82:
1074             return s[80:37:-1] + s[7] + s[36:7:-1] + s[0] + s[6:0:-1] + s[37]
1075         elif len(s) == 81:
1076             return s[56] + s[79:56:-1] + s[41] + s[55:41:-1] + s[80] + s[40:34:-1] + s[0] + s[33:29:-1] + s[34] + s[28:9:-1] + s[29] + s[8:0:-1] + s[9]
1077         elif len(s) == 80:
1078             return s[1:19] + s[0] + s[20:68] + s[19] + s[69:80]
1079         elif len(s) == 79:
1080             return s[54] + s[77:54:-1] + s[39] + s[53:39:-1] + s[78] + s[38:34:-1] + s[0] + s[33:29:-1] + s[34] + s[28:9:-1] + s[29] + s[8:0:-1] + s[9]
1081
1082         else:
1083             raise ExtractorError(u'Unable to decrypt signature, key length %d not supported; retrying might work' % (len(s)))
1084
1085     def _get_available_subtitles(self, video_id, webpage):
1086         try:
1087             sub_list = self._download_webpage(
1088                 'http://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
1089                 video_id, note=False)
1090         except ExtractorError as err:
1091             self._downloader.report_warning(u'unable to download video subtitles: %s' % compat_str(err))
1092             return {}
1093         lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list)
1094
1095         sub_lang_list = {}
1096         for l in lang_list:
1097             lang = l[1]
1098             params = compat_urllib_parse.urlencode({
1099                 'lang': lang,
1100                 'v': video_id,
1101                 'fmt': self._downloader.params.get('subtitlesformat', 'srt'),
1102                 'name': l[0].encode('utf-8'),
1103             })
1104             url = u'http://www.youtube.com/api/timedtext?' + params
1105             sub_lang_list[lang] = url
1106         if not sub_lang_list:
1107             self._downloader.report_warning(u'video doesn\'t have subtitles')
1108             return {}
1109         return sub_lang_list
1110
1111     def _get_available_automatic_caption(self, video_id, webpage):
1112         """We need the webpage for getting the captions url, pass it as an
1113            argument to speed up the process."""
1114         sub_format = self._downloader.params.get('subtitlesformat', 'srt')
1115         self.to_screen(u'%s: Looking for automatic captions' % video_id)
1116         mobj = re.search(r';ytplayer.config = ({.*?});', webpage)
1117         err_msg = u'Couldn\'t find automatic captions for %s' % video_id
1118         if mobj is None:
1119             self._downloader.report_warning(err_msg)
1120             return {}
1121         player_config = json.loads(mobj.group(1))
1122         try:
1123             args = player_config[u'args']
1124             caption_url = args[u'ttsurl']
1125             timestamp = args[u'timestamp']
1126             # We get the available subtitles
1127             list_params = compat_urllib_parse.urlencode({
1128                 'type': 'list',
1129                 'tlangs': 1,
1130                 'asrs': 1,
1131             })
1132             list_url = caption_url + '&' + list_params
1133             list_page = self._download_webpage(list_url, video_id)
1134             caption_list = xml.etree.ElementTree.fromstring(list_page.encode('utf-8'))
1135             original_lang_node = caption_list.find('track')
1136             if original_lang_node is None or original_lang_node.attrib.get('kind') != 'asr' :
1137                 self._downloader.report_warning(u'Video doesn\'t have automatic captions')
1138                 return {}
1139             original_lang = original_lang_node.attrib['lang_code']
1140
1141             sub_lang_list = {}
1142             for lang_node in caption_list.findall('target'):
1143                 sub_lang = lang_node.attrib['lang_code']
1144                 params = compat_urllib_parse.urlencode({
1145                     'lang': original_lang,
1146                     'tlang': sub_lang,
1147                     'fmt': sub_format,
1148                     'ts': timestamp,
1149                     'kind': 'asr',
1150                 })
1151                 sub_lang_list[sub_lang] = caption_url + '&' + params
1152             return sub_lang_list
1153         # An extractor error can be raise by the download process if there are
1154         # no automatic captions but there are subtitles
1155         except (KeyError, ExtractorError):
1156             self._downloader.report_warning(err_msg)
1157             return {}
1158
1159     def _print_formats(self, formats):
1160         print('Available formats:')
1161         for x in formats:
1162             print('%s\t:\t%s\t[%s]%s' %(x, self._video_extensions.get(x, 'flv'),
1163                                         self._video_dimensions.get(x, '???'),
1164                                         ' ('+self._special_itags[x]+')' if x in self._special_itags else ''))
1165
1166     def _extract_id(self, url):
1167         mobj = re.match(self._VALID_URL, url, re.VERBOSE)
1168         if mobj is None:
1169             raise ExtractorError(u'Invalid URL: %s' % url)
1170         video_id = mobj.group(2)
1171         return video_id
1172
1173     def _get_video_url_list(self, url_map):
1174         """
1175         Transform a dictionary in the format {itag:url} to a list of (itag, url)
1176         with the requested formats.
1177         """
1178         req_format = self._downloader.params.get('format', None)
1179         format_limit = self._downloader.params.get('format_limit', None)
1180         available_formats = self._available_formats_prefer_free if self._downloader.params.get('prefer_free_formats', False) else self._available_formats
1181         if format_limit is not None and format_limit in available_formats:
1182             format_list = available_formats[available_formats.index(format_limit):]
1183         else:
1184             format_list = available_formats
1185         existing_formats = [x for x in format_list if x in url_map]
1186         if len(existing_formats) == 0:
1187             raise ExtractorError(u'no known formats available for video')
1188         if self._downloader.params.get('listformats', None):
1189             self._print_formats(existing_formats)
1190             return
1191         if req_format is None or req_format == 'best':
1192             video_url_list = [(existing_formats[0], url_map[existing_formats[0]])] # Best quality
1193         elif req_format == 'worst':
1194             video_url_list = [(existing_formats[-1], url_map[existing_formats[-1]])] # worst quality
1195         elif req_format in ('-1', 'all'):
1196             video_url_list = [(f, url_map[f]) for f in existing_formats] # All formats
1197         else:
1198             # Specific formats. We pick the first in a slash-delimeted sequence.
1199             # Format can be specified as itag or 'mp4' or 'flv' etc. We pick the highest quality
1200             # available in the specified format. For example,
1201             # if '1/2/3/4' is requested and '2' and '4' are available, we pick '2'.
1202             # if '1/mp4/3/4' is requested and '1' and '5' (is a mp4) are available, we pick '1'.
1203             # if '1/mp4/3/4' is requested and '4' and '5' (is a mp4) are available, we pick '5'.
1204             req_formats = req_format.split('/')
1205             video_url_list = None
1206             for rf in req_formats:
1207                 if rf in url_map:
1208                     video_url_list = [(rf, url_map[rf])]
1209                     break
1210                 if rf in self._video_formats_map:
1211                     for srf in self._video_formats_map[rf]:
1212                         if srf in url_map:
1213                             video_url_list = [(srf, url_map[srf])]
1214                             break
1215                     else:
1216                         continue
1217                     break
1218             if video_url_list is None:
1219                 raise ExtractorError(u'requested format not available')
1220         return video_url_list
1221
1222     def _extract_from_m3u8(self, manifest_url, video_id):
1223         url_map = {}
1224         def _get_urls(_manifest):
1225             lines = _manifest.split('\n')
1226             urls = filter(lambda l: l and not l.startswith('#'),
1227                             lines)
1228             return urls
1229         manifest = self._download_webpage(manifest_url, video_id, u'Downloading formats manifest')
1230         formats_urls = _get_urls(manifest)
1231         for format_url in formats_urls:
1232             itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
1233             url_map[itag] = format_url
1234         return url_map
1235
1236     def _extract_annotations(self, video_id):
1237         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
1238         return self._download_webpage(url, video_id, note=u'Searching for annotations.', errnote=u'Unable to download video annotations.')
1239
1240     def _real_extract(self, url):
1241         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1242         mobj = re.search(self._NEXT_URL_RE, url)
1243         if mobj:
1244             url = 'https://www.youtube.com/' + compat_urllib_parse.unquote(mobj.group(1)).lstrip('/')
1245         video_id = self._extract_id(url)
1246
1247         # Get video webpage
1248         self.report_video_webpage_download(video_id)
1249         url = 'https://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1' % video_id
1250         request = compat_urllib_request.Request(url)
1251         try:
1252             video_webpage_bytes = compat_urllib_request.urlopen(request).read()
1253         except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1254             raise ExtractorError(u'Unable to download video webpage: %s' % compat_str(err))
1255
1256         video_webpage = video_webpage_bytes.decode('utf-8', 'ignore')
1257
1258         # Attempt to extract SWF player URL
1259         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1260         if mobj is not None:
1261             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
1262         else:
1263             player_url = None
1264
1265         # Get video info
1266         self.report_video_info_webpage_download(video_id)
1267         if re.search(r'player-age-gate-content">', video_webpage) is not None:
1268             self.report_age_confirmation()
1269             age_gate = True
1270             # We simulate the access to the video from www.youtube.com/v/{video_id}
1271             # this can be viewed without login into Youtube
1272             data = compat_urllib_parse.urlencode({'video_id': video_id,
1273                                                   'el': 'embedded',
1274                                                   'gl': 'US',
1275                                                   'hl': 'en',
1276                                                   'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1277                                                   'asv': 3,
1278                                                   'sts':'1588',
1279                                                   })
1280             video_info_url = 'https://www.youtube.com/get_video_info?' + data
1281             video_info_webpage = self._download_webpage(video_info_url, video_id,
1282                                     note=False,
1283                                     errnote='unable to download video info webpage')
1284             video_info = compat_parse_qs(video_info_webpage)
1285         else:
1286             age_gate = False
1287             for el_type in ['&el=embedded', '&el=detailpage', '&el=vevo', '']:
1288                 video_info_url = ('https://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
1289                         % (video_id, el_type))
1290                 video_info_webpage = self._download_webpage(video_info_url, video_id,
1291                                         note=False,
1292                                         errnote='unable to download video info webpage')
1293                 video_info = compat_parse_qs(video_info_webpage)
1294                 if 'token' in video_info:
1295                     break
1296         if 'token' not in video_info:
1297             if 'reason' in video_info:
1298                 raise ExtractorError(u'YouTube said: %s' % video_info['reason'][0], expected=True)
1299             else:
1300                 raise ExtractorError(u'"token" parameter not in video info for unknown reason')
1301
1302         # Check for "rental" videos
1303         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
1304             raise ExtractorError(u'"rental" videos not supported')
1305
1306         # Start extracting information
1307         self.report_information_extraction(video_id)
1308
1309         # uploader
1310         if 'author' not in video_info:
1311             raise ExtractorError(u'Unable to extract uploader name')
1312         video_uploader = compat_urllib_parse.unquote_plus(video_info['author'][0])
1313
1314         # uploader_id
1315         video_uploader_id = None
1316         mobj = re.search(r'<link itemprop="url" href="http://www.youtube.com/(?:user|channel)/([^"]+)">', video_webpage)
1317         if mobj is not None:
1318             video_uploader_id = mobj.group(1)
1319         else:
1320             self._downloader.report_warning(u'unable to extract uploader nickname')
1321
1322         # title
1323         if 'title' in video_info:
1324             video_title = compat_urllib_parse.unquote_plus(video_info['title'][0])
1325         else:
1326             self._downloader.report_warning(u'Unable to extract video title')
1327             video_title = u'_'
1328
1329         # thumbnail image
1330         # We try first to get a high quality image:
1331         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
1332                             video_webpage, re.DOTALL)
1333         if m_thumb is not None:
1334             video_thumbnail = m_thumb.group(1)
1335         elif 'thumbnail_url' not in video_info:
1336             self._downloader.report_warning(u'unable to extract video thumbnail')
1337             video_thumbnail = None
1338         else:   # don't panic if we can't find it
1339             video_thumbnail = compat_urllib_parse.unquote_plus(video_info['thumbnail_url'][0])
1340
1341         # upload date
1342         upload_date = None
1343         mobj = re.search(r'id="eow-date.*?>(.*?)</span>', video_webpage, re.DOTALL)
1344         if mobj is not None:
1345             upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
1346             upload_date = unified_strdate(upload_date)
1347
1348         # description
1349         video_description = get_element_by_id("eow-description", video_webpage)
1350         if video_description:
1351             video_description = clean_html(video_description)
1352         else:
1353             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
1354             if fd_mobj:
1355                 video_description = unescapeHTML(fd_mobj.group(1))
1356             else:
1357                 video_description = u''
1358
1359         # subtitles
1360         video_subtitles = self.extract_subtitles(video_id, video_webpage)
1361
1362         if self._downloader.params.get('listsubtitles', False):
1363             self._list_available_subtitles(video_id, video_webpage)
1364             return
1365
1366         if 'length_seconds' not in video_info:
1367             self._downloader.report_warning(u'unable to extract video duration')
1368             video_duration = ''
1369         else:
1370             video_duration = compat_urllib_parse.unquote_plus(video_info['length_seconds'][0])
1371
1372         # annotations
1373         video_annotations = None
1374         if self._downloader.params.get('writeannotations', False):
1375                 video_annotations = self._extract_annotations(video_id)
1376
1377         # Decide which formats to download
1378
1379         try:
1380             mobj = re.search(r';ytplayer.config = ({.*?});', video_webpage)
1381             if not mobj:
1382                 raise ValueError('Could not find vevo ID')
1383             info = json.loads(mobj.group(1))
1384             args = info['args']
1385             # Easy way to know if the 's' value is in url_encoded_fmt_stream_map
1386             # this signatures are encrypted
1387             if 'url_encoded_fmt_stream_map' not in args:
1388                 raise ValueError(u'No stream_map present')  # caught below
1389             re_signature = re.compile(r'[&,]s=')
1390             m_s = re_signature.search(args['url_encoded_fmt_stream_map'])
1391             if m_s is not None:
1392                 self.to_screen(u'%s: Encrypted signatures detected.' % video_id)
1393                 video_info['url_encoded_fmt_stream_map'] = [args['url_encoded_fmt_stream_map']]
1394             m_s = re_signature.search(args.get('adaptive_fmts', u''))
1395             if m_s is not None:
1396                 if 'adaptive_fmts' in video_info:
1397                     video_info['adaptive_fmts'][0] += ',' + args['adaptive_fmts']
1398                 else:
1399                     video_info['adaptive_fmts'] = [args['adaptive_fmts']]
1400         except ValueError:
1401             pass
1402
1403         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
1404             self.report_rtmp_download()
1405             video_url_list = [(None, video_info['conn'][0])]
1406         elif len(video_info.get('url_encoded_fmt_stream_map', [])) >= 1 or len(video_info.get('adaptive_fmts', [])) >= 1:
1407             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts',[''])[0]
1408             if 'rtmpe%3Dyes' in encoded_url_map:
1409                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
1410             url_map = {}
1411             for url_data_str in encoded_url_map.split(','):
1412                 url_data = compat_parse_qs(url_data_str)
1413                 if 'itag' in url_data and 'url' in url_data:
1414                     url = url_data['url'][0]
1415                     if 'sig' in url_data:
1416                         url += '&signature=' + url_data['sig'][0]
1417                     elif 's' in url_data:
1418                         encrypted_sig = url_data['s'][0]
1419                         if self._downloader.params.get('verbose'):
1420                             if age_gate:
1421                                 if player_url is None:
1422                                     player_version = 'unknown'
1423                                 else:
1424                                     player_version = self._search_regex(
1425                                         r'-(.+)\.swf$', player_url,
1426                                         u'flash player', fatal=False)
1427                                 player_desc = 'flash player %s' % player_version
1428                             else:
1429                                 player_version = self._search_regex(
1430                                     r'html5player-(.+?)\.js', video_webpage,
1431                                     'html5 player', fatal=False)
1432                                 player_desc = u'html5 player %s' % player_version
1433
1434                             parts_sizes = u'.'.join(compat_str(len(part)) for part in encrypted_sig.split('.'))
1435                             self.to_screen(u'encrypted signature length %d (%s), itag %s, %s' %
1436                                 (len(encrypted_sig), parts_sizes, url_data['itag'][0], player_desc))
1437
1438                         if not age_gate:
1439                             jsplayer_url_json = self._search_regex(
1440                                 r'"assets":.+?"js":\s*("[^"]+")',
1441                                 video_webpage, u'JS player URL')
1442                             player_url = json.loads(jsplayer_url_json)
1443
1444                         signature = self._decrypt_signature(
1445                             encrypted_sig, video_id, player_url, age_gate)
1446                         url += '&signature=' + signature
1447                     if 'ratebypass' not in url:
1448                         url += '&ratebypass=yes'
1449                     url_map[url_data['itag'][0]] = url
1450             video_url_list = self._get_video_url_list(url_map)
1451             if not video_url_list:
1452                 return
1453         elif video_info.get('hlsvp'):
1454             manifest_url = video_info['hlsvp'][0]
1455             url_map = self._extract_from_m3u8(manifest_url, video_id)
1456             video_url_list = self._get_video_url_list(url_map)
1457             if not video_url_list:
1458                 return
1459
1460         else:
1461             raise ExtractorError(u'no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1462
1463         results = []
1464         for itag, video_real_url in video_url_list:
1465             # Extension
1466             video_extension = self._video_extensions.get(itag, 'flv')
1467
1468             video_format = '{0} - {1}{2}'.format(itag if itag else video_extension,
1469                                               self._video_dimensions.get(itag, '???'),
1470                                               ' ('+self._special_itags[itag]+')' if itag in self._special_itags else '')
1471
1472             results.append({
1473                 'id':       video_id,
1474                 'url':      video_real_url,
1475                 'uploader': video_uploader,
1476                 'uploader_id': video_uploader_id,
1477                 'upload_date':  upload_date,
1478                 'title':    video_title,
1479                 'ext':      video_extension,
1480                 'format':   video_format,
1481                 'format_id': itag,
1482                 'thumbnail':    video_thumbnail,
1483                 'description':  video_description,
1484                 'player_url':   player_url,
1485                 'subtitles':    video_subtitles,
1486                 'duration':     video_duration,
1487                 'age_limit':    18 if age_gate else 0,
1488                 'annotations':  video_annotations,
1489                 'webpage_url': 'https://www.youtube.com/watch?v=%s' % video_id,
1490             })
1491         return results
1492
1493 class YoutubePlaylistIE(InfoExtractor):
1494     IE_DESC = u'YouTube.com playlists'
1495     _VALID_URL = r"""(?:
1496                         (?:https?://)?
1497                         (?:\w+\.)?
1498                         youtube\.com/
1499                         (?:
1500                            (?:course|view_play_list|my_playlists|artist|playlist|watch)
1501                            \? (?:.*?&)*? (?:p|a|list)=
1502                         |  p/
1503                         )
1504                         ((?:PL|EC|UU|FL)?[0-9A-Za-z-_]{10,})
1505                         .*
1506                      |
1507                         ((?:PL|EC|UU|FL)[0-9A-Za-z-_]{10,})
1508                      )"""
1509     _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/playlists/%s?max-results=%i&start-index=%i&v=2&alt=json&safeSearch=none'
1510     _MAX_RESULTS = 50
1511     IE_NAME = u'youtube:playlist'
1512
1513     @classmethod
1514     def suitable(cls, url):
1515         """Receives a URL and returns True if suitable for this IE."""
1516         return re.match(cls._VALID_URL, url, re.VERBOSE) is not None
1517
1518     def _real_extract(self, url):
1519         # Extract playlist id
1520         mobj = re.match(self._VALID_URL, url, re.VERBOSE)
1521         if mobj is None:
1522             raise ExtractorError(u'Invalid URL: %s' % url)
1523         playlist_id = mobj.group(1) or mobj.group(2)
1524
1525         # Check if it's a video-specific URL
1526         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
1527         if 'v' in query_dict:
1528             video_id = query_dict['v'][0]
1529             if self._downloader.params.get('noplaylist'):
1530                 self.to_screen(u'Downloading just video %s because of --no-playlist' % video_id)
1531                 return self.url_result('https://www.youtube.com/watch?v=' + video_id, 'Youtube')
1532             else:
1533                 self.to_screen(u'Downloading playlist PL%s - add --no-playlist to just download video %s' % (playlist_id, video_id))
1534
1535         # Download playlist videos from API
1536         videos = []
1537
1538         for page_num in itertools.count(1):
1539             start_index = self._MAX_RESULTS * (page_num - 1) + 1
1540             if start_index >= 1000:
1541                 self._downloader.report_warning(u'Max number of results reached')
1542                 break
1543             url = self._TEMPLATE_URL % (playlist_id, self._MAX_RESULTS, start_index)
1544             page = self._download_webpage(url, playlist_id, u'Downloading page #%s' % page_num)
1545
1546             try:
1547                 response = json.loads(page)
1548             except ValueError as err:
1549                 raise ExtractorError(u'Invalid JSON in API response: ' + compat_str(err))
1550
1551             if 'feed' not in response:
1552                 raise ExtractorError(u'Got a malformed response from YouTube API')
1553             playlist_title = response['feed']['title']['$t']
1554             if 'entry' not in response['feed']:
1555                 # Number of videos is a multiple of self._MAX_RESULTS
1556                 break
1557
1558             for entry in response['feed']['entry']:
1559                 index = entry['yt$position']['$t']
1560                 if 'media$group' in entry and 'yt$videoid' in entry['media$group']:
1561                     videos.append((
1562                         index,
1563                         'https://www.youtube.com/watch?v=' + entry['media$group']['yt$videoid']['$t']
1564                     ))
1565
1566         videos = [v[1] for v in sorted(videos)]
1567
1568         url_results = [self.url_result(vurl, 'Youtube') for vurl in videos]
1569         return [self.playlist_result(url_results, playlist_id, playlist_title)]
1570
1571
1572 class YoutubeChannelIE(InfoExtractor):
1573     IE_DESC = u'YouTube.com channels'
1574     _VALID_URL = r"^(?:https?://)?(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/([0-9A-Za-z_-]+)"
1575     _MORE_PAGES_INDICATOR = 'yt-uix-load-more'
1576     _MORE_PAGES_URL = 'http://www.youtube.com/c4_browse_ajax?action_load_more_videos=1&flow=list&paging=%s&view=0&sort=da&channel_id=%s'
1577     IE_NAME = u'youtube:channel'
1578
1579     def extract_videos_from_page(self, page):
1580         ids_in_page = []
1581         for mobj in re.finditer(r'href="/watch\?v=([0-9A-Za-z_-]+)&?', page):
1582             if mobj.group(1) not in ids_in_page:
1583                 ids_in_page.append(mobj.group(1))
1584         return ids_in_page
1585
1586     def _real_extract(self, url):
1587         # Extract channel id
1588         mobj = re.match(self._VALID_URL, url)
1589         if mobj is None:
1590             raise ExtractorError(u'Invalid URL: %s' % url)
1591
1592         # Download channel page
1593         channel_id = mobj.group(1)
1594         video_ids = []
1595
1596         # Download all channel pages using the json-based channel_ajax query
1597         for pagenum in itertools.count(1):
1598             url = self._MORE_PAGES_URL % (pagenum, channel_id)
1599             page = self._download_webpage(url, channel_id,
1600                                           u'Downloading page #%s' % pagenum)
1601
1602             page = json.loads(page)
1603
1604             ids_in_page = self.extract_videos_from_page(page['content_html'])
1605             video_ids.extend(ids_in_page)
1606
1607             if self._MORE_PAGES_INDICATOR not in page['load_more_widget_html']:
1608                 break
1609
1610         self._downloader.to_screen(u'[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids)))
1611
1612         urls = ['http://www.youtube.com/watch?v=%s' % id for id in video_ids]
1613         url_entries = [self.url_result(eurl, 'Youtube') for eurl in urls]
1614         return [self.playlist_result(url_entries, channel_id)]
1615
1616
1617 class YoutubeUserIE(InfoExtractor):
1618     IE_DESC = u'YouTube.com user videos (URL or "ytuser" keyword)'
1619     _VALID_URL = r'(?:(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:user/)?(?!(?:attribution_link|watch)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)([A-Za-z0-9_-]+)'
1620     _TEMPLATE_URL = 'http://gdata.youtube.com/feeds/api/users/%s'
1621     _GDATA_PAGE_SIZE = 50
1622     _GDATA_URL = 'http://gdata.youtube.com/feeds/api/users/%s/uploads?max-results=%d&start-index=%d&alt=json'
1623     IE_NAME = u'youtube:user'
1624
1625     @classmethod
1626     def suitable(cls, url):
1627         # Don't return True if the url can be extracted with other youtube
1628         # extractor, the regex would is too permissive and it would match.
1629         other_ies = iter(klass for (name, klass) in globals().items() if name.endswith('IE') and klass is not cls)
1630         if any(ie.suitable(url) for ie in other_ies): return False
1631         else: return super(YoutubeUserIE, cls).suitable(url)
1632
1633     def _real_extract(self, url):
1634         # Extract username
1635         mobj = re.match(self._VALID_URL, url)
1636         if mobj is None:
1637             raise ExtractorError(u'Invalid URL: %s' % url)
1638
1639         username = mobj.group(1)
1640
1641         # Download video ids using YouTube Data API. Result size per
1642         # query is limited (currently to 50 videos) so we need to query
1643         # page by page until there are no video ids - it means we got
1644         # all of them.
1645
1646         video_ids = []
1647
1648         for pagenum in itertools.count(0):
1649             start_index = pagenum * self._GDATA_PAGE_SIZE + 1
1650
1651             gdata_url = self._GDATA_URL % (username, self._GDATA_PAGE_SIZE, start_index)
1652             page = self._download_webpage(gdata_url, username,
1653                                           u'Downloading video ids from %d to %d' % (start_index, start_index + self._GDATA_PAGE_SIZE))
1654
1655             try:
1656                 response = json.loads(page)
1657             except ValueError as err:
1658                 raise ExtractorError(u'Invalid JSON in API response: ' + compat_str(err))
1659             if 'entry' not in response['feed']:
1660                 # Number of videos is a multiple of self._MAX_RESULTS
1661                 break
1662
1663             # Extract video identifiers
1664             ids_in_page = []
1665             for entry in response['feed']['entry']:
1666                 ids_in_page.append(entry['id']['$t'].split('/')[-1])
1667             video_ids.extend(ids_in_page)
1668
1669             # A little optimization - if current page is not
1670             # "full", ie. does not contain PAGE_SIZE video ids then
1671             # we can assume that this page is the last one - there
1672             # are no more ids on further pages - no need to query
1673             # again.
1674
1675             if len(ids_in_page) < self._GDATA_PAGE_SIZE:
1676                 break
1677
1678         urls = ['http://www.youtube.com/watch?v=%s' % video_id for video_id in video_ids]
1679         url_results = [self.url_result(rurl, 'Youtube') for rurl in urls]
1680         return [self.playlist_result(url_results, playlist_title = username)]
1681
1682 class YoutubeSearchIE(SearchInfoExtractor):
1683     IE_DESC = u'YouTube.com searches'
1684     _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc'
1685     _MAX_RESULTS = 1000
1686     IE_NAME = u'youtube:search'
1687     _SEARCH_KEY = 'ytsearch'
1688
1689     def report_download_page(self, query, pagenum):
1690         """Report attempt to download search page with given number."""
1691         self._downloader.to_screen(u'[youtube] query "%s": Downloading page %s' % (query, pagenum))
1692
1693     def _get_n_results(self, query, n):
1694         """Get a specified number of results for a query"""
1695
1696         video_ids = []
1697         pagenum = 0
1698         limit = n
1699
1700         while (50 * pagenum) < limit:
1701             self.report_download_page(query, pagenum+1)
1702             result_url = self._API_URL % (compat_urllib_parse.quote_plus(query), (50*pagenum)+1)
1703             request = compat_urllib_request.Request(result_url)
1704             try:
1705                 data = compat_urllib_request.urlopen(request).read().decode('utf-8')
1706             except (compat_urllib_error.URLError, compat_http_client.HTTPException, socket.error) as err:
1707                 raise ExtractorError(u'Unable to download API page: %s' % compat_str(err))
1708             api_response = json.loads(data)['data']
1709
1710             if not 'items' in api_response:
1711                 raise ExtractorError(u'[youtube] No video results')
1712
1713             new_ids = list(video['id'] for video in api_response['items'])
1714             video_ids += new_ids
1715
1716             limit = min(n, api_response['totalItems'])
1717             pagenum += 1
1718
1719         if len(video_ids) > n:
1720             video_ids = video_ids[:n]
1721         videos = [self.url_result('http://www.youtube.com/watch?v=%s' % id, 'Youtube') for id in video_ids]
1722         return self.playlist_result(videos, query)
1723
1724 class YoutubeSearchDateIE(YoutubeSearchIE):
1725     _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc&orderby=published'
1726     _SEARCH_KEY = 'ytsearchdate'
1727     IE_DESC = u'YouTube.com searches, newest videos first'
1728
1729 class YoutubeShowIE(InfoExtractor):
1730     IE_DESC = u'YouTube.com (multi-season) shows'
1731     _VALID_URL = r'https?://www\.youtube\.com/show/(.*)'
1732     IE_NAME = u'youtube:show'
1733
1734     def _real_extract(self, url):
1735         mobj = re.match(self._VALID_URL, url)
1736         show_name = mobj.group(1)
1737         webpage = self._download_webpage(url, show_name, u'Downloading show webpage')
1738         # There's one playlist for each season of the show
1739         m_seasons = list(re.finditer(r'href="(/playlist\?list=.*?)"', webpage))
1740         self.to_screen(u'%s: Found %s seasons' % (show_name, len(m_seasons)))
1741         return [self.url_result('https://www.youtube.com' + season.group(1), 'YoutubePlaylist') for season in m_seasons]
1742
1743
1744 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
1745     """
1746     Base class for extractors that fetch info from
1747     http://www.youtube.com/feed_ajax
1748     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
1749     """
1750     _LOGIN_REQUIRED = True
1751     _PAGING_STEP = 30
1752     # use action_load_personal_feed instead of action_load_system_feed
1753     _PERSONAL_FEED = False
1754
1755     @property
1756     def _FEED_TEMPLATE(self):
1757         action = 'action_load_system_feed'
1758         if self._PERSONAL_FEED:
1759             action = 'action_load_personal_feed'
1760         return 'http://www.youtube.com/feed_ajax?%s=1&feed_name=%s&paging=%%s' % (action, self._FEED_NAME)
1761
1762     @property
1763     def IE_NAME(self):
1764         return u'youtube:%s' % self._FEED_NAME
1765
1766     def _real_initialize(self):
1767         self._login()
1768
1769     def _real_extract(self, url):
1770         feed_entries = []
1771         # The step argument is available only in 2.7 or higher
1772         for i in itertools.count(0):
1773             paging = i*self._PAGING_STEP
1774             info = self._download_webpage(self._FEED_TEMPLATE % paging,
1775                                           u'%s feed' % self._FEED_NAME,
1776                                           u'Downloading page %s' % i)
1777             info = json.loads(info)
1778             feed_html = info['feed_html']
1779             m_ids = re.finditer(r'"/watch\?v=(.*?)["&]', feed_html)
1780             ids = orderedSet(m.group(1) for m in m_ids)
1781             feed_entries.extend(self.url_result(id, 'Youtube') for id in ids)
1782             if info['paging'] is None:
1783                 break
1784         return self.playlist_result(feed_entries, playlist_title=self._PLAYLIST_TITLE)
1785
1786 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
1787     IE_DESC = u'YouTube.com subscriptions feed, "ytsubs" keyword(requires authentication)'
1788     _VALID_URL = r'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
1789     _FEED_NAME = 'subscriptions'
1790     _PLAYLIST_TITLE = u'Youtube Subscriptions'
1791
1792 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
1793     IE_DESC = u'YouTube.com recommended videos, "ytrec" keyword (requires authentication)'
1794     _VALID_URL = r'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?'
1795     _FEED_NAME = 'recommended'
1796     _PLAYLIST_TITLE = u'Youtube Recommended videos'
1797
1798 class YoutubeWatchLaterIE(YoutubeFeedsInfoExtractor):
1799     IE_DESC = u'Youtube watch later list, "ytwatchlater" keyword (requires authentication)'
1800     _VALID_URL = r'https?://www\.youtube\.com/feed/watch_later|:ytwatchlater'
1801     _FEED_NAME = 'watch_later'
1802     _PLAYLIST_TITLE = u'Youtube Watch Later'
1803     _PAGING_STEP = 100
1804     _PERSONAL_FEED = True
1805
1806 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
1807     IE_NAME = u'youtube:favorites'
1808     IE_DESC = u'YouTube.com favourite videos, "ytfav" keyword (requires authentication)'
1809     _VALID_URL = r'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
1810     _LOGIN_REQUIRED = True
1811
1812     def _real_extract(self, url):
1813         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
1814         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, u'favourites playlist id')
1815         return self.url_result(playlist_id, 'YoutubePlaylist')
1816
1817
1818 class YoutubeTruncatedURLIE(InfoExtractor):
1819     IE_NAME = 'youtube:truncated_url'
1820     IE_DESC = False  # Do not list
1821     _VALID_URL = r'(?:https?://)?[^/]+/watch\?feature=[a-z_]+$'
1822
1823     def _real_extract(self, url):
1824         raise ExtractorError(
1825             u'Did you forget to quote the URL? Remember that & is a meta '
1826             u'character in most shells, so you want to put the URL in quotes, '
1827             u'like  youtube-dl '
1828             u'\'http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc\''
1829             u' (or simply  youtube-dl BaW_jenozKc  ).',
1830             expected=True)