[youtube] improve width and height extraction from fmt_list
[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             formats_spec = {}
1403             fmt_list = video_info.get('fmt_list', [''])[0]
1404             if fmt_list:
1405                 for fmt in fmt_list.split(','):
1406                     spec = fmt.split('/')
1407                     if len(spec) > 1:
1408                         width_height = spec[1].split('x')
1409                         if len(width_height) == 2:
1410                             formats_spec[spec[0]] = {
1411                                 'resolution': spec[1],
1412                                 'width': int_or_none(width_height[0]),
1413                                 'height': int_or_none(width_height[1]),
1414                             }
1415             formats = []
1416             for url_data_str in encoded_url_map.split(','):
1417                 url_data = compat_parse_qs(url_data_str)
1418                 if 'itag' not in url_data or 'url' not in url_data:
1419                     continue
1420                 format_id = url_data['itag'][0]
1421                 url = url_data['url'][0]
1422
1423                 if 'sig' in url_data:
1424                     url += '&signature=' + url_data['sig'][0]
1425                 elif 's' in url_data:
1426                     encrypted_sig = url_data['s'][0]
1427                     ASSETS_RE = r'"assets":.+?"js":\s*("[^"]+")'
1428
1429                     jsplayer_url_json = self._search_regex(
1430                         ASSETS_RE,
1431                         embed_webpage if age_gate else video_webpage,
1432                         'JS player URL (1)', default=None)
1433                     if not jsplayer_url_json and not age_gate:
1434                         # We need the embed website after all
1435                         if embed_webpage is None:
1436                             embed_url = proto + '://www.youtube.com/embed/%s' % video_id
1437                             embed_webpage = self._download_webpage(
1438                                 embed_url, video_id, 'Downloading embed webpage')
1439                         jsplayer_url_json = self._search_regex(
1440                             ASSETS_RE, embed_webpage, 'JS player URL')
1441
1442                     player_url = json.loads(jsplayer_url_json)
1443                     if player_url is None:
1444                         player_url_json = self._search_regex(
1445                             r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
1446                             video_webpage, 'age gate player URL')
1447                         player_url = json.loads(player_url_json)
1448
1449                     if self._downloader.params.get('verbose'):
1450                         if player_url is None:
1451                             player_version = 'unknown'
1452                             player_desc = 'unknown'
1453                         else:
1454                             if player_url.endswith('swf'):
1455                                 player_version = self._search_regex(
1456                                     r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
1457                                     'flash player', fatal=False)
1458                                 player_desc = 'flash player %s' % player_version
1459                             else:
1460                                 player_version = self._search_regex(
1461                                     [r'html5player-([^/]+?)(?:/html5player(?:-new)?)?\.js', r'(?:www|player)-([^/]+)/base\.js'],
1462                                     player_url,
1463                                     'html5 player', fatal=False)
1464                                 player_desc = 'html5 player %s' % player_version
1465
1466                         parts_sizes = self._signature_cache_id(encrypted_sig)
1467                         self.to_screen('{%s} signature length %s, %s' %
1468                                        (format_id, parts_sizes, player_desc))
1469
1470                     signature = self._decrypt_signature(
1471                         encrypted_sig, video_id, player_url, age_gate)
1472                     url += '&signature=' + signature
1473                 if 'ratebypass' not in url:
1474                     url += '&ratebypass=yes'
1475
1476                 dct = {
1477                     'format_id': format_id,
1478                     'url': url,
1479                     'player_url': player_url,
1480                 }
1481                 if format_id in self._formats:
1482                     dct.update(self._formats[format_id])
1483                 if format_id in formats_spec:
1484                     dct.update(formats_spec[format_id])
1485
1486                 # Some itags are not included in DASH manifest thus corresponding formats will
1487                 # lack metadata (see https://github.com/rg3/youtube-dl/pull/5993).
1488                 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
1489                 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
1490                 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
1491
1492                 more_fields = {
1493                     'filesize': int_or_none(url_data.get('clen', [None])[0]),
1494                     'tbr': float_or_none(url_data.get('bitrate', [None])[0], 1000),
1495                     'width': width,
1496                     'height': height,
1497                     'fps': int_or_none(url_data.get('fps', [None])[0]),
1498                     'format_note': url_data.get('quality_label', [None])[0] or url_data.get('quality', [None])[0],
1499                 }
1500                 for key, value in more_fields.items():
1501                     if value:
1502                         dct[key] = value
1503                 type_ = url_data.get('type', [None])[0]
1504                 if type_:
1505                     type_split = type_.split(';')
1506                     kind_ext = type_split[0].split('/')
1507                     if len(kind_ext) == 2:
1508                         kind, _ = kind_ext
1509                         dct['ext'] = mimetype2ext(type_split[0])
1510                         if kind in ('audio', 'video'):
1511                             codecs = None
1512                             for mobj in re.finditer(
1513                                     r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?)(?P<val>.+?)(?P=quote)(?:;|$)', type_):
1514                                 if mobj.group('key') == 'codecs':
1515                                     codecs = mobj.group('val')
1516                                     break
1517                             if codecs:
1518                                 codecs = codecs.split(',')
1519                                 if len(codecs) == 2:
1520                                     acodec, vcodec = codecs[1], codecs[0]
1521                                 else:
1522                                     acodec, vcodec = (codecs[0], 'none') if kind == 'audio' else ('none', codecs[0])
1523                                 dct.update({
1524                                     'acodec': acodec,
1525                                     'vcodec': vcodec,
1526                                 })
1527                 formats.append(dct)
1528         elif video_info.get('hlsvp'):
1529             manifest_url = video_info['hlsvp'][0]
1530             url_map = self._extract_from_m3u8(manifest_url, video_id)
1531             formats = _map_to_format_list(url_map)
1532             # Accept-Encoding header causes failures in live streams on Youtube and Youtube Gaming
1533             for a_format in formats:
1534                 a_format.setdefault('http_headers', {})['Youtubedl-no-compression'] = 'True'
1535         else:
1536             unavailable_message = self._html_search_regex(
1537                 r'(?s)<h1[^>]+id="unavailable-message"[^>]*>(.+?)</h1>',
1538                 video_webpage, 'unavailable message', default=None)
1539             if unavailable_message:
1540                 raise ExtractorError(unavailable_message, expected=True)
1541             raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
1542
1543         # Look for the DASH manifest
1544         if self._downloader.params.get('youtube_include_dash_manifest', True):
1545             dash_mpd_fatal = True
1546             for mpd_url in dash_mpds:
1547                 dash_formats = {}
1548                 try:
1549                     def decrypt_sig(mobj):
1550                         s = mobj.group(1)
1551                         dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
1552                         return '/signature/%s' % dec_s
1553
1554                     mpd_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, mpd_url)
1555
1556                     for df in self._extract_mpd_formats(
1557                             mpd_url, video_id, fatal=dash_mpd_fatal,
1558                             formats_dict=self._formats):
1559                         # Do not overwrite DASH format found in some previous DASH manifest
1560                         if df['format_id'] not in dash_formats:
1561                             dash_formats[df['format_id']] = df
1562                         # Additional DASH manifests may end up in HTTP Error 403 therefore
1563                         # allow them to fail without bug report message if we already have
1564                         # some DASH manifest succeeded. This is temporary workaround to reduce
1565                         # burst of bug reports until we figure out the reason and whether it
1566                         # can be fixed at all.
1567                         dash_mpd_fatal = False
1568                 except (ExtractorError, KeyError) as e:
1569                     self.report_warning(
1570                         'Skipping DASH manifest: %r' % e, video_id)
1571                 if dash_formats:
1572                     # Remove the formats we found through non-DASH, they
1573                     # contain less info and it can be wrong, because we use
1574                     # fixed values (for example the resolution). See
1575                     # https://github.com/rg3/youtube-dl/issues/5774 for an
1576                     # example.
1577                     formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
1578                     formats.extend(dash_formats.values())
1579
1580         # Check for malformed aspect ratio
1581         stretched_m = re.search(
1582             r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
1583             video_webpage)
1584         if stretched_m:
1585             w = float(stretched_m.group('w'))
1586             h = float(stretched_m.group('h'))
1587             # yt:stretch may hold invalid ratio data (e.g. for Q39EVAstoRM ratio is 17:0).
1588             # We will only process correct ratios.
1589             if w > 0 and h > 0:
1590                 ratio = w / h
1591                 for f in formats:
1592                     if f.get('vcodec') != 'none':
1593                         f['stretched_ratio'] = ratio
1594
1595         self._sort_formats(formats)
1596
1597         self.mark_watched(video_id, video_info)
1598
1599         return {
1600             'id': video_id,
1601             'uploader': video_uploader,
1602             'uploader_id': video_uploader_id,
1603             'upload_date': upload_date,
1604             'creator': video_creator,
1605             'title': video_title,
1606             'alt_title': video_alt_title,
1607             'thumbnail': video_thumbnail,
1608             'description': video_description,
1609             'categories': video_categories,
1610             'tags': video_tags,
1611             'subtitles': video_subtitles,
1612             'automatic_captions': automatic_captions,
1613             'duration': video_duration,
1614             'age_limit': 18 if age_gate else 0,
1615             'annotations': video_annotations,
1616             'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
1617             'view_count': view_count,
1618             'like_count': like_count,
1619             'dislike_count': dislike_count,
1620             'average_rating': float_or_none(video_info.get('avg_rating', [None])[0]),
1621             'formats': formats,
1622             'is_live': is_live,
1623             'start_time': start_time,
1624             'end_time': end_time,
1625         }
1626
1627
1628 class YoutubePlaylistIE(YoutubePlaylistBaseInfoExtractor):
1629     IE_DESC = 'YouTube.com playlists'
1630     _VALID_URL = r"""(?x)(?:
1631                         (?:https?://)?
1632                         (?:\w+\.)?
1633                         youtube\.com/
1634                         (?:
1635                            (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
1636                            \? (?:.*?[&;])*? (?:p|a|list)=
1637                         |  p/
1638                         )
1639                         (
1640                             (?:PL|LL|EC|UU|FL|RD|UL)?[0-9A-Za-z-_]{10,}
1641                             # Top tracks, they can also include dots
1642                             |(?:MC)[\w\.]*
1643                         )
1644                         .*
1645                      |
1646                         ((?:PL|LL|EC|UU|FL|RD|UL)[0-9A-Za-z-_]{10,})
1647                      )"""
1648     _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
1649     _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)(?:[^>]+>(?P<title>[^<]+))?'
1650     IE_NAME = 'youtube:playlist'
1651     _TESTS = [{
1652         'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1653         'info_dict': {
1654             'title': 'ytdl test PL',
1655             'id': 'PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1656         },
1657         'playlist_count': 3,
1658     }, {
1659         'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1660         'info_dict': {
1661             'id': 'PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1662             'title': 'YDL_Empty_List',
1663         },
1664         'playlist_count': 0,
1665     }, {
1666         'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1667         'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1668         'info_dict': {
1669             'title': '29C3: Not my department',
1670             'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1671         },
1672         'playlist_count': 95,
1673     }, {
1674         'note': 'issue #673',
1675         'url': 'PLBB231211A4F62143',
1676         'info_dict': {
1677             'title': '[OLD]Team Fortress 2 (Class-based LP)',
1678             'id': 'PLBB231211A4F62143',
1679         },
1680         'playlist_mincount': 26,
1681     }, {
1682         'note': 'Large playlist',
1683         'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1684         'info_dict': {
1685             'title': 'Uploads from Cauchemar',
1686             'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
1687         },
1688         'playlist_mincount': 799,
1689     }, {
1690         'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1691         'info_dict': {
1692             'title': 'YDL_safe_search',
1693             'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1694         },
1695         'playlist_count': 2,
1696     }, {
1697         'note': 'embedded',
1698         'url': 'http://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1699         'playlist_count': 4,
1700         'info_dict': {
1701             'title': 'JODA15',
1702             'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1703         }
1704     }, {
1705         'note': 'Embedded SWF player',
1706         'url': 'http://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1707         'playlist_count': 4,
1708         'info_dict': {
1709             'title': 'JODA7',
1710             'id': 'YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ',
1711         }
1712     }, {
1713         'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
1714         'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
1715         'info_dict': {
1716             'title': 'Uploads from Interstellar Movie',
1717             'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
1718         },
1719         'playlist_mincout': 21,
1720     }]
1721
1722     def _real_initialize(self):
1723         self._login()
1724
1725     def _extract_mix(self, playlist_id):
1726         # The mixes are generated from a single video
1727         # the id of the playlist is just 'RD' + video_id
1728         url = 'https://youtube.com/watch?v=%s&list=%s' % (playlist_id[-11:], playlist_id)
1729         webpage = self._download_webpage(
1730             url, playlist_id, 'Downloading Youtube mix')
1731         search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
1732         title_span = (
1733             search_title('playlist-title') or
1734             search_title('title long-title') or
1735             search_title('title'))
1736         title = clean_html(title_span)
1737         ids = orderedSet(re.findall(
1738             r'''(?xs)data-video-username=".*?".*?
1739                        href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
1740             webpage))
1741         url_results = self._ids_to_results(ids)
1742
1743         return self.playlist_result(url_results, playlist_id, title)
1744
1745     def _extract_playlist(self, playlist_id):
1746         url = self._TEMPLATE_URL % playlist_id
1747         page = self._download_webpage(url, playlist_id)
1748
1749         for match in re.findall(r'<div class="yt-alert-message">([^<]+)</div>', page):
1750             match = match.strip()
1751             # Check if the playlist exists or is private
1752             if re.match(r'[^<]*(The|This) playlist (does not exist|is private)[^<]*', match):
1753                 raise ExtractorError(
1754                     'The playlist doesn\'t exist or is private, use --username or '
1755                     '--netrc to access it.',
1756                     expected=True)
1757             elif re.match(r'[^<]*Invalid parameters[^<]*', match):
1758                 raise ExtractorError(
1759                     'Invalid parameters. Maybe URL is incorrect.',
1760                     expected=True)
1761             elif re.match(r'[^<]*Choose your language[^<]*', match):
1762                 continue
1763             else:
1764                 self.report_warning('Youtube gives an alert message: ' + match)
1765
1766         playlist_title = self._html_search_regex(
1767             r'(?s)<h1 class="pl-header-title[^"]*"[^>]*>\s*(.*?)\s*</h1>',
1768             page, 'title')
1769
1770         return self.playlist_result(self._entries(page, playlist_id), playlist_id, playlist_title)
1771
1772     def _check_download_just_video(self, url, playlist_id):
1773         # Check if it's a video-specific URL
1774         query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
1775         if 'v' in query_dict:
1776             video_id = query_dict['v'][0]
1777             if self._downloader.params.get('noplaylist'):
1778                 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
1779                 return self.url_result(video_id, 'Youtube', video_id=video_id)
1780             else:
1781                 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
1782
1783     def _real_extract(self, url):
1784         # Extract playlist id
1785         mobj = re.match(self._VALID_URL, url)
1786         if mobj is None:
1787             raise ExtractorError('Invalid URL: %s' % url)
1788         playlist_id = mobj.group(1) or mobj.group(2)
1789
1790         video = self._check_download_just_video(url, playlist_id)
1791         if video:
1792             return video
1793
1794         if playlist_id.startswith('RD') or playlist_id.startswith('UL'):
1795             # Mixes require a custom extraction process
1796             return self._extract_mix(playlist_id)
1797
1798         return self._extract_playlist(playlist_id)
1799
1800
1801 class YoutubeChannelIE(YoutubePlaylistBaseInfoExtractor):
1802     IE_DESC = 'YouTube.com channels'
1803     _VALID_URL = r'https?://(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/(?P<id>[0-9A-Za-z_-]+)'
1804     _TEMPLATE_URL = 'https://www.youtube.com/channel/%s/videos'
1805     _VIDEO_RE = r'(?:title="(?P<title>[^"]+)"[^>]+)?href="/watch\?v=(?P<id>[0-9A-Za-z_-]+)&?'
1806     IE_NAME = 'youtube:channel'
1807     _TESTS = [{
1808         'note': 'paginated channel',
1809         'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
1810         'playlist_mincount': 91,
1811         'info_dict': {
1812             'id': 'UUKfVa3S1e4PHvxWcwyMMg8w',
1813             'title': 'Uploads from lex will',
1814         }
1815     }, {
1816         'note': 'Age restricted channel',
1817         # from https://www.youtube.com/user/DeusExOfficial
1818         'url': 'https://www.youtube.com/channel/UCs0ifCMCm1icqRbqhUINa0w',
1819         'playlist_mincount': 64,
1820         'info_dict': {
1821             'id': 'UUs0ifCMCm1icqRbqhUINa0w',
1822             'title': 'Uploads from Deus Ex',
1823         },
1824     }]
1825
1826     @classmethod
1827     def suitable(cls, url):
1828         return False if YoutubePlaylistsIE.suitable(url) else super(YoutubeChannelIE, cls).suitable(url)
1829
1830     def _real_extract(self, url):
1831         channel_id = self._match_id(url)
1832
1833         url = self._TEMPLATE_URL % channel_id
1834
1835         # Channel by page listing is restricted to 35 pages of 30 items, i.e. 1050 videos total (see #5778)
1836         # Workaround by extracting as a playlist if managed to obtain channel playlist URL
1837         # otherwise fallback on channel by page extraction
1838         channel_page = self._download_webpage(
1839             url + '?view=57', channel_id,
1840             'Downloading channel page', fatal=False)
1841         if channel_page is False:
1842             channel_playlist_id = False
1843         else:
1844             channel_playlist_id = self._html_search_meta(
1845                 'channelId', channel_page, 'channel id', default=None)
1846             if not channel_playlist_id:
1847                 channel_playlist_id = self._search_regex(
1848                     r'data-(?:channel-external-|yt)id="([^"]+)"',
1849                     channel_page, 'channel id', default=None)
1850         if channel_playlist_id and channel_playlist_id.startswith('UC'):
1851             playlist_id = 'UU' + channel_playlist_id[2:]
1852             return self.url_result(
1853                 compat_urlparse.urljoin(url, '/playlist?list=%s' % playlist_id), 'YoutubePlaylist')
1854
1855         channel_page = self._download_webpage(url, channel_id, 'Downloading page #1')
1856         autogenerated = re.search(r'''(?x)
1857                 class="[^"]*?(?:
1858                     channel-header-autogenerated-label|
1859                     yt-channel-title-autogenerated
1860                 )[^"]*"''', channel_page) is not None
1861
1862         if autogenerated:
1863             # The videos are contained in a single page
1864             # the ajax pages can't be used, they are empty
1865             entries = [
1866                 self.url_result(
1867                     video_id, 'Youtube', video_id=video_id,
1868                     video_title=video_title)
1869                 for video_id, video_title in self.extract_videos_from_page(channel_page)]
1870             return self.playlist_result(entries, channel_id)
1871
1872         return self.playlist_result(self._entries(channel_page, channel_id), channel_id)
1873
1874
1875 class YoutubeUserIE(YoutubeChannelIE):
1876     IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
1877     _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_-]+)'
1878     _TEMPLATE_URL = 'https://www.youtube.com/user/%s/videos'
1879     IE_NAME = 'youtube:user'
1880
1881     _TESTS = [{
1882         'url': 'https://www.youtube.com/user/TheLinuxFoundation',
1883         'playlist_mincount': 320,
1884         'info_dict': {
1885             'title': 'TheLinuxFoundation',
1886         }
1887     }, {
1888         'url': 'ytuser:phihag',
1889         'only_matching': True,
1890     }]
1891
1892     @classmethod
1893     def suitable(cls, url):
1894         # Don't return True if the url can be extracted with other youtube
1895         # extractor, the regex would is too permissive and it would match.
1896         other_ies = iter(klass for (name, klass) in globals().items() if name.endswith('IE') and klass is not cls)
1897         if any(ie.suitable(url) for ie in other_ies):
1898             return False
1899         else:
1900             return super(YoutubeUserIE, cls).suitable(url)
1901
1902
1903 class YoutubePlaylistsIE(YoutubePlaylistsBaseInfoExtractor):
1904     IE_DESC = 'YouTube.com user/channel playlists'
1905     _VALID_URL = r'https?://(?:\w+\.)?youtube\.com/(?:user|channel)/(?P<id>[^/]+)/playlists'
1906     IE_NAME = 'youtube:playlists'
1907
1908     _TESTS = [{
1909         'url': 'http://www.youtube.com/user/ThirstForScience/playlists',
1910         'playlist_mincount': 4,
1911         'info_dict': {
1912             'id': 'ThirstForScience',
1913             'title': 'Thirst for Science',
1914         },
1915     }, {
1916         # with "Load more" button
1917         'url': 'http://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
1918         'playlist_mincount': 70,
1919         'info_dict': {
1920             'id': 'igorkle1',
1921             'title': 'Игорь Клейнер',
1922         },
1923     }, {
1924         'url': 'https://www.youtube.com/channel/UCiU1dHvZObB2iP6xkJ__Icw/playlists',
1925         'playlist_mincount': 17,
1926         'info_dict': {
1927             'id': 'UCiU1dHvZObB2iP6xkJ__Icw',
1928             'title': 'Chem Player',
1929         },
1930     }]
1931
1932
1933 class YoutubeSearchIE(SearchInfoExtractor, YoutubePlaylistIE):
1934     IE_DESC = 'YouTube.com searches'
1935     # there doesn't appear to be a real limit, for example if you search for
1936     # 'python' you get more than 8.000.000 results
1937     _MAX_RESULTS = float('inf')
1938     IE_NAME = 'youtube:search'
1939     _SEARCH_KEY = 'ytsearch'
1940     _EXTRA_QUERY_ARGS = {}
1941     _TESTS = []
1942
1943     def _get_n_results(self, query, n):
1944         """Get a specified number of results for a query"""
1945
1946         videos = []
1947         limit = n
1948
1949         for pagenum in itertools.count(1):
1950             url_query = {
1951                 'search_query': query.encode('utf-8'),
1952                 'page': pagenum,
1953                 'spf': 'navigate',
1954             }
1955             url_query.update(self._EXTRA_QUERY_ARGS)
1956             result_url = 'https://www.youtube.com/results?' + compat_urllib_parse.urlencode(url_query)
1957             data = self._download_json(
1958                 result_url, video_id='query "%s"' % query,
1959                 note='Downloading page %s' % pagenum,
1960                 errnote='Unable to download API page')
1961             html_content = data[1]['body']['content']
1962
1963             if 'class="search-message' in html_content:
1964                 raise ExtractorError(
1965                     '[youtube] No video results', expected=True)
1966
1967             new_videos = self._ids_to_results(orderedSet(re.findall(
1968                 r'href="/watch\?v=(.{11})', html_content)))
1969             videos += new_videos
1970             if not new_videos or len(videos) > limit:
1971                 break
1972
1973         if len(videos) > n:
1974             videos = videos[:n]
1975         return self.playlist_result(videos, query)
1976
1977
1978 class YoutubeSearchDateIE(YoutubeSearchIE):
1979     IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
1980     _SEARCH_KEY = 'ytsearchdate'
1981     IE_DESC = 'YouTube.com searches, newest videos first'
1982     _EXTRA_QUERY_ARGS = {'search_sort': 'video_date_uploaded'}
1983
1984
1985 class YoutubeSearchURLIE(InfoExtractor):
1986     IE_DESC = 'YouTube.com search URLs'
1987     IE_NAME = 'youtube:search_url'
1988     _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?P<query>[^&]+)(?:[&]|$)'
1989     _TESTS = [{
1990         'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
1991         'playlist_mincount': 5,
1992         'info_dict': {
1993             'title': 'youtube-dl test video',
1994         }
1995     }, {
1996         'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
1997         'only_matching': True,
1998     }]
1999
2000     def _real_extract(self, url):
2001         mobj = re.match(self._VALID_URL, url)
2002         query = compat_urllib_parse_unquote_plus(mobj.group('query'))
2003
2004         webpage = self._download_webpage(url, query)
2005         result_code = self._search_regex(
2006             r'(?s)<ol[^>]+class="item-section"(.*?)</ol>', webpage, 'result HTML')
2007
2008         part_codes = re.findall(
2009             r'(?s)<h3[^>]+class="[^"]*yt-lockup-title[^"]*"[^>]*>(.*?)</h3>', result_code)
2010         entries = []
2011         for part_code in part_codes:
2012             part_title = self._html_search_regex(
2013                 [r'(?s)title="([^"]+)"', r'>([^<]+)</a>'], part_code, 'item title', fatal=False)
2014             part_url_snippet = self._html_search_regex(
2015                 r'(?s)href="([^"]+)"', part_code, 'item URL')
2016             part_url = compat_urlparse.urljoin(
2017                 'https://www.youtube.com/', part_url_snippet)
2018             entries.append({
2019                 '_type': 'url',
2020                 'url': part_url,
2021                 'title': part_title,
2022             })
2023
2024         return {
2025             '_type': 'playlist',
2026             'entries': entries,
2027             'title': query,
2028         }
2029
2030
2031 class YoutubeShowIE(YoutubePlaylistsBaseInfoExtractor):
2032     IE_DESC = 'YouTube.com (multi-season) shows'
2033     _VALID_URL = r'https?://www\.youtube\.com/show/(?P<id>[^?#]*)'
2034     IE_NAME = 'youtube:show'
2035     _TESTS = [{
2036         'url': 'https://www.youtube.com/show/airdisasters',
2037         'playlist_mincount': 5,
2038         'info_dict': {
2039             'id': 'airdisasters',
2040             'title': 'Air Disasters',
2041         }
2042     }]
2043
2044     def _real_extract(self, url):
2045         playlist_id = self._match_id(url)
2046         return super(YoutubeShowIE, self)._real_extract(
2047             'https://www.youtube.com/show/%s/playlists' % playlist_id)
2048
2049
2050 class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
2051     """
2052     Base class for feed extractors
2053     Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
2054     """
2055     _LOGIN_REQUIRED = True
2056
2057     @property
2058     def IE_NAME(self):
2059         return 'youtube:%s' % self._FEED_NAME
2060
2061     def _real_initialize(self):
2062         self._login()
2063
2064     def _real_extract(self, url):
2065         page = self._download_webpage(
2066             'https://www.youtube.com/feed/%s' % self._FEED_NAME, self._PLAYLIST_TITLE)
2067
2068         # The extraction process is the same as for playlists, but the regex
2069         # for the video ids doesn't contain an index
2070         ids = []
2071         more_widget_html = content_html = page
2072         for page_num in itertools.count(1):
2073             matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
2074
2075             # 'recommended' feed has infinite 'load more' and each new portion spins
2076             # the same videos in (sometimes) slightly different order, so we'll check
2077             # for unicity and break when portion has no new videos
2078             new_ids = filter(lambda video_id: video_id not in ids, orderedSet(matches))
2079             if not new_ids:
2080                 break
2081
2082             ids.extend(new_ids)
2083
2084             mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
2085             if not mobj:
2086                 break
2087
2088             more = self._download_json(
2089                 'https://youtube.com/%s' % mobj.group('more'), self._PLAYLIST_TITLE,
2090                 'Downloading page #%s' % page_num,
2091                 transform_source=uppercase_escape)
2092             content_html = more['content_html']
2093             more_widget_html = more['load_more_widget_html']
2094
2095         return self.playlist_result(
2096             self._ids_to_results(ids), playlist_title=self._PLAYLIST_TITLE)
2097
2098
2099 class YoutubeWatchLaterIE(YoutubePlaylistIE):
2100     IE_NAME = 'youtube:watchlater'
2101     IE_DESC = 'Youtube watch later list, ":ytwatchlater" for short (requires authentication)'
2102     _VALID_URL = r'https?://www\.youtube\.com/(?:feed/watch_later|(?:playlist|watch)\?(?:.+&)?list=WL)|:ytwatchlater'
2103
2104     _TESTS = [{
2105         'url': 'https://www.youtube.com/playlist?list=WL',
2106         'only_matching': True,
2107     }, {
2108         'url': 'https://www.youtube.com/watch?v=bCNU9TrbiRk&index=1&list=WL',
2109         'only_matching': True,
2110     }]
2111
2112     def _real_extract(self, url):
2113         video = self._check_download_just_video(url, 'WL')
2114         if video:
2115             return video
2116         return self._extract_playlist('WL')
2117
2118
2119 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
2120     IE_NAME = 'youtube:favorites'
2121     IE_DESC = 'YouTube.com favourite videos, ":ytfav" for short (requires authentication)'
2122     _VALID_URL = r'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
2123     _LOGIN_REQUIRED = True
2124
2125     def _real_extract(self, url):
2126         webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
2127         playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
2128         return self.url_result(playlist_id, 'YoutubePlaylist')
2129
2130
2131 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
2132     IE_DESC = 'YouTube.com recommended videos, ":ytrec" for short (requires authentication)'
2133     _VALID_URL = r'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?'
2134     _FEED_NAME = 'recommended'
2135     _PLAYLIST_TITLE = 'Youtube Recommended videos'
2136
2137
2138 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor):
2139     IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
2140     _VALID_URL = r'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
2141     _FEED_NAME = 'subscriptions'
2142     _PLAYLIST_TITLE = 'Youtube Subscriptions'
2143
2144
2145 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
2146     IE_DESC = 'Youtube watch history, ":ythistory" for short (requires authentication)'
2147     _VALID_URL = 'https?://www\.youtube\.com/feed/history|:ythistory'
2148     _FEED_NAME = 'history'
2149     _PLAYLIST_TITLE = 'Youtube History'
2150
2151
2152 class YoutubeTruncatedURLIE(InfoExtractor):
2153     IE_NAME = 'youtube:truncated_url'
2154     IE_DESC = False  # Do not list
2155     _VALID_URL = r'''(?x)
2156         (?:https?://)?
2157         (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
2158         (?:watch\?(?:
2159             feature=[a-z_]+|
2160             annotation_id=annotation_[^&]+|
2161             x-yt-cl=[0-9]+|
2162             hl=[^&]*|
2163             t=[0-9]+
2164         )?
2165         |
2166             attribution_link\?a=[^&]+
2167         )
2168         $
2169     '''
2170
2171     _TESTS = [{
2172         'url': 'http://www.youtube.com/watch?annotation_id=annotation_3951667041',
2173         'only_matching': True,
2174     }, {
2175         'url': 'http://www.youtube.com/watch?',
2176         'only_matching': True,
2177     }, {
2178         'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
2179         'only_matching': True,
2180     }, {
2181         'url': 'https://www.youtube.com/watch?feature=foo',
2182         'only_matching': True,
2183     }, {
2184         'url': 'https://www.youtube.com/watch?hl=en-GB',
2185         'only_matching': True,
2186     }, {
2187         'url': 'https://www.youtube.com/watch?t=2372',
2188         'only_matching': True,
2189     }]
2190
2191     def _real_extract(self, url):
2192         raise ExtractorError(
2193             'Did you forget to quote the URL? Remember that & is a meta '
2194             'character in most shells, so you want to put the URL in quotes, '
2195             'like  youtube-dl '
2196             '"http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
2197             ' or simply  youtube-dl BaW_jenozKc  .',
2198             expected=True)
2199
2200
2201 class YoutubeTruncatedIDIE(InfoExtractor):
2202     IE_NAME = 'youtube:truncated_id'
2203     IE_DESC = False  # Do not list
2204     _VALID_URL = r'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
2205
2206     _TESTS = [{
2207         'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
2208         'only_matching': True,
2209     }]
2210
2211     def _real_extract(self, url):
2212         video_id = self._match_id(url)
2213         raise ExtractorError(
2214             'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id, url),
2215             expected=True)