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