Merge branch 'fstirlitz-filmon'
[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 random
10 import re
11 import time
12 import traceback
13
14 from .common import InfoExtractor, SearchInfoExtractor
15 from ..jsinterp import JSInterpreter
16 from ..swfinterp import SWFInterpreter
17 from ..compat import (
18     compat_chr,
19     compat_parse_qs,
20     compat_urllib_parse_unquote,
21     compat_urllib_parse_unquote_plus,
22     compat_urllib_parse_urlencode,
23     compat_urllib_parse_urlparse,
24     compat_urlparse,
25     compat_str,
26 )
27 from ..utils import (
28     clean_html,
29     error_to_compat_str,
30     ExtractorError,
31     float_or_none,
32     get_element_by_attribute,
33     get_element_by_id,
34     int_or_none,
35     mimetype2ext,
36     orderedSet,
37     parse_duration,
38     remove_quotes,
39     remove_start,
40     sanitized_Request,
41     smuggle_url,
42     str_to_int,
43     try_get,
44     unescapeHTML,
45     unified_strdate,
46     unsmuggle_url,
47     uppercase_escape,
48     urlencode_postdata,
49     ISO3166Utils,
50 )
51
52
53 class YoutubeBaseInfoExtractor(InfoExtractor):
54     """Provide base functions for Youtube extractors"""
55     _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
56     _TWOFACTOR_URL = 'https://accounts.google.com/signin/challenge'
57     _PASSWORD_CHALLENGE_URL = 'https://accounts.google.com/signin/challenge/sl/password'
58     _NETRC_MACHINE = 'youtube'
59     # If True it will raise an error if no login info is provided
60     _LOGIN_REQUIRED = False
61
62     def _set_language(self):
63         self._set_cookie(
64             '.youtube.com', 'PREF', 'f1=50000000&hl=en',
65             # YouTube sets the expire time to about two months
66             expire_time=time.time() + 2 * 30 * 24 * 3600)
67
68     def _ids_to_results(self, ids):
69         return [
70             self.url_result(vid_id, 'Youtube', video_id=vid_id)
71             for vid_id in ids]
72
73     def _login(self):
74         """
75         Attempt to log in to YouTube.
76         True is returned if successful or skipped.
77         False is returned if login failed.
78
79         If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
80         """
81         (username, password) = self._get_login_info()
82         # No authentication to be performed
83         if username is None:
84             if self._LOGIN_REQUIRED:
85                 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
86             return True
87
88         login_page = self._download_webpage(
89             self._LOGIN_URL, None,
90             note='Downloading login page',
91             errnote='unable to fetch login page', fatal=False)
92         if login_page is False:
93             return
94
95         login_form = self._hidden_inputs(login_page)
96
97         login_form.update({
98             'checkConnection': 'youtube',
99             'Email': username,
100             'Passwd': password,
101         })
102
103         login_results = self._download_webpage(
104             self._PASSWORD_CHALLENGE_URL, None,
105             note='Logging in', errnote='unable to log in', fatal=False,
106             data=urlencode_postdata(login_form))
107         if login_results is False:
108             return False
109
110         error_msg = self._html_search_regex(
111             r'<[^>]+id="errormsg_0_Passwd"[^>]*>([^<]+)<',
112             login_results, 'error message', default=None)
113         if error_msg:
114             raise ExtractorError('Unable to login: %s' % error_msg, expected=True)
115
116         if re.search(r'id="errormsg_0_Passwd"', login_results) is not None:
117             raise ExtractorError('Please use your account password and a two-factor code instead of an application-specific password.', expected=True)
118
119         # Two-Factor
120         # TODO add SMS and phone call support - these require making a request and then prompting the user
121
122         if re.search(r'(?i)<form[^>]+id="challenge"', login_results) is not None:
123             tfa_code = self._get_tfa_info('2-step verification code')
124
125             if not tfa_code:
126                 self._downloader.report_warning(
127                     'Two-factor authentication required. Provide it either interactively or with --twofactor <code>'
128                     '(Note that only TOTP (Google Authenticator App) codes work at this time.)')
129                 return False
130
131             tfa_code = remove_start(tfa_code, 'G-')
132
133             tfa_form_strs = self._form_hidden_inputs('challenge', login_results)
134
135             tfa_form_strs.update({
136                 'Pin': tfa_code,
137                 'TrustDevice': 'on',
138             })
139
140             tfa_data = urlencode_postdata(tfa_form_strs)
141
142             tfa_req = sanitized_Request(self._TWOFACTOR_URL, tfa_data)
143             tfa_results = self._download_webpage(
144                 tfa_req, None,
145                 note='Submitting TFA code', errnote='unable to submit tfa', fatal=False)
146
147             if tfa_results is False:
148                 return False
149
150             if re.search(r'(?i)<form[^>]+id="challenge"', tfa_results) is not None:
151                 self._downloader.report_warning('Two-factor code expired or invalid. Please try again, or use a one-use backup code instead.')
152                 return False
153             if re.search(r'(?i)<form[^>]+id="gaia_loginform"', tfa_results) is not None:
154                 self._downloader.report_warning('unable to log in - did the page structure change?')
155                 return False
156             if re.search(r'smsauth-interstitial-reviewsettings', tfa_results) is not None:
157                 self._downloader.report_warning('Your Google account has a security notice. Please log in on your web browser, resolve the notice, and try again.')
158                 return False
159
160         if re.search(r'(?i)<form[^>]+id="gaia_loginform"', login_results) is not None:
161             self._downloader.report_warning('unable to log in: bad username or password')
162             return False
163         return True
164
165     def _real_initialize(self):
166         if self._downloader is None:
167             return
168         self._set_language()
169         if not self._login():
170             return
171
172
173 class YoutubeEntryListBaseInfoExtractor(YoutubeBaseInfoExtractor):
174     # Extract entries from page with "Load more" button
175     def _entries(self, page, playlist_id):
176         more_widget_html = content_html = page
177         for page_num in itertools.count(1):
178             for entry in self._process_page(content_html):
179                 yield entry
180
181             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
182             if not mobj:
183                 break
184
185             more = self._download_json(
186                 'https://youtube.com/%s' % mobj.group('more'), playlist_id,
187                 'Downloading page #%s' % page_num,
188                 transform_source=uppercase_escape)
189             content_html = more['content_html']
190             if not content_html.strip():
191                 # Some webpages show a "Load more" button but they don't
192                 # have more videos
193                 break
194             more_widget_html = more['load_more_widget_html']
195
196
197 class YoutubePlaylistBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
198     def _process_page(self, content):
199         for video_id, video_title in self.extract_videos_from_page(content):
200             yield self.url_result(video_id, 'Youtube', video_id, video_title)
201
202     def extract_videos_from_page(self, page):
203         ids_in_page = []
204         titles_in_page = []
205         for mobj in re.finditer(self._VIDEO_RE, page):
206             # The link with index 0 is not the first video of the playlist (not sure if still actual)
207             if 'index' in mobj.groupdict() and mobj.group('id') == '0':
208                 continue
209             video_id = mobj.group('id')
210             video_title = unescapeHTML(mobj.group('title'))
211             if video_title:
212                 video_title = video_title.strip()
213             try:
214                 idx = ids_in_page.index(video_id)
215                 if video_title and not titles_in_page[idx]:
216                     titles_in_page[idx] = video_title
217             except ValueError:
218                 ids_in_page.append(video_id)
219                 titles_in_page.append(video_title)
220         return zip(ids_in_page, titles_in_page)
221
222
223 class YoutubePlaylistsBaseInfoExtractor(YoutubeEntryListBaseInfoExtractor):
224     def _process_page(self, content):
225         for playlist_id in orderedSet(re.findall(
226                 r'<h3[^>]+class="[^"]*yt-lockup-title[^"]*"[^>]*><a[^>]+href="/?playlist\?list=([0-9A-Za-z-_]{10,})"',
227                 content)):
228             yield self.url_result(
229                 'https://www.youtube.com/playlist?list=%s' % playlist_id, 'YoutubePlaylist')
230
231     def _real_extract(self, url):
232         playlist_id = self._match_id(url)
233         webpage = self._download_webpage(url, playlist_id)
234         title = self._og_search_title(webpage, fatal=False)
235         return self.playlist_result(self._entries(webpage, playlist_id), playlist_id, title)
236
237
238 class YoutubeIE(YoutubeBaseInfoExtractor):
239     IE_DESC = 'YouTube.com'
240     _VALID_URL = r"""(?x)^
241                      (
242                          (?:https?://|//)                                    # http(s):// or protocol-independent URL
243                          (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
244                             (?:www\.)?deturl\.com/www\.youtube\.com/|
245                             (?:www\.)?pwnyoutube\.com/|
246                             (?:www\.)?yourepeat\.com/|
247                             tube\.majestyc\.net/|
248                             youtube\.googleapis\.com/)                        # the various hostnames, with wildcard subdomains
249                          (?:.*?\#/)?                                          # handle anchor (#/) redirect urls
250                          (?:                                                  # the various things that can precede the ID:
251                              (?:(?:v|embed|e)/(?!videoseries))                # v/ or embed/ or e/
252                              |(?:                                             # or the v= param in all its forms
253                                  (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)?  # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
254                                  (?:\?|\#!?)                                  # the params delimiter ? or # or #!
255                                  (?:.*?[&;])??                                # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&amp;v=V36LpHqtcDY)
256                                  v=
257                              )
258                          ))
259                          |(?:
260                             youtu\.be|                                        # just youtu.be/xxxx
261                             vid\.plus|                                        # or vid.plus/xxxx
262                             zwearz\.com/watch|                                # or zwearz.com/watch/xxxx
263                          )/
264                          |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
265                          )
266                      )?                                                       # all until now is optional -> you can pass the naked ID
267                      ([0-9A-Za-z_-]{11})                                      # here is it! the YouTube video ID
268                      (?!.*?\blist=)                                            # combined list/video URLs are handled by the playlist IE
269                      (?(1).+)?                                                # if we found the ID, everything can follow
270                      $"""
271     _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
272     _formats = {
273         '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
274         '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
275         '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
276         '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
277         '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
278         '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
279         '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
280         '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
281         # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
282         '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
283         '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
284         '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
285         '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
286         '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
287         '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
288         '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
289         '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
290         '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
291
292
293         # 3D videos
294         '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
295         '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
296         '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
297         '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
298         '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
299         '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
300         '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
301
302         # Apple HTTP Live Streaming
303         '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
304         '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
305         '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
306         '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
307         '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
308         '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
309         '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
310         '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
311
312         # DASH mp4 video
313         '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
314         '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
315         '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
316         '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
317         '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
318         '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},  # Height can vary (https://github.com/rg3/youtube-dl/issues/4559)
319         '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
320         '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
321         '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
322         '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60, 'preference': -40},
323         '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60, 'preference': -40},
324         '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264', 'preference': -40},
325
326         # Dash mp4 audio
327         '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'preference': -50, 'container': 'm4a_dash'},
328         '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'preference': -50, 'container': 'm4a_dash'},
329         '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'preference': -50, 'container': 'm4a_dash'},
330         '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'preference': -50, 'container': 'm4a_dash'},
331         '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'preference': -50, 'container': 'm4a_dash'},
332
333         # Dash webm
334         '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
335         '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
336         '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
337         '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
338         '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
339         '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8', 'preference': -40},
340         '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9', 'preference': -40},
341         '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
342         '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
343         '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
344         '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
345         '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
346         '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
347         '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
348         '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
349         # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
350         '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
351         '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60, 'preference': -40},
352         '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60, 'preference': -40},
353         '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60, 'preference': -40},
354         '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'preference': -40},
355         '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60, 'preference': -40},
356
357         # Dash webm audio
358         '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128, 'preference': -50},
359         '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256, 'preference': -50},
360
361         # Dash webm audio with opus inside
362         '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50, 'preference': -50},
363         '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70, 'preference': -50},
364         '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160, 'preference': -50},
365
366         # RTMP (unnamed)
367         '_rtmp': {'protocol': 'rtmp'},
368     }
369     _SUBTITLE_FORMATS = ('ttml', 'vtt')
370
371     IE_NAME = 'youtube'
372     _TESTS = [
373         {
374             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
375             'info_dict': {
376                 'id': 'BaW_jenozKc',
377                 'ext': 'mp4',
378                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
379                 'uploader': 'Philipp Hagemeister',
380                 'uploader_id': 'phihag',
381                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
382                 'upload_date': '20121002',
383                 'license': 'Standard YouTube License',
384                 '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 .',
385                 'categories': ['Science & Technology'],
386                 'tags': ['youtube-dl'],
387                 'duration': 10,
388                 'like_count': int,
389                 'dislike_count': int,
390                 'start_time': 1,
391                 'end_time': 9,
392             }
393         },
394         {
395             'url': 'https://www.youtube.com/watch?v=UxxajLWwzqY',
396             'note': 'Test generic use_cipher_signature video (#897)',
397             'info_dict': {
398                 'id': 'UxxajLWwzqY',
399                 'ext': 'mp4',
400                 'upload_date': '20120506',
401                 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]',
402                 'alt_title': 'I Love It (feat. Charli XCX)',
403                 'description': 'md5:f3ceb5ef83a08d95b9d146f973157cc8',
404                 'tags': ['Icona Pop i love it', 'sweden', 'pop music', 'big beat records', 'big beat', 'charli',
405                          'xcx', 'charli xcx', 'girls', 'hbo', 'i love it', "i don't care", 'icona', 'pop',
406                          'iconic ep', 'iconic', 'love', 'it'],
407                 'duration': 180,
408                 'uploader': 'Icona Pop',
409                 'uploader_id': 'IconaPop',
410                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IconaPop',
411                 'license': 'Standard YouTube License',
412                 'creator': 'Icona Pop',
413             }
414         },
415         {
416             'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ',
417             'note': 'Test VEVO video with age protection (#956)',
418             'info_dict': {
419                 'id': '07FYdnEawAQ',
420                 'ext': 'mp4',
421                 'upload_date': '20130703',
422                 'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
423                 'alt_title': 'Tunnel Vision',
424                 'description': 'md5:64249768eec3bc4276236606ea996373',
425                 'duration': 419,
426                 'uploader': 'justintimberlakeVEVO',
427                 'uploader_id': 'justintimberlakeVEVO',
428                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/justintimberlakeVEVO',
429                 'license': 'Standard YouTube License',
430                 'creator': 'Justin Timberlake',
431                 'age_limit': 18,
432             }
433         },
434         {
435             'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
436             'note': 'Embed-only video (#1746)',
437             'info_dict': {
438                 'id': 'yZIXLfi8CZQ',
439                 'ext': 'mp4',
440                 'upload_date': '20120608',
441                 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
442                 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
443                 'uploader': 'SET India',
444                 'uploader_id': 'setindia',
445                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/setindia',
446                 'license': 'Standard YouTube License',
447                 'age_limit': 18,
448             }
449         },
450         {
451             'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=UxxajLWwzqY',
452             'note': 'Use the first video ID in the URL',
453             'info_dict': {
454                 'id': 'BaW_jenozKc',
455                 'ext': 'mp4',
456                 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
457                 'uploader': 'Philipp Hagemeister',
458                 'uploader_id': 'phihag',
459                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
460                 'upload_date': '20121002',
461                 'license': 'Standard YouTube License',
462                 '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 .',
463                 'categories': ['Science & Technology'],
464                 'tags': ['youtube-dl'],
465                 'duration': 10,
466                 'like_count': int,
467                 'dislike_count': int,
468             },
469             'params': {
470                 'skip_download': True,
471             },
472         },
473         {
474             'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
475             'note': '256k DASH audio (format 141) via DASH manifest',
476             'info_dict': {
477                 'id': 'a9LDPn-MO4I',
478                 'ext': 'm4a',
479                 'upload_date': '20121002',
480                 'uploader_id': '8KVIDEO',
481                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
482                 'description': '',
483                 'uploader': '8KVIDEO',
484                 'license': 'Standard YouTube License',
485                 'title': 'UHDTV TEST 8K VIDEO.mp4'
486             },
487             'params': {
488                 'youtube_include_dash_manifest': True,
489                 'format': '141',
490             },
491             'skip': 'format 141 not served anymore',
492         },
493         # DASH manifest with encrypted signature
494         {
495             'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
496             'info_dict': {
497                 'id': 'IB3lcPjvWLA',
498                 'ext': 'm4a',
499                 'title': 'Afrojack, Spree Wilson - The Spark ft. Spree Wilson',
500                 'description': 'md5:12e7067fa6735a77bdcbb58cb1187d2d',
501                 'duration': 244,
502                 'uploader': 'AfrojackVEVO',
503                 'uploader_id': 'AfrojackVEVO',
504                 'upload_date': '20131011',
505                 'license': 'Standard YouTube License',
506             },
507             'params': {
508                 'youtube_include_dash_manifest': True,
509                 'format': '141/bestaudio[ext=m4a]',
510             },
511         },
512         # JS player signature function name containing $
513         {
514             'url': 'https://www.youtube.com/watch?v=nfWlot6h_JM',
515             'info_dict': {
516                 'id': 'nfWlot6h_JM',
517                 'ext': 'm4a',
518                 'title': 'Taylor Swift - Shake It Off',
519                 'alt_title': 'Shake It Off',
520                 'description': 'md5:95f66187cd7c8b2c13eb78e1223b63c3',
521                 'duration': 242,
522                 'uploader': 'TaylorSwiftVEVO',
523                 'uploader_id': 'TaylorSwiftVEVO',
524                 'upload_date': '20140818',
525                 'license': 'Standard YouTube License',
526                 'creator': 'Taylor Swift',
527             },
528             'params': {
529                 'youtube_include_dash_manifest': True,
530                 'format': '141/bestaudio[ext=m4a]',
531             },
532         },
533         # Controversy video
534         {
535             'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
536             'info_dict': {
537                 'id': 'T4XJQO3qol8',
538                 'ext': 'mp4',
539                 'duration': 219,
540                 'upload_date': '20100909',
541                 'uploader': 'The Amazing Atheist',
542                 'uploader_id': 'TheAmazingAtheist',
543                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheAmazingAtheist',
544                 'license': 'Standard YouTube License',
545                 'title': 'Burning Everyone\'s Koran',
546                 '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',
547             }
548         },
549         # Normal age-gate video (No vevo, embed allowed)
550         {
551             'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
552             'info_dict': {
553                 'id': 'HtVdAasjOgU',
554                 'ext': 'mp4',
555                 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
556                 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
557                 'duration': 142,
558                 'uploader': 'The Witcher',
559                 'uploader_id': 'WitcherGame',
560                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame',
561                 'upload_date': '20140605',
562                 'license': 'Standard YouTube License',
563                 'age_limit': 18,
564             },
565         },
566         # Age-gate video with encrypted signature
567         {
568             'url': 'https://www.youtube.com/watch?v=6kLq3WMV1nU',
569             'info_dict': {
570                 'id': '6kLq3WMV1nU',
571                 'ext': 'mp4',
572                 'title': 'Dedication To My Ex (Miss That) (Lyric Video)',
573                 'description': 'md5:33765bb339e1b47e7e72b5490139bb41',
574                 'duration': 247,
575                 'uploader': 'LloydVEVO',
576                 'uploader_id': 'LloydVEVO',
577                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/LloydVEVO',
578                 'upload_date': '20110629',
579                 'license': 'Standard YouTube License',
580                 'age_limit': 18,
581             },
582         },
583         # video_info is None (https://github.com/rg3/youtube-dl/issues/4421)
584         {
585             'url': '__2ABJjxzNo',
586             'info_dict': {
587                 'id': '__2ABJjxzNo',
588                 'ext': 'mp4',
589                 'duration': 266,
590                 'upload_date': '20100430',
591                 'uploader_id': 'deadmau5',
592                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/deadmau5',
593                 'creator': 'deadmau5',
594                 'description': 'md5:12c56784b8032162bb936a5f76d55360',
595                 'uploader': 'deadmau5',
596                 'license': 'Standard YouTube License',
597                 'title': 'Deadmau5 - Some Chords (HD)',
598                 'alt_title': 'Some Chords',
599             },
600             'expected_warnings': [
601                 'DASH manifest missing',
602             ]
603         },
604         # Olympics (https://github.com/rg3/youtube-dl/issues/4431)
605         {
606             'url': 'lqQg6PlCWgI',
607             'info_dict': {
608                 'id': 'lqQg6PlCWgI',
609                 'ext': 'mp4',
610                 'duration': 6085,
611                 'upload_date': '20150827',
612                 'uploader_id': 'olympic',
613                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic',
614                 'license': 'Standard YouTube License',
615                 'description': 'HO09  - Women -  GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games',
616                 'uploader': 'Olympic',
617                 'title': 'Hockey - Women -  GER-AUS - London 2012 Olympic Games',
618             },
619             'params': {
620                 'skip_download': 'requires avconv',
621             }
622         },
623         # Non-square pixels
624         {
625             'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
626             'info_dict': {
627                 'id': '_b-2C3KPAM0',
628                 'ext': 'mp4',
629                 'stretched_ratio': 16 / 9.,
630                 'duration': 85,
631                 'upload_date': '20110310',
632                 'uploader_id': 'AllenMeow',
633                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/AllenMeow',
634                 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
635                 'uploader': '孫艾倫',
636                 'license': 'Standard YouTube License',
637                 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
638             },
639         },
640         # url_encoded_fmt_stream_map is empty string
641         {
642             'url': 'qEJwOuvDf7I',
643             'info_dict': {
644                 'id': 'qEJwOuvDf7I',
645                 'ext': 'webm',
646                 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
647                 'description': '',
648                 'upload_date': '20150404',
649                 'uploader_id': 'spbelect',
650                 'uploader': 'Наблюдатели Петербурга',
651             },
652             'params': {
653                 'skip_download': 'requires avconv',
654             },
655             'skip': 'This live event has ended.',
656         },
657         # Extraction from multiple DASH manifests (https://github.com/rg3/youtube-dl/pull/6097)
658         {
659             'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
660             'info_dict': {
661                 'id': 'FIl7x6_3R5Y',
662                 'ext': 'mp4',
663                 'title': 'md5:7b81415841e02ecd4313668cde88737a',
664                 'description': 'md5:116377fd2963b81ec4ce64b542173306',
665                 'duration': 220,
666                 'upload_date': '20150625',
667                 'uploader_id': 'dorappi2000',
668                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/dorappi2000',
669                 'uploader': 'dorappi2000',
670                 'license': 'Standard YouTube License',
671                 'formats': 'mincount:32',
672             },
673         },
674         # DASH manifest with segment_list
675         {
676             'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
677             'md5': '8ce563a1d667b599d21064e982ab9e31',
678             'info_dict': {
679                 'id': 'CsmdDsKjzN8',
680                 'ext': 'mp4',
681                 'upload_date': '20150501',  # According to '<meta itemprop="datePublished"', but in other places it's 20150510
682                 'uploader': 'Airtek',
683                 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
684                 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
685                 'license': 'Standard YouTube License',
686                 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
687             },
688             'params': {
689                 'youtube_include_dash_manifest': True,
690                 'format': '135',  # bestvideo
691             },
692             'skip': 'This live event has ended.',
693         },
694         {
695             # Multifeed videos (multiple cameras), URL is for Main Camera
696             'url': 'https://www.youtube.com/watch?v=jqWvoWXjCVs',
697             'info_dict': {
698                 'id': 'jqWvoWXjCVs',
699                 'title': 'teamPGP: Rocket League Noob Stream',
700                 'description': 'md5:dc7872fb300e143831327f1bae3af010',
701             },
702             'playlist': [{
703                 'info_dict': {
704                     'id': 'jqWvoWXjCVs',
705                     'ext': 'mp4',
706                     'title': 'teamPGP: Rocket League Noob Stream (Main Camera)',
707                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
708                     'duration': 7335,
709                     'upload_date': '20150721',
710                     'uploader': 'Beer Games Beer',
711                     'uploader_id': 'beergamesbeer',
712                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
713                     'license': 'Standard YouTube License',
714                 },
715             }, {
716                 'info_dict': {
717                     'id': '6h8e8xoXJzg',
718                     'ext': 'mp4',
719                     'title': 'teamPGP: Rocket League Noob Stream (kreestuh)',
720                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
721                     'duration': 7337,
722                     'upload_date': '20150721',
723                     'uploader': 'Beer Games Beer',
724                     'uploader_id': 'beergamesbeer',
725                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
726                     'license': 'Standard YouTube License',
727                 },
728             }, {
729                 'info_dict': {
730                     'id': 'PUOgX5z9xZw',
731                     'ext': 'mp4',
732                     'title': 'teamPGP: Rocket League Noob Stream (grizzle)',
733                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
734                     'duration': 7337,
735                     'upload_date': '20150721',
736                     'uploader': 'Beer Games Beer',
737                     'uploader_id': 'beergamesbeer',
738                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
739                     'license': 'Standard YouTube License',
740                 },
741             }, {
742                 'info_dict': {
743                     'id': 'teuwxikvS5k',
744                     'ext': 'mp4',
745                     'title': 'teamPGP: Rocket League Noob Stream (zim)',
746                     'description': 'md5:dc7872fb300e143831327f1bae3af010',
747                     'duration': 7334,
748                     'upload_date': '20150721',
749                     'uploader': 'Beer Games Beer',
750                     'uploader_id': 'beergamesbeer',
751                     'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
752                     'license': 'Standard YouTube License',
753                 },
754             }],
755             'params': {
756                 'skip_download': True,
757             },
758         },
759         {
760             # Multifeed video with comma in title (see https://github.com/rg3/youtube-dl/issues/8536)
761             'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
762             'info_dict': {
763                 'id': 'gVfLd0zydlo',
764                 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
765             },
766             'playlist_count': 2,
767             'skip': 'Not multifeed anymore',
768         },
769         {
770             'url': 'https://vid.plus/FlRa-iH7PGw',
771             'only_matching': True,
772         },
773         {
774             'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
775             'only_matching': True,
776         },
777         {
778             # Title with JS-like syntax "};" (see https://github.com/rg3/youtube-dl/issues/7468)
779             # Also tests cut-off URL expansion in video description (see
780             # https://github.com/rg3/youtube-dl/issues/1892,
781             # https://github.com/rg3/youtube-dl/issues/8164)
782             'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
783             'info_dict': {
784                 'id': 'lsguqyKfVQg',
785                 'ext': 'mp4',
786                 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
787                 'alt_title': 'Dark Walk',
788                 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
789                 'duration': 133,
790                 'upload_date': '20151119',
791                 'uploader_id': 'IronSoulElf',
792                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IronSoulElf',
793                 'uploader': 'IronSoulElf',
794                 'license': 'Standard YouTube License',
795                 'creator': 'Todd Haberman, Daniel Law Heath & Aaron Kaplan',
796             },
797             'params': {
798                 'skip_download': True,
799             },
800         },
801         {
802             # Tags with '};' (see https://github.com/rg3/youtube-dl/issues/7468)
803             'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
804             'only_matching': True,
805         },
806         {
807             # Video with yt:stretch=17:0
808             'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
809             'info_dict': {
810                 'id': 'Q39EVAstoRM',
811                 'ext': 'mp4',
812                 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
813                 'description': 'md5:ee18a25c350637c8faff806845bddee9',
814                 'upload_date': '20151107',
815                 'uploader_id': 'UCCr7TALkRbo3EtFzETQF1LA',
816                 'uploader': 'CH GAMER DROID',
817             },
818             'params': {
819                 'skip_download': True,
820             },
821             'skip': 'This video does not exist.',
822         },
823         {
824             # Video licensed under Creative Commons
825             'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
826             'info_dict': {
827                 'id': 'M4gD1WSo5mA',
828                 'ext': 'mp4',
829                 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
830                 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
831                 'duration': 721,
832                 'upload_date': '20150127',
833                 'uploader_id': 'BerkmanCenter',
834                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/BerkmanCenter',
835                 'uploader': 'The Berkman Klein Center for Internet & Society',
836                 'license': 'Creative Commons Attribution license (reuse allowed)',
837             },
838             'params': {
839                 'skip_download': True,
840             },
841         },
842         {
843             # Channel-like uploader_url
844             'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
845             'info_dict': {
846                 'id': 'eQcmzGIKrzg',
847                 'ext': 'mp4',
848                 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
849                 'description': 'md5:dda0d780d5a6e120758d1711d062a867',
850                 'duration': 4060,
851                 'upload_date': '20151119',
852                 'uploader': 'Bernie 2016',
853                 'uploader_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
854                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
855                 'license': 'Creative Commons Attribution license (reuse allowed)',
856             },
857             'params': {
858                 'skip_download': True,
859             },
860         },
861         {
862             'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;amp;v=V36LpHqtcDY',
863             'only_matching': True,
864         },
865         {
866             # YouTube Red paid video (https://github.com/rg3/youtube-dl/issues/10059)
867             'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
868             'only_matching': True,
869         },
870         {
871             # Rental video preview
872             'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
873             'info_dict': {
874                 'id': 'uGpuVWrhIzE',
875                 'ext': 'mp4',
876                 'title': 'Piku - Trailer',
877                 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
878                 'upload_date': '20150811',
879                 'uploader': 'FlixMatrix',
880                 'uploader_id': 'FlixMatrixKaravan',
881                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/FlixMatrixKaravan',
882                 'license': 'Standard YouTube License',
883             },
884             'params': {
885                 'skip_download': True,
886             },
887         },
888         {
889             # YouTube Red video with episode data
890             'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
891             'info_dict': {
892                 'id': 'iqKdEhx-dD4',
893                 'ext': 'mp4',
894                 'title': 'Isolation - Mind Field (Ep 1)',
895                 'description': 'md5:8013b7ddea787342608f63a13ddc9492',
896                 'duration': 2085,
897                 'upload_date': '20170118',
898                 'uploader': 'Vsauce',
899                 'uploader_id': 'Vsauce',
900                 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Vsauce',
901                 'license': 'Standard YouTube License',
902                 'series': 'Mind Field',
903                 'season_number': 1,
904                 'episode_number': 1,
905             },
906             'params': {
907                 'skip_download': True,
908             },
909             'expected_warnings': [
910                 'Skipping DASH manifest',
911             ],
912         },
913         {
914             # itag 212
915             'url': '1t24XAntNCY',
916             'only_matching': True,
917         }
918     ]
919
920     def __init__(self, *args, **kwargs):
921         super(YoutubeIE, self).__init__(*args, **kwargs)
922         self._player_cache = {}
923
924     def report_video_info_webpage_download(self, video_id):
925         """Report attempt to download video info webpage."""
926         self.to_screen('%s: Downloading video info webpage' % video_id)
927
928     def report_information_extraction(self, video_id):
929         """Report attempt to extract video information."""
930         self.to_screen('%s: Extracting video information' % video_id)
931
932     def report_unavailable_format(self, video_id, format):
933         """Report extracted video URL."""
934         self.to_screen('%s: Format %s not available' % (video_id, format))
935
936     def report_rtmp_download(self):
937         """Indicate the download will use the RTMP protocol."""
938         self.to_screen('RTMP download detected')
939
940     def _signature_cache_id(self, example_sig):
941         """ Return a string representation of a signature """
942         return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
943
944     def _extract_signature_function(self, video_id, player_url, example_sig):
945         id_m = re.match(
946             r'.*?-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player(?:-new)?|/base)?\.(?P<ext>[a-z]+)$',
947             player_url)
948         if not id_m:
949             raise ExtractorError('Cannot identify player %r' % player_url)
950         player_type = id_m.group('ext')
951         player_id = id_m.group('id')
952
953         # Read from filesystem cache
954         func_id = '%s_%s_%s' % (
955             player_type, player_id, self._signature_cache_id(example_sig))
956         assert os.path.basename(func_id) == func_id
957
958         cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
959         if cache_spec is not None:
960             return lambda s: ''.join(s[i] for i in cache_spec)
961
962         download_note = (
963             'Downloading player %s' % player_url
964             if self._downloader.params.get('verbose') else
965             'Downloading %s player %s' % (player_type, player_id)
966         )
967         if player_type == 'js':
968             code = self._download_webpage(
969                 player_url, video_id,
970                 note=download_note,
971                 errnote='Download of %s failed' % player_url)
972             res = self._parse_sig_js(code)
973         elif player_type == 'swf':
974             urlh = self._request_webpage(
975                 player_url, video_id,
976                 note=download_note,
977                 errnote='Download of %s failed' % player_url)
978             code = urlh.read()
979             res = self._parse_sig_swf(code)
980         else:
981             assert False, 'Invalid player type %r' % player_type
982
983         test_string = ''.join(map(compat_chr, range(len(example_sig))))
984         cache_res = res(test_string)
985         cache_spec = [ord(c) for c in cache_res]
986
987         self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
988         return res
989
990     def _print_sig_code(self, func, example_sig):
991         def gen_sig_code(idxs):
992             def _genslice(start, end, step):
993                 starts = '' if start == 0 else str(start)
994                 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
995                 steps = '' if step == 1 else (':%d' % step)
996                 return 's[%s%s%s]' % (starts, ends, steps)
997
998             step = None
999             # Quelch pyflakes warnings - start will be set when step is set
1000             start = '(Never used)'
1001             for i, prev in zip(idxs[1:], idxs[:-1]):
1002                 if step is not None:
1003                     if i - prev == step:
1004                         continue
1005                     yield _genslice(start, prev, step)
1006                     step = None
1007                     continue
1008                 if i - prev in [-1, 1]:
1009                     step = i - prev
1010                     start = prev
1011                     continue
1012                 else:
1013                     yield 's[%d]' % prev
1014             if step is None:
1015                 yield 's[%d]' % i
1016             else:
1017                 yield _genslice(start, i, step)
1018
1019         test_string = ''.join(map(compat_chr, range(len(example_sig))))
1020         cache_res = func(test_string)
1021         cache_spec = [ord(c) for c in cache_res]
1022         expr_code = ' + '.join(gen_sig_code(cache_spec))
1023         signature_id_tuple = '(%s)' % (
1024             ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
1025         code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
1026                 '    return %s\n') % (signature_id_tuple, expr_code)
1027         self.to_screen('Extracted signature function:\n' + code)
1028
1029     def _parse_sig_js(self, jscode):
1030         funcname = self._search_regex(
1031             (r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1032              r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\('),
1033             jscode, 'Initial JS player signature function name', group='sig')
1034
1035         jsi = JSInterpreter(jscode)
1036         initial_function = jsi.extract_function(funcname)
1037         return lambda s: initial_function([s])
1038
1039     def _parse_sig_swf(self, file_contents):
1040         swfi = SWFInterpreter(file_contents)
1041         TARGET_CLASSNAME = 'SignatureDecipher'
1042         searched_class = swfi.extract_class(TARGET_CLASSNAME)
1043         initial_function = swfi.extract_function(searched_class, 'decipher')
1044         return lambda s: initial_function([s])
1045
1046     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
1047         """Turn the encrypted s field into a working signature"""
1048
1049         if player_url is None:
1050             raise ExtractorError('Cannot decrypt signature without player_url')
1051
1052         if player_url.startswith('//'):
1053             player_url = 'https:' + player_url
1054         elif not re.match(r'https?://', player_url):
1055             player_url = compat_urlparse.urljoin(
1056                 'https://www.youtube.com', player_url)
1057         try:
1058             player_id = (player_url, self._signature_cache_id(s))
1059             if player_id not in self._player_cache:
1060                 func = self._extract_signature_function(
1061                     video_id, player_url, s
1062                 )
1063                 self._player_cache[player_id] = func
1064             func = self._player_cache[player_id]
1065             if self._downloader.params.get('youtube_print_sig_code'):
1066                 self._print_sig_code(func, s)
1067             return func(s)
1068         except Exception as e:
1069             tb = traceback.format_exc()
1070             raise ExtractorError(
1071                 'Signature extraction failed: ' + tb, cause=e)
1072
1073     def _get_subtitles(self, video_id, webpage):
1074         try:
1075             subs_doc = self._download_xml(
1076                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
1077                 video_id, note=False)
1078         except ExtractorError as err:
1079             self._downloader.report_warning('unable to download video subtitles: %s' % error_to_compat_str(err))
1080             return {}
1081
1082         sub_lang_list = {}
1083         for track in subs_doc.findall('track'):
1084             lang = track.attrib['lang_code']
1085             if lang in sub_lang_list:
1086                 continue
1087             sub_formats = []
1088             for ext in self._SUBTITLE_FORMATS:
1089                 params = compat_urllib_parse_urlencode({
1090                     'lang': lang,
1091                     'v': video_id,
1092                     'fmt': ext,
1093                     'name': track.attrib['name'].encode('utf-8'),
1094                 })
1095                 sub_formats.append({
1096                     'url': 'https://www.youtube.com/api/timedtext?' + params,
1097                     'ext': ext,
1098                 })
1099             sub_lang_list[lang] = sub_formats
1100         if not sub_lang_list:
1101             self._downloader.report_warning('video doesn\'t have subtitles')
1102             return {}
1103         return sub_lang_list
1104
1105     def _get_ytplayer_config(self, video_id, webpage):
1106         patterns = (
1107             # User data may contain arbitrary character sequences that may affect
1108             # JSON extraction with regex, e.g. when '};' is contained the second
1109             # regex won't capture the whole JSON. Yet working around by trying more
1110             # concrete regex first keeping in mind proper quoted string handling
1111             # to be implemented in future that will replace this workaround (see
1112             # https://github.com/rg3/youtube-dl/issues/7468,
1113             # https://github.com/rg3/youtube-dl/pull/7599)
1114             r';ytplayer\.config\s*=\s*({.+?});ytplayer',
1115             r';ytplayer\.config\s*=\s*({.+?});',
1116         )
1117         config = self._search_regex(
1118             patterns, webpage, 'ytplayer.config', default=None)
1119         if config:
1120             return self._parse_json(
1121                 uppercase_escape(config), video_id, fatal=False)
1122
1123     def _get_automatic_captions(self, video_id, webpage):
1124         """We need the webpage for getting the captions url, pass it as an
1125            argument to speed up the process."""
1126         self.to_screen('%s: Looking for automatic captions' % video_id)
1127         player_config = self._get_ytplayer_config(video_id, webpage)
1128         err_msg = 'Couldn\'t find automatic captions for %s' % video_id
1129         if not player_config:
1130             self._downloader.report_warning(err_msg)
1131             return {}
1132         try:
1133             args = player_config['args']
1134             caption_url = args.get('ttsurl')
1135             if caption_url:
1136                 timestamp = args['timestamp']
1137                 # We get the available subtitles
1138                 list_params = compat_urllib_parse_urlencode({
1139                     'type': 'list',
1140                     'tlangs': 1,
1141                     'asrs': 1,
1142                 })
1143                 list_url = caption_url + '&' + list_params
1144                 caption_list = self._download_xml(list_url, video_id)
1145                 original_lang_node = caption_list.find('track')
1146                 if original_lang_node is None:
1147                     self._downloader.report_warning('Video doesn\'t have automatic captions')
1148                     return {}
1149                 original_lang = original_lang_node.attrib['lang_code']
1150                 caption_kind = original_lang_node.attrib.get('kind', '')
1151
1152                 sub_lang_list = {}
1153                 for lang_node in caption_list.findall('target'):
1154                     sub_lang = lang_node.attrib['lang_code']
1155                     sub_formats = []
1156                     for ext in self._SUBTITLE_FORMATS:
1157                         params = compat_urllib_parse_urlencode({
1158                             'lang': original_lang,
1159                             'tlang': sub_lang,
1160                             'fmt': ext,
1161                             'ts': timestamp,
1162                             'kind': caption_kind,
1163                         })
1164                         sub_formats.append({
1165                             'url': caption_url + '&' + params,
1166                             'ext': ext,
1167                         })
1168                     sub_lang_list[sub_lang] = sub_formats
1169                 return sub_lang_list
1170
1171             # Some videos don't provide ttsurl but rather caption_tracks and
1172             # caption_translation_languages (e.g. 20LmZk1hakA)
1173             caption_tracks = args['caption_tracks']
1174             caption_translation_languages = args['caption_translation_languages']
1175             caption_url = compat_parse_qs(caption_tracks.split(',')[0])['u'][0]
1176             parsed_caption_url = compat_urllib_parse_urlparse(caption_url)
1177             caption_qs = compat_parse_qs(parsed_caption_url.query)
1178
1179             sub_lang_list = {}
1180             for lang in caption_translation_languages.split(','):
1181                 lang_qs = compat_parse_qs(compat_urllib_parse_unquote_plus(lang))
1182                 sub_lang = lang_qs.get('lc', [None])[0]
1183                 if not sub_lang:
1184                     continue
1185                 sub_formats = []
1186                 for ext in self._SUBTITLE_FORMATS:
1187                     caption_qs.update({
1188                         'tlang': [sub_lang],
1189                         'fmt': [ext],
1190                     })
1191                     sub_url = compat_urlparse.urlunparse(parsed_caption_url._replace(
1192                         query=compat_urllib_parse_urlencode(caption_qs, True)))
1193                     sub_formats.append({
1194                         'url': sub_url,
1195                         'ext': ext,
1196                     })
1197                 sub_lang_list[sub_lang] = sub_formats
1198             return sub_lang_list
1199         # An extractor error can be raise by the download process if there are
1200         # no automatic captions but there are subtitles
1201         except (KeyError, ExtractorError):
1202             self._downloader.report_warning(err_msg)
1203             return {}
1204
1205     def _mark_watched(self, video_id, video_info):
1206         playback_url = video_info.get('videostats_playback_base_url', [None])[0]
1207         if not playback_url:
1208             return
1209         parsed_playback_url = compat_urlparse.urlparse(playback_url)
1210         qs = compat_urlparse.parse_qs(parsed_playback_url.query)
1211
1212         # cpn generation algorithm is reverse engineered from base.js.
1213         # In fact it works even with dummy cpn.
1214         CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
1215         cpn = ''.join((CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16)))
1216
1217         qs.update({
1218             'ver': ['2'],
1219             'cpn': [cpn],
1220         })
1221         playback_url = compat_urlparse.urlunparse(
1222             parsed_playback_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
1223
1224         self._download_webpage(
1225             playback_url, video_id, 'Marking watched',
1226             'Unable to mark watched', fatal=False)
1227
1228     @classmethod
1229     def extract_id(cls, url):
1230         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
1231         if mobj is None:
1232             raise ExtractorError('Invalid URL: %s' % url)
1233         video_id = mobj.group(2)
1234         return video_id
1235
1236     def _extract_from_m3u8(self, manifest_url, video_id):
1237         url_map = {}
1238
1239         def _get_urls(_manifest):
1240             lines = _manifest.split('\n')
1241             urls = filter(lambda l: l and not l.startswith('#'),
1242                           lines)
1243             return urls
1244         manifest = self._download_webpage(manifest_url, video_id, 'Downloading formats manifest')
1245         formats_urls = _get_urls(manifest)
1246         for format_url in formats_urls:
1247             itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
1248             url_map[itag] = format_url
1249         return url_map
1250
1251     def _extract_annotations(self, video_id):
1252         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
1253         return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
1254
1255     def _real_extract(self, url):
1256         url, smuggled_data = unsmuggle_url(url, {})
1257
1258         proto = (
1259             'http' if self._downloader.params.get('prefer_insecure', False)
1260             else 'https')
1261
1262         start_time = None
1263         end_time = None
1264         parsed_url = compat_urllib_parse_urlparse(url)
1265         for component in [parsed_url.fragment, parsed_url.query]:
1266             query = compat_parse_qs(component)
1267             if start_time is None and 't' in query:
1268                 start_time = parse_duration(query['t'][0])
1269             if start_time is None and 'start' in query:
1270                 start_time = parse_duration(query['start'][0])
1271             if end_time is None and 'end' in query:
1272                 end_time = parse_duration(query['end'][0])
1273
1274         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1275         mobj = re.search(self._NEXT_URL_RE, url)
1276         if mobj:
1277             url = proto + '://www.youtube.com/' + compat_urllib_parse_unquote(mobj.group(1)).lstrip('/')
1278         video_id = self.extract_id(url)
1279
1280         # Get video webpage
1281         url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
1282         video_webpage = self._download_webpage(url, video_id)
1283
1284         # Attempt to extract SWF player URL
1285         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1286         if mobj is not None:
1287             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
1288         else:
1289             player_url = None
1290
1291         dash_mpds = []
1292
1293         def add_dash_mpd(video_info):
1294             dash_mpd = video_info.get('dashmpd')
1295             if dash_mpd and dash_mpd[0] not in dash_mpds:
1296                 dash_mpds.append(dash_mpd[0])
1297
1298         # Get video info
1299         embed_webpage = None
1300         is_live = None
1301         if re.search(r'player-age-gate-content">', video_webpage) is not None:
1302             age_gate = True
1303             # We simulate the access to the video from www.youtube.com/v/{video_id}
1304             # this can be viewed without login into Youtube
1305             url = proto + '://www.youtube.com/embed/%s' % video_id
1306             embed_webpage = self._download_webpage(url, video_id, 'Downloading embed webpage')
1307             data = compat_urllib_parse_urlencode({
1308                 'video_id': video_id,
1309                 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1310                 'sts': self._search_regex(
1311                     r'"sts"\s*:\s*(\d+)', embed_webpage, 'sts', default=''),
1312             })
1313             video_info_url = proto + '://www.youtube.com/get_video_info?' + data
1314             video_info_webpage = self._download_webpage(
1315                 video_info_url, video_id,
1316                 note='Refetching age-gated info webpage',
1317                 errnote='unable to download video info webpage')
1318             video_info = compat_parse_qs(video_info_webpage)
1319             add_dash_mpd(video_info)
1320         else:
1321             age_gate = False
1322             video_info = None
1323             # Try looking directly into the video webpage
1324             ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1325             if ytplayer_config:
1326                 args = ytplayer_config['args']
1327                 if args.get('url_encoded_fmt_stream_map'):
1328                     # Convert to the same format returned by compat_parse_qs
1329                     video_info = dict((k, [v]) for k, v in args.items())
1330                     add_dash_mpd(video_info)
1331                 # Rental video is not rented but preview is available (e.g.
1332                 # https://www.youtube.com/watch?v=yYr8q0y5Jfg,
1333                 # https://github.com/rg3/youtube-dl/issues/10532)
1334                 if not video_info and args.get('ypc_vid'):
1335                     return self.url_result(
1336                         args['ypc_vid'], YoutubeIE.ie_key(), video_id=args['ypc_vid'])
1337                 if args.get('livestream') == '1' or args.get('live_playback') == 1:
1338                     is_live = True
1339             if not video_info or self._downloader.params.get('youtube_include_dash_manifest', True):
1340                 # We also try looking in get_video_info since it may contain different dashmpd
1341                 # URL that points to a DASH manifest with possibly different itag set (some itags
1342                 # are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
1343                 # manifest pointed by get_video_info's dashmpd).
1344                 # The general idea is to take a union of itags of both DASH manifests (for example
1345                 # video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
1346                 self.report_video_info_webpage_download(video_id)
1347                 for el_type in ['&el=info', '&el=embedded', '&el=detailpage', '&el=vevo', '']:
1348                     video_info_url = (
1349                         '%s://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
1350                         % (proto, video_id, el_type))
1351                     video_info_webpage = self._download_webpage(
1352                         video_info_url,
1353                         video_id, note=False,
1354                         errnote='unable to download video info webpage')
1355                     get_video_info = compat_parse_qs(video_info_webpage)
1356                     if get_video_info.get('use_cipher_signature') != ['True']:
1357                         add_dash_mpd(get_video_info)
1358                     if not video_info:
1359                         video_info = get_video_info
1360                     if 'token' in get_video_info:
1361                         # Different get_video_info requests may report different results, e.g.
1362                         # some may report video unavailability, but some may serve it without
1363                         # any complaint (see https://github.com/rg3/youtube-dl/issues/7362,
1364                         # the original webpage as well as el=info and el=embedded get_video_info
1365                         # requests report video unavailability due to geo restriction while
1366                         # el=detailpage succeeds and returns valid data). This is probably
1367                         # due to YouTube measures against IP ranges of hosting providers.
1368                         # Working around by preferring the first succeeded video_info containing
1369                         # the token if no such video_info yet was found.
1370                         if 'token' not in video_info:
1371                             video_info = get_video_info
1372                         break
1373         if 'token' not in video_info:
1374             if 'reason' in video_info:
1375                 if 'The uploader has not made this video available in your country.' in video_info['reason']:
1376                     regions_allowed = self._html_search_meta('regionsAllowed', video_webpage, default=None)
1377                     if regions_allowed:
1378                         raise ExtractorError('YouTube said: This video is available in %s only' % (
1379                             ', '.join(map(ISO3166Utils.short2full, regions_allowed.split(',')))),
1380                             expected=True)
1381                 raise ExtractorError(
1382                     'YouTube said: %s' % video_info['reason'][0],
1383                     expected=True, video_id=video_id)
1384             else:
1385                 raise ExtractorError(
1386                     '"token" parameter not in video info for unknown reason',
1387                     video_id=video_id)
1388
1389         # title
1390         if 'title' in video_info:
1391             video_title = video_info['title'][0]
1392         else:
1393             self._downloader.report_warning('Unable to extract video title')
1394             video_title = '_'
1395
1396         # description
1397         video_description = get_element_by_id("eow-description", video_webpage)
1398         if video_description:
1399             video_description = re.sub(r'''(?x)
1400                 <a\s+
1401                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1402                     (?:title|href)="([^"]+)"\s+
1403                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1404                     class="[^"]*"[^>]*>
1405                 [^<]+\.{3}\s*
1406                 </a>
1407             ''', r'\1', video_description)
1408             video_description = clean_html(video_description)
1409         else:
1410             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
1411             if fd_mobj:
1412                 video_description = unescapeHTML(fd_mobj.group(1))
1413             else:
1414                 video_description = ''
1415
1416         if 'multifeed_metadata_list' in video_info and not smuggled_data.get('force_singlefeed', False):
1417             if not self._downloader.params.get('noplaylist'):
1418                 entries = []
1419                 feed_ids = []
1420                 multifeed_metadata_list = video_info['multifeed_metadata_list'][0]
1421                 for feed in multifeed_metadata_list.split(','):
1422                     # Unquote should take place before split on comma (,) since textual
1423                     # fields may contain comma as well (see
1424                     # https://github.com/rg3/youtube-dl/issues/8536)
1425                     feed_data = compat_parse_qs(compat_urllib_parse_unquote_plus(feed))
1426                     entries.append({
1427                         '_type': 'url_transparent',
1428                         'ie_key': 'Youtube',
1429                         'url': smuggle_url(
1430                             '%s://www.youtube.com/watch?v=%s' % (proto, feed_data['id'][0]),
1431                             {'force_singlefeed': True}),
1432                         'title': '%s (%s)' % (video_title, feed_data['title'][0]),
1433                     })
1434                     feed_ids.append(feed_data['id'][0])
1435                 self.to_screen(
1436                     'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
1437                     % (', '.join(feed_ids), video_id))
1438                 return self.playlist_result(entries, video_id, video_title, video_description)
1439             self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1440
1441         if 'view_count' in video_info:
1442             view_count = int(video_info['view_count'][0])
1443         else:
1444             view_count = None
1445
1446         # Check for "rental" videos
1447         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
1448             raise ExtractorError('"rental" videos not supported')
1449
1450         # Start extracting information
1451         self.report_information_extraction(video_id)
1452
1453         # uploader
1454         if 'author' not in video_info:
1455             raise ExtractorError('Unable to extract uploader name')
1456         video_uploader = compat_urllib_parse_unquote_plus(video_info['author'][0])
1457
1458         # uploader_id
1459         video_uploader_id = None
1460         video_uploader_url = None
1461         mobj = re.search(
1462             r'<link itemprop="url" href="(?P<uploader_url>https?://www.youtube.com/(?:user|channel)/(?P<uploader_id>[^"]+))">',
1463             video_webpage)
1464         if mobj is not None:
1465             video_uploader_id = mobj.group('uploader_id')
1466             video_uploader_url = mobj.group('uploader_url')
1467         else:
1468             self._downloader.report_warning('unable to extract uploader nickname')
1469
1470         # thumbnail image
1471         # We try first to get a high quality image:
1472         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
1473                             video_webpage, re.DOTALL)
1474         if m_thumb is not None:
1475             video_thumbnail = m_thumb.group(1)
1476         elif 'thumbnail_url' not in video_info:
1477             self._downloader.report_warning('unable to extract video thumbnail')
1478             video_thumbnail = None
1479         else:   # don't panic if we can't find it
1480             video_thumbnail = compat_urllib_parse_unquote_plus(video_info['thumbnail_url'][0])
1481
1482         # upload date
1483         upload_date = self._html_search_meta(
1484             'datePublished', video_webpage, 'upload date', default=None)
1485         if not upload_date:
1486             upload_date = self._search_regex(
1487                 [r'(?s)id="eow-date.*?>(.*?)</span>',
1488                  r'id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live|Started) on (.+?)</strong>'],
1489                 video_webpage, 'upload date', default=None)
1490             if upload_date:
1491                 upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
1492         upload_date = unified_strdate(upload_date)
1493
1494         video_license = self._html_search_regex(
1495             r'<h4[^>]+class="title"[^>]*>\s*License\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li',
1496             video_webpage, 'license', default=None)
1497
1498         m_music = re.search(
1499             r'<h4[^>]+class="title"[^>]*>\s*Music\s*</h4>\s*<ul[^>]*>\s*<li>(?P<title>.+?) by (?P<creator>.+?)(?:\(.+?\))?</li',
1500             video_webpage)
1501         if m_music:
1502             video_alt_title = remove_quotes(unescapeHTML(m_music.group('title')))
1503             video_creator = clean_html(m_music.group('creator'))
1504         else:
1505             video_alt_title = video_creator = None
1506
1507         m_episode = re.search(
1508             r'<div[^>]+id="watch7-headline"[^>]*>\s*<span[^>]*>.*?>(?P<series>[^<]+)</a></b>\s*S(?P<season>\d+)\s*•\s*E(?P<episode>\d+)</span>',
1509             video_webpage)
1510         if m_episode:
1511             series = m_episode.group('series')
1512             season_number = int(m_episode.group('season'))
1513             episode_number = int(m_episode.group('episode'))
1514         else:
1515             series = season_number = episode_number = None
1516
1517         m_cat_container = self._search_regex(
1518             r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
1519             video_webpage, 'categories', default=None)
1520         if m_cat_container:
1521             category = self._html_search_regex(
1522                 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
1523                 default=None)
1524             video_categories = None if category is None else [category]
1525         else:
1526             video_categories = None
1527
1528         video_tags = [
1529             unescapeHTML(m.group('content'))
1530             for m in re.finditer(self._meta_regex('og:video:tag'), video_webpage)]
1531
1532         def _extract_count(count_name):
1533             return str_to_int(self._search_regex(
1534                 r'-%s-button[^>]+><span[^>]+class="yt-uix-button-content"[^>]*>([\d,]+)</span>'
1535                 % re.escape(count_name),
1536                 video_webpage, count_name, default=None))
1537
1538         like_count = _extract_count('like')
1539         dislike_count = _extract_count('dislike')
1540
1541         # subtitles
1542         video_subtitles = self.extract_subtitles(video_id, video_webpage)
1543         automatic_captions = self.extract_automatic_captions(video_id, video_webpage)
1544
1545         video_duration = try_get(
1546             video_info, lambda x: int_or_none(x['length_seconds'][0]))
1547         if not video_duration:
1548             video_duration = parse_duration(self._html_search_meta(
1549                 'duration', video_webpage, 'video duration'))
1550
1551         # annotations
1552         video_annotations = None
1553         if self._downloader.params.get('writeannotations', False):
1554             video_annotations = self._extract_annotations(video_id)
1555
1556         def _map_to_format_list(urlmap):
1557             formats = []
1558             for itag, video_real_url in urlmap.items():
1559                 dct = {
1560                     'format_id': itag,
1561                     'url': video_real_url,
1562                     'player_url': player_url,
1563                 }
1564                 if itag in self._formats:
1565                     dct.update(self._formats[itag])
1566                 formats.append(dct)
1567             return formats
1568
1569         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
1570             self.report_rtmp_download()
1571             formats = [{
1572                 'format_id': '_rtmp',
1573                 'protocol': 'rtmp',
1574                 'url': video_info['conn'][0],
1575                 'player_url': player_url,
1576             }]
1577         elif len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or len(video_info.get('adaptive_fmts', [''])[0]) >= 1:
1578             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts', [''])[0]
1579             if 'rtmpe%3Dyes' in encoded_url_map:
1580                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
1581             formats_spec = {}
1582             fmt_list = video_info.get('fmt_list', [''])[0]
1583             if fmt_list:
1584                 for fmt in fmt_list.split(','):
1585                     spec = fmt.split('/')
1586                     if len(spec) > 1:
1587                         width_height = spec[1].split('x')
1588                         if len(width_height) == 2:
1589                             formats_spec[spec[0]] = {
1590                                 'resolution': spec[1],
1591                                 'width': int_or_none(width_height[0]),
1592                                 'height': int_or_none(width_height[1]),
1593                             }
1594             formats = []
1595             for url_data_str in encoded_url_map.split(','):
1596                 url_data = compat_parse_qs(url_data_str)
1597                 if 'itag' not in url_data or 'url' not in url_data:
1598                     continue
1599                 format_id = url_data['itag'][0]
1600                 url = url_data['url'][0]
1601
1602                 if 'sig' in url_data:
1603                     url += '&signature=' + url_data['sig'][0]
1604                 elif 's' in url_data:
1605                     encrypted_sig = url_data['s'][0]
1606                     ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
1607
1608                     jsplayer_url_json = self._search_regex(
1609                         ASSETS_RE,
1610                         embed_webpage if age_gate else video_webpage,
1611                         'JS player URL (1)', default=None)
1612                     if not jsplayer_url_json and not age_gate:
1613                         # We need the embed website after all
1614                         if embed_webpage is None:
1615                             embed_url = proto + '://www.youtube.com/embed/%s' % video_id
1616                             embed_webpage = self._download_webpage(
1617                                 embed_url, video_id, 'Downloading embed webpage')
1618                         jsplayer_url_json = self._search_regex(
1619                             ASSETS_RE, embed_webpage, 'JS player URL')
1620
1621                     player_url = json.loads(jsplayer_url_json)
1622                     if player_url is None:
1623                         player_url_json = self._search_regex(
1624                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
1625                             video_webpage, 'age gate player URL')
1626                         player_url = json.loads(player_url_json)
1627
1628                     if self._downloader.params.get('verbose'):
1629                         if player_url is None:
1630                             player_version = 'unknown'
1631                             player_desc = 'unknown'
1632                         else:
1633                             if player_url.endswith('swf'):
1634                                 player_version = self._search_regex(
1635                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
1636                                     'flash player', fatal=False)
1637                                 player_desc = 'flash player %s' % player_version
1638                             else:
1639                                 player_version = self._search_regex(
1640                                     [r'html5player-([^/]+?)(?:/html5player(?:-new)?)?\.js', r'(?:www|player)-([^/]+)/base\.js'],
1641                                     player_url,
1642                                     'html5 player', fatal=False)
1643                                 player_desc = 'html5 player %s' % player_version
1644
1645                         parts_sizes = self._signature_cache_id(encrypted_sig)
1646                         self.to_screen('{%s} signature length %s, %s' %
1647                                        (format_id, parts_sizes, player_desc))
1648
1649                     signature = self._decrypt_signature(
1650                         encrypted_sig, video_id, player_url, age_gate)
1651                     url += '&signature=' + signature
1652                 if 'ratebypass' not in url:
1653                     url += '&ratebypass=yes'
1654
1655                 dct = {
1656                     'format_id': format_id,
1657                     'url': url,
1658                     'player_url': player_url,
1659                 }
1660                 if format_id in self._formats:
1661                     dct.update(self._formats[format_id])
1662                 if format_id in formats_spec:
1663                     dct.update(formats_spec[format_id])
1664
1665                 # Some itags are not included in DASH manifest thus corresponding formats will
1666                 # lack metadata (see https://github.com/rg3/youtube-dl/pull/5993).
1667                 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
1668                 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
1669                 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
1670
1671                 more_fields = {
1672                     'filesize': int_or_none(url_data.get('clen', [None])[0]),
1673                     'tbr': float_or_none(url_data.get('bitrate', [None])[0], 1000),
1674                     'width': width,
1675                     'height': height,
1676                     'fps': int_or_none(url_data.get('fps', [None])[0]),
1677                     'format_note': url_data.get('quality_label', [None])[0] or url_data.get('quality', [None])[0],
1678                 }
1679                 for key, value in more_fields.items():
1680                     if value:
1681                         dct[key] = value
1682                 type_ = url_data.get('type', [None])[0]
1683                 if type_:
1684                     type_split = type_.split(';')
1685                     kind_ext = type_split[0].split('/')
1686                     if len(kind_ext) == 2:
1687                         kind, _ = kind_ext
1688                         dct['ext'] = mimetype2ext(type_split[0])
1689                         if kind in ('audio', 'video'):
1690                             codecs = None
1691                             for mobj in re.finditer(
1692                                     r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?)(?P<val>.+?)(?P=quote)(?:;|$)', type_):
1693                                 if mobj.group('key') == 'codecs':
1694                                     codecs = mobj.group('val')
1695                                     break
1696                             if codecs:
1697                                 codecs = codecs.split(',')
1698                                 if len(codecs) == 2:
1699                                     acodec, vcodec = codecs[1], codecs[0]
1700                                 else:
1701                                     acodec, vcodec = (codecs[0], 'none') if kind == 'audio' else ('none', codecs[0])
1702                                 dct.update({
1703                                     'acodec': acodec,
1704                                     'vcodec': vcodec,
1705                                 })
1706                 formats.append(dct)
1707         elif video_info.get('hlsvp'):
1708             manifest_url = video_info['hlsvp'][0]
1709             url_map = self._extract_from_m3u8(manifest_url, video_id)
1710             formats = _map_to_format_list(url_map)
1711             # Accept-Encoding header causes failures in live streams on Youtube and Youtube Gaming
1712             for a_format in formats:
1713                 a_format.setdefault('http_headers', {})['Youtubedl-no-compression'] = 'True'
1714         else:
1715             unavailable_message = self._html_search_regex(
1716                 r'(?s)<h1[^>]+id="unavailable-message"[^>]*>(.+?)</h1>',
1717                 video_webpage, 'unavailable message', default=None)
1718             if unavailable_message:
1719                 raise ExtractorError(unavailable_message, expected=True)
1720             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1721
1722         # Look for the DASH manifest
1723         if self._downloader.params.get('youtube_include_dash_manifest', True):
1724             dash_mpd_fatal = True
1725             for mpd_url in dash_mpds:
1726                 dash_formats = {}
1727                 try:
1728                     def decrypt_sig(mobj):
1729                         s = mobj.group(1)
1730                         dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
1731                         return '/signature/%s' % dec_s
1732
1733                     mpd_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, mpd_url)
1734
1735                     for df in self._extract_mpd_formats(
1736                             mpd_url, video_id, fatal=dash_mpd_fatal,
1737                             formats_dict=self._formats):
1738                         # Do not overwrite DASH format found in some previous DASH manifest
1739                         if df['format_id'] not in dash_formats:
1740                             dash_formats[df['format_id']] = df
1741                         # Additional DASH manifests may end up in HTTP Error 403 therefore
1742                         # allow them to fail without bug report message if we already have
1743                         # some DASH manifest succeeded. This is temporary workaround to reduce
1744                         # burst of bug reports until we figure out the reason and whether it
1745                         # can be fixed at all.
1746                         dash_mpd_fatal = False
1747                 except (ExtractorError, KeyError) as e:
1748                     self.report_warning(
1749                         'Skipping DASH manifest: %r' % e, video_id)
1750                 if dash_formats:
1751                     # Remove the formats we found through non-DASH, they
1752                     # contain less info and it can be wrong, because we use
1753                     # fixed values (for example the resolution). See
1754                     # https://github.com/rg3/youtube-dl/issues/5774 for an
1755                     # example.
1756                     formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
1757                     formats.extend(dash_formats.values())
1758
1759         # Check for malformed aspect ratio
1760         stretched_m = re.search(
1761             r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
1762             video_webpage)
1763         if stretched_m:
1764             w = float(stretched_m.group('w'))
1765             h = float(stretched_m.group('h'))
1766             # yt:stretch may hold invalid ratio data (e.g. for Q39EVAstoRM ratio is 17:0).
1767             # We will only process correct ratios.
1768             if w > 0 and h > 0:
1769                 ratio = w / h
1770                 for f in formats:
1771                     if f.get('vcodec') != 'none':
1772                         f['stretched_ratio'] = ratio
1773
1774         self._sort_formats(formats)
1775
1776         self.mark_watched(video_id, video_info)
1777
1778         return {
1779             'id': video_id,
1780             'uploader': video_uploader,
1781             'uploader_id': video_uploader_id,
1782             'uploader_url': video_uploader_url,
1783             'upload_date': upload_date,
1784             'license': video_license,
1785             'creator': video_creator,
1786             'title': video_title,
1787             'alt_title': video_alt_title,
1788             'thumbnail': video_thumbnail,
1789             'description': video_description,
1790             'categories': video_categories,
1791             'tags': video_tags,
1792             'subtitles': video_subtitles,
1793             'automatic_captions': automatic_captions,
1794             'duration': video_duration,
1795             'age_limit': 18 if age_gate else 0,
1796             'annotations': video_annotations,
1797             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
1798             'view_count': view_count,
1799             'like_count': like_count,
1800             'dislike_count': dislike_count,
1801             'average_rating': float_or_none(video_info.get('avg_rating', [None])[0]),
1802             'formats': formats,
1803             'is_live': is_live,
1804             'start_time': start_time,
1805             'end_time': end_time,
1806             'series': series,
1807             'season_number': season_number,
1808             'episode_number': episode_number,
1809         }
1810
1811
1812 class YoutubeSharedVideoIE(InfoExtractor):
1813     _VALID_URL = r'(?:https?:)?//(?:www\.)?youtube\.com/shared\?.*\bci=(?P<id>[0-9A-Za-z_-]{11})'
1814     IE_NAME = 'youtube:shared'
1815
1816     _TEST = {
1817         'url': 'https://www.youtube.com/shared?ci=1nEzmT-M4fU',
1818         'info_dict': {
1819             'id': 'uPDB5I9wfp8',
1820             'ext': 'webm',
1821             'title': 'Pocoyo: 90 minutos de episódios completos Português para crianças - PARTE 3',
1822             'description': 'md5:d9e4d9346a2dfff4c7dc4c8cec0f546d',
1823             'upload_date': '20160219',
1824             'uploader': 'Pocoyo - Português (BR)',
1825             'uploader_id': 'PocoyoBrazil',
1826         },
1827         'add_ie': ['Youtube'],
1828         'params': {
1829             # There are already too many Youtube downloads
1830             'skip_download': True,
1831         },
1832     }
1833
1834     def _real_extract(self, url):
1835         video_id = self._match_id(url)
1836
1837         webpage = self._download_webpage(url, video_id)
1838
1839         real_video_id = self._html_search_meta(
1840             'videoId', webpage, 'YouTube video id', fatal=True)
1841
1842         return self.url_result(real_video_id, YoutubeIE.ie_key())
1843
1844
1845 class YoutubePlaylistIE(YoutubePlaylistBaseInfoExtractor):
1846     IE_DESC = 'YouTube.com playlists'
1847     _VALID_URL = r"""(?x)(?:
1848                         (?:https?://)?
1849                         (?:\w+\.)?
1850                         (?:
1851                             youtube\.com/
1852                             (?:
1853                                (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
1854                                \? (?:.*?[&;])*? (?:p|a|list)=
1855                             |  p/
1856                             )|
1857                             youtu\.be/[0-9A-Za-z_-]{11}\?.*?\blist=
1858                         )
1859                         (
1860                             (?:PL|LL|EC|UU|FL|RD|UL|TL)?[0-9A-Za-z-_]{10,}
1861                             # Top tracks, they can also include dots
1862                             |(?:MC)[\w\.]*
1863                         )
1864                         .*
1865                      |
1866                         ((?:PL|LL|EC|UU|FL|RD|UL|TL)[0-9A-Za-z-_]{10,})
1867                      )"""
1868     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s&disable_polymer=true'
1869     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)(?:[^>]+>(?P<title>[^<]+))?'
1870     IE_NAME = 'youtube:playlist'
1871     _TESTS = [{
1872         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1873         'info_dict': {
1874             'title': 'ytdl test PL',
1875             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1876         },
1877         'playlist_count': 3,
1878     }, {
1879         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1880         'info_dict': {
1881             'id': 'PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1882             'title': 'YDL_Empty_List',
1883         },
1884         'playlist_count': 0,
1885         'skip': 'This playlist is private',
1886     }, {
1887         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1888         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1889         'info_dict': {
1890             'title': '29C3: Not my department',
1891             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1892         },
1893         'playlist_count': 95,
1894     }, {
1895         'note': 'issue #673',
1896         'url': 'PLBB231211A4F62143',
1897         'info_dict': {
1898             'title': '[OLD]Team Fortress 2 (Class-based LP)',
1899             'id': 'PLBB231211A4F62143',
1900         },
1901         'playlist_mincount': 26,
1902     }, {
1903         'note': 'Large playlist',
1904         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1905         'info_dict': {
1906             'title': 'Uploads from Cauchemar',
1907             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
1908         },
1909         'playlist_mincount': 799,
1910     }, {
1911         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1912         'info_dict': {
1913             'title': 'YDL_safe_search',
1914             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1915         },
1916         'playlist_count': 2,
1917         'skip': 'This playlist is private',
1918     }, {
1919         'note': 'embedded',
1920         'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1921         'playlist_count': 4,
1922         'info_dict': {
1923             'title': 'JODA15',
1924             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1925         }
1926     }, {
1927         'note': 'Embedded SWF player',
1928         'url': 'https://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1929         'playlist_count': 4,
1930         'info_dict': {
1931             'title': 'JODA7',
1932             'id': 'YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ',
1933         }
1934     }, {
1935         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
1936         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
1937         'info_dict': {
1938             'title': 'Uploads from Interstellar Movie',
1939             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
1940         },
1941         'playlist_mincount': 21,
1942     }, {
1943         # Playlist URL that does not actually serve a playlist
1944         'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
1945         'info_dict': {
1946             'id': 'FqZTN594JQw',
1947             'ext': 'webm',
1948             'title': "Smiley's People 01 detective, Adventure Series, Action",
1949             'uploader': 'STREEM',
1950             'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
1951             'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
1952             'upload_date': '20150526',
1953             'license': 'Standard YouTube License',
1954             'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
1955             'categories': ['People & Blogs'],
1956             'tags': list,
1957             'like_count': int,
1958             'dislike_count': int,
1959         },
1960         'params': {
1961             'skip_download': True,
1962         },
1963         'add_ie': [YoutubeIE.ie_key()],
1964     }, {
1965         'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
1966         'info_dict': {
1967             'id': 'yeWKywCrFtk',
1968             'ext': 'mp4',
1969             'title': 'Small Scale Baler and Braiding Rugs',
1970             'uploader': 'Backus-Page House Museum',
1971             'uploader_id': 'backuspagemuseum',
1972             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
1973             'upload_date': '20161008',
1974             'license': 'Standard YouTube License',
1975             'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
1976             'categories': ['Nonprofits & Activism'],
1977             'tags': list,
1978             'like_count': int,
1979             'dislike_count': int,
1980         },
1981         'params': {
1982             'noplaylist': True,
1983             'skip_download': True,
1984         },
1985     }, {
1986         'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
1987         'only_matching': True,
1988     }, {
1989         'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
1990         'only_matching': True,
1991     }]
1992
1993     def _real_initialize(self):
1994         self._login()
1995
1996     def _extract_mix(self, playlist_id):
1997         # The mixes are generated from a single video
1998         # the id of the playlist is just 'RD' + video_id
1999         ids = []
2000         last_id = playlist_id[-11:]
2001         for n in itertools.count(1):
2002             url = 'https://youtube.com/watch?v=%s&list=%s' % (last_id, playlist_id)
2003             webpage = self._download_webpage(
2004                 url, playlist_id, 'Downloading page {0} of Youtube mix'.format(n))
2005             new_ids = orderedSet(re.findall(
2006                 r'''(?xs)data-video-username=".*?".*?
2007                            href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
2008                 webpage))
2009             # Fetch new pages until all the videos are repeated, it seems that
2010             # there are always 51 unique videos.
2011             new_ids = [_id for _id in new_ids if _id not in ids]
2012             if not new_ids:
2013                 break
2014             ids.extend(new_ids)
2015             last_id = ids[-1]
2016
2017         url_results = self._ids_to_results(ids)
2018
2019         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
2020         title_span = (
2021             search_title('playlist-title') or
2022             search_title('title long-title') or
2023             search_title('title'))
2024         title = clean_html(title_span)
2025
2026         return self.playlist_result(url_results, playlist_id, title)
2027
2028     def _extract_playlist(self, playlist_id):
2029         url = self._TEMPLATE_URL % playlist_id
2030         page = self._download_webpage(url, playlist_id)
2031
2032         # the yt-alert-message now has tabindex attribute (see https://github.com/rg3/youtube-dl/issues/11604)
2033         for match in re.findall(r'<div class="yt-alert-message"[^>]*>([^<]+)</div>', page):
2034             match = match.strip()
2035             # Check if the playlist exists or is private
2036             mobj = re.match(r'[^<]*(?:The|This) playlist (?P<reason>does not exist|is private)[^<]*', match)
2037             if mobj:
2038                 reason = mobj.group('reason')
2039                 message = 'This playlist %s' % reason
2040                 if 'private' in reason:
2041                     message += ', use --username or --netrc to access it'
2042                 message += '.'
2043                 raise ExtractorError(message, expected=True)
2044             elif re.match(r'[^<]*Invalid parameters[^<]*', match):
2045                 raise ExtractorError(
2046                     'Invalid parameters. Maybe URL is incorrect.',
2047                     expected=True)
2048             elif re.match(r'[^<]*Choose your language[^<]*', match):
2049                 continue
2050             else:
2051                 self.report_warning('Youtube gives an alert message: ' + match)
2052
2053         playlist_title = self._html_search_regex(
2054             r'(?s)<h1 class="pl-header-title[^"]*"[^>]*>\s*(.*?)\s*</h1>',
2055             page, 'title', default=None)
2056
2057         has_videos = True
2058
2059         if not playlist_title:
2060             try:
2061                 # Some playlist URLs don't actually serve a playlist (e.g.
2062                 # https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4)
2063                 next(self._entries(page, playlist_id))
2064             except StopIteration:
2065                 has_videos = False
2066
2067         return has_videos, self.playlist_result(
2068             self._entries(page, playlist_id), playlist_id, playlist_title)
2069
2070     def _check_download_just_video(self, url, playlist_id):
2071         # Check if it's a video-specific URL
2072         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
2073         video_id = query_dict.get('v', [None])[0] or self._search_regex(
2074             r'(?:^|//)youtu\.be/([0-9A-Za-z_-]{11})', url,
2075             'video id', default=None)
2076         if video_id:
2077             if self._downloader.params.get('noplaylist'):
2078                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
2079                 return video_id, self.url_result(video_id, 'Youtube', video_id=video_id)
2080             else:
2081                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
2082                 return video_id, None
2083         return None, None
2084
2085     def _real_extract(self, url):
2086         # Extract playlist id
2087         mobj = re.match(self._VALID_URL, url)
2088         if mobj is None:
2089             raise ExtractorError('Invalid URL: %s' % url)
2090         playlist_id = mobj.group(1) or mobj.group(2)
2091
2092         video_id, video = self._check_download_just_video(url, playlist_id)
2093         if video:
2094             return video
2095
2096         if playlist_id.startswith(('RD', 'UL', 'PU')):
2097             # Mixes require a custom extraction process
2098             return self._extract_mix(playlist_id)
2099
2100         has_videos, playlist = self._extract_playlist(playlist_id)
2101         if has_videos or not video_id:
2102             return playlist
2103
2104         # Some playlist URLs don't actually serve a playlist (see
2105         # https://github.com/rg3/youtube-dl/issues/10537).
2106         # Fallback to plain video extraction if there is a video id
2107         # along with playlist id.
2108         return self.url_result(video_id, 'Youtube', video_id=video_id)
2109
2110
2111 class YoutubeChannelIE(YoutubePlaylistBaseInfoExtractor):
2112     IE_DESC = 'YouTube.com channels'
2113     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/(?P<id>[0-9A-Za-z_-]+)'
2114     _TEMPLATE_URL = 'https://www.youtube.com/channel/%s/videos'
2115     _VIDEO_RE = r'(?:title="(?P<title>[^"]+)"[^>]+)?href="/watch\?v=(?P<id>[0-9A-Za-z_-]+)&?'
2116     IE_NAME = 'youtube:channel'
2117     _TESTS = [{
2118         'note': 'paginated channel',
2119         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
2120         'playlist_mincount': 91,
2121         'info_dict': {
2122             'id': 'UUKfVa3S1e4PHvxWcwyMMg8w',
2123             'title': 'Uploads from lex will',
2124         }
2125     }, {
2126         'note': 'Age restricted channel',
2127         # from https://www.youtube.com/user/DeusExOfficial
2128         'url': 'https://www.youtube.com/channel/UCs0ifCMCm1icqRbqhUINa0w',
2129         'playlist_mincount': 64,
2130         'info_dict': {
2131             'id': 'UUs0ifCMCm1icqRbqhUINa0w',
2132             'title': 'Uploads from Deus Ex',
2133         },
2134     }]
2135
2136     @classmethod
2137     def suitable(cls, url):
2138         return (False if YoutubePlaylistsIE.suitable(url) or YoutubeLiveIE.suitable(url)
2139                 else super(YoutubeChannelIE, cls).suitable(url))
2140
2141     def _build_template_url(self, url, channel_id):
2142         return self._TEMPLATE_URL % channel_id
2143
2144     def _real_extract(self, url):
2145         channel_id = self._match_id(url)
2146
2147         url = self._build_template_url(url, channel_id)
2148
2149         # Channel by page listing is restricted to 35 pages of 30 items, i.e. 1050 videos total (see #5778)
2150         # Workaround by extracting as a playlist if managed to obtain channel playlist URL
2151         # otherwise fallback on channel by page extraction
2152         channel_page = self._download_webpage(
2153             url + '?view=57', channel_id,
2154             'Downloading channel page', fatal=False)
2155         if channel_page is False:
2156             channel_playlist_id = False
2157         else:
2158             channel_playlist_id = self._html_search_meta(
2159                 'channelId', channel_page, 'channel id', default=None)
2160             if not channel_playlist_id:
2161                 channel_url = self._html_search_meta(
2162                     ('al:ios:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad'),
2163                     channel_page, 'channel url', default=None)
2164                 if channel_url:
2165                     channel_playlist_id = self._search_regex(
2166                         r'vnd\.youtube://user/([0-9A-Za-z_-]+)',
2167                         channel_url, 'channel id', default=None)
2168         if channel_playlist_id and channel_playlist_id.startswith('UC'):
2169             playlist_id = 'UU' + channel_playlist_id[2:]
2170             return self.url_result(
2171                 compat_urlparse.urljoin(url, '/playlist?list=%s' % playlist_id), 'YoutubePlaylist')
2172
2173         channel_page = self._download_webpage(url, channel_id, 'Downloading page #1')
2174         autogenerated = re.search(r'''(?x)
2175                 class="[^"]*?(?:
2176                     channel-header-autogenerated-label|
2177                     yt-channel-title-autogenerated
2178                 )[^"]*"''', channel_page) is not None
2179
2180         if autogenerated:
2181             # The videos are contained in a single page
2182             # the ajax pages can't be used, they are empty
2183             entries = [
2184                 self.url_result(
2185                     video_id, 'Youtube', video_id=video_id,
2186                     video_title=video_title)
2187                 for video_id, video_title in self.extract_videos_from_page(channel_page)]
2188             return self.playlist_result(entries, channel_id)
2189
2190         try:
2191             next(self._entries(channel_page, channel_id))
2192         except StopIteration:
2193             alert_message = self._html_search_regex(
2194                 r'(?s)<div[^>]+class=(["\']).*?\byt-alert-message\b.*?\1[^>]*>(?P<alert>[^<]+)</div>',
2195                 channel_page, 'alert', default=None, group='alert')
2196             if alert_message:
2197                 raise ExtractorError('Youtube said: %s' % alert_message, expected=True)
2198
2199         return self.playlist_result(self._entries(channel_page, channel_id), channel_id)
2200
2201
2202 class YoutubeUserIE(YoutubeChannelIE):
2203     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
2204     _VALID_URL = r'(?:(?:https?://(?:\w+\.)?youtube\.com/(?:(?P<user>user|c)/)?(?!(?:attribution_link|watch|results)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)(?P<id>[A-Za-z0-9_-]+)'
2205     _TEMPLATE_URL = 'https://www.youtube.com/%s/%s/videos'
2206     IE_NAME = 'youtube:user'
2207
2208     _TESTS = [{
2209         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
2210         'playlist_mincount': 320,
2211         'info_dict': {
2212             'id': 'UUfX55Sx5hEFjoC3cNs6mCUQ',
2213             'title': 'Uploads from The Linux Foundation',
2214         }
2215     }, {
2216         # Only available via https://www.youtube.com/c/12minuteathlete/videos
2217         # but not https://www.youtube.com/user/12minuteathlete/videos
2218         'url': 'https://www.youtube.com/c/12minuteathlete/videos',
2219         'playlist_mincount': 249,
2220         'info_dict': {
2221             'id': 'UUVjM-zV6_opMDx7WYxnjZiQ',
2222             'title': 'Uploads from 12 Minute Athlete',
2223         }
2224     }, {
2225         'url': 'ytuser:phihag',
2226         'only_matching': True,
2227     }, {
2228         'url': 'https://www.youtube.com/c/gametrailers',
2229         'only_matching': True,
2230     }, {
2231         'url': 'https://www.youtube.com/gametrailers',
2232         'only_matching': True,
2233     }, {
2234         # This channel is not available.
2235         'url': 'https://www.youtube.com/user/kananishinoSMEJ/videos',
2236         'only_matching': True,
2237     }]
2238
2239     @classmethod
2240     def suitable(cls, url):
2241         # Don't return True if the url can be extracted with other youtube
2242         # extractor, the regex would is too permissive and it would match.
2243         other_yt_ies = iter(klass for (name, klass) in globals().items() if name.startswith('Youtube') and name.endswith('IE') and klass is not cls)
2244         if any(ie.suitable(url) for ie in other_yt_ies):
2245             return False
2246         else:
2247             return super(YoutubeUserIE, cls).suitable(url)
2248
2249     def _build_template_url(self, url, channel_id):
2250         mobj = re.match(self._VALID_URL, url)
2251         return self._TEMPLATE_URL % (mobj.group('user') or 'user', mobj.group('id'))
2252
2253
2254 class YoutubeLiveIE(YoutubeBaseInfoExtractor):
2255     IE_DESC = 'YouTube.com live streams'
2256     _VALID_URL = r'(?P<base_url>https?://(?:\w+\.)?youtube\.com/(?:(?:user|channel|c)/)?(?P<id>[^/]+))/live'
2257     IE_NAME = 'youtube:live'
2258
2259     _TESTS = [{
2260         'url': 'https://www.youtube.com/user/TheYoungTurks/live',
2261         'info_dict': {
2262             'id': 'a48o2S1cPoo',
2263             'ext': 'mp4',
2264             'title': 'The Young Turks - Live Main Show',
2265             'uploader': 'The Young Turks',
2266             'uploader_id': 'TheYoungTurks',
2267             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
2268             'upload_date': '20150715',
2269             'license': 'Standard YouTube License',
2270             'description': 'md5:438179573adcdff3c97ebb1ee632b891',
2271             'categories': ['News & Politics'],
2272             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
2273             'like_count': int,
2274             'dislike_count': int,
2275         },
2276         'params': {
2277             'skip_download': True,
2278         },
2279     }, {
2280         'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
2281         'only_matching': True,
2282     }, {
2283         'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
2284         'only_matching': True,
2285     }, {
2286         'url': 'https://www.youtube.com/TheYoungTurks/live',
2287         'only_matching': True,
2288     }]
2289
2290     def _real_extract(self, url):
2291         mobj = re.match(self._VALID_URL, url)
2292         channel_id = mobj.group('id')
2293         base_url = mobj.group('base_url')
2294         webpage = self._download_webpage(url, channel_id, fatal=False)
2295         if webpage:
2296             page_type = self._og_search_property(
2297                 'type', webpage, 'page type', default=None)
2298             video_id = self._html_search_meta(
2299                 'videoId', webpage, 'video id', default=None)
2300             if page_type == 'video' and video_id and re.match(r'^[0-9A-Za-z_-]{11}$', video_id):
2301                 return self.url_result(video_id, YoutubeIE.ie_key())
2302         return self.url_result(base_url)
2303
2304
2305 class YoutubePlaylistsIE(YoutubePlaylistsBaseInfoExtractor):
2306     IE_DESC = 'YouTube.com user/channel playlists'
2307     _VALID_URL = r'https?://(?:\w+\.)?youtube\.com/(?:user|channel)/(?P<id>[^/]+)/playlists'
2308     IE_NAME = 'youtube:playlists'
2309
2310     _TESTS = [{
2311         'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
2312         'playlist_mincount': 4,
2313         'info_dict': {
2314             'id': 'ThirstForScience',
2315             'title': 'Thirst for Science',
2316         },
2317     }, {
2318         # with "Load more" button
2319         'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
2320         'playlist_mincount': 70,
2321         'info_dict': {
2322             'id': 'igorkle1',
2323             'title': 'Игорь Клейнер',
2324         },
2325     }, {
2326         'url': 'https://www.youtube.com/channel/UCiU1dHvZObB2iP6xkJ__Icw/playlists',
2327         'playlist_mincount': 17,
2328         'info_dict': {
2329             'id': 'UCiU1dHvZObB2iP6xkJ__Icw',
2330             'title': 'Chem Player',
2331         },
2332     }]
2333
2334
2335 class YoutubeSearchIE(SearchInfoExtractor, YoutubePlaylistIE):
2336     IE_DESC = 'YouTube.com searches'
2337     # there doesn't appear to be a real limit, for example if you search for
2338     # 'python' you get more than 8.000.000 results
2339     _MAX_RESULTS = float('inf')
2340     IE_NAME = 'youtube:search'
2341     _SEARCH_KEY = 'ytsearch'
2342     _EXTRA_QUERY_ARGS = {}
2343     _TESTS = []
2344
2345     def _get_n_results(self, query, n):
2346         """Get a specified number of results for a query"""
2347
2348         videos = []
2349         limit = n
2350
2351         url_query = {
2352             'search_query': query.encode('utf-8'),
2353         }
2354         url_query.update(self._EXTRA_QUERY_ARGS)
2355         result_url = 'https://www.youtube.com/results?' + compat_urllib_parse_urlencode(url_query)
2356
2357         for pagenum in itertools.count(1):
2358             data = self._download_json(
2359                 result_url, video_id='query "%s"' % query,
2360                 note='Downloading page %s' % pagenum,
2361                 errnote='Unable to download API page',
2362                 query={'spf': 'navigate'})
2363             html_content = data[1]['body']['content']
2364
2365             if 'class="search-message' in html_content:
2366                 raise ExtractorError(
2367                     '[youtube] No video results', expected=True)
2368
2369             new_videos = self._ids_to_results(orderedSet(re.findall(
2370                 r'href="/watch\?v=(.{11})', html_content)))
2371             videos += new_videos
2372             if not new_videos or len(videos) > limit:
2373                 break
2374             next_link = self._html_search_regex(
2375                 r'href="(/results\?[^"]*\bsp=[^"]+)"[^>]*>\s*<span[^>]+class="[^"]*\byt-uix-button-content\b[^"]*"[^>]*>Next',
2376                 html_content, 'next link', default=None)
2377             if next_link is None:
2378                 break
2379             result_url = compat_urlparse.urljoin('https://www.youtube.com/', next_link)
2380
2381         if len(videos) > n:
2382             videos = videos[:n]
2383         return self.playlist_result(videos, query)
2384
2385
2386 class YoutubeSearchDateIE(YoutubeSearchIE):
2387     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
2388     _SEARCH_KEY = 'ytsearchdate'
2389     IE_DESC = 'YouTube.com searches, newest videos first'
2390     _EXTRA_QUERY_ARGS = {'search_sort': 'video_date_uploaded'}
2391
2392
2393 class YoutubeSearchURLIE(YoutubePlaylistBaseInfoExtractor):
2394     IE_DESC = 'YouTube.com search URLs'
2395     IE_NAME = 'youtube:search_url'
2396     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?P<query>[^&]+)(?:[&]|$)'
2397     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})(?:[^"]*"[^>]+\btitle="(?P<title>[^"]+))?'
2398     _TESTS = [{
2399         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
2400         'playlist_mincount': 5,
2401         'info_dict': {
2402             'title': 'youtube-dl test video',
2403         }
2404     }, {
2405         'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
2406         'only_matching': True,
2407     }]
2408
2409     def _real_extract(self, url):
2410         mobj = re.match(self._VALID_URL, url)
2411         query = compat_urllib_parse_unquote_plus(mobj.group('query'))
2412         webpage = self._download_webpage(url, query)
2413         return self.playlist_result(self._process_page(webpage), playlist_title=query)
2414
2415
2416 class YoutubeShowIE(YoutubePlaylistsBaseInfoExtractor):
2417     IE_DESC = 'YouTube.com (multi-season) shows'
2418     _VALID_URL = r'https?://(?:www\.)?youtube\.com/show/(?P<id>[^?#]*)'
2419     IE_NAME = 'youtube:show'
2420     _TESTS = [{
2421         'url': 'https://www.youtube.com/show/airdisasters',
2422         'playlist_mincount': 5,
2423         'info_dict': {
2424             'id': 'airdisasters',
2425             'title': 'Air Disasters',
2426         }
2427     }]
2428
2429     def _real_extract(self, url):
2430         playlist_id = self._match_id(url)
2431         return super(YoutubeShowIE, self)._real_extract(
2432             'https://www.youtube.com/show/%s/playlists' % playlist_id)
2433
2434
2435 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
2436     """
2437     Base class for feed extractors
2438     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
2439     """
2440     _LOGIN_REQUIRED = True
2441
2442     @property
2443     def IE_NAME(self):
2444         return 'youtube:%s' % self._FEED_NAME
2445
2446     def _real_initialize(self):
2447         self._login()
2448
2449     def _real_extract(self, url):
2450         page = self._download_webpage(
2451             'https://www.youtube.com/feed/%s' % self._FEED_NAME, self._PLAYLIST_TITLE)
2452
2453         # The extraction process is the same as for playlists, but the regex
2454         # for the video ids doesn't contain an index
2455         ids = []
2456         more_widget_html = content_html = page
2457         for page_num in itertools.count(1):
2458             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
2459
2460             # 'recommended' feed has infinite 'load more' and each new portion spins
2461             # the same videos in (sometimes) slightly different order, so we'll check
2462             # for unicity and break when portion has no new videos
2463             new_ids = filter(lambda video_id: video_id not in ids, orderedSet(matches))
2464             if not new_ids:
2465                 break
2466
2467             ids.extend(new_ids)
2468
2469             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
2470             if not mobj:
2471                 break
2472
2473             more = self._download_json(
2474                 'https://youtube.com/%s' % mobj.group('more'), self._PLAYLIST_TITLE,
2475                 'Downloading page #%s' % page_num,
2476                 transform_source=uppercase_escape)
2477             content_html = more['content_html']
2478             more_widget_html = more['load_more_widget_html']
2479
2480         return self.playlist_result(
2481             self._ids_to_results(ids), playlist_title=self._PLAYLIST_TITLE)
2482
2483
2484 class YoutubeWatchLaterIE(YoutubePlaylistIE):
2485     IE_NAME = 'youtube:watchlater'
2486     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
2487     _VALID_URL = r'https?://(?:www\.)?youtube\.com/(?:feed/watch_later|(?:playlist|watch)\?(?:.+&)?list=WL)|:ytwatchlater'
2488
2489     _TESTS = [{
2490         'url': 'https://www.youtube.com/playlist?list=WL',
2491         'only_matching': True,
2492     }, {
2493         'url': 'https://www.youtube.com/watch?v=bCNU9TrbiRk&index=1&list=WL',
2494         'only_matching': True,
2495     }]
2496
2497     def _real_extract(self, url):
2498         _, video = self._check_download_just_video(url, 'WL')
2499         if video:
2500             return video
2501         _, playlist = self._extract_playlist('WL')
2502         return playlist
2503
2504
2505 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
2506     IE_NAME = 'youtube:favorites'
2507     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
2508     _VALID_URL = r'https?://(?:www\.)?youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
2509     _LOGIN_REQUIRED = True
2510
2511     def _real_extract(self, url):
2512         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
2513         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
2514         return self.url_result(playlist_id, 'YoutubePlaylist')
2515
2516
2517 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
2518     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
2519     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/recommended|:ytrec(?:ommended)?'
2520     _FEED_NAME = 'recommended'
2521     _PLAYLIST_TITLE = 'Youtube Recommended videos'
2522
2523
2524 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
2525     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
2526     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
2527     _FEED_NAME = 'subscriptions'
2528     _PLAYLIST_TITLE = 'Youtube Subscriptions'
2529
2530
2531 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
2532     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
2533     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/history|:ythistory'
2534     _FEED_NAME = 'history'
2535     _PLAYLIST_TITLE = 'Youtube History'
2536
2537
2538 class YoutubeTruncatedURLIE(InfoExtractor):
2539     IE_NAME = 'youtube:truncated_url'
2540     IE_DESC = False  # Do not list
2541     _VALID_URL = r'''(?x)
2542         (?:https?://)?
2543         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
2544         (?:watch\?(?:
2545             feature=[a-z_]+|
2546             annotation_id=annotation_[^&]+|
2547             x-yt-cl=[0-9]+|
2548             hl=[^&]*|
2549             t=[0-9]+
2550         )?
2551         |
2552             attribution_link\?a=[^&]+
2553         )
2554         $
2555     '''
2556
2557     _TESTS = [{
2558         'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
2559         'only_matching': True,
2560     }, {
2561         'url': 'https://www.youtube.com/watch?',
2562         'only_matching': True,
2563     }, {
2564         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
2565         'only_matching': True,
2566     }, {
2567         'url': 'https://www.youtube.com/watch?feature=foo',
2568         'only_matching': True,
2569     }, {
2570         'url': 'https://www.youtube.com/watch?hl=en-GB',
2571         'only_matching': True,
2572     }, {
2573         'url': 'https://www.youtube.com/watch?t=2372',
2574         'only_matching': True,
2575     }]
2576
2577     def _real_extract(self, url):
2578         raise ExtractorError(
2579             'Did you forget to quote the URL? Remember that & is a meta '
2580             'character in most shells, so you want to put the URL in quotes, '
2581             'like  youtube-dl '
2582             '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
2583             ' or simply  youtube-dl BaW_jenozKc  .',
2584             expected=True)
2585
2586
2587 class YoutubeTruncatedIDIE(InfoExtractor):
2588     IE_NAME = 'youtube:truncated_id'
2589     IE_DESC = False  # Do not list
2590     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
2591
2592     _TESTS = [{
2593         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
2594         'only_matching': True,
2595     }]
2596
2597     def _real_extract(self, url):
2598         video_id = self._match_id(url)
2599         raise ExtractorError(
2600             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
2601             expected=True)