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