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