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