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