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