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