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