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