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