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