[youtube] Add fallback for duration extraction (closes #11841)
[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'\.sig\|\|([a-zA-Z0-9$]+)\(', jscode,
1032             'Initial JS player signature function name')
1033
1034         jsi = JSInterpreter(jscode)
1035         initial_function = jsi.extract_function(funcname)
1036         return lambda s: initial_function([s])
1037
1038     def _parse_sig_swf(self, file_contents):
1039         swfi = SWFInterpreter(file_contents)
1040         TARGET_CLASSNAME = 'SignatureDecipher'
1041         searched_class = swfi.extract_class(TARGET_CLASSNAME)
1042         initial_function = swfi.extract_function(searched_class, 'decipher')
1043         return lambda s: initial_function([s])
1044
1045     def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
1046         """Turn the encrypted s field into a working signature"""
1047
1048         if player_url is None:
1049             raise ExtractorError('Cannot decrypt signature without player_url')
1050
1051         if player_url.startswith('//'):
1052             player_url = 'https:' + player_url
1053         try:
1054             player_id = (player_url, self._signature_cache_id(s))
1055             if player_id not in self._player_cache:
1056                 func = self._extract_signature_function(
1057                     video_id, player_url, s
1058                 )
1059                 self._player_cache[player_id] = func
1060             func = self._player_cache[player_id]
1061             if self._downloader.params.get('youtube_print_sig_code'):
1062                 self._print_sig_code(func, s)
1063             return func(s)
1064         except Exception as e:
1065             tb = traceback.format_exc()
1066             raise ExtractorError(
1067                 'Signature extraction failed: ' + tb, cause=e)
1068
1069     def _get_subtitles(self, video_id, webpage):
1070         try:
1071             subs_doc = self._download_xml(
1072                 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
1073                 video_id, note=False)
1074         except ExtractorError as err:
1075             self._downloader.report_warning('unable to download video subtitles: %s' % error_to_compat_str(err))
1076             return {}
1077
1078         sub_lang_list = {}
1079         for track in subs_doc.findall('track'):
1080             lang = track.attrib['lang_code']
1081             if lang in sub_lang_list:
1082                 continue
1083             sub_formats = []
1084             for ext in self._SUBTITLE_FORMATS:
1085                 params = compat_urllib_parse_urlencode({
1086                     'lang': lang,
1087                     'v': video_id,
1088                     'fmt': ext,
1089                     'name': track.attrib['name'].encode('utf-8'),
1090                 })
1091                 sub_formats.append({
1092                     'url': 'https://www.youtube.com/api/timedtext?' + params,
1093                     'ext': ext,
1094                 })
1095             sub_lang_list[lang] = sub_formats
1096         if not sub_lang_list:
1097             self._downloader.report_warning('video doesn\'t have subtitles')
1098             return {}
1099         return sub_lang_list
1100
1101     def _get_ytplayer_config(self, video_id, webpage):
1102         patterns = (
1103             # User data may contain arbitrary character sequences that may affect
1104             # JSON extraction with regex, e.g. when '};' is contained the second
1105             # regex won't capture the whole JSON. Yet working around by trying more
1106             # concrete regex first keeping in mind proper quoted string handling
1107             # to be implemented in future that will replace this workaround (see
1108             # https://github.com/rg3/youtube-dl/issues/7468,
1109             # https://github.com/rg3/youtube-dl/pull/7599)
1110             r';ytplayer\.config\s*=\s*({.+?});ytplayer',
1111             r';ytplayer\.config\s*=\s*({.+?});',
1112         )
1113         config = self._search_regex(
1114             patterns, webpage, 'ytplayer.config', default=None)
1115         if config:
1116             return self._parse_json(
1117                 uppercase_escape(config), video_id, fatal=False)
1118
1119     def _get_automatic_captions(self, video_id, webpage):
1120         """We need the webpage for getting the captions url, pass it as an
1121            argument to speed up the process."""
1122         self.to_screen('%s: Looking for automatic captions' % video_id)
1123         player_config = self._get_ytplayer_config(video_id, webpage)
1124         err_msg = 'Couldn\'t find automatic captions for %s' % video_id
1125         if not player_config:
1126             self._downloader.report_warning(err_msg)
1127             return {}
1128         try:
1129             args = player_config['args']
1130             caption_url = args.get('ttsurl')
1131             if caption_url:
1132                 timestamp = args['timestamp']
1133                 # We get the available subtitles
1134                 list_params = compat_urllib_parse_urlencode({
1135                     'type': 'list',
1136                     'tlangs': 1,
1137                     'asrs': 1,
1138                 })
1139                 list_url = caption_url + '&' + list_params
1140                 caption_list = self._download_xml(list_url, video_id)
1141                 original_lang_node = caption_list.find('track')
1142                 if original_lang_node is None:
1143                     self._downloader.report_warning('Video doesn\'t have automatic captions')
1144                     return {}
1145                 original_lang = original_lang_node.attrib['lang_code']
1146                 caption_kind = original_lang_node.attrib.get('kind', '')
1147
1148                 sub_lang_list = {}
1149                 for lang_node in caption_list.findall('target'):
1150                     sub_lang = lang_node.attrib['lang_code']
1151                     sub_formats = []
1152                     for ext in self._SUBTITLE_FORMATS:
1153                         params = compat_urllib_parse_urlencode({
1154                             'lang': original_lang,
1155                             'tlang': sub_lang,
1156                             'fmt': ext,
1157                             'ts': timestamp,
1158                             'kind': caption_kind,
1159                         })
1160                         sub_formats.append({
1161                             'url': caption_url + '&' + params,
1162                             'ext': ext,
1163                         })
1164                     sub_lang_list[sub_lang] = sub_formats
1165                 return sub_lang_list
1166
1167             # Some videos don't provide ttsurl but rather caption_tracks and
1168             # caption_translation_languages (e.g. 20LmZk1hakA)
1169             caption_tracks = args['caption_tracks']
1170             caption_translation_languages = args['caption_translation_languages']
1171             caption_url = compat_parse_qs(caption_tracks.split(',')[0])['u'][0]
1172             parsed_caption_url = compat_urllib_parse_urlparse(caption_url)
1173             caption_qs = compat_parse_qs(parsed_caption_url.query)
1174
1175             sub_lang_list = {}
1176             for lang in caption_translation_languages.split(','):
1177                 lang_qs = compat_parse_qs(compat_urllib_parse_unquote_plus(lang))
1178                 sub_lang = lang_qs.get('lc', [None])[0]
1179                 if not sub_lang:
1180                     continue
1181                 sub_formats = []
1182                 for ext in self._SUBTITLE_FORMATS:
1183                     caption_qs.update({
1184                         'tlang': [sub_lang],
1185                         'fmt': [ext],
1186                     })
1187                     sub_url = compat_urlparse.urlunparse(parsed_caption_url._replace(
1188                         query=compat_urllib_parse_urlencode(caption_qs, True)))
1189                     sub_formats.append({
1190                         'url': sub_url,
1191                         'ext': ext,
1192                     })
1193                 sub_lang_list[sub_lang] = sub_formats
1194             return sub_lang_list
1195         # An extractor error can be raise by the download process if there are
1196         # no automatic captions but there are subtitles
1197         except (KeyError, ExtractorError):
1198             self._downloader.report_warning(err_msg)
1199             return {}
1200
1201     def _mark_watched(self, video_id, video_info):
1202         playback_url = video_info.get('videostats_playback_base_url', [None])[0]
1203         if not playback_url:
1204             return
1205         parsed_playback_url = compat_urlparse.urlparse(playback_url)
1206         qs = compat_urlparse.parse_qs(parsed_playback_url.query)
1207
1208         # cpn generation algorithm is reverse engineered from base.js.
1209         # In fact it works even with dummy cpn.
1210         CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
1211         cpn = ''.join((CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16)))
1212
1213         qs.update({
1214             'ver': ['2'],
1215             'cpn': [cpn],
1216         })
1217         playback_url = compat_urlparse.urlunparse(
1218             parsed_playback_url._replace(query=compat_urllib_parse_urlencode(qs, True)))
1219
1220         self._download_webpage(
1221             playback_url, video_id, 'Marking watched',
1222             'Unable to mark watched', fatal=False)
1223
1224     @classmethod
1225     def extract_id(cls, url):
1226         mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
1227         if mobj is None:
1228             raise ExtractorError('Invalid URL: %s' % url)
1229         video_id = mobj.group(2)
1230         return video_id
1231
1232     def _extract_from_m3u8(self, manifest_url, video_id):
1233         url_map = {}
1234
1235         def _get_urls(_manifest):
1236             lines = _manifest.split('\n')
1237             urls = filter(lambda l: l and not l.startswith('#'),
1238                           lines)
1239             return urls
1240         manifest = self._download_webpage(manifest_url, video_id, 'Downloading formats manifest')
1241         formats_urls = _get_urls(manifest)
1242         for format_url in formats_urls:
1243             itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
1244             url_map[itag] = format_url
1245         return url_map
1246
1247     def _extract_annotations(self, video_id):
1248         url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
1249         return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
1250
1251     def _real_extract(self, url):
1252         url, smuggled_data = unsmuggle_url(url, {})
1253
1254         proto = (
1255             'http' if self._downloader.params.get('prefer_insecure', False)
1256             else 'https')
1257
1258         start_time = None
1259         end_time = None
1260         parsed_url = compat_urllib_parse_urlparse(url)
1261         for component in [parsed_url.fragment, parsed_url.query]:
1262             query = compat_parse_qs(component)
1263             if start_time is None and 't' in query:
1264                 start_time = parse_duration(query['t'][0])
1265             if start_time is None and 'start' in query:
1266                 start_time = parse_duration(query['start'][0])
1267             if end_time is None and 'end' in query:
1268                 end_time = parse_duration(query['end'][0])
1269
1270         # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1271         mobj = re.search(self._NEXT_URL_RE, url)
1272         if mobj:
1273             url = proto + '://www.youtube.com/' + compat_urllib_parse_unquote(mobj.group(1)).lstrip('/')
1274         video_id = self.extract_id(url)
1275
1276         # Get video webpage
1277         url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1&bpctr=9999999999' % video_id
1278         video_webpage = self._download_webpage(url, video_id)
1279
1280         # Attempt to extract SWF player URL
1281         mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1282         if mobj is not None:
1283             player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
1284         else:
1285             player_url = None
1286
1287         dash_mpds = []
1288
1289         def add_dash_mpd(video_info):
1290             dash_mpd = video_info.get('dashmpd')
1291             if dash_mpd and dash_mpd[0] not in dash_mpds:
1292                 dash_mpds.append(dash_mpd[0])
1293
1294         # Get video info
1295         embed_webpage = None
1296         is_live = None
1297         if re.search(r'player-age-gate-content">', video_webpage) is not None:
1298             age_gate = True
1299             # We simulate the access to the video from www.youtube.com/v/{video_id}
1300             # this can be viewed without login into Youtube
1301             url = proto + '://www.youtube.com/embed/%s' % video_id
1302             embed_webpage = self._download_webpage(url, video_id, 'Downloading embed webpage')
1303             data = compat_urllib_parse_urlencode({
1304                 'video_id': video_id,
1305                 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
1306                 'sts': self._search_regex(
1307                     r'"sts"\s*:\s*(\d+)', embed_webpage, 'sts', default=''),
1308             })
1309             video_info_url = proto + '://www.youtube.com/get_video_info?' + data
1310             video_info_webpage = self._download_webpage(
1311                 video_info_url, video_id,
1312                 note='Refetching age-gated info webpage',
1313                 errnote='unable to download video info webpage')
1314             video_info = compat_parse_qs(video_info_webpage)
1315             add_dash_mpd(video_info)
1316         else:
1317             age_gate = False
1318             video_info = None
1319             # Try looking directly into the video webpage
1320             ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1321             if ytplayer_config:
1322                 args = ytplayer_config['args']
1323                 if args.get('url_encoded_fmt_stream_map'):
1324                     # Convert to the same format returned by compat_parse_qs
1325                     video_info = dict((k, [v]) for k, v in args.items())
1326                     add_dash_mpd(video_info)
1327                 # Rental video is not rented but preview is available (e.g.
1328                 # https://www.youtube.com/watch?v=yYr8q0y5Jfg,
1329                 # https://github.com/rg3/youtube-dl/issues/10532)
1330                 if not video_info and args.get('ypc_vid'):
1331                     return self.url_result(
1332                         args['ypc_vid'], YoutubeIE.ie_key(), video_id=args['ypc_vid'])
1333                 if args.get('livestream') == '1' or args.get('live_playback') == 1:
1334                     is_live = True
1335             if not video_info or self._downloader.params.get('youtube_include_dash_manifest', True):
1336                 # We also try looking in get_video_info since it may contain different dashmpd
1337                 # URL that points to a DASH manifest with possibly different itag set (some itags
1338                 # are missing from DASH manifest pointed by webpage's dashmpd, some - from DASH
1339                 # manifest pointed by get_video_info's dashmpd).
1340                 # The general idea is to take a union of itags of both DASH manifests (for example
1341                 # video with such 'manifest behavior' see https://github.com/rg3/youtube-dl/issues/6093)
1342                 self.report_video_info_webpage_download(video_id)
1343                 for el_type in ['&el=info', '&el=embedded', '&el=detailpage', '&el=vevo', '']:
1344                     video_info_url = (
1345                         '%s://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
1346                         % (proto, video_id, el_type))
1347                     video_info_webpage = self._download_webpage(
1348                         video_info_url,
1349                         video_id, note=False,
1350                         errnote='unable to download video info webpage')
1351                     get_video_info = compat_parse_qs(video_info_webpage)
1352                     if get_video_info.get('use_cipher_signature') != ['True']:
1353                         add_dash_mpd(get_video_info)
1354                     if not video_info:
1355                         video_info = get_video_info
1356                     if 'token' in get_video_info:
1357                         # Different get_video_info requests may report different results, e.g.
1358                         # some may report video unavailability, but some may serve it without
1359                         # any complaint (see https://github.com/rg3/youtube-dl/issues/7362,
1360                         # the original webpage as well as el=info and el=embedded get_video_info
1361                         # requests report video unavailability due to geo restriction while
1362                         # el=detailpage succeeds and returns valid data). This is probably
1363                         # due to YouTube measures against IP ranges of hosting providers.
1364                         # Working around by preferring the first succeeded video_info containing
1365                         # the token if no such video_info yet was found.
1366                         if 'token' not in video_info:
1367                             video_info = get_video_info
1368                         break
1369         if 'token' not in video_info:
1370             if 'reason' in video_info:
1371                 if 'The uploader has not made this video available in your country.' in video_info['reason']:
1372                     regions_allowed = self._html_search_meta('regionsAllowed', video_webpage, default=None)
1373                     if regions_allowed:
1374                         raise ExtractorError('YouTube said: This video is available in %s only' % (
1375                             ', '.join(map(ISO3166Utils.short2full, regions_allowed.split(',')))),
1376                             expected=True)
1377                 raise ExtractorError(
1378                     'YouTube said: %s' % video_info['reason'][0],
1379                     expected=True, video_id=video_id)
1380             else:
1381                 raise ExtractorError(
1382                     '"token" parameter not in video info for unknown reason',
1383                     video_id=video_id)
1384
1385         # title
1386         if 'title' in video_info:
1387             video_title = video_info['title'][0]
1388         else:
1389             self._downloader.report_warning('Unable to extract video title')
1390             video_title = '_'
1391
1392         # description
1393         video_description = get_element_by_id("eow-description", video_webpage)
1394         if video_description:
1395             video_description = re.sub(r'''(?x)
1396                 <a\s+
1397                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1398                     (?:title|href)="([^"]+)"\s+
1399                     (?:[a-zA-Z-]+="[^"]*"\s+)*?
1400                     class="[^"]*"[^>]*>
1401                 [^<]+\.{3}\s*
1402                 </a>
1403             ''', r'\1', video_description)
1404             video_description = clean_html(video_description)
1405         else:
1406             fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
1407             if fd_mobj:
1408                 video_description = unescapeHTML(fd_mobj.group(1))
1409             else:
1410                 video_description = ''
1411
1412         if 'multifeed_metadata_list' in video_info and not smuggled_data.get('force_singlefeed', False):
1413             if not self._downloader.params.get('noplaylist'):
1414                 entries = []
1415                 feed_ids = []
1416                 multifeed_metadata_list = video_info['multifeed_metadata_list'][0]
1417                 for feed in multifeed_metadata_list.split(','):
1418                     # Unquote should take place before split on comma (,) since textual
1419                     # fields may contain comma as well (see
1420                     # https://github.com/rg3/youtube-dl/issues/8536)
1421                     feed_data = compat_parse_qs(compat_urllib_parse_unquote_plus(feed))
1422                     entries.append({
1423                         '_type': 'url_transparent',
1424                         'ie_key': 'Youtube',
1425                         'url': smuggle_url(
1426                             '%s://www.youtube.com/watch?v=%s' % (proto, feed_data['id'][0]),
1427                             {'force_singlefeed': True}),
1428                         'title': '%s (%s)' % (video_title, feed_data['title'][0]),
1429                     })
1430                     feed_ids.append(feed_data['id'][0])
1431                 self.to_screen(
1432                     'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
1433                     % (', '.join(feed_ids), video_id))
1434                 return self.playlist_result(entries, video_id, video_title, video_description)
1435             self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1436
1437         if 'view_count' in video_info:
1438             view_count = int(video_info['view_count'][0])
1439         else:
1440             view_count = None
1441
1442         # Check for "rental" videos
1443         if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
1444             raise ExtractorError('"rental" videos not supported')
1445
1446         # Start extracting information
1447         self.report_information_extraction(video_id)
1448
1449         # uploader
1450         if 'author' not in video_info:
1451             raise ExtractorError('Unable to extract uploader name')
1452         video_uploader = compat_urllib_parse_unquote_plus(video_info['author'][0])
1453
1454         # uploader_id
1455         video_uploader_id = None
1456         video_uploader_url = None
1457         mobj = re.search(
1458             r'<link itemprop="url" href="(?P<uploader_url>https?://www.youtube.com/(?:user|channel)/(?P<uploader_id>[^"]+))">',
1459             video_webpage)
1460         if mobj is not None:
1461             video_uploader_id = mobj.group('uploader_id')
1462             video_uploader_url = mobj.group('uploader_url')
1463         else:
1464             self._downloader.report_warning('unable to extract uploader nickname')
1465
1466         # thumbnail image
1467         # We try first to get a high quality image:
1468         m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
1469                             video_webpage, re.DOTALL)
1470         if m_thumb is not None:
1471             video_thumbnail = m_thumb.group(1)
1472         elif 'thumbnail_url' not in video_info:
1473             self._downloader.report_warning('unable to extract video thumbnail')
1474             video_thumbnail = None
1475         else:   # don't panic if we can't find it
1476             video_thumbnail = compat_urllib_parse_unquote_plus(video_info['thumbnail_url'][0])
1477
1478         # upload date
1479         upload_date = self._html_search_meta(
1480             'datePublished', video_webpage, 'upload date', default=None)
1481         if not upload_date:
1482             upload_date = self._search_regex(
1483                 [r'(?s)id="eow-date.*?>(.*?)</span>',
1484                  r'id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live|Started) on (.+?)</strong>'],
1485                 video_webpage, 'upload date', default=None)
1486             if upload_date:
1487                 upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
1488         upload_date = unified_strdate(upload_date)
1489
1490         video_license = self._html_search_regex(
1491             r'<h4[^>]+class="title"[^>]*>\s*License\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li',
1492             video_webpage, 'license', default=None)
1493
1494         m_music = re.search(
1495             r'<h4[^>]+class="title"[^>]*>\s*Music\s*</h4>\s*<ul[^>]*>\s*<li>(?P<title>.+?) by (?P<creator>.+?)(?:\(.+?\))?</li',
1496             video_webpage)
1497         if m_music:
1498             video_alt_title = remove_quotes(unescapeHTML(m_music.group('title')))
1499             video_creator = clean_html(m_music.group('creator'))
1500         else:
1501             video_alt_title = video_creator = None
1502
1503         m_episode = re.search(
1504             r'<div[^>]+id="watch7-headline"[^>]*>\s*<span[^>]*>.*?>(?P<series>[^<]+)</a></b>\s*S(?P<season>\d+)\s*•\s*E(?P<episode>\d+)</span>',
1505             video_webpage)
1506         if m_episode:
1507             series = m_episode.group('series')
1508             season_number = int(m_episode.group('season'))
1509             episode_number = int(m_episode.group('episode'))
1510         else:
1511             series = season_number = episode_number = None
1512
1513         m_cat_container = self._search_regex(
1514             r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
1515             video_webpage, 'categories', default=None)
1516         if m_cat_container:
1517             category = self._html_search_regex(
1518                 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
1519                 default=None)
1520             video_categories = None if category is None else [category]
1521         else:
1522             video_categories = None
1523
1524         video_tags = [
1525             unescapeHTML(m.group('content'))
1526             for m in re.finditer(self._meta_regex('og:video:tag'), video_webpage)]
1527
1528         def _extract_count(count_name):
1529             return str_to_int(self._search_regex(
1530                 r'-%s-button[^>]+><span[^>]+class="yt-uix-button-content"[^>]*>([\d,]+)</span>'
1531                 % re.escape(count_name),
1532                 video_webpage, count_name, default=None))
1533
1534         like_count = _extract_count('like')
1535         dislike_count = _extract_count('dislike')
1536
1537         # subtitles
1538         video_subtitles = self.extract_subtitles(video_id, video_webpage)
1539         automatic_captions = self.extract_automatic_captions(video_id, video_webpage)
1540
1541         video_duration = try_get(
1542             video_info, lambda x: int_or_none(x['length_seconds'][0]))
1543         if not video_duration:
1544             video_duration = parse_duration(self._html_search_meta(
1545                 'duration', video_webpage, 'video duration'))
1546
1547         # annotations
1548         video_annotations = None
1549         if self._downloader.params.get('writeannotations', False):
1550             video_annotations = self._extract_annotations(video_id)
1551
1552         def _map_to_format_list(urlmap):
1553             formats = []
1554             for itag, video_real_url in urlmap.items():
1555                 dct = {
1556                     'format_id': itag,
1557                     'url': video_real_url,
1558                     'player_url': player_url,
1559                 }
1560                 if itag in self._formats:
1561                     dct.update(self._formats[itag])
1562                 formats.append(dct)
1563             return formats
1564
1565         if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
1566             self.report_rtmp_download()
1567             formats = [{
1568                 'format_id': '_rtmp',
1569                 'protocol': 'rtmp',
1570                 'url': video_info['conn'][0],
1571                 'player_url': player_url,
1572             }]
1573         elif len(video_info.get('url_encoded_fmt_stream_map', [''])[0]) >= 1 or len(video_info.get('adaptive_fmts', [''])[0]) >= 1:
1574             encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts', [''])[0]
1575             if 'rtmpe%3Dyes' in encoded_url_map:
1576                 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
1577             formats_spec = {}
1578             fmt_list = video_info.get('fmt_list', [''])[0]
1579             if fmt_list:
1580                 for fmt in fmt_list.split(','):
1581                     spec = fmt.split('/')
1582                     if len(spec) > 1:
1583                         width_height = spec[1].split('x')
1584                         if len(width_height) == 2:
1585                             formats_spec[spec[0]] = {
1586                                 'resolution': spec[1],
1587                                 'width': int_or_none(width_height[0]),
1588                                 'height': int_or_none(width_height[1]),
1589                             }
1590             formats = []
1591             for url_data_str in encoded_url_map.split(','):
1592                 url_data = compat_parse_qs(url_data_str)
1593                 if 'itag' not in url_data or 'url' not in url_data:
1594                     continue
1595                 format_id = url_data['itag'][0]
1596                 url = url_data['url'][0]
1597
1598                 if 'sig' in url_data:
1599                     url += '&signature=' + url_data['sig'][0]
1600                 elif 's' in url_data:
1601                     encrypted_sig = url_data['s'][0]
1602                     ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
1603
1604                     jsplayer_url_json = self._search_regex(
1605                         ASSETS_RE,
1606                         embed_webpage if age_gate else video_webpage,
1607                         'JS player URL (1)', default=None)
1608                     if not jsplayer_url_json and not age_gate:
1609                         # We need the embed website after all
1610                         if embed_webpage is None:
1611                             embed_url = proto + '://www.youtube.com/embed/%s' % video_id
1612                             embed_webpage = self._download_webpage(
1613                                 embed_url, video_id, 'Downloading embed webpage')
1614                         jsplayer_url_json = self._search_regex(
1615                             ASSETS_RE, embed_webpage, 'JS player URL')
1616
1617                     player_url = json.loads(jsplayer_url_json)
1618                     if player_url is None:
1619                         player_url_json = self._search_regex(
1620                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
1621                             video_webpage, 'age gate player URL')
1622                         player_url = json.loads(player_url_json)
1623
1624                     if self._downloader.params.get('verbose'):
1625                         if player_url is None:
1626                             player_version = 'unknown'
1627                             player_desc = 'unknown'
1628                         else:
1629                             if player_url.endswith('swf'):
1630                                 player_version = self._search_regex(
1631                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
1632                                     'flash player', fatal=False)
1633                                 player_desc = 'flash player %s' % player_version
1634                             else:
1635                                 player_version = self._search_regex(
1636                                     [r'html5player-([^/]+?)(?:/html5player(?:-new)?)?\.js', r'(?:www|player)-([^/]+)/base\.js'],
1637                                     player_url,
1638                                     'html5 player', fatal=False)
1639                                 player_desc = 'html5 player %s' % player_version
1640
1641                         parts_sizes = self._signature_cache_id(encrypted_sig)
1642                         self.to_screen('{%s} signature length %s, %s' %
1643                                        (format_id, parts_sizes, player_desc))
1644
1645                     signature = self._decrypt_signature(
1646                         encrypted_sig, video_id, player_url, age_gate)
1647                     url += '&signature=' + signature
1648                 if 'ratebypass' not in url:
1649                     url += '&ratebypass=yes'
1650
1651                 dct = {
1652                     'format_id': format_id,
1653                     'url': url,
1654                     'player_url': player_url,
1655                 }
1656                 if format_id in self._formats:
1657                     dct.update(self._formats[format_id])
1658                 if format_id in formats_spec:
1659                     dct.update(formats_spec[format_id])
1660
1661                 # Some itags are not included in DASH manifest thus corresponding formats will
1662                 # lack metadata (see https://github.com/rg3/youtube-dl/pull/5993).
1663                 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
1664                 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
1665                 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
1666
1667                 more_fields = {
1668                     'filesize': int_or_none(url_data.get('clen', [None])[0]),
1669                     'tbr': float_or_none(url_data.get('bitrate', [None])[0], 1000),
1670                     'width': width,
1671                     'height': height,
1672                     'fps': int_or_none(url_data.get('fps', [None])[0]),
1673                     'format_note': url_data.get('quality_label', [None])[0] or url_data.get('quality', [None])[0],
1674                 }
1675                 for key, value in more_fields.items():
1676                     if value:
1677                         dct[key] = value
1678                 type_ = url_data.get('type', [None])[0]
1679                 if type_:
1680                     type_split = type_.split(';')
1681                     kind_ext = type_split[0].split('/')
1682                     if len(kind_ext) == 2:
1683                         kind, _ = kind_ext
1684                         dct['ext'] = mimetype2ext(type_split[0])
1685                         if kind in ('audio', 'video'):
1686                             codecs = None
1687                             for mobj in re.finditer(
1688                                     r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?)(?P<val>.+?)(?P=quote)(?:;|$)', type_):
1689                                 if mobj.group('key') == 'codecs':
1690                                     codecs = mobj.group('val')
1691                                     break
1692                             if codecs:
1693                                 codecs = codecs.split(',')
1694                                 if len(codecs) == 2:
1695                                     acodec, vcodec = codecs[1], codecs[0]
1696                                 else:
1697                                     acodec, vcodec = (codecs[0], 'none') if kind == 'audio' else ('none', codecs[0])
1698                                 dct.update({
1699                                     'acodec': acodec,
1700                                     'vcodec': vcodec,
1701                                 })
1702                 formats.append(dct)
1703         elif video_info.get('hlsvp'):
1704             manifest_url = video_info['hlsvp'][0]
1705             url_map = self._extract_from_m3u8(manifest_url, video_id)
1706             formats = _map_to_format_list(url_map)
1707             # Accept-Encoding header causes failures in live streams on Youtube and Youtube Gaming
1708             for a_format in formats:
1709                 a_format.setdefault('http_headers', {})['Youtubedl-no-compression'] = 'True'
1710         else:
1711             unavailable_message = self._html_search_regex(
1712                 r'(?s)<h1[^>]+id="unavailable-message"[^>]*>(.+?)</h1>',
1713                 video_webpage, 'unavailable message', default=None)
1714             if unavailable_message:
1715                 raise ExtractorError(unavailable_message, expected=True)
1716             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1717
1718         # Look for the DASH manifest
1719         if self._downloader.params.get('youtube_include_dash_manifest', True):
1720             dash_mpd_fatal = True
1721             for mpd_url in dash_mpds:
1722                 dash_formats = {}
1723                 try:
1724                     def decrypt_sig(mobj):
1725                         s = mobj.group(1)
1726                         dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
1727                         return '/signature/%s' % dec_s
1728
1729                     mpd_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, mpd_url)
1730
1731                     for df in self._extract_mpd_formats(
1732                             mpd_url, video_id, fatal=dash_mpd_fatal,
1733                             formats_dict=self._formats):
1734                         # Do not overwrite DASH format found in some previous DASH manifest
1735                         if df['format_id'] not in dash_formats:
1736                             dash_formats[df['format_id']] = df
1737                         # Additional DASH manifests may end up in HTTP Error 403 therefore
1738                         # allow them to fail without bug report message if we already have
1739                         # some DASH manifest succeeded. This is temporary workaround to reduce
1740                         # burst of bug reports until we figure out the reason and whether it
1741                         # can be fixed at all.
1742                         dash_mpd_fatal = False
1743                 except (ExtractorError, KeyError) as e:
1744                     self.report_warning(
1745                         'Skipping DASH manifest: %r' % e, video_id)
1746                 if dash_formats:
1747                     # Remove the formats we found through non-DASH, they
1748                     # contain less info and it can be wrong, because we use
1749                     # fixed values (for example the resolution). See
1750                     # https://github.com/rg3/youtube-dl/issues/5774 for an
1751                     # example.
1752                     formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
1753                     formats.extend(dash_formats.values())
1754
1755         # Check for malformed aspect ratio
1756         stretched_m = re.search(
1757             r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
1758             video_webpage)
1759         if stretched_m:
1760             w = float(stretched_m.group('w'))
1761             h = float(stretched_m.group('h'))
1762             # yt:stretch may hold invalid ratio data (e.g. for Q39EVAstoRM ratio is 17:0).
1763             # We will only process correct ratios.
1764             if w > 0 and h > 0:
1765                 ratio = w / h
1766                 for f in formats:
1767                     if f.get('vcodec') != 'none':
1768                         f['stretched_ratio'] = ratio
1769
1770         self._sort_formats(formats)
1771
1772         self.mark_watched(video_id, video_info)
1773
1774         return {
1775             'id': video_id,
1776             'uploader': video_uploader,
1777             'uploader_id': video_uploader_id,
1778             'uploader_url': video_uploader_url,
1779             'upload_date': upload_date,
1780             'license': video_license,
1781             'creator': video_creator,
1782             'title': video_title,
1783             'alt_title': video_alt_title,
1784             'thumbnail': video_thumbnail,
1785             'description': video_description,
1786             'categories': video_categories,
1787             'tags': video_tags,
1788             'subtitles': video_subtitles,
1789             'automatic_captions': automatic_captions,
1790             'duration': video_duration,
1791             'age_limit': 18 if age_gate else 0,
1792             'annotations': video_annotations,
1793             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
1794             'view_count': view_count,
1795             'like_count': like_count,
1796             'dislike_count': dislike_count,
1797             'average_rating': float_or_none(video_info.get('avg_rating', [None])[0]),
1798             'formats': formats,
1799             'is_live': is_live,
1800             'start_time': start_time,
1801             'end_time': end_time,
1802             'series': series,
1803             'season_number': season_number,
1804             'episode_number': episode_number,
1805         }
1806
1807
1808 class YoutubeSharedVideoIE(InfoExtractor):
1809     _VALID_URL = r'(?:https?:)?//(?:www\.)?youtube\.com/shared\?.*\bci=(?P<id>[0-9A-Za-z_-]{11})'
1810     IE_NAME = 'youtube:shared'
1811
1812     _TEST = {
1813         'url': 'https://www.youtube.com/shared?ci=1nEzmT-M4fU',
1814         'info_dict': {
1815             'id': 'uPDB5I9wfp8',
1816             'ext': 'webm',
1817             'title': 'Pocoyo: 90 minutos de episódios completos Português para crianças - PARTE 3',
1818             'description': 'md5:d9e4d9346a2dfff4c7dc4c8cec0f546d',
1819             'upload_date': '20160219',
1820             'uploader': 'Pocoyo - Português (BR)',
1821             'uploader_id': 'PocoyoBrazil',
1822         },
1823         'add_ie': ['Youtube'],
1824         'params': {
1825             # There are already too many Youtube downloads
1826             'skip_download': True,
1827         },
1828     }
1829
1830     def _real_extract(self, url):
1831         video_id = self._match_id(url)
1832
1833         webpage = self._download_webpage(url, video_id)
1834
1835         real_video_id = self._html_search_meta(
1836             'videoId', webpage, 'YouTube video id', fatal=True)
1837
1838         return self.url_result(real_video_id, YoutubeIE.ie_key())
1839
1840
1841 class YoutubePlaylistIE(YoutubePlaylistBaseInfoExtractor):
1842     IE_DESC = 'YouTube.com playlists'
1843     _VALID_URL = r"""(?x)(?:
1844                         (?:https?://)?
1845                         (?:\w+\.)?
1846                         (?:
1847                             youtube\.com/
1848                             (?:
1849                                (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
1850                                \? (?:.*?[&;])*? (?:p|a|list)=
1851                             |  p/
1852                             )|
1853                             youtu\.be/[0-9A-Za-z_-]{11}\?.*?\blist=
1854                         )
1855                         (
1856                             (?:PL|LL|EC|UU|FL|RD|UL)?[0-9A-Za-z-_]{10,}
1857                             # Top tracks, they can also include dots
1858                             |(?:MC)[\w\.]*
1859                         )
1860                         .*
1861                      |
1862                         ((?:PL|LL|EC|UU|FL|RD|UL)[0-9A-Za-z-_]{10,})
1863                      )"""
1864     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s&disable_polymer=true'
1865     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)(?:[^>]+>(?P<title>[^<]+))?'
1866     IE_NAME = 'youtube:playlist'
1867     _TESTS = [{
1868         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1869         'info_dict': {
1870             'title': 'ytdl test PL',
1871             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1872         },
1873         'playlist_count': 3,
1874     }, {
1875         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1876         'info_dict': {
1877             'id': 'PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1878             'title': 'YDL_Empty_List',
1879         },
1880         'playlist_count': 0,
1881         'skip': 'This playlist is private',
1882     }, {
1883         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1884         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1885         'info_dict': {
1886             'title': '29C3: Not my department',
1887             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1888         },
1889         'playlist_count': 95,
1890     }, {
1891         'note': 'issue #673',
1892         'url': 'PLBB231211A4F62143',
1893         'info_dict': {
1894             'title': '[OLD]Team Fortress 2 (Class-based LP)',
1895             'id': 'PLBB231211A4F62143',
1896         },
1897         'playlist_mincount': 26,
1898     }, {
1899         'note': 'Large playlist',
1900         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1901         'info_dict': {
1902             'title': 'Uploads from Cauchemar',
1903             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
1904         },
1905         'playlist_mincount': 799,
1906     }, {
1907         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1908         'info_dict': {
1909             'title': 'YDL_safe_search',
1910             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1911         },
1912         'playlist_count': 2,
1913         'skip': 'This playlist is private',
1914     }, {
1915         'note': 'embedded',
1916         'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1917         'playlist_count': 4,
1918         'info_dict': {
1919             'title': 'JODA15',
1920             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1921         }
1922     }, {
1923         'note': 'Embedded SWF player',
1924         'url': 'https://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1925         'playlist_count': 4,
1926         'info_dict': {
1927             'title': 'JODA7',
1928             'id': 'YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ',
1929         }
1930     }, {
1931         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
1932         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
1933         'info_dict': {
1934             'title': 'Uploads from Interstellar Movie',
1935             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
1936         },
1937         'playlist_mincount': 21,
1938     }, {
1939         # Playlist URL that does not actually serve a playlist
1940         'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
1941         'info_dict': {
1942             'id': 'FqZTN594JQw',
1943             'ext': 'webm',
1944             'title': "Smiley's People 01 detective, Adventure Series, Action",
1945             'uploader': 'STREEM',
1946             'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
1947             'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
1948             'upload_date': '20150526',
1949             'license': 'Standard YouTube License',
1950             'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
1951             'categories': ['People & Blogs'],
1952             'tags': list,
1953             'like_count': int,
1954             'dislike_count': int,
1955         },
1956         'params': {
1957             'skip_download': True,
1958         },
1959         'add_ie': [YoutubeIE.ie_key()],
1960     }, {
1961         'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
1962         'info_dict': {
1963             'id': 'yeWKywCrFtk',
1964             'ext': 'mp4',
1965             'title': 'Small Scale Baler and Braiding Rugs',
1966             'uploader': 'Backus-Page House Museum',
1967             'uploader_id': 'backuspagemuseum',
1968             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
1969             'upload_date': '20161008',
1970             'license': 'Standard YouTube License',
1971             'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
1972             'categories': ['Nonprofits & Activism'],
1973             'tags': list,
1974             'like_count': int,
1975             'dislike_count': int,
1976         },
1977         'params': {
1978             'noplaylist': True,
1979             'skip_download': True,
1980         },
1981     }, {
1982         'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
1983         'only_matching': True,
1984     }]
1985
1986     def _real_initialize(self):
1987         self._login()
1988
1989     def _extract_mix(self, playlist_id):
1990         # The mixes are generated from a single video
1991         # the id of the playlist is just 'RD' + video_id
1992         ids = []
1993         last_id = playlist_id[-11:]
1994         for n in itertools.count(1):
1995             url = 'https://youtube.com/watch?v=%s&list=%s' % (last_id, playlist_id)
1996             webpage = self._download_webpage(
1997                 url, playlist_id, 'Downloading page {0} of Youtube mix'.format(n))
1998             new_ids = orderedSet(re.findall(
1999                 r'''(?xs)data-video-username=".*?".*?
2000                            href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
2001                 webpage))
2002             # Fetch new pages until all the videos are repeated, it seems that
2003             # there are always 51 unique videos.
2004             new_ids = [_id for _id in new_ids if _id not in ids]
2005             if not new_ids:
2006                 break
2007             ids.extend(new_ids)
2008             last_id = ids[-1]
2009
2010         url_results = self._ids_to_results(ids)
2011
2012         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
2013         title_span = (
2014             search_title('playlist-title') or
2015             search_title('title long-title') or
2016             search_title('title'))
2017         title = clean_html(title_span)
2018
2019         return self.playlist_result(url_results, playlist_id, title)
2020
2021     def _extract_playlist(self, playlist_id):
2022         url = self._TEMPLATE_URL % playlist_id
2023         page = self._download_webpage(url, playlist_id)
2024
2025         # the yt-alert-message now has tabindex attribute (see https://github.com/rg3/youtube-dl/issues/11604)
2026         for match in re.findall(r'<div class="yt-alert-message"[^>]*>([^<]+)</div>', page):
2027             match = match.strip()
2028             # Check if the playlist exists or is private
2029             mobj = re.match(r'[^<]*(?:The|This) playlist (?P<reason>does not exist|is private)[^<]*', match)
2030             if mobj:
2031                 reason = mobj.group('reason')
2032                 message = 'This playlist %s' % reason
2033                 if 'private' in reason:
2034                     message += ', use --username or --netrc to access it'
2035                 message += '.'
2036                 raise ExtractorError(message, expected=True)
2037             elif re.match(r'[^<]*Invalid parameters[^<]*', match):
2038                 raise ExtractorError(
2039                     'Invalid parameters. Maybe URL is incorrect.',
2040                     expected=True)
2041             elif re.match(r'[^<]*Choose your language[^<]*', match):
2042                 continue
2043             else:
2044                 self.report_warning('Youtube gives an alert message: ' + match)
2045
2046         playlist_title = self._html_search_regex(
2047             r'(?s)<h1 class="pl-header-title[^"]*"[^>]*>\s*(.*?)\s*</h1>',
2048             page, 'title', default=None)
2049
2050         has_videos = True
2051
2052         if not playlist_title:
2053             try:
2054                 # Some playlist URLs don't actually serve a playlist (e.g.
2055                 # https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4)
2056                 next(self._entries(page, playlist_id))
2057             except StopIteration:
2058                 has_videos = False
2059
2060         return has_videos, self.playlist_result(
2061             self._entries(page, playlist_id), playlist_id, playlist_title)
2062
2063     def _check_download_just_video(self, url, playlist_id):
2064         # Check if it's a video-specific URL
2065         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
2066         video_id = query_dict.get('v', [None])[0] or self._search_regex(
2067             r'(?:^|//)youtu\.be/([0-9A-Za-z_-]{11})', url,
2068             'video id', default=None)
2069         if video_id:
2070             if self._downloader.params.get('noplaylist'):
2071                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
2072                 return video_id, self.url_result(video_id, 'Youtube', video_id=video_id)
2073             else:
2074                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
2075                 return video_id, None
2076         return None, None
2077
2078     def _real_extract(self, url):
2079         # Extract playlist id
2080         mobj = re.match(self._VALID_URL, url)
2081         if mobj is None:
2082             raise ExtractorError('Invalid URL: %s' % url)
2083         playlist_id = mobj.group(1) or mobj.group(2)
2084
2085         video_id, video = self._check_download_just_video(url, playlist_id)
2086         if video:
2087             return video
2088
2089         if playlist_id.startswith(('RD', 'UL', 'PU')):
2090             # Mixes require a custom extraction process
2091             return self._extract_mix(playlist_id)
2092
2093         has_videos, playlist = self._extract_playlist(playlist_id)
2094         if has_videos or not video_id:
2095             return playlist
2096
2097         # Some playlist URLs don't actually serve a playlist (see
2098         # https://github.com/rg3/youtube-dl/issues/10537).
2099         # Fallback to plain video extraction if there is a video id
2100         # along with playlist id.
2101         return self.url_result(video_id, 'Youtube', video_id=video_id)
2102
2103
2104 class YoutubeChannelIE(YoutubePlaylistBaseInfoExtractor):
2105     IE_DESC = 'YouTube.com channels'
2106     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/(?P<id>[0-9A-Za-z_-]+)'
2107     _TEMPLATE_URL = 'https://www.youtube.com/channel/%s/videos'
2108     _VIDEO_RE = r'(?:title="(?P<title>[^"]+)"[^>]+)?href="/watch\?v=(?P<id>[0-9A-Za-z_-]+)&?'
2109     IE_NAME = 'youtube:channel'
2110     _TESTS = [{
2111         'note': 'paginated channel',
2112         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
2113         'playlist_mincount': 91,
2114         'info_dict': {
2115             'id': 'UUKfVa3S1e4PHvxWcwyMMg8w',
2116             'title': 'Uploads from lex will',
2117         }
2118     }, {
2119         'note': 'Age restricted channel',
2120         # from https://www.youtube.com/user/DeusExOfficial
2121         'url': 'https://www.youtube.com/channel/UCs0ifCMCm1icqRbqhUINa0w',
2122         'playlist_mincount': 64,
2123         'info_dict': {
2124             'id': 'UUs0ifCMCm1icqRbqhUINa0w',
2125             'title': 'Uploads from Deus Ex',
2126         },
2127     }]
2128
2129     @classmethod
2130     def suitable(cls, url):
2131         return (False if YoutubePlaylistsIE.suitable(url) or YoutubeLiveIE.suitable(url)
2132                 else super(YoutubeChannelIE, cls).suitable(url))
2133
2134     def _build_template_url(self, url, channel_id):
2135         return self._TEMPLATE_URL % channel_id
2136
2137     def _real_extract(self, url):
2138         channel_id = self._match_id(url)
2139
2140         url = self._build_template_url(url, channel_id)
2141
2142         # Channel by page listing is restricted to 35 pages of 30 items, i.e. 1050 videos total (see #5778)
2143         # Workaround by extracting as a playlist if managed to obtain channel playlist URL
2144         # otherwise fallback on channel by page extraction
2145         channel_page = self._download_webpage(
2146             url + '?view=57', channel_id,
2147             'Downloading channel page', fatal=False)
2148         if channel_page is False:
2149             channel_playlist_id = False
2150         else:
2151             channel_playlist_id = self._html_search_meta(
2152                 'channelId', channel_page, 'channel id', default=None)
2153             if not channel_playlist_id:
2154                 channel_url = self._html_search_meta(
2155                     ('al:ios:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad'),
2156                     channel_page, 'channel url', default=None)
2157                 if channel_url:
2158                     channel_playlist_id = self._search_regex(
2159                         r'vnd\.youtube://user/([0-9A-Za-z_-]+)',
2160                         channel_url, 'channel id', default=None)
2161         if channel_playlist_id and channel_playlist_id.startswith('UC'):
2162             playlist_id = 'UU' + channel_playlist_id[2:]
2163             return self.url_result(
2164                 compat_urlparse.urljoin(url, '/playlist?list=%s' % playlist_id), 'YoutubePlaylist')
2165
2166         channel_page = self._download_webpage(url, channel_id, 'Downloading page #1')
2167         autogenerated = re.search(r'''(?x)
2168                 class="[^"]*?(?:
2169                     channel-header-autogenerated-label|
2170                     yt-channel-title-autogenerated
2171                 )[^"]*"''', channel_page) is not None
2172
2173         if autogenerated:
2174             # The videos are contained in a single page
2175             # the ajax pages can't be used, they are empty
2176             entries = [
2177                 self.url_result(
2178                     video_id, 'Youtube', video_id=video_id,
2179                     video_title=video_title)
2180                 for video_id, video_title in self.extract_videos_from_page(channel_page)]
2181             return self.playlist_result(entries, channel_id)
2182
2183         try:
2184             next(self._entries(channel_page, channel_id))
2185         except StopIteration:
2186             alert_message = self._html_search_regex(
2187                 r'(?s)<div[^>]+class=(["\']).*?\byt-alert-message\b.*?\1[^>]*>(?P<alert>[^<]+)</div>',
2188                 channel_page, 'alert', default=None, group='alert')
2189             if alert_message:
2190                 raise ExtractorError('Youtube said: %s' % alert_message, expected=True)
2191
2192         return self.playlist_result(self._entries(channel_page, channel_id), channel_id)
2193
2194
2195 class YoutubeUserIE(YoutubeChannelIE):
2196     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
2197     _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_-]+)'
2198     _TEMPLATE_URL = 'https://www.youtube.com/%s/%s/videos'
2199     IE_NAME = 'youtube:user'
2200
2201     _TESTS = [{
2202         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
2203         'playlist_mincount': 320,
2204         'info_dict': {
2205             'id': 'UUfX55Sx5hEFjoC3cNs6mCUQ',
2206             'title': 'Uploads from The Linux Foundation',
2207         }
2208     }, {
2209         # Only available via https://www.youtube.com/c/12minuteathlete/videos
2210         # but not https://www.youtube.com/user/12minuteathlete/videos
2211         'url': 'https://www.youtube.com/c/12minuteathlete/videos',
2212         'playlist_mincount': 249,
2213         'info_dict': {
2214             'id': 'UUVjM-zV6_opMDx7WYxnjZiQ',
2215             'title': 'Uploads from 12 Minute Athlete',
2216         }
2217     }, {
2218         'url': 'ytuser:phihag',
2219         'only_matching': True,
2220     }, {
2221         'url': 'https://www.youtube.com/c/gametrailers',
2222         'only_matching': True,
2223     }, {
2224         'url': 'https://www.youtube.com/gametrailers',
2225         'only_matching': True,
2226     }, {
2227         # This channel is not available.
2228         'url': 'https://www.youtube.com/user/kananishinoSMEJ/videos',
2229         'only_matching': True,
2230     }]
2231
2232     @classmethod
2233     def suitable(cls, url):
2234         # Don't return True if the url can be extracted with other youtube
2235         # extractor, the regex would is too permissive and it would match.
2236         other_yt_ies = iter(klass for (name, klass) in globals().items() if name.startswith('Youtube') and name.endswith('IE') and klass is not cls)
2237         if any(ie.suitable(url) for ie in other_yt_ies):
2238             return False
2239         else:
2240             return super(YoutubeUserIE, cls).suitable(url)
2241
2242     def _build_template_url(self, url, channel_id):
2243         mobj = re.match(self._VALID_URL, url)
2244         return self._TEMPLATE_URL % (mobj.group('user') or 'user', mobj.group('id'))
2245
2246
2247 class YoutubeLiveIE(YoutubeBaseInfoExtractor):
2248     IE_DESC = 'YouTube.com live streams'
2249     _VALID_URL = r'(?P<base_url>https?://(?:\w+\.)?youtube\.com/(?:(?:user|channel|c)/)?(?P<id>[^/]+))/live'
2250     IE_NAME = 'youtube:live'
2251
2252     _TESTS = [{
2253         'url': 'https://www.youtube.com/user/TheYoungTurks/live',
2254         'info_dict': {
2255             'id': 'a48o2S1cPoo',
2256             'ext': 'mp4',
2257             'title': 'The Young Turks - Live Main Show',
2258             'uploader': 'The Young Turks',
2259             'uploader_id': 'TheYoungTurks',
2260             'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
2261             'upload_date': '20150715',
2262             'license': 'Standard YouTube License',
2263             'description': 'md5:438179573adcdff3c97ebb1ee632b891',
2264             'categories': ['News & Politics'],
2265             'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
2266             'like_count': int,
2267             'dislike_count': int,
2268         },
2269         'params': {
2270             'skip_download': True,
2271         },
2272     }, {
2273         'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
2274         'only_matching': True,
2275     }, {
2276         'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
2277         'only_matching': True,
2278     }, {
2279         'url': 'https://www.youtube.com/TheYoungTurks/live',
2280         'only_matching': True,
2281     }]
2282
2283     def _real_extract(self, url):
2284         mobj = re.match(self._VALID_URL, url)
2285         channel_id = mobj.group('id')
2286         base_url = mobj.group('base_url')
2287         webpage = self._download_webpage(url, channel_id, fatal=False)
2288         if webpage:
2289             page_type = self._og_search_property(
2290                 'type', webpage, 'page type', default=None)
2291             video_id = self._html_search_meta(
2292                 'videoId', webpage, 'video id', default=None)
2293             if page_type == 'video' and video_id and re.match(r'^[0-9A-Za-z_-]{11}$', video_id):
2294                 return self.url_result(video_id, YoutubeIE.ie_key())
2295         return self.url_result(base_url)
2296
2297
2298 class YoutubePlaylistsIE(YoutubePlaylistsBaseInfoExtractor):
2299     IE_DESC = 'YouTube.com user/channel playlists'
2300     _VALID_URL = r'https?://(?:\w+\.)?youtube\.com/(?:user|channel)/(?P<id>[^/]+)/playlists'
2301     IE_NAME = 'youtube:playlists'
2302
2303     _TESTS = [{
2304         'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
2305         'playlist_mincount': 4,
2306         'info_dict': {
2307             'id': 'ThirstForScience',
2308             'title': 'Thirst for Science',
2309         },
2310     }, {
2311         # with "Load more" button
2312         'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
2313         'playlist_mincount': 70,
2314         'info_dict': {
2315             'id': 'igorkle1',
2316             'title': 'Игорь Клейнер',
2317         },
2318     }, {
2319         'url': 'https://www.youtube.com/channel/UCiU1dHvZObB2iP6xkJ__Icw/playlists',
2320         'playlist_mincount': 17,
2321         'info_dict': {
2322             'id': 'UCiU1dHvZObB2iP6xkJ__Icw',
2323             'title': 'Chem Player',
2324         },
2325     }]
2326
2327
2328 class YoutubeSearchIE(SearchInfoExtractor, YoutubePlaylistIE):
2329     IE_DESC = 'YouTube.com searches'
2330     # there doesn't appear to be a real limit, for example if you search for
2331     # 'python' you get more than 8.000.000 results
2332     _MAX_RESULTS = float('inf')
2333     IE_NAME = 'youtube:search'
2334     _SEARCH_KEY = 'ytsearch'
2335     _EXTRA_QUERY_ARGS = {}
2336     _TESTS = []
2337
2338     def _get_n_results(self, query, n):
2339         """Get a specified number of results for a query"""
2340
2341         videos = []
2342         limit = n
2343
2344         for pagenum in itertools.count(1):
2345             url_query = {
2346                 'search_query': query.encode('utf-8'),
2347                 'page': pagenum,
2348                 'spf': 'navigate',
2349             }
2350             url_query.update(self._EXTRA_QUERY_ARGS)
2351             result_url = 'https://www.youtube.com/results?' + compat_urllib_parse_urlencode(url_query)
2352             data = self._download_json(
2353                 result_url, video_id='query "%s"' % query,
2354                 note='Downloading page %s' % pagenum,
2355                 errnote='Unable to download API page')
2356             html_content = data[1]['body']['content']
2357
2358             if 'class="search-message' in html_content:
2359                 raise ExtractorError(
2360                     '[youtube] No video results', expected=True)
2361
2362             new_videos = self._ids_to_results(orderedSet(re.findall(
2363                 r'href="/watch\?v=(.{11})', html_content)))
2364             videos += new_videos
2365             if not new_videos or len(videos) > limit:
2366                 break
2367
2368         if len(videos) > n:
2369             videos = videos[:n]
2370         return self.playlist_result(videos, query)
2371
2372
2373 class YoutubeSearchDateIE(YoutubeSearchIE):
2374     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
2375     _SEARCH_KEY = 'ytsearchdate'
2376     IE_DESC = 'YouTube.com searches, newest videos first'
2377     _EXTRA_QUERY_ARGS = {'search_sort': 'video_date_uploaded'}
2378
2379
2380 class YoutubeSearchURLIE(YoutubePlaylistBaseInfoExtractor):
2381     IE_DESC = 'YouTube.com search URLs'
2382     IE_NAME = 'youtube:search_url'
2383     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?P<query>[^&]+)(?:[&]|$)'
2384     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})(?:[^"]*"[^>]+\btitle="(?P<title>[^"]+))?'
2385     _TESTS = [{
2386         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
2387         'playlist_mincount': 5,
2388         'info_dict': {
2389             'title': 'youtube-dl test video',
2390         }
2391     }, {
2392         'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
2393         'only_matching': True,
2394     }]
2395
2396     def _real_extract(self, url):
2397         mobj = re.match(self._VALID_URL, url)
2398         query = compat_urllib_parse_unquote_plus(mobj.group('query'))
2399         webpage = self._download_webpage(url, query)
2400         return self.playlist_result(self._process_page(webpage), playlist_title=query)
2401
2402
2403 class YoutubeShowIE(YoutubePlaylistsBaseInfoExtractor):
2404     IE_DESC = 'YouTube.com (multi-season) shows'
2405     _VALID_URL = r'https?://(?:www\.)?youtube\.com/show/(?P<id>[^?#]*)'
2406     IE_NAME = 'youtube:show'
2407     _TESTS = [{
2408         'url': 'https://www.youtube.com/show/airdisasters',
2409         'playlist_mincount': 5,
2410         'info_dict': {
2411             'id': 'airdisasters',
2412             'title': 'Air Disasters',
2413         }
2414     }]
2415
2416     def _real_extract(self, url):
2417         playlist_id = self._match_id(url)
2418         return super(YoutubeShowIE, self)._real_extract(
2419             'https://www.youtube.com/show/%s/playlists' % playlist_id)
2420
2421
2422 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
2423     """
2424     Base class for feed extractors
2425     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
2426     """
2427     _LOGIN_REQUIRED = True
2428
2429     @property
2430     def IE_NAME(self):
2431         return 'youtube:%s' % self._FEED_NAME
2432
2433     def _real_initialize(self):
2434         self._login()
2435
2436     def _real_extract(self, url):
2437         page = self._download_webpage(
2438             'https://www.youtube.com/feed/%s' % self._FEED_NAME, self._PLAYLIST_TITLE)
2439
2440         # The extraction process is the same as for playlists, but the regex
2441         # for the video ids doesn't contain an index
2442         ids = []
2443         more_widget_html = content_html = page
2444         for page_num in itertools.count(1):
2445             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
2446
2447             # 'recommended' feed has infinite 'load more' and each new portion spins
2448             # the same videos in (sometimes) slightly different order, so we'll check
2449             # for unicity and break when portion has no new videos
2450             new_ids = filter(lambda video_id: video_id not in ids, orderedSet(matches))
2451             if not new_ids:
2452                 break
2453
2454             ids.extend(new_ids)
2455
2456             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
2457             if not mobj:
2458                 break
2459
2460             more = self._download_json(
2461                 'https://youtube.com/%s' % mobj.group('more'), self._PLAYLIST_TITLE,
2462                 'Downloading page #%s' % page_num,
2463                 transform_source=uppercase_escape)
2464             content_html = more['content_html']
2465             more_widget_html = more['load_more_widget_html']
2466
2467         return self.playlist_result(
2468             self._ids_to_results(ids), playlist_title=self._PLAYLIST_TITLE)
2469
2470
2471 class YoutubeWatchLaterIE(YoutubePlaylistIE):
2472     IE_NAME = 'youtube:watchlater'
2473     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
2474     _VALID_URL = r'https?://(?:www\.)?youtube\.com/(?:feed/watch_later|(?:playlist|watch)\?(?:.+&)?list=WL)|:ytwatchlater'
2475
2476     _TESTS = [{
2477         'url': 'https://www.youtube.com/playlist?list=WL',
2478         'only_matching': True,
2479     }, {
2480         'url': 'https://www.youtube.com/watch?v=bCNU9TrbiRk&index=1&list=WL',
2481         'only_matching': True,
2482     }]
2483
2484     def _real_extract(self, url):
2485         _, video = self._check_download_just_video(url, 'WL')
2486         if video:
2487             return video
2488         _, playlist = self._extract_playlist('WL')
2489         return playlist
2490
2491
2492 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
2493     IE_NAME = 'youtube:favorites'
2494     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
2495     _VALID_URL = r'https?://(?:www\.)?youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
2496     _LOGIN_REQUIRED = True
2497
2498     def _real_extract(self, url):
2499         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
2500         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
2501         return self.url_result(playlist_id, 'YoutubePlaylist')
2502
2503
2504 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
2505     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
2506     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/recommended|:ytrec(?:ommended)?'
2507     _FEED_NAME = 'recommended'
2508     _PLAYLIST_TITLE = 'Youtube Recommended videos'
2509
2510
2511 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
2512     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
2513     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
2514     _FEED_NAME = 'subscriptions'
2515     _PLAYLIST_TITLE = 'Youtube Subscriptions'
2516
2517
2518 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
2519     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
2520     _VALID_URL = r'https?://(?:www\.)?youtube\.com/feed/history|:ythistory'
2521     _FEED_NAME = 'history'
2522     _PLAYLIST_TITLE = 'Youtube History'
2523
2524
2525 class YoutubeTruncatedURLIE(InfoExtractor):
2526     IE_NAME = 'youtube:truncated_url'
2527     IE_DESC = False  # Do not list
2528     _VALID_URL = r'''(?x)
2529         (?:https?://)?
2530         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
2531         (?:watch\?(?:
2532             feature=[a-z_]+|
2533             annotation_id=annotation_[^&]+|
2534             x-yt-cl=[0-9]+|
2535             hl=[^&]*|
2536             t=[0-9]+
2537         )?
2538         |
2539             attribution_link\?a=[^&]+
2540         )
2541         $
2542     '''
2543
2544     _TESTS = [{
2545         'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
2546         'only_matching': True,
2547     }, {
2548         'url': 'https://www.youtube.com/watch?',
2549         'only_matching': True,
2550     }, {
2551         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
2552         'only_matching': True,
2553     }, {
2554         'url': 'https://www.youtube.com/watch?feature=foo',
2555         'only_matching': True,
2556     }, {
2557         'url': 'https://www.youtube.com/watch?hl=en-GB',
2558         'only_matching': True,
2559     }, {
2560         'url': 'https://www.youtube.com/watch?t=2372',
2561         'only_matching': True,
2562     }]
2563
2564     def _real_extract(self, url):
2565         raise ExtractorError(
2566             'Did you forget to quote the URL? Remember that & is a meta '
2567             'character in most shells, so you want to put the URL in quotes, '
2568             'like  youtube-dl '
2569             '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
2570             ' or simply  youtube-dl BaW_jenozKc  .',
2571             expected=True)
2572
2573
2574 class YoutubeTruncatedIDIE(InfoExtractor):
2575     IE_NAME = 'youtube:truncated_id'
2576     IE_DESC = False  # Do not list
2577     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
2578
2579     _TESTS = [{
2580         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
2581         'only_matching': True,
2582     }]
2583
2584     def _real_extract(self, url):
2585         video_id = self._match_id(url)
2586         raise ExtractorError(
2587             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
2588             expected=True)