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