]> jfr.im git - yt-dlp.git/blame - youtube_dl/extractor/youtube.py
[youtube] Download DASH manifest by default (Closes #3887)
[yt-dlp.git] / youtube_dl / extractor / youtube.py
CommitLineData
c5e8d7af 1# coding: utf-8
c5e8d7af 2
78caa52a
PH
3from __future__ import unicode_literals
4
5
0ca96d48 6import itertools
c5e8d7af 7import json
c4417ddb 8import os.path
c5e8d7af 9import re
e0df6211 10import traceback
c5e8d7af 11
b05654f0 12from .common import InfoExtractor, SearchInfoExtractor
54d39d8b 13from .subtitles import SubtitlesInfoExtractor
2b25cb5d 14from ..jsinterp import JSInterpreter
54256267 15from ..swfinterp import SWFInterpreter
c5e8d7af 16from ..utils import (
edf3e38e 17 compat_chr,
c5e8d7af 18 compat_parse_qs,
c5e8d7af
PH
19 compat_urllib_parse,
20 compat_urllib_request,
7c61bd36 21 compat_urlparse,
c5e8d7af
PH
22 compat_str,
23
24 clean_html,
25 get_element_by_id,
652cdaa2 26 get_element_by_attribute,
c5e8d7af 27 ExtractorError,
dd27fd17 28 int_or_none,
9c44d242 29 OnDemandPagedList,
c5e8d7af
PH
30 unescapeHTML,
31 unified_strdate,
04cc9617 32 orderedSet,
81c2f20b 33 uppercase_escape,
c5e8d7af
PH
34)
35
de7f3446 36class YoutubeBaseInfoExtractor(InfoExtractor):
b2e8bc1b
JMF
37 """Provide base functions for Youtube extractors"""
38 _LOGIN_URL = 'https://accounts.google.com/ServiceLogin'
83317f69 39 _TWOFACTOR_URL = 'https://accounts.google.com/SecondFactor'
b2e8bc1b 40 _LANG_URL = r'https://www.youtube.com/?hl=en&persist_hl=1&gl=US&persist_gl=1&opt_out_ackd=1'
38c2e5b8 41 _AGE_URL = 'https://www.youtube.com/verify_age?next_url=/&gl=US&hl=en'
b2e8bc1b
JMF
42 _NETRC_MACHINE = 'youtube'
43 # If True it will raise an error if no login info is provided
44 _LOGIN_REQUIRED = False
45
b2e8bc1b 46 def _set_language(self):
7cc3570e
PH
47 return bool(self._download_webpage(
48 self._LANG_URL, None,
69ea8ca4 49 note='Setting language', errnote='unable to set language',
7cc3570e 50 fatal=False))
b2e8bc1b
JMF
51
52 def _login(self):
83317f69 53 """
54 Attempt to log in to YouTube.
55 True is returned if successful or skipped.
56 False is returned if login failed.
57
58 If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
59 """
b2e8bc1b
JMF
60 (username, password) = self._get_login_info()
61 # No authentication to be performed
62 if username is None:
63 if self._LOGIN_REQUIRED:
69ea8ca4 64 raise ExtractorError('No login info available, needed for using %s.' % self.IE_NAME, expected=True)
83317f69 65 return True
b2e8bc1b 66
7cc3570e
PH
67 login_page = self._download_webpage(
68 self._LOGIN_URL, None,
69ea8ca4
PH
69 note='Downloading login page',
70 errnote='unable to fetch login page', fatal=False)
7cc3570e
PH
71 if login_page is False:
72 return
b2e8bc1b 73
795f28f8 74 galx = self._search_regex(r'(?s)<input.+?name="GALX".+?value="(.+?)"',
78caa52a 75 login_page, 'Login GALX parameter')
c5e8d7af 76
b2e8bc1b
JMF
77 # Log in
78 login_form_strs = {
78caa52a
PH
79 'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
80 'Email': username,
81 'GALX': galx,
82 'Passwd': password,
83
84 'PersistentCookie': 'yes',
85 '_utf8': '霱',
86 'bgresponse': 'js_disabled',
87 'checkConnection': '',
88 'checkedDomains': 'youtube',
89 'dnConn': '',
90 'pstMsg': '0',
91 'rmShown': '1',
92 'secTok': '',
93 'signIn': 'Sign in',
94 'timeStmp': '',
95 'service': 'youtube',
96 'uilel': '3',
97 'hl': 'en_US',
b2e8bc1b 98 }
83317f69 99
b2e8bc1b
JMF
100 # Convert to UTF-8 *before* urlencode because Python 2.x's urlencode
101 # chokes on unicode
102 login_form = dict((k.encode('utf-8'), v.encode('utf-8')) for k,v in login_form_strs.items())
103 login_data = compat_urllib_parse.urlencode(login_form).encode('ascii')
7cc3570e
PH
104
105 req = compat_urllib_request.Request(self._LOGIN_URL, login_data)
106 login_results = self._download_webpage(
107 req, None,
69ea8ca4 108 note='Logging in', errnote='unable to log in', fatal=False)
7cc3570e
PH
109 if login_results is False:
110 return False
83317f69 111
112 if re.search(r'id="errormsg_0_Passwd"', login_results) is not None:
69ea8ca4 113 raise ExtractorError('Please use your account password and a two-factor code instead of an application-specific password.', expected=True)
83317f69 114
115 # Two-Factor
116 # TODO add SMS and phone call support - these require making a request and then prompting the user
117
118 if re.search(r'(?i)<form[^>]* id="gaia_secondfactorform"', login_results) is not None:
119 tfa_code = self._get_tfa_info()
120
121 if tfa_code is None:
69ea8ca4
PH
122 self._downloader.report_warning('Two-factor authentication required. Provide it with --twofactor <code>')
123 self._downloader.report_warning('(Note that only TOTP (Google Authenticator App) codes work at this time.)')
83317f69 124 return False
125
126 # Unlike the first login form, secTok and timeStmp are both required for the TFA form
127
128 match = re.search(r'id="secTok"\n\s+value=\'(.+)\'/>', login_results, re.M | re.U)
129 if match is None:
69ea8ca4 130 self._downloader.report_warning('Failed to get secTok - did the page structure change?')
83317f69 131 secTok = match.group(1)
132 match = re.search(r'id="timeStmp"\n\s+value=\'(.+)\'/>', login_results, re.M | re.U)
133 if match is None:
69ea8ca4 134 self._downloader.report_warning('Failed to get timeStmp - did the page structure change?')
83317f69 135 timeStmp = match.group(1)
136
137 tfa_form_strs = {
78caa52a
PH
138 'continue': 'https://www.youtube.com/signin?action_handle_signin=true&feature=sign_in_button&hl=en_US&nomobiletemp=1',
139 'smsToken': '',
140 'smsUserPin': tfa_code,
141 'smsVerifyPin': 'Verify',
142
143 'PersistentCookie': 'yes',
144 'checkConnection': '',
145 'checkedDomains': 'youtube',
146 'pstMsg': '1',
147 'secTok': secTok,
148 'timeStmp': timeStmp,
149 'service': 'youtube',
150 'hl': 'en_US',
83317f69 151 }
152 tfa_form = dict((k.encode('utf-8'), v.encode('utf-8')) for k,v in tfa_form_strs.items())
153 tfa_data = compat_urllib_parse.urlencode(tfa_form).encode('ascii')
154
155 tfa_req = compat_urllib_request.Request(self._TWOFACTOR_URL, tfa_data)
156 tfa_results = self._download_webpage(
157 tfa_req, None,
69ea8ca4 158 note='Submitting TFA code', errnote='unable to submit tfa', fatal=False)
83317f69 159
160 if tfa_results is False:
161 return False
162
163 if re.search(r'(?i)<form[^>]* id="gaia_secondfactorform"', tfa_results) is not None:
69ea8ca4 164 self._downloader.report_warning('Two-factor code expired. Please try again, or use a one-use backup code instead.')
83317f69 165 return False
166 if re.search(r'(?i)<form[^>]* id="gaia_loginform"', tfa_results) is not None:
69ea8ca4 167 self._downloader.report_warning('unable to log in - did the page structure change?')
83317f69 168 return False
169 if re.search(r'smsauth-interstitial-reviewsettings', tfa_results) is not None:
69ea8ca4 170 self._downloader.report_warning('Your Google account has a security notice. Please log in on your web browser, resolve the notice, and try again.')
83317f69 171 return False
172
7cc3570e 173 if re.search(r'(?i)<form[^>]* id="gaia_loginform"', login_results) is not None:
69ea8ca4 174 self._downloader.report_warning('unable to log in: bad username or password')
b2e8bc1b
JMF
175 return False
176 return True
177
178 def _confirm_age(self):
179 age_form = {
7cc3570e
PH
180 'next_url': '/',
181 'action_confirm': 'Confirm',
182 }
5700e779
JMF
183 req = compat_urllib_request.Request(self._AGE_URL,
184 compat_urllib_parse.urlencode(age_form).encode('ascii'))
7cc3570e
PH
185
186 self._download_webpage(
187 req, None,
69ea8ca4 188 note='Confirming age', errnote='Unable to confirm age')
b2e8bc1b
JMF
189 return True
190
191 def _real_initialize(self):
192 if self._downloader is None:
193 return
194 if not self._set_language():
195 return
196 if not self._login():
197 return
198 self._confirm_age()
c5e8d7af 199
8377574c 200
de7f3446 201class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor):
78caa52a 202 IE_DESC = 'YouTube.com'
cb7dfeea 203 _VALID_URL = r"""(?x)^
c5e8d7af 204 (
edb53e2d 205 (?:https?://|//) # http(s):// or protocol-independent URL
cb7dfeea 206 (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/|
484aaeb2 207 (?:www\.)?deturl\.com/www\.youtube\.com/|
e70dc1d1 208 (?:www\.)?pwnyoutube\.com/|
f7000f3a 209 (?:www\.)?yourepeat\.com/|
e69ae5b9
JMF
210 tube\.majestyc\.net/|
211 youtube\.googleapis\.com/) # the various hostnames, with wildcard subdomains
c5e8d7af
PH
212 (?:.*?\#/)? # handle anchor (#/) redirect urls
213 (?: # the various things that can precede the ID:
ac7553d0 214 (?:(?:v|embed|e)/(?!videoseries)) # v/ or embed/ or e/
c5e8d7af 215 |(?: # or the v= param in all its forms
f7000f3a 216 (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
c5e8d7af
PH
217 (?:\?|\#!?) # the params delimiter ? or # or #!
218 (?:.*?&)? # any other preceding param (like /?s=tuff&v=xxxx)
219 v=
220 )
f4b05232
JMF
221 ))
222 |youtu\.be/ # just youtu.be/xxxx
edb53e2d 223 |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
f4b05232 224 )
c5e8d7af 225 )? # all until now is optional -> you can pass the naked ID
8963d9c2 226 ([0-9A-Za-z_-]{11}) # here is it! the YouTube video ID
9291475f 227 (?!.*?&list=) # combined list/video URLs are handled by the playlist IE
c5e8d7af
PH
228 (?(1).+)? # if we found the ID, everything can follow
229 $"""
c5e8d7af 230 _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
2c62dc26
PH
231 _formats = {
232 '5': {'ext': 'flv', 'width': 400, 'height': 240},
233 '6': {'ext': 'flv', 'width': 450, 'height': 270},
234 '13': {'ext': '3gp'},
235 '17': {'ext': '3gp', 'width': 176, 'height': 144},
236 '18': {'ext': 'mp4', 'width': 640, 'height': 360},
237 '22': {'ext': 'mp4', 'width': 1280, 'height': 720},
238 '34': {'ext': 'flv', 'width': 640, 'height': 360},
239 '35': {'ext': 'flv', 'width': 854, 'height': 480},
240 '36': {'ext': '3gp', 'width': 320, 'height': 240},
241 '37': {'ext': 'mp4', 'width': 1920, 'height': 1080},
242 '38': {'ext': 'mp4', 'width': 4096, 'height': 3072},
243 '43': {'ext': 'webm', 'width': 640, 'height': 360},
244 '44': {'ext': 'webm', 'width': 854, 'height': 480},
245 '45': {'ext': 'webm', 'width': 1280, 'height': 720},
246 '46': {'ext': 'webm', 'width': 1920, 'height': 1080},
247
1d043b93 248
86fe61c8 249 # 3d videos
43b81eb9
PH
250 '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'preference': -20},
251 '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'preference': -20},
252 '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'preference': -20},
253 '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'preference': -20},
254 '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'preference': -20},
255 '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'preference': -20},
256 '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'preference': -20},
836a086c 257
96fb5605 258 # Apple HTTP Live Streaming
43b81eb9
PH
259 '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
260 '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'preference': -10},
261 '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'preference': -10},
262 '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'preference': -10},
263 '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'preference': -10},
264 '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'preference': -10},
265 '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'preference': -10},
2c62dc26
PH
266
267 # DASH mp4 video
43b81eb9
PH
268 '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
269 '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
270 '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
271 '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
272 '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
273 '138': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
274 '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
275 '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
836a086c 276
f6f1fc92 277 # Dash mp4 audio
2c62dc26
PH
278 '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 48, 'preference': -50},
279 '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 128, 'preference': -50},
280 '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'vcodec': 'none', 'abr': 256, 'preference': -50},
836a086c
AZ
281
282 # Dash webm
e75cafe9
A
283 '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
284 '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
285 '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
286 '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
287 '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
288 '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'acodec': 'none', 'container': 'webm', 'vcodec': 'VP8', 'preference': -40},
289 '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
290 '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
291 '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
292 '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
293 '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
294 '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
295 '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
3c80377b 296 '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
18061bba 297 '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'acodec': 'none', 'preference': -40},
2c62dc26
PH
298
299 # Dash webm audio
55db73ef 300 '171': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 128, 'preference': -50},
e75cafe9 301 '172': {'ext': 'webm', 'vcodec': 'none', 'format_note': 'DASH audio', 'abr': 256, 'preference': -50},
ce6b9a2d
PH
302
303 # RTMP (unnamed)
304 '_rtmp': {'protocol': 'rtmp'},
c5e8d7af 305 }
836a086c 306
78caa52a 307 IE_NAME = 'youtube'
2eb88d95
PH
308 _TESTS = [
309 {
4bc3a23e
PH
310 'url': 'http://www.youtube.com/watch?v=BaW_jenozKc',
311 'info_dict': {
312 'id': 'BaW_jenozKc',
313 'ext': 'mp4',
314 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
315 'uploader': 'Philipp Hagemeister',
316 'uploader_id': 'phihag',
317 'upload_date': '20121002',
318 '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 .',
319 'categories': ['Science & Technology'],
3e7c1224
PH
320 'like_count': int,
321 'dislike_count': int,
2eb88d95 322 }
0e853ca4 323 },
0e853ca4 324 {
4bc3a23e
PH
325 'url': 'http://www.youtube.com/watch?v=UxxajLWwzqY',
326 'note': 'Test generic use_cipher_signature video (#897)',
327 'info_dict': {
328 'id': 'UxxajLWwzqY',
329 'ext': 'mp4',
330 'upload_date': '20120506',
331 'title': 'Icona Pop - I Love It (feat. Charli XCX) [OFFICIAL VIDEO]',
332 'description': 'md5:fea86fda2d5a5784273df5c7cc994d9f',
333 'uploader': 'Icona Pop',
334 'uploader_id': 'IconaPop',
2eb88d95 335 }
c108eb73
JMF
336 },
337 {
4bc3a23e
PH
338 'url': 'https://www.youtube.com/watch?v=07FYdnEawAQ',
339 'note': 'Test VEVO video with age protection (#956)',
340 'info_dict': {
341 'id': '07FYdnEawAQ',
342 'ext': 'mp4',
343 'upload_date': '20130703',
344 'title': 'Justin Timberlake - Tunnel Vision (Explicit)',
345 'description': 'md5:64249768eec3bc4276236606ea996373',
346 'uploader': 'justintimberlakeVEVO',
347 'uploader_id': 'justintimberlakeVEVO',
c108eb73
JMF
348 }
349 },
fccd3771 350 {
4bc3a23e
PH
351 'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
352 'note': 'Embed-only video (#1746)',
353 'info_dict': {
354 'id': 'yZIXLfi8CZQ',
355 'ext': 'mp4',
356 'upload_date': '20120608',
357 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
358 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
359 'uploader': 'SET India',
360 'uploader_id': 'setindia'
fccd3771
PH
361 }
362 },
dd27fd17 363 {
4bc3a23e
PH
364 'url': 'http://www.youtube.com/watch?v=a9LDPn-MO4I',
365 'note': '256k DASH audio (format 141) via DASH manifest',
366 'info_dict': {
367 'id': 'a9LDPn-MO4I',
368 'ext': 'm4a',
369 'upload_date': '20121002',
370 'uploader_id': '8KVIDEO',
371 'description': '',
372 'uploader': '8KVIDEO',
373 'title': 'UHDTV TEST 8K VIDEO.mp4'
4919603f 374 },
4bc3a23e
PH
375 'params': {
376 'youtube_include_dash_manifest': True,
377 'format': '141',
4919603f 378 },
dd27fd17 379 },
3489b7d2
JMF
380 # DASH manifest with encrypted signature
381 {
78caa52a
PH
382 'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
383 'info_dict': {
384 'id': 'IB3lcPjvWLA',
385 'ext': 'm4a',
386 'title': 'Afrojack - The Spark ft. Spree Wilson',
387 'description': 'md5:9717375db5a9a3992be4668bbf3bc0a8',
388 'uploader': 'AfrojackVEVO',
389 'uploader_id': 'AfrojackVEVO',
390 'upload_date': '20131011',
3489b7d2 391 },
4bc3a23e 392 'params': {
78caa52a
PH
393 'youtube_include_dash_manifest': True,
394 'format': '141',
3489b7d2
JMF
395 },
396 },
2eb88d95
PH
397 ]
398
e0df6211
PH
399 def __init__(self, *args, **kwargs):
400 super(YoutubeIE, self).__init__(*args, **kwargs)
83799698 401 self._player_cache = {}
e0df6211 402
c5e8d7af
PH
403 def report_video_info_webpage_download(self, video_id):
404 """Report attempt to download video info webpage."""
69ea8ca4 405 self.to_screen('%s: Downloading video info webpage' % video_id)
c5e8d7af 406
c5e8d7af
PH
407 def report_information_extraction(self, video_id):
408 """Report attempt to extract video information."""
69ea8ca4 409 self.to_screen('%s: Extracting video information' % video_id)
c5e8d7af
PH
410
411 def report_unavailable_format(self, video_id, format):
412 """Report extracted video URL."""
69ea8ca4 413 self.to_screen('%s: Format %s not available' % (video_id, format))
c5e8d7af
PH
414
415 def report_rtmp_download(self):
416 """Indicate the download will use the RTMP protocol."""
69ea8ca4 417 self.to_screen('RTMP download detected')
c5e8d7af 418
60064c53
PH
419 def _signature_cache_id(self, example_sig):
420 """ Return a string representation of a signature """
78caa52a 421 return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
60064c53
PH
422
423 def _extract_signature_function(self, video_id, player_url, example_sig):
cf010131 424 id_m = re.match(
c081b35c 425 r'.*-(?P<id>[a-zA-Z0-9_-]+)(?:/watch_as3|/html5player)?\.(?P<ext>[a-z]+)$',
cf010131 426 player_url)
c081b35c
PH
427 if not id_m:
428 raise ExtractorError('Cannot identify player %r' % player_url)
e0df6211
PH
429 player_type = id_m.group('ext')
430 player_id = id_m.group('id')
431
c4417ddb 432 # Read from filesystem cache
60064c53
PH
433 func_id = '%s_%s_%s' % (
434 player_type, player_id, self._signature_cache_id(example_sig))
c4417ddb 435 assert os.path.basename(func_id) == func_id
a0e07d31 436
69ea8ca4 437 cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
a0e07d31 438 if cache_spec is not None:
78caa52a 439 return lambda s: ''.join(s[i] for i in cache_spec)
83799698 440
e0df6211
PH
441 if player_type == 'js':
442 code = self._download_webpage(
443 player_url, video_id,
69ea8ca4
PH
444 note='Downloading %s player %s' % (player_type, player_id),
445 errnote='Download of %s failed' % player_url)
83799698 446 res = self._parse_sig_js(code)
c4417ddb 447 elif player_type == 'swf':
e0df6211
PH
448 urlh = self._request_webpage(
449 player_url, video_id,
69ea8ca4
PH
450 note='Downloading %s player %s' % (player_type, player_id),
451 errnote='Download of %s failed' % player_url)
e0df6211 452 code = urlh.read()
83799698 453 res = self._parse_sig_swf(code)
e0df6211
PH
454 else:
455 assert False, 'Invalid player type %r' % player_type
456
a0e07d31 457 if cache_spec is None:
78caa52a 458 test_string = ''.join(map(compat_chr, range(len(example_sig))))
a0e07d31
PH
459 cache_res = res(test_string)
460 cache_spec = [ord(c) for c in cache_res]
83799698 461
69ea8ca4 462 self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
83799698
PH
463 return res
464
60064c53 465 def _print_sig_code(self, func, example_sig):
edf3e38e
PH
466 def gen_sig_code(idxs):
467 def _genslice(start, end, step):
78caa52a 468 starts = '' if start == 0 else str(start)
69ea8ca4
PH
469 ends = (':%d' % (end+step)) if end + step >= 0 else ':'
470 steps = '' if step == 1 else (':%d' % step)
78caa52a 471 return 's[%s%s%s]' % (starts, ends, steps)
edf3e38e
PH
472
473 step = None
0ca96d48
PH
474 start = '(Never used)' # Quelch pyflakes warnings - start will be
475 # set as soon as step is set
edf3e38e
PH
476 for i, prev in zip(idxs[1:], idxs[:-1]):
477 if step is not None:
478 if i - prev == step:
479 continue
480 yield _genslice(start, prev, step)
481 step = None
482 continue
483 if i - prev in [-1, 1]:
484 step = i - prev
485 start = prev
486 continue
487 else:
78caa52a 488 yield 's[%d]' % prev
edf3e38e 489 if step is None:
78caa52a 490 yield 's[%d]' % i
edf3e38e
PH
491 else:
492 yield _genslice(start, i, step)
493
78caa52a 494 test_string = ''.join(map(compat_chr, range(len(example_sig))))
c705320f 495 cache_res = func(test_string)
edf3e38e 496 cache_spec = [ord(c) for c in cache_res]
78caa52a 497 expr_code = ' + '.join(gen_sig_code(cache_spec))
60064c53
PH
498 signature_id_tuple = '(%s)' % (
499 ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
69ea8ca4 500 code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
78caa52a 501 ' return %s\n') % (signature_id_tuple, expr_code)
69ea8ca4 502 self.to_screen('Extracted signature function:\n' + code)
edf3e38e 503
e0df6211
PH
504 def _parse_sig_js(self, jscode):
505 funcname = self._search_regex(
c26e9ac4 506 r'signature=([$a-zA-Z]+)', jscode,
78caa52a 507 'Initial JS player signature function name')
2b25cb5d
PH
508
509 jsi = JSInterpreter(jscode)
510 initial_function = jsi.extract_function(funcname)
e0df6211
PH
511 return lambda s: initial_function([s])
512
513 def _parse_sig_swf(self, file_contents):
54256267 514 swfi = SWFInterpreter(file_contents)
78caa52a 515 TARGET_CLASSNAME = 'SignatureDecipher'
54256267 516 searched_class = swfi.extract_class(TARGET_CLASSNAME)
78caa52a 517 initial_function = swfi.extract_function(searched_class, 'decipher')
e0df6211
PH
518 return lambda s: initial_function([s])
519
83799698 520 def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
257a2501 521 """Turn the encrypted s field into a working signature"""
6b37f0be 522
c8bf86d5 523 if player_url is None:
69ea8ca4 524 raise ExtractorError('Cannot decrypt signature without player_url')
920de7a2 525
69ea8ca4 526 if player_url.startswith('//'):
78caa52a 527 player_url = 'https:' + player_url
c8bf86d5 528 try:
62af3a0e 529 player_id = (player_url, self._signature_cache_id(s))
c8bf86d5
PH
530 if player_id not in self._player_cache:
531 func = self._extract_signature_function(
60064c53 532 video_id, player_url, s
c8bf86d5
PH
533 )
534 self._player_cache[player_id] = func
535 func = self._player_cache[player_id]
536 if self._downloader.params.get('youtube_print_sig_code'):
60064c53 537 self._print_sig_code(func, s)
c8bf86d5
PH
538 return func(s)
539 except Exception as e:
540 tb = traceback.format_exc()
541 raise ExtractorError(
78caa52a 542 'Signature extraction failed: ' + tb, cause=e)
e0df6211 543
1f343eaa 544 def _get_available_subtitles(self, video_id, webpage):
de7f3446 545 try:
7fad1c63 546 sub_list = self._download_webpage(
38c2e5b8 547 'https://video.google.com/timedtext?hl=en&type=list&v=%s' % video_id,
7fad1c63
JMF
548 video_id, note=False)
549 except ExtractorError as err:
69ea8ca4 550 self._downloader.report_warning('unable to download video subtitles: %s' % compat_str(err))
de7f3446
JMF
551 return {}
552 lang_list = re.findall(r'name="([^"]*)"[^>]+lang_code="([\w\-]+)"', sub_list)
553
554 sub_lang_list = {}
555 for l in lang_list:
556 lang = l[1]
7e660ac1
LD
557 if lang in sub_lang_list:
558 continue
de7f3446
JMF
559 params = compat_urllib_parse.urlencode({
560 'lang': lang,
561 'v': video_id,
ca715127 562 'fmt': self._downloader.params.get('subtitlesformat', 'srt'),
c3197e3e 563 'name': unescapeHTML(l[0]).encode('utf-8'),
de7f3446 564 })
78caa52a 565 url = 'https://www.youtube.com/api/timedtext?' + params
de7f3446
JMF
566 sub_lang_list[lang] = url
567 if not sub_lang_list:
69ea8ca4 568 self._downloader.report_warning('video doesn\'t have subtitles')
de7f3446
JMF
569 return {}
570 return sub_lang_list
571
055e6f36 572 def _get_available_automatic_caption(self, video_id, webpage):
de7f3446
JMF
573 """We need the webpage for getting the captions url, pass it as an
574 argument to speed up the process."""
ca715127 575 sub_format = self._downloader.params.get('subtitlesformat', 'srt')
69ea8ca4 576 self.to_screen('%s: Looking for automatic captions' % video_id)
de7f3446 577 mobj = re.search(r';ytplayer.config = ({.*?});', webpage)
78caa52a 578 err_msg = 'Couldn\'t find automatic captions for %s' % video_id
de7f3446
JMF
579 if mobj is None:
580 self._downloader.report_warning(err_msg)
581 return {}
582 player_config = json.loads(mobj.group(1))
583 try:
584 args = player_config[u'args']
585 caption_url = args[u'ttsurl']
586 timestamp = args[u'timestamp']
055e6f36
JMF
587 # We get the available subtitles
588 list_params = compat_urllib_parse.urlencode({
589 'type': 'list',
590 'tlangs': 1,
591 'asrs': 1,
de7f3446 592 })
055e6f36 593 list_url = caption_url + '&' + list_params
e26f8712 594 caption_list = self._download_xml(list_url, video_id)
e3dc22ca 595 original_lang_node = caption_list.find('track')
f6a54188 596 if original_lang_node is None or original_lang_node.attrib.get('kind') != 'asr' :
69ea8ca4 597 self._downloader.report_warning('Video doesn\'t have automatic captions')
e3dc22ca
JMF
598 return {}
599 original_lang = original_lang_node.attrib['lang_code']
055e6f36
JMF
600
601 sub_lang_list = {}
602 for lang_node in caption_list.findall('target'):
603 sub_lang = lang_node.attrib['lang_code']
604 params = compat_urllib_parse.urlencode({
605 'lang': original_lang,
606 'tlang': sub_lang,
607 'fmt': sub_format,
608 'ts': timestamp,
609 'kind': 'asr',
610 })
611 sub_lang_list[sub_lang] = caption_url + '&' + params
612 return sub_lang_list
de7f3446
JMF
613 # An extractor error can be raise by the download process if there are
614 # no automatic captions but there are subtitles
615 except (KeyError, ExtractorError):
616 self._downloader.report_warning(err_msg)
617 return {}
618
97665381
PH
619 @classmethod
620 def extract_id(cls, url):
621 mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
c5e8d7af 622 if mobj is None:
69ea8ca4 623 raise ExtractorError('Invalid URL: %s' % url)
c5e8d7af
PH
624 video_id = mobj.group(2)
625 return video_id
626
1d043b93
JMF
627 def _extract_from_m3u8(self, manifest_url, video_id):
628 url_map = {}
629 def _get_urls(_manifest):
630 lines = _manifest.split('\n')
631 urls = filter(lambda l: l and not l.startswith('#'),
632 lines)
633 return urls
78caa52a 634 manifest = self._download_webpage(manifest_url, video_id, 'Downloading formats manifest')
1d043b93
JMF
635 formats_urls = _get_urls(manifest)
636 for format_url in formats_urls:
890f62e8 637 itag = self._search_regex(r'itag/(\d+?)/', format_url, 'itag')
1d043b93
JMF
638 url_map[itag] = format_url
639 return url_map
640
1fb07d10
JG
641 def _extract_annotations(self, video_id):
642 url = 'https://www.youtube.com/annotations_invideo?features=1&legacy=1&video_id=%s' % video_id
69ea8ca4 643 return self._download_webpage(url, video_id, note='Searching for annotations.', errnote='Unable to download video annotations.')
1fb07d10 644
c5e8d7af 645 def _real_extract(self, url):
7e8c0af0 646 proto = (
78caa52a
PH
647 'http' if self._downloader.params.get('prefer_insecure', False)
648 else 'https')
7e8c0af0 649
c5e8d7af
PH
650 # Extract original video URL from URL with redirection, like age verification, using next_url parameter
651 mobj = re.search(self._NEXT_URL_RE, url)
652 if mobj:
7e8c0af0 653 url = proto + '://www.youtube.com/' + compat_urllib_parse.unquote(mobj.group(1)).lstrip('/')
97665381 654 video_id = self.extract_id(url)
c5e8d7af
PH
655
656 # Get video webpage
7e8c0af0 657 url = proto + '://www.youtube.com/watch?v=%s&gl=US&hl=en&has_verified=1' % video_id
a1f934b1
PH
658 pref_cookies = [
659 c for c in self._downloader.cookiejar
660 if c.domain == '.youtube.com' and c.name == 'PREF']
661 for pc in pref_cookies:
662 if 'hl=' in pc.value:
663 pc.value = re.sub(r'hl=[^&]+', 'hl=en', pc.value)
664 else:
665 if pc.value:
666 pc.value += '&'
667 pc.value += 'hl=en'
668 video_webpage = self._download_webpage(url, video_id)
c5e8d7af
PH
669
670 # Attempt to extract SWF player URL
e0df6211 671 mobj = re.search(r'swfConfig.*?"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
c5e8d7af
PH
672 if mobj is not None:
673 player_url = re.sub(r'\\(.)', r'\1', mobj.group(1))
674 else:
675 player_url = None
676
677 # Get video info
678 self.report_video_info_webpage_download(video_id)
c108eb73
JMF
679 if re.search(r'player-age-gate-content">', video_webpage) is not None:
680 self.report_age_confirmation()
681 age_gate = True
682 # We simulate the access to the video from www.youtube.com/v/{video_id}
683 # this can be viewed without login into Youtube
2c57c7fa
JMF
684 data = compat_urllib_parse.urlencode({
685 'video_id': video_id,
686 'eurl': 'https://youtube.googleapis.com/v/' + video_id,
c084c934
JMF
687 'sts': self._search_regex(
688 r'"sts"\s*:\s*(\d+)', video_webpage, 'sts'),
2c57c7fa 689 })
7e8c0af0 690 video_info_url = proto + '://www.youtube.com/get_video_info?' + data
c5e8d7af
PH
691 video_info_webpage = self._download_webpage(video_info_url, video_id,
692 note=False,
693 errnote='unable to download video info webpage')
694 video_info = compat_parse_qs(video_info_webpage)
c108eb73
JMF
695 else:
696 age_gate = False
697 for el_type in ['&el=embedded', '&el=detailpage', '&el=vevo', '']:
7e8c0af0 698 video_info_url = (proto + '://www.youtube.com/get_video_info?&video_id=%s%s&ps=default&eurl=&gl=US&hl=en'
c108eb73
JMF
699 % (video_id, el_type))
700 video_info_webpage = self._download_webpage(video_info_url, video_id,
701 note=False,
702 errnote='unable to download video info webpage')
703 video_info = compat_parse_qs(video_info_webpage)
704 if 'token' in video_info:
705 break
c5e8d7af
PH
706 if 'token' not in video_info:
707 if 'reason' in video_info:
d11271dd 708 raise ExtractorError(
78caa52a 709 'YouTube said: %s' % video_info['reason'][0],
d11271dd 710 expected=True, video_id=video_id)
c5e8d7af 711 else:
d11271dd 712 raise ExtractorError(
78caa52a 713 '"token" parameter not in video info for unknown reason',
d11271dd 714 video_id=video_id)
c5e8d7af 715
1d699755
PH
716 if 'view_count' in video_info:
717 view_count = int(video_info['view_count'][0])
718 else:
719 view_count = None
720
c5e8d7af
PH
721 # Check for "rental" videos
722 if 'ypc_video_rental_bar_text' in video_info and 'author' not in video_info:
69ea8ca4 723 raise ExtractorError('"rental" videos not supported')
c5e8d7af
PH
724
725 # Start extracting information
726 self.report_information_extraction(video_id)
727
728 # uploader
729 if 'author' not in video_info:
69ea8ca4 730 raise ExtractorError('Unable to extract uploader name')
c5e8d7af
PH
731 video_uploader = compat_urllib_parse.unquote_plus(video_info['author'][0])
732
733 # uploader_id
734 video_uploader_id = None
735 mobj = re.search(r'<link itemprop="url" href="http://www.youtube.com/(?:user|channel)/([^"]+)">', video_webpage)
736 if mobj is not None:
737 video_uploader_id = mobj.group(1)
738 else:
69ea8ca4 739 self._downloader.report_warning('unable to extract uploader nickname')
c5e8d7af
PH
740
741 # title
a8c6b241 742 if 'title' in video_info:
aa92f063 743 video_title = video_info['title'][0]
a8c6b241 744 else:
69ea8ca4 745 self._downloader.report_warning('Unable to extract video title')
78caa52a 746 video_title = '_'
c5e8d7af
PH
747
748 # thumbnail image
7763b04e
JMF
749 # We try first to get a high quality image:
750 m_thumb = re.search(r'<span itemprop="thumbnail".*?href="(.*?)">',
751 video_webpage, re.DOTALL)
752 if m_thumb is not None:
753 video_thumbnail = m_thumb.group(1)
754 elif 'thumbnail_url' not in video_info:
69ea8ca4 755 self._downloader.report_warning('unable to extract video thumbnail')
f490e77e 756 video_thumbnail = None
c5e8d7af
PH
757 else: # don't panic if we can't find it
758 video_thumbnail = compat_urllib_parse.unquote_plus(video_info['thumbnail_url'][0])
759
760 # upload date
761 upload_date = None
ad3bc6ac 762 mobj = re.search(r'(?s)id="eow-date.*?>(.*?)</span>', video_webpage)
beee53de
PH
763 if mobj is None:
764 mobj = re.search(
263bd4ec 765 r'(?s)id="watch-uploader-info".*?>.*?(?:Published|Uploaded|Streamed live) on (.*?)</strong>',
beee53de 766 video_webpage)
c5e8d7af
PH
767 if mobj is not None:
768 upload_date = ' '.join(re.sub(r'[/,-]', r' ', mobj.group(1)).split())
769 upload_date = unified_strdate(upload_date)
770
55f7bd2d
PH
771 m_cat_container = self._search_regex(
772 r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
773 video_webpage, 'categories', fatal=False)
ec8deefc 774 if m_cat_container:
ad3bc6ac 775 category = self._html_search_regex(
01ed5c9b 776 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
ad3bc6ac
PH
777 default=None)
778 video_categories = None if category is None else [category]
779 else:
780 video_categories = None
ec8deefc 781
c5e8d7af
PH
782 # description
783 video_description = get_element_by_id("eow-description", video_webpage)
784 if video_description:
27dcce19
PH
785 video_description = re.sub(r'''(?x)
786 <a\s+
787 (?:[a-zA-Z-]+="[^"]+"\s+)*?
788 title="([^"]+)"\s+
789 (?:[a-zA-Z-]+="[^"]+"\s+)*?
790 class="yt-uix-redirect-link"\s*>
791 [^<]+
792 </a>
793 ''', r'\1', video_description)
c5e8d7af
PH
794 video_description = clean_html(video_description)
795 else:
796 fd_mobj = re.search(r'<meta name="description" content="([^"]+)"', video_webpage)
797 if fd_mobj:
798 video_description = unescapeHTML(fd_mobj.group(1))
799 else:
78caa52a 800 video_description = ''
c5e8d7af 801
f30a38be 802 def _extract_count(count_name):
46374a56 803 count = self._search_regex(
f30a38be
JMF
804 r'id="watch-%s"[^>]*>.*?([\d,]+)\s*</span>' % re.escape(count_name),
805 video_webpage, count_name, default=None)
336c3a69
JMF
806 if count is not None:
807 return int(count.replace(',', ''))
808 return None
69ea8ca4
PH
809 like_count = _extract_count('like')
810 dislike_count = _extract_count('dislike')
336c3a69 811
c5e8d7af 812 # subtitles
d82134c3 813 video_subtitles = self.extract_subtitles(video_id, video_webpage)
c5e8d7af 814
c5e8d7af 815 if self._downloader.params.get('listsubtitles', False):
d665f8d3 816 self._list_available_subtitles(video_id, video_webpage)
c5e8d7af
PH
817 return
818
819 if 'length_seconds' not in video_info:
69ea8ca4 820 self._downloader.report_warning('unable to extract video duration')
b466b702 821 video_duration = None
c5e8d7af 822 else:
b466b702 823 video_duration = int(compat_urllib_parse.unquote_plus(video_info['length_seconds'][0]))
c5e8d7af 824
1fb07d10
JG
825 # annotations
826 video_annotations = None
827 if self._downloader.params.get('writeannotations', False):
828 video_annotations = self._extract_annotations(video_id)
829
c5e8d7af 830 # Decide which formats to download
c5e8d7af 831 try:
ae7ed920 832 mobj = re.search(r';ytplayer\.config\s*=\s*({.*?});', video_webpage)
50be92c1
PH
833 if not mobj:
834 raise ValueError('Could not find vevo ID')
ae7ed920
PH
835 json_code = uppercase_escape(mobj.group(1))
836 ytplayer_config = json.loads(json_code)
3489b7d2 837 args = ytplayer_config['args']
7ce7e394
JMF
838 # Easy way to know if the 's' value is in url_encoded_fmt_stream_map
839 # this signatures are encrypted
44d46655 840 if 'url_encoded_fmt_stream_map' not in args:
69ea8ca4 841 raise ValueError('No stream_map present') # caught below
00fe14fc
JMF
842 re_signature = re.compile(r'[&,]s=')
843 m_s = re_signature.search(args['url_encoded_fmt_stream_map'])
7ce7e394 844 if m_s is not None:
69ea8ca4 845 self.to_screen('%s: Encrypted signatures detected.' % video_id)
c5e8d7af 846 video_info['url_encoded_fmt_stream_map'] = [args['url_encoded_fmt_stream_map']]
78caa52a 847 m_s = re_signature.search(args.get('adaptive_fmts', ''))
b7a68384 848 if m_s is not None:
00fe14fc
JMF
849 if 'adaptive_fmts' in video_info:
850 video_info['adaptive_fmts'][0] += ',' + args['adaptive_fmts']
37b6d5f6 851 else:
00fe14fc 852 video_info['adaptive_fmts'] = [args['adaptive_fmts']]
c5e8d7af
PH
853 except ValueError:
854 pass
855
dd27fd17
PH
856 def _map_to_format_list(urlmap):
857 formats = []
858 for itag, video_real_url in urlmap.items():
859 dct = {
860 'format_id': itag,
861 'url': video_real_url,
862 'player_url': player_url,
863 }
0b65e5d4
PH
864 if itag in self._formats:
865 dct.update(self._formats[itag])
dd27fd17
PH
866 formats.append(dct)
867 return formats
868
c5e8d7af
PH
869 if 'conn' in video_info and video_info['conn'][0].startswith('rtmp'):
870 self.report_rtmp_download()
dd27fd17
PH
871 formats = [{
872 'format_id': '_rtmp',
873 'protocol': 'rtmp',
874 'url': video_info['conn'][0],
875 'player_url': player_url,
876 }]
00fe14fc
JMF
877 elif len(video_info.get('url_encoded_fmt_stream_map', [])) >= 1 or len(video_info.get('adaptive_fmts', [])) >= 1:
878 encoded_url_map = video_info.get('url_encoded_fmt_stream_map', [''])[0] + ',' + video_info.get('adaptive_fmts',[''])[0]
879 if 'rtmpe%3Dyes' in encoded_url_map:
a7055eb9 880 raise ExtractorError('rtmpe downloads are not supported, see https://github.com/rg3/youtube-dl/issues/343 for more information.', expected=True)
c5e8d7af 881 url_map = {}
00fe14fc 882 for url_data_str in encoded_url_map.split(','):
c5e8d7af 883 url_data = compat_parse_qs(url_data_str)
201e9eaa
PH
884 if 'itag' not in url_data or 'url' not in url_data:
885 continue
886 format_id = url_data['itag'][0]
887 url = url_data['url'][0]
888
889 if 'sig' in url_data:
890 url += '&signature=' + url_data['sig'][0]
891 elif 's' in url_data:
892 encrypted_sig = url_data['s'][0]
893
894 if not age_gate:
895 jsplayer_url_json = self._search_regex(
896 r'"assets":.+?"js":\s*("[^"]+")',
78caa52a 897 video_webpage, 'JS player URL')
201e9eaa
PH
898 player_url = json.loads(jsplayer_url_json)
899 if player_url is None:
900 player_url_json = self._search_regex(
901 r'ytplayer\.config.*?"url"\s*:\s*("[^"]+")',
78caa52a 902 video_webpage, 'age gate player URL')
201e9eaa
PH
903 player_url = json.loads(player_url_json)
904
905 if self._downloader.params.get('verbose'):
cf010131 906 if player_url is None:
201e9eaa
PH
907 player_version = 'unknown'
908 player_desc = 'unknown'
909 else:
910 if player_url.endswith('swf'):
911 player_version = self._search_regex(
912 r'-(.+?)(?:/watch_as3)?\.swf$', player_url,
78caa52a 913 'flash player', fatal=False)
201e9eaa 914 player_desc = 'flash player %s' % player_version
cf010131 915 else:
201e9eaa
PH
916 player_version = self._search_regex(
917 r'html5player-([^/]+?)(?:/html5player)?\.js',
918 player_url,
919 'html5 player', fatal=False)
78caa52a 920 player_desc = 'html5 player %s' % player_version
201e9eaa 921
60064c53 922 parts_sizes = self._signature_cache_id(encrypted_sig)
69ea8ca4 923 self.to_screen('{%s} signature length %s, %s' %
98eb1c3f 924 (format_id, parts_sizes, player_desc))
201e9eaa
PH
925
926 signature = self._decrypt_signature(
927 encrypted_sig, video_id, player_url, age_gate)
928 url += '&signature=' + signature
929 if 'ratebypass' not in url:
930 url += '&ratebypass=yes'
931 url_map[format_id] = url
dd27fd17 932 formats = _map_to_format_list(url_map)
1d043b93
JMF
933 elif video_info.get('hlsvp'):
934 manifest_url = video_info['hlsvp'][0]
935 url_map = self._extract_from_m3u8(manifest_url, video_id)
dd27fd17 936 formats = _map_to_format_list(url_map)
c5e8d7af 937 else:
69ea8ca4 938 raise ExtractorError('no conn, hlsvp or url_encoded_fmt_stream_map information found in video info')
c5e8d7af 939
dd27fd17 940 # Look for the DASH manifest
203fb43f 941 if self._downloader.params.get('youtube_include_dash_manifest', True):
dd27fd17 942 try:
d68f0cdb 943 # The DASH manifest used needs to be the one from the original video_webpage.
944 # The one found in get_video_info seems to be using different signatures.
945 # However, in the case of an age restriction there won't be any embedded dashmpd in the video_webpage.
946 # Luckily, it seems, this case uses some kind of default signature (len == 86), so the
947 # combination of get_video_info and the _static_decrypt_signature() decryption fallback will work here.
948 if age_gate:
3489b7d2 949 dash_manifest_url = video_info.get('dashmpd')[0]
d68f0cdb 950 else:
3489b7d2 951 dash_manifest_url = ytplayer_config['args']['dashmpd']
d68f0cdb 952 def decrypt_sig(mobj):
953 s = mobj.group(1)
954 dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
955 return '/signature/%s' % dec_s
956 dash_manifest_url = re.sub(r'/s/([\w\.]+)', decrypt_sig, dash_manifest_url)
dd27fd17 957 dash_doc = self._download_xml(
d68f0cdb 958 dash_manifest_url, video_id,
69ea8ca4
PH
959 note='Downloading DASH manifest',
960 errnote='Could not download DASH manifest')
961 for r in dash_doc.findall('.//{urn:mpeg:DASH:schema:MPD:2011}Representation'):
dd27fd17
PH
962 url_el = r.find('{urn:mpeg:DASH:schema:MPD:2011}BaseURL')
963 if url_el is None:
964 continue
965 format_id = r.attrib['id']
966 video_url = url_el.text
967 filesize = int_or_none(url_el.attrib.get('{http://youtube.com/yt/2012/10/10}contentLength'))
968 f = {
969 'format_id': format_id,
970 'url': video_url,
971 'width': int_or_none(r.attrib.get('width')),
972 'tbr': int_or_none(r.attrib.get('bandwidth'), 1000),
973 'asr': int_or_none(r.attrib.get('audioSamplingRate')),
974 'filesize': filesize,
975 }
976 try:
977 existing_format = next(
978 fo for fo in formats
979 if fo['format_id'] == format_id)
980 except StopIteration:
981 f.update(self._formats.get(format_id, {}))
982 formats.append(f)
983 else:
984 existing_format.update(f)
985
986 except (ExtractorError, KeyError) as e:
69ea8ca4 987 self.report_warning('Skipping DASH manifest: %s' % e, video_id)
d80044c2 988
4bcc7bd1 989 self._sort_formats(formats)
4ea3be0a 990
991 return {
992 'id': video_id,
993 'uploader': video_uploader,
994 'uploader_id': video_uploader_id,
995 'upload_date': upload_date,
996 'title': video_title,
997 'thumbnail': video_thumbnail,
998 'description': video_description,
ec8deefc 999 'categories': video_categories,
4ea3be0a 1000 'subtitles': video_subtitles,
1001 'duration': video_duration,
1002 'age_limit': 18 if age_gate else 0,
1003 'annotations': video_annotations,
7e8c0af0 1004 'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
4ea3be0a 1005 'view_count': view_count,
1006 'like_count': like_count,
1007 'dislike_count': dislike_count,
1008 'formats': formats,
1009 }
c5e8d7af 1010
880e1c52 1011class YoutubePlaylistIE(YoutubeBaseInfoExtractor):
78caa52a 1012 IE_DESC = 'YouTube.com playlists'
d67cc9fa 1013 _VALID_URL = r"""(?x)(?:
c5e8d7af
PH
1014 (?:https?://)?
1015 (?:\w+\.)?
1016 youtube\.com/
1017 (?:
ac7553d0 1018 (?:course|view_play_list|my_playlists|artist|playlist|watch|embed/videoseries)
c5e8d7af
PH
1019 \? (?:.*?&)*? (?:p|a|list)=
1020 | p/
1021 )
d67cc9fa 1022 (
7d568f5a 1023 (?:PL|LL|EC|UU|FL|RD)?[0-9A-Za-z-_]{10,}
d67cc9fa
JMF
1024 # Top tracks, they can also include dots
1025 |(?:MC)[\w\.]*
1026 )
c5e8d7af
PH
1027 .*
1028 |
7d568f5a 1029 ((?:PL|LL|EC|UU|FL|RD)[0-9A-Za-z-_]{10,})
c5e8d7af 1030 )"""
dbb94fb0 1031 _TEMPLATE_URL = 'https://www.youtube.com/playlist?list=%s'
dcbb4580 1032 _MORE_PAGES_INDICATOR = r'data-link-type="next"'
dbb94fb0 1033 _VIDEO_RE = r'href="\s*/watch\?v=(?P<id>[0-9A-Za-z_-]{11})&amp;[^"]*?index=(?P<index>\d+)'
78caa52a 1034 IE_NAME = 'youtube:playlist'
81127aa5
PH
1035 _TESTS = [{
1036 'url': 'https://www.youtube.com/playlist?list=PLwiyx1dc3P2JR9N8gQaQN_BCvlSlap7re',
1037 'info_dict': {
1038 'title': 'ytdl test PL',
1039 },
1040 'playlist_count': 3,
9291475f
PH
1041 }, {
1042 'url': 'https://www.youtube.com/playlist?list=PLtPgu7CB4gbZDA7i_euNxn75ISqxwZPYx',
1043 'info_dict': {
1044 'title': 'YDL_Empty_List',
1045 },
1046 'playlist_count': 0,
1047 }, {
1048 'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
1049 'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
1050 'info_dict': {
1051 'title': '29C3: Not my department',
1052 },
1053 'playlist_count': 95,
1054 }, {
1055 'note': 'issue #673',
1056 'url': 'PLBB231211A4F62143',
1057 'info_dict': {
1058 'title': 'Team Fortress 2 (Class-based LP)',
1059 },
1060 'playlist_mincount': 26,
1061 }, {
1062 'note': 'Large playlist',
1063 'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
1064 'info_dict': {
1065 'title': 'Uploads from Cauchemar',
1066 },
1067 'playlist_mincount': 799,
1068 }, {
1069 'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
1070 'info_dict': {
1071 'title': 'YDL_safe_search',
1072 },
1073 'playlist_count': 2,
ac7553d0
PH
1074 }, {
1075 'note': 'embedded',
1076 'url': 'http://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
1077 'playlist_count': 4,
1078 'info_dict': {
1079 'title': 'JODA15',
1080 }
6b08cdf6
PH
1081 }, {
1082 'note': 'Embedded SWF player',
1083 'url': 'http://www.youtube.com/p/YN5VISEtHet5D4NEvfTd0zcgFk84NqFZ?hl=en_US&fs=1&rel=0',
1084 'playlist_count': 4,
1085 'info_dict': {
1086 'title': 'JODA7',
1087 }
81127aa5 1088 }]
c5e8d7af 1089
880e1c52
JMF
1090 def _real_initialize(self):
1091 self._login()
1092
652cdaa2 1093 def _ids_to_results(self, ids):
c9cc0bf5
PH
1094 return [
1095 self.url_result(vid_id, 'Youtube', video_id=vid_id)
1096 for vid_id in ids]
652cdaa2
JMF
1097
1098 def _extract_mix(self, playlist_id):
1099 # The mixes are generated from a a single video
1100 # the id of the playlist is just 'RD' + video_id
7d4afc55 1101 url = 'https://youtube.com/watch?v=%s&list=%s' % (playlist_id[-11:], playlist_id)
c9cc0bf5 1102 webpage = self._download_webpage(
78caa52a 1103 url, playlist_id, 'Downloading Youtube mix')
bc2f773b 1104 search_title = lambda class_name: get_element_by_attribute('class', class_name, webpage)
c9cc0bf5
PH
1105 title_span = (
1106 search_title('playlist-title') or
1107 search_title('title long-title') or
1108 search_title('title'))
76d1700b 1109 title = clean_html(title_span)
c9cc0bf5
PH
1110 ids = orderedSet(re.findall(
1111 r'''(?xs)data-video-username=".*?".*?
1112 href="/watch\?v=([0-9A-Za-z_-]{11})&amp;[^"]*?list=%s''' % re.escape(playlist_id),
1113 webpage))
652cdaa2
JMF
1114 url_results = self._ids_to_results(ids)
1115
1116 return self.playlist_result(url_results, playlist_id, title)
1117
c5e8d7af
PH
1118 def _real_extract(self, url):
1119 # Extract playlist id
d67cc9fa 1120 mobj = re.match(self._VALID_URL, url)
c5e8d7af 1121 if mobj is None:
69ea8ca4 1122 raise ExtractorError('Invalid URL: %s' % url)
47192f92
FV
1123 playlist_id = mobj.group(1) or mobj.group(2)
1124
1125 # Check if it's a video-specific URL
7c61bd36 1126 query_dict = compat_urlparse.parse_qs(compat_urlparse.urlparse(url).query)
47192f92
FV
1127 if 'v' in query_dict:
1128 video_id = query_dict['v'][0]
1129 if self._downloader.params.get('noplaylist'):
69ea8ca4 1130 self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
7012b23c 1131 return self.url_result(video_id, 'Youtube', video_id=video_id)
47192f92 1132 else:
69ea8ca4 1133 self.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id, video_id))
c5e8d7af 1134
7d4afc55 1135 if playlist_id.startswith('RD'):
652cdaa2
JMF
1136 # Mixes require a custom extraction process
1137 return self._extract_mix(playlist_id)
0a688bc0 1138 if playlist_id.startswith('TL'):
69ea8ca4 1139 raise ExtractorError('For downloading YouTube.com top lists, use '
78caa52a 1140 'the "yttoplist" keyword, for example "youtube-dl \'yttoplist:music:Top Tracks\'"', expected=True)
652cdaa2 1141
dbb94fb0
S
1142 url = self._TEMPLATE_URL % playlist_id
1143 page = self._download_webpage(url, playlist_id)
1144 more_widget_html = content_html = page
1145
10c0e2d8 1146 # Check if the playlist exists or is private
e399853d 1147 if re.search(r'<div class="yt-alert-message">[^<]*?(The|This) playlist (does not exist|is private)[^<]*?</div>', page) is not None:
10c0e2d8 1148 raise ExtractorError(
78caa52a 1149 'The playlist doesn\'t exist or is private, use --username or '
10c0e2d8
JMF
1150 '--netrc to access it.',
1151 expected=True)
1152
dcbb4580
JMF
1153 # Extract the video ids from the playlist pages
1154 ids = []
c5e8d7af 1155
755eb032 1156 for page_num in itertools.count(1):
dbb94fb0 1157 matches = re.finditer(self._VIDEO_RE, content_html)
6e47b51e
JMF
1158 # We remove the duplicates and the link with index 0
1159 # (it's not the first video of the playlist)
1160 new_ids = orderedSet(m.group('id') for m in matches if m.group('index') != '0')
dcbb4580 1161 ids.extend(new_ids)
c5e8d7af 1162
dbb94fb0
S
1163 mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
1164 if not mobj:
c5e8d7af
PH
1165 break
1166
dbb94fb0 1167 more = self._download_json(
5912c639
PH
1168 'https://youtube.com/%s' % mobj.group('more'), playlist_id,
1169 'Downloading page #%s' % page_num,
1170 transform_source=uppercase_escape)
dbb94fb0
S
1171 content_html = more['content_html']
1172 more_widget_html = more['load_more_widget_html']
1173
1174 playlist_title = self._html_search_regex(
68eb8e90 1175 r'(?s)<h1 class="pl-header-title[^"]*">\s*(.*?)\s*</h1>',
78caa52a 1176 page, 'title')
c5e8d7af 1177
652cdaa2 1178 url_results = self._ids_to_results(ids)
dcbb4580 1179 return self.playlist_result(url_results, playlist_id, playlist_title)
c5e8d7af
PH
1180
1181
0a688bc0 1182class YoutubeTopListIE(YoutubePlaylistIE):
78caa52a 1183 IE_NAME = 'youtube:toplist'
69ea8ca4 1184 IE_DESC = ('YouTube.com top lists, "yttoplist:{channel}:{list title}"'
78caa52a 1185 ' (Example: "yttoplist:music:Top Tracks")')
0a688bc0 1186 _VALID_URL = r'yttoplist:(?P<chann>.*?):(?P<title>.*?)$'
cdc628a4
PH
1187 _TESTS = [{
1188 'url': 'yttoplist:music:Trending',
1189 'playlist_mincount': 5,
1190 'skip': 'Only works for logged-in users',
1191 }]
0a688bc0
JMF
1192
1193 def _real_extract(self, url):
1194 mobj = re.match(self._VALID_URL, url)
1195 channel = mobj.group('chann')
1196 title = mobj.group('title')
1197 query = compat_urllib_parse.urlencode({'title': title})
cdc628a4
PH
1198 channel_page = self._download_webpage(
1199 'https://www.youtube.com/%s' % channel, title)
1200 link = self._html_search_regex(
1201 r'''(?x)
1202 <a\s+href="([^"]+)".*?>\s*
1203 <span\s+class="branded-page-module-title-text">\s*
1204 <span[^>]*>.*?%s.*?</span>''' % re.escape(query),
1205 channel_page, 'list')
0a688bc0
JMF
1206 url = compat_urlparse.urljoin('https://www.youtube.com/', link)
1207
1208 video_re = r'data-index="\d+".*?data-video-id="([0-9A-Za-z_-]{11})"'
1209 ids = []
1210 # sometimes the webpage doesn't contain the videos
1211 # retry until we get them
1212 for i in itertools.count(0):
78caa52a 1213 msg = 'Downloading Youtube mix'
0a688bc0
JMF
1214 if i > 0:
1215 msg += ', retry #%d' % i
c9cc0bf5 1216
0a688bc0
JMF
1217 webpage = self._download_webpage(url, title, msg)
1218 ids = orderedSet(re.findall(video_re, webpage))
1219 if ids:
1220 break
1221 url_results = self._ids_to_results(ids)
1222 return self.playlist_result(url_results, playlist_title=title)
1223
1224
c5e8d7af 1225class YoutubeChannelIE(InfoExtractor):
78caa52a 1226 IE_DESC = 'YouTube.com channels'
c5e8d7af 1227 _VALID_URL = r"^(?:https?://)?(?:youtu\.be|(?:\w+\.)?youtube(?:-nocookie)?\.com)/channel/([0-9A-Za-z_-]+)"
c5e8d7af 1228 _MORE_PAGES_INDICATOR = 'yt-uix-load-more'
38c2e5b8 1229 _MORE_PAGES_URL = 'https://www.youtube.com/c4_browse_ajax?action_load_more_videos=1&flow=list&paging=%s&view=0&sort=da&channel_id=%s'
78caa52a 1230 IE_NAME = 'youtube:channel'
cdc628a4
PH
1231 _TESTS = [{
1232 'note': 'paginated channel',
1233 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
1234 'playlist_mincount': 91,
1235 }]
c5e8d7af
PH
1236
1237 def extract_videos_from_page(self, page):
1238 ids_in_page = []
1239 for mobj in re.finditer(r'href="/watch\?v=([0-9A-Za-z_-]+)&?', page):
1240 if mobj.group(1) not in ids_in_page:
1241 ids_in_page.append(mobj.group(1))
1242 return ids_in_page
1243
1244 def _real_extract(self, url):
1245 # Extract channel id
1246 mobj = re.match(self._VALID_URL, url)
1247 if mobj is None:
69ea8ca4 1248 raise ExtractorError('Invalid URL: %s' % url)
c5e8d7af
PH
1249
1250 # Download channel page
1251 channel_id = mobj.group(1)
1252 video_ids = []
b9643eed
JMF
1253 url = 'https://www.youtube.com/channel/%s/videos' % channel_id
1254 channel_page = self._download_webpage(url, channel_id)
31812a9e
PH
1255 autogenerated = re.search(r'''(?x)
1256 class="[^"]*?(?:
1257 channel-header-autogenerated-label|
1258 yt-channel-title-autogenerated
1259 )[^"]*"''', channel_page) is not None
c5e8d7af 1260
b9643eed
JMF
1261 if autogenerated:
1262 # The videos are contained in a single page
1263 # the ajax pages can't be used, they are empty
1264 video_ids = self.extract_videos_from_page(channel_page)
1265 else:
1266 # Download all channel pages using the json-based channel_ajax query
1267 for pagenum in itertools.count(1):
1268 url = self._MORE_PAGES_URL % (pagenum, channel_id)
81c2f20b 1269 page = self._download_json(
69ea8ca4 1270 url, channel_id, note='Downloading page #%s' % pagenum,
81c2f20b
PH
1271 transform_source=uppercase_escape)
1272
b9643eed
JMF
1273 ids_in_page = self.extract_videos_from_page(page['content_html'])
1274 video_ids.extend(ids_in_page)
1275
1276 if self._MORE_PAGES_INDICATOR not in page['load_more_widget_html']:
1277 break
c5e8d7af 1278
69ea8ca4 1279 self._downloader.to_screen('[youtube] Channel %s: Found %i videos' % (channel_id, len(video_ids)))
c5e8d7af 1280
7012b23c
PH
1281 url_entries = [self.url_result(video_id, 'Youtube', video_id=video_id)
1282 for video_id in video_ids]
1283 return self.playlist_result(url_entries, channel_id)
c5e8d7af
PH
1284
1285
1286class YoutubeUserIE(InfoExtractor):
78caa52a 1287 IE_DESC = 'YouTube.com user videos (URL or "ytuser" keyword)'
c9ae7b95 1288 _VALID_URL = r'(?:(?:(?:https?://)?(?:\w+\.)?youtube\.com/(?:user/)?(?!(?:attribution_link|watch|results)(?:$|[^a-z_A-Z0-9-])))|ytuser:)(?!feed/)([A-Za-z0-9_-]+)'
38c2e5b8 1289 _TEMPLATE_URL = 'https://gdata.youtube.com/feeds/api/users/%s'
c5e8d7af 1290 _GDATA_PAGE_SIZE = 50
38c2e5b8 1291 _GDATA_URL = 'https://gdata.youtube.com/feeds/api/users/%s/uploads?max-results=%d&start-index=%d&alt=json'
78caa52a 1292 IE_NAME = 'youtube:user'
c5e8d7af 1293
cdc628a4
PH
1294 _TESTS = [{
1295 'url': 'https://www.youtube.com/user/TheLinuxFoundation',
1296 'playlist_mincount': 320,
1297 'info_dict': {
1298 'title': 'TheLinuxFoundation',
1299 }
1300 }, {
1301 'url': 'ytuser:phihag',
1302 'only_matching': True,
1303 }]
1304
e3ea4790 1305 @classmethod
f4b05232 1306 def suitable(cls, url):
e3ea4790
JMF
1307 # Don't return True if the url can be extracted with other youtube
1308 # extractor, the regex would is too permissive and it would match.
1309 other_ies = iter(klass for (name, klass) in globals().items() if name.endswith('IE') and klass is not cls)
1310 if any(ie.suitable(url) for ie in other_ies): return False
f4b05232
JMF
1311 else: return super(YoutubeUserIE, cls).suitable(url)
1312
c5e8d7af
PH
1313 def _real_extract(self, url):
1314 # Extract username
1315 mobj = re.match(self._VALID_URL, url)
1316 if mobj is None:
69ea8ca4 1317 raise ExtractorError('Invalid URL: %s' % url)
c5e8d7af
PH
1318
1319 username = mobj.group(1)
1320
1321 # Download video ids using YouTube Data API. Result size per
1322 # query is limited (currently to 50 videos) so we need to query
1323 # page by page until there are no video ids - it means we got
1324 # all of them.
1325
b7ab0590 1326 def download_page(pagenum):
c5e8d7af
PH
1327 start_index = pagenum * self._GDATA_PAGE_SIZE + 1
1328
1329 gdata_url = self._GDATA_URL % (username, self._GDATA_PAGE_SIZE, start_index)
b7ab0590
PH
1330 page = self._download_webpage(
1331 gdata_url, username,
78caa52a 1332 'Downloading video ids from %d to %d' % (
b7ab0590 1333 start_index, start_index + self._GDATA_PAGE_SIZE))
c5e8d7af 1334
fd9cf738
JMF
1335 try:
1336 response = json.loads(page)
1337 except ValueError as err:
69ea8ca4 1338 raise ExtractorError('Invalid JSON in API response: ' + compat_str(err))
71c82637 1339 if 'entry' not in response['feed']:
b7ab0590 1340 return
fd9cf738 1341
c5e8d7af 1342 # Extract video identifiers
e302f9ce
PH
1343 entries = response['feed']['entry']
1344 for entry in entries:
1345 title = entry['title']['$t']
1346 video_id = entry['id']['$t'].split('/')[-1]
b7ab0590 1347 yield {
e302f9ce
PH
1348 '_type': 'url',
1349 'url': video_id,
1350 'ie_key': 'Youtube',
b11cec41 1351 'id': video_id,
e302f9ce 1352 'title': title,
b7ab0590 1353 }
9c44d242 1354 url_results = OnDemandPagedList(download_page, self._GDATA_PAGE_SIZE)
c5e8d7af 1355
7012b23c
PH
1356 return self.playlist_result(url_results, playlist_title=username)
1357
b05654f0
PH
1358
1359class YoutubeSearchIE(SearchInfoExtractor):
78caa52a
PH
1360 IE_DESC = 'YouTube.com searches'
1361 _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc'
b05654f0 1362 _MAX_RESULTS = 1000
78caa52a 1363 IE_NAME = 'youtube:search'
b05654f0
PH
1364 _SEARCH_KEY = 'ytsearch'
1365
b05654f0
PH
1366 def _get_n_results(self, query, n):
1367 """Get a specified number of results for a query"""
1368
1369 video_ids = []
1370 pagenum = 0
1371 limit = n
83d548ef 1372 PAGE_SIZE = 50
b05654f0 1373
83d548ef
PH
1374 while (PAGE_SIZE * pagenum) < limit:
1375 result_url = self._API_URL % (
1376 compat_urllib_parse.quote_plus(query.encode('utf-8')),
1377 (PAGE_SIZE * pagenum) + 1)
7cc3570e 1378 data_json = self._download_webpage(
69ea8ca4
PH
1379 result_url, video_id='query "%s"' % query,
1380 note='Downloading page %s' % (pagenum + 1),
1381 errnote='Unable to download API page')
7cc3570e
PH
1382 data = json.loads(data_json)
1383 api_response = data['data']
1384
1385 if 'items' not in api_response:
07ad22b8 1386 raise ExtractorError(
78caa52a 1387 '[youtube] No video results', expected=True)
b05654f0
PH
1388
1389 new_ids = list(video['id'] for video in api_response['items'])
1390 video_ids += new_ids
1391
1392 limit = min(n, api_response['totalItems'])
1393 pagenum += 1
1394
1395 if len(video_ids) > n:
1396 video_ids = video_ids[:n]
7012b23c
PH
1397 videos = [self.url_result(video_id, 'Youtube', video_id=video_id)
1398 for video_id in video_ids]
b05654f0 1399 return self.playlist_result(videos, query)
75dff0ee 1400
c9ae7b95 1401
a3dd9248 1402class YoutubeSearchDateIE(YoutubeSearchIE):
cb7fb546 1403 IE_NAME = YoutubeSearchIE.IE_NAME + ':date'
a3dd9248
CM
1404 _API_URL = 'https://gdata.youtube.com/feeds/api/videos?q=%s&start-index=%i&max-results=50&v=2&alt=jsonc&orderby=published'
1405 _SEARCH_KEY = 'ytsearchdate'
78caa52a 1406 IE_DESC = 'YouTube.com searches, newest videos first'
75dff0ee 1407
c9ae7b95
PH
1408
1409class YoutubeSearchURLIE(InfoExtractor):
78caa52a
PH
1410 IE_DESC = 'YouTube.com search URLs'
1411 IE_NAME = 'youtube:search_url'
c9ae7b95 1412 _VALID_URL = r'https?://(?:www\.)?youtube\.com/results\?(.*?&)?search_query=(?P<query>[^&]+)(?:[&]|$)'
cdc628a4
PH
1413 _TESTS = [{
1414 'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
1415 'playlist_mincount': 5,
1416 'info_dict': {
1417 'title': 'youtube-dl test video',
1418 }
1419 }]
c9ae7b95
PH
1420
1421 def _real_extract(self, url):
1422 mobj = re.match(self._VALID_URL, url)
1423 query = compat_urllib_parse.unquote_plus(mobj.group('query'))
1424
1425 webpage = self._download_webpage(url, query)
1426 result_code = self._search_regex(
78caa52a 1427 r'(?s)<ol class="item-section"(.*?)</ol>', webpage, 'result HTML')
c9ae7b95
PH
1428
1429 part_codes = re.findall(
1430 r'(?s)<h3 class="yt-lockup-title">(.*?)</h3>', result_code)
1431 entries = []
1432 for part_code in part_codes:
1433 part_title = self._html_search_regex(
6feb2d5e 1434 [r'(?s)title="([^"]+)"', r'>([^<]+)</a>'], part_code, 'item title', fatal=False)
c9ae7b95
PH
1435 part_url_snippet = self._html_search_regex(
1436 r'(?s)href="([^"]+)"', part_code, 'item URL')
1437 part_url = compat_urlparse.urljoin(
1438 'https://www.youtube.com/', part_url_snippet)
1439 entries.append({
1440 '_type': 'url',
1441 'url': part_url,
1442 'title': part_title,
1443 })
1444
1445 return {
1446 '_type': 'playlist',
1447 'entries': entries,
1448 'title': query,
1449 }
1450
1451
75dff0ee 1452class YoutubeShowIE(InfoExtractor):
78caa52a 1453 IE_DESC = 'YouTube.com (multi-season) shows'
cdc628a4 1454 _VALID_URL = r'https?://www\.youtube\.com/show/(?P<id>[^?#]*)'
78caa52a 1455 IE_NAME = 'youtube:show'
cdc628a4
PH
1456 _TESTS = [{
1457 'url': 'http://www.youtube.com/show/airdisasters',
1458 'playlist_mincount': 3,
1459 'info_dict': {
1460 'id': 'airdisasters',
1461 'title': 'Air Disasters',
1462 }
1463 }]
75dff0ee
JMF
1464
1465 def _real_extract(self, url):
1466 mobj = re.match(self._VALID_URL, url)
cdc628a4
PH
1467 playlist_id = mobj.group('id')
1468 webpage = self._download_webpage(
1469 url, playlist_id, 'Downloading show webpage')
75dff0ee
JMF
1470 # There's one playlist for each season of the show
1471 m_seasons = list(re.finditer(r'href="(/playlist\?list=.*?)"', webpage))
cdc628a4
PH
1472 self.to_screen('%s: Found %s seasons' % (playlist_id, len(m_seasons)))
1473 entries = [
1474 self.url_result(
1475 'https://www.youtube.com' + season.group(1), 'YoutubePlaylist')
1476 for season in m_seasons
1477 ]
1478 title = self._og_search_title(webpage, fatal=False)
1479
1480 return {
1481 '_type': 'playlist',
1482 'id': playlist_id,
1483 'title': title,
1484 'entries': entries,
1485 }
04cc9617
JMF
1486
1487
b2e8bc1b 1488class YoutubeFeedsInfoExtractor(YoutubeBaseInfoExtractor):
d7ae0639
JMF
1489 """
1490 Base class for extractors that fetch info from
1491 http://www.youtube.com/feed_ajax
1492 Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
1493 """
b2e8bc1b 1494 _LOGIN_REQUIRED = True
43ba5456
JMF
1495 # use action_load_personal_feed instead of action_load_system_feed
1496 _PERSONAL_FEED = False
04cc9617 1497
d7ae0639
JMF
1498 @property
1499 def _FEED_TEMPLATE(self):
43ba5456
JMF
1500 action = 'action_load_system_feed'
1501 if self._PERSONAL_FEED:
1502 action = 'action_load_personal_feed'
38c2e5b8 1503 return 'https://www.youtube.com/feed_ajax?%s=1&feed_name=%s&paging=%%s' % (action, self._FEED_NAME)
d7ae0639
JMF
1504
1505 @property
1506 def IE_NAME(self):
78caa52a 1507 return 'youtube:%s' % self._FEED_NAME
04cc9617 1508
81f0259b 1509 def _real_initialize(self):
b2e8bc1b 1510 self._login()
81f0259b 1511
04cc9617
JMF
1512 def _real_extract(self, url):
1513 feed_entries = []
0e44d838
JMF
1514 paging = 0
1515 for i in itertools.count(1):
f6177462 1516 info = self._download_json(self._FEED_TEMPLATE % paging,
78caa52a
PH
1517 '%s feed' % self._FEED_NAME,
1518 'Downloading page %s' % i)
f6177462 1519 feed_html = info.get('feed_html') or info.get('content_html')
1a9b9649 1520 load_more_widget_html = info.get('load_more_widget_html') or feed_html
43ba5456 1521 m_ids = re.finditer(r'"/watch\?v=(.*?)["&]', feed_html)
04cc9617 1522 ids = orderedSet(m.group(1) for m in m_ids)
7012b23c
PH
1523 feed_entries.extend(
1524 self.url_result(video_id, 'Youtube', video_id=video_id)
1525 for video_id in ids)
05ee2b6d
JMF
1526 mobj = re.search(
1527 r'data-uix-load-more-href="/?[^"]+paging=(?P<paging>\d+)',
1a9b9649 1528 load_more_widget_html)
05ee2b6d 1529 if mobj is None:
04cc9617 1530 break
05ee2b6d 1531 paging = mobj.group('paging')
d7ae0639
JMF
1532 return self.playlist_result(feed_entries, playlist_title=self._PLAYLIST_TITLE)
1533
d7ae0639 1534class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
78caa52a 1535 IE_DESC = 'YouTube.com recommended videos, "ytrec" keyword (requires authentication)'
d7ae0639
JMF
1536 _VALID_URL = r'https?://www\.youtube\.com/feed/recommended|:ytrec(?:ommended)?'
1537 _FEED_NAME = 'recommended'
78caa52a 1538 _PLAYLIST_TITLE = 'Youtube Recommended videos'
c626a3d9 1539
43ba5456 1540class YoutubeWatchLaterIE(YoutubeFeedsInfoExtractor):
78caa52a 1541 IE_DESC = 'Youtube watch later list, "ytwatchlater" keyword (requires authentication)'
43ba5456
JMF
1542 _VALID_URL = r'https?://www\.youtube\.com/feed/watch_later|:ytwatchlater'
1543 _FEED_NAME = 'watch_later'
78caa52a 1544 _PLAYLIST_TITLE = 'Youtube Watch Later'
43ba5456 1545 _PERSONAL_FEED = True
c626a3d9 1546
f459d170 1547class YoutubeHistoryIE(YoutubeFeedsInfoExtractor):
78caa52a
PH
1548 IE_DESC = 'Youtube watch history, "ythistory" keyword (requires authentication)'
1549 _VALID_URL = 'https?://www\.youtube\.com/feed/history|:ythistory'
f459d170
JMF
1550 _FEED_NAME = 'history'
1551 _PERSONAL_FEED = True
78caa52a 1552 _PLAYLIST_TITLE = 'Youtube Watch History'
f459d170 1553
c626a3d9 1554class YoutubeFavouritesIE(YoutubeBaseInfoExtractor):
78caa52a
PH
1555 IE_NAME = 'youtube:favorites'
1556 IE_DESC = 'YouTube.com favourite videos, "ytfav" keyword (requires authentication)'
c7a7750d 1557 _VALID_URL = r'https?://www\.youtube\.com/my_favorites|:ytfav(?:ou?rites)?'
c626a3d9
JMF
1558 _LOGIN_REQUIRED = True
1559
1560 def _real_extract(self, url):
1561 webpage = self._download_webpage('https://www.youtube.com/my_favorites', 'Youtube Favourites videos')
78caa52a 1562 playlist_id = self._search_regex(r'list=(.+?)["&]', webpage, 'favourites playlist id')
c626a3d9 1563 return self.url_result(playlist_id, 'YoutubePlaylist')
15870e90
PH
1564
1565
1ed5b5c9 1566class YoutubeSubscriptionsIE(YoutubePlaylistIE):
78caa52a
PH
1567 IE_NAME = 'youtube:subscriptions'
1568 IE_DESC = 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
1ed5b5c9 1569 _VALID_URL = r'https?://www\.youtube\.com/feed/subscriptions|:ytsubs(?:criptions)?'
81127aa5 1570 _TESTS = []
1ed5b5c9
JMF
1571
1572 def _real_extract(self, url):
78caa52a 1573 title = 'Youtube Subscriptions'
1ed5b5c9
JMF
1574 page = self._download_webpage('https://www.youtube.com/feed/subscriptions', title)
1575
1576 # The extraction process is the same as for playlists, but the regex
1577 # for the video ids doesn't contain an index
1578 ids = []
1579 more_widget_html = content_html = page
1580
1581 for page_num in itertools.count(1):
1582 matches = re.findall(r'href="\s*/watch\?v=([0-9A-Za-z_-]{11})', content_html)
1583 new_ids = orderedSet(matches)
1584 ids.extend(new_ids)
1585
1586 mobj = re.search(r'data-uix-load-more-href="/?(?P<more>[^"]+)"', more_widget_html)
1587 if not mobj:
1588 break
1589
1590 more = self._download_json(
1591 'https://youtube.com/%s' % mobj.group('more'), title,
1592 'Downloading page #%s' % page_num,
1593 transform_source=uppercase_escape)
1594 content_html = more['content_html']
1595 more_widget_html = more['load_more_widget_html']
1596
1597 return {
1598 '_type': 'playlist',
1599 'title': title,
1600 'entries': self._ids_to_results(ids),
1601 }
1602
1603
15870e90
PH
1604class YoutubeTruncatedURLIE(InfoExtractor):
1605 IE_NAME = 'youtube:truncated_url'
1606 IE_DESC = False # Do not list
975d35db 1607 _VALID_URL = r'''(?x)
c4808c60
PH
1608 (?:https?://)?[^/]+/watch\?(?:
1609 feature=[a-z_]+|
1610 annotation_id=annotation_[^&]+
1611 )?$|
975d35db
PH
1612 (?:https?://)?(?:www\.)?youtube\.com/attribution_link\?a=[^&]+$
1613 '''
15870e90 1614
c4808c60
PH
1615 _TESTS = [{
1616 'url': 'http://www.youtube.com/watch?annotation_id=annotation_3951667041',
1617 'only_matching': True,
dc2fc736
PH
1618 }, {
1619 'url': 'http://www.youtube.com/watch?',
1620 'only_matching': True,
c4808c60
PH
1621 }]
1622
15870e90
PH
1623 def _real_extract(self, url):
1624 raise ExtractorError(
78caa52a
PH
1625 'Did you forget to quote the URL? Remember that & is a meta '
1626 'character in most shells, so you want to put the URL in quotes, '
1627 'like youtube-dl '
1628 '"http://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
1629 ' or simply youtube-dl BaW_jenozKc .',
15870e90 1630 expected=True)