3 from __future__
import unicode_literals
14 from .common
import InfoExtractor
, SearchInfoExtractor
15 from ..jsinterp
import JSInterpreter
16 from ..swfinterp
import SWFInterpreter
17 from ..compat
import (
21 compat_urllib_parse_unquote
,
22 compat_urllib_parse_unquote_plus
,
23 compat_urllib_parse_urlencode
,
24 compat_urllib_parse_urlparse
,
57 class YoutubeBaseInfoExtractor(InfoExtractor
):
58 """Provide base functions for Youtube extractors"""
59 _LOGIN_URL
= 'https://accounts.google.com/ServiceLogin'
60 _TWOFACTOR_URL
= 'https://accounts.google.com/signin/challenge'
62 _LOOKUP_URL
= 'https://accounts.google.com/_/signin/sl/lookup'
63 _CHALLENGE_URL
= 'https://accounts.google.com/_/signin/sl/challenge'
64 _TFA_URL
= 'https://accounts.google.com/_/signin/challenge?hl=en&TL={0}'
67 r
'course|embed|playlist|watch|w|results|storefront|'
68 r
'shared|index|account|reporthistory|t/terms|about|upload|signin|logout|'
69 r
'feed/(watch_later|history|subscriptions|library|trending|recommended)')
71 _NETRC_MACHINE
= 'youtube'
72 # If True it will raise an error if no login info is provided
73 _LOGIN_REQUIRED
= False
75 _PLAYLIST_ID_RE
= r
'(?:(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}|RDMM)'
77 _YOUTUBE_CLIENT_HEADERS
= {
78 'x-youtube-client-name': '1',
79 'x-youtube-client-version': '1.20200609.04.02',
82 def _set_language(self
):
84 '.youtube.com', 'PREF', 'f1=50000000&f6=8&hl=en',
85 # YouTube sets the expire time to about two months
86 expire_time
=time
.time() + 2 * 30 * 24 * 3600)
88 def _ids_to_results(self
, ids
):
90 self
.url_result(vid_id
, 'Youtube', video_id
=vid_id
)
95 Attempt to log in to YouTube.
96 True is returned if successful or skipped.
97 False is returned if login failed.
99 If _LOGIN_REQUIRED is set and no authentication was provided, an error is raised.
101 username
, password
= self
._get
_login
_info
()
102 # No authentication to be performed
104 if self
._LOGIN
_REQUIRED
and self
._downloader
.params
.get('cookiefile') is None:
105 raise ExtractorError('No login info available, needed for using %s.' % self
.IE_NAME
, expected
=True)
106 if self
._downloader
.params
.get('cookiefile') and False: # TODO remove 'and False' later - too many people using outdated cookies and open issues, remind them.
107 self
.to_screen('[Cookies] Reminder - Make sure to always use up to date cookies!')
110 login_page
= self
._download
_webpage
(
111 self
._LOGIN
_URL
, None,
112 note
='Downloading login page',
113 errnote
='unable to fetch login page', fatal
=False)
114 if login_page
is False:
117 login_form
= self
._hidden
_inputs
(login_page
)
119 def req(url
, f_req
, note
, errnote
):
120 data
= login_form
.copy()
123 'checkConnection': 'youtube',
124 'checkedDomains': 'youtube',
126 'deviceinfo': '[null,null,null,[],null,"US",null,null,[],"GlifWebSignIn",null,[null,null,[]]]',
127 'f.req': json
.dumps(f_req
),
128 'flowName': 'GlifWebSignIn',
129 'flowEntry': 'ServiceLogin',
130 # TODO: reverse actual botguard identifier generation algo
131 'bgRequest': '["identifier",""]',
133 return self
._download
_json
(
134 url
, None, note
=note
, errnote
=errnote
,
135 transform_source
=lambda s
: re
.sub(r
'^[^[]*', '', s
),
137 data
=urlencode_postdata(data
), headers
={
138 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8',
139 'Google-Accounts-XSRF': 1,
143 self
._downloader
.report_warning(message
)
147 None, [], None, 'US', None, None, 2, False, True,
151 'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn',
153 1, [None, None, []], None, None, None, True
158 lookup_results
= req(
159 self
._LOOKUP
_URL
, lookup_req
,
160 'Looking up account info', 'Unable to look up account info')
162 if lookup_results
is False:
165 user_hash
= try_get(lookup_results
, lambda x
: x
[0][2], compat_str
)
167 warn('Unable to extract user hash')
172 None, 1, None, [1, None, None, None, [password
, None, True]],
174 None, None, [2, 1, None, 1, 'https://accounts.google.com/ServiceLogin?passive=true&continue=https%3A%2F%2Fwww.youtube.com%2Fsignin%3Fnext%3D%252F%26action_handle_signin%3Dtrue%26hl%3Den%26app%3Ddesktop%26feature%3Dsign_in_button&hl=en&service=youtube&uilel=3&requestPath=%2FServiceLogin&Page=PasswordSeparationSignIn', None, [], 4],
175 1, [None, None, []], None, None, None, True
178 challenge_results
= req(
179 self
._CHALLENGE
_URL
, challenge_req
,
180 'Logging in', 'Unable to log in')
182 if challenge_results
is False:
185 login_res
= try_get(challenge_results
, lambda x
: x
[0][5], list)
187 login_msg
= try_get(login_res
, lambda x
: x
[5], compat_str
)
189 'Unable to login: %s' % 'Invalid password'
190 if login_msg
== 'INCORRECT_ANSWER_ENTERED' else login_msg
)
193 res
= try_get(challenge_results
, lambda x
: x
[0][-1], list)
195 warn('Unable to extract result entry')
198 login_challenge
= try_get(res
, lambda x
: x
[0][0], list)
200 challenge_str
= try_get(login_challenge
, lambda x
: x
[2], compat_str
)
201 if challenge_str
== 'TWO_STEP_VERIFICATION':
202 # SEND_SUCCESS - TFA code has been successfully sent to phone
203 # QUOTA_EXCEEDED - reached the limit of TFA codes
204 status
= try_get(login_challenge
, lambda x
: x
[5], compat_str
)
205 if status
== 'QUOTA_EXCEEDED':
206 warn('Exceeded the limit of TFA codes, try later')
209 tl
= try_get(challenge_results
, lambda x
: x
[1][2], compat_str
)
211 warn('Unable to extract TL')
214 tfa_code
= self
._get
_tfa
_info
('2-step verification code')
218 'Two-factor authentication required. Provide it either interactively or with --twofactor <code>'
219 '(Note that only TOTP (Google Authenticator App) codes work at this time.)')
222 tfa_code
= remove_start(tfa_code
, 'G-')
225 user_hash
, None, 2, None,
227 9, None, None, None, None, None, None, None,
228 [None, tfa_code
, True, 2]
232 self
._TFA
_URL
.format(tl
), tfa_req
,
233 'Submitting TFA code', 'Unable to submit TFA code')
235 if tfa_results
is False:
238 tfa_res
= try_get(tfa_results
, lambda x
: x
[0][5], list)
240 tfa_msg
= try_get(tfa_res
, lambda x
: x
[5], compat_str
)
242 'Unable to finish TFA: %s' % 'Invalid TFA code'
243 if tfa_msg
== 'INCORRECT_ANSWER_ENTERED' else tfa_msg
)
246 check_cookie_url
= try_get(
247 tfa_results
, lambda x
: x
[0][-1][2], compat_str
)
250 'LOGIN_CHALLENGE': "This device isn't recognized. For your security, Google wants to make sure it's really you.",
251 'USERNAME_RECOVERY': 'Please provide additional information to aid in the recovery process.',
252 'REAUTH': "There is something unusual about your activity. For your security, Google wants to make sure it's really you.",
254 challenge
= CHALLENGES
.get(
256 '%s returned error %s.' % (self
.IE_NAME
, challenge_str
))
257 warn('%s\nGo to https://accounts.google.com/, login and solve a challenge.' % challenge
)
260 check_cookie_url
= try_get(res
, lambda x
: x
[2], compat_str
)
262 if not check_cookie_url
:
263 warn('Unable to extract CheckCookie URL')
266 check_cookie_results
= self
._download
_webpage
(
267 check_cookie_url
, None, 'Checking cookie', fatal
=False)
269 if check_cookie_results
is False:
272 if 'https://myaccount.google.com/' not in check_cookie_results
:
273 warn('Unable to log in')
278 def _download_webpage_handle(self
, *args
, **kwargs
):
279 query
= kwargs
.get('query', {}).copy()
280 kwargs
['query'] = query
281 return super(YoutubeBaseInfoExtractor
, self
)._download
_webpage
_handle
(
282 *args
, **compat_kwargs(kwargs
))
284 def _get_yt_initial_data(self
, video_id
, webpage
):
285 config
= self
._search
_regex
(
286 (r
'window\["ytInitialData"\]\s*=\s*(.*?)(?<=});',
287 r
'var\s+ytInitialData\s*=\s*(.*?)(?<=});'),
288 webpage
, 'ytInitialData', default
=None)
290 return self
._parse
_json
(
291 uppercase_escape(config
), video_id
, fatal
=False)
293 def _real_initialize(self
):
294 if self
._downloader
is None:
297 if not self
._login
():
300 _DEFAULT_API_DATA
= {
304 'clientVersion': '2.20201021.03.00',
309 _YT_INITIAL_DATA_RE
= r
'(?:window\s*\[\s*["\']ytInitialData
["\']\s*\]|ytInitialData)\s*=\s*({.+?})\s*;'
311 def _call_api(self, ep, query, video_id):
312 data = self._DEFAULT_API_DATA.copy()
315 response = self._download_json(
316 'https://www.youtube.com/youtubei/v1/%s' % ep, video_id=video_id,
317 note='Downloading API JSON', errnote='Unable to download API page',
318 data=json.dumps(data).encode('utf8'),
319 headers={'content-type': 'application/json'},
320 query={'key': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8'})
324 def _extract_yt_initial_data(self, video_id, webpage):
325 return self._parse_json(
327 (r'%s\s*\n' % self._YT_INITIAL_DATA_RE,
328 self._YT_INITIAL_DATA_RE), webpage, 'yt initial data'),
332 class YoutubeIE(YoutubeBaseInfoExtractor):
333 IE_DESC = 'YouTube.com'
334 _VALID_URL = r"""(?x)^
336 (?:https?://|//) # http(s):// or protocol-independent URL
337 (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie|kids)?\.com/|
338 (?:www\.)?deturl\.com/www\.youtube\.com/|
339 (?:www\.)?pwnyoutube\.com/|
340 (?:www\.)?hooktube\.com/|
341 (?:www\.)?yourepeat\.com/|
342 tube\.majestyc\.net/|
343 # Invidious instances taken from https://github.com/omarroth/invidious/wiki/Invidious-Instances
344 (?:(?:www|dev)\.)?invidio\.us/|
345 (?:(?:www|no)\.)?invidiou\.sh/|
346 (?:(?:www|fi|de)\.)?invidious\.snopyta\.org/|
347 (?:www\.)?invidious\.kabi\.tk/|
348 (?:www\.)?invidious\.13ad\.de/|
349 (?:www\.)?invidious\.mastodon\.host/|
350 (?:www\.)?invidious\.nixnet\.xyz/|
351 (?:www\.)?invidious\.drycat\.fr/|
352 (?:www\.)?tube\.poal\.co/|
353 (?:www\.)?vid\.wxzm\.sx/|
354 (?:www\.)?yewtu\.be/|
355 (?:www\.)?yt\.elukerio\.org/|
356 (?:www\.)?yt\.lelux\.fi/|
357 (?:www\.)?invidious\.ggc-project\.de/|
358 (?:www\.)?yt\.maisputain\.ovh/|
359 (?:www\.)?invidious\.13ad\.de/|
360 (?:www\.)?invidious\.toot\.koeln/|
361 (?:www\.)?invidious\.fdn\.fr/|
362 (?:www\.)?watch\.nettohikari\.com/|
363 (?:www\.)?kgg2m7yk5aybusll\.onion/|
364 (?:www\.)?qklhadlycap4cnod\.onion/|
365 (?:www\.)?axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid\.onion/|
366 (?:www\.)?c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid\.onion/|
367 (?:www\.)?fz253lmuao3strwbfbmx46yu7acac2jz27iwtorgmbqlkurlclmancad\.onion/|
368 (?:www\.)?invidious\.l4qlywnpwqsluw65ts7md3khrivpirse744un3x7mlskqauz5pyuzgqd\.onion/|
369 (?:www\.)?owxfohz4kjyv25fvlqilyxast7inivgiktls3th44jhk3ej3i7ya\.b32\.i2p/|
370 (?:www\.)?4l2dgddgsrkf2ous66i6seeyi6etzfgrue332grh2n7madpwopotugyd\.onion/|
371 youtube\.googleapis\.com/) # the various hostnames, with wildcard subdomains
372 (?:.*?\#/)? # handle anchor (#/) redirect urls
373 (?: # the various things that can precede the ID:
374 (?:(?:v|embed|e)/(?!videoseries)) # v/ or embed/ or e/
375 |(?: # or the v= param in all its forms
376 (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
377 (?:\?|\#!?) # the params delimiter ? or # or #!
378 (?:.*?[&;])?? # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&v=V36LpHqtcDY)
383 youtu\.be| # just youtu.be/xxxx
384 vid\.plus| # or vid.plus/xxxx
385 zwearz\.com/watch| # or zwearz.com/watch/xxxx
387 |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
389 )? # all until now is optional -> you can pass the naked ID
390 (?P<id>[0-9A-Za-z_-]{11}) # here is it! the YouTube video ID
393 %(playlist_id)s| # combined list/video URLs are handled by the playlist IE
394 WL # WL are handled by the watch later IE
397 (?(1).+)? # if we found the ID, everything can follow
398 $""" % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
399 _NEXT_URL_RE = r'[\?&]next_url=([^&]+)'
401 r'/(?P<id>[a-zA-Z0-9_-]{8,})/player_ias\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?/base\.(?P<ext>[a-z]+)$',
402 r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.(?P<ext>[a-z]+)$',
405 '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
406 '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
407 '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
408 '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
409 '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
410 '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
411 '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
412 '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
413 # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
414 '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
415 '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
416 '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
417 '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
418 '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
419 '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
420 '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
421 '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
422 '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
426 '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
427 '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
428 '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
429 '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
430 '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
431 '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
432 '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
434 # Apple HTTP Live Streaming
435 '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
436 '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
437 '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
438 '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
439 '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
440 '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
441 '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
442 '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
445 '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
446 '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
447 '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
448 '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
449 '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
450 '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
451 '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
452 '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
453 '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
454 '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
455 '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
456 '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
459 '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
460 '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
461 '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
462 '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
463 '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
464 '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
465 '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
468 '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
469 '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
470 '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
471 '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
472 '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
473 '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
474 '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
475 '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
476 '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
477 '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
478 '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
479 '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
480 '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
481 '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
482 '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
483 # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
484 '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
485 '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
486 '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
487 '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
488 '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
489 '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
492 '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
493 '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
495 # Dash webm audio with opus inside
496 '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
497 '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
498 '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
501 '_rtmp': {'protocol': 'rtmp'},
503 # av01 video only formats sometimes served with "unknown
" codecs
504 '394': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
505 '395': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
506 '396': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
507 '397': {'acodec': 'none', 'vcodec': 'av01.0.05M.08'},
509 _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
516 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
520 'title': 'youtube-dl test video "\'/\\ä↭𝕐
',
521 'uploader
': 'Philipp Hagemeister
',
522 'uploader_id
': 'phihag
',
523 'uploader_url
': r're
:https?
://(?
:www\
.)?youtube\
.com
/user
/phihag
',
524 'channel_id
': 'UCLqxVugv74EIW3VWh2NOa3Q
',
525 'channel_url
': r're
:https?
://(?
:www\
.)?youtube\
.com
/channel
/UCLqxVugv74EIW3VWh2NOa3Q
',
526 'upload_date
': '20121002',
527 '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 .',
528 'categories': ['Science & Technology'],
529 'tags': ['youtube-dl'],
533 'dislike_count': int,
539 'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
540 'note': 'Embed-only video (#1746)',
544 'upload_date': '20120608',
545 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
546 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
547 'uploader': 'SET India',
548 'uploader_id': 'setindia',
549 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/setindia',
554 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=yZIXLfi8CZQ',
555 'note': 'Use the first video ID in the URL',
559 'title': 'youtube-dl test video "\'/\\ä↭𝕐
',
560 'uploader
': 'Philipp Hagemeister
',
561 'uploader_id
': 'phihag
',
562 'uploader_url
': r're
:https?
://(?
:www\
.)?youtube\
.com
/user
/phihag
',
563 'upload_date
': '20121002',
564 '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 .',
565 'categories': ['Science & Technology'],
566 'tags': ['youtube-dl'],
570 'dislike_count': int,
573 'skip_download': True,
577 'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
578 'note': '256k DASH audio (format 141) via DASH manifest',
582 'upload_date': '20121002',
583 'uploader_id': '8KVIDEO',
584 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
586 'uploader': '8KVIDEO',
587 'title': 'UHDTV TEST 8K VIDEO.mp4'
590 'youtube_include_dash_manifest': True,
593 'skip': 'format 141 not served anymore',
595 # DASH manifest with encrypted signature
597 'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
601 'title': 'Afrojack, Spree Wilson - The Spark (Official Music Video) ft. Spree Wilson',
602 'description': 'md5:8f5e2b82460520b619ccac1f509d43bf',
604 'uploader': 'AfrojackVEVO',
605 'uploader_id': 'AfrojackVEVO',
606 'upload_date': '20131011',
609 'youtube_include_dash_manifest': True,
610 'format': '141/bestaudio[ext=m4a]',
615 'url': 'https://www.youtube.com/watch?v=T4XJQO3qol8',
620 'upload_date': '20100909',
621 'uploader': 'Amazing Atheist',
622 'uploader_id': 'TheAmazingAtheist',
623 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/TheAmazingAtheist',
624 'title': 'Burning Everyone\'s Koran',
625 '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',
628 # Normal age-gate video (embed allowed)
630 'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
634 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
635 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
637 'uploader': 'The Witcher',
638 'uploader_id': 'WitcherGame',
639 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame',
640 'upload_date': '20140605',
644 # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421)
645 # YouTube Red ad is not captured for creator
647 'url': '__2ABJjxzNo',
652 'upload_date': '20100430',
653 'uploader_id': 'deadmau5',
654 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/deadmau5',
655 'creator': 'Dada Life, deadmau5',
656 'description': 'md5:12c56784b8032162bb936a5f76d55360',
657 'uploader': 'deadmau5',
658 'title': 'Deadmau5 - Some Chords (HD)',
659 'alt_title': 'This Machine Kills Some Chords',
661 'expected_warnings': [
662 'DASH manifest missing',
665 # Olympics (https://github.com/ytdl-org/youtube-dl/issues/4431)
667 'url': 'lqQg6PlCWgI',
672 'upload_date': '20150827',
673 'uploader_id': 'olympic',
674 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic',
675 'description': 'HO09 - Women - GER-AUS - Hockey - 31 July 2012 - London 2012 Olympic Games',
676 'uploader': 'Olympic',
677 'title': 'Hockey - Women - GER-AUS - London 2012 Olympic Games',
680 'skip_download': 'requires avconv',
685 'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
689 'stretched_ratio': 16 / 9.,
691 'upload_date': '20110310',
692 'uploader_id': 'AllenMeow',
693 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/AllenMeow',
694 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
696 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
699 # url_encoded_fmt_stream_map is empty string
701 'url': 'qEJwOuvDf7I',
705 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
707 'upload_date': '20150404',
708 'uploader_id': 'spbelect',
709 'uploader': 'Наблюдатели Петербурга',
712 'skip_download': 'requires avconv',
714 'skip': 'This live event has ended.',
716 # Extraction from multiple DASH manifests (https://github.com/ytdl-org/youtube-dl/pull/6097)
718 'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
722 'title': 'md5:7b81415841e02ecd4313668cde88737a',
723 'description': 'md5:116377fd2963b81ec4ce64b542173306',
725 'upload_date': '20150625',
726 'uploader_id': 'dorappi2000',
727 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/dorappi2000',
728 'uploader': 'dorappi2000',
729 'formats': 'mincount:31',
731 'skip': 'not actual anymore',
733 # DASH manifest with segment_list
735 'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
736 'md5': '8ce563a1d667b599d21064e982ab9e31',
740 'upload_date': '20150501', # According to '<meta itemprop="datePublished
"', but in other places it's 20150510
741 'uploader': 'Airtek',
742 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
743 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
744 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
747 'youtube_include_dash_manifest': True,
748 'format': '135', # bestvideo
750 'skip': 'This live event has ended.',
753 # Multifeed videos (multiple cameras), URL is for Main Camera
754 'url': 'https://www.youtube.com/watch?v=jqWvoWXjCVs',
757 'title': 'teamPGP: Rocket League Noob Stream',
758 'description': 'md5:dc7872fb300e143831327f1bae3af010',
764 'title': 'teamPGP: Rocket League Noob Stream (Main Camera)',
765 'description': 'md5:dc7872fb300e143831327f1bae3af010',
767 'upload_date': '20150721',
768 'uploader': 'Beer Games Beer',
769 'uploader_id': 'beergamesbeer',
770 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
771 'license': 'Standard YouTube License',
777 'title': 'teamPGP: Rocket League Noob Stream (kreestuh)',
778 'description': 'md5:dc7872fb300e143831327f1bae3af010',
780 'upload_date': '20150721',
781 'uploader': 'Beer Games Beer',
782 'uploader_id': 'beergamesbeer',
783 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
784 'license': 'Standard YouTube License',
790 'title': 'teamPGP: Rocket League Noob Stream (grizzle)',
791 'description': 'md5:dc7872fb300e143831327f1bae3af010',
793 'upload_date': '20150721',
794 'uploader': 'Beer Games Beer',
795 'uploader_id': 'beergamesbeer',
796 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
797 'license': 'Standard YouTube License',
803 'title': 'teamPGP: Rocket League Noob Stream (zim)',
804 'description': 'md5:dc7872fb300e143831327f1bae3af010',
806 'upload_date': '20150721',
807 'uploader': 'Beer Games Beer',
808 'uploader_id': 'beergamesbeer',
809 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/beergamesbeer',
810 'license': 'Standard YouTube License',
814 'skip_download': True,
816 'skip': 'This video is not available.',
819 # Multifeed video with comma in title (see https://github.com/ytdl-org/youtube-dl/issues/8536)
820 'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
823 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
826 'skip': 'Not multifeed anymore',
829 'url': 'https://vid.plus/FlRa-iH7PGw',
830 'only_matching': True,
833 'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
834 'only_matching': True,
837 # Title with JS-like syntax "};" (see https://github.com/ytdl-org/youtube-dl/issues/7468)
838 # Also tests cut-off URL expansion in video description (see
839 # https://github.com/ytdl-org/youtube-dl/issues/1892,
840 # https://github.com/ytdl-org/youtube-dl/issues/8164)
841 'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
845 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
846 'alt_title': 'Dark Walk - Position Music',
847 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
849 'upload_date': '20151119',
850 'uploader_id': 'IronSoulElf',
851 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IronSoulElf',
852 'uploader': 'IronSoulElf',
853 'creator': 'Todd Haberman, Daniel Law Heath and Aaron Kaplan',
854 'track': 'Dark Walk - Position Music',
855 'artist': 'Todd Haberman, Daniel Law Heath and Aaron Kaplan',
856 'album': 'Position Music - Production Music Vol. 143 - Dark Walk',
859 'skip_download': True,
863 # Tags with '};' (see https://github.com/ytdl-org/youtube-dl/issues/7468)
864 'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
865 'only_matching': True,
868 # Video with yt:stretch=17:0
869 'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
873 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
874 'description': 'md5:ee18a25c350637c8faff806845bddee9',
875 'upload_date': '20151107',
876 'uploader_id': 'UCCr7TALkRbo3EtFzETQF1LA',
877 'uploader': 'CH GAMER DROID',
880 'skip_download': True,
882 'skip': 'This video does not exist.',
885 # Video licensed under Creative Commons
886 'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
890 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
891 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
893 'upload_date': '20150127',
894 'uploader_id': 'BerkmanCenter',
895 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/BerkmanCenter',
896 'uploader': 'The Berkman Klein Center for Internet & Society',
897 'license': 'Creative Commons Attribution license (reuse allowed)',
900 'skip_download': True,
904 # Channel-like uploader_url
905 'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
909 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
910 'description': 'md5:dda0d780d5a6e120758d1711d062a867',
912 'upload_date': '20151119',
913 'uploader': 'Bernie Sanders',
914 'uploader_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
915 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
916 'license': 'Creative Commons Attribution license (reuse allowed)',
919 'skip_download': True,
923 'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;v=V36LpHqtcDY',
924 'only_matching': True,
927 # YouTube Red paid video (https://github.com/ytdl-org/youtube-dl/issues/10059)
928 'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
929 'only_matching': True,
932 # Rental video preview
933 'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
937 'title': 'Piku - Trailer',
938 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
939 'upload_date': '20150811',
940 'uploader': 'FlixMatrix',
941 'uploader_id': 'FlixMatrixKaravan',
942 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/FlixMatrixKaravan',
943 'license': 'Standard YouTube License',
946 'skip_download': True,
948 'skip': 'This video is not available.',
951 # YouTube Red video with episode data
952 'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
956 'title': 'Isolation - Mind Field (Ep 1)',
957 'description': 'md5:46a29be4ceffa65b92d277b93f463c0f',
959 'upload_date': '20170118',
960 'uploader': 'Vsauce',
961 'uploader_id': 'Vsauce',
962 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Vsauce',
963 'series': 'Mind Field',
968 'skip_download': True,
970 'expected_warnings': [
971 'Skipping DASH manifest',
975 # The following content has been identified by the YouTube community
976 # as inappropriate or offensive to some audiences.
977 'url': 'https://www.youtube.com/watch?v=6SJNVb0GnPI',
981 'title': 'Race Differences in Intelligence',
982 'description': 'md5:5d161533167390427a1f8ee89a1fc6f1',
984 'upload_date': '20140124',
985 'uploader': 'New Century Foundation',
986 'uploader_id': 'UCEJYpZGqgUob0zVVEaLhvVg',
987 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCEJYpZGqgUob0zVVEaLhvVg',
990 'skip_download': True,
995 'url': '1t24XAntNCY',
996 'only_matching': True,
999 # geo restricted to JP
1000 'url': 'sJL6WA-aGkQ',
1001 'only_matching': True,
1004 'url': 'https://invidio.us/watch?v=BaW_jenozKc',
1005 'only_matching': True,
1009 'url': 'https://www.youtube.com/watch?v=s7_qI6_mIXc',
1010 'only_matching': True,
1013 # Video with unsupported adaptive stream type formats
1014 'url': 'https://www.youtube.com/watch?v=Z4Vy8R84T1U',
1016 'id': 'Z4Vy8R84T1U',
1018 'title': 'saman SMAN 53 Jakarta(Sancety) opening COFFEE4th at SMAN 53 Jakarta',
1019 'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
1021 'upload_date': '20130923',
1022 'uploader': 'Amelia Putri Harwita',
1023 'uploader_id': 'UCpOxM49HJxmC1qCalXyB3_Q',
1024 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCpOxM49HJxmC1qCalXyB3_Q',
1025 'formats': 'maxcount:10',
1028 'skip_download': True,
1029 'youtube_include_dash_manifest': False,
1031 'skip': 'not actual anymore',
1034 # Youtube Music Auto-generated description
1035 'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
1037 'id': 'MgNrAu2pzNs',
1039 'title': 'Voyeur Girl',
1040 'description': 'md5:7ae382a65843d6df2685993e90a8628f',
1041 'upload_date': '20190312',
1042 'uploader': 'Stephen - Topic',
1043 'uploader_id': 'UC-pWHpBjdGG69N9mM2auIAA',
1044 'artist': 'Stephen',
1045 'track': 'Voyeur Girl',
1046 'album': 'it\'s too much love to know my dear',
1047 'release_date': '20190313',
1048 'release_year': 2019,
1051 'skip_download': True,
1055 'url': 'https://www.youtubekids.com/watch?v=3b8nCWDgZ6Q',
1056 'only_matching': True,
1059 # invalid -> valid video id redirection
1060 'url': 'DJztXj2GPfl',
1062 'id': 'DJztXj2GPfk',
1064 'title': 'Panjabi MC - Mundian To Bach Ke (The Dictator Soundtrack)',
1065 'description': 'md5:bf577a41da97918e94fa9798d9228825',
1066 'upload_date': '20090125',
1067 'uploader': 'Prochorowka',
1068 'uploader_id': 'Prochorowka',
1069 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Prochorowka',
1070 'artist': 'Panjabi MC',
1071 'track': 'Beware of the Boys (Mundian to Bach Ke) - Motivo Hi-Lectro Remix',
1072 'album': 'Beware of the Boys (Mundian To Bach Ke)',
1075 'skip_download': True,
1079 # empty description results in an empty string
1080 'url': 'https://www.youtube.com/watch?v=x41yOUIvK2k',
1082 'id': 'x41yOUIvK2k',
1084 'title': 'IMG 3456',
1086 'upload_date': '20170613',
1087 'uploader_id': 'ElevageOrVert',
1088 'uploader': 'ElevageOrVert',
1091 'skip_download': True,
1095 # with '};' inside yt initial data (see https://github.com/ytdl-org/youtube-dl/issues/27093)
1096 'url': 'https://www.youtube.com/watch?v=CHqg6qOn4no',
1098 'id': 'CHqg6qOn4no',
1100 'title': 'Part 77 Sort a list of simple types in c#',
1101 'description': 'md5:b8746fa52e10cdbf47997903f13b20dc',
1102 'upload_date': '20130831',
1103 'uploader_id': 'kudvenkat',
1104 'uploader': 'kudvenkat',
1107 'skip_download': True,
1112 def __init__(self, *args, **kwargs):
1113 super(YoutubeIE, self).__init__(*args, **kwargs)
1114 self._player_cache = {}
1116 def report_video_info_webpage_download(self, video_id):
1117 """Report attempt to download video info webpage."""
1118 self.to_screen('%s: Downloading video info webpage' % video_id)
1120 def report_information_extraction(self, video_id):
1121 """Report attempt to extract video information."""
1122 self.to_screen('%s: Extracting video information' % video_id)
1124 def report_unavailable_format(self, video_id, format):
1125 """Report extracted video URL."""
1126 self.to_screen('%s: Format %s not available' % (video_id, format))
1128 def report_rtmp_download(self):
1129 """Indicate the download will use the RTMP protocol."""
1130 self.to_screen('RTMP download detected')
1132 def _signature_cache_id(self, example_sig):
1133 """ Return a string representation of a signature """
1134 return '.'.join(compat_str(len(part)) for part in example_sig.split('.'))
1137 def _extract_player_info(cls, player_url):
1138 for player_re in cls._PLAYER_INFO_RE:
1139 id_m = re.search(player_re, player_url)
1143 raise ExtractorError('Cannot identify player %r' % player_url)
1144 return id_m.group('ext'), id_m.group('id')
1146 def _extract_signature_function(self, video_id, player_url, example_sig):
1147 player_type, player_id = self._extract_player_info(player_url)
1149 # Read from filesystem cache
1150 func_id = '%s_%s_%s' % (
1151 player_type, player_id, self._signature_cache_id(example_sig))
1152 assert os.path.basename(func_id) == func_id
1154 cache_spec = self._downloader.cache.load('youtube-sigfuncs', func_id)
1155 if cache_spec is not None:
1156 return lambda s: ''.join(s[i] for i in cache_spec)
1159 'Downloading player %s' % player_url
1160 if self._downloader.params.get('verbose') else
1161 'Downloading %s player %s' % (player_type, player_id)
1163 if player_type == 'js':
1164 code = self._download_webpage(
1165 player_url, video_id,
1167 errnote='Download of %s failed' % player_url)
1168 res = self._parse_sig_js(code)
1169 elif player_type == 'swf':
1170 urlh = self._request_webpage(
1171 player_url, video_id,
1173 errnote='Download of %s failed' % player_url)
1175 res = self._parse_sig_swf(code)
1177 assert False, 'Invalid player type %r' % player_type
1179 test_string = ''.join(map(compat_chr, range(len(example_sig))))
1180 cache_res = res(test_string)
1181 cache_spec = [ord(c) for c in cache_res]
1183 self._downloader.cache.store('youtube-sigfuncs', func_id, cache_spec)
1186 def _print_sig_code(self, func, example_sig):
1187 def gen_sig_code(idxs):
1188 def _genslice(start, end, step):
1189 starts = '' if start == 0 else str(start)
1190 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
1191 steps = '' if step == 1 else (':%d' % step)
1192 return 's[%s%s%s]' % (starts, ends, steps)
1195 # Quelch pyflakes warnings - start will be set when step is set
1196 start = '(Never used)'
1197 for i, prev in zip(idxs[1:], idxs[:-1]):
1198 if step is not None:
1199 if i - prev == step:
1201 yield _genslice(start, prev, step)
1204 if i - prev in [-1, 1]:
1209 yield 's[%d]' % prev
1213 yield _genslice(start, i, step)
1215 test_string = ''.join(map(compat_chr, range(len(example_sig))))
1216 cache_res = func(test_string)
1217 cache_spec = [ord(c) for c in cache_res]
1218 expr_code = ' + '.join(gen_sig_code(cache_spec))
1219 signature_id_tuple = '(%s)' % (
1220 ', '.join(compat_str(len(p)) for p in example_sig.split('.')))
1221 code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
1222 ' return %s\n') % (signature_id_tuple, expr_code)
1223 self.to_screen('Extracted signature function:\n' + code)
1225 def _parse_sig_js(self, jscode):
1226 funcname = self._search_regex(
1227 (r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1228 r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
1229 r'(?:\b|[^a-zA-Z0-9$])(?P<sig>[a-zA-Z0-9$]{2})\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
1230 r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
1232 r'(["\'])signature\
1\s
*,\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1233 r'\
.sig\|\|
(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1234 r'yt\
.akamaized\
.net
/\
)\s
*\|\|\s
*.*?\s
*[cs
]\s
*&&\s
*[adf
]\
.set\
([^
,]+\s
*,\s
*(?
:encodeURIComponent\s
*\
()?\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1235 r'\b[cs
]\s
*&&\s
*[adf
]\
.set\
([^
,]+\s
*,\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1236 r'\b[a
-zA
-Z0
-9]+\s
*&&\s
*[a
-zA
-Z0
-9]+\
.set\
([^
,]+\s
*,\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1237 r'\bc\s
*&&\s
*a\
.set\
([^
,]+\s
*,\s
*\
([^
)]*\
)\s
*\
(\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1238 r'\bc\s
*&&\s
*[a
-zA
-Z0
-9]+\
.set\
([^
,]+\s
*,\s
*\
([^
)]*\
)\s
*\
(\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
(',
1239 r'\bc\s
*&&\s
*[a
-zA
-Z0
-9]+\
.set\
([^
,]+\s
*,\s
*\
([^
)]*\
)\s
*\
(\s
*(?P
<sig
>[a
-zA
-Z0
-9$
]+)\
('),
1240 jscode, 'Initial JS player signature function name
', group='sig
')
1242 jsi = JSInterpreter(jscode)
1243 initial_function = jsi.extract_function(funcname)
1244 return lambda s: initial_function([s])
1246 def _parse_sig_swf(self, file_contents):
1247 swfi = SWFInterpreter(file_contents)
1248 TARGET_CLASSNAME = 'SignatureDecipher
'
1249 searched_class = swfi.extract_class(TARGET_CLASSNAME)
1250 initial_function = swfi.extract_function(searched_class, 'decipher
')
1251 return lambda s: initial_function([s])
1253 def _decrypt_signature(self, s, video_id, player_url, age_gate=False):
1254 """Turn the encrypted s field into a working signature"""
1256 if player_url is None:
1257 raise ExtractorError('Cannot decrypt signature without player_url
')
1259 if player_url.startswith('//'):
1260 player_url = 'https
:' + player_url
1261 elif not re.match(r'https?
://', player_url):
1262 player_url = compat_urlparse.urljoin(
1263 'https
://www
.youtube
.com
', player_url)
1265 player_id = (player_url, self._signature_cache_id(s))
1266 if player_id not in self._player_cache:
1267 func = self._extract_signature_function(
1268 video_id, player_url, s
1270 self._player_cache[player_id] = func
1271 func = self._player_cache[player_id]
1272 if self._downloader.params.get('youtube_print_sig_code
'):
1273 self._print_sig_code(func, s)
1275 except Exception as e:
1276 tb = traceback.format_exc()
1277 raise ExtractorError(
1278 'Signature extraction failed
: ' + tb, cause=e)
1280 def _get_subtitles(self, video_id, webpage, has_live_chat_replay):
1282 subs_doc = self._download_xml(
1283 'https
://video
.google
.com
/timedtext?hl
=en
&type=list&v
=%s' % video_id,
1284 video_id, note=False)
1285 except ExtractorError as err:
1286 self._downloader.report_warning('unable to download video subtitles
: %s' % error_to_compat_str(err))
1290 for track in subs_doc.findall('track
'):
1291 lang = track.attrib['lang_code
']
1292 if lang in sub_lang_list:
1295 for ext in self._SUBTITLE_FORMATS:
1296 params = compat_urllib_parse_urlencode({
1300 'name
': track.attrib['name
'].encode('utf
-8'),
1302 sub_formats.append({
1303 'url
': 'https
://www
.youtube
.com
/api
/timedtext?
' + params,
1306 sub_lang_list[lang] = sub_formats
1307 if has_live_chat_replay:
1308 sub_lang_list['live_chat
'] = [
1310 'video_id
': video_id,
1312 'protocol
': 'youtube_live_chat_replay
',
1315 if not sub_lang_list:
1316 self._downloader.report_warning('video doesn
\'t have subtitles
')
1318 return sub_lang_list
1320 def _get_ytplayer_config(self, video_id, webpage):
1322 # User data may contain arbitrary character sequences that may affect
1323 # JSON extraction with regex, e.g. when '};' is contained the second
1324 # regex won't capture the whole JSON
. Yet working around by trying more
1325 # concrete regex first keeping in mind proper quoted string handling
1326 # to be implemented in future that will replace this workaround (see
1327 # https://github.com/ytdl-org/youtube-dl/issues/7468,
1328 # https://github.com/ytdl-org/youtube-dl/pull/7599)
1329 r
';ytplayer\.config\s*=\s*({.+?});ytplayer',
1330 r
';ytplayer\.config\s*=\s*({.+?});',
1331 r
'ytInitialPlayerResponse\s*=\s*({.+?});var meta' # Needed???
1333 config
= self
._search
_regex
(
1334 patterns
, webpage
, 'ytplayer.config', default
=None)
1336 return self
._parse
_json
(
1337 uppercase_escape(config
), video_id
, fatal
=False)
1339 def _get_music_metadata_from_yt_initial(self
, yt_initial
):
1346 contents
= try_get(yt_initial
, lambda x
: x
['contents']['twoColumnWatchNextResults']['results']['results']['contents'])
1347 if type(contents
) is list:
1348 for content
in contents
:
1350 if type(content
) is not dict:
1352 videoSecondaryInfoRenderer
= try_get(content
, lambda x
: x
['videoSecondaryInfoRenderer'])
1353 if type(videoSecondaryInfoRenderer
) is not dict:
1355 rows
= try_get(videoSecondaryInfoRenderer
, lambda x
: x
['metadataRowContainer']['metadataRowContainerRenderer']['rows'])
1356 if type(rows
) is not list:
1359 metadataRowRenderer
= try_get(row
, lambda x
: x
['metadataRowRenderer'])
1360 if type(metadataRowRenderer
) is not dict:
1362 key
= try_get(metadataRowRenderer
, lambda x
: x
['title']['simpleText'])
1363 value
= try_get(metadataRowRenderer
, lambda x
: x
['contents'][0]['simpleText']) or \
1364 try_get(metadataRowRenderer
, lambda x
: x
['contents'][0]['runs'][0]['text'])
1365 if type(key
) is not str or type(value
) is not str:
1368 if key_map
[key
] in music_track
:
1369 # we've started on a new track
1370 music_metadata
.append(music_track
)
1372 music_track
[key_map
[key
]] = value
1373 if len(music_track
.keys()):
1374 music_metadata
.append(music_track
)
1375 return music_metadata
1377 def _get_automatic_captions(self
, video_id
, webpage
):
1378 """We need the webpage for getting the captions url, pass it as an
1379 argument to speed up the process."""
1380 self
.to_screen('%s: Looking for automatic captions' % video_id
)
1381 player_config
= self
._get
_ytplayer
_config
(video_id
, webpage
)
1382 err_msg
= 'Couldn\'t find automatic captions for %s' % video_id
1383 if not player_config
:
1384 self
._downloader
.report_warning(err_msg
)
1387 args
= player_config
['args']
1388 caption_url
= args
.get('ttsurl')
1390 timestamp
= args
['timestamp']
1391 # We get the available subtitles
1392 list_params
= compat_urllib_parse_urlencode({
1397 list_url
= caption_url
+ '&' + list_params
1398 caption_list
= self
._download
_xml
(list_url
, video_id
)
1399 original_lang_node
= caption_list
.find('track')
1400 if original_lang_node
is None:
1401 self
._downloader
.report_warning('Video doesn\'t have automatic captions')
1403 original_lang
= original_lang_node
.attrib
['lang_code']
1404 caption_kind
= original_lang_node
.attrib
.get('kind', '')
1407 for lang_node
in caption_list
.findall('target'):
1408 sub_lang
= lang_node
.attrib
['lang_code']
1410 for ext
in self
._SUBTITLE
_FORMATS
:
1411 params
= compat_urllib_parse_urlencode({
1412 'lang': original_lang
,
1416 'kind': caption_kind
,
1418 sub_formats
.append({
1419 'url': caption_url
+ '&' + params
,
1422 sub_lang_list
[sub_lang
] = sub_formats
1423 return sub_lang_list
1425 def make_captions(sub_url
, sub_langs
):
1426 parsed_sub_url
= compat_urllib_parse_urlparse(sub_url
)
1427 caption_qs
= compat_parse_qs(parsed_sub_url
.query
)
1429 for sub_lang
in sub_langs
:
1431 for ext
in self
._SUBTITLE
_FORMATS
:
1433 'tlang': [sub_lang
],
1436 sub_url
= compat_urlparse
.urlunparse(parsed_sub_url
._replace
(
1437 query
=compat_urllib_parse_urlencode(caption_qs
, True)))
1438 sub_formats
.append({
1442 captions
[sub_lang
] = sub_formats
1445 # New captions format as of 22.06.2017
1446 player_response
= args
.get('player_response')
1447 if player_response
and isinstance(player_response
, compat_str
):
1448 player_response
= self
._parse
_json
(
1449 player_response
, video_id
, fatal
=False)
1451 renderer
= player_response
['captions']['playerCaptionsTracklistRenderer']
1452 base_url
= renderer
['captionTracks'][0]['baseUrl']
1454 for lang
in renderer
['translationLanguages']:
1455 lang_code
= lang
.get('languageCode')
1457 sub_lang_list
.append(lang_code
)
1458 return make_captions(base_url
, sub_lang_list
)
1460 # Some videos don't provide ttsurl but rather caption_tracks and
1461 # caption_translation_languages (e.g. 20LmZk1hakA)
1462 # Does not used anymore as of 22.06.2017
1463 caption_tracks
= args
['caption_tracks']
1464 caption_translation_languages
= args
['caption_translation_languages']
1465 caption_url
= compat_parse_qs(caption_tracks
.split(',')[0])['u'][0]
1467 for lang
in caption_translation_languages
.split(','):
1468 lang_qs
= compat_parse_qs(compat_urllib_parse_unquote_plus(lang
))
1469 sub_lang
= lang_qs
.get('lc', [None])[0]
1471 sub_lang_list
.append(sub_lang
)
1472 return make_captions(caption_url
, sub_lang_list
)
1473 # An extractor error can be raise by the download process if there are
1474 # no automatic captions but there are subtitles
1475 except (KeyError, IndexError, ExtractorError
):
1476 self
._downloader
.report_warning(err_msg
)
1479 def _mark_watched(self
, video_id
, video_info
, player_response
):
1480 playback_url
= url_or_none(try_get(
1482 lambda x
: x
['playbackTracking']['videostatsPlaybackUrl']['baseUrl']) or try_get(
1483 video_info
, lambda x
: x
['videostats_playback_base_url'][0]))
1484 if not playback_url
:
1486 parsed_playback_url
= compat_urlparse
.urlparse(playback_url
)
1487 qs
= compat_urlparse
.parse_qs(parsed_playback_url
.query
)
1489 # cpn generation algorithm is reverse engineered from base.js.
1490 # In fact it works even with dummy cpn.
1491 CPN_ALPHABET
= 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
1492 cpn
= ''.join((CPN_ALPHABET
[random
.randint(0, 256) & 63] for _
in range(0, 16)))
1498 playback_url
= compat_urlparse
.urlunparse(
1499 parsed_playback_url
._replace
(query
=compat_urllib_parse_urlencode(qs
, True)))
1501 self
._download
_webpage
(
1502 playback_url
, video_id
, 'Marking watched',
1503 'Unable to mark watched', fatal
=False)
1506 def _extract_urls(webpage
):
1507 # Embedded YouTube player
1509 unescapeHTML(mobj
.group('url'))
1510 for mobj
in re
.finditer(r
'''(?x)
1520 (?P
<url
>(?
:https?
:)?
//(?
:www\
.)?
youtube(?
:-nocookie
)?\
.com
/
1521 (?
:embed|v|p
)/[0-9A
-Za
-z_
-]{11}
.*?
)
1524 # lazyYT YouTube embed
1525 entries.extend(list(map(
1527 re.findall(r'class="lazyYT" data-youtube-id="([^"]+)"', webpage))))
1529 # Wordpress "YouTube Video Importer" plugin
1530 matches = re.findall(r'''(?x
)<div
[^
>]+
1531 class=(?P
<q1
>[\'"])[^\'"]*\byvii
_single
_video
_player
\b[^
\'"]*(?P=q1)[^>]+
1532 data-video_id=(?P<q2>[\'"])([^
\'"]+)(?P=q2)''', webpage)
1533 entries.extend(m[-1] for m in matches)
1538 def _extract_url(webpage):
1539 urls = YoutubeIE._extract_urls(webpage)
1540 return urls[0] if urls else None
1543 def extract_id(cls, url):
1544 mobj = re.match(cls._VALID_URL, url, re.VERBOSE)
1546 raise ExtractorError('Invalid URL: %s' % url)
1547 video_id = mobj.group(2)
1550 def _extract_chapters_from_json(self, webpage, video_id, duration):
1553 data = self._extract_yt_initial_data(video_id, webpage)
1554 if not data or not isinstance(data, dict):
1556 chapters_list = try_get(
1558 lambda x: x['playerOverlays']
1559 ['playerOverlayRenderer']
1560 ['decoratedPlayerBarRenderer']
1561 ['decoratedPlayerBarRenderer']
1563 ['chapteredPlayerBarRenderer']
1566 if not chapters_list:
1569 def chapter_time(chapter):
1570 return float_or_none(
1573 lambda x: x['chapterRenderer']['timeRangeStartMillis'],
1577 for next_num, chapter in enumerate(chapters_list, start=1):
1578 start_time = chapter_time(chapter)
1579 if start_time is None:
1581 end_time = (chapter_time(chapters_list[next_num])
1582 if next_num < len(chapters_list) else duration)
1583 if end_time is None:
1586 chapter, lambda x: x['chapterRenderer']['title']['simpleText'],
1589 'start_time': start_time,
1590 'end_time': end_time,
1596 def _extract_chapters_from_description(description, duration):
1599 chapter_lines = re.findall(
1600 r'(?:^|<br\s*/>)([^<]*<a[^>]+onclick=["\']yt\
.www\
.watch\
.player\
.seekTo
[^
>]+>(\d{1,2}
:\d{1,2}
(?
::\d{1,2}
)?
)</a
>[^
>]*)(?
=$|
<br\s
*/>)',
1602 if not chapter_lines:
1605 for next_num, (chapter_line, time_point) in enumerate(
1606 chapter_lines, start=1):
1607 start_time = parse_duration(time_point)
1608 if start_time is None:
1610 if start_time > duration:
1612 end_time = (duration if next_num == len(chapter_lines)
1613 else parse_duration(chapter_lines[next_num][1]))
1614 if end_time is None:
1616 if end_time > duration:
1618 if start_time > end_time:
1620 chapter_title = re.sub(
1621 r'<a
[^
>]+>[^
<]+</a
>', '', chapter_line).strip(' \t-')
1622 chapter_title = re.sub(r'\s
+', ' ', chapter_title)
1624 'start_time
': start_time,
1625 'end_time
': end_time,
1626 'title
': chapter_title,
1630 def _extract_chapters(self, webpage, description, video_id, duration):
1631 return (self._extract_chapters_from_json(webpage, video_id, duration)
1632 or self._extract_chapters_from_description(description, duration))
1634 def _real_extract(self, url):
1635 url, smuggled_data = unsmuggle_url(url, {})
1638 'http
' if self._downloader.params.get('prefer_insecure
', False)
1643 parsed_url = compat_urllib_parse_urlparse(url)
1644 for component in [parsed_url.fragment, parsed_url.query]:
1645 query = compat_parse_qs(component)
1646 if start_time is None and 't
' in query:
1647 start_time = parse_duration(query['t
'][0])
1648 if start_time is None and 'start
' in query:
1649 start_time = parse_duration(query['start
'][0])
1650 if end_time is None and 'end
' in query:
1651 end_time = parse_duration(query['end
'][0])
1653 # Extract original video URL from URL with redirection, like age verification, using next_url parameter
1654 mobj = re.search(self._NEXT_URL_RE, url)
1656 url = proto + '://www
.youtube
.com
/' + compat_urllib_parse_unquote(mobj.group(1)).lstrip('/')
1657 video_id = self.extract_id(url)
1660 url = proto + '://www
.youtube
.com
/watch?v
=%s&gl
=US
&hl
=en
&has_verified
=1&bpctr
=9999999999' % video_id
1661 video_webpage, urlh = self._download_webpage_handle(url, video_id)
1663 qs = compat_parse_qs(compat_urllib_parse_urlparse(urlh.geturl()).query)
1664 video_id = qs.get('v
', [None])[0] or video_id
1666 # Attempt to extract SWF player URL
1667 mobj = re.search(r'swfConfig
.*?
"(https?:\\/\\/.*?watch.*?-.*?\.swf)"', video_webpage)
1668 if mobj is not None:
1669 player_url = re.sub(r'\\(.)', r'\
1', mobj.group(1))
1675 def add_dash_mpd(video_info):
1676 dash_mpd = video_info.get('dashmpd
')
1677 if dash_mpd and dash_mpd[0] not in dash_mpds:
1678 dash_mpds.append(dash_mpd[0])
1680 def add_dash_mpd_pr(pl_response):
1681 dash_mpd = url_or_none(try_get(
1682 pl_response, lambda x: x['streamingData
']['dashManifestUrl
'],
1684 if dash_mpd and dash_mpd not in dash_mpds:
1685 dash_mpds.append(dash_mpd)
1690 def extract_view_count(v_info):
1691 return int_or_none(try_get(v_info, lambda x: x['view_count
'][0]))
1693 def extract_player_response(player_response, video_id):
1694 pl_response = str_or_none(player_response)
1697 pl_response = self._parse_json(pl_response, video_id, fatal=False)
1698 if isinstance(pl_response, dict):
1699 add_dash_mpd_pr(pl_response)
1702 def extract_embedded_config(embed_webpage, video_id):
1703 embedded_config = self._search_regex(
1704 r'setConfig\
(({.*}
)\
);',
1705 embed_webpage, 'ytInitialData
', default=None)
1707 return embedded_config
1709 player_response = {}
1713 embed_webpage = None
1714 if (self._og_search_property('restrictions
:age
', video_webpage, default=None) == '18+'
1715 or re.search(r'player
-age
-gate
-content
">', video_webpage) is not None):
1716 cookie_keys = self._get_cookies('https://www.youtube.com').keys()
1718 # We simulate the access to the video from www.youtube.com/v/{video_id}
1719 # this can be viewed without login into Youtube
1720 url = proto + '://www.youtube.com/embed/%s' % video_id
1721 embed_webpage = self._download_webpage(url, video_id, 'Downloading embed webpage')
1722 ext = extract_embedded_config(embed_webpage, video_id)
1723 # playabilityStatus = re.search(r'{\\\"status\\\":\\\"(?P<playabilityStatus>[^\"]+)\\\"', ext)
1724 playable_in_embed = re.search(r'{\\\"playableInEmbed\\\":(?P<playableinEmbed>[^\,]+)', ext)
1725 if not playable_in_embed:
1726 self.to_screen('Could not determine whether playabale in embed for video %s' % video_id)
1727 playable_in_embed = ''
1729 playable_in_embed = playable_in_embed.group('playableinEmbed')
1730 # check if video is only playable on youtube in other words not playable in embed - if so it requires auth (cookies)
1731 # if re.search(r'player-unavailable">', embed_webpage) is not None:
1732 if playable_in_embed == 'false
':
1734 # TODO apply this patch when Support for Python 2.6(!) and above drops
1735 if ({'VISITOR_INFO1_LIVE', 'HSID', 'SSID', 'SID'} <= cookie_keys
1736 or {'VISITOR_INFO1_LIVE', '__Secure-3PSID', 'LOGIN_INFO'} <= cookie_keys):
1738 if (set(('VISITOR_INFO1_LIVE
', 'HSID
', 'SSID
', 'SID
')) <= set(cookie_keys)
1739 or set(('VISITOR_INFO1_LIVE
', '__Secure
-3PSID
', 'LOGIN_INFO
')) <= set(cookie_keys)):
1741 # Try looking directly into the video webpage
1742 ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1744 args = ytplayer_config.get("args")
1745 if args is not None:
1746 if args.get('url_encoded_fmt_stream_map
') or args.get('hlsvp
'):
1747 # Convert to the same format returned by compat_parse_qs
1748 video_info = dict((k, [v]) for k, v in args.items())
1749 add_dash_mpd(video_info)
1750 # Rental video is not rented but preview is available (e.g.
1751 # https://www.youtube.com/watch?v=yYr8q0y5Jfg,
1752 # https://github.com/ytdl-org/youtube-dl/issues/10532)
1753 if not video_info and args.get('ypc_vid
'):
1754 return self.url_result(
1755 args['ypc_vid
'], YoutubeIE.ie_key(), video_id=args['ypc_vid
'])
1756 if args.get('livestream
') == '1' or args.get('live_playback
') == 1:
1758 if not player_response:
1759 player_response = extract_player_response(args.get('player_response
'), video_id)
1760 elif not player_response:
1761 player_response = ytplayer_config
1762 if not video_info or self._downloader.params.get('youtube_include_dash_manifest
', True):
1763 add_dash_mpd_pr(player_response)
1765 raise ExtractorError('Video
is age restricted
and only playable on Youtube
. Requires cookies
!', expected=True)
1767 data = compat_urllib_parse_urlencode({
1768 'video_id
': video_id,
1769 'eurl
': 'https
://youtube
.googleapis
.com
/v
/' + video_id,
1770 'sts
': self._search_regex(
1771 r'"sts"\s
*:\s
*(\d
+)', embed_webpage, 'sts
', default=''),
1773 video_info_url = proto + '://www
.youtube
.com
/get_video_info?
' + data
1775 video_info_webpage = self._download_webpage(
1776 video_info_url, video_id,
1777 note='Refetching age
-gated info webpage
',
1778 errnote='unable to download video info webpage
')
1779 except ExtractorError:
1780 video_info_webpage = None
1781 if video_info_webpage:
1782 video_info = compat_parse_qs(video_info_webpage)
1783 pl_response = video_info.get('player_response
', [None])[0]
1784 player_response = extract_player_response(pl_response, video_id)
1785 add_dash_mpd(video_info)
1786 view_count = extract_view_count(video_info)
1789 # Try looking directly into the video webpage
1790 ytplayer_config = self._get_ytplayer_config(video_id, video_webpage)
1792 args = ytplayer_config.get('args
', {})
1793 if args.get('url_encoded_fmt_stream_map
') or args.get('hlsvp
'):
1794 # Convert to the same format returned by compat_parse_qs
1795 video_info = dict((k, [v]) for k, v in args.items())
1796 add_dash_mpd(video_info)
1797 # Rental video is not rented but preview is available (e.g.
1798 # https://www.youtube.com/watch?v=yYr8q0y5Jfg,
1799 # https://github.com/ytdl-org/youtube-dl/issues/10532)
1800 if not video_info and args.get('ypc_vid
'):
1801 return self.url_result(
1802 args['ypc_vid
'], YoutubeIE.ie_key(), video_id=args['ypc_vid
'])
1803 if args.get('livestream
') == '1' or args.get('live_playback
') == 1:
1805 if not player_response:
1806 player_response = extract_player_response(args.get('player_response
'), video_id)
1807 if not video_info or self._downloader.params.get('youtube_include_dash_manifest
', True):
1808 add_dash_mpd_pr(player_response)
1810 if not video_info and not player_response:
1811 player_response = extract_player_response(
1813 r'ytInitialPlayerResponse\s
*=\s
*({.+?}
)\s
*;', video_webpage,
1814 'initial player response
', default='{}'),
1817 def extract_unavailable_message():
1819 for tag, kind in (('h1
', 'message
'), ('div
', 'submessage
')):
1820 msg = self._html_search_regex(
1821 r'(?s
)<{tag}
[^
>]+id=["\']unavailable-{kind}["\'][^
>]*>(.+?
)</{tag}
>'.format(tag=tag, kind=kind),
1822 video_webpage, 'unavailable
%s' % kind, default=None)
1824 messages.append(msg)
1826 return '\n'.join(messages)
1828 if not video_info and not player_response:
1829 unavailable_message = extract_unavailable_message()
1830 if not unavailable_message:
1831 unavailable_message = 'Unable to extract video data
'
1832 raise ExtractorError(
1833 'YouTube said
: %s' % unavailable_message, expected=True, video_id=video_id)
1835 if not isinstance(video_info, dict):
1838 video_details = try_get(
1839 player_response, lambda x: x['videoDetails
'], dict) or {}
1841 microformat = try_get(
1842 player_response, lambda x: x['microformat
']['playerMicroformatRenderer
'], dict) or {}
1844 video_title = video_info.get('title
', [None])[0] or video_details.get('title
')
1846 self._downloader.report_warning('Unable to extract video title
')
1849 description_original = video_description = get_element_by_id("eow-description", video_webpage)
1850 if video_description:
1853 redir_url = compat_urlparse.urljoin(url, m.group(1))
1854 parsed_redir_url = compat_urllib_parse_urlparse(redir_url)
1855 if re.search(r'^
(?
:www\
.)?
(?
:youtube(?
:-nocookie
)?\
.com|youtu\
.be
)$
', parsed_redir_url.netloc) and parsed_redir_url.path == '/redirect
':
1856 qs = compat_parse_qs(parsed_redir_url.query)
1862 description_original = video_description = re.sub(r'''(?x)
1864 (?:[a-zA-Z-]+="[^"]*"\s+)*?
1865 (?:title|href)="([^"]+)"\s+
1866 (?:[a-zA-Z-]+="[^"]*"\s+)*?
1870 ''', replace_url, video_description)
1871 video_description = clean_html(video_description)
1873 video_description = video_details.get('shortDescription
')
1874 if video_description is None:
1875 video_description = self._html_search_meta('description
', video_webpage)
1877 if not smuggled_data.get('force_singlefeed
', False):
1878 if not self._downloader.params.get('noplaylist
'):
1879 multifeed_metadata_list = try_get(
1881 lambda x: x['multicamera
']['playerLegacyMulticameraRenderer
']['metadataList
'],
1882 compat_str) or try_get(
1883 video_info, lambda x: x['multifeed_metadata_list
'][0], compat_str)
1884 if multifeed_metadata_list:
1887 for feed in multifeed_metadata_list.split(','):
1888 # Unquote should take place before split on comma (,) since textual
1889 # fields may contain comma as well (see
1890 # https://github.com/ytdl-org/youtube-dl/issues/8536)
1891 feed_data = compat_parse_qs(compat_urllib_parse_unquote_plus(feed))
1893 def feed_entry(name):
1894 return try_get(feed_data, lambda x: x[name][0], compat_str)
1896 feed_id = feed_entry('id')
1899 feed_title = feed_entry('title
')
1902 title += ' (%s)' % feed_title
1904 '_type
': 'url_transparent
',
1905 'ie_key
': 'Youtube
',
1907 '%s://www
.youtube
.com
/watch?v
=%s' % (proto, feed_data['id'][0]),
1908 {'force_singlefeed': True}),
1911 feed_ids.append(feed_id)
1913 'Downloading multifeed
video (%s) - add
--no
-playlist to just download video
%s'
1914 % (', '.join(feed_ids), video_id))
1915 return self.playlist_result(entries, video_id, video_title, video_description)
1917 self.to_screen('Downloading just video
%s because of
--no
-playlist
' % video_id)
1919 if view_count is None:
1920 view_count = extract_view_count(video_info)
1921 if view_count is None and video_details:
1922 view_count = int_or_none(video_details.get('viewCount
'))
1923 if view_count is None and microformat:
1924 view_count = int_or_none(microformat.get('viewCount
'))
1927 is_live = bool_or_none(video_details.get('isLive
'))
1929 has_live_chat_replay = False
1931 yt_initial_data = self._get_yt_initial_data(video_id, video_webpage)
1933 yt_initial_data['contents
']['twoColumnWatchNextResults
']['conversationBar
']['liveChatRenderer
']['continuations
'][0]['reloadContinuationData
']['continuation
']
1934 has_live_chat_replay = True
1935 except (KeyError, IndexError, TypeError):
1938 # Check for "rental" videos
1939 if 'ypc_video_rental_bar_text
' in video_info and 'author
' not in video_info:
1940 raise ExtractorError('"rental" videos
not supported
. See https
://github
.com
/ytdl
-org
/youtube
-dl
/issues
/359 for more information
.', expected=True)
1942 def _extract_filesize(media_url):
1943 return int_or_none(self._search_regex(
1944 r'\bclen
[=/](\d
+)', media_url, 'filesize
', default=None))
1946 streaming_formats = try_get(player_response, lambda x: x['streamingData
']['formats
'], list) or []
1947 streaming_formats.extend(try_get(player_response, lambda x: x['streamingData
']['adaptiveFormats
'], list) or [])
1949 if 'conn
' in video_info and video_info['conn
'][0].startswith('rtmp
'):
1950 self.report_rtmp_download()
1952 'format_id
': '_rtmp
',
1954 'url
': video_info['conn
'][0],
1955 'player_url
': player_url,
1957 elif not is_live and (streaming_formats or len(video_info.get('url_encoded_fmt_stream_map
', [''])[0]) >= 1 or len(video_info.get('adaptive_fmts
', [''])[0]) >= 1):
1958 encoded_url_map = video_info.get('url_encoded_fmt_stream_map
', [''])[0] + ',' + video_info.get('adaptive_fmts
', [''])[0]
1959 if 'rtmpe
%3Dyes
' in encoded_url_map:
1960 raise ExtractorError('rtmpe downloads are
not supported
, see https
://github
.com
/ytdl
-org
/youtube
-dl
/issues
/343 for more information
.', expected=True)
1963 fmt_list = video_info.get('fmt_list
', [''])[0]
1965 for fmt in fmt_list.split(','):
1966 spec = fmt.split('/')
1968 width_height = spec[1].split('x
')
1969 if len(width_height) == 2:
1970 formats_spec[spec[0]] = {
1971 'resolution
': spec[1],
1972 'width
': int_or_none(width_height[0]),
1973 'height
': int_or_none(width_height[1]),
1975 for fmt in streaming_formats:
1976 itag = str_or_none(fmt.get('itag
'))
1979 quality = fmt.get('quality
')
1980 quality_label = fmt.get('qualityLabel
') or quality
1981 formats_spec[itag] = {
1982 'asr
': int_or_none(fmt.get('audioSampleRate
')),
1983 'filesize
': int_or_none(fmt.get('contentLength
')),
1984 'format_note
': quality_label,
1985 'fps
': int_or_none(fmt.get('fps
')),
1986 'height
': int_or_none(fmt.get('height
')),
1987 # bitrate for itag 43 is always 2147483647
1988 'tbr
': float_or_none(fmt.get('averageBitrate
') or fmt.get('bitrate
'), 1000) if itag != '43' else None,
1989 'width
': int_or_none(fmt.get('width
')),
1992 for fmt in streaming_formats:
1993 if fmt.get('drmFamilies
') or fmt.get('drm_families
'):
1995 url = url_or_none(fmt.get('url
'))
1998 cipher = fmt.get('cipher
') or fmt.get('signatureCipher
')
2001 url_data = compat_parse_qs(cipher)
2002 url = url_or_none(try_get(url_data, lambda x: x['url
'][0], compat_str))
2007 url_data = compat_parse_qs(compat_urllib_parse_urlparse(url).query)
2009 stream_type = int_or_none(try_get(url_data, lambda x: x['stream_type
'][0]))
2010 # Unsupported FORMAT_STREAM_TYPE_OTF
2011 if stream_type == 3:
2014 format_id = fmt.get('itag
') or url_data['itag
'][0]
2017 format_id = compat_str(format_id)
2020 if 's
' in url_data or self._downloader.params.get('youtube_include_dash_manifest
', True):
2022 r'<script
[^
>]+\bsrc
=("[^"]+")[^>]+\bname=["\']player_ias
/base
',
2023 r'"jsUrl"\s
*:\s
*("[^"]+")',
2024 r'"assets
":.+?"js
":\s*("[^
"]+")')
2025 jsplayer_url_json = self._search_regex(
2027 embed_webpage if age_gate else video_webpage,
2028 'JS player
URL (1)', default=None)
2029 if not jsplayer_url_json and not age_gate:
2030 # We need the embed website after all
2031 if embed_webpage is None:
2032 embed_url = proto + '://www
.youtube
.com
/embed
/%s' % video_id
2033 embed_webpage = self._download_webpage(
2034 embed_url, video_id, 'Downloading embed webpage
')
2035 jsplayer_url_json = self._search_regex(
2036 ASSETS_RE, embed_webpage, 'JS player URL
')
2038 player_url = json.loads(jsplayer_url_json)
2039 if player_url is None:
2040 player_url_json = self._search_regex(
2041 r'ytplayer\
.config
.*?
"url"\s
*:\s
*("[^"]+")',
2042 video_webpage, 'age gate player URL')
2043 player_url = json.loads(player_url_json)
2045 if 'sig' in url_data:
2046 url += '&signature=' + url_data['sig'][0]
2047 elif 's' in url_data:
2048 encrypted_sig = url_data['s'][0]
2050 if self._downloader.params.get('verbose'):
2051 if player_url is None:
2052 player_desc = 'unknown'
2054 player_type, player_version = self._extract_player_info(player_url)
2055 player_desc = '%s player %s' % ('flash' if player_type == 'swf' else 'html5', player_version)
2056 parts_sizes = self._signature_cache_id(encrypted_sig)
2057 self.to_screen('{%s} signature length %s, %s' %
2058 (format_id, parts_sizes, player_desc))
2060 signature = self._decrypt_signature(
2061 encrypted_sig, video_id, player_url, age_gate)
2062 sp = try_get(url_data, lambda x: x['sp'][0], compat_str) or 'signature'
2063 url += '&%s=%s' % (sp, signature)
2064 if 'ratebypass' not in url:
2065 url += '&ratebypass=yes'
2068 'format_id': format_id,
2070 'player_url': player_url,
2072 if format_id in self._formats:
2073 dct.update(self._formats[format_id])
2074 if format_id in formats_spec:
2075 dct.update(formats_spec[format_id])
2077 # Some itags are not included in DASH manifest thus corresponding formats will
2078 # lack metadata (see https://github.com/ytdl-org/youtube-dl/pull/5993).
2079 # Trying to extract metadata from url_encoded_fmt_stream_map entry.
2080 mobj = re.search(r'^(?P<width>\d+)[xX](?P<height>\d+)$', url_data.get('size', [''])[0])
2081 width, height = (int(mobj.group('width')), int(mobj.group('height'))) if mobj else (None, None)
2084 width = int_or_none(fmt.get('width'))
2086 height = int_or_none(fmt.get('height'))
2088 filesize = int_or_none(url_data.get(
2089 'clen', [None])[0]) or _extract_filesize(url)
2091 quality = url_data.get('quality', [None])[0] or fmt.get('quality')
2092 quality_label = url_data.get('quality_label', [None])[0] or fmt.get('qualityLabel')
2094 tbr = (float_or_none(url_data.get('bitrate', [None])[0], 1000)
2095 or float_or_none(fmt.get('bitrate'), 1000)) if format_id != '43' else None
2096 fps = int_or_none(url_data.get('fps', [None])[0]) or int_or_none(fmt.get('fps'))
2099 'filesize': filesize,
2104 'format_note': quality_label or quality,
2106 for key, value in more_fields.items():
2109 type_ = url_data.get('type', [None])[0] or fmt.get('mimeType')
2111 type_split = type_.split(';')
2112 kind_ext = type_split[0].split('/')
2113 if len(kind_ext) == 2:
2115 dct['ext'] = mimetype2ext(type_split[0])
2116 if kind in ('audio', 'video'):
2118 for mobj in re.finditer(
2119 r'(?P<key>[a-zA-Z_-]+)=(?P<quote>["\']?
)(?P
<val
>.+?
)(?P
=quote
)(?
:;|$
)', type_):
2120 if mobj.group('key
') == 'codecs
':
2121 codecs = mobj.group('val
')
2124 dct.update(parse_codecs(codecs))
2125 if dct.get('acodec
') == 'none
' or dct.get('vcodec
') == 'none
':
2126 dct['downloader_options
'] = {
2127 # Youtube throttles chunks >~10M
2128 'http_chunk_size
': 10485760,
2133 url_or_none(try_get(
2135 lambda x: x['streamingData
']['hlsManifestUrl
'],
2137 or url_or_none(try_get(
2138 video_info, lambda x: x['hlsvp
'][0], compat_str)))
2141 m3u8_formats = self._extract_m3u8_formats(
2142 manifest_url, video_id, 'mp4
', fatal=False)
2143 for a_format in m3u8_formats:
2144 itag = self._search_regex(
2145 r'/itag
/(\d
+)/', a_format['url
'], 'itag
', default=None)
2147 a_format['format_id
'] = itag
2148 if itag in self._formats:
2149 dct = self._formats[itag].copy()
2150 dct.update(a_format)
2152 a_format['player_url
'] = player_url
2153 # Accept-Encoding header causes failures in live streams on Youtube and Youtube Gaming
2154 a_format.setdefault('http_headers
', {})['Youtubedl
-no
-compression
'] = 'True'
2155 if self._downloader.params.get('youtube_include_hls_manifest
', True):
2156 formats.append(a_format)
2158 error_message = extract_unavailable_message()
2159 if not error_message:
2160 reason_list = try_get(
2162 lambda x: x['playabilityStatus
']['errorScreen
']['playerErrorMessageRenderer
']['subreason
']['runs
'],
2164 for reason in reason_list:
2165 if not isinstance(reason, dict):
2167 reason_text = try_get(reason, lambda x: x['text
'], compat_str)
2169 if not error_message:
2171 error_message += reason_text
2173 error_message = clean_html(error_message)
2174 if not error_message:
2175 error_message = clean_html(try_get(
2176 player_response, lambda x: x['playabilityStatus
']['reason
'],
2178 if not error_message:
2179 error_message = clean_html(
2180 try_get(video_info, lambda x: x['reason
'][0], compat_str))
2182 raise ExtractorError(error_message, expected=True)
2183 raise ExtractorError('no conn
, hlsvp
, hlsManifestUrl
or url_encoded_fmt_stream_map information found
in video info
')
2186 video_uploader = try_get(
2187 video_info, lambda x: x['author
'][0],
2188 compat_str) or str_or_none(video_details.get('author
'))
2190 video_uploader = compat_urllib_parse_unquote_plus(video_uploader)
2192 self._downloader.report_warning('unable to extract uploader name
')
2195 video_uploader_id = None
2196 video_uploader_url = None
2198 r'<link itemprop
="url" href
="(?P<uploader_url>https?://www\.youtube\.com/(?:user|channel)/(?P<uploader_id>[^"]+))">',
2200 if mobj is not None:
2201 video_uploader_id = mobj.group('uploader_id')
2202 video_uploader_url = mobj.group('uploader_url')
2204 owner_profile_url = url_or_none(microformat.get('ownerProfileUrl'))
2205 if owner_profile_url:
2206 video_uploader_id = self._search_regex(
2207 r'(?:user|channel)/([^/]+)', owner_profile_url, 'uploader id',
2209 video_uploader_url = owner_profile_url
2212 str_or_none(video_details.get('channelId'))
2213 or self._html_search_meta(
2214 'channelId', video_webpage, 'channel id', default=None)
2215 or self._search_regex(
2216 r'data-channel-external-id=(["\'])(?P
<id>(?
:(?
!\
1).)+)\
1',
2217 video_webpage, 'channel
id', default=None, group='id'))
2218 channel_url = 'http
://www
.youtube
.com
/channel
/%s' % channel_id if channel_id else None
2221 thumbnails_list = try_get(
2222 video_details, lambda x: x['thumbnail
']['thumbnails
'], list) or []
2223 for t in thumbnails_list:
2224 if not isinstance(t, dict):
2226 thumbnail_url = url_or_none(t.get('url
'))
2227 if not thumbnail_url:
2230 'url
': thumbnail_url,
2231 'width
': int_or_none(t.get('width
')),
2232 'height
': int_or_none(t.get('height
')),
2236 video_thumbnail = None
2237 # We try first to get a high quality image:
2238 m_thumb = re.search(r'<span itemprop
="thumbnail".*?href
="(.*?)">',
2239 video_webpage, re.DOTALL)
2240 if m_thumb is not None:
2241 video_thumbnail = m_thumb.group(1)
2242 thumbnail_url = try_get(video_info, lambda x: x['thumbnail_url
'][0], compat_str)
2244 video_thumbnail = compat_urllib_parse_unquote_plus(thumbnail_url)
2246 thumbnails.append({'url': video_thumbnail})
2249 upload_date = self._html_search_meta(
2250 'datePublished
', video_webpage, 'upload date
', default=None)
2252 upload_date = self._search_regex(
2253 [r'(?s
)id="eow-date.*?>(.*?)</span>',
2254 r'(?:id="watch
-uploader
-info
".*?>.*?|["\']simpleText
["\']\s*:\s*["\'])(?
:Published|Uploaded|Streamed live|Started
) on (.+?
)[<"\']'],
2255 video_webpage, 'upload date', default=None)
2257 upload_date = microformat.get('publishDate') or microformat.get('uploadDate')
2258 upload_date = unified_strdate(upload_date)
2260 video_license = self._html_search_regex(
2261 r'<h4[^>]+class="title
"[^>]*>\s*License\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li',
2262 video_webpage, 'license', default=None)
2264 m_music = re.search(
2266 <h4[^>]+class="title
"[^>]*>\s*Music\s*</h4>\s*
2274 \bhref=["\']/red
[^
>]*>|
# drop possible
2275 >\s
*Listen ad
-free
with YouTube Red
# YouTube Red ad
2282 video_alt_title = remove_quotes(unescapeHTML(m_music.group('title')))
2283 video_creator = clean_html(m_music.group('creator'))
2285 video_alt_title = video_creator = None
2287 def extract_meta(field):
2288 return self._html_search_regex(
2289 r'<h4[^>]+class="title"[^>]*>\s*%s\s*</h4>\s*<ul[^>]*>\s*<li>(.+?)</li>\s*' % field,
2290 video_webpage, field, default=None)
2292 track = extract_meta('Song')
2293 artist = extract_meta('Artist')
2294 album = extract_meta('Album')
2296 # Youtube Music Auto-generated description
2297 release_date = release_year = None
2298 if video_description:
2299 mobj = re.search(r'(?s)Provided to YouTube by [^\n]+\n+(?P<track>[^·]+)·(?P<artist>[^\n]+)\n+(?P<album>[^\n]+)(?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?(?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?(.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?', video_description)
2302 track = mobj.group('track').strip()
2304 artist = mobj.group('clean_artist') or ', '.join(a.strip() for a in mobj.group('artist').split('·'))
2306 album = mobj.group('album'.strip())
2307 release_year = mobj.group('release_year')
2308 release_date = mobj.group('release_date')
2310 release_date = release_date.replace('-', '')
2311 if not release_year:
2312 release_year = int(release_date[:4])
2314 release_year = int(release_year)
2316 yt_initial = self._get_yt_initial_data(video_id, video_webpage)
2318 music_metadata = self._get_music_metadata_from_yt_initial(yt_initial)
2319 if len(music_metadata):
2320 album = music_metadata[0].get('album')
2321 artist = music_metadata[0].get('artist')
2322 track = music_metadata[0].get('track')
2324 m_episode = re.search(
2325 r'<div[^>]+id="watch7-headline"[^>]*>\s*<span[^>]*>.*?>(?P<series>[^<]+)</a></b>\s*S(?P<season>\d+)\s*•\s*E(?P<episode>\d+)</span>',
2328 series = unescapeHTML(m_episode.group('series'))
2329 season_number = int(m_episode.group('season'))
2330 episode_number = int(m_episode.group('episode'))
2332 series = season_number = episode_number = None
2334 m_cat_container = self._search_regex(
2335 r'(?s)<h4[^>]*>\s*Category\s*</h4>\s*<ul[^>]*>(.*?)</ul>',
2336 video_webpage, 'categories', default=None)
2339 category = self._html_search_regex(
2340 r'(?s)<a[^<]+>(.*?)</a>', m_cat_container, 'category',
2344 microformat, lambda x: x['category'], compat_str)
2345 video_categories = None if category is None else [category]
2348 unescapeHTML(m.group('content'))
2349 for m in re.finditer(self._meta_regex('og:video:tag'), video_webpage)]
2351 video_tags = try_get(video_details, lambda x: x['keywords'], list)
2353 def _extract_count(count_name):
2354 return str_to_int(self._search_regex(
2355 (r'-%s-button[^>]+><span[^>]+class="yt-uix-button-content"[^>]*>([\d,]+)</span>' % re.escape(count_name),
2356 r'["\']label["\']\s*:\s*["\']([\d,.]+)\s+%ss["\']' % re.escape(count_name)),
2357 video_webpage, count_name, default=None))
2359 like_count = _extract_count('like')
2360 dislike_count = _extract_count('dislike')
2362 if view_count is None:
2363 view_count = str_to_int(self._search_regex(
2364 r'<[^>]+class=["\']watch-view-count[^>]+>\s*([\d,\s]+)', video_webpage,
2365 'view count', default=None))
2368 float_or_none(video_details.get('averageRating'))
2369 or try_get(video_info, lambda x: float_or_none(x['avg_rating'][0])))
2372 video_subtitles = self.extract_subtitles(
2373 video_id, video_webpage, has_live_chat_replay)
2374 automatic_captions = self.extract_automatic_captions(video_id, video_webpage)
2376 video_duration = try_get(
2377 video_info, lambda x: int_or_none(x['length_seconds'][0]))
2378 if not video_duration:
2379 video_duration = int_or_none(video_details.get('lengthSeconds'))
2380 if not video_duration:
2381 video_duration = parse_duration(self._html_search_meta(
2382 'duration', video_webpage, 'video duration'))
2384 # Get Subscriber Count of channel
2385 subscriber_count = parse_count(self._search_regex(
2386 r'"text":"([\d\.]+\w?) subscribers"',
2393 video_annotations = None
2394 if self._downloader.params.get('writeannotations', False):
2395 xsrf_token = self._search_regex(
2396 r'([\'"])XSRF_TOKEN\1\s*:\s*([\'"])(?P<xsrf_token>[A-Za-z0-9+/=]+)\2',
2397 video_webpage, 'xsrf token', group='xsrf_token', fatal=False)
2398 invideo_url = try_get(
2399 player_response, lambda x: x['annotations'][0]['playerAnnotationsUrlsRenderer']['invideoUrl'], compat_str)
2400 if xsrf_token and invideo_url:
2401 xsrf_field_name = self._search_regex(
2402 r'([\'"])XSRF_FIELD_NAME\1\s*:\s*([\'"])(?P<xsrf_field_name>\w+)\2',
2403 video_webpage, 'xsrf field name',
2404 group='xsrf_field_name', default='session_token')
2405 video_annotations = self._download_webpage(
2406 self._proto_relative_url(invideo_url),
2407 video_id, note='Downloading annotations',
2408 errnote='Unable to download video annotations', fatal=False,
2409 data=urlencode_postdata({xsrf_field_name: xsrf_token}))
2411 chapters = self._extract_chapters(video_webpage, description_original, video_id, video_duration)
2413 # Look for the DASH manifest
2414 if self._downloader.params.get('youtube_include_dash_manifest', True):
2415 dash_mpd_fatal = True
2416 for mpd_url in dash_mpds:
2419 def decrypt_sig(mobj):
2421 dec_s = self._decrypt_signature(s, video_id, player_url, age_gate)
2422 return '/signature/%s' % dec_s
2424 mpd_url = re.sub(r'/s/([a-fA-F0-9\.]+)', decrypt_sig, mpd_url)
2426 for df in self._extract_mpd_formats(
2427 mpd_url, video_id, fatal=dash_mpd_fatal,
2428 formats_dict=self._formats):
2429 if not df.get('filesize'):
2430 df['filesize'] = _extract_filesize(df['url'])
2431 # Do not overwrite DASH format found in some previous DASH manifest
2432 if df['format_id'] not in dash_formats:
2433 dash_formats[df['format_id']] = df
2434 # Additional DASH manifests may end up in HTTP Error 403 therefore
2435 # allow them to fail without bug report message if we already have
2436 # some DASH manifest succeeded. This is temporary workaround to reduce
2437 # burst of bug reports until we figure out the reason and whether it
2438 # can be fixed at all.
2439 dash_mpd_fatal = False
2440 except (ExtractorError, KeyError) as e:
2441 self.report_warning(
2442 'Skipping DASH manifest: %r' % e, video_id)
2444 # Remove the formats we found through non-DASH, they
2445 # contain less info and it can be wrong, because we use
2446 # fixed values (for example the resolution). See
2447 # https://github.com/ytdl-org/youtube-dl/issues/5774 for an
2449 formats = [f for f in formats if f['format_id'] not in dash_formats.keys()]
2450 formats.extend(dash_formats.values())
2452 # Check for malformed aspect ratio
2453 stretched_m = re.search(
2454 r'<meta\s+property="og:video:tag".*?content="yt:stretch=(?P<w>[0-9]+):(?P<h>[0-9]+)">',
2457 w = float(stretched_m.group('w'))
2458 h = float(stretched_m.group('h'))
2459 # yt:stretch may hold invalid ratio data (e.g. for Q39EVAstoRM ratio is 17:0).
2460 # We will only process correct ratios.
2464 if f.get('vcodec') != 'none':
2465 f['stretched_ratio'] = ratio
2468 if 'reason' in video_info:
2469 if 'The uploader has not made this video available in your country.' in video_info['reason']:
2470 regions_allowed = self._html_search_meta(
2471 'regionsAllowed', video_webpage, default=None)
2472 countries = regions_allowed.split(',') if regions_allowed else None
2473 self.raise_geo_restricted(
2474 msg=video_info['reason'][0], countries=countries)
2475 reason = video_info['reason'][0]
2476 if 'Invalid parameters' in reason:
2477 unavailable_message = extract_unavailable_message()
2478 if unavailable_message:
2479 reason = unavailable_message
2480 raise ExtractorError(
2481 'YouTube said: %s' % reason,
2482 expected=True, video_id=video_id)
2483 if video_info.get('license_info') or try_get(player_response, lambda x: x['streamingData']['licenseInfos']):
2484 raise ExtractorError('This video is DRM protected.', expected=True)
2486 self._sort_formats(formats)
2488 self.mark_watched(video_id, video_info, player_response)
2492 'uploader': video_uploader,
2493 'uploader_id': video_uploader_id,
2494 'uploader_url': video_uploader_url,
2495 'channel_id': channel_id,
2496 'channel_url': channel_url,
2497 'upload_date': upload_date,
2498 'license': video_license,
2499 'creator': video_creator or artist,
2500 'title': video_title,
2501 'alt_title': video_alt_title or track,
2502 'thumbnails': thumbnails,
2503 'description': video_description,
2504 'categories': video_categories,
2506 'subtitles': video_subtitles,
2507 'automatic_captions': automatic_captions,
2508 'duration': video_duration,
2509 'age_limit': 18 if age_gate else 0,
2510 'annotations': video_annotations,
2511 'chapters': chapters,
2512 'webpage_url': proto + '://www.youtube.com/watch?v=%s' % video_id,
2513 'view_count': view_count,
2514 'like_count': like_count,
2515 'dislike_count': dislike_count,
2516 'average_rating': average_rating,
2519 'start_time': start_time,
2520 'end_time': end_time,
2522 'season_number': season_number,
2523 'episode_number': episode_number,
2527 'release_date': release_date,
2528 'release_year': release_year,
2529 'subscriber_count': subscriber_count,
2533 class YoutubeTabIE(YoutubeBaseInfoExtractor):
2534 IE_DESC = 'YouTube.com tab'
2535 _VALID_URL = (r'''(?x
)
2536 https?
://(?
:\w
+\
.)?
(?
:youtube(?
:kids
)?\
.com|invidio\
.us
)/(?
:
2537 (?
!(%s)([/#?]|$))|channel/|c/|user/|
2538 (?P
<not_channel
>playlist|watch
)/?
\?.*?
\blist
=)
2539 (?P
<id>[^
/?
#&]+)''') % YoutubeBaseInfoExtractor._RESERVED_NAMES
2540 IE_NAME
= 'youtube:tab'
2543 # playlists, multipage
2544 'url': 'https://www.youtube.com/c/ИгорьКлейнер/playlists?view=1&flow=grid',
2545 'playlist_mincount': 94,
2547 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
2548 'title': 'Игорь Клейнер - Playlists',
2549 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
2552 # playlists, multipage, different order
2553 'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
2554 'playlist_mincount': 94,
2556 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
2557 'title': 'Игорь Клейнер - Playlists',
2558 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
2561 # playlists, singlepage
2562 'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
2563 'playlist_mincount': 4,
2565 'id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
2566 'title': 'ThirstForScience - Playlists',
2567 'description': 'md5:609399d937ea957b0f53cbffb747a14c',
2570 'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
2571 'only_matching': True,
2573 # basic, single video playlist
2574 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2576 'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
2577 'uploader': 'Sergey M.',
2578 'id': 'PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2579 'title': 'youtube-dl public playlist',
2581 'playlist_count': 1,
2584 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
2586 'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
2587 'uploader': 'Sergey M.',
2588 'id': 'PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
2589 'title': 'youtube-dl empty playlist',
2591 'playlist_count': 0,
2594 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/featured',
2596 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2597 'title': 'lex will - Home',
2598 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2600 'playlist_mincount': 2,
2603 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos',
2605 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2606 'title': 'lex will - Videos',
2607 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2609 'playlist_mincount': 975,
2611 # Videos tab, sorted by popular
2612 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos?view=0&sort=p&flow=grid',
2614 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2615 'title': 'lex will - Videos',
2616 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2618 'playlist_mincount': 199,
2621 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/playlists',
2623 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2624 'title': 'lex will - Playlists',
2625 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2627 'playlist_mincount': 17,
2630 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/community',
2632 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2633 'title': 'lex will - Community',
2634 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2636 'playlist_mincount': 18,
2639 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/channels',
2641 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
2642 'title': 'lex will - Channels',
2643 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
2645 'playlist_mincount': 138,
2647 'url': 'https://invidio.us/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2648 'only_matching': True,
2650 'url': 'https://www.youtubekids.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2651 'only_matching': True,
2653 'url': 'https://music.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
2654 'only_matching': True,
2656 'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
2657 'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2659 'title': '29C3: Not my department',
2660 'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
2661 'uploader': 'Christiaan008',
2662 'uploader_id': 'UCEPzS1rYsrkqzSLNp76nrcg',
2664 'playlist_count': 96,
2666 'note': 'Large playlist',
2667 'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
2669 'title': 'Uploads from Cauchemar',
2670 'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
2671 'uploader': 'Cauchemar',
2672 'uploader_id': 'UCBABnxM4Ar9ten8Mdjj1j0Q',
2674 'playlist_mincount': 1123,
2676 # even larger playlist, 8832 videos
2677 'url': 'http://www.youtube.com/user/NASAgovVideo/videos',
2678 'only_matching': True,
2680 'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
2681 'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
2683 'title': 'Uploads from Interstellar Movie',
2684 'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
2685 'uploader': 'Interstellar Movie',
2686 'uploader_id': 'UCXw-G3eDE9trcvY2sBMM_aA',
2688 'playlist_mincount': 21,
2690 # https://github.com/ytdl-org/youtube-dl/issues/21844
2691 'url': 'https://www.youtube.com/playlist?list=PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
2693 'title': 'Data Analysis with Dr Mike Pound',
2694 'id': 'PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
2695 'uploader_id': 'UC9-y-6csu5WGm29I7JiwpnA',
2696 'uploader': 'Computerphile',
2698 'playlist_mincount': 11,
2700 'url': 'https://invidio.us/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
2701 'only_matching': True,
2703 # Playlist URL that does not actually serve a playlist
2704 'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
2706 'id': 'FqZTN594JQw',
2708 'title': "Smiley's People 01 detective, Adventure Series, Action",
2709 'uploader': 'STREEM',
2710 'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
2711 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
2712 'upload_date': '20150526',
2713 'license': 'Standard YouTube License',
2714 'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
2715 'categories': ['People & Blogs'],
2719 'dislike_count': int,
2722 'skip_download': True,
2724 'skip': 'This video is not available.',
2725 'add_ie': [YoutubeIE
.ie_key()],
2727 'url': 'https://www.youtubekids.com/watch?v=Agk7R8I8o5U&list=PUZ6jURNr1WQZCNHF0ao-c0g',
2728 'only_matching': True,
2730 'url': 'https://www.youtube.com/watch?v=MuAGGZNfUkU&list=RDMM',
2731 'only_matching': True,
2733 'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
2735 'id': '9Auq9mYxFEE',
2737 'title': 'Watch Sky News live',
2738 'uploader': 'Sky News',
2739 'uploader_id': 'skynews',
2740 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/skynews',
2741 'upload_date': '20191102',
2742 'description': 'md5:78de4e1c2359d0ea3ed829678e38b662',
2743 'categories': ['News & Politics'],
2746 'dislike_count': int,
2749 'skip_download': True,
2752 'url': 'https://www.youtube.com/user/TheYoungTurks/live',
2754 'id': 'a48o2S1cPoo',
2756 'title': 'The Young Turks - Live Main Show',
2757 'uploader': 'The Young Turks',
2758 'uploader_id': 'TheYoungTurks',
2759 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
2760 'upload_date': '20150715',
2761 'license': 'Standard YouTube License',
2762 'description': 'md5:438179573adcdff3c97ebb1ee632b891',
2763 'categories': ['News & Politics'],
2764 'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
2766 'dislike_count': int,
2769 'skip_download': True,
2771 'only_matching': True,
2773 'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
2774 'only_matching': True,
2776 'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
2777 'only_matching': True,
2781 # 'url': 'https://www.youtube.com/TheYoungTurks/live',
2782 # 'only_matching': True,
2786 def _extract_channel_id(self
, webpage
):
2787 channel_id
= self
._html
_search
_meta
(
2788 'channelId', webpage
, 'channel id', default
=None)
2791 channel_url
= self
._html
_search
_meta
(
2792 ('og:url', 'al:ios:url', 'al:android:url', 'al:web:url',
2793 'twitter:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad',
2794 'twitter:app:url:googleplay'), webpage
, 'channel url')
2795 return self
._search
_regex
(
2796 r
'https?://(?:www\.)?youtube\.com/channel/([^/?#&])+',
2797 channel_url
, 'channel id')
2800 def _extract_grid_item_renderer(item
):
2801 for item_kind
in ('Playlist', 'Video', 'Channel'):
2802 renderer
= item
.get('grid%sRenderer' % item_kind
)
2806 def _extract_video(self
, renderer
):
2807 video_id
= renderer
.get('videoId')
2810 (lambda x
: x
['title']['runs'][0]['text'],
2811 lambda x
: x
['title']['simpleText']), compat_str
)
2812 description
= try_get(
2813 renderer
, lambda x
: x
['descriptionSnippet']['runs'][0]['text'],
2815 duration
= parse_duration(try_get(
2816 renderer
, lambda x
: x
['lengthText']['simpleText'], compat_str
))
2817 view_count_text
= try_get(
2818 renderer
, lambda x
: x
['viewCountText']['simpleText'], compat_str
) or ''
2819 view_count
= str_to_int(self
._search
_regex
(
2820 r
'^([\d,]+)', re
.sub(r
'\s', '', view_count_text
),
2821 'view count', default
=None))
2823 renderer
, lambda x
: x
['ownerText']['runs'][0]['text'], compat_str
)
2825 '_type': 'url_transparent',
2826 'ie_key': YoutubeIE
.ie_key(),
2830 'description': description
,
2831 'duration': duration
,
2832 'view_count': view_count
,
2833 'uploader': uploader
,
2836 def _grid_entries(self
, grid_renderer
):
2837 for item
in grid_renderer
['items']:
2838 if not isinstance(item
, dict):
2840 renderer
= self
._extract
_grid
_item
_renderer
(item
)
2841 if not isinstance(renderer
, dict):
2844 renderer
, lambda x
: x
['title']['runs'][0]['text'], compat_str
)
2846 playlist_id
= renderer
.get('playlistId')
2848 yield self
.url_result(
2849 'https://www.youtube.com/playlist?list=%s' % playlist_id
,
2850 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
2853 video_id
= renderer
.get('videoId')
2855 yield self
._extract
_video
(renderer
)
2857 channel_id
= renderer
.get('channelId')
2860 renderer
, lambda x
: x
['title']['simpleText'], compat_str
)
2861 yield self
.url_result(
2862 'https://www.youtube.com/channel/%s' % channel_id
,
2863 ie
=YoutubeTabIE
.ie_key(), video_title
=title
)
2865 def _shelf_entries_trimmed(self
, shelf_renderer
):
2867 shelf_renderer
, lambda x
: x
['content']['horizontalListRenderer'], dict)
2870 # TODO: add support for nested playlists so each shelf is processed
2871 # as separate playlist
2872 # TODO: this includes only first N items
2873 for entry
in self
._grid
_entries
(renderer
):
2876 def _shelf_entries(self
, shelf_renderer
):
2878 shelf_renderer
, lambda x
: x
['endpoint']['commandMetadata']['webCommandMetadata']['url'],
2880 shelf_url
= urljoin('https://www.youtube.com', ep
)
2884 shelf_renderer
, lambda x
: x
['title']['runs'][0]['text'], compat_str
)
2885 yield self
.url_result(shelf_url
, video_title
=title
)
2887 def _playlist_entries(self
, video_list_renderer
):
2888 for content
in video_list_renderer
['contents']:
2889 if not isinstance(content
, dict):
2891 renderer
= content
.get('playlistVideoRenderer') or content
.get('playlistPanelVideoRenderer')
2892 if not isinstance(renderer
, dict):
2894 video_id
= renderer
.get('videoId')
2897 yield self
._extract
_video
(renderer
)
2899 def _itemSection_entries(self
, item_sect_renderer
):
2900 for content
in item_sect_renderer
['contents']:
2901 if not isinstance(content
, dict):
2903 renderer
= content
.get('videoRenderer', {})
2904 if not isinstance(renderer
, dict):
2906 video_id
= renderer
.get('videoId')
2909 yield self
._extract
_video
(renderer
)
2911 def _rich_entries(self
, rich_grid_renderer
):
2913 rich_grid_renderer
, lambda x
: x
['content']['videoRenderer'], dict)
2914 video_id
= renderer
.get('videoId')
2917 yield self
._extract
_video
(renderer
)
2919 def _video_entry(self
, video_renderer
):
2920 video_id
= video_renderer
.get('videoId')
2922 return self
._extract
_video
(video_renderer
)
2924 def _post_thread_entries(self
, post_thread_renderer
):
2925 post_renderer
= try_get(
2926 post_thread_renderer
, lambda x
: x
['post']['backstagePostRenderer'], dict)
2927 if not post_renderer
:
2930 video_renderer
= try_get(
2931 post_renderer
, lambda x
: x
['backstageAttachment']['videoRenderer'], dict)
2934 entry
= self
._video
_entry
(video_renderer
)
2937 # inline video links
2938 runs
= try_get(post_renderer
, lambda x
: x
['contentText']['runs'], list) or []
2940 if not isinstance(run
, dict):
2943 run
, lambda x
: x
['navigationEndpoint']['urlEndpoint']['url'], compat_str
)
2946 if not YoutubeIE
.suitable(ep_url
):
2948 ep_video_id
= YoutubeIE
._match
_id
(ep_url
)
2949 if video_id
== ep_video_id
:
2951 yield self
.url_result(ep_url
, ie
=YoutubeIE
.ie_key(), video_id
=video_id
)
2953 def _post_thread_continuation_entries(self
, post_thread_continuation
):
2954 contents
= post_thread_continuation
.get('contents')
2955 if not isinstance(contents
, list):
2957 for content
in contents
:
2958 renderer
= content
.get('backstagePostThreadRenderer')
2959 if not isinstance(renderer
, dict):
2961 for entry
in self
._post
_thread
_entries
(renderer
):
2965 def _extract_next_continuation_data(renderer
):
2966 next_continuation
= try_get(
2967 renderer
, lambda x
: x
['continuations'][0]['nextContinuationData'], dict)
2968 if not next_continuation
:
2970 continuation
= next_continuation
.get('continuation')
2971 if not continuation
:
2973 ctp
= next_continuation
.get('clickTrackingParams')
2975 'ctoken': continuation
,
2976 'continuation': continuation
,
2981 def _extract_continuation(cls
, renderer
):
2982 next_continuation
= cls
._extract
_next
_continuation
_data
(renderer
)
2983 if next_continuation
:
2984 return next_continuation
2985 contents
= renderer
.get('contents')
2986 if not isinstance(contents
, list):
2988 for content
in contents
:
2989 if not isinstance(content
, dict):
2991 continuation_ep
= try_get(
2992 content
, lambda x
: x
['continuationItemRenderer']['continuationEndpoint'],
2994 if not continuation_ep
:
2996 continuation
= try_get(
2997 continuation_ep
, lambda x
: x
['continuationCommand']['token'], compat_str
)
2998 if not continuation
:
3000 ctp
= continuation_ep
.get('clickTrackingParams')
3004 'ctoken': continuation
,
3005 'continuation': continuation
,
3009 def _entries(self
, tab
, identity_token
):
3011 def extract_entries(parent_renderer
):
3012 slr_contents
= try_get(parent_renderer
, lambda x
: x
['contents'], list) or []
3013 for slr_content
in slr_contents
:
3014 if not isinstance(slr_content
, dict):
3016 is_renderer
= try_get(slr_content
, lambda x
: x
['itemSectionRenderer'], dict)
3018 renderer
= slr_content
.get('richItemRenderer')
3020 for entry
in self
._rich
_entries
(renderer
):
3022 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
3024 isr_contents
= try_get(is_renderer
, lambda x
: x
['contents'], list) or []
3025 for isr_content
in isr_contents
:
3026 if not isinstance(isr_content
, dict):
3028 renderer
= isr_content
.get('playlistVideoListRenderer')
3030 for entry
in self
._playlist
_entries
(renderer
):
3032 continuation_list
[0] = self
._extract
_continuation
(renderer
)
3034 renderer
= isr_content
.get('gridRenderer')
3036 for entry
in self
._grid
_entries
(renderer
):
3038 continuation_list
[0] = self
._extract
_continuation
(renderer
)
3040 renderer
= isr_content
.get('shelfRenderer')
3042 for entry
in self
._shelf
_entries
(renderer
):
3044 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
3046 renderer
= isr_content
.get('backstagePostThreadRenderer')
3048 for entry
in self
._post
_thread
_entries
(renderer
):
3050 continuation_list
[0] = self
._extract
_continuation
(renderer
)
3052 renderer
= isr_content
.get('videoRenderer')
3054 entry
= self
._video
_entry
(renderer
)
3057 if not continuation_list
[0]:
3058 continuation_list
[0] = self
._extract
_continuation
(is_renderer
)
3059 if not continuation_list
[0]:
3060 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
3062 continuation_list
= [None] # Python 2 doesnot support nonlocal
3064 try_get(tab
, lambda x
: x
['sectionListRenderer'], dict)
3065 or try_get(tab
, lambda x
: x
['richGridRenderer'], dict) or {})
3067 for entry
in extract_entries(parent_renderer
):
3070 continuation
= continuation_list
[0]
3073 'x-youtube-client-name': '1',
3074 'x-youtube-client-version': '2.20201112.04.01',
3077 headers
['x-youtube-identity-token'] = identity_token
3079 for page_num
in itertools
.count(1):
3080 if not continuation
:
3082 if hasattr(self
, '_MAX_PAGES') and page_num
> self
._MAX
_PAGES
:
3084 browse
= self
._download
_json
(
3085 'https://www.youtube.com/browse_ajax', None,
3086 'Downloading page %d' % page_num
,
3087 headers
=headers
, query
=continuation
, fatal
=False)
3090 response
= try_get(browse
, lambda x
: x
[1]['response'], dict)
3094 continuation_contents
= try_get(
3095 response
, lambda x
: x
['continuationContents'], dict)
3096 if continuation_contents
:
3097 continuation_renderer
= continuation_contents
.get('playlistVideoListContinuation')
3098 if continuation_renderer
:
3099 for entry
in self
._playlist
_entries
(continuation_renderer
):
3101 continuation
= self
._extract
_continuation
(continuation_renderer
)
3103 continuation_renderer
= continuation_contents
.get('gridContinuation')
3104 if continuation_renderer
:
3105 for entry
in self
._grid
_entries
(continuation_renderer
):
3107 continuation
= self
._extract
_continuation
(continuation_renderer
)
3109 continuation_renderer
= continuation_contents
.get('itemSectionContinuation')
3110 if continuation_renderer
:
3111 for entry
in self
._post
_thread
_continuation
_entries
(continuation_renderer
):
3113 continuation
= self
._extract
_continuation
(continuation_renderer
)
3115 continuation_renderer
= continuation_contents
.get('sectionListContinuation')
3116 if continuation_renderer
:
3117 continuation_list
= [None]
3118 for entry
in extract_entries(continuation_renderer
):
3120 continuation
= continuation_list
[0]
3123 continuation_items
= try_get(
3124 response
, lambda x
: x
['onResponseReceivedActions'][0]['appendContinuationItemsAction']['continuationItems'], list)
3125 if continuation_items
:
3126 continuation_item
= continuation_items
[0]
3127 if not isinstance(continuation_item
, dict):
3129 renderer
= continuation_item
.get('playlistVideoRenderer')
3131 video_list_renderer
= {'contents': continuation_items}
3132 for entry
in self
._playlist
_entries
(video_list_renderer
):
3134 continuation
= self
._extract
_continuation
(video_list_renderer
)
3136 renderer
= continuation_item
.get('itemSectionRenderer')
3138 for entry
in self
._itemSection
_entries
(renderer
):
3140 continuation
= self
._extract
_continuation
({'contents': continuation_items}
)
3145 def _extract_selected_tab(tabs
):
3147 if try_get(tab
, lambda x
: x
['tabRenderer']['selected'], bool):
3148 return tab
['tabRenderer']
3150 raise ExtractorError('Unable to find selected tab')
3153 def _extract_uploader(data
):
3155 sidebar_renderer
= try_get(
3156 data
, lambda x
: x
['sidebar']['playlistSidebarRenderer']['items'], list)
3157 if sidebar_renderer
:
3158 for item
in sidebar_renderer
:
3159 if not isinstance(item
, dict):
3161 renderer
= item
.get('playlistSidebarSecondaryInfoRenderer')
3162 if not isinstance(renderer
, dict):
3165 renderer
, lambda x
: x
['videoOwner']['videoOwnerRenderer']['title']['runs'][0], dict)
3167 uploader
['uploader'] = owner
.get('text')
3168 uploader
['uploader_id'] = try_get(
3169 owner
, lambda x
: x
['navigationEndpoint']['browseEndpoint']['browseId'], compat_str
)
3170 uploader
['uploader_url'] = urljoin(
3171 'https://www.youtube.com/',
3172 try_get(owner
, lambda x
: x
['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], compat_str
))
3175 def _extract_from_tabs(self
, item_id
, webpage
, data
, tabs
, identity_token
):
3176 selected_tab
= self
._extract
_selected
_tab
(tabs
)
3178 data
, lambda x
: x
['metadata']['channelMetadataRenderer'], dict)
3181 channel_title
= renderer
.get('title') or item_id
3182 tab_title
= selected_tab
.get('title')
3183 title
= channel_title
or item_id
3185 title
+= ' - %s' % tab_title
3186 description
= renderer
.get('description')
3187 playlist_id
= renderer
.get('externalId')
3189 data
, lambda x
: x
['metadata']['playlistMetadataRenderer'], dict)
3191 title
= renderer
.get('title')
3193 playlist_id
= item_id
3194 if playlist_id
is None:
3196 playlist
= self
.playlist_result(
3197 self
._entries
(selected_tab
['content'], identity_token
),
3198 playlist_id
=playlist_id
, playlist_title
=title
,
3199 playlist_description
=description
)
3200 playlist
.update(self
._extract
_uploader
(data
))
3203 def _extract_from_playlist(self
, item_id
, data
, playlist
):
3204 title
= playlist
.get('title') or try_get(
3205 data
, lambda x
: x
['titleText']['simpleText'], compat_str
)
3206 playlist_id
= playlist
.get('playlistId') or item_id
3207 return self
.playlist_result(
3208 self
._playlist
_entries
(playlist
), playlist_id
=playlist_id
,
3209 playlist_title
=title
)
3211 def _real_extract(self
, url
):
3212 item_id
= self
._match
_id
(url
)
3213 url
= compat_urlparse
.urlunparse(
3214 compat_urlparse
.urlparse(url
)._replace
(netloc
='www.youtube.com'))
3215 # Handle both video/playlist URLs
3216 qs
= compat_urlparse
.parse_qs(compat_urlparse
.urlparse(url
).query
)
3217 video_id
= qs
.get('v', [None])[0]
3218 playlist_id
= qs
.get('list', [None])[0]
3219 if video_id
and playlist_id
:
3220 if self
._downloader
.params
.get('noplaylist'):
3221 self
.to_screen('Downloading just video %s because of --no-playlist' % video_id
)
3222 return self
.url_result(video_id
, ie
=YoutubeIE
.ie_key(), video_id
=video_id
)
3223 self
.to_screen('Downloading playlist %s - add --no-playlist to just download video %s' % (playlist_id
, video_id
))
3224 webpage
= self
._download
_webpage
(url
, item_id
)
3225 identity_token
= self
._search
_regex
(
3226 r
'\bID_TOKEN["\']\s
*:\s
/l
*["\'](.+?)["\']', webpage,
3227 'identity token
', default=None)
3228 data = self._extract_yt_initial_data(item_id, webpage)
3230 data, lambda x: x['contents
']['twoColumnBrowseResultsRenderer
']['tabs
'], list)
3232 return self._extract_from_tabs(item_id, webpage, data, tabs, identity_token)
3234 data, lambda x: x['contents
']['twoColumnWatchNextResults
']['playlist
']['playlist
'], dict)
3236 return self._extract_from_playlist(item_id, data, playlist)
3237 # Fallback to video extraction if no playlist alike page is recognized.
3238 # First check for the current video then try the v attribute of URL query.
3240 data, lambda x: x['currentVideoEndpoint
']['watchEndpoint
']['videoId
'],
3241 compat_str) or video_id
3243 return self.url_result(video_id, ie=YoutubeIE.ie_key(), video_id=video_id)
3244 # Failed to recognize
3245 raise ExtractorError('Unable to recognize tab page
')
3248 class YoutubePlaylistIE(InfoExtractor):
3249 IE_DESC = 'YouTube
.com playlists
'
3250 _VALID_URL = r'''(?x)(?:
3255 youtube(?:kids)?\.com|
3261 (?P<id>%(playlist_id)s)
3262 )''' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
3263 IE_NAME = 'youtube
:playlist
'
3265 'note
': 'issue
#673',
3266 'url': 'PLBB231211A4F62143',
3268 'title': '[OLD]Team Fortress 2 (Class-based LP)',
3269 'id': 'PLBB231211A4F62143',
3270 'uploader': 'Wickydoo',
3271 'uploader_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
3273 'playlist_mincount': 29,
3275 'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
3277 'title': 'YDL_safe_search',
3278 'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
3280 'playlist_count': 2,
3281 'skip': 'This playlist is private',
3284 'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
3285 'playlist_count': 4,
3288 'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
3289 'uploader': 'milan',
3290 'uploader_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
3293 'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
3294 'playlist_mincount': 982,
3296 'title': '2018 Chinese New Singles (11/6 updated)',
3297 'id': 'PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
3299 'uploader_id': 'UC21nz3_MesPLqtDqwdvnoxA',
3302 'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
3304 'id': 'yeWKywCrFtk',
3306 'title': 'Small Scale Baler and Braiding Rugs',
3307 'uploader': 'Backus-Page House Museum',
3308 'uploader_id': 'backuspagemuseum',
3309 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
3310 'upload_date': '20161008',
3311 'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
3312 'categories': ['Nonprofits & Activism'],
3315 'dislike_count': int,
3319 'skip_download': True,
3322 'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
3323 'only_matching': True,
3325 'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
3326 'only_matching': True,
3328 # music album playlist
3329 'url': 'OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM',
3330 'only_matching': True,
3334 def suitable(cls
, url
):
3335 return False if YoutubeTabIE
.suitable(url
) else super(
3336 YoutubePlaylistIE
, cls
).suitable(url
)
3338 def _real_extract(self
, url
):
3339 playlist_id
= self
._match
_id
(url
)
3340 qs
= compat_urlparse
.parse_qs(compat_urlparse
.urlparse(url
).query
)
3342 qs
= {'list': playlist_id}
3343 return self
.url_result(
3344 update_url_query('https://www.youtube.com/playlist', qs
),
3345 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
3348 class YoutubeYtUserIE(InfoExtractor
):
3349 _VALID_URL
= r
'ytuser:(?P<id>.+)'
3351 'url': 'ytuser:phihag',
3352 'only_matching': True,
3355 def _real_extract(self
, url
):
3356 user_id
= self
._match
_id
(url
)
3357 return self
.url_result(
3358 'https://www.youtube.com/user/%s' % user_id
,
3359 ie
=YoutubeTabIE
.ie_key(), video_id
=user_id
)
3362 class YoutubeSearchIE(SearchInfoExtractor
, YoutubeBaseInfoExtractor
):
3363 IE_DESC
= 'YouTube.com searches'
3364 # there doesn't appear to be a real limit, for example if you search for
3365 # 'python' you get more than 8.000.000 results
3366 _MAX_RESULTS
= float('inf')
3367 IE_NAME
= 'youtube:search'
3368 _SEARCH_KEY
= 'ytsearch'
3369 _SEARCH_PARAMS
= None
3372 def _entries(self
, query
, n
):
3376 'clientName': 'WEB',
3377 'clientVersion': '2.20201021.03.00',
3382 if self
._SEARCH
_PARAMS
:
3383 data
['params'] = self
._SEARCH
_PARAMS
3385 for page_num
in itertools
.count(1):
3386 search
= self
._download
_json
(
3387 'https://www.youtube.com/youtubei/v1/search?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
3388 video_id
='query "%s"' % query
,
3389 note
='Downloading page %s' % page_num
,
3390 errnote
='Unable to download API page', fatal
=False,
3391 data
=json
.dumps(data
).encode('utf8'),
3392 headers
={'content-type': 'application/json'}
)
3395 slr_contents
= try_get(
3397 (lambda x
: x
['contents']['twoColumnSearchResultsRenderer']['primaryContents']['sectionListRenderer']['contents'],
3398 lambda x
: x
['onResponseReceivedCommands'][0]['appendContinuationItemsAction']['continuationItems']),
3400 if not slr_contents
:
3402 isr_contents
= try_get(
3404 lambda x
: x
[0]['itemSectionRenderer']['contents'],
3406 if not isr_contents
:
3408 for content
in isr_contents
:
3409 if not isinstance(content
, dict):
3411 video
= content
.get('videoRenderer')
3412 if not isinstance(video
, dict):
3414 video_id
= video
.get('videoId')
3417 title
= try_get(video
, lambda x
: x
['title']['runs'][0]['text'], compat_str
)
3418 description
= try_get(video
, lambda x
: x
['descriptionSnippet']['runs'][0]['text'], compat_str
)
3419 duration
= parse_duration(try_get(video
, lambda x
: x
['lengthText']['simpleText'], compat_str
))
3420 view_count_text
= try_get(video
, lambda x
: x
['viewCountText']['simpleText'], compat_str
) or ''
3421 view_count
= int_or_none(self
._search
_regex
(
3422 r
'^(\d+)', re
.sub(r
'\s', '', view_count_text
),
3423 'view count', default
=None))
3424 uploader
= try_get(video
, lambda x
: x
['ownerText']['runs'][0]['text'], compat_str
)
3427 '_type': 'url_transparent',
3428 'ie_key': YoutubeIE
.ie_key(),
3432 'description': description
,
3433 'duration': duration
,
3434 'view_count': view_count
,
3435 'uploader': uploader
,
3441 lambda x
: x
[1]['continuationItemRenderer']['continuationEndpoint']['continuationCommand']['token'],
3445 data
['continuation'] = token
3447 def _get_n_results(self
, query
, n
):
3448 """Get a specified number of results for a query"""
3449 return self
.playlist_result(self
._entries
(query
, n
), query
)
3452 class YoutubeSearchDateIE(YoutubeSearchIE
):
3453 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ ':date'
3454 _SEARCH_KEY
= 'ytsearchdate'
3455 IE_DESC
= 'YouTube.com searches, newest videos first'
3456 _SEARCH_PARAMS
= 'CAI%3D'
3459 class YoutubeSearchURLIE(YoutubeSearchIE
):
3460 IE_DESC
= 'YouTube.com search URLs'
3461 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ '_url'
3462 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/results\?(.*?&)?(?:search_query|q)=(?:[^&]+)(?:[&]|$)'
3463 # _MAX_RESULTS = 100
3465 'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
3466 'playlist_mincount': 5,
3468 'title': 'youtube-dl test video',
3471 'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
3472 'only_matching': True,
3476 def _make_valid_url(cls
):
3477 return cls
._VALID
_URL
3479 def _real_extract(self
, url
):
3480 qs
= compat_parse_qs(compat_urllib_parse_urlparse(url
).query
)
3481 query
= (qs
.get('search_query') or qs
.get('q'))[0]
3482 self
._SEARCH
_PARAMS
= qs
.get('sp', ('',))[0]
3483 return self
._get
_n
_results
(query
, self
._MAX
_RESULTS
)
3486 class YoutubeFeedsInfoExtractor(YoutubeTabIE
):
3488 Base class for feed extractors
3489 Subclasses must define the _FEED_NAME and _PLAYLIST_TITLE properties.
3491 _LOGIN_REQUIRED
= True
3497 return 'youtube:%s' % self
._FEED
_NAME
3499 def _real_initialize(self
):
3502 def _shelf_entries(self
, shelf_renderer
):
3503 renderer
= try_get(shelf_renderer
, lambda x
: x
['content']['gridRenderer'], dict)
3506 for entry
in self
._grid
_entries
(renderer
):
3509 def _extract_from_tabs(self
, item_id
, webpage
, data
, tabs
, identity_token
):
3510 selected_tab
= self
._extract
_selected
_tab
(tabs
)
3511 return self
.playlist_result(
3512 self
._entries
(selected_tab
['content'], identity_token
),
3513 playlist_title
=self
._PLAYLIST
_TITLE
)
3515 def _real_extract(self
, url
):
3516 item_id
= self
._FEED
_NAME
3517 url
= 'https://www.youtube.com/feed/%s' % self
._FEED
_NAME
3518 webpage
= self
._download
_webpage
(url
, item_id
)
3519 identity_token
= self
._search
_regex
(
3520 r
'\bID_TOKEN["\']\s
*:\s
*["\'](.+?)["\']', webpage,
3521 'identity token
', default=None)
3522 data = self._extract_yt_initial_data(item_id, webpage)
3524 data, lambda x: x['contents
']['twoColumnBrowseResultsRenderer
']['tabs
'], list)
3526 return self._extract_from_tabs(item_id, webpage, data, tabs, identity_token)
3527 # Failed to recognize
3528 raise ExtractorError('Unable to recognize feed page
')
3531 class YoutubeWatchLaterIE(InfoExtractor):
3532 IE_NAME = 'youtube
:watchlater
'
3533 IE_DESC = 'Youtube watch later
list, ":ytwatchlater" or "WL" for short (requires authentication
)'
3534 _VALID_URL = r'https?
://(?
:www\
.)?youtube\
.com
/feed
/watch_later|
:ytwatchlater|WL
'
3537 'url
': 'https
://www
.youtube
.com
/feed
/watch_later
',
3538 'only_matching
': True,
3540 'url
': ':ytwatchlater
',
3541 'only_matching
': True,
3544 def _real_extract(self, url):
3545 return self.url_result(
3546 'https
://www
.youtube
.com
/playlist?
list=WL
', ie=YoutubeTabIE.ie_key())
3549 class YoutubeFavouritesIE(InfoExtractor):
3550 IE_NAME = 'youtube
:favourites
'
3551 IE_DESC = 'YouTube
.com liked videos
, ":ytfav" or "LL" for short (requires authentication
)'
3552 _VALID_URL = r':ytfav(?
:ou?rite
)?s?|LL
'
3556 'only_matching
': True,
3559 def _real_extract(self, url):
3560 return self.url_result(
3561 'https
://www
.youtube
.com
/playlist?
list=LL
', ie=YoutubeTabIE.ie_key())
3564 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor):
3565 IE_DESC = 'YouTube
.com recommended videos
, ":ytrec" for short (requires authentication
)'
3566 _VALID_URL = r'https?
://(?
:www\
.)?youtube\
.com(?
:/feed
/recommended|
/?
[?
#]|/?$)|:ytrec(?:ommended)?'
3567 _FEED_NAME
= 'recommended'
3568 _PLAYLIST_TITLE
= 'Youtube Recommended videos'
3571 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor
):
3572 IE_DESC
= 'YouTube.com subscriptions feed, "ytsubs" keyword (requires authentication)'
3573 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/feed/subscriptions|:ytsub(?:scription)?s?'
3574 _FEED_NAME
= 'subscriptions'
3575 _PLAYLIST_TITLE
= 'Youtube Subscriptions'
3578 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor
):
3579 IE_DESC
= 'Youtube watch history, ":ythistory" for short (requires authentication)'
3580 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/feed/history|:ythistory'
3581 _FEED_NAME
= 'history'
3582 _PLAYLIST_TITLE
= 'Youtube History'
3585 class YoutubeTruncatedURLIE(InfoExtractor
):
3586 IE_NAME
= 'youtube:truncated_url'
3587 IE_DESC
= False # Do not list
3588 _VALID_URL
= r
'''(?x)
3590 (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
3593 annotation_id=annotation_[^&]+|
3599 attribution_link\?a=[^&]+
3605 'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
3606 'only_matching': True,
3608 'url': 'https://www.youtube.com/watch?',
3609 'only_matching': True,
3611 'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
3612 'only_matching': True,
3614 'url': 'https://www.youtube.com/watch?feature=foo',
3615 'only_matching': True,
3617 'url': 'https://www.youtube.com/watch?hl=en-GB',
3618 'only_matching': True,
3620 'url': 'https://www.youtube.com/watch?t=2372',
3621 'only_matching': True,
3624 def _real_extract(self
, url
):
3625 raise ExtractorError(
3626 'Did you forget to quote the URL? Remember that & is a meta '
3627 'character in most shells, so you want to put the URL in quotes, '
3629 '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
3630 ' or simply youtube-dl BaW_jenozKc .',
3634 class YoutubeTruncatedIDIE(InfoExtractor
):
3635 IE_NAME
= 'youtube:truncated_id'
3636 IE_DESC
= False # Do not list
3637 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
3640 'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
3641 'only_matching': True,
3644 def _real_extract(self
, url
):
3645 video_id
= self
._match
_id
(url
)
3646 raise ExtractorError(
3647 'Incomplete YouTube ID %s. URL %s looks truncated.' % (video_id
, url
),
3651 # Do Youtube show urls even exist anymore? I couldn't find any
3653 class YoutubeShowIE(YoutubeTabIE):
3654 IE_DESC = 'YouTube.com (multi-season) shows'
3655 _VALID_URL = r'https?://(?:www\.)?youtube\.com/show/(?P<id>[^?#]*)'
3656 IE_NAME = 'youtube:show'
3658 'url': 'https://www.youtube.com/show/airdisasters',
3659 'playlist_mincount': 5,
3661 'id': 'airdisasters',
3662 'title': 'Air Disasters',
3666 def _real_extract(self, url):
3667 playlist_id = self._match_id(url)
3668 return super(YoutubeShowIE, self)._real_extract(
3669 'https://www.youtube.com/show/%s/playlists' % playlist_id)