20 from .common
import InfoExtractor
, SearchInfoExtractor
21 from .openload
import PhantomJSwrapper
22 from ..compat
import functools
23 from ..jsinterp
import JSInterpreter
66 # any clients starting with _ cannot be explicitly requested by the user
69 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
70 'INNERTUBE_CONTEXT': {
73 'clientVersion': '2.20220801.00.00',
76 'INNERTUBE_CONTEXT_CLIENT_NAME': 1
79 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
80 'INNERTUBE_CONTEXT': {
82 'clientName': 'WEB_EMBEDDED_PLAYER',
83 'clientVersion': '1.20220731.00.00',
86 'INNERTUBE_CONTEXT_CLIENT_NAME': 56
89 'INNERTUBE_API_KEY': 'AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30',
90 'INNERTUBE_HOST': 'music.youtube.com',
91 'INNERTUBE_CONTEXT': {
93 'clientName': 'WEB_REMIX',
94 'clientVersion': '1.20220727.01.00',
97 'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
100 'INNERTUBE_API_KEY': 'AIzaSyBUPetSUmoZL-OhlxA7wSac5XinrygCqMo',
101 'INNERTUBE_CONTEXT': {
103 'clientName': 'WEB_CREATOR',
104 'clientVersion': '1.20220726.00.00',
107 'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
110 'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
111 'INNERTUBE_CONTEXT': {
113 'clientName': 'ANDROID',
114 'clientVersion': '17.31.35',
115 'androidSdkVersion': 30,
116 'userAgent': 'com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip'
119 'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
120 'REQUIRE_JS_PLAYER': False
122 'android_embedded': {
123 'INNERTUBE_API_KEY': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw',
124 'INNERTUBE_CONTEXT': {
126 'clientName': 'ANDROID_EMBEDDED_PLAYER',
127 'clientVersion': '17.31.35',
128 'androidSdkVersion': 30,
129 'userAgent': 'com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip'
132 'INNERTUBE_CONTEXT_CLIENT_NAME': 55,
133 'REQUIRE_JS_PLAYER': False
136 'INNERTUBE_API_KEY': 'AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI',
137 'INNERTUBE_CONTEXT': {
139 'clientName': 'ANDROID_MUSIC',
140 'clientVersion': '5.16.51',
141 'androidSdkVersion': 30,
142 'userAgent': 'com.google.android.apps.youtube.music/5.16.51 (Linux; U; Android 11) gzip'
145 'INNERTUBE_CONTEXT_CLIENT_NAME': 21,
146 'REQUIRE_JS_PLAYER': False
149 'INNERTUBE_API_KEY': 'AIzaSyD_qjV8zaaUMehtLkrKFgVeSX_Iqbtyws8',
150 'INNERTUBE_CONTEXT': {
152 'clientName': 'ANDROID_CREATOR',
153 'clientVersion': '22.30.100',
154 'androidSdkVersion': 30,
155 'userAgent': 'com.google.android.apps.youtube.creator/22.30.100 (Linux; U; Android 11) gzip'
158 'INNERTUBE_CONTEXT_CLIENT_NAME': 14,
159 'REQUIRE_JS_PLAYER': False
161 # iOS clients have HLS live streams. Setting device model to get 60fps formats.
162 # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
164 'INNERTUBE_API_KEY': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc',
165 'INNERTUBE_CONTEXT': {
168 'clientVersion': '17.33.2',
169 'deviceModel': 'iPhone14,3',
170 'userAgent': 'com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
173 'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
174 'REQUIRE_JS_PLAYER': False
177 'INNERTUBE_CONTEXT': {
179 'clientName': 'IOS_MESSAGES_EXTENSION',
180 'clientVersion': '17.33.2',
181 'deviceModel': 'iPhone14,3',
182 'userAgent': 'com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
185 'INNERTUBE_CONTEXT_CLIENT_NAME': 66,
186 'REQUIRE_JS_PLAYER': False
189 'INNERTUBE_API_KEY': 'AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s',
190 'INNERTUBE_CONTEXT': {
192 'clientName': 'IOS_MUSIC',
193 'clientVersion': '5.21',
194 'deviceModel': 'iPhone14,3',
195 'userAgent': 'com.google.ios.youtubemusic/5.21 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
198 'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
199 'REQUIRE_JS_PLAYER': False
202 'INNERTUBE_CONTEXT': {
204 'clientName': 'IOS_CREATOR',
205 'clientVersion': '22.33.101',
206 'deviceModel': 'iPhone14,3',
207 'userAgent': 'com.google.ios.ytcreator/22.33.101 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
210 'INNERTUBE_CONTEXT_CLIENT_NAME': 15,
211 'REQUIRE_JS_PLAYER': False
213 # mweb has 'ultralow' formats
214 # See: https://github.com/yt-dlp/yt-dlp/pull/557
216 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
217 'INNERTUBE_CONTEXT': {
219 'clientName': 'MWEB',
220 'clientVersion': '2.20220801.00.00',
223 'INNERTUBE_CONTEXT_CLIENT_NAME': 2
225 # This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
226 # See: https://github.com/zerodytrash/YouTube-Internal-Clients
228 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
229 'INNERTUBE_CONTEXT': {
231 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
232 'clientVersion': '2.0',
235 'INNERTUBE_CONTEXT_CLIENT_NAME': 85
240 def _split_innertube_client(client_name
):
241 variant
, *base
= client_name
.rsplit('.', 1)
243 return variant
, base
[0], variant
244 base
, *variant
= client_name
.split('_', 1)
245 return client_name
, base
, variant
[0] if variant
else None
248 def build_innertube_clients():
250 'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
252 BASE_CLIENTS
= ('android', 'web', 'tv', 'ios', 'mweb')
253 priority
= qualities(BASE_CLIENTS
[::-1])
255 for client
, ytcfg
in tuple(INNERTUBE_CLIENTS
.items()):
256 ytcfg
.setdefault('INNERTUBE_API_KEY', 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8')
257 ytcfg
.setdefault('INNERTUBE_HOST', 'www.youtube.com')
258 ytcfg
.setdefault('REQUIRE_JS_PLAYER', True)
259 ytcfg
['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en')
261 _
, base_client
, variant
= _split_innertube_client(client
)
262 ytcfg
['priority'] = 10 * priority(base_client
)
265 INNERTUBE_CLIENTS
[f
'{client}_embedscreen'] = embedscreen
= copy
.deepcopy(ytcfg
)
266 embedscreen
['INNERTUBE_CONTEXT']['client']['clientScreen'] = 'EMBED'
267 embedscreen
['INNERTUBE_CONTEXT']['thirdParty'] = THIRD_PARTY
268 embedscreen
['priority'] -= 3
269 elif variant
== 'embedded':
270 ytcfg
['INNERTUBE_CONTEXT']['thirdParty'] = THIRD_PARTY
271 ytcfg
['priority'] -= 2
273 ytcfg
['priority'] -= 3
276 build_innertube_clients()
279 class BadgeType(enum
.Enum
):
280 AVAILABILITY_UNLISTED
= enum
.auto()
281 AVAILABILITY_PRIVATE
= enum
.auto()
282 AVAILABILITY_PUBLIC
= enum
.auto()
283 AVAILABILITY_PREMIUM
= enum
.auto()
284 AVAILABILITY_SUBSCRIPTION
= enum
.auto()
285 LIVE_NOW
= enum
.auto()
288 class YoutubeBaseInfoExtractor(InfoExtractor
):
289 """Provide base functions for Youtube extractors"""
292 r
'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|clip|'
293 r
'shorts|movies|results|search|shared|hashtag|trending|explore|feed|feeds|'
294 r
'browse|oembed|get_video_info|iframe_api|s/player|source|'
295 r
'storefront|oops|index|account|t/terms|about|upload|signin|logout')
297 _PLAYLIST_ID_RE
= r
'(?:(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}|RDMM|WL|LL|LM)'
299 # _NETRC_MACHINE = 'youtube'
301 # If True it will raise an error if no login info is provided
302 _LOGIN_REQUIRED
= False
305 # invidious-redirect websites
306 r
'(?:www\.)?redirect\.invidious\.io',
307 r
'(?:(?:www|dev)\.)?invidio\.us',
308 # Invidious instances taken from https://github.com/iv-org/documentation/blob/master/docs/instances.md
309 r
'(?:www\.)?invidious\.pussthecat\.org',
310 r
'(?:www\.)?invidious\.zee\.li',
311 r
'(?:www\.)?invidious\.ethibox\.fr',
312 r
'(?:www\.)?invidious\.3o7z6yfxhbw7n3za4rss6l434kmv55cgw2vuziwuigpwegswvwzqipyd\.onion',
313 r
'(?:www\.)?osbivz6guyeahrwp2lnwyjk2xos342h4ocsxyqrlaopqjuhwn2djiiyd\.onion',
314 r
'(?:www\.)?u2cvlit75owumwpy4dj2hsmvkq7nvrclkpht7xgyye2pyoxhpmclkrad\.onion',
315 # youtube-dl invidious instances list
316 r
'(?:(?:www|no)\.)?invidiou\.sh',
317 r
'(?:(?:www|fi)\.)?invidious\.snopyta\.org',
318 r
'(?:www\.)?invidious\.kabi\.tk',
319 r
'(?:www\.)?invidious\.mastodon\.host',
320 r
'(?:www\.)?invidious\.zapashcanon\.fr',
321 r
'(?:www\.)?(?:invidious(?:-us)?|piped)\.kavin\.rocks',
322 r
'(?:www\.)?invidious\.tinfoil-hat\.net',
323 r
'(?:www\.)?invidious\.himiko\.cloud',
324 r
'(?:www\.)?invidious\.reallyancient\.tech',
325 r
'(?:www\.)?invidious\.tube',
326 r
'(?:www\.)?invidiou\.site',
327 r
'(?:www\.)?invidious\.site',
328 r
'(?:www\.)?invidious\.xyz',
329 r
'(?:www\.)?invidious\.nixnet\.xyz',
330 r
'(?:www\.)?invidious\.048596\.xyz',
331 r
'(?:www\.)?invidious\.drycat\.fr',
332 r
'(?:www\.)?inv\.skyn3t\.in',
333 r
'(?:www\.)?tube\.poal\.co',
334 r
'(?:www\.)?tube\.connect\.cafe',
335 r
'(?:www\.)?vid\.wxzm\.sx',
336 r
'(?:www\.)?vid\.mint\.lgbt',
337 r
'(?:www\.)?vid\.puffyan\.us',
338 r
'(?:www\.)?yewtu\.be',
339 r
'(?:www\.)?yt\.elukerio\.org',
340 r
'(?:www\.)?yt\.lelux\.fi',
341 r
'(?:www\.)?invidious\.ggc-project\.de',
342 r
'(?:www\.)?yt\.maisputain\.ovh',
343 r
'(?:www\.)?ytprivate\.com',
344 r
'(?:www\.)?invidious\.13ad\.de',
345 r
'(?:www\.)?invidious\.toot\.koeln',
346 r
'(?:www\.)?invidious\.fdn\.fr',
347 r
'(?:www\.)?watch\.nettohikari\.com',
348 r
'(?:www\.)?invidious\.namazso\.eu',
349 r
'(?:www\.)?invidious\.silkky\.cloud',
350 r
'(?:www\.)?invidious\.exonip\.de',
351 r
'(?:www\.)?invidious\.riverside\.rocks',
352 r
'(?:www\.)?invidious\.blamefran\.net',
353 r
'(?:www\.)?invidious\.moomoo\.de',
354 r
'(?:www\.)?ytb\.trom\.tf',
355 r
'(?:www\.)?yt\.cyberhost\.uk',
356 r
'(?:www\.)?kgg2m7yk5aybusll\.onion',
357 r
'(?:www\.)?qklhadlycap4cnod\.onion',
358 r
'(?:www\.)?axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid\.onion',
359 r
'(?:www\.)?c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid\.onion',
360 r
'(?:www\.)?fz253lmuao3strwbfbmx46yu7acac2jz27iwtorgmbqlkurlclmancad\.onion',
361 r
'(?:www\.)?invidious\.l4qlywnpwqsluw65ts7md3khrivpirse744un3x7mlskqauz5pyuzgqd\.onion',
362 r
'(?:www\.)?owxfohz4kjyv25fvlqilyxast7inivgiktls3th44jhk3ej3i7ya\.b32\.i2p',
363 r
'(?:www\.)?4l2dgddgsrkf2ous66i6seeyi6etzfgrue332grh2n7madpwopotugyd\.onion',
364 r
'(?:www\.)?w6ijuptxiku4xpnnaetxvnkc5vqcdu7mgns2u77qefoixi63vbvnpnqd\.onion',
365 r
'(?:www\.)?kbjggqkzv65ivcqj6bumvp337z6264huv5kpkwuv6gu5yjiskvan7fad\.onion',
366 r
'(?:www\.)?grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad\.onion',
367 r
'(?:www\.)?hpniueoejy4opn7bc4ftgazyqjoeqwlvh2uiku2xqku6zpoa4bf5ruid\.onion',
368 # piped instances from https://github.com/TeamPiped/Piped/wiki/Instances
369 r
'(?:www\.)?piped\.kavin\.rocks',
370 r
'(?:www\.)?piped\.silkky\.cloud',
371 r
'(?:www\.)?piped\.tokhmi\.xyz',
372 r
'(?:www\.)?piped\.moomoo\.me',
374 r
'(?:www\.)?piped\.syncpundit\.com',
375 r
'(?:www\.)?piped\.mha\.fi',
376 r
'(?:www\.)?piped\.mint\.lgbt',
377 r
'(?:www\.)?piped\.privacy\.com\.de',
380 # extracted from account/account_menu ep
381 # XXX: These are the supported YouTube UI and API languages,
382 # which is slightly different from languages supported for translation in YouTube studio
383 _SUPPORTED_LANG_CODES
= [
384 'af', 'az', 'id', 'ms', 'bs', 'ca', 'cs', 'da', 'de', 'et', 'en-IN', 'en-GB', 'en', 'es',
385 'es-419', 'es-US', 'eu', 'fil', 'fr', 'fr-CA', 'gl', 'hr', 'zu', 'is', 'it', 'sw', 'lv',
386 'lt', 'hu', 'nl', 'no', 'uz', 'pl', 'pt-PT', 'pt', 'ro', 'sq', 'sk', 'sl', 'sr-Latn', 'fi',
387 'sv', 'vi', 'tr', 'be', 'bg', 'ky', 'kk', 'mk', 'mn', 'ru', 'sr', 'uk', 'el', 'hy', 'iw',
388 'ur', 'ar', 'fa', 'ne', 'mr', 'hi', 'as', 'bn', 'pa', 'gu', 'or', 'ta', 'te', 'kn', 'ml',
389 'si', 'th', 'lo', 'my', 'ka', 'am', 'km', 'zh-CN', 'zh-TW', 'zh-HK', 'ja', 'ko'
392 @functools.cached_property
393 def _preferred_lang(self
):
395 Returns a language code supported by YouTube for the user preferred language.
396 Returns None if no preferred language set.
398 preferred_lang
= self
._configuration
_arg
('lang', ie_key
='Youtube', casesense
=True, default
=[''])[0]
399 if not preferred_lang
:
401 if preferred_lang
not in self
._SUPPORTED
_LANG
_CODES
:
402 raise ExtractorError(
403 f
'Unsupported language code: {preferred_lang}. Supported language codes (case-sensitive): {join_nonempty(*self._SUPPORTED_LANG_CODES, delim=", ")}.',
405 elif preferred_lang
!= 'en':
407 f
'Preferring "{preferred_lang}" translated fields. Note that some metadata extraction may fail or be incorrect.')
408 return preferred_lang
410 def _initialize_consent(self
):
411 cookies
= self
._get
_cookies
('https://www.youtube.com/')
412 if cookies
.get('__Secure-3PSID'):
415 consent
= cookies
.get('CONSENT')
417 if 'YES' in consent
.value
:
419 consent_id
= self
._search
_regex
(
420 r
'PENDING\+(\d+)', consent
.value
, 'consent', default
=None)
422 consent_id
= random
.randint(100, 999)
423 self
._set
_cookie
('.youtube.com', 'CONSENT', 'YES+cb.20210328-17-p0.en+FX+%s' % consent_id
)
425 def _initialize_pref(self
):
426 cookies
= self
._get
_cookies
('https://www.youtube.com/')
427 pref_cookie
= cookies
.get('PREF')
431 pref
= dict(urllib
.parse
.parse_qsl(pref_cookie
.value
))
433 self
.report_warning('Failed to parse user PREF cookie' + bug_reports_message())
434 pref
.update({'hl': self._preferred_lang or 'en', 'tz': 'UTC'}
)
435 self
._set
_cookie
('.youtube.com', name
='PREF', value
=urllib
.parse
.urlencode(pref
))
437 def _real_initialize(self
):
438 self
._initialize
_pref
()
439 self
._initialize
_consent
()
440 self
._check
_login
_required
()
442 def _check_login_required(self
):
443 if self
._LOGIN
_REQUIRED
and not self
._cookies
_passed
:
444 self
.raise_login_required('Login details are needed to download this content', method
='cookies')
446 _YT_INITIAL_DATA_RE
= r
'(?:window\s*\[\s*["\']ytInitialData
["\']\s*\]|ytInitialData)\s*='
447 _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*='
449 def _get_default_ytcfg(self, client='web'):
450 return copy.deepcopy(INNERTUBE_CLIENTS[client])
452 def _get_innertube_host(self, client='web'):
453 return INNERTUBE_CLIENTS[client]['INNERTUBE_HOST']
455 def _ytcfg_get_safe(self, ytcfg, getter, expected_type=None, default_client='web'):
456 # try_get but with fallback to default ytcfg client values when present
457 _func = lambda y: try_get(y, getter, expected_type)
458 return _func(ytcfg) or _func(self._get_default_ytcfg(default_client))
460 def _extract_client_name(self, ytcfg, default_client='web'):
461 return self._ytcfg_get_safe(
462 ytcfg, (lambda x: x['INNERTUBE_CLIENT_NAME'],
463 lambda x: x['INNERTUBE_CONTEXT']['client']['clientName']), str, default_client)
465 def _extract_client_version(self, ytcfg, default_client='web'):
466 return self._ytcfg_get_safe(
467 ytcfg, (lambda x: x['INNERTUBE_CLIENT_VERSION'],
468 lambda x: x['INNERTUBE_CONTEXT']['client']['clientVersion']), str, default_client)
470 def _select_api_hostname(self, req_api_hostname, default_client=None):
471 return (self._configuration_arg('innertube_host', [''], ie_key=YoutubeIE.ie_key())[0]
472 or req_api_hostname or self._get_innertube_host(default_client or 'web'))
474 def _extract_api_key(self, ytcfg=None, default_client='web'):
475 return self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_API_KEY'], str, default_client)
477 def _extract_context(self, ytcfg=None, default_client='web'):
479 (ytcfg, self._get_default_ytcfg(default_client)), 'INNERTUBE_CONTEXT', expected_type=dict)
480 # Enforce language and tz for extraction
481 client_context = traverse_obj(context, 'client', expected_type=dict, default={})
482 client_context.update({'hl': self._preferred_lang or 'en', 'timeZone': 'UTC', 'utcOffsetMinutes': 0})
487 def _generate_sapisidhash_header(self, origin='https://www.youtube.com'):
488 time_now = round(time.time())
489 if self._SAPISID is None:
490 yt_cookies = self._get_cookies('https://www.youtube.com')
491 # Sometimes SAPISID cookie isn't present but __Secure-3PAPISID is.
492 # See: https://github.com/yt-dlp/yt-dlp/issues/393
493 sapisid_cookie = dict_get(
494 yt_cookies, ('__Secure-3PAPISID', 'SAPISID'))
495 if sapisid_cookie and sapisid_cookie.value:
496 self._SAPISID = sapisid_cookie.value
497 self.write_debug('Extracted SAPISID cookie')
498 # SAPISID cookie is required if not already present
499 if not yt_cookies.get('SAPISID'):
500 self.write_debug('Copying __Secure-3PAPISID cookie to SAPISID cookie')
502 '.youtube.com', 'SAPISID', self._SAPISID, secure=True, expire_time=time_now + 3600)
504 self._SAPISID = False
505 if not self._SAPISID:
507 # SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323
508 sapisidhash = hashlib.sha1(
509 f'{time_now} {self._SAPISID} {origin}'.encode()).hexdigest()
510 return f'SAPISIDHASH {time_now}_{sapisidhash}'
512 def _call_api(self, ep, query, video_id, fatal=True, headers=None,
513 note='Downloading API JSON', errnote='Unable to download API page',
514 context=None, api_key=None, api_hostname=None, default_client='web'):
516 data = {'context': context} if context else {'context': self._extract_context(default_client=default_client)}
518 real_headers = self.generate_api_headers(default_client=default_client)
519 real_headers.update({'content-type': 'application/json'})
521 real_headers.update(headers)
522 api_key = (self._configuration_arg('innertube_key', [''], ie_key=YoutubeIE.ie_key(), casesense=True)[0]
523 or api_key or self._extract_api_key(default_client=default_client))
524 return self._download_json(
525 f'https://{self._select_api_hostname(api_hostname, default_client)}/youtubei/v1/{ep}',
526 video_id=video_id, fatal=fatal, note=note, errnote=errnote,
527 data=json.dumps(data).encode('utf8'), headers=real_headers,
528 query={'key': api_key, 'prettyPrint': 'false'})
530 def extract_yt_initial_data(self, item_id, webpage, fatal=True):
531 return self._search_json(self._YT_INITIAL_DATA_RE, webpage, 'yt initial data', item_id, fatal=fatal)
534 def _extract_session_index(*data):
536 Index of current account in account list.
537 See: https://github.com/yt-dlp/yt-dlp/pull/519
540 session_index = int_or_none(try_get(ytcfg, lambda x: x['SESSION_INDEX']))
541 if session_index is not None:
545 def _extract_identity_token(self, ytcfg=None, webpage=None):
547 token = try_get(ytcfg, lambda x: x['ID_TOKEN'], str)
551 return self._search_regex(
552 r'\bID_TOKEN["\']\s
*:\s
*["\'](.+?)["\']', webpage,
553 'identity token
', default=None, fatal=False)
556 def _extract_account_syncid(*args):
558 Extract syncId required to download private playlists of secondary channels
559 @params response and/or ytcfg
562 # ytcfg includes channel_syncid if on secondary channel
563 delegated_sid = try_get(data, lambda x: x['DELEGATED_SESSION_ID
'], str)
567 data, (lambda x: x['responseContext
']['mainAppWebResponseContext
']['datasyncId
'],
568 lambda x: x['DATASYNC_ID
']), str) or '').split('||
')
569 if len(sync_ids) >= 2 and sync_ids[1]:
570 # datasyncid is of the form "channel_syncid||user_syncid" for secondary channel
571 # and just "user_syncid||" for primary channel. We only want the channel_syncid
575 def _extract_visitor_data(*args):
577 Extracts visitorData from an API response or ytcfg
578 Appears to be used to track session state
581 args, [('VISITOR_DATA
', ('INNERTUBE_CONTEXT
', 'client
', 'visitorData
'), ('responseContext
', 'visitorData
'))],
584 @functools.cached_property
585 def is_authenticated(self):
586 return bool(self._generate_sapisidhash_header())
588 def extract_ytcfg(self, video_id, webpage):
591 return self._parse_json(
593 r'ytcfg\
.set\s
*\
(\s
*({.+?}
)\s
*\
)\s
*;', webpage, 'ytcfg
',
594 default='{}'), video_id, fatal=False) or {}
596 def generate_api_headers(
597 self
, *, ytcfg
=None, account_syncid
=None, session_index
=None,
598 visitor_data
=None, identity_token
=None, api_hostname
=None, default_client
='web'):
600 origin
= 'https://' + (self
._select
_api
_hostname
(api_hostname
, default_client
))
602 'X-YouTube-Client-Name': str(
603 self
._ytcfg
_get
_safe
(ytcfg
, lambda x
: x
['INNERTUBE_CONTEXT_CLIENT_NAME'], default_client
=default_client
)),
604 'X-YouTube-Client-Version': self
._extract
_client
_version
(ytcfg
, default_client
),
606 'X-Youtube-Identity-Token': identity_token
or self
._extract
_identity
_token
(ytcfg
),
607 'X-Goog-PageId': account_syncid
or self
._extract
_account
_syncid
(ytcfg
),
608 'X-Goog-Visitor-Id': visitor_data
or self
._extract
_visitor
_data
(ytcfg
),
609 'User-Agent': self
._ytcfg
_get
_safe
(ytcfg
, lambda x
: x
['INNERTUBE_CONTEXT']['client']['userAgent'], default_client
=default_client
)
611 if session_index
is None:
612 session_index
= self
._extract
_session
_index
(ytcfg
)
613 if account_syncid
or session_index
is not None:
614 headers
['X-Goog-AuthUser'] = session_index
if session_index
is not None else 0
616 auth
= self
._generate
_sapisidhash
_header
(origin
)
618 headers
['Authorization'] = auth
619 headers
['X-Origin'] = origin
620 return {h: v for h, v in headers.items() if v is not None}
622 def _download_ytcfg(self
, client
, video_id
):
624 'web': 'https://www.youtube.com',
625 'web_music': 'https://music.youtube.com',
626 'web_embedded': f
'https://www.youtube.com/embed/{video_id}?html5=1'
630 webpage
= self
._download
_webpage
(
631 url
, video_id
, fatal
=False, note
=f
'Downloading {client.replace("_", " ").strip()} client config')
632 return self
.extract_ytcfg(video_id
, webpage
) or {}
635 def _build_api_continuation_query(continuation
, ctp
=None):
637 'continuation': continuation
639 # TODO: Inconsistency with clickTrackingParams.
640 # Currently we have a fixed ctp contained within context (from ytcfg)
641 # and a ctp in root query for continuation.
643 query
['clickTracking'] = {'clickTrackingParams': ctp}
647 def _extract_next_continuation_data(cls
, renderer
):
648 next_continuation
= try_get(
649 renderer
, (lambda x
: x
['continuations'][0]['nextContinuationData'],
650 lambda x
: x
['continuation']['reloadContinuationData']), dict)
651 if not next_continuation
:
653 continuation
= next_continuation
.get('continuation')
656 ctp
= next_continuation
.get('clickTrackingParams')
657 return cls
._build
_api
_continuation
_query
(continuation
, ctp
)
660 def _extract_continuation_ep_data(cls
, continuation_ep
: dict):
661 if isinstance(continuation_ep
, dict):
662 continuation
= try_get(
663 continuation_ep
, lambda x
: x
['continuationCommand']['token'], str)
666 ctp
= continuation_ep
.get('clickTrackingParams')
667 return cls
._build
_api
_continuation
_query
(continuation
, ctp
)
670 def _extract_continuation(cls
, renderer
):
671 next_continuation
= cls
._extract
_next
_continuation
_data
(renderer
)
672 if next_continuation
:
673 return next_continuation
676 for key
in ('contents', 'items', 'rows'):
677 contents
.extend(try_get(renderer
, lambda x
: x
[key
], list) or [])
679 for content
in contents
:
680 if not isinstance(content
, dict):
682 continuation_ep
= try_get(
683 content
, (lambda x
: x
['continuationItemRenderer']['continuationEndpoint'],
684 lambda x
: x
['continuationItemRenderer']['button']['buttonRenderer']['command']),
686 continuation
= cls
._extract
_continuation
_ep
_data
(continuation_ep
)
691 def _extract_alerts(cls
, data
):
692 for alert_dict
in try_get(data
, lambda x
: x
['alerts'], list) or []:
693 if not isinstance(alert_dict
, dict):
695 for alert
in alert_dict
.values():
696 alert_type
= alert
.get('type')
699 message
= cls
._get
_text
(alert
, 'text')
701 yield alert_type
, message
703 def _report_alerts(self
, alerts
, expected
=True, fatal
=True, only_once
=False):
706 for alert_type
, alert_message
in alerts
:
707 if alert_type
.lower() == 'error' and fatal
:
708 errors
.append([alert_type
, alert_message
])
710 warnings
.append([alert_type
, alert_message
])
712 for alert_type
, alert_message
in (warnings
+ errors
[:-1]):
713 self
.report_warning(f
'YouTube said: {alert_type} - {alert_message}', only_once
=only_once
)
715 raise ExtractorError('YouTube said: %s' % errors
[-1][1], expected
=expected
)
717 def _extract_and_report_alerts(self
, data
, *args
, **kwargs
):
718 return self
._report
_alerts
(self
._extract
_alerts
(data
), *args
, **kwargs
)
720 def _extract_badges(self
, renderer
: dict):
722 'PRIVACY_UNLISTED': BadgeType
.AVAILABILITY_UNLISTED
,
723 'PRIVACY_PRIVATE': BadgeType
.AVAILABILITY_PRIVATE
,
724 'PRIVACY_PUBLIC': BadgeType
.AVAILABILITY_PUBLIC
728 'BADGE_STYLE_TYPE_MEMBERS_ONLY': BadgeType
.AVAILABILITY_SUBSCRIPTION
,
729 'BADGE_STYLE_TYPE_PREMIUM': BadgeType
.AVAILABILITY_PREMIUM
,
730 'BADGE_STYLE_TYPE_LIVE_NOW': BadgeType
.LIVE_NOW
734 'unlisted': BadgeType
.AVAILABILITY_UNLISTED
,
735 'private': BadgeType
.AVAILABILITY_PRIVATE
,
736 'members only': BadgeType
.AVAILABILITY_SUBSCRIPTION
,
737 'live': BadgeType
.LIVE_NOW
,
738 'premium': BadgeType
.AVAILABILITY_PREMIUM
742 for badge
in traverse_obj(renderer
, ('badges', ..., 'metadataBadgeRenderer'), default
=[]):
744 privacy_icon_map
.get(traverse_obj(badge
, ('icon', 'iconType'), expected_type
=str))
745 or badge_style_map
.get(traverse_obj(badge
, 'style'))
748 badges
.append({'type': badge_type}
)
751 # fallback, won't work in some languages
752 label
= traverse_obj(badge
, 'label', expected_type
=str, default
='')
753 for match
, label_badge_type
in label_map
.items():
754 if match
in label
.lower():
755 badges
.append({'type': badge_type}
)
761 def _has_badge(badges
, badge_type
):
762 return bool(traverse_obj(badges
, lambda _
, v
: v
['type'] == badge_type
))
765 def _get_text(data
, *path_list
, max_runs
=None):
766 for path
in path_list
or [None]:
770 obj
= traverse_obj(data
, path
, default
=[])
771 if not any(key
is ... or isinstance(key
, (list, tuple)) for key
in variadic(path
)):
774 text
= try_get(item
, lambda x
: x
['simpleText'], str)
777 runs
= try_get(item
, lambda x
: x
['runs'], list) or []
778 if not runs
and isinstance(item
, list):
781 runs
= runs
[:min(len(runs
), max_runs
or len(runs
))]
782 text
= ''.join(traverse_obj(runs
, (..., 'text'), expected_type
=str, default
=[]))
786 def _get_count(self
, data
, *path_list
):
787 count_text
= self
._get
_text
(data
, *path_list
) or ''
788 count
= parse_count(count_text
)
791 self
._search
_regex
(r
'^([\d,]+)', re
.sub(r
'\s', '', count_text
), 'count', default
=None))
795 def _extract_thumbnails(data
, *path_list
):
797 Extract thumbnails from thumbnails dict
798 @param path_list: path list to level that contains 'thumbnails' key
801 for path
in path_list
or [()]:
802 for thumbnail
in traverse_obj(data
, (*variadic(path
), 'thumbnails', ...), default
=[]):
803 thumbnail_url
= url_or_none(thumbnail
.get('url'))
804 if not thumbnail_url
:
806 # Sometimes youtube gives a wrong thumbnail URL. See:
807 # https://github.com/yt-dlp/yt-dlp/issues/233
808 # https://github.com/ytdl-org/youtube-dl/issues/28023
809 if 'maxresdefault' in thumbnail_url
:
810 thumbnail_url
= thumbnail_url
.split('?')[0]
812 'url': thumbnail_url
,
813 'height': int_or_none(thumbnail
.get('height')),
814 'width': int_or_none(thumbnail
.get('width')),
819 def extract_relative_time(relative_time_text
):
821 Extracts a relative time from string and converts to dt object
822 e.g. 'streamed 6 days ago', '5 seconds ago (edited)', 'updated today'
824 mobj
= re
.search(r
'(?P<start>today|yesterday|now)|(?P<time>\d+)\s*(?P<unit>microsecond|second|minute|hour|day|week|month|year)s?\s*ago', relative_time_text
)
826 start
= mobj
.group('start')
828 return datetime_from_str(start
)
830 return datetime_from_str('now-%s%s' % (mobj
.group('time'), mobj
.group('unit')))
834 def _parse_time_text(self
, text
):
837 dt
= self
.extract_relative_time(text
)
839 if isinstance(dt
, datetime
.datetime
):
840 timestamp
= calendar
.timegm(dt
.timetuple())
842 if timestamp
is None:
844 unified_timestamp(text
) or unified_timestamp(
846 (r
'([a-z]+\s*\d{1,2},?\s*20\d{2})', r
'(?:.+|^)(?:live|premieres|ed|ing)(?:\s*(?:on|for))?\s*(.+\d)'),
847 text
.lower(), 'time text', default
=None)))
849 if text
and timestamp
is None and self
._preferred
_lang
in (None, 'en'):
851 f
'Cannot parse localized time text "{text}"', only_once
=True)
854 def _extract_response(self
, item_id
, query
, note
='Downloading API JSON', headers
=None,
855 ytcfg
=None, check_get_keys
=None, ep
='browse', fatal
=True, api_hostname
=None,
856 default_client
='web'):
857 for retry
in self
.RetryManager():
859 response
= self
._call
_api
(
860 ep
=ep
, fatal
=True, headers
=headers
,
861 video_id
=item_id
, query
=query
, note
=note
,
862 context
=self
._extract
_context
(ytcfg
, default_client
),
863 api_key
=self
._extract
_api
_key
(ytcfg
, default_client
),
864 api_hostname
=api_hostname
, default_client
=default_client
)
865 except ExtractorError
as e
:
866 if not isinstance(e
.cause
, network_exceptions
):
867 return self
._error
_or
_warning
(e
, fatal
=fatal
)
868 elif not isinstance(e
.cause
, urllib
.error
.HTTPError
):
872 first_bytes
= e
.cause
.read(512)
873 if not is_html(first_bytes
):
876 self
._webpage
_read
_content
(e
.cause
, None, item_id
, prefix
=first_bytes
) or '{}', item_id
, fatal
=False),
877 lambda x
: x
['error']['message'], str)
879 self
._report
_alerts
([('ERROR', yt_error
)], fatal
=False)
880 # Downloading page may result in intermittent 5xx HTTP error
881 # Sometimes a 404 is also recieved. See: https://github.com/ytdl-org/youtube-dl/issues/28289
882 # We also want to catch all other network exceptions since errors in later pages can be troublesome
883 # See https://github.com/yt-dlp/yt-dlp/issues/507#issuecomment-880188210
884 if e
.cause
.code
not in (403, 429):
887 return self
._error
_or
_warning
(e
, fatal
=fatal
)
890 self
._extract
_and
_report
_alerts
(response
, only_once
=True)
891 except ExtractorError
as e
:
892 # YouTube servers may return errors we want to retry on in a 200 OK response
893 # See: https://github.com/yt-dlp/yt-dlp/issues/839
894 if 'unknown error' in e
.msg
.lower():
897 return self
._error
_or
_warning
(e
, fatal
=fatal
)
898 # Youtube sometimes sends incomplete data
899 # See: https://github.com/ytdl-org/youtube-dl/issues/28194
900 if not traverse_obj(response
, *variadic(check_get_keys
)):
901 retry
.error
= ExtractorError('Incomplete data received', expected
=True)
907 def is_music_url(url
):
908 return re
.match(r
'https?://music\.youtube\.com/', url
) is not None
910 def _extract_video(self
, renderer
):
911 video_id
= renderer
.get('videoId')
912 title
= self
._get
_text
(renderer
, 'title')
913 description
= self
._get
_text
(renderer
, 'descriptionSnippet')
914 duration
= parse_duration(self
._get
_text
(
915 renderer
, 'lengthText', ('thumbnailOverlays', ..., 'thumbnailOverlayTimeStatusRenderer', 'text')))
917 duration
= parse_duration(self
._search
_regex
(
918 r
'(?i)(ago)(?!.*\1)\s+(?P<duration>[a-z0-9 ,]+?)(?:\s+[\d,]+\s+views)?(?:\s+-\s+play\s+short)?$',
919 traverse_obj(renderer
, ('title', 'accessibility', 'accessibilityData', 'label'), default
='', expected_type
=str),
920 video_id
, default
=None, group
='duration'))
922 view_count
= self
._get
_count
(renderer
, 'viewCountText')
924 uploader
= self
._get
_text
(renderer
, 'ownerText', 'shortBylineText')
925 channel_id
= traverse_obj(
926 renderer
, ('shortBylineText', 'runs', ..., 'navigationEndpoint', 'browseEndpoint', 'browseId'),
927 expected_type
=str, get_all
=False)
928 time_text
= self
._get
_text
(renderer
, 'publishedTimeText') or ''
929 scheduled_timestamp
= str_to_int(traverse_obj(renderer
, ('upcomingEventData', 'startTime'), get_all
=False))
930 overlay_style
= traverse_obj(
931 renderer
, ('thumbnailOverlays', ..., 'thumbnailOverlayTimeStatusRenderer', 'style'),
932 get_all
=False, expected_type
=str)
933 badges
= self
._extract
_badges
(renderer
)
934 thumbnails
= self
._extract
_thumbnails
(renderer
, 'thumbnail')
935 navigation_url
= urljoin('https://www.youtube.com/', traverse_obj(
936 renderer
, ('navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url'),
937 expected_type
=str)) or ''
938 url
= f
'https://www.youtube.com/watch?v={video_id}'
939 if overlay_style
== 'SHORTS' or '/shorts/' in navigation_url
:
940 url
= f
'https://www.youtube.com/shorts/{video_id}'
944 'ie_key': YoutubeIE
.ie_key(),
948 'description': description
,
949 'duration': duration
,
950 'view_count': view_count
,
951 'uploader': uploader
,
952 'channel_id': channel_id
,
953 'thumbnails': thumbnails
,
954 'upload_date': (strftime_or_none(self
._parse
_time
_text
(time_text
), '%Y%m%d')
955 if self
._configuration
_arg
('approximate_date', ie_key
='youtubetab')
957 'live_status': ('is_upcoming' if scheduled_timestamp
is not None
958 else 'was_live' if 'streamed' in time_text
.lower()
959 else 'is_live' if overlay_style
== 'LIVE' or self
._has
_badge
(badges
, BadgeType
.LIVE_NOW
)
961 'release_timestamp': scheduled_timestamp
,
963 'public' if self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
964 else self
._availability
(
965 is_private
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
) or None,
966 needs_premium
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
) or None,
967 needs_subscription
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
) or None,
968 is_unlisted
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
) or None)
972 class YoutubeIE(YoutubeBaseInfoExtractor
):
974 _VALID_URL
= r
"""(?x)^
976 (?:https?://|//) # http(s):// or protocol-independent URL
977 (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie|kids)?\.com|
978 (?:www\.)?deturl\.com/www\.youtube\.com|
979 (?:www\.)?pwnyoutube\.com|
980 (?:www\.)?hooktube\.com|
981 (?:www\.)?yourepeat\.com|
984 youtube\.googleapis\.com)/ # the various hostnames, with wildcard subdomains
985 (?:.*?\#/)? # handle anchor (#/) redirect urls
986 (?: # the various things that can precede the ID:
987 (?:(?:v|embed|e|shorts)/(?!videoseries|live_stream)) # v/ or embed/ or e/ or shorts/
988 |(?: # or the v= param in all its forms
989 (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
990 (?:\?|\#!?) # the params delimiter ? or # or #!
991 (?:.*?[&;])?? # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&v=V36LpHqtcDY)
996 youtu\.be| # just youtu.be/xxxx
997 vid\.plus| # or vid.plus/xxxx
998 zwearz\.com/watch| # or zwearz.com/watch/xxxx
1001 |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
1003 )? # all until now is optional -> you can pass the naked ID
1004 (?P<id>[0-9A-Za-z_-]{11}) # here is it! the YouTube video ID
1005 (?(1).+)? # if we found the ID, everything can follow
1007 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
1012 <(?:[0-9A-Za-z-]+?)?iframe[^>]+?src=|
1020 (?P
<url
>(?
:https?
:)?
//(?
:www\
.)?
youtube(?
:-nocookie
)?\
.com
/
1021 (?
:embed|v|p
)/[0-9A
-Za
-z_
-]{11}
.*?
)
1023 # https://wordpress.org/plugins/lazy-load-for-videos/
1025 <a\s
[^
>]*\bhref
="(?P<url>https://www\.youtube\.com/watch\?v=[0-9A-Za-z_-]{11})"
1026 \s
[^
>]*\bclass
="[^"]*\blazy
-load
-youtube
''',
1030 r'/s/player/(?P<id>[a-zA-Z0-9_-]{8,})/player',
1031 r'/(?P<id>[a-zA-Z0-9_-]{8,})/player(?:_ias\.vflset(?:/[a-zA-Z]{2,3}_[a-zA-Z]{2,3})?|-plasma-ias-(?:phone|tablet)-[a-z]{2}_[A-Z]{2}\.vflset)/base\.js$',
1032 r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
1035 '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1036 '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1037 '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
1038 '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
1039 '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
1040 '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1041 '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1042 '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1043 # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
1044 '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
1045 '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1046 '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1047 '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1048 '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1049 '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1050 '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1051 '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1052 '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1056 '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1057 '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1058 '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1059 '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1060 '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
1061 '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1062 '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1064 # Apple HTTP Live Streaming
1065 '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1066 '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1067 '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1068 '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1069 '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1070 '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1071 '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1072 '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
1075 '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
1076 '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
1077 '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1078 '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
1079 '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
1080 '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
1081 '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
1082 '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1083 '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
1084 '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1085 '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1086 '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
1089 '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
1090 '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
1091 '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
1092 '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1093 '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1094 '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
1095 '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
1098 '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1099 '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1100 '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1101 '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1102 '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1103 '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1104 '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
1105 '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1106 '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1107 '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1108 '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1109 '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1110 '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1111 '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1112 '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1113 # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
1114 '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1115 '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1116 '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1117 '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1118 '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1119 '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1122 '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
1123 '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
1125 # Dash webm audio with opus inside
1126 '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
1127 '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
1128 '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
1131 '_rtmp': {'protocol': 'rtmp'},
1133 # av01 video only formats sometimes served with "unknown" codecs
1134 '394': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'av01.0.00M.08'},
1135 '395': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'av01.0.00M.08'},
1136 '396': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'av01.0.01M.08'},
1137 '397': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'av01.0.04M.08'},
1138 '398': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'av01.0.05M.08'},
1139 '399': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'av01.0.08M.08'},
1140 '400': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
1141 '401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
1143 _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
1150 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
1152 'id': 'BaW_jenozKc',
1154 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
1155 'uploader': 'Philipp Hagemeister',
1156 'uploader_id': 'phihag',
1157 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
1158 'channel': 'Philipp Hagemeister',
1159 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
1160 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
1161 'upload_date': '20121002',
1162 'description': 'md5:8fb536f4877b8a7455c2ec23794dbc22',
1163 'categories': ['Science & Technology'],
1164 'tags': ['youtube-dl'],
1168 'availability': 'public',
1169 'playable_in_embed': True,
1170 'thumbnail': 'https://i.ytimg.com/vi/BaW_jenozKc/maxresdefault.jpg',
1171 'live_status': 'not_live',
1175 'comment_count': int,
1176 'channel_follower_count': int
1180 'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
1181 'note': 'Embed-only video (#1746)',
1183 'id': 'yZIXLfi8CZQ',
1185 'upload_date': '20120608',
1186 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
1187 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
1188 'uploader': 'SET India',
1189 'uploader_id': 'setindia',
1190 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/setindia',
1193 'skip': 'Private video',
1196 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=yZIXLfi8CZQ',
1197 'note': 'Use the first video ID in the URL',
1199 'id': 'BaW_jenozKc',
1201 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
1202 'uploader': 'Philipp Hagemeister',
1203 'uploader_id': 'phihag',
1204 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/phihag',
1205 'channel': 'Philipp Hagemeister',
1206 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
1207 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
1208 'upload_date': '20121002',
1209 'description': 'md5:8fb536f4877b8a7455c2ec23794dbc22',
1210 'categories': ['Science & Technology'],
1211 'tags': ['youtube-dl'],
1215 'availability': 'public',
1216 'playable_in_embed': True,
1217 'thumbnail': 'https://i.ytimg.com/vi/BaW_jenozKc/maxresdefault.jpg',
1218 'live_status': 'not_live',
1220 'comment_count': int,
1221 'channel_follower_count': int
1224 'skip_download': True,
1228 'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
1229 'note': '256k DASH audio (format 141) via DASH manifest',
1231 'id': 'a9LDPn-MO4I',
1233 'upload_date': '20121002',
1234 'uploader_id': '8KVIDEO',
1235 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/8KVIDEO',
1237 'uploader': '8KVIDEO',
1238 'title': 'UHDTV TEST 8K VIDEO.mp4'
1241 'youtube_include_dash_manifest': True,
1244 'skip': 'format 141 not served anymore',
1246 # DASH manifest with encrypted signature
1248 'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
1250 'id': 'IB3lcPjvWLA',
1252 'title': 'Afrojack, Spree Wilson - The Spark (Official Music Video) ft. Spree Wilson',
1253 'description': 'md5:8f5e2b82460520b619ccac1f509d43bf',
1255 'uploader': 'AfrojackVEVO',
1256 'uploader_id': 'AfrojackVEVO',
1257 'upload_date': '20131011',
1260 'channel_id': 'UChuZAo1RKL85gev3Eal9_zg',
1261 'playable_in_embed': True,
1262 'channel_url': 'https://www.youtube.com/channel/UChuZAo1RKL85gev3Eal9_zg',
1264 'track': 'The Spark',
1265 'live_status': 'not_live',
1266 'thumbnail': 'https://i.ytimg.com/vi_webp/IB3lcPjvWLA/maxresdefault.webp',
1267 'channel': 'Afrojack',
1268 'uploader_url': 'http://www.youtube.com/user/AfrojackVEVO',
1270 'availability': 'public',
1271 'categories': ['Music'],
1273 'alt_title': 'The Spark',
1274 'channel_follower_count': int
1277 'youtube_include_dash_manifest': True,
1278 'format': '141/bestaudio[ext=m4a]',
1281 # Age-gate videos. See https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-888837000
1283 'note': 'Embed allowed age-gate video',
1284 'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
1286 'id': 'HtVdAasjOgU',
1288 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
1289 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
1291 'uploader': 'The Witcher',
1292 'uploader_id': 'WitcherGame',
1293 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/WitcherGame',
1294 'upload_date': '20140605',
1296 'categories': ['Gaming'],
1297 'thumbnail': 'https://i.ytimg.com/vi_webp/HtVdAasjOgU/maxresdefault.webp',
1298 'availability': 'needs_auth',
1299 'channel_url': 'https://www.youtube.com/channel/UCzybXLxv08IApdjdN0mJhEg',
1301 'channel': 'The Witcher',
1302 'live_status': 'not_live',
1304 'channel_id': 'UCzybXLxv08IApdjdN0mJhEg',
1305 'playable_in_embed': True,
1307 'channel_follower_count': int
1311 'note': 'Age-gate video with embed allowed in public site',
1312 'url': 'https://youtube.com/watch?v=HsUATh_Nc2U',
1314 'id': 'HsUATh_Nc2U',
1316 'title': 'Godzilla 2 (Official Video)',
1317 'description': 'md5:bf77e03fcae5529475e500129b05668a',
1318 'upload_date': '20200408',
1319 'uploader_id': 'FlyingKitty900',
1320 'uploader': 'FlyingKitty',
1322 'availability': 'needs_auth',
1323 'channel_id': 'UCYQT13AtrJC0gsM1far_zJg',
1324 'uploader_url': 'http://www.youtube.com/user/FlyingKitty900',
1325 'channel': 'FlyingKitty',
1326 'channel_url': 'https://www.youtube.com/channel/UCYQT13AtrJC0gsM1far_zJg',
1328 'categories': ['Entertainment'],
1329 'live_status': 'not_live',
1330 'tags': ['Flyingkitty', 'godzilla 2'],
1331 'thumbnail': 'https://i.ytimg.com/vi/HsUATh_Nc2U/maxresdefault.jpg',
1334 'playable_in_embed': True,
1335 'channel_follower_count': int
1339 'note': 'Age-gate video embedable only with clientScreen=EMBED',
1340 'url': 'https://youtube.com/watch?v=Tq92D6wQ1mg',
1342 'id': 'Tq92D6wQ1mg',
1343 'title': '[MMD] Adios - EVERGLOW [+Motion DL]',
1345 'upload_date': '20191228',
1346 'uploader_id': 'UC1yoRdFoFJaCY-AGfD9W0wQ',
1347 'uploader': 'Projekt Melody',
1348 'description': 'md5:17eccca93a786d51bc67646756894066',
1351 'availability': 'needs_auth',
1352 'uploader_url': 'http://www.youtube.com/channel/UC1yoRdFoFJaCY-AGfD9W0wQ',
1353 'channel_id': 'UC1yoRdFoFJaCY-AGfD9W0wQ',
1355 'thumbnail': 'https://i.ytimg.com/vi_webp/Tq92D6wQ1mg/sddefault.webp',
1356 'channel': 'Projekt Melody',
1357 'live_status': 'not_live',
1358 'tags': ['mmd', 'dance', 'mikumikudance', 'kpop', 'vtuber'],
1359 'playable_in_embed': True,
1360 'categories': ['Entertainment'],
1362 'channel_url': 'https://www.youtube.com/channel/UC1yoRdFoFJaCY-AGfD9W0wQ',
1363 'comment_count': int,
1364 'channel_follower_count': int
1368 'note': 'Non-Agegated non-embeddable video',
1369 'url': 'https://youtube.com/watch?v=MeJVWBSsPAY',
1371 'id': 'MeJVWBSsPAY',
1373 'title': 'OOMPH! - Such Mich Find Mich (Lyrics)',
1374 'uploader': 'Herr Lurik',
1375 'uploader_id': 'st3in234',
1376 'description': 'Fan Video. Music & Lyrics by OOMPH!.',
1377 'upload_date': '20130730',
1378 'track': 'Such mich find mich',
1380 'tags': ['oomph', 'such mich find mich', 'lyrics', 'german industrial', 'musica industrial'],
1382 'playable_in_embed': False,
1383 'creator': 'OOMPH!',
1384 'thumbnail': 'https://i.ytimg.com/vi/MeJVWBSsPAY/sddefault.jpg',
1386 'alt_title': 'Such mich find mich',
1388 'channel': 'Herr Lurik',
1389 'channel_id': 'UCdR3RSDPqub28LjZx0v9-aA',
1390 'categories': ['Music'],
1391 'availability': 'public',
1392 'uploader_url': 'http://www.youtube.com/user/st3in234',
1393 'channel_url': 'https://www.youtube.com/channel/UCdR3RSDPqub28LjZx0v9-aA',
1394 'live_status': 'not_live',
1396 'channel_follower_count': int
1400 'note': 'Non-bypassable age-gated video',
1401 'url': 'https://youtube.com/watch?v=Cr381pDsSsA',
1402 'only_matching': True,
1404 # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421)
1405 # YouTube Red ad is not captured for creator
1407 'url': '__2ABJjxzNo',
1409 'id': '__2ABJjxzNo',
1412 'upload_date': '20100430',
1413 'uploader_id': 'deadmau5',
1414 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/deadmau5',
1415 'creator': 'deadmau5',
1416 'description': 'md5:6cbcd3a92ce1bc676fc4d6ab4ace2336',
1417 'uploader': 'deadmau5',
1418 'title': 'Deadmau5 - Some Chords (HD)',
1419 'alt_title': 'Some Chords',
1420 'availability': 'public',
1422 'channel_id': 'UCYEK6xds6eo-3tr4xRdflmQ',
1424 'live_status': 'not_live',
1425 'channel': 'deadmau5',
1426 'thumbnail': 'https://i.ytimg.com/vi_webp/__2ABJjxzNo/maxresdefault.webp',
1428 'track': 'Some Chords',
1429 'artist': 'deadmau5',
1430 'playable_in_embed': True,
1432 'channel_url': 'https://www.youtube.com/channel/UCYEK6xds6eo-3tr4xRdflmQ',
1433 'categories': ['Music'],
1434 'album': 'Some Chords',
1435 'channel_follower_count': int
1437 'expected_warnings': [
1438 'DASH manifest missing',
1441 # Olympics (https://github.com/ytdl-org/youtube-dl/issues/4431)
1443 'url': 'lqQg6PlCWgI',
1445 'id': 'lqQg6PlCWgI',
1448 'upload_date': '20150827',
1449 'uploader_id': 'olympic',
1450 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/olympic',
1451 'description': 'md5:04bbbf3ccceb6795947572ca36f45904',
1452 'uploader': 'Olympics',
1453 'title': 'Hockey - Women - GER-AUS - London 2012 Olympic Games',
1455 'release_timestamp': 1343767800,
1456 'playable_in_embed': True,
1457 'categories': ['Sports'],
1458 'release_date': '20120731',
1459 'channel': 'Olympics',
1460 'tags': ['Hockey', '2012-07-31', '31 July 2012', 'Riverbank Arena', 'Session', 'Olympics', 'Olympic Games', 'London 2012', '2012 Summer Olympics', 'Summer Games'],
1461 'channel_id': 'UCTl3QQTvqHFjurroKxexy2Q',
1462 'thumbnail': 'https://i.ytimg.com/vi/lqQg6PlCWgI/maxresdefault.jpg',
1464 'availability': 'public',
1465 'live_status': 'was_live',
1467 'channel_url': 'https://www.youtube.com/channel/UCTl3QQTvqHFjurroKxexy2Q',
1468 'channel_follower_count': int
1471 'skip_download': 'requires avconv',
1476 'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
1478 'id': '_b-2C3KPAM0',
1480 'stretched_ratio': 16 / 9.,
1482 'upload_date': '20110310',
1483 'uploader_id': 'AllenMeow',
1484 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/AllenMeow',
1485 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
1487 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
1488 'playable_in_embed': True,
1492 'channel_url': 'https://www.youtube.com/channel/UCS-xxCmRaA6BFdmgDPA_BIw',
1493 'channel_id': 'UCS-xxCmRaA6BFdmgDPA_BIw',
1494 'thumbnail': 'https://i.ytimg.com/vi/_b-2C3KPAM0/maxresdefault.jpg',
1496 'categories': ['People & Blogs'],
1498 'live_status': 'not_live',
1499 'availability': 'unlisted',
1500 'comment_count': int,
1501 'channel_follower_count': int
1504 # url_encoded_fmt_stream_map is empty string
1506 'url': 'qEJwOuvDf7I',
1508 'id': 'qEJwOuvDf7I',
1510 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
1512 'upload_date': '20150404',
1513 'uploader_id': 'spbelect',
1514 'uploader': 'Наблюдатели Петербурга',
1517 'skip_download': 'requires avconv',
1519 'skip': 'This live event has ended.',
1521 # Extraction from multiple DASH manifests (https://github.com/ytdl-org/youtube-dl/pull/6097)
1523 'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
1525 'id': 'FIl7x6_3R5Y',
1527 'title': 'md5:7b81415841e02ecd4313668cde88737a',
1528 'description': 'md5:116377fd2963b81ec4ce64b542173306',
1530 'upload_date': '20150625',
1531 'uploader_id': 'dorappi2000',
1532 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/dorappi2000',
1533 'uploader': 'dorappi2000',
1534 'formats': 'mincount:31',
1536 'skip': 'not actual anymore',
1538 # DASH manifest with segment_list
1540 'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
1541 'md5': '8ce563a1d667b599d21064e982ab9e31',
1543 'id': 'CsmdDsKjzN8',
1545 'upload_date': '20150501', # According to '<meta itemprop="datePublished"', but in other places it's 20150510
1546 'uploader': 'Airtek',
1547 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
1548 'uploader_id': 'UCzTzUmjXxxacNnL8I3m4LnQ',
1549 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
1552 'youtube_include_dash_manifest': True,
1553 'format': '135', # bestvideo
1555 'skip': 'This live event has ended.',
1558 # Multifeed videos (multiple cameras), URL is for Main Camera
1559 'url': 'https://www.youtube.com/watch?v=jvGDaLqkpTg',
1561 'id': 'jvGDaLqkpTg',
1562 'title': 'Tom Clancy Free Weekend Rainbow Whatever',
1563 'description': 'md5:e03b909557865076822aa169218d6a5d',
1567 'id': 'jvGDaLqkpTg',
1569 'title': 'Tom Clancy Free Weekend Rainbow Whatever (Main Camera)',
1570 'description': 'md5:e03b909557865076822aa169218d6a5d',
1572 'upload_date': '20161111',
1573 'uploader': 'Team PGP',
1574 'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
1575 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
1579 'id': '3AKt1R1aDnw',
1581 'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 2)',
1582 'description': 'md5:e03b909557865076822aa169218d6a5d',
1584 'upload_date': '20161111',
1585 'uploader': 'Team PGP',
1586 'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
1587 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
1591 'id': 'RtAMM00gpVc',
1593 'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 3)',
1594 'description': 'md5:e03b909557865076822aa169218d6a5d',
1596 'upload_date': '20161111',
1597 'uploader': 'Team PGP',
1598 'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
1599 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
1603 'id': '6N2fdlP3C5U',
1605 'title': 'Tom Clancy Free Weekend Rainbow Whatever (Camera 4)',
1606 'description': 'md5:e03b909557865076822aa169218d6a5d',
1608 'upload_date': '20161111',
1609 'uploader': 'Team PGP',
1610 'uploader_id': 'UChORY56LMMETTuGjXaJXvLg',
1611 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UChORY56LMMETTuGjXaJXvLg',
1615 'skip_download': True,
1617 'skip': 'Not multifeed anymore',
1620 # Multifeed video with comma in title (see https://github.com/ytdl-org/youtube-dl/issues/8536)
1621 'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
1623 'id': 'gVfLd0zydlo',
1624 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
1626 'playlist_count': 2,
1627 'skip': 'Not multifeed anymore',
1630 'url': 'https://vid.plus/FlRa-iH7PGw',
1631 'only_matching': True,
1634 'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
1635 'only_matching': True,
1638 # Title with JS-like syntax "};" (see https://github.com/ytdl-org/youtube-dl/issues/7468)
1639 # Also tests cut-off URL expansion in video description (see
1640 # https://github.com/ytdl-org/youtube-dl/issues/1892,
1641 # https://github.com/ytdl-org/youtube-dl/issues/8164)
1642 'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
1644 'id': 'lsguqyKfVQg',
1646 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
1647 'alt_title': 'Dark Walk',
1648 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
1650 'upload_date': '20151119',
1651 'uploader_id': 'IronSoulElf',
1652 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/IronSoulElf',
1653 'uploader': 'IronSoulElf',
1654 'creator': 'Todd Haberman;\nDaniel Law Heath and Aaron Kaplan',
1655 'track': 'Dark Walk',
1656 'artist': 'Todd Haberman;\nDaniel Law Heath and Aaron Kaplan',
1657 'album': 'Position Music - Production Music Vol. 143 - Dark Walk',
1658 'thumbnail': 'https://i.ytimg.com/vi_webp/lsguqyKfVQg/maxresdefault.webp',
1659 'categories': ['Film & Animation'],
1661 'live_status': 'not_live',
1662 'channel_url': 'https://www.youtube.com/channel/UCTSRgz5jylBvFt_S7wnsqLQ',
1663 'channel_id': 'UCTSRgz5jylBvFt_S7wnsqLQ',
1665 'availability': 'public',
1666 'channel': 'IronSoulElf',
1667 'playable_in_embed': True,
1670 'channel_follower_count': int
1673 'skip_download': True,
1677 # Tags with '};' (see https://github.com/ytdl-org/youtube-dl/issues/7468)
1678 'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
1679 'only_matching': True,
1682 # Video with yt:stretch=17:0
1683 'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
1685 'id': 'Q39EVAstoRM',
1687 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
1688 'description': 'md5:ee18a25c350637c8faff806845bddee9',
1689 'upload_date': '20151107',
1690 'uploader_id': 'UCCr7TALkRbo3EtFzETQF1LA',
1691 'uploader': 'CH GAMER DROID',
1694 'skip_download': True,
1696 'skip': 'This video does not exist.',
1699 # Video with incomplete 'yt:stretch=16:'
1700 'url': 'https://www.youtube.com/watch?v=FRhJzUSJbGI',
1701 'only_matching': True,
1704 # Video licensed under Creative Commons
1705 'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
1707 'id': 'M4gD1WSo5mA',
1709 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
1710 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
1712 'upload_date': '20150128',
1713 'uploader_id': 'BerkmanCenter',
1714 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/BerkmanCenter',
1715 'uploader': 'The Berkman Klein Center for Internet & Society',
1716 'license': 'Creative Commons Attribution license (reuse allowed)',
1717 'channel_id': 'UCuLGmD72gJDBwmLw06X58SA',
1718 'channel_url': 'https://www.youtube.com/channel/UCuLGmD72gJDBwmLw06X58SA',
1721 'tags': ['Copyright (Legal Subject)', 'Law (Industry)', 'William W. Fisher (Author)'],
1722 'channel': 'The Berkman Klein Center for Internet & Society',
1723 'availability': 'public',
1725 'categories': ['Education'],
1726 'thumbnail': 'https://i.ytimg.com/vi_webp/M4gD1WSo5mA/maxresdefault.webp',
1727 'live_status': 'not_live',
1728 'playable_in_embed': True,
1729 'comment_count': int,
1730 'channel_follower_count': int
1733 'skip_download': True,
1737 # Channel-like uploader_url
1738 'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
1740 'id': 'eQcmzGIKrzg',
1742 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
1743 'description': 'md5:13a2503d7b5904ef4b223aa101628f39',
1745 'upload_date': '20151120',
1746 'uploader': 'Bernie Sanders',
1747 'uploader_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
1748 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
1749 'license': 'Creative Commons Attribution license (reuse allowed)',
1750 'playable_in_embed': True,
1753 'channel_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
1755 'availability': 'public',
1756 'categories': ['News & Politics'],
1757 'channel': 'Bernie Sanders',
1758 'thumbnail': 'https://i.ytimg.com/vi_webp/eQcmzGIKrzg/maxresdefault.webp',
1760 'live_status': 'not_live',
1761 'channel_url': 'https://www.youtube.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
1762 'comment_count': int,
1763 'channel_follower_count': int
1766 'skip_download': True,
1770 'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;v=V36LpHqtcDY',
1771 'only_matching': True,
1774 # YouTube Red paid video (https://github.com/ytdl-org/youtube-dl/issues/10059)
1775 'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
1776 'only_matching': True,
1779 # Rental video preview
1780 'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
1782 'id': 'uGpuVWrhIzE',
1784 'title': 'Piku - Trailer',
1785 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
1786 'upload_date': '20150811',
1787 'uploader': 'FlixMatrix',
1788 'uploader_id': 'FlixMatrixKaravan',
1789 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/FlixMatrixKaravan',
1790 'license': 'Standard YouTube License',
1793 'skip_download': True,
1795 'skip': 'This video is not available.',
1798 # YouTube Red video with episode data
1799 'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
1801 'id': 'iqKdEhx-dD4',
1803 'title': 'Isolation - Mind Field (Ep 1)',
1804 'description': 'md5:f540112edec5d09fc8cc752d3d4ba3cd',
1806 'upload_date': '20170118',
1807 'uploader': 'Vsauce',
1808 'uploader_id': 'Vsauce',
1809 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Vsauce',
1810 'series': 'Mind Field',
1812 'episode_number': 1,
1813 'thumbnail': 'https://i.ytimg.com/vi_webp/iqKdEhx-dD4/maxresdefault.webp',
1816 'availability': 'public',
1818 'channel': 'Vsauce',
1819 'episode': 'Episode 1',
1820 'categories': ['Entertainment'],
1821 'season': 'Season 1',
1822 'channel_id': 'UC6nSFpj9HTCZ5t-N3Rm3-HA',
1823 'channel_url': 'https://www.youtube.com/channel/UC6nSFpj9HTCZ5t-N3Rm3-HA',
1825 'playable_in_embed': True,
1826 'live_status': 'not_live',
1827 'channel_follower_count': int
1830 'skip_download': True,
1832 'expected_warnings': [
1833 'Skipping DASH manifest',
1837 # The following content has been identified by the YouTube community
1838 # as inappropriate or offensive to some audiences.
1839 'url': 'https://www.youtube.com/watch?v=6SJNVb0GnPI',
1841 'id': '6SJNVb0GnPI',
1843 'title': 'Race Differences in Intelligence',
1844 'description': 'md5:5d161533167390427a1f8ee89a1fc6f1',
1846 'upload_date': '20140124',
1847 'uploader': 'New Century Foundation',
1848 'uploader_id': 'UCEJYpZGqgUob0zVVEaLhvVg',
1849 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCEJYpZGqgUob0zVVEaLhvVg',
1852 'skip_download': True,
1854 'skip': 'This video has been removed for violating YouTube\'s policy on hate speech.',
1858 'url': '1t24XAntNCY',
1859 'only_matching': True,
1862 # geo restricted to JP
1863 'url': 'sJL6WA-aGkQ',
1864 'only_matching': True,
1867 'url': 'https://invidio.us/watch?v=BaW_jenozKc',
1868 'only_matching': True,
1871 'url': 'https://redirect.invidious.io/watch?v=BaW_jenozKc',
1872 'only_matching': True,
1875 # from https://nitter.pussthecat.org/YouTube/status/1360363141947944964#m
1876 'url': 'https://redirect.invidious.io/Yh0AhrY9GjA',
1877 'only_matching': True,
1881 'url': 'https://www.youtube.com/watch?v=s7_qI6_mIXc',
1882 'only_matching': True,
1885 # Video with unsupported adaptive stream type formats
1886 'url': 'https://www.youtube.com/watch?v=Z4Vy8R84T1U',
1888 'id': 'Z4Vy8R84T1U',
1890 'title': 'saman SMAN 53 Jakarta(Sancety) opening COFFEE4th at SMAN 53 Jakarta',
1891 'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
1893 'upload_date': '20130923',
1894 'uploader': 'Amelia Putri Harwita',
1895 'uploader_id': 'UCpOxM49HJxmC1qCalXyB3_Q',
1896 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCpOxM49HJxmC1qCalXyB3_Q',
1897 'formats': 'maxcount:10',
1900 'skip_download': True,
1901 'youtube_include_dash_manifest': False,
1903 'skip': 'not actual anymore',
1906 # Youtube Music Auto-generated description
1907 'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
1909 'id': 'MgNrAu2pzNs',
1911 'title': 'Voyeur Girl',
1912 'description': 'md5:7ae382a65843d6df2685993e90a8628f',
1913 'upload_date': '20190312',
1914 'uploader': 'Stephen - Topic',
1915 'uploader_id': 'UC-pWHpBjdGG69N9mM2auIAA',
1916 'artist': 'Stephen',
1917 'track': 'Voyeur Girl',
1918 'album': 'it\'s too much love to know my dear',
1919 'release_date': '20190313',
1920 'release_year': 2019,
1921 'alt_title': 'Voyeur Girl',
1923 'uploader_url': 'http://www.youtube.com/channel/UC-pWHpBjdGG69N9mM2auIAA',
1924 'playable_in_embed': True,
1926 'categories': ['Music'],
1927 'channel_url': 'https://www.youtube.com/channel/UC-pWHpBjdGG69N9mM2auIAA',
1928 'channel': 'Stephen',
1929 'availability': 'public',
1930 'creator': 'Stephen',
1932 'thumbnail': 'https://i.ytimg.com/vi_webp/MgNrAu2pzNs/maxresdefault.webp',
1934 'channel_id': 'UC-pWHpBjdGG69N9mM2auIAA',
1936 'live_status': 'not_live',
1937 'channel_follower_count': int
1940 'skip_download': True,
1944 'url': 'https://www.youtubekids.com/watch?v=3b8nCWDgZ6Q',
1945 'only_matching': True,
1948 # invalid -> valid video id redirection
1949 'url': 'DJztXj2GPfl',
1951 'id': 'DJztXj2GPfk',
1953 'title': 'Panjabi MC - Mundian To Bach Ke (The Dictator Soundtrack)',
1954 'description': 'md5:bf577a41da97918e94fa9798d9228825',
1955 'upload_date': '20090125',
1956 'uploader': 'Prochorowka',
1957 'uploader_id': 'Prochorowka',
1958 'uploader_url': r're:https?://(?:www\.)?youtube\.com/user/Prochorowka',
1959 'artist': 'Panjabi MC',
1960 'track': 'Beware of the Boys (Mundian to Bach Ke) - Motivo Hi-Lectro Remix',
1961 'album': 'Beware of the Boys (Mundian To Bach Ke)',
1964 'skip_download': True,
1966 'skip': 'Video unavailable',
1969 # empty description results in an empty string
1970 'url': 'https://www.youtube.com/watch?v=x41yOUIvK2k',
1972 'id': 'x41yOUIvK2k',
1974 'title': 'IMG 3456',
1976 'upload_date': '20170613',
1977 'uploader_id': 'ElevageOrVert',
1978 'uploader': 'ElevageOrVert',
1980 'thumbnail': 'https://i.ytimg.com/vi_webp/x41yOUIvK2k/maxresdefault.webp',
1981 'uploader_url': 'http://www.youtube.com/user/ElevageOrVert',
1983 'channel_id': 'UCo03ZQPBW5U4UC3regpt1nw',
1985 'channel_url': 'https://www.youtube.com/channel/UCo03ZQPBW5U4UC3regpt1nw',
1986 'availability': 'public',
1988 'categories': ['Pets & Animals'],
1990 'playable_in_embed': True,
1991 'live_status': 'not_live',
1992 'channel': 'ElevageOrVert',
1993 'channel_follower_count': int
1996 'skip_download': True,
2000 # with '};' inside yt initial data (see [1])
2001 # see [2] for an example with '};' inside ytInitialPlayerResponse
2002 # 1. https://github.com/ytdl-org/youtube-dl/issues/27093
2003 # 2. https://github.com/ytdl-org/youtube-dl/issues/27216
2004 'url': 'https://www.youtube.com/watch?v=CHqg6qOn4no',
2006 'id': 'CHqg6qOn4no',
2008 'title': 'Part 77 Sort a list of simple types in c#',
2009 'description': 'md5:b8746fa52e10cdbf47997903f13b20dc',
2010 'upload_date': '20130831',
2011 'uploader_id': 'kudvenkat',
2012 'uploader': 'kudvenkat',
2013 'channel_id': 'UCCTVrRB5KpIiK6V2GGVsR1Q',
2015 'uploader_url': 'http://www.youtube.com/user/kudvenkat',
2016 'channel_url': 'https://www.youtube.com/channel/UCCTVrRB5KpIiK6V2GGVsR1Q',
2017 'live_status': 'not_live',
2018 'categories': ['Education'],
2019 'availability': 'public',
2020 'thumbnail': 'https://i.ytimg.com/vi/CHqg6qOn4no/sddefault.jpg',
2022 'playable_in_embed': True,
2026 'channel': 'kudvenkat',
2027 'comment_count': int,
2028 'channel_follower_count': int
2031 'skip_download': True,
2035 # another example of '};' in ytInitialData
2036 'url': 'https://www.youtube.com/watch?v=gVfgbahppCY',
2037 'only_matching': True,
2040 'url': 'https://www.youtube.com/watch_popup?v=63RmMXCd_bQ',
2041 'only_matching': True,
2044 # https://github.com/ytdl-org/youtube-dl/pull/28094
2045 'url': 'OtqTfy26tG0',
2047 'id': 'OtqTfy26tG0',
2049 'title': 'Burn Out',
2050 'description': 'md5:8d07b84dcbcbfb34bc12a56d968b6131',
2051 'upload_date': '20141120',
2052 'uploader': 'The Cinematic Orchestra - Topic',
2053 'uploader_id': 'UCIzsJBIyo8hhpFm1NK0uLgw',
2054 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UCIzsJBIyo8hhpFm1NK0uLgw',
2055 'artist': 'The Cinematic Orchestra',
2056 'track': 'Burn Out',
2057 'album': 'Every Day',
2059 'live_status': 'not_live',
2060 'alt_title': 'Burn Out',
2064 'channel_url': 'https://www.youtube.com/channel/UCIzsJBIyo8hhpFm1NK0uLgw',
2065 'creator': 'The Cinematic Orchestra',
2066 'channel': 'The Cinematic Orchestra',
2067 'tags': ['The Cinematic Orchestra', 'Every Day', 'Burn Out'],
2068 'channel_id': 'UCIzsJBIyo8hhpFm1NK0uLgw',
2069 'availability': 'public',
2070 'thumbnail': 'https://i.ytimg.com/vi/OtqTfy26tG0/maxresdefault.jpg',
2071 'categories': ['Music'],
2072 'playable_in_embed': True,
2073 'channel_follower_count': int
2076 'skip_download': True,
2080 # controversial video, only works with bpctr when authenticated with cookies
2081 'url': 'https://www.youtube.com/watch?v=nGC3D_FkCmg',
2082 'only_matching': True,
2085 # controversial video, requires bpctr/contentCheckOk
2086 'url': 'https://www.youtube.com/watch?v=SZJvDhaSDnc',
2088 'id': 'SZJvDhaSDnc',
2090 'title': 'San Diego teen commits suicide after bullying over embarrassing video',
2091 'channel_id': 'UC-SJ6nODDmufqBzPBwCvYvQ',
2092 'uploader': 'CBS Mornings',
2093 'uploader_id': 'CBSThisMorning',
2094 'upload_date': '20140716',
2095 'description': 'md5:acde3a73d3f133fc97e837a9f76b53b7',
2097 'categories': ['News & Politics'],
2098 'uploader_url': 'http://www.youtube.com/user/CBSThisMorning',
2100 'channel': 'CBS Mornings',
2101 'tags': ['suicide', 'bullying', 'video', 'cbs', 'news'],
2102 'thumbnail': 'https://i.ytimg.com/vi/SZJvDhaSDnc/hqdefault.jpg',
2104 'availability': 'needs_auth',
2105 'channel_url': 'https://www.youtube.com/channel/UC-SJ6nODDmufqBzPBwCvYvQ',
2107 'live_status': 'not_live',
2108 'playable_in_embed': True,
2109 'channel_follower_count': int
2113 # restricted location, https://github.com/ytdl-org/youtube-dl/issues/28685
2114 'url': 'cBvYw8_A0vQ',
2116 'id': 'cBvYw8_A0vQ',
2118 'title': '4K Ueno Okachimachi Street Scenes 上野御徒町歩き',
2119 'description': 'md5:ea770e474b7cd6722b4c95b833c03630',
2120 'upload_date': '20201120',
2121 'uploader': 'Walk around Japan',
2122 'uploader_id': 'UC3o_t8PzBmXf5S9b7GLx1Mw',
2123 'uploader_url': r're:https?://(?:www\.)?youtube\.com/channel/UC3o_t8PzBmXf5S9b7GLx1Mw',
2125 'categories': ['Travel & Events'],
2126 'channel_id': 'UC3o_t8PzBmXf5S9b7GLx1Mw',
2128 'channel': 'Walk around Japan',
2129 'tags': ['Ueno Tokyo', 'Okachimachi Tokyo', 'Ameyoko Street', 'Tokyo attraction', 'Travel in Tokyo'],
2130 'thumbnail': 'https://i.ytimg.com/vi_webp/cBvYw8_A0vQ/hqdefault.webp',
2132 'availability': 'public',
2133 'channel_url': 'https://www.youtube.com/channel/UC3o_t8PzBmXf5S9b7GLx1Mw',
2134 'live_status': 'not_live',
2135 'playable_in_embed': True,
2136 'channel_follower_count': int
2139 'skip_download': True,
2142 # Has multiple audio streams
2143 'url': 'WaOKSUlf4TM',
2144 'only_matching': True
2146 # Requires Premium: has format 141 when requested using YTM url
2147 'url': 'https://music.youtube.com/watch?v=XclachpHxis',
2148 'only_matching': True
2150 # multiple subtitles with same lang_code
2151 'url': 'https://www.youtube.com/watch?v=wsQiKKfKxug',
2152 'only_matching': True,
2154 # Force use android client fallback
2155 'url': 'https://www.youtube.com/watch?v=YOelRv7fMxY',
2157 'id': 'YOelRv7fMxY',
2158 'title': 'DIGGING A SECRET TUNNEL Part 1',
2160 'upload_date': '20210624',
2161 'channel_id': 'UCp68_FLety0O-n9QU6phsgw',
2162 'uploader': 'colinfurze',
2163 'uploader_id': 'colinfurze',
2164 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCp68_FLety0O-n9QU6phsgw',
2165 'description': 'md5:5d5991195d599b56cd0c4148907eec50',
2167 'categories': ['Entertainment'],
2168 'uploader_url': 'http://www.youtube.com/user/colinfurze',
2170 'channel': 'colinfurze',
2171 'tags': ['Colin', 'furze', 'Terry', 'tunnel', 'underground', 'bunker'],
2172 'thumbnail': 'https://i.ytimg.com/vi/YOelRv7fMxY/maxresdefault.jpg',
2174 'availability': 'public',
2176 'live_status': 'not_live',
2177 'playable_in_embed': True,
2178 'channel_follower_count': int
2181 'format': '17', # 3gp format available on android
2182 'extractor_args': {'youtube': {'player_client': ['android']}},
2186 # Skip download of additional client configs (remix client config in this case)
2187 'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
2188 'only_matching': True,
2190 'extractor_args': {'youtube': {'player_skip': ['configs']}},
2194 'url': 'https://www.youtube.com/shorts/BGQWPY4IigY',
2195 'only_matching': True,
2197 'note': 'Storyboards',
2198 'url': 'https://www.youtube.com/watch?v=5KLPxDtMqe8',
2200 'id': '5KLPxDtMqe8',
2203 'title': 'Your Brain is Plastic',
2204 'uploader_id': 'scishow',
2205 'description': 'md5:89cd86034bdb5466cd87c6ba206cd2bc',
2206 'upload_date': '20140324',
2207 'uploader': 'SciShow',
2209 'channel_id': 'UCZYTClx2T1of7BRZ86-8fow',
2210 'channel_url': 'https://www.youtube.com/channel/UCZYTClx2T1of7BRZ86-8fow',
2212 'thumbnail': 'https://i.ytimg.com/vi/5KLPxDtMqe8/maxresdefault.jpg',
2213 'playable_in_embed': True,
2215 'uploader_url': 'http://www.youtube.com/user/scishow',
2216 'availability': 'public',
2217 'channel': 'SciShow',
2218 'live_status': 'not_live',
2220 'categories': ['Education'],
2222 'channel_follower_count': int
2223 }, 'params': {'format': 'mhtml', 'skip_download': True}
2225 # Ensure video upload_date is in UTC timezone (video was uploaded 1641170939)
2226 'url': 'https://www.youtube.com/watch?v=2NUZ8W2llS4',
2228 'id': '2NUZ8W2llS4',
2230 'title': 'The NP that test your phone performance 🙂',
2231 'description': 'md5:144494b24d4f9dfacb97c1bbef5de84d',
2232 'uploader': 'Leon Nguyen',
2233 'uploader_id': 'VNSXIII',
2234 'uploader_url': 'http://www.youtube.com/user/VNSXIII',
2235 'channel_id': 'UCRqNBSOHgilHfAczlUmlWHA',
2236 'channel_url': 'https://www.youtube.com/channel/UCRqNBSOHgilHfAczlUmlWHA',
2240 'categories': ['Gaming'],
2242 'playable_in_embed': True,
2243 'live_status': 'not_live',
2244 'upload_date': '20220103',
2246 'availability': 'public',
2247 'channel': 'Leon Nguyen',
2248 'thumbnail': 'https://i.ytimg.com/vi_webp/2NUZ8W2llS4/maxresdefault.webp',
2249 'comment_count': int,
2250 'channel_follower_count': int
2253 # Same video as above, but with --compat-opt no-youtube-prefer-utc-upload-date
2254 'url': 'https://www.youtube.com/watch?v=2NUZ8W2llS4',
2256 'id': '2NUZ8W2llS4',
2258 'title': 'The NP that test your phone performance 🙂',
2259 'description': 'md5:144494b24d4f9dfacb97c1bbef5de84d',
2260 'uploader': 'Leon Nguyen',
2261 'uploader_id': 'VNSXIII',
2262 'uploader_url': 'http://www.youtube.com/user/VNSXIII',
2263 'channel_id': 'UCRqNBSOHgilHfAczlUmlWHA',
2264 'channel_url': 'https://www.youtube.com/channel/UCRqNBSOHgilHfAczlUmlWHA',
2268 'categories': ['Gaming'],
2270 'playable_in_embed': True,
2271 'live_status': 'not_live',
2272 'upload_date': '20220102',
2274 'availability': 'public',
2275 'channel': 'Leon Nguyen',
2276 'thumbnail': 'https://i.ytimg.com/vi_webp/2NUZ8W2llS4/maxresdefault.webp',
2277 'comment_count': int,
2278 'channel_follower_count': int
2280 'params': {'compat_opts': ['no-youtube-prefer-utc-upload-date']}
2282 # date text is premiered video, ensure upload date in UTC (published 1641172509)
2283 'url': 'https://www.youtube.com/watch?v=mzZzzBU6lrM',
2285 'id': 'mzZzzBU6lrM',
2287 'title': 'I Met GeorgeNotFound In Real Life...',
2288 'description': 'md5:cca98a355c7184e750f711f3a1b22c84',
2289 'uploader': 'Quackity',
2290 'uploader_id': 'QuackityHQ',
2291 'uploader_url': 'http://www.youtube.com/user/QuackityHQ',
2292 'channel_id': 'UC_8NknAFiyhOUaZqHR3lq3Q',
2293 'channel_url': 'https://www.youtube.com/channel/UC_8NknAFiyhOUaZqHR3lq3Q',
2297 'categories': ['Entertainment'],
2299 'playable_in_embed': True,
2300 'live_status': 'not_live',
2301 'release_timestamp': 1641172509,
2302 'release_date': '20220103',
2303 'upload_date': '20220103',
2305 'availability': 'public',
2306 'channel': 'Quackity',
2307 'thumbnail': 'https://i.ytimg.com/vi/mzZzzBU6lrM/maxresdefault.jpg',
2308 'channel_follower_count': int
2311 { # continuous livestream. Microformat upload date should be preferred.
2312 # Upload date was 2021-06-19 (not UTC), while stream start is 2021-11-27
2313 'url': 'https://www.youtube.com/watch?v=kgx4WGK0oNU',
2315 'id': 'kgx4WGK0oNU',
2316 'title': r're:jazz\/lofi hip hop radio🌱chill beats to relax\/study to \[LIVE 24\/7\] \d{4}-\d{2}-\d{2} \d{2}:\d{2}',
2318 'channel_id': 'UC84whx2xxsiA1gXHXXqKGOA',
2319 'availability': 'public',
2321 'release_timestamp': 1637975704,
2322 'upload_date': '20210619',
2323 'channel_url': 'https://www.youtube.com/channel/UC84whx2xxsiA1gXHXXqKGOA',
2324 'live_status': 'is_live',
2325 'thumbnail': 'https://i.ytimg.com/vi/kgx4WGK0oNU/maxresdefault.jpg',
2326 'uploader': '阿鲍Abao',
2327 'uploader_url': 'http://www.youtube.com/channel/UC84whx2xxsiA1gXHXXqKGOA',
2328 'channel': 'Abao in Tokyo',
2329 'channel_follower_count': int,
2330 'release_date': '20211127',
2332 'categories': ['People & Blogs'],
2334 'uploader_id': 'UC84whx2xxsiA1gXHXXqKGOA',
2336 'playable_in_embed': True,
2337 'description': 'md5:2ef1d002cad520f65825346e2084e49d',
2339 'params': {'skip_download': True}
2341 # Story. Requires specific player params to work.
2342 'url': 'https://www.youtube.com/watch?v=vv8qTUWmulI',
2344 'id': 'vv8qTUWmulI',
2346 'availability': 'unlisted',
2348 'channel_id': 'UCzIZ8HrzDgc-pNQDUG6avBA',
2349 'upload_date': '20220526',
2350 'categories': ['Education'],
2352 'channel': 'IT\'S HISTORY',
2354 'uploader_id': 'BlastfromthePast',
2356 'uploader': 'IT\'S HISTORY',
2357 'playable_in_embed': True,
2359 'live_status': 'not_live',
2361 'thumbnail': 'https://i.ytimg.com/vi_webp/vv8qTUWmulI/maxresdefault.webp',
2362 'uploader_url': 'http://www.youtube.com/user/BlastfromthePast',
2363 'channel_url': 'https://www.youtube.com/channel/UCzIZ8HrzDgc-pNQDUG6avBA',
2365 'skip': 'stories get removed after some period of time',
2367 'url': 'https://www.youtube.com/watch?v=tjjjtzRLHvA',
2369 'id': 'tjjjtzRLHvA',
2371 'title': 'ハッシュタグ無し };if window.ytcsi',
2372 'upload_date': '20220323',
2374 'availability': 'unlisted',
2375 'channel': 'nao20010128nao',
2376 'thumbnail': 'https://i.ytimg.com/vi_webp/tjjjtzRLHvA/maxresdefault.webp',
2378 'uploader': 'nao20010128nao',
2379 'uploader_id': 'nao20010128nao',
2380 'categories': ['Music'],
2383 'channel_url': 'https://www.youtube.com/channel/UCdqltm_7iv1Vs6kp6Syke5A',
2384 'channel_id': 'UCdqltm_7iv1Vs6kp6Syke5A',
2385 'live_status': 'not_live',
2386 'playable_in_embed': True,
2387 'channel_follower_count': int,
2390 'uploader_url': 'http://www.youtube.com/user/nao20010128nao',
2393 # Prefer primary title+description language metadata by default
2394 # Do not prefer translated description if primary is empty
2395 'url': 'https://www.youtube.com/watch?v=el3E4MbxRqQ',
2397 'id': 'el3E4MbxRqQ',
2399 'title': 'dlp test video 2 - primary sv no desc',
2401 'channel': 'cole-dlp-test-acc',
2404 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2406 'playable_in_embed': True,
2407 'availability': 'unlisted',
2408 'thumbnail': 'https://i.ytimg.com/vi_webp/el3E4MbxRqQ/maxresdefault.webp',
2411 'uploader_id': 'UCiu-3thuViMebBjw_5nWYrA',
2412 'uploader_url': 'http://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2413 'live_status': 'not_live',
2414 'upload_date': '20220908',
2415 'categories': ['People & Blogs'],
2416 'uploader': 'cole-dlp-test-acc',
2417 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
2419 'params': {'skip_download': True}
2421 # Extractor argument: prefer translated title+description
2422 'url': 'https://www.youtube.com/watch?v=gHKT4uU8Zng',
2424 'id': 'gHKT4uU8Zng',
2426 'channel': 'cole-dlp-test-acc',
2429 'live_status': 'not_live',
2430 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
2431 'upload_date': '20220728',
2432 'uploader_id': 'UCiu-3thuViMebBjw_5nWYrA',
2434 'categories': ['People & Blogs'],
2435 'thumbnail': 'https://i.ytimg.com/vi_webp/gHKT4uU8Zng/maxresdefault.webp',
2436 'title': 'dlp test video title translated (fr)',
2437 'availability': 'public',
2438 'uploader': 'cole-dlp-test-acc',
2440 'description': 'dlp test video description translated (fr)',
2441 'playable_in_embed': True,
2442 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2443 'uploader_url': 'http://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2445 'params': {'skip_download': True, 'extractor_args': {'youtube': {'lang': ['fr']}}},
2446 'expected_warnings': [r'Preferring "fr" translated fields'],
2448 'note': '6 channel audio',
2449 'url': 'https://www.youtube.com/watch?v=zgdo7-RRjgo',
2450 'only_matching': True,
2455 # YouTube <object> embed
2457 'url': 'http://www.improbable.com/2017/04/03/untrained-modern-youths-and-ancient-masters-in-selfie-portraits/',
2458 'md5': '873c81d308b979f0e23ee7e620b312a3',
2460 'id': 'msN87y-iEx0',
2462 'title': 'Feynman: Mirrors FUN TO IMAGINE 6',
2463 'upload_date': '20080526',
2464 'description': 'md5:873c81d308b979f0e23ee7e620b312a3',
2465 'uploader': 'Christopher Sykes',
2466 'uploader_id': 'ChristopherJSykes',
2468 'tags': ['feynman', 'mirror', 'science', 'physics', 'imagination', 'fun', 'cool', 'puzzle'],
2469 'channel_id': 'UCCeo--lls1vna5YJABWAcVA',
2470 'playable_in_embed': True,
2471 'thumbnail': 'https://i.ytimg.com/vi/msN87y-iEx0/hqdefault.jpg',
2473 'comment_count': int,
2474 'channel': 'Christopher Sykes',
2475 'live_status': 'not_live',
2476 'channel_url': 'https://www.youtube.com/channel/UCCeo--lls1vna5YJABWAcVA',
2477 'availability': 'public',
2480 'categories': ['Science & Technology'],
2481 'channel_follower_count': int,
2482 'uploader_url': 'http://www.youtube.com/user/ChristopherJSykes',
2485 'skip_download': True,
2491 def suitable(cls, url):
2492 from ..utils import parse_qs
2495 if qs.get('list', [None])[0]:
2497 return super().suitable(url)
2499 def __init__(self, *args, **kwargs):
2500 super().__init__(*args, **kwargs)
2501 self._code_cache = {}
2502 self._player_cache = {}
2504 def _prepare_live_from_start_formats(self, formats, video_id, live_start_time, url, webpage_url, smuggled_data):
2505 lock = threading.Lock()
2508 start_time = time.time()
2509 formats = [f for f in formats if f.get('is_from_start')]
2511 def refetch_manifest(format_id, delay):
2512 nonlocal formats, start_time, is_live
2513 if time.time() <= start_time + delay:
2516 _, _, prs, player_url = self._download_player_responses(url, smuggled_data, video_id, webpage_url)
2517 video_details = traverse_obj(
2518 prs, (..., 'videoDetails'), expected_type=dict, default=[])
2519 microformats = traverse_obj(
2520 prs, (..., 'microformat', 'playerMicroformatRenderer'),
2521 expected_type=dict, default=[])
2522 _, is_live, _, formats, _ = self._list_formats(video_id, microformats, video_details, prs, player_url)
2523 start_time = time.time()
2525 def mpd_feed(format_id, delay):
2527 @returns (manifest_url, manifest_stream_number, is_live) or None
2530 refetch_manifest(format_id, delay)
2532 f = next((f for f in formats if f['format_id'] == format_id), None)
2535 self.to_screen(f'{video_id}: Video is no longer live')
2537 self.report_warning(
2538 f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}')
2540 return f['manifest_url'], f['manifest_stream_number'], is_live
2544 f['protocol'] = 'http_dash_segments_generator'
2545 f['fragments'] = functools.partial(
2546 self._live_dash_fragments, f['format_id'], live_start_time, mpd_feed)
2548 def _live_dash_fragments(self, format_id, live_start_time, mpd_feed, ctx):
2549 FETCH_SPAN, MAX_DURATION = 5, 432000
2551 mpd_url, stream_number, is_live = None, None, True
2554 download_start_time = ctx.get('start') or time.time()
2556 lack_early_segments = download_start_time - (live_start_time or download_start_time) > MAX_DURATION
2557 if lack_early_segments:
2558 self.report_warning(bug_reports_message(
2559 'Starting download from the last 120 hours of the live stream since '
2560 'YouTube does not have data before that. If you think this is wrong,'), only_once=True)
2561 lack_early_segments = True
2563 known_idx, no_fragment_score, last_segment_url = begin_index, 0, None
2564 fragments, fragment_base_url = None, None
2566 def _extract_sequence_from_mpd(refresh_sequence, immediate):
2567 nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
2568 # Obtain from MPD's maximum seq value
2569 old_mpd_url = mpd_url
2570 last_error = ctx.pop('last_error', None)
2571 expire_fast = immediate or last_error and isinstance(last_error, urllib.error.HTTPError) and last_error.code == 403
2572 mpd_url, stream_number, is_live = (mpd_feed(format_id, 5 if expire_fast else 18000)
2573 or (mpd_url, stream_number, False))
2574 if not refresh_sequence:
2575 if expire_fast and not is_live:
2576 return False, last_seq
2577 elif old_mpd_url == mpd_url:
2578 return True, last_seq
2580 fmts, _ = self._extract_mpd_formats_and_subtitles(
2581 mpd_url, None, note=False, errnote=False, fatal=False)
2582 except ExtractorError:
2585 no_fragment_score += 2
2586 return False, last_seq
2587 fmt_info = next(x for x in fmts if x['manifest_stream_number'] == stream_number)
2588 fragments = fmt_info['fragments']
2589 fragment_base_url = fmt_info['fragment_base_url']
2590 assert fragment_base_url
2592 _last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1))
2593 return True, _last_seq
2596 fetch_time = time.time()
2597 if no_fragment_score > 30:
2599 if last_segment_url:
2600 # Obtain from "X-Head-Seqnum" header value from each segment
2602 urlh = self._request_webpage(
2603 last_segment_url, None, note=False, errnote=False, fatal=False)
2604 except ExtractorError:
2606 last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum']))
2607 if last_seq is None:
2608 no_fragment_score += 2
2609 last_segment_url = None
2612 should_continue, last_seq = _extract_sequence_from_mpd(True, no_fragment_score > 15)
2613 no_fragment_score += 2
2614 if not should_continue:
2617 if known_idx > last_seq:
2618 last_segment_url = None
2623 if begin_index < 0 and known_idx < 0:
2624 # skip from the start when it's negative value
2625 known_idx = last_seq + begin_index
2626 if lack_early_segments:
2627 known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragments[-1]['duration']))
2629 for idx in range(known_idx, last_seq):
2630 # do not update sequence here or you'll get skipped some part of it
2631 should_continue, _ = _extract_sequence_from_mpd(False, False)
2632 if not should_continue:
2634 raise ExtractorError('breaking out of outer loop')
2635 last_segment_url = urljoin(fragment_base_url, 'sq/%d' % idx)
2637 'url': last_segment_url,
2638 'fragment_count': last_seq,
2640 if known_idx == last_seq:
2641 no_fragment_score += 5
2643 no_fragment_score = 0
2644 known_idx = last_seq
2645 except ExtractorError:
2648 time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
2650 def _extract_player_url(self, *ytcfgs, webpage=None):
2651 player_url = traverse_obj(
2652 ytcfgs, (..., 'PLAYER_JS_URL'), (..., 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'jsUrl'),
2653 get_all=False, expected_type=str)
2656 return urljoin('https://www.youtube.com', player_url)
2658 def _download_player_url(self, video_id, fatal=False):
2659 res = self._download_webpage(
2660 'https://www.youtube.com/iframe_api',
2661 note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
2663 player_version = self._search_regex(
2664 r'player\\?/([0-9a-fA-F]{8})\\?/', res, 'player version', fatal=fatal)
2666 return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
2668 def _signature_cache_id(self, example_sig):
2669 """ Return a string representation of a signature """
2670 return '.'.join(str(len(part)) for part in example_sig.split('.'))
2673 def _extract_player_info(cls, player_url):
2674 for player_re in cls._PLAYER_INFO_RE:
2675 id_m = re.search(player_re, player_url)
2679 raise ExtractorError('Cannot identify player %r' % player_url)
2680 return id_m.group('id')
2682 def _load_player(self, video_id, player_url, fatal=True):
2683 player_id = self._extract_player_info(player_url)
2684 if player_id not in self._code_cache:
2685 code = self._download_webpage(
2686 player_url, video_id, fatal=fatal,
2687 note='Downloading player ' + player_id,
2688 errnote='Download of %s failed' % player_url)
2690 self._code_cache[player_id] = code
2691 return self._code_cache.get(player_id)
2693 def _extract_signature_function(self, video_id, player_url, example_sig):
2694 player_id = self._extract_player_info(player_url)
2696 # Read from filesystem cache
2697 func_id = f'js_{player_id}_{self._signature_cache_id(example_sig)}'
2698 assert os.path.basename(func_id) == func_id
2700 self.write_debug(f'Extracting signature function {func_id}')
2701 cache_spec, code = self.cache.load('youtube-sigfuncs', func_id), None
2704 code = self._load_player(video_id, player_url)
2706 res = self._parse_sig_js(code)
2707 test_string = ''.join(map(chr, range(len(example_sig))))
2708 cache_spec = [ord(c) for c in res(test_string)]
2709 self.cache.store('youtube-sigfuncs', func_id, cache_spec)
2711 return lambda s: ''.join(s[i] for i in cache_spec)
2713 def _print_sig_code(self, func, example_sig):
2714 if not self.get_param('youtube_print_sig_code'):
2717 def gen_sig_code(idxs):
2718 def _genslice(start, end, step):
2719 starts = '' if start == 0 else str(start)
2720 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
2721 steps = '' if step == 1 else (':%d' % step)
2722 return f's[{starts}{ends}{steps}]'
2725 # Quelch pyflakes warnings - start will be set when step is set
2726 start = '(Never used)'
2727 for i, prev in zip(idxs[1:], idxs[:-1]):
2728 if step is not None:
2729 if i - prev == step:
2731 yield _genslice(start, prev, step)
2734 if i - prev in [-1, 1]:
2739 yield 's[%d]' % prev
2743 yield _genslice(start, i, step)
2745 test_string = ''.join(map(chr, range(len(example_sig))))
2746 cache_res = func(test_string)
2747 cache_spec = [ord(c) for c in cache_res]
2748 expr_code = ' + '.join(gen_sig_code(cache_spec))
2749 signature_id_tuple = '(%s)' % (
2750 ', '.join(str(len(p)) for p in example_sig.split('.')))
2751 code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
2752 ' return %s\n') % (signature_id_tuple, expr_code)
2753 self.to_screen('Extracted signature function:\n' + code)
2755 def _parse_sig_js(self, jscode):
2756 funcname = self._search_regex(
2757 (r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2758 r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2759 r'\bm=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)',
2760 r'\bc&&\(c=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(c\)\)',
2761 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*\);[a-zA-Z0-9$]{2}\.[a-zA-Z0-9$]{2}\(a,\d+\)',
2762 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*\)',
2763 r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
2765 r'(["\'])signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2766 r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
2767 r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2768 r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2769 r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2770 r'\bc\s*&&\s*a\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2771 r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2772 r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\('),
2773 jscode, 'Initial JS player signature function name', group='sig')
2775 jsi = JSInterpreter(jscode)
2776 initial_function = jsi.extract_function(funcname)
2777 return lambda s: initial_function([s])
2779 def _cached(self, func, *cache_id):
2780 def inner(*args, **kwargs):
2781 if cache_id not in self._player_cache:
2783 self._player_cache[cache_id] = func(*args, **kwargs)
2784 except ExtractorError as e:
2785 self._player_cache[cache_id] = e
2786 except Exception as e:
2787 self._player_cache[cache_id] = ExtractorError(traceback.format_exc(), cause=e)
2789 ret = self._player_cache[cache_id]
2790 if isinstance(ret, Exception):
2795 def _decrypt_signature(self, s, video_id, player_url):
2796 """Turn the encrypted s field into a working signature"""
2797 extract_sig = self._cached(
2798 self._extract_signature_function, 'sig', player_url, self._signature_cache_id(s))
2799 func = extract_sig(video_id, player_url, s)
2800 self._print_sig_code(func, s)
2803 def _decrypt_nsig(self, s, video_id, player_url):
2804 """Turn the encrypted n field into a working signature"""
2805 if player_url is None:
2806 raise ExtractorError('Cannot decrypt nsig without player_url')
2807 player_url = urljoin('https://www.youtube.com', player_url)
2810 jsi, player_id, func_code = self._extract_n_function_code(video_id, player_url)
2811 except ExtractorError as e:
2812 raise ExtractorError('Unable to extract nsig function code', cause=e)
2813 if self.get_param('youtube_print_sig_code'):
2814 self.to_screen(f'Extracted nsig function from {player_id}:\n{func_code[1]}\n')
2817 extract_nsig = self._cached(self._extract_n_function_from_code, 'nsig func', player_url)
2818 ret = extract_nsig(jsi, func_code)(s)
2819 except JSInterpreter.Exception as e:
2821 jsi = PhantomJSwrapper(self, timeout=5000)
2822 except ExtractorError:
2824 self.report_warning(
2825 f'Native nsig extraction failed: Trying with PhantomJS\n'
2826 f' n = {s} ; player = {player_url}', video_id)
2829 args, func_body = func_code
2831 f'console.log(function({", ".join(args)}) {{ {func_body} }}({s!r}));',
2832 video_id=video_id, note='Executing signature code').strip()
2834 self.write_debug(f'Decrypted nsig {s} => {ret}')
2837 def _extract_n_function_name(self, jscode):
2838 funcname, idx = self._search_regex(
2839 r'\.get\("n"\)\)&&\(b=(?P<nfunc>[a-zA-Z0-9$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z0-9]\)',
2840 jscode, 'Initial JS player n function name', group=('nfunc', 'idx'))
2844 return json.loads(js_to_json(self._search_regex(
2845 rf'var {re.escape(funcname)}\s*=\s*(\[.+?\]);', jscode,
2846 f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
2848 def _extract_n_function_code(self, video_id, player_url):
2849 player_id = self._extract_player_info(player_url)
2850 func_code = self.cache.load('youtube-nsig', player_id, min_ver='2022.09.1')
2851 jscode = func_code or self._load_player(video_id, player_url)
2852 jsi = JSInterpreter(jscode)
2855 return jsi, player_id, func_code
2857 func_name = self._extract_n_function_name(jscode)
2860 func_code = self._search_regex(
2861 r'''(?xs
)%s\s
*=\s
*function\s
*\
((?P
<var
>[\w$
]+)\
)\s
*
2862 # NB: The end of the regex is intentionally kept strict
2863 {(?P<code>.+?}\s
*return\
[\w$
]+.join\
(""\
))};''' % func_name,
2864 jscode, 'nsig function', group=('var', 'code'), default=None)
2866 func_code = ([func_code[0]], func_code[1])
2868 self.write_debug('Extracting nsig function with jsinterp')
2869 func_code = jsi.extract_function_code(func_name)
2871 self.cache.store('youtube-nsig', player_id, func_code)
2872 return jsi, player_id, func_code
2874 def _extract_n_function_from_code(self, jsi, func_code):
2875 func = jsi.extract_function_from_code(*func_code)
2877 def extract_nsig(s):
2880 except JSInterpreter.Exception:
2882 except Exception as e:
2883 raise JSInterpreter.Exception(traceback.format_exc(), cause=e)
2885 if ret.startswith('enhanced_except_'):
2886 raise JSInterpreter.Exception('Signature function returned an exception')
2891 def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
2893 Extract signatureTimestamp (sts)
2894 Required to tell API what sig/player version is in use.
2897 if isinstance(ytcfg, dict):
2898 sts = int_or_none(ytcfg.get('STS'))
2901 # Attempt to extract from player
2902 if player_url is None:
2903 error_msg = 'Cannot extract signature timestamp without player_url.'
2905 raise ExtractorError(error_msg)
2906 self.report_warning(error_msg)
2908 code = self._load_player(video_id, player_url, fatal=fatal)
2910 sts = int_or_none(self._search_regex(
2911 r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
2912 'JS player signature timestamp', group='sts', fatal=fatal))
2915 def _mark_watched(self, video_id, player_responses):
2916 for is_full, key in enumerate(('videostatsPlaybackUrl', 'videostatsWatchtimeUrl')):
2917 label = 'fully ' if is_full else ''
2918 url = get_first(player_responses, ('playbackTracking', key, 'baseUrl'),
2919 expected_type=url_or_none)
2921 self.report_warning(f'Unable to mark {label}watched')
2923 parsed_url = urllib.parse.urlparse(url)
2924 qs = urllib.parse.parse_qs(parsed_url.query)
2926 # cpn generation algorithm is reverse engineered from base.js.
2927 # In fact it works even with dummy cpn.
2928 CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
2929 cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))
2931 # # more consistent results setting it to right before the end
2932 video_length = [str(float((qs.get('len') or ['1.5'])[0]) - 1)]
2937 'cmt': video_length,
2938 'el': 'detailpage', # otherwise defaults to "shorts"
2942 # these seem to mark watchtime "history" in the real world
2943 # they're required, so send in a single value
2949 url = urllib.parse.urlunparse(
2950 parsed_url._replace(query=urllib.parse.urlencode(qs, True)))
2952 self._download_webpage(
2953 url, video_id, f'Marking {label}watched',
2954 'Unable to mark watched', fatal=False)
2957 def _extract_from_webpage(cls, url, webpage):
2958 # Invidious Instances
2959 # https://github.com/yt-dlp/yt-dlp/issues/195
2960 # https://github.com/iv-org/invidious/pull/1730
2962 r'<link rel="alternate" href="(?P<url>https://www\.youtube\.com/watch\?v=[0-9A-Za-z_-]{11})"',
2965 yield cls.url_result(mobj.group('url'), cls)
2966 raise cls.StopExtraction()
2968 yield from super()._extract_from_webpage(url, webpage)
2970 # lazyYT YouTube embed
2971 for id_ in re.findall(r'class="lazyYT" data-youtube-id="([^"]+)"', webpage):
2972 yield cls.url_result(unescapeHTML(id_), cls, id_)
2974 # Wordpress "YouTube Video Importer" plugin
2975 for m in re.findall(r'''(?x
)<div
[^
>]+
2976 class=(?P
<q1
>[\'"])[^\'"]*\byvii
_single
_video
_player
\b[^
\'"]*(?P=q1)[^>]+
2977 data-video_id=(?P<q2>[\'"])([^
\'"]+)(?P=q2)''', webpage):
2978 yield cls.url_result(m[-1], cls, m[-1])
2981 def extract_id(cls, url):
2982 video_id = cls.get_temp_id(url)
2984 raise ExtractorError(f'Invalid URL: {url}')
2987 def _extract_chapters_from_json(self, data, duration):
2988 chapter_list = traverse_obj(
2990 'playerOverlays', 'playerOverlayRenderer', 'decoratedPlayerBarRenderer',
2991 'decoratedPlayerBarRenderer', 'playerBar', 'chapteredPlayerBarRenderer', 'chapters'
2992 ), expected_type=list)
2994 return self._extract_chapters(
2996 chapter_time=lambda chapter: float_or_none(
2997 traverse_obj(chapter, ('chapterRenderer', 'timeRangeStartMillis')), scale=1000),
2998 chapter_title=lambda chapter: traverse_obj(
2999 chapter, ('chapterRenderer', 'title', 'simpleText'), expected_type=str),
3002 def _extract_chapters_from_engagement_panel(self, data, duration):
3003 content_list = traverse_obj(
3005 ('engagementPanels', ..., 'engagementPanelSectionListRenderer', 'content', 'macroMarkersListRenderer', 'contents'),
3006 expected_type=list, default=[])
3007 chapter_time = lambda chapter: parse_duration(self._get_text(chapter, 'timeDescription'))
3008 chapter_title = lambda chapter: self._get_text(chapter, 'title')
3010 return next(filter(None, (
3011 self._extract_chapters(traverse_obj(contents, (..., 'macroMarkersListItemRenderer')),
3012 chapter_time, chapter_title, duration)
3013 for contents in content_list)), [])
3015 def _extract_chapters_from_description(self, description, duration):
3016 return self._extract_chapters(
3017 re.findall(r'(?m)^((?:\d+:)?\d{1,2}:\d{2})\b\W*\s(.+?)\s*$', description or ''),
3018 chapter_time=lambda x: parse_duration(x[0]), chapter_title=lambda x: x[1],
3019 duration=duration, strict=False)
3021 def _extract_chapters(self, chapter_list, chapter_time, chapter_title, duration, strict=True):
3025 'start_time': chapter_time(chapter),
3026 'title': chapter_title(chapter),
3027 } for chapter in chapter_list or []]
3029 chapter_list.sort(key=lambda c: c['start_time'] or 0)
3031 chapters = [{'start_time': 0}]
3032 for idx, chapter in enumerate(chapter_list):
3033 if chapter['start_time'] is None:
3034 self.report_warning(f'Incomplete chapter {idx}')
3035 elif chapters[-1]['start_time'] <= chapter['start_time'] <= duration:
3036 chapters.append(chapter)
3038 self.report_warning(f'Invalid start time for chapter "{chapter["title"]}
"')
3041 def _extract_comment(self, comment_renderer, parent=None):
3042 comment_id = comment_renderer.get('commentId')
3046 text = self._get_text(comment_renderer, 'contentText')
3048 # Timestamp is an estimate calculated from the current time and time_text
3049 time_text = self._get_text(comment_renderer, 'publishedTimeText') or ''
3050 timestamp = self._parse_time_text(time_text)
3052 author = self._get_text(comment_renderer, 'authorText')
3053 author_id = try_get(comment_renderer,
3054 lambda x: x['authorEndpoint']['browseEndpoint']['browseId'], str)
3056 votes = parse_count(try_get(comment_renderer, (lambda x: x['voteCount']['simpleText'],
3057 lambda x: x['likeCount']), str)) or 0
3058 author_thumbnail = try_get(comment_renderer,
3059 lambda x: x['authorThumbnail']['thumbnails'][-1]['url'], str)
3061 author_is_uploader = try_get(comment_renderer, lambda x: x['authorIsChannelOwner'], bool)
3062 is_favorited = 'creatorHeart' in (try_get(
3063 comment_renderer, lambda x: x['actionButtons']['commentActionButtonsRenderer'], dict) or {})
3067 'timestamp': timestamp,
3068 'time_text': time_text,
3069 'like_count': votes,
3070 'is_favorited': is_favorited,
3072 'author_id': author_id,
3073 'author_thumbnail': author_thumbnail,
3074 'author_is_uploader': author_is_uploader,
3075 'parent': parent or 'root'
3078 def _comment_entries(self, root_continuation_data, ytcfg, video_id, parent=None, tracker=None):
3080 get_single_config_arg = lambda c: self._configuration_arg(c, [''])[0]
3082 def extract_header(contents):
3083 _continuation = None
3084 for content in contents:
3085 comments_header_renderer = traverse_obj(content, 'commentsHeaderRenderer')
3086 expected_comment_count = self._get_count(
3087 comments_header_renderer, 'countText', 'commentsCount')
3089 if expected_comment_count:
3090 tracker['est_total'] = expected_comment_count
3091 self.to_screen(f'Downloading ~{expected_comment_count} comments')
3092 comment_sort_index = int(get_single_config_arg('comment_sort') != 'top') # 1 = new, 0 = top
3094 sort_menu_item = try_get(
3095 comments_header_renderer,
3096 lambda x: x['sortMenu']['sortFilterSubMenuRenderer']['subMenuItems'][comment_sort_index], dict) or {}
3097 sort_continuation_ep = sort_menu_item.get('serviceEndpoint') or {}
3099 _continuation = self._extract_continuation_ep_data(sort_continuation_ep) or self._extract_continuation(sort_menu_item)
3100 if not _continuation:
3103 sort_text = str_or_none(sort_menu_item.get('title'))
3105 sort_text = 'top comments' if comment_sort_index == 0 else 'newest first'
3106 self.to_screen('Sorting comments by %s' % sort_text.lower())
3108 return _continuation
3110 def extract_thread(contents):
3112 tracker['current_page_thread'] = 0
3113 for content in contents:
3114 if not parent and tracker['total_parent_comments'] >= max_parents:
3116 comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer'])
3117 comment_renderer = get_first(
3118 (comment_thread_renderer, content), [['commentRenderer', ('comment', 'commentRenderer')]],
3119 expected_type=dict, default={})
3121 comment = self._extract_comment(comment_renderer, parent)
3125 tracker['running_total'] += 1
3126 tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1
3129 # Attempt to get the replies
3130 comment_replies_renderer = try_get(
3131 comment_thread_renderer, lambda x: x['replies']['commentRepliesRenderer'], dict)
3133 if comment_replies_renderer:
3134 tracker['current_page_thread'] += 1
3135 comment_entries_iter = self._comment_entries(
3136 comment_replies_renderer, ytcfg, video_id,
3137 parent=comment.get('id'), tracker=tracker)
3138 yield from itertools.islice(comment_entries_iter, min(
3139 max_replies_per_thread, max(0, max_replies - tracker['total_reply_comments'])))
3141 # Keeps track of counts across recursive calls
3146 current_page_thread=0,
3147 total_parent_comments=0,
3148 total_reply_comments=0)
3151 # YouTube comments have a max depth of 2
3152 max_depth = int_or_none(get_single_config_arg('max_comment_depth'))
3154 self._downloader.deprecated_feature('[youtube] max_comment_depth extractor argument is deprecated. '
3155 'Set max replies in the max-comments extractor argument instead')
3156 if max_depth == 1 and parent:
3159 max_comments, max_parents, max_replies, max_replies_per_thread, *_ = map(
3160 lambda p: int_or_none(p, default=sys.maxsize), self._configuration_arg('max_comments', ) + [''] * 4)
3162 continuation = self._extract_continuation(root_continuation_data)
3165 is_forced_continuation = False
3166 is_first_continuation = parent is None
3167 if is_first_continuation and not continuation:
3168 # Sometimes you can get comments by generating the continuation yourself,
3169 # even if YouTube initially reports them being disabled - e.g. stories comments.
3170 # Note: if the comment section is actually disabled, YouTube may return a response with
3171 # required check_get_keys missing. So we will disable that check initially in this case.
3172 continuation = self._build_api_continuation_query(self._generate_comment_continuation(video_id))
3173 is_forced_continuation = True
3175 for page_num in itertools.count(0):
3176 if not continuation:
3178 headers = self.generate_api_headers(ytcfg=ytcfg, visitor_data=self._extract_visitor_data(response))
3179 comment_prog_str = f"({tracker['running_total']}
/{tracker['est_total']}
)"
3181 if is_first_continuation:
3182 note_prefix = 'Downloading comment section API JSON'
3184 note_prefix = ' Downloading comment API JSON reply thread %d %s' % (
3185 tracker['current_page_thread'], comment_prog_str)
3187 note_prefix = '%sDownloading comment%s API JSON page %d %s' % (
3188 ' ' if parent else '', ' replies' if parent else '',
3189 page_num, comment_prog_str)
3191 response = self._extract_response(
3192 item_id=None, query=continuation,
3193 ep='next', ytcfg=ytcfg, headers=headers, note=note_prefix,
3194 check_get_keys='onResponseReceivedEndpoints' if not is_forced_continuation else None)
3195 is_forced_continuation = False
3196 continuation_contents = traverse_obj(
3197 response, 'onResponseReceivedEndpoints', expected_type=list, default=[])
3200 for continuation_section in continuation_contents:
3201 continuation_items = traverse_obj(
3202 continuation_section,
3203 (('reloadContinuationItemsCommand', 'appendContinuationItemsAction'), 'continuationItems'),
3204 get_all=False, expected_type=list) or []
3205 if is_first_continuation:
3206 continuation = extract_header(continuation_items)
3207 is_first_continuation = False
3212 for entry in extract_thread(continuation_items):
3216 continuation = self._extract_continuation({'contents': continuation_items})
3220 message = self._get_text(root_continuation_data, ('contents', ..., 'messageRenderer', 'text'), max_runs=1)
3221 if message and not parent and tracker['running_total'] == 0:
3222 self.report_warning(f'Youtube said: {message}', video_id=video_id, only_once=True)
3225 def _generate_comment_continuation(video_id):
3227 Generates initial comment section continuation token from given video id
3229 token = f'\x12\r\x12\x0b{video_id}\x18\x062\'"\x11"\x0b{video_id}0\x00x\x020\x00B\x10comments-section'
3230 return base64.b64encode(token.encode()).decode()
3232 def _get_comments(self, ytcfg, video_id, contents, webpage):
3233 """Entry for comment extraction"""
3234 def _real_comment_extract(contents):
3236 item for item in traverse_obj(contents, (..., 'itemSectionRenderer'), default={})
3237 if item.get('sectionIdentifier') == 'comment-item-section'), None)
3238 yield from self._comment_entries(renderer, ytcfg, video_id)
3240 max_comments = int_or_none(self._configuration_arg('max_comments', [''])[0])
3241 return itertools.islice(_real_comment_extract(contents), 0, max_comments)
3244 def _get_checkok_params():
3245 return {'contentCheckOk': True, 'racyCheckOk': True}
3248 def _generate_player_context(cls, sts=None):
3250 'html5Preference': 'HTML5_PREF_WANTS',
3253 context['signatureTimestamp'] = sts
3255 'playbackContext': {
3256 'contentPlaybackContext': context
3258 **cls._get_checkok_params()
3262 def _is_agegated(player_response):
3263 if traverse_obj(player_response, ('playabilityStatus', 'desktopLegacyAgeGateReason')):
3266 reasons = traverse_obj(player_response, ('playabilityStatus', ('status', 'reason')), default=[])
3267 AGE_GATE_REASONS = (
3268 'confirm your age', 'age-restricted', 'inappropriate', # reason
3269 'age_verification_required', 'age_check_required', # status
3271 return any(expected in reason for expected in AGE_GATE_REASONS for reason in reasons)
3274 def _is_unplayable(player_response):
3275 return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
3277 _STORY_PLAYER_PARAMS = '8AEB'
3279 def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr, smuggled_data):
3281 session_index = self._extract_session_index(player_ytcfg, master_ytcfg)
3282 syncid = self._extract_account_syncid(player_ytcfg, master_ytcfg, initial_pr)
3283 sts = self._extract_signature_timestamp(video_id, player_url, master_ytcfg, fatal=False) if player_url else None
3284 headers = self.generate_api_headers(
3285 ytcfg=player_ytcfg, account_syncid=syncid, session_index=session_index, default_client=client)
3288 'videoId': video_id,
3290 if smuggled_data.get('is_story') or _split_innertube_client(client)[0] == 'android':
3291 yt_query['params'] = self._STORY_PLAYER_PARAMS
3293 yt_query.update(self._generate_player_context(sts))
3294 return self._extract_response(
3295 item_id=video_id, ep='player', query=yt_query,
3296 ytcfg=player_ytcfg, headers=headers, fatal=True,
3297 default_client=client,
3298 note='Downloading %s player API JSON' % client.replace('_', ' ').strip()
3301 def _get_requested_clients(self, url, smuggled_data):
3302 requested_clients = []
3303 default = ['android', 'web']
3304 allowed_clients = sorted(
3305 (client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'),
3306 key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
3307 for client in self._configuration_arg('player_client'):
3308 if client in allowed_clients:
3309 requested_clients.append(client)
3310 elif client == 'default':
3311 requested_clients.extend(default)
3312 elif client == 'all':
3313 requested_clients.extend(allowed_clients)
3315 self.report_warning(f'Skipping unsupported client {client}')
3316 if not requested_clients:
3317 requested_clients = default
3319 if smuggled_data.get('is_music_url') or self.is_music_url(url):
3320 requested_clients.extend(
3321 f'{client}_music' for client in requested_clients if f'{client}_music' in INNERTUBE_CLIENTS)
3323 return orderedSet(requested_clients)
3325 def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, smuggled_data):
3328 initial_pr = self._search_json(
3329 self._YT_INITIAL_PLAYER_RESPONSE_RE, webpage, 'initial player response', video_id, fatal=False)
3331 all_clients = set(clients)
3332 clients = clients[::-1]
3335 def append_client(*client_names):
3336 """ Append the first client name that exists but not already used """
3337 for client_name in client_names:
3338 actual_client = _split_innertube_client(client_name)[0]
3339 if actual_client in INNERTUBE_CLIENTS:
3340 if actual_client not in all_clients:
3341 clients.append(client_name)
3342 all_clients.add(actual_client)
3345 # Android player_response does not have microFormats which are needed for
3346 # extraction of some data. So we return the initial_pr with formats
3347 # stripped out even if not requested by the user
3348 # See: https://github.com/yt-dlp/yt-dlp/issues/501
3350 pr = dict(initial_pr)
3351 pr['streamingData'] = None
3355 tried_iframe_fallback = False
3358 client, base_client, variant = _split_innertube_client(clients.pop())
3359 player_ytcfg = master_ytcfg if client == 'web' else {}
3360 if 'configs' not in self._configuration_arg('player_skip') and client != 'web':
3361 player_ytcfg = self._download_ytcfg(client, video_id) or player_ytcfg
3363 player_url = player_url or self._extract_player_url(master_ytcfg, player_ytcfg, webpage=webpage)
3364 require_js_player = self._get_default_ytcfg(client).get('REQUIRE_JS_PLAYER')
3365 if 'js' in self._configuration_arg('player_skip'):
3366 require_js_player = False
3369 if not player_url and not tried_iframe_fallback and require_js_player:
3370 player_url = self._download_player_url(video_id)
3371 tried_iframe_fallback = True
3374 pr = initial_pr if client == 'web' and initial_pr else self._extract_player_response(
3375 client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, player_url if require_js_player else None, initial_pr, smuggled_data)
3376 except ExtractorError as e:
3378 self.report_warning(last_error)
3383 # YouTube may return a different video player response than expected.
3384 # See: https://github.com/TeamNewPipe/NewPipe/issues/8713
3385 pr_video_id = traverse_obj(pr, ('videoDetails', 'videoId'))
3386 if pr_video_id and pr_video_id != video_id:
3387 self.report_warning(
3388 f'Skipping player response from {client} client (got player response for video "{pr_video_id}
" instead of "{video_id}
")' + bug_reports_message())
3392 # creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
3393 if variant == 'embedded' and self._is_unplayable(pr) and self.is_authenticated:
3394 append_client(f'{base_client}_creator')
3395 elif self._is_agegated(pr):
3396 if variant == 'tv_embedded':
3397 append_client(f'{base_client}_embedded')
3399 append_client(f'tv_embedded.{base_client}', f'{base_client}_embedded')
3404 self.report_warning(last_error)
3405 return prs, player_url
3407 def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, is_live, duration):
3408 itags, stream_ids = {}, []
3409 itag_qualities, res_qualities = {}, {0: None}
3411 # Normally tiny is the smallest video-only formats. But
3412 # audio-only formats with unknown quality may get tagged as tiny
3414 'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high', # Audio only formats
3415 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
3417 streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...), default=[])
3419 for fmt in streaming_formats:
3420 if fmt.get('targetDurationSec'):
3423 itag = str_or_none(fmt.get('itag'))
3424 audio_track = fmt.get('audioTrack') or {}
3425 stream_id = '%s.%s' % (itag or '', audio_track.get('id', ''))
3426 if stream_id in stream_ids:
3429 quality = fmt.get('quality')
3430 height = int_or_none(fmt.get('height'))
3431 if quality == 'tiny' or not quality:
3432 quality = fmt.get('audioQuality', '').lower() or quality
3433 # The 3gp format (17) in android client has a quality of "small
",
3434 # but is actually worse than other formats
3439 itag_qualities[itag] = quality
3441 res_qualities[height] = quality
3442 # FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
3443 # (adding `&sq=0` to the URL) and parsing emsg box to determine the
3444 # number of fragment that would subsequently requested with (`&sq=N`)
3445 if fmt.get('type') == 'FORMAT_STREAM_TYPE_OTF':
3448 fmt_url = fmt.get('url')
3450 sc = urllib.parse.parse_qs(fmt.get('signatureCipher'))
3451 fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0]))
3452 encrypted_sig = try_get(sc, lambda x: x['s'][0])
3453 if not all((sc, fmt_url, player_url, encrypted_sig)):
3456 fmt_url += '&%s=%s' % (
3457 traverse_obj(sc, ('sp', -1)) or 'signature',
3458 self._decrypt_signature(encrypted_sig, video_id, player_url)
3460 except ExtractorError as e:
3461 self.report_warning('Signature extraction failed: Some formats may be missing',
3462 video_id=video_id, only_once=True)
3463 self.write_debug(e, only_once=True)
3466 query = parse_qs(fmt_url)
3470 decrypt_nsig = self._cached(self._decrypt_nsig, 'nsig', query['n'][0])
3471 fmt_url = update_url_query(fmt_url, {
3472 'n': decrypt_nsig(query['n'][0], video_id, player_url)
3474 except ExtractorError as e:
3476 if isinstance(e, JSInterpreter.Exception):
3477 phantomjs_hint = (f' Install {self._downloader._format_err("PhantomJS", self._downloader.Styles.EMPHASIS)} '
3478 f'to workaround the issue. {PhantomJSwrapper.INSTALL_HINT}\n')
3480 self.report_warning(
3481 f'nsig extraction failed: You may experience throttling for some formats\n{phantomjs_hint}'
3482 f' n = {query["n"][0]} ; player = {player_url}', video_id=video_id, only_once=True)
3483 self.write_debug(e, only_once=True)
3485 self.report_warning(
3486 'Cannot decrypt nsig without player_url: You may experience throttling for some formats',
3487 video_id=video_id, only_once=True)
3491 itags[itag] = 'https'
3492 stream_ids.append(stream_id)
3494 tbr = float_or_none(fmt.get('averageBitrate') or fmt.get('bitrate'), 1000)
3495 language_preference = (
3496 10 if audio_track.get('audioIsDefault') and 10
3497 else -10 if 'descriptive' in (audio_track.get('displayName') or '').lower() and -10
3499 # Some formats may have much smaller duration than others (possibly damaged during encoding)
3500 # E.g. 2-nOtRESiUc Ref: https://github.com/yt-dlp/yt-dlp/issues/2823
3501 # Make sure to avoid false positives with small duration differences.
3502 # E.g. __2ABJjxzNo, ySuUZEjARPY
3503 is_damaged = try_get(fmt, lambda x: float(x['approxDurationMs']) / duration < 500)
3505 self.report_warning(
3506 f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
3508 'asr': int_or_none(fmt.get('audioSampleRate')),
3509 'filesize': int_or_none(fmt.get('contentLength')),
3511 'format_note': join_nonempty(
3512 '%s%s' % (audio_track.get('displayName') or '',
3513 ' (default)' if language_preference > 0 else ''),
3514 fmt.get('qualityLabel') or quality.replace('audio_quality_', ''),
3515 try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
3516 try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
3517 throttled and 'THROTTLED', is_damaged and 'DAMAGED', delim=', '),
3518 # Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
3519 'source_preference': -10 if throttled else -5 if itag == '22' else -1,
3520 'fps': int_or_none(fmt.get('fps')) or None,
3521 'audio_channels': fmt.get('audioChannels'),
3523 'quality': q(quality),
3524 'has_drm': bool(fmt.get('drmFamilies')),
3527 'width': int_or_none(fmt.get('width')),
3528 'language': join_nonempty(audio_track.get('id', '').split('.')[0],
3529 'desc' if language_preference < -1 else ''),
3530 'language_preference': language_preference,
3531 # Strictly de-prioritize damaged and 3gp formats
3532 'preference': -10 if is_damaged else -2 if itag == '17' else None,
3534 mime_mobj = re.match(
3535 r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^
"]+)")?
', fmt.get('mimeType
') or '')
3537 dct['ext
'] = mimetype2ext(mime_mobj.group(1))
3538 dct.update(parse_codecs(mime_mobj.group(2)))
3539 no_audio = dct.get('acodec
') == 'none
'
3540 no_video = dct.get('vcodec
') == 'none
'
3545 if no_audio or no_video:
3546 dct['downloader_options
'] = {
3547 # Youtube throttles chunks >~10M
3548 'http_chunk_size
': 10485760,
3551 dct['container
'] = dct['ext
'] + '_dash
'
3554 live_from_start = is_live and self.get_param('live_from_start
')
3555 skip_manifests = self._configuration_arg('skip
')
3556 if not self.get_param('youtube_include_hls_manifest
', True):
3557 skip_manifests.append('hls
')
3558 if not self.get_param('youtube_include_dash_manifest
', True):
3559 skip_manifests.append('dash
')
3560 get_dash = 'dash
' not in skip_manifests and (
3561 not is_live or live_from_start or self._configuration_arg('include_live_dash
'))
3562 get_hls = not live_from_start and 'hls
' not in skip_manifests
3564 def process_manifest_format(f, proto, itag):
3566 if itags[itag] == proto or f'{itag}
-{proto}
' in itags:
3568 itag = f'{itag}
-{proto}
'
3570 f['format_id
'] = itag
3573 f['quality
'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id
'].split('-')[0]), -1))
3574 if f['quality
'] == -1 and f.get('height
'):
3575 f['quality
'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height
']))])
3579 for sd in streaming_data:
3580 hls_manifest_url = get_hls and sd.get('hlsManifestUrl
')
3581 if hls_manifest_url:
3582 fmts, subs = self._extract_m3u8_formats_and_subtitles(hls_manifest_url, video_id, 'mp4
', fatal=False, live=is_live)
3583 subtitles = self._merge_subtitles(subs, subtitles)
3585 if process_manifest_format(f, 'hls
', self._search_regex(
3586 r'/itag
/(\d
+)', f['url
'], 'itag
', default=None)):
3589 dash_manifest_url = get_dash and sd.get('dashManifestUrl
')
3590 if dash_manifest_url:
3591 formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
3592 subtitles = self._merge_subtitles(subs, subtitles) # Prioritize HLS subs over DASH
3594 if process_manifest_format(f, 'dash
', f['format_id
']):
3595 f['filesize
'] = int_or_none(self._search_regex(
3596 r'/clen
/(\d
+)', f.get('fragment_base_url
') or f['url
'], 'file size
', default=None))
3598 f['is_from_start
'] = True
3603 def _extract_storyboard(self, player_responses, duration):
3605 player_responses, ('storyboards
', 'playerStoryboardSpecRenderer
', 'spec
'), default='').split('|
')[::-1]
3606 base_url = url_or_none(urljoin('https
://i
.ytimg
.com
/', spec.pop() or None))
3610 for i, args in enumerate(spec):
3611 args = args.split('#')
3612 counts
= list(map(int_or_none
, args
[:5]))
3613 if len(args
) != 8 or not all(counts
):
3614 self
.report_warning(f
'Malformed storyboard {i}: {"#".join(args)}{bug_reports_message()}')
3616 width
, height
, frame_count
, cols
, rows
= counts
3619 url
= base_url
.replace('$L', str(L
- i
)).replace('$N', N
) + f
'&sigh={sigh}'
3620 fragment_count
= frame_count
/ (cols
* rows
)
3621 fragment_duration
= duration
/ fragment_count
3623 'format_id': f
'sb{i}',
3624 'format_note': 'storyboard',
3626 'protocol': 'mhtml',
3632 'fps': frame_count
/ duration
,
3636 'url': url
.replace('$M', str(j
)),
3637 'duration': min(fragment_duration
, duration
- (j
* fragment_duration
)),
3638 } for j
in range(math
.ceil(fragment_count
))],
3641 def _download_player_responses(self
, url
, smuggled_data
, video_id
, webpage_url
):
3643 if 'webpage' not in self
._configuration
_arg
('player_skip'):
3644 query
= {'bpctr': '9999999999', 'has_verified': '1'}
3645 if smuggled_data
.get('is_story'):
3646 query
['pp'] = self
._STORY
_PLAYER
_PARAMS
3647 webpage
= self
._download
_webpage
(
3648 webpage_url
, video_id
, fatal
=False, query
=query
)
3650 master_ytcfg
= self
.extract_ytcfg(video_id
, webpage
) or self
._get
_default
_ytcfg
()
3652 player_responses
, player_url
= self
._extract
_player
_responses
(
3653 self
._get
_requested
_clients
(url
, smuggled_data
),
3654 video_id
, webpage
, master_ytcfg
, smuggled_data
)
3656 return webpage
, master_ytcfg
, player_responses
, player_url
3658 def _list_formats(self
, video_id
, microformats
, video_details
, player_responses
, player_url
, duration
=None):
3659 live_broadcast_details
= traverse_obj(microformats
, (..., 'liveBroadcastDetails'))
3660 is_live
= get_first(video_details
, 'isLive')
3662 is_live
= get_first(live_broadcast_details
, 'isLiveNow')
3664 streaming_data
= traverse_obj(player_responses
, (..., 'streamingData'), default
=[])
3665 *formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(streaming_data
, video_id
, player_url
, is_live
, duration
)
3667 return live_broadcast_details
, is_live
, streaming_data
, formats
, subtitles
3669 def _real_extract(self
, url
):
3670 url
, smuggled_data
= unsmuggle_url(url
, {})
3671 video_id
= self
._match
_id
(url
)
3673 base_url
= self
.http_scheme() + '//www.youtube.com/'
3674 webpage_url
= base_url
+ 'watch?v=' + video_id
3676 webpage
, master_ytcfg
, player_responses
, player_url
= self
._download
_player
_responses
(url
, smuggled_data
, video_id
, webpage_url
)
3678 playability_statuses
= traverse_obj(
3679 player_responses
, (..., 'playabilityStatus'), expected_type
=dict, default
=[])
3681 trailer_video_id
= get_first(
3682 playability_statuses
,
3683 ('errorScreen', 'playerLegacyDesktopYpcTrailerRenderer', 'trailerVideoId'),
3685 if trailer_video_id
:
3686 return self
.url_result(
3687 trailer_video_id
, self
.ie_key(), trailer_video_id
)
3689 search_meta
= ((lambda x
: self
._html
_search
_meta
(x
, webpage
, default
=None))
3690 if webpage
else (lambda x
: None))
3692 video_details
= traverse_obj(
3693 player_responses
, (..., 'videoDetails'), expected_type
=dict, default
=[])
3694 microformats
= traverse_obj(
3695 player_responses
, (..., 'microformat', 'playerMicroformatRenderer'),
3696 expected_type
=dict, default
=[])
3698 translated_title
= self
._get
_text
(microformats
, (..., 'title'))
3699 video_title
= (self
._preferred
_lang
and translated_title
3700 or get_first(video_details
, 'title') # primary
3702 or search_meta(['og:title', 'twitter:title', 'title']))
3703 translated_description
= self
._get
_text
(microformats
, (..., 'description'))
3704 original_description
= get_first(video_details
, 'shortDescription')
3705 video_description
= (
3706 self
._preferred
_lang
and translated_description
3707 # If original description is blank, it will be an empty string.
3708 # Do not prefer translated description in this case.
3709 or original_description
if original_description
is not None else translated_description
)
3711 multifeed_metadata_list
= get_first(
3713 ('multicamera', 'playerLegacyMulticameraRenderer', 'metadataList'),
3715 if multifeed_metadata_list
and not smuggled_data
.get('force_singlefeed'):
3716 if self
.get_param('noplaylist'):
3717 self
.to_screen('Downloading just video %s because of --no-playlist' % video_id
)
3721 for feed
in multifeed_metadata_list
.split(','):
3722 # Unquote should take place before split on comma (,) since textual
3723 # fields may contain comma as well (see
3724 # https://github.com/ytdl-org/youtube-dl/issues/8536)
3725 feed_data
= urllib
.parse
.parse_qs(
3726 urllib
.parse
.unquote_plus(feed
))
3728 def feed_entry(name
):
3730 feed_data
, lambda x
: x
[name
][0], str)
3732 feed_id
= feed_entry('id')
3735 feed_title
= feed_entry('title')
3738 title
+= ' (%s)' % feed_title
3740 '_type': 'url_transparent',
3741 'ie_key': 'Youtube',
3743 '%swatch?v=%s' % (base_url
, feed_data
['id'][0]),
3744 {'force_singlefeed': True}
),
3747 feed_ids
.append(feed_id
)
3749 'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
3750 % (', '.join(feed_ids
), video_id
))
3751 return self
.playlist_result(
3752 entries
, video_id
, video_title
, video_description
)
3754 duration
= int_or_none(
3755 get_first(video_details
, 'lengthSeconds')
3756 or get_first(microformats
, 'lengthSeconds')
3757 or parse_duration(search_meta('duration'))) or None
3759 live_broadcast_details
, is_live
, streaming_data
, formats
, automatic_captions
= \
3760 self
._list
_formats
(video_id
, microformats
, video_details
, player_responses
, player_url
)
3763 if not self
.get_param('allow_unplayable_formats') and traverse_obj(streaming_data
, (..., 'licenseInfos')):
3764 self
.report_drm(video_id
)
3766 playability_statuses
,
3767 ('errorScreen', 'playerErrorMessageRenderer'), expected_type
=dict) or {}
3768 reason
= self
._get
_text
(pemr
, 'reason') or get_first(playability_statuses
, 'reason')
3769 subreason
= clean_html(self
._get
_text
(pemr
, 'subreason') or '')
3771 if subreason
== 'The uploader has not made this video available in your country.':
3772 countries
= get_first(microformats
, 'availableCountries')
3774 regions_allowed
= search_meta('regionsAllowed')
3775 countries
= regions_allowed
.split(',') if regions_allowed
else None
3776 self
.raise_geo_restricted(subreason
, countries
, metadata_available
=True)
3777 reason
+= f
'. {subreason}'
3779 self
.raise_no_formats(reason
, expected
=True)
3781 keywords
= get_first(video_details
, 'keywords', expected_type
=list) or []
3782 if not keywords
and webpage
:
3784 unescapeHTML(m
.group('content'))
3785 for m
in re
.finditer(self
._meta
_regex
('og:video:tag'), webpage
)]
3786 for keyword
in keywords
:
3787 if keyword
.startswith('yt:stretch='):
3788 mobj
= re
.search(r
'(\d+)\s*:\s*(\d+)', keyword
)
3790 # NB: float is intentional for forcing float division
3791 w
, h
= (float(v
) for v
in mobj
.groups())
3795 if f
.get('vcodec') != 'none':
3796 f
['stretched_ratio'] = ratio
3798 thumbnails
= self
._extract
_thumbnails
((video_details
, microformats
), (..., ..., 'thumbnail'))
3799 thumbnail_url
= search_meta(['og:image', 'twitter:image'])
3802 'url': thumbnail_url
,
3804 original_thumbnails
= thumbnails
.copy()
3806 # The best resolution thumbnails sometimes does not appear in the webpage
3807 # See: https://github.com/yt-dlp/yt-dlp/issues/340
3808 # List of possible thumbnails - Ref: <https://stackoverflow.com/a/20542029>
3810 # While the *1,*2,*3 thumbnails are just below their corresponding "*default" variants
3811 # in resolution, these are not the custom thumbnail. So de-prioritize them
3812 'maxresdefault', 'hq720', 'sddefault', 'hqdefault', '0', 'mqdefault', 'default',
3813 'sd1', 'sd2', 'sd3', 'hq1', 'hq2', 'hq3', 'mq1', 'mq2', 'mq3', '1', '2', '3'
3815 n_thumbnail_names
= len(thumbnail_names
)
3817 'url': 'https://i.ytimg.com/vi{webp}/{video_id}/{name}{live}.{ext}'.format(
3818 video_id
=video_id
, name
=name
, ext
=ext
,
3819 webp
='_webp' if ext
== 'webp' else '', live
='_live' if is_live
else ''),
3820 } for name
in thumbnail_names
for ext
in ('webp', 'jpg'))
3821 for thumb
in thumbnails
:
3822 i
= next((i
for i
, t
in enumerate(thumbnail_names
) if f
'/{video_id}/{t}' in thumb
['url']), n_thumbnail_names
)
3823 thumb
['preference'] = (0 if '.webp' in thumb
['url'] else -1) - (2 * i
)
3824 self
._remove
_duplicate
_formats
(thumbnails
)
3825 self
._downloader
._sort
_thumbnails
(original_thumbnails
)
3827 category
= get_first(microformats
, 'category') or search_meta('genre')
3828 channel_id
= str_or_none(
3829 get_first(video_details
, 'channelId')
3830 or get_first(microformats
, 'externalChannelId')
3831 or search_meta('channelId'))
3832 owner_profile_url
= get_first(microformats
, 'ownerProfileUrl')
3834 live_content
= get_first(video_details
, 'isLiveContent')
3835 is_upcoming
= get_first(video_details
, 'isUpcoming')
3837 if is_upcoming
or live_content
is False:
3839 if is_upcoming
is None and (live_content
or is_live
):
3841 live_start_time
= parse_iso8601(get_first(live_broadcast_details
, 'startTimestamp'))
3842 live_end_time
= parse_iso8601(get_first(live_broadcast_details
, 'endTimestamp'))
3843 if not duration
and live_end_time
and live_start_time
:
3844 duration
= live_end_time
- live_start_time
3846 if is_live
and self
.get_param('live_from_start'):
3847 self
._prepare
_live
_from
_start
_formats
(formats
, video_id
, live_start_time
, url
, webpage_url
, smuggled_data
)
3849 formats
.extend(self
._extract
_storyboard
(player_responses
, duration
))
3851 # source_preference is lower for throttled/potentially damaged formats
3852 self
._sort
_formats
(formats
, (
3853 'quality', 'res', 'fps', 'hdr:12', 'source', 'vcodec:vp9.2', 'channels', 'acodec', 'lang', 'proto'))
3857 'title': video_title
,
3859 'thumbnails': thumbnails
,
3860 # The best thumbnail that we are sure exists. Prevents unnecessary
3861 # URL checking if user don't care about getting the best possible thumbnail
3862 'thumbnail': traverse_obj(original_thumbnails
, (-1, 'url')),
3863 'description': video_description
,
3864 'uploader': get_first(video_details
, 'author'),
3865 'uploader_id': self
._search
_regex
(r
'/(?:channel|user)/([^/?&#]+)', owner_profile_url
, 'uploader id') if owner_profile_url
else None,
3866 'uploader_url': owner_profile_url
,
3867 'channel_id': channel_id
,
3868 'channel_url': format_field(channel_id
, None, 'https://www.youtube.com/channel/%s'),
3869 'duration': duration
,
3870 'view_count': int_or_none(
3871 get_first((video_details
, microformats
), (..., 'viewCount'))
3872 or search_meta('interactionCount')),
3873 'average_rating': float_or_none(get_first(video_details
, 'averageRating')),
3874 'age_limit': 18 if (
3875 get_first(microformats
, 'isFamilySafe') is False
3876 or search_meta('isFamilyFriendly') == 'false'
3877 or search_meta('og:restrictions:age') == '18+') else 0,
3878 'webpage_url': webpage_url
,
3879 'categories': [category
] if category
else None,
3881 'playable_in_embed': get_first(playability_statuses
, 'playableInEmbed'),
3883 'was_live': (False if is_live
or is_upcoming
or live_content
is False
3884 else None if is_live
is None or is_upcoming
is None
3886 'live_status': 'is_upcoming' if is_upcoming
else None, # rest will be set by YoutubeDL
3887 'release_timestamp': live_start_time
,
3890 if get_first(video_details
, 'isPostLiveDvr'):
3891 self
.write_debug('Video is in Post-Live Manifestless mode')
3892 info
['live_status'] = 'post_live'
3893 if (duration
or 0) > 4 * 3600:
3894 self
.report_warning(
3895 'The livestream has not finished processing. Only 4 hours of the video can be currently downloaded. '
3896 'This is a known issue and patches are welcome')
3899 pctr
= traverse_obj(player_responses
, (..., 'captions', 'playerCaptionsTracklistRenderer'), expected_type
=dict)
3901 def get_lang_code(track
):
3902 return (remove_start(track
.get('vssId') or '', '.').replace('.', '-')
3903 or track
.get('languageCode'))
3905 # Converted into dicts to remove duplicates
3907 get_lang_code(sub
): sub
3908 for sub
in traverse_obj(pctr
, (..., 'captionTracks', ...), default
=[])}
3909 translation_languages
= {
3910 lang
.get('languageCode'): self
._get
_text
(lang
.get('languageName'), max_runs
=1)
3911 for lang
in traverse_obj(pctr
, (..., 'translationLanguages', ...), default
=[])}
3913 def process_language(container
, base_url
, lang_code
, sub_name
, query
):
3914 lang_subs
= container
.setdefault(lang_code
, [])
3915 for fmt
in self
._SUBTITLE
_FORMATS
:
3921 'url': urljoin('https://www.youtube.com', update_url_query(base_url
, query
)),
3925 # NB: Constructing the full subtitle dictionary is slow
3926 get_translated_subs
= 'translated_subs' not in self
._configuration
_arg
('skip') and (
3927 self
.get_param('writeautomaticsub', False) or self
.get_param('listsubtitles'))
3928 for lang_code
, caption_track
in captions
.items():
3929 base_url
= caption_track
.get('baseUrl')
3930 orig_lang
= parse_qs(base_url
).get('lang', [None])[-1]
3933 lang_name
= self
._get
_text
(caption_track
, 'name', max_runs
=1)
3934 if caption_track
.get('kind') != 'asr':
3938 subtitles
, base_url
, lang_code
, lang_name
, {})
3939 if not caption_track
.get('isTranslatable'):
3941 for trans_code
, trans_name
in translation_languages
.items():
3944 orig_trans_code
= trans_code
3945 if caption_track
.get('kind') != 'asr':
3946 if not get_translated_subs
:
3948 trans_code
+= f
'-{lang_code}'
3949 trans_name
+= format_field(lang_name
, None, ' from %s')
3950 # Add an "-orig" label to the original language so that it can be distinguished.
3951 # The subs are returned without "-orig" as well for compatibility
3952 if lang_code
== f
'a-{orig_trans_code}':
3954 automatic_captions
, base_url
, f
'{trans_code}-orig', f
'{trans_name} (Original)', {})
3955 # Setting tlang=lang returns damaged subtitles.
3956 process_language(automatic_captions
, base_url
, trans_code
, trans_name
,
3957 {} if orig_lang == orig_trans_code else {'tlang': trans_code}
)
3959 info
['automatic_captions'] = automatic_captions
3960 info
['subtitles'] = subtitles
3962 parsed_url
= urllib
.parse
.urlparse(url
)
3963 for component
in [parsed_url
.fragment
, parsed_url
.query
]:
3964 query
= urllib
.parse
.parse_qs(component
)
3965 for k
, v
in query
.items():
3966 for d_k
, s_ks
in [('start', ('start', 't')), ('end', ('end',))]:
3968 if d_k
not in info
and k
in s_ks
:
3969 info
[d_k
] = parse_duration(query
[k
][0])
3971 # Youtube Music Auto-generated description
3972 if video_description
:
3975 (?P<track>[^·\n]+)·(?P<artist>[^\n]+)\n+
3977 (?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
3978 (?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
3979 (.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?
3980 .+\nAuto-generated\ by\ YouTube\.\s*$
3981 ''', video_description
)
3983 release_year
= mobj
.group('release_year')
3984 release_date
= mobj
.group('release_date')
3986 release_date
= release_date
.replace('-', '')
3987 if not release_year
:
3988 release_year
= release_date
[:4]
3990 'album': mobj
.group('album'.strip()),
3991 'artist': mobj
.group('clean_artist') or ', '.join(a
.strip() for a
in mobj
.group('artist').split('·')),
3992 'track': mobj
.group('track').strip(),
3993 'release_date': release_date
,
3994 'release_year': int_or_none(release_year
),
3999 initial_data
= self
.extract_yt_initial_data(video_id
, webpage
, fatal
=False)
4000 if not initial_data
:
4001 query
= {'videoId': video_id}
4002 query
.update(self
._get
_checkok
_params
())
4003 initial_data
= self
._extract
_response
(
4004 item_id
=video_id
, ep
='next', fatal
=False,
4005 ytcfg
=master_ytcfg
, query
=query
,
4006 headers
=self
.generate_api_headers(ytcfg
=master_ytcfg
),
4007 note
='Downloading initial data API JSON')
4009 info
['comment_count'] = traverse_obj(initial_data
, (
4010 'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'itemSectionRenderer',
4011 'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount', 'simpleText'
4013 'engagementPanels', lambda _
, v
: v
['engagementPanelSectionListRenderer']['panelIdentifier'] == 'comment-item-section',
4014 'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo', 'runs', ..., 'text'
4015 ), expected_type
=int_or_none
, get_all
=False)
4017 try: # This will error if there is no livechat
4018 initial_data
['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']
4019 except (KeyError, IndexError, TypeError):
4022 info
.setdefault('subtitles', {})['live_chat'] = [{
4023 # url is needed to set cookies
4024 'url': f
'https://www.youtube.com/watch?v={video_id}&bpctr=9999999999&has_verified=1',
4025 'video_id': video_id
,
4027 'protocol': 'youtube_live_chat' if is_live
or is_upcoming
else 'youtube_live_chat_replay',
4031 info
['chapters'] = (
4032 self
._extract
_chapters
_from
_json
(initial_data
, duration
)
4033 or self
._extract
_chapters
_from
_engagement
_panel
(initial_data
, duration
)
4034 or self
._extract
_chapters
_from
_description
(video_description
, duration
)
4037 contents
= traverse_obj(
4038 initial_data
, ('contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents'),
4039 expected_type
=list, default
=[])
4041 vpir
= get_first(contents
, 'videoPrimaryInfoRenderer')
4043 stl
= vpir
.get('superTitleLink')
4045 stl
= self
._get
_text
(stl
)
4048 lambda x
: x
['superTitleIcon']['iconType']) == 'LOCATION_PIN':
4049 info
['location'] = stl
4051 mobj
= re
.search(r
'(.+?)\s*S(\d+)\s*•?\s*E(\d+)', stl
)
4054 'series': mobj
.group(1),
4055 'season_number': int(mobj
.group(2)),
4056 'episode_number': int(mobj
.group(3)),
4058 for tlb
in (try_get(
4060 lambda x
: x
['videoActions']['menuRenderer']['topLevelButtons'],
4064 tlb
, 'toggleButtonRenderer',
4065 ('segmentedLikeDislikeButtonRenderer', ..., 'toggleButtonRenderer'),
4068 for getter
, regex
in [(
4069 lambda x
: x
['defaultText']['accessibility']['accessibilityData'],
4070 r
'(?P<count>[\d,]+)\s*(?P<type>(?:dis)?like)'), ([
4071 lambda x
: x
['accessibility'],
4072 lambda x
: x
['accessibilityData']['accessibilityData'],
4073 ], r
'(?P<type>(?:dis)?like) this video along with (?P<count>[\d,]+) other people')]:
4074 label
= (try_get(tbr
, getter
, dict) or {}).get('label')
4076 mobj
= re
.match(regex
, label
)
4078 info
[mobj
.group('type') + '_count'] = str_to_int(mobj
.group('count'))
4080 sbr_tooltip
= try_get(
4081 vpir
, lambda x
: x
['sentimentBar']['sentimentBarRenderer']['tooltip'])
4083 like_count
, dislike_count
= sbr_tooltip
.split(' / ')
4085 'like_count': str_to_int(like_count
),
4086 'dislike_count': str_to_int(dislike_count
),
4088 vsir
= get_first(contents
, 'videoSecondaryInfoRenderer')
4090 vor
= traverse_obj(vsir
, ('owner', 'videoOwnerRenderer'))
4092 'channel': self
._get
_text
(vor
, 'title'),
4093 'channel_follower_count': self
._get
_count
(vor
, 'subscriberCountText')})
4097 lambda x
: x
['metadataRowContainer']['metadataRowContainerRenderer']['rows'],
4099 multiple_songs
= False
4101 if try_get(row
, lambda x
: x
['metadataRowRenderer']['hasDividerLine']) is True:
4102 multiple_songs
= True
4105 mrr
= row
.get('metadataRowRenderer') or {}
4106 mrr_title
= mrr
.get('title')
4109 mrr_title
= self
._get
_text
(mrr
, 'title')
4110 mrr_contents_text
= self
._get
_text
(mrr
, ('contents', 0))
4111 if mrr_title
== 'License':
4112 info
['license'] = mrr_contents_text
4113 elif not multiple_songs
:
4114 if mrr_title
== 'Album':
4115 info
['album'] = mrr_contents_text
4116 elif mrr_title
== 'Artist':
4117 info
['artist'] = mrr_contents_text
4118 elif mrr_title
== 'Song':
4119 info
['track'] = mrr_contents_text
4122 'channel': 'uploader',
4123 'channel_id': 'uploader_id',
4124 'channel_url': 'uploader_url',
4127 # The upload date for scheduled, live and past live streams / premieres in microformats
4128 # may be different from the stream date. Although not in UTC, we will prefer it in this case.
4129 # See: https://github.com/yt-dlp/yt-dlp/pull/2223#issuecomment-1008485139
4131 unified_strdate(get_first(microformats
, 'uploadDate'))
4132 or unified_strdate(search_meta('uploadDate')))
4133 if not upload_date
or (
4134 not info
.get('is_live')
4135 and not info
.get('was_live')
4136 and info
.get('live_status') != 'is_upcoming'
4137 and 'no-youtube-prefer-utc-upload-date' not in self
.get_param('compat_opts', [])
4139 upload_date
= strftime_or_none(
4140 self
._parse
_time
_text
(self
._get
_text
(vpir
, 'dateText')), '%Y%m%d') or upload_date
4141 info
['upload_date'] = upload_date
4143 for to
, frm
in fallbacks
.items():
4144 if not info
.get(to
):
4145 info
[to
] = info
.get(frm
)
4147 for s_k
, d_k
in [('artist', 'creator'), ('track', 'alt_title')]:
4152 badges
= self
._extract
_badges
(traverse_obj(contents
, (..., 'videoPrimaryInfoRenderer'), get_all
=False))
4154 is_private
= (self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
)
4155 or get_first(video_details
, 'isPrivate', expected_type
=bool))
4157 info
['availability'] = (
4158 'public' if self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
4159 else self
._availability
(
4160 is_private
=is_private
,
4162 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
)
4163 or False if initial_data
and is_private
is not None else None),
4164 needs_subscription
=(
4165 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
)
4166 or False if initial_data
and is_private
is not None else None),
4167 needs_auth
=info
['age_limit'] >= 18,
4168 is_unlisted
=None if is_private
is None else (
4169 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
)
4170 or get_first(microformats
, 'isUnlisted', expected_type
=bool))))
4172 info
['__post_extractor'] = self
.extract_comments(master_ytcfg
, video_id
, contents
, webpage
)
4174 self
.mark_watched(video_id
, player_responses
)
4179 class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor
):
4182 def passthrough_smuggled_data(func
):
4183 def _smuggle(entries
, smuggled_data
):
4184 for entry
in entries
:
4185 # TODO: Convert URL to music.youtube instead.
4186 # Do we need to passthrough any other smuggled_data?
4187 entry
['url'] = smuggle_url(entry
['url'], smuggled_data
)
4190 @functools.wraps(func
)
4191 def wrapper(self
, url
):
4192 url
, smuggled_data
= unsmuggle_url(url
, {})
4193 if self
.is_music_url(url
):
4194 smuggled_data
['is_music_url'] = True
4195 info_dict
= func(self
, url
, smuggled_data
)
4196 if smuggled_data
and info_dict
.get('entries'):
4197 info_dict
['entries'] = _smuggle(info_dict
['entries'], smuggled_data
)
4201 def _extract_channel_id(self
, webpage
):
4202 channel_id
= self
._html
_search
_meta
(
4203 'channelId', webpage
, 'channel id', default
=None)
4206 channel_url
= self
._html
_search
_meta
(
4207 ('og:url', 'al:ios:url', 'al:android:url', 'al:web:url',
4208 'twitter:url', 'twitter:app:url:iphone', 'twitter:app:url:ipad',
4209 'twitter:app:url:googleplay'), webpage
, 'channel url')
4210 return self
._search
_regex
(
4211 r
'https?://(?:www\.)?youtube\.com/channel/([^/?#&])+',
4212 channel_url
, 'channel id')
4215 def _extract_basic_item_renderer(item
):
4216 # Modified from _extract_grid_item_renderer
4217 known_basic_renderers
= (
4218 'playlistRenderer', 'videoRenderer', 'channelRenderer', 'showRenderer', 'reelItemRenderer'
4220 for key
, renderer
in item
.items():
4221 if not isinstance(renderer
, dict):
4223 elif key
in known_basic_renderers
:
4225 elif key
.startswith('grid') and key
.endswith('Renderer'):
4228 def _grid_entries(self
, grid_renderer
):
4229 for item
in grid_renderer
['items']:
4230 if not isinstance(item
, dict):
4232 renderer
= self
._extract
_basic
_item
_renderer
(item
)
4233 if not isinstance(renderer
, dict):
4235 title
= self
._get
_text
(renderer
, 'title')
4238 playlist_id
= renderer
.get('playlistId')
4240 yield self
.url_result(
4241 'https://www.youtube.com/playlist?list=%s' % playlist_id
,
4242 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
4246 video_id
= renderer
.get('videoId')
4248 yield self
._extract
_video
(renderer
)
4251 channel_id
= renderer
.get('channelId')
4253 yield self
.url_result(
4254 'https://www.youtube.com/channel/%s' % channel_id
,
4255 ie
=YoutubeTabIE
.ie_key(), video_title
=title
)
4257 # generic endpoint URL support
4258 ep_url
= urljoin('https://www.youtube.com/', try_get(
4259 renderer
, lambda x
: x
['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
4262 for ie
in (YoutubeTabIE
, YoutubePlaylistIE
, YoutubeIE
):
4263 if ie
.suitable(ep_url
):
4264 yield self
.url_result(
4265 ep_url
, ie
=ie
.ie_key(), video_id
=ie
._match
_id
(ep_url
), video_title
=title
)
4268 def _music_reponsive_list_entry(self
, renderer
):
4269 video_id
= traverse_obj(renderer
, ('playlistItemData', 'videoId'))
4271 return self
.url_result(f
'https://music.youtube.com/watch?v={video_id}',
4272 ie
=YoutubeIE
.ie_key(), video_id
=video_id
)
4273 playlist_id
= traverse_obj(renderer
, ('navigationEndpoint', 'watchEndpoint', 'playlistId'))
4275 video_id
= traverse_obj(renderer
, ('navigationEndpoint', 'watchEndpoint', 'videoId'))
4277 return self
.url_result(f
'https://music.youtube.com/watch?v={video_id}&list={playlist_id}',
4278 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4279 return self
.url_result(f
'https://music.youtube.com/playlist?list={playlist_id}',
4280 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4281 browse_id
= traverse_obj(renderer
, ('navigationEndpoint', 'browseEndpoint', 'browseId'))
4283 return self
.url_result(f
'https://music.youtube.com/browse/{browse_id}',
4284 ie
=YoutubeTabIE
.ie_key(), video_id
=browse_id
)
4286 def _shelf_entries_from_content(self
, shelf_renderer
):
4287 content
= shelf_renderer
.get('content')
4288 if not isinstance(content
, dict):
4290 renderer
= content
.get('gridRenderer') or content
.get('expandedShelfContentsRenderer')
4292 # TODO: add support for nested playlists so each shelf is processed
4293 # as separate playlist
4294 # TODO: this includes only first N items
4295 yield from self
._grid
_entries
(renderer
)
4296 renderer
= content
.get('horizontalListRenderer')
4301 def _shelf_entries(self
, shelf_renderer
, skip_channels
=False):
4303 shelf_renderer
, lambda x
: x
['endpoint']['commandMetadata']['webCommandMetadata']['url'],
4305 shelf_url
= urljoin('https://www.youtube.com', ep
)
4307 # Skipping links to another channels, note that checking for
4308 # endpoint.commandMetadata.webCommandMetadata.webPageTypwebPageType == WEB_PAGE_TYPE_CHANNEL
4310 if skip_channels
and '/channels?' in shelf_url
:
4312 title
= self
._get
_text
(shelf_renderer
, 'title')
4313 yield self
.url_result(shelf_url
, video_title
=title
)
4314 # Shelf may not contain shelf URL, fallback to extraction from content
4315 yield from self
._shelf
_entries
_from
_content
(shelf_renderer
)
4317 def _playlist_entries(self
, video_list_renderer
):
4318 for content
in video_list_renderer
['contents']:
4319 if not isinstance(content
, dict):
4321 renderer
= content
.get('playlistVideoRenderer') or content
.get('playlistPanelVideoRenderer')
4322 if not isinstance(renderer
, dict):
4324 video_id
= renderer
.get('videoId')
4327 yield self
._extract
_video
(renderer
)
4329 def _rich_entries(self
, rich_grid_renderer
):
4330 renderer
= traverse_obj(
4331 rich_grid_renderer
, ('content', ('videoRenderer', 'reelItemRenderer')), get_all
=False) or {}
4332 video_id
= renderer
.get('videoId')
4335 yield self
._extract
_video
(renderer
)
4337 def _video_entry(self
, video_renderer
):
4338 video_id
= video_renderer
.get('videoId')
4340 return self
._extract
_video
(video_renderer
)
4342 def _hashtag_tile_entry(self
, hashtag_tile_renderer
):
4343 url
= urljoin('https://youtube.com', traverse_obj(
4344 hashtag_tile_renderer
, ('onTapCommand', 'commandMetadata', 'webCommandMetadata', 'url')))
4346 return self
.url_result(
4347 url
, ie
=YoutubeTabIE
.ie_key(), title
=self
._get
_text
(hashtag_tile_renderer
, 'hashtag'))
4349 def _post_thread_entries(self
, post_thread_renderer
):
4350 post_renderer
= try_get(
4351 post_thread_renderer
, lambda x
: x
['post']['backstagePostRenderer'], dict)
4352 if not post_renderer
:
4355 video_renderer
= try_get(
4356 post_renderer
, lambda x
: x
['backstageAttachment']['videoRenderer'], dict) or {}
4357 video_id
= video_renderer
.get('videoId')
4359 entry
= self
._extract
_video
(video_renderer
)
4362 # playlist attachment
4363 playlist_id
= try_get(
4364 post_renderer
, lambda x
: x
['backstageAttachment']['playlistRenderer']['playlistId'], str)
4366 yield self
.url_result(
4367 'https://www.youtube.com/playlist?list=%s' % playlist_id
,
4368 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4369 # inline video links
4370 runs
= try_get(post_renderer
, lambda x
: x
['contentText']['runs'], list) or []
4372 if not isinstance(run
, dict):
4375 run
, lambda x
: x
['navigationEndpoint']['urlEndpoint']['url'], str)
4378 if not YoutubeIE
.suitable(ep_url
):
4380 ep_video_id
= YoutubeIE
._match
_id
(ep_url
)
4381 if video_id
== ep_video_id
:
4383 yield self
.url_result(ep_url
, ie
=YoutubeIE
.ie_key(), video_id
=ep_video_id
)
4385 def _post_thread_continuation_entries(self
, post_thread_continuation
):
4386 contents
= post_thread_continuation
.get('contents')
4387 if not isinstance(contents
, list):
4389 for content
in contents
:
4390 renderer
= content
.get('backstagePostThreadRenderer')
4391 if isinstance(renderer
, dict):
4392 yield from self
._post
_thread
_entries
(renderer
)
4394 renderer
= content
.get('videoRenderer')
4395 if isinstance(renderer
, dict):
4396 yield self
._video
_entry
(renderer
)
4399 def _rich_grid_entries(self, contents):
4400 for content in contents:
4401 video_renderer = try_get(content, lambda x: x['richItemRenderer']['content']['videoRenderer'], dict)
4403 entry = self._video_entry(video_renderer)
4408 def _report_history_entries(self
, renderer
):
4409 for url
in traverse_obj(renderer
, (
4410 'rows', ..., 'reportHistoryTableRowRenderer', 'cells', ...,
4411 'reportHistoryTableCellRenderer', 'cell', 'reportHistoryTableTextCellRenderer', 'text', 'runs', ...,
4412 'navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url')):
4413 yield self
.url_result(urljoin('https://www.youtube.com', url
), YoutubeIE
)
4415 def _extract_entries(self
, parent_renderer
, continuation_list
):
4416 # continuation_list is modified in-place with continuation_list = [continuation_token]
4417 continuation_list
[:] = [None]
4418 contents
= try_get(parent_renderer
, lambda x
: x
['contents'], list) or []
4419 for content
in contents
:
4420 if not isinstance(content
, dict):
4422 is_renderer
= traverse_obj(
4423 content
, 'itemSectionRenderer', 'musicShelfRenderer', 'musicShelfContinuation',
4426 if content
.get('richItemRenderer'):
4427 for entry
in self
._rich
_entries
(content
['richItemRenderer']):
4429 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
4430 elif content
.get('reportHistorySectionRenderer'): # https://www.youtube.com/reporthistory
4431 table
= traverse_obj(content
, ('reportHistorySectionRenderer', 'table', 'tableRenderer'))
4432 yield from self
._report
_history
_entries
(table
)
4433 continuation_list
[0] = self
._extract
_continuation
(table
)
4436 isr_contents
= try_get(is_renderer
, lambda x
: x
['contents'], list) or []
4437 for isr_content
in isr_contents
:
4438 if not isinstance(isr_content
, dict):
4442 'playlistVideoListRenderer': self
._playlist
_entries
,
4443 'gridRenderer': self
._grid
_entries
,
4444 'reelShelfRenderer': self
._grid
_entries
,
4445 'shelfRenderer': self
._shelf
_entries
,
4446 'musicResponsiveListItemRenderer': lambda x
: [self
._music
_reponsive
_list
_entry
(x
)],
4447 'backstagePostThreadRenderer': self
._post
_thread
_entries
,
4448 'videoRenderer': lambda x
: [self
._video
_entry
(x
)],
4449 'playlistRenderer': lambda x
: self
._grid
_entries
({'items': [{'playlistRenderer': x}
]}),
4450 'channelRenderer': lambda x
: self
._grid
_entries
({'items': [{'channelRenderer': x}
]}),
4451 'hashtagTileRenderer': lambda x
: [self
._hashtag
_tile
_entry
(x
)]
4453 for key
, renderer
in isr_content
.items():
4454 if key
not in known_renderers
:
4456 for entry
in known_renderers
[key
](renderer
):
4459 continuation_list
[0] = self
._extract
_continuation
(renderer
)
4462 if not continuation_list
[0]:
4463 continuation_list
[0] = self
._extract
_continuation
(is_renderer
)
4465 if not continuation_list
[0]:
4466 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
4468 def _entries(self
, tab
, item_id
, ytcfg
, account_syncid
, visitor_data
):
4469 continuation_list
= [None]
4470 extract_entries
= lambda x
: self
._extract
_entries
(x
, continuation_list
)
4471 tab_content
= try_get(tab
, lambda x
: x
['content'], dict)
4475 try_get(tab_content
, lambda x
: x
['sectionListRenderer'], dict)
4476 or try_get(tab_content
, lambda x
: x
['richGridRenderer'], dict) or {})
4477 yield from extract_entries(parent_renderer
)
4478 continuation
= continuation_list
[0]
4480 for page_num
in itertools
.count(1):
4481 if not continuation
:
4483 headers
= self
.generate_api_headers(
4484 ytcfg
=ytcfg
, account_syncid
=account_syncid
, visitor_data
=visitor_data
)
4485 response
= self
._extract
_response
(
4486 item_id
=f
'{item_id} page {page_num}',
4487 query
=continuation
, headers
=headers
, ytcfg
=ytcfg
,
4488 check_get_keys
=('continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints'))
4492 # Extracting updated visitor data is required to prevent an infinite extraction loop in some cases
4493 # See: https://github.com/ytdl-org/youtube-dl/issues/28702
4494 visitor_data
= self
._extract
_visitor
_data
(response
) or visitor_data
4496 known_continuation_renderers
= {
4497 'playlistVideoListContinuation': self
._playlist
_entries
,
4498 'gridContinuation': self
._grid
_entries
,
4499 'itemSectionContinuation': self
._post
_thread
_continuation
_entries
,
4500 'sectionListContinuation': extract_entries
, # for feeds
4502 continuation_contents
= try_get(
4503 response
, lambda x
: x
['continuationContents'], dict) or {}
4504 continuation_renderer
= None
4505 for key
, value
in continuation_contents
.items():
4506 if key
not in known_continuation_renderers
:
4508 continuation_renderer
= value
4509 continuation_list
= [None]
4510 yield from known_continuation_renderers
[key
](continuation_renderer
)
4511 continuation
= continuation_list
[0] or self
._extract
_continuation
(continuation_renderer
)
4513 if continuation_renderer
:
4517 'videoRenderer': (self
._grid
_entries
, 'items'), # for membership tab
4518 'gridPlaylistRenderer': (self
._grid
_entries
, 'items'),
4519 'gridVideoRenderer': (self
._grid
_entries
, 'items'),
4520 'gridChannelRenderer': (self
._grid
_entries
, 'items'),
4521 'playlistVideoRenderer': (self
._playlist
_entries
, 'contents'),
4522 'itemSectionRenderer': (extract_entries
, 'contents'), # for feeds
4523 'richItemRenderer': (extract_entries
, 'contents'), # for hashtag
4524 'backstagePostThreadRenderer': (self
._post
_thread
_continuation
_entries
, 'contents'),
4525 'reportHistoryTableRowRenderer': (self
._report
_history
_entries
, 'rows'),
4527 on_response_received
= dict_get(response
, ('onResponseReceivedActions', 'onResponseReceivedEndpoints'))
4528 continuation_items
= try_get(
4529 on_response_received
, lambda x
: x
[0]['appendContinuationItemsAction']['continuationItems'], list)
4530 continuation_item
= try_get(continuation_items
, lambda x
: x
[0], dict) or {}
4531 video_items_renderer
= None
4532 for key
, value
in continuation_item
.items():
4533 if key
not in known_renderers
:
4535 video_items_renderer
= {known_renderers[key][1]: continuation_items}
4536 continuation_list
= [None]
4537 yield from known_renderers
[key
][0](video_items_renderer
)
4538 continuation
= continuation_list
[0] or self
._extract
_continuation
(video_items_renderer
)
4540 if video_items_renderer
:
4545 def _extract_selected_tab(tabs
, fatal
=True):
4547 renderer
= dict_get(tab
, ('tabRenderer', 'expandableTabRenderer')) or {}
4548 if renderer
.get('selected') is True:
4552 raise ExtractorError('Unable to find selected tab')
4554 def _extract_uploader(self
, data
):
4556 renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarSecondaryInfoRenderer') or {}
4558 renderer
, lambda x
: x
['videoOwner']['videoOwnerRenderer']['title']['runs'][0], dict)
4560 owner_text
= owner
.get('text')
4561 uploader
['uploader'] = self
._search
_regex
(
4562 r
'^by (.+) and \d+ others?$', owner_text
, 'uploader', default
=owner_text
)
4563 uploader
['uploader_id'] = try_get(
4564 owner
, lambda x
: x
['navigationEndpoint']['browseEndpoint']['browseId'], str)
4565 uploader
['uploader_url'] = urljoin(
4566 'https://www.youtube.com/',
4567 try_get(owner
, lambda x
: x
['navigationEndpoint']['browseEndpoint']['canonicalBaseUrl'], str))
4568 return {k: v for k, v in uploader.items() if v is not None}
4570 def _extract_from_tabs(self
, item_id
, ytcfg
, data
, tabs
):
4571 playlist_id
= title
= description
= channel_url
= channel_name
= channel_id
= None
4574 selected_tab
= self
._extract
_selected
_tab
(tabs
)
4575 primary_sidebar_renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarPrimaryInfoRenderer')
4577 data
, lambda x
: x
['metadata']['channelMetadataRenderer'], dict)
4579 channel_name
= renderer
.get('title')
4580 channel_url
= renderer
.get('channelUrl')
4581 channel_id
= renderer
.get('externalId')
4584 data
, lambda x
: x
['metadata']['playlistMetadataRenderer'], dict)
4587 title
= renderer
.get('title')
4588 description
= renderer
.get('description', '')
4589 playlist_id
= channel_id
4590 tags
= renderer
.get('keywords', '').split()
4592 # We can get the uncropped banner/avatar by replacing the crop params with '=s0'
4593 # See: https://github.com/yt-dlp/yt-dlp/issues/2237#issuecomment-1013694714
4594 def _get_uncropped(url
):
4595 return url_or_none((url
or '').split('=')[0] + '=s0')
4597 avatar_thumbnails
= self
._extract
_thumbnails
(renderer
, 'avatar')
4598 if avatar_thumbnails
:
4599 uncropped_avatar
= _get_uncropped(avatar_thumbnails
[0]['url'])
4600 if uncropped_avatar
:
4601 avatar_thumbnails
.append({
4602 'url': uncropped_avatar
,
4603 'id': 'avatar_uncropped',
4607 channel_banners
= self
._extract
_thumbnails
(
4608 data
, ('header', ..., ['banner', 'mobileBanner', 'tvBanner']))
4609 for banner
in channel_banners
:
4610 banner
['preference'] = -10
4613 uncropped_banner
= _get_uncropped(channel_banners
[0]['url'])
4614 if uncropped_banner
:
4615 channel_banners
.append({
4616 'url': uncropped_banner
,
4617 'id': 'banner_uncropped',
4621 primary_thumbnails
= self
._extract
_thumbnails
(
4622 primary_sidebar_renderer
, ('thumbnailRenderer', ('playlistVideoThumbnailRenderer', 'playlistCustomThumbnailRenderer'), 'thumbnail'))
4624 if playlist_id
is None:
4625 playlist_id
= item_id
4627 playlist_stats
= traverse_obj(primary_sidebar_renderer
, 'stats')
4628 last_updated_unix
= self
._parse
_time
_text
(self
._get
_text
(playlist_stats
, 2))
4630 title
= self
._get
_text
(data
, ('header', 'hashtagHeaderRenderer', 'hashtag')) or playlist_id
4631 title
+= format_field(selected_tab
, 'title', ' - %s')
4632 title
+= format_field(selected_tab
, 'expandedText', ' - %s')
4635 'playlist_id': playlist_id
,
4636 'playlist_title': title
,
4637 'playlist_description': description
,
4638 'uploader': channel_name
,
4639 'uploader_id': channel_id
,
4640 'uploader_url': channel_url
,
4641 'thumbnails': primary_thumbnails
+ avatar_thumbnails
+ channel_banners
,
4643 'view_count': self
._get
_count
(playlist_stats
, 1),
4644 'availability': self
._extract
_availability
(data
),
4645 'modified_date': strftime_or_none(last_updated_unix
, '%Y%m%d'),
4646 'playlist_count': self
._get
_count
(playlist_stats
, 0),
4647 'channel_follower_count': self
._get
_count
(data
, ('header', ..., 'subscriberCountText')),
4650 metadata
.update(self
._extract
_uploader
(data
))
4652 'channel': metadata
['uploader'],
4653 'channel_id': metadata
['uploader_id'],
4654 'channel_url': metadata
['uploader_url']})
4655 return self
.playlist_result(
4657 selected_tab
, playlist_id
, ytcfg
,
4658 self
._extract
_account
_syncid
(ytcfg
, data
),
4659 self
._extract
_visitor
_data
(data
, ytcfg
)),
4662 def _extract_inline_playlist(self
, playlist
, playlist_id
, data
, ytcfg
):
4663 first_id
= last_id
= response
= None
4664 for page_num
in itertools
.count(1):
4665 videos
= list(self
._playlist
_entries
(playlist
))
4668 start
= next((i
for i
, v
in enumerate(videos
) if v
['id'] == last_id
), -1) + 1
4669 if start
>= len(videos
):
4671 yield from videos
[start
:]
4672 first_id
= first_id
or videos
[0]['id']
4673 last_id
= videos
[-1]['id']
4674 watch_endpoint
= try_get(
4675 playlist
, lambda x
: x
['contents'][-1]['playlistPanelVideoRenderer']['navigationEndpoint']['watchEndpoint'])
4676 headers
= self
.generate_api_headers(
4677 ytcfg
=ytcfg
, account_syncid
=self
._extract
_account
_syncid
(ytcfg
, data
),
4678 visitor_data
=self
._extract
_visitor
_data
(response
, data
, ytcfg
))
4680 'playlistId': playlist_id
,
4681 'videoId': watch_endpoint
.get('videoId') or last_id
,
4682 'index': watch_endpoint
.get('index') or len(videos
),
4683 'params': watch_endpoint
.get('params') or 'OAE%3D'
4685 response
= self
._extract
_response
(
4686 item_id
='%s page %d' % (playlist_id
, page_num
),
4687 query
=query
, ep
='next', headers
=headers
, ytcfg
=ytcfg
,
4688 check_get_keys
='contents'
4691 response
, lambda x
: x
['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
4693 def _extract_from_playlist(self
, item_id
, url
, data
, playlist
, ytcfg
):
4694 title
= playlist
.get('title') or try_get(
4695 data
, lambda x
: x
['titleText']['simpleText'], str)
4696 playlist_id
= playlist
.get('playlistId') or item_id
4698 # Delegating everything except mix playlists to regular tab-based playlist URL
4699 playlist_url
= urljoin(url
, try_get(
4700 playlist
, lambda x
: x
['endpoint']['commandMetadata']['webCommandMetadata']['url'],
4703 # Some playlists are unviewable but YouTube still provides a link to the (broken) playlist page [1]
4704 # [1] MLCT, RLTDwFCb4jeqaKWnciAYM-ZVHg
4705 is_known_unviewable
= re
.fullmatch(r
'MLCT|RLTD[\w-]{22}', playlist_id
)
4707 if playlist_url
and playlist_url
!= url
and not is_known_unviewable
:
4708 return self
.url_result(
4709 playlist_url
, ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
4712 return self
.playlist_result(
4713 self
._extract
_inline
_playlist
(playlist
, playlist_id
, data
, ytcfg
),
4714 playlist_id
=playlist_id
, playlist_title
=title
)
4716 def _extract_availability(self
, data
):
4718 Gets the availability of a given playlist/tab.
4719 Note: Unless YouTube tells us explicitly, we do not assume it is public
4720 @param data: response
4722 renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarPrimaryInfoRenderer') or {}
4724 player_header_privacy
= traverse_obj(
4725 data
, ('header', 'playlistHeaderRenderer', 'privacy'), expected_type
=str)
4727 badges
= self
._extract
_badges
(renderer
)
4729 # Personal playlists, when authenticated, have a dropdown visibility selector instead of a badge
4730 privacy_setting_icon
= traverse_obj(
4732 'privacyForm', 'dropdownFormFieldRenderer', 'dropdown', 'dropdownRenderer', 'entries',
4733 lambda _
, v
: v
['privacyDropdownItemRenderer']['isSelected'], 'privacyDropdownItemRenderer', 'icon', 'iconType'),
4734 get_all
=False, expected_type
=str)
4738 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
4739 or player_header_privacy
== 'PUBLIC'
4740 or privacy_setting_icon
== 'PRIVACY_PUBLIC')
4741 else self
._availability
(
4743 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
)
4744 or player_header_privacy
== 'PRIVATE' if player_header_privacy
is not None
4745 else privacy_setting_icon
== 'PRIVACY_PRIVATE' if privacy_setting_icon
is not None else None),
4747 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
)
4748 or player_header_privacy
== 'UNLISTED' if player_header_privacy
is not None
4749 else privacy_setting_icon
== 'PRIVACY_UNLISTED' if privacy_setting_icon
is not None else None),
4750 needs_subscription
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
) or None,
4751 needs_premium
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
) or None,
4755 def _extract_sidebar_info_renderer(data
, info_renderer
, expected_type
=dict):
4756 sidebar_renderer
= try_get(
4757 data
, lambda x
: x
['sidebar']['playlistSidebarRenderer']['items'], list) or []
4758 for item
in sidebar_renderer
:
4759 renderer
= try_get(item
, lambda x
: x
[info_renderer
], expected_type
)
4763 def _reload_with_unavailable_videos(self
, item_id
, data
, ytcfg
):
4765 Get playlist with unavailable videos if the 'show unavailable videos' button exists.
4767 browse_id
= params
= None
4768 renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarPrimaryInfoRenderer')
4771 menu_renderer
= try_get(
4772 renderer
, lambda x
: x
['menu']['menuRenderer']['items'], list) or []
4773 for menu_item
in menu_renderer
:
4774 if not isinstance(menu_item
, dict):
4776 nav_item_renderer
= menu_item
.get('menuNavigationItemRenderer')
4778 nav_item_renderer
, lambda x
: x
['text']['simpleText'], str)
4779 if not text
or text
.lower() != 'show unavailable videos':
4781 browse_endpoint
= try_get(
4782 nav_item_renderer
, lambda x
: x
['navigationEndpoint']['browseEndpoint'], dict) or {}
4783 browse_id
= browse_endpoint
.get('browseId')
4784 params
= browse_endpoint
.get('params')
4787 headers
= self
.generate_api_headers(
4788 ytcfg
=ytcfg
, account_syncid
=self
._extract
_account
_syncid
(ytcfg
, data
),
4789 visitor_data
=self
._extract
_visitor
_data
(data
, ytcfg
))
4791 'params': params
or 'wgYCCAA=',
4792 'browseId': browse_id
or 'VL%s' % item_id
4794 return self
._extract
_response
(
4795 item_id
=item_id
, headers
=headers
, query
=query
,
4796 check_get_keys
='contents', fatal
=False, ytcfg
=ytcfg
,
4797 note
='Downloading API JSON with unavailable videos')
4799 @functools.cached_property
4800 def skip_webpage(self
):
4801 return 'webpage' in self
._configuration
_arg
('skip', ie_key
=YoutubeTabIE
.ie_key())
4803 def _extract_webpage(self
, url
, item_id
, fatal
=True):
4804 webpage
, data
= None, None
4805 for retry
in self
.RetryManager(fatal
=fatal
):
4807 webpage
= self
._download
_webpage
(url
, item_id
, note
='Downloading webpage')
4808 data
= self
.extract_yt_initial_data(item_id
, webpage
or '', fatal
=fatal
) or {}
4809 except ExtractorError
as e
:
4810 if isinstance(e
.cause
, network_exceptions
):
4811 if not isinstance(e
.cause
, urllib
.error
.HTTPError
) or e
.cause
.code
not in (403, 429):
4814 self
._error
_or
_warning
(e
, fatal
=fatal
)
4818 self
._extract
_and
_report
_alerts
(data
)
4819 except ExtractorError
as e
:
4820 self
._error
_or
_warning
(e
, fatal
=fatal
)
4823 # Sometimes youtube returns a webpage with incomplete ytInitialData
4824 # See: https://github.com/yt-dlp/yt-dlp/issues/116
4825 if not traverse_obj(data
, 'contents', 'currentVideoEndpoint', 'onResponseReceivedActions'):
4826 retry
.error
= ExtractorError('Incomplete yt initial data received')
4829 return webpage
, data
4831 def _report_playlist_authcheck(self
, ytcfg
, fatal
=True):
4832 """Use if failed to extract ytcfg (and data) from initial webpage"""
4833 if not ytcfg
and self
.is_authenticated
:
4834 msg
= 'Playlists that require authentication may not extract correctly without a successful webpage download'
4835 if 'authcheck' not in self
._configuration
_arg
('skip', ie_key
=YoutubeTabIE
.ie_key()) and fatal
:
4836 raise ExtractorError(
4837 f
'{msg}. If you are not downloading private content, or '
4838 'your cookies are only for the first account and channel,'
4839 ' pass "--extractor-args youtubetab:skip=authcheck" to skip this check',
4841 self
.report_warning(msg
, only_once
=True)
4843 def _extract_data(self
, url
, item_id
, ytcfg
=None, fatal
=True, webpage_fatal
=False, default_client
='web'):
4845 if not self
.skip_webpage
:
4846 webpage
, data
= self
._extract
_webpage
(url
, item_id
, fatal
=webpage_fatal
)
4847 ytcfg
= ytcfg
or self
.extract_ytcfg(item_id
, webpage
)
4848 # Reject webpage data if redirected to home page without explicitly requesting
4849 selected_tab
= self
._extract
_selected
_tab
(traverse_obj(
4850 data
, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type
=list, default
=[]), fatal
=False) or {}
4851 if (url
!= 'https://www.youtube.com/feed/recommended'
4852 and selected_tab
.get('tabIdentifier') == 'FEwhat_to_watch' # Home page
4853 and 'no-youtube-channel-redirect' not in self
.get_param('compat_opts', [])):
4854 msg
= 'The channel/playlist does not exist and the URL redirected to youtube.com home page'
4856 raise ExtractorError(msg
, expected
=True)
4857 self
.report_warning(msg
, only_once
=True)
4859 self
._report
_playlist
_authcheck
(ytcfg
, fatal
=fatal
)
4860 data
= self
._extract
_tab
_endpoint
(url
, item_id
, ytcfg
, fatal
=fatal
, default_client
=default_client
)
4863 def _extract_tab_endpoint(self
, url
, item_id
, ytcfg
=None, fatal
=True, default_client
='web'):
4864 headers
= self
.generate_api_headers(ytcfg
=ytcfg
, default_client
=default_client
)
4865 resolve_response
= self
._extract
_response
(
4866 item_id
=item_id
, query
={'url': url}
, check_get_keys
='endpoint', headers
=headers
, ytcfg
=ytcfg
, fatal
=fatal
,
4867 ep
='navigation/resolve_url', note
='Downloading API parameters API JSON', default_client
=default_client
)
4868 endpoints
= {'browseEndpoint': 'browse', 'watchEndpoint': 'next'}
4869 for ep_key
, ep
in endpoints
.items():
4870 params
= try_get(resolve_response
, lambda x
: x
['endpoint'][ep_key
], dict)
4872 return self
._extract
_response
(
4873 item_id
=item_id
, query
=params
, ep
=ep
, headers
=headers
,
4874 ytcfg
=ytcfg
, fatal
=fatal
, default_client
=default_client
,
4875 check_get_keys
=('contents', 'currentVideoEndpoint', 'onResponseReceivedActions'))
4876 err_note
= 'Failed to resolve url (does the playlist exist?)'
4878 raise ExtractorError(err_note
, expected
=True)
4879 self
.report_warning(err_note
, item_id
)
4881 _SEARCH_PARAMS
= None
4883 def _search_results(self
, query
, params
=NO_DEFAULT
, default_client
='web'):
4884 data
= {'query': query}
4885 if params
is NO_DEFAULT
:
4886 params
= self
._SEARCH
_PARAMS
4888 data
['params'] = params
4891 ('contents', 'twoColumnSearchResultsRenderer', 'primaryContents', 'sectionListRenderer', 'contents'),
4892 ('onResponseReceivedCommands', 0, 'appendContinuationItemsAction', 'continuationItems'),
4894 ('contents', 'tabbedSearchResultsRenderer', 'tabs', 0, 'tabRenderer', 'content', 'sectionListRenderer', 'contents'),
4895 ('continuationContents', ),
4897 display_id
= f
'query "{query}"'
4898 check_get_keys
= tuple({keys[0] for keys in content_keys}
)
4899 ytcfg
= self
._download
_ytcfg
(default_client
, display_id
) if not self
.skip_webpage
else {}
4900 self
._report
_playlist
_authcheck
(ytcfg
, fatal
=False)
4902 continuation_list
= [None]
4904 for page_num
in itertools
.count(1):
4905 data
.update(continuation_list
[0] or {})
4906 headers
= self
.generate_api_headers(
4907 ytcfg
=ytcfg
, visitor_data
=self
._extract
_visitor
_data
(search
), default_client
=default_client
)
4908 search
= self
._extract
_response
(
4909 item_id
=f
'{display_id} page {page_num}', ep
='search', query
=data
,
4910 default_client
=default_client
, check_get_keys
=check_get_keys
, ytcfg
=ytcfg
, headers
=headers
)
4911 slr_contents
= traverse_obj(search
, *content_keys
)
4912 yield from self
._extract
_entries
({'contents': list(variadic(slr_contents))}
, continuation_list
)
4913 if not continuation_list
[0]:
4917 class YoutubeTabIE(YoutubeTabBaseInfoExtractor
):
4918 IE_DESC
= 'YouTube Tabs'
4919 _VALID_URL
= r
'''(?x:
4923 youtube(?:kids)?\.com|
4927 (?P<channel_type>channel|c|user|browse)/|
4930 (?:playlist|watch)\?.*?\blist=
4932 (?!(?:%(reserved_names)s)\b) # Direct URLs
4936 'reserved_names': YoutubeBaseInfoExtractor
._RESERVED
_NAMES
,
4937 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
4939 IE_NAME
= 'youtube:tab'
4942 'note': 'playlists, multipage',
4943 'url': 'https://www.youtube.com/c/ИгорьКлейнер/playlists?view=1&flow=grid',
4944 'playlist_mincount': 94,
4946 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
4947 'title': 'Igor Kleiner - Playlists',
4948 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
4949 'uploader': 'Igor Kleiner',
4950 'uploader_id': 'UCqj7Cz7revf5maW9g5pgNcg',
4951 'channel': 'Igor Kleiner',
4952 'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
4953 'tags': ['"критическое', 'мышление"', '"наука', 'просто"', 'математика', '"анализ', 'данных"'],
4954 'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
4955 'uploader_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
4956 'channel_follower_count': int
4959 'note': 'playlists, multipage, different order',
4960 'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
4961 'playlist_mincount': 94,
4963 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
4964 'title': 'Igor Kleiner - Playlists',
4965 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
4966 'uploader_id': 'UCqj7Cz7revf5maW9g5pgNcg',
4967 'uploader': 'Igor Kleiner',
4968 'uploader_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
4969 'tags': ['"критическое', 'мышление"', '"наука', 'просто"', 'математика', '"анализ', 'данных"'],
4970 'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
4971 'channel': 'Igor Kleiner',
4972 'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
4973 'channel_follower_count': int
4976 'note': 'playlists, series',
4977 'url': 'https://www.youtube.com/c/3blue1brown/playlists?view=50&sort=dd&shelf_id=3',
4978 'playlist_mincount': 5,
4980 'id': 'UCYO_jab_esuFRV4b17AJtAw',
4981 'title': '3Blue1Brown - Playlists',
4982 'description': 'md5:e1384e8a133307dd10edee76e875d62f',
4983 'uploader_id': 'UCYO_jab_esuFRV4b17AJtAw',
4984 'uploader': '3Blue1Brown',
4985 'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
4986 'uploader_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
4987 'channel': '3Blue1Brown',
4988 'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
4989 'tags': ['Mathematics'],
4990 'channel_follower_count': int
4993 'note': 'playlists, singlepage',
4994 'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
4995 'playlist_mincount': 4,
4997 'id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
4998 'title': 'ThirstForScience - Playlists',
4999 'description': 'md5:609399d937ea957b0f53cbffb747a14c',
5000 'uploader': 'ThirstForScience',
5001 'uploader_id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
5002 'uploader_url': 'https://www.youtube.com/channel/UCAEtajcuhQ6an9WEzY9LEMQ',
5003 'channel_url': 'https://www.youtube.com/channel/UCAEtajcuhQ6an9WEzY9LEMQ',
5004 'channel_id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
5006 'channel': 'ThirstForScience',
5007 'channel_follower_count': int
5010 'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
5011 'only_matching': True,
5013 'note': 'basic, single video playlist',
5014 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5016 'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5017 'uploader': 'Sergey M.',
5018 'id': 'PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5019 'title': 'youtube-dl public playlist',
5023 'modified_date': '20201130',
5024 'channel': 'Sergey M.',
5025 'channel_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5026 'uploader_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5027 'channel_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5028 'availability': 'public',
5030 'playlist_count': 1,
5032 'note': 'empty playlist',
5033 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
5035 'uploader_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5036 'uploader': 'Sergey M.',
5037 'id': 'PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
5038 'title': 'youtube-dl empty playlist',
5040 'channel': 'Sergey M.',
5042 'modified_date': '20160902',
5043 'channel_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5044 'channel_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5045 'uploader_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5046 'availability': 'public',
5048 'playlist_count': 0,
5051 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/featured',
5053 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5054 'title': 'lex will - Home',
5055 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5056 'uploader': 'lex will',
5057 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5058 'channel': 'lex will',
5059 'tags': ['bible', 'history', 'prophesy'],
5060 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5061 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5062 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5063 'channel_follower_count': int
5065 'playlist_mincount': 2,
5067 'note': 'Videos tab',
5068 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos',
5070 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5071 'title': 'lex will - Videos',
5072 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5073 'uploader': 'lex will',
5074 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5075 'tags': ['bible', 'history', 'prophesy'],
5076 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5077 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5078 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5079 'channel': 'lex will',
5080 'channel_follower_count': int
5082 'playlist_mincount': 975,
5084 'note': 'Videos tab, sorted by popular',
5085 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos?view=0&sort=p&flow=grid',
5087 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5088 'title': 'lex will - Videos',
5089 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5090 'uploader': 'lex will',
5091 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5092 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5093 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5094 'channel': 'lex will',
5095 'tags': ['bible', 'history', 'prophesy'],
5096 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5097 'channel_follower_count': int
5099 'playlist_mincount': 199,
5101 'note': 'Playlists tab',
5102 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/playlists',
5104 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5105 'title': 'lex will - Playlists',
5106 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5107 'uploader': 'lex will',
5108 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5109 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5110 'channel': 'lex will',
5111 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5112 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5113 'tags': ['bible', 'history', 'prophesy'],
5114 'channel_follower_count': int
5116 'playlist_mincount': 17,
5118 'note': 'Community tab',
5119 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/community',
5121 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5122 'title': 'lex will - Community',
5123 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5124 'uploader': 'lex will',
5125 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5126 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5127 'channel': 'lex will',
5128 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5129 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5130 'tags': ['bible', 'history', 'prophesy'],
5131 'channel_follower_count': int
5133 'playlist_mincount': 18,
5135 'note': 'Channels tab',
5136 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/channels',
5138 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5139 'title': 'lex will - Channels',
5140 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5141 'uploader': 'lex will',
5142 'uploader_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5143 'uploader_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5144 'channel': 'lex will',
5145 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5146 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5147 'tags': ['bible', 'history', 'prophesy'],
5148 'channel_follower_count': int
5150 'playlist_mincount': 12,
5152 'note': 'Search tab',
5153 'url': 'https://www.youtube.com/c/3blue1brown/search?query=linear%20algebra',
5154 'playlist_mincount': 40,
5156 'id': 'UCYO_jab_esuFRV4b17AJtAw',
5157 'title': '3Blue1Brown - Search - linear algebra',
5158 'description': 'md5:e1384e8a133307dd10edee76e875d62f',
5159 'uploader': '3Blue1Brown',
5160 'uploader_id': 'UCYO_jab_esuFRV4b17AJtAw',
5161 'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
5162 'uploader_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
5163 'tags': ['Mathematics'],
5164 'channel': '3Blue1Brown',
5165 'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
5166 'channel_follower_count': int
5169 'url': 'https://invidio.us/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5170 'only_matching': True,
5172 'url': 'https://www.youtubekids.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5173 'only_matching': True,
5175 'url': 'https://music.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5176 'only_matching': True,
5178 'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
5179 'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
5181 'title': '29C3: Not my department',
5182 'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
5183 'uploader': 'Christiaan008',
5184 'uploader_id': 'UCEPzS1rYsrkqzSLNp76nrcg',
5185 'description': 'md5:a14dc1a8ef8307a9807fe136a0660268',
5187 'uploader_url': 'https://www.youtube.com/c/ChRiStIaAn008',
5189 'modified_date': '20150605',
5190 'channel_id': 'UCEPzS1rYsrkqzSLNp76nrcg',
5191 'channel_url': 'https://www.youtube.com/c/ChRiStIaAn008',
5192 'channel': 'Christiaan008',
5193 'availability': 'public',
5195 'playlist_count': 96,
5197 'note': 'Large playlist',
5198 'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
5200 'title': 'Uploads from Cauchemar',
5201 'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
5202 'uploader': 'Cauchemar',
5203 'uploader_id': 'UCBABnxM4Ar9ten8Mdjj1j0Q',
5204 'channel_url': 'https://www.youtube.com/c/Cauchemar89',
5206 'modified_date': r
're:\d{8}',
5207 'channel': 'Cauchemar',
5208 'uploader_url': 'https://www.youtube.com/c/Cauchemar89',
5211 'channel_id': 'UCBABnxM4Ar9ten8Mdjj1j0Q',
5212 'availability': 'public',
5214 'playlist_mincount': 1123,
5215 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5217 'note': 'even larger playlist, 8832 videos',
5218 'url': 'http://www.youtube.com/user/NASAgovVideo/videos',
5219 'only_matching': True,
5221 'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
5222 'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
5224 'title': 'Uploads from Interstellar Movie',
5225 'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
5226 'uploader': 'Interstellar Movie',
5227 'uploader_id': 'UCXw-G3eDE9trcvY2sBMM_aA',
5228 'uploader_url': 'https://www.youtube.com/c/InterstellarMovie',
5231 'channel_id': 'UCXw-G3eDE9trcvY2sBMM_aA',
5232 'channel_url': 'https://www.youtube.com/c/InterstellarMovie',
5233 'channel': 'Interstellar Movie',
5235 'modified_date': r
're:\d{8}',
5236 'availability': 'public',
5238 'playlist_mincount': 21,
5240 'note': 'Playlist with "show unavailable videos" button',
5241 'url': 'https://www.youtube.com/playlist?list=UUTYLiWFZy8xtPwxFwX9rV7Q',
5243 'title': 'Uploads from Phim Siêu Nhân Nhật Bản',
5244 'id': 'UUTYLiWFZy8xtPwxFwX9rV7Q',
5245 'uploader': 'Phim Siêu Nhân Nhật Bản',
5246 'uploader_id': 'UCTYLiWFZy8xtPwxFwX9rV7Q',
5248 'channel': 'Phim Siêu Nhân Nhật Bản',
5250 'uploader_url': 'https://www.youtube.com/channel/UCTYLiWFZy8xtPwxFwX9rV7Q',
5252 'channel_url': 'https://www.youtube.com/channel/UCTYLiWFZy8xtPwxFwX9rV7Q',
5253 'channel_id': 'UCTYLiWFZy8xtPwxFwX9rV7Q',
5254 'modified_date': r
're:\d{8}',
5255 'availability': 'public',
5257 'playlist_mincount': 200,
5258 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5260 'note': 'Playlist with unavailable videos in page 7',
5261 'url': 'https://www.youtube.com/playlist?list=UU8l9frL61Yl5KFOl87nIm2w',
5263 'title': 'Uploads from BlankTV',
5264 'id': 'UU8l9frL61Yl5KFOl87nIm2w',
5265 'uploader': 'BlankTV',
5266 'uploader_id': 'UC8l9frL61Yl5KFOl87nIm2w',
5267 'channel': 'BlankTV',
5268 'channel_url': 'https://www.youtube.com/c/blanktv',
5269 'channel_id': 'UC8l9frL61Yl5KFOl87nIm2w',
5272 'uploader_url': 'https://www.youtube.com/c/blanktv',
5273 'modified_date': r
're:\d{8}',
5275 'availability': 'public',
5277 'playlist_mincount': 1000,
5278 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5280 'note': 'https://github.com/ytdl-org/youtube-dl/issues/21844',
5281 'url': 'https://www.youtube.com/playlist?list=PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
5283 'title': 'Data Analysis with Dr Mike Pound',
5284 'id': 'PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
5285 'uploader_id': 'UC9-y-6csu5WGm29I7JiwpnA',
5286 'uploader': 'Computerphile',
5287 'description': 'md5:7f567c574d13d3f8c0954d9ffee4e487',
5288 'uploader_url': 'https://www.youtube.com/user/Computerphile',
5291 'channel_id': 'UC9-y-6csu5WGm29I7JiwpnA',
5292 'channel_url': 'https://www.youtube.com/user/Computerphile',
5293 'channel': 'Computerphile',
5294 'availability': 'public',
5296 'playlist_mincount': 11,
5298 'url': 'https://invidio.us/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5299 'only_matching': True,
5301 'note': 'Playlist URL that does not actually serve a playlist',
5302 'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
5304 'id': 'FqZTN594JQw',
5306 'title': "Smiley's People 01 detective, Adventure Series, Action",
5307 'uploader': 'STREEM',
5308 'uploader_id': 'UCyPhqAZgwYWZfxElWVbVJng',
5309 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/channel/UCyPhqAZgwYWZfxElWVbVJng',
5310 'upload_date': '20150526',
5311 'license': 'Standard YouTube License',
5312 'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
5313 'categories': ['People & Blogs'],
5319 'skip_download': True,
5321 'skip': 'This video is not available.',
5322 'add_ie': [YoutubeIE
.ie_key()],
5324 'url': 'https://www.youtubekids.com/watch?v=Agk7R8I8o5U&list=PUZ6jURNr1WQZCNHF0ao-c0g',
5325 'only_matching': True,
5327 'url': 'https://www.youtube.com/watch?v=MuAGGZNfUkU&list=RDMM',
5328 'only_matching': True,
5330 'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
5332 'id': 'Wq15eF5vCbI', # This will keep changing
5335 'uploader': 'Sky News',
5336 'uploader_id': 'skynews',
5337 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/skynews',
5338 'upload_date': r
're:\d{8}',
5340 'categories': ['News & Politics'],
5343 'release_timestamp': 1642502819,
5344 'channel': 'Sky News',
5345 'channel_id': 'UCoMdktPbSTixAyNGwb-UYkQ',
5348 'thumbnail': 'https://i.ytimg.com/vi/GgL890LIznQ/maxresdefault_live.jpg',
5349 'playable_in_embed': True,
5350 'release_date': '20220118',
5351 'availability': 'public',
5352 'live_status': 'is_live',
5353 'channel_url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ',
5354 'channel_follower_count': int
5357 'skip_download': True,
5359 'expected_warnings': ['Ignoring subtitle tracks found in '],
5361 'url': 'https://www.youtube.com/user/TheYoungTurks/live',
5363 'id': 'a48o2S1cPoo',
5365 'title': 'The Young Turks - Live Main Show',
5366 'uploader': 'The Young Turks',
5367 'uploader_id': 'TheYoungTurks',
5368 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/TheYoungTurks',
5369 'upload_date': '20150715',
5370 'license': 'Standard YouTube License',
5371 'description': 'md5:438179573adcdff3c97ebb1ee632b891',
5372 'categories': ['News & Politics'],
5373 'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
5377 'skip_download': True,
5379 'only_matching': True,
5381 'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
5382 'only_matching': True,
5384 'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
5385 'only_matching': True,
5387 'note': 'A channel that is not live. Should raise error',
5388 'url': 'https://www.youtube.com/user/numberphile/live',
5389 'only_matching': True,
5391 'url': 'https://www.youtube.com/feed/trending',
5392 'only_matching': True,
5394 'url': 'https://www.youtube.com/feed/library',
5395 'only_matching': True,
5397 'url': 'https://www.youtube.com/feed/history',
5398 'only_matching': True,
5400 'url': 'https://www.youtube.com/feed/subscriptions',
5401 'only_matching': True,
5403 'url': 'https://www.youtube.com/feed/watch_later',
5404 'only_matching': True,
5406 'note': 'Recommended - redirects to home page.',
5407 'url': 'https://www.youtube.com/feed/recommended',
5408 'only_matching': True,
5410 'note': 'inline playlist with not always working continuations',
5411 'url': 'https://www.youtube.com/watch?v=UC6u0Tct-Fo&list=PL36D642111D65BE7C',
5412 'only_matching': True,
5414 'url': 'https://www.youtube.com/course',
5415 'only_matching': True,
5417 'url': 'https://www.youtube.com/zsecurity',
5418 'only_matching': True,
5420 'url': 'http://www.youtube.com/NASAgovVideo/videos',
5421 'only_matching': True,
5423 'url': 'https://www.youtube.com/TheYoungTurks/live',
5424 'only_matching': True,
5426 'url': 'https://www.youtube.com/hashtag/cctv9',
5432 'playlist_mincount': 350,
5434 'url': 'https://www.youtube.com/watch?list=PLW4dVinRY435CBE_JD3t-0SRXKfnZHS1P&feature=youtu.be&v=M9cJMXmQ_ZU',
5435 'only_matching': True,
5437 'note': 'Requires Premium: should request additional YTM-info webpage (and have format 141) for videos in playlist',
5438 'url': 'https://music.youtube.com/playlist?list=PLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5439 'only_matching': True
5441 'note': '/browse/ should redirect to /channel/',
5442 'url': 'https://music.youtube.com/browse/UC1a8OFewdjuLq6KlF8M_8Ng',
5443 'only_matching': True
5445 'note': 'VLPL, should redirect to playlist?list=PL...',
5446 'url': 'https://music.youtube.com/browse/VLPLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5448 'id': 'PLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5449 'uploader': 'NoCopyrightSounds',
5450 'description': 'Providing you with copyright free / safe music for gaming, live streaming, studying and more!',
5451 'uploader_id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
5452 'title': 'NCS : All Releases 💿',
5453 'uploader_url': 'https://www.youtube.com/c/NoCopyrightSounds',
5454 'channel_url': 'https://www.youtube.com/c/NoCopyrightSounds',
5455 'modified_date': r
're:\d{8}',
5457 'channel_id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
5459 'channel': 'NoCopyrightSounds',
5460 'availability': 'public',
5462 'playlist_mincount': 166,
5463 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5465 'note': 'Topic, should redirect to playlist?list=UU...',
5466 'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
5468 'id': 'UU9ALqqC4aIeG5iDs7i90Bfw',
5469 'uploader_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5470 'title': 'Uploads from Royalty Free Music - Topic',
5471 'uploader': 'Royalty Free Music - Topic',
5473 'channel_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5474 'channel': 'Royalty Free Music - Topic',
5476 'channel_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5477 'channel_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5478 'modified_date': r
're:\d{8}',
5479 'uploader_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5481 'availability': 'public',
5483 'expected_warnings': [
5484 'The URL does not have a videos tab',
5485 r
'[Uu]navailable videos (are|will be) hidden',
5487 'playlist_mincount': 101,
5489 'note': 'Topic without a UU playlist',
5490 'url': 'https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg',
5492 'id': 'UCtFRv9O2AHqOZjjynzrv-xg',
5493 'title': 'UCtFRv9O2AHqOZjjynzrv-xg',
5496 'expected_warnings': [
5497 'the playlist redirect gave error',
5499 'playlist_mincount': 9,
5501 'note': 'Youtube music Album',
5502 'url': 'https://music.youtube.com/browse/MPREb_gTAcphH99wE',
5504 'id': 'OLAK5uy_l1m0thk3g31NmIIz_vMIbWtyv7eZixlH0',
5505 'title': 'Album - Royalty Free Music Library V2 (50 Songs)',
5509 'availability': 'unlisted',
5510 'modified_date': r
're:\d{8}',
5512 'playlist_count': 50,
5514 'note': 'unlisted single video playlist',
5515 'url': 'https://www.youtube.com/playlist?list=PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
5517 'uploader_id': 'UC9zHu_mHU96r19o-wV5Qs1Q',
5518 'uploader': 'colethedj',
5519 'id': 'PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
5520 'title': 'yt-dlp unlisted playlist test',
5521 'availability': 'unlisted',
5523 'modified_date': '20220418',
5524 'channel': 'colethedj',
5527 'uploader_url': 'https://www.youtube.com/channel/UC9zHu_mHU96r19o-wV5Qs1Q',
5528 'channel_id': 'UC9zHu_mHU96r19o-wV5Qs1Q',
5529 'channel_url': 'https://www.youtube.com/channel/UC9zHu_mHU96r19o-wV5Qs1Q',
5531 'playlist_count': 1,
5533 'note': 'API Fallback: Recommended - redirects to home page. Requires visitorData',
5534 'url': 'https://www.youtube.com/feed/recommended',
5536 'id': 'recommended',
5537 'title': 'recommended',
5540 'playlist_mincount': 50,
5542 'skip_download': True,
5543 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5546 'note': 'API Fallback: /videos tab, sorted by oldest first',
5547 'url': 'https://www.youtube.com/user/theCodyReeder/videos?view=0&sort=da&flow=grid',
5549 'id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
5550 'title': 'Cody\'sLab - Videos',
5551 'description': 'md5:d083b7c2f0c67ee7a6c74c3e9b4243fa',
5552 'uploader': 'Cody\'sLab',
5553 'uploader_id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
5554 'channel': 'Cody\'sLab',
5555 'channel_id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
5557 'channel_url': 'https://www.youtube.com/channel/UCu6mSoMNzHQiBIOCkHUa2Aw',
5558 'uploader_url': 'https://www.youtube.com/channel/UCu6mSoMNzHQiBIOCkHUa2Aw',
5559 'channel_follower_count': int
5561 'playlist_mincount': 650,
5563 'skip_download': True,
5564 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5567 'note': 'API Fallback: Topic, should redirect to playlist?list=UU...',
5568 'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
5570 'id': 'UU9ALqqC4aIeG5iDs7i90Bfw',
5571 'uploader_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5572 'title': 'Uploads from Royalty Free Music - Topic',
5573 'uploader': 'Royalty Free Music - Topic',
5574 'modified_date': r
're:\d{8}',
5575 'channel_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5577 'channel_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5579 'channel': 'Royalty Free Music - Topic',
5581 'uploader_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5582 'availability': 'public',
5584 'expected_warnings': [
5585 'does not have a videos tab',
5586 r
'[Uu]navailable videos (are|will be) hidden',
5588 'playlist_mincount': 101,
5590 'skip_download': True,
5591 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5594 'note': 'non-standard redirect to regional channel',
5595 'url': 'https://www.youtube.com/channel/UCwVVpHQ2Cs9iGJfpdFngePQ',
5596 'only_matching': True
5598 'note': 'collaborative playlist (uploader name in the form "by <uploader> and x other(s)")',
5599 'url': 'https://www.youtube.com/playlist?list=PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
5601 'id': 'PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
5602 'modified_date': '20220407',
5603 'channel_url': 'https://www.youtube.com/channel/UCKcqXmCcyqnhgpA5P0oHH_Q',
5605 'uploader_id': 'UCKcqXmCcyqnhgpA5P0oHH_Q',
5606 'uploader': 'pukkandan',
5607 'availability': 'unlisted',
5608 'channel_id': 'UCKcqXmCcyqnhgpA5P0oHH_Q',
5609 'channel': 'pukkandan',
5610 'description': 'Test for collaborative playlist',
5611 'title': 'yt-dlp test - collaborative playlist',
5613 'uploader_url': 'https://www.youtube.com/channel/UCKcqXmCcyqnhgpA5P0oHH_Q',
5615 'playlist_mincount': 2
5617 'note': 'translated tab name',
5618 'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/playlists',
5620 'id': 'UCiu-3thuViMebBjw_5nWYrA',
5622 'uploader_id': 'UCiu-3thuViMebBjw_5nWYrA',
5623 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5625 'title': 'cole-dlp-test-acc - 再生リスト',
5626 'uploader_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5627 'uploader': 'cole-dlp-test-acc',
5628 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
5629 'channel': 'cole-dlp-test-acc',
5631 'playlist_mincount': 1,
5632 'params': {'extractor_args': {'youtube': {'lang': ['ja']}
}},
5633 'expected_warnings': ['Preferring "ja"'],
5635 # XXX: this should really check flat playlist entries, but the test suite doesn't support that
5636 'note': 'preferred lang set with playlist with translated video titles',
5637 'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlQAaPZ5Z-rJoTdbT-45Q7c0',
5639 'id': 'PLt5yu3-wZAlQAaPZ5Z-rJoTdbT-45Q7c0',
5642 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5643 'uploader': 'cole-dlp-test-acc',
5644 'uploader_id': 'UCiu-3thuViMebBjw_5nWYrA',
5645 'channel': 'cole-dlp-test-acc',
5646 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
5647 'description': 'test',
5648 'uploader_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5649 'title': 'dlp test playlist',
5650 'availability': 'public',
5652 'playlist_mincount': 1,
5653 'params': {'extractor_args': {'youtube': {'lang': ['ja']}
}},
5654 'expected_warnings': ['Preferring "ja"'],
5656 # shorts audio pivot for 2GtVksBMYFM.
5657 'url': 'https://www.youtube.com/feed/sfv_audio_pivot?bp=8gUrCikSJwoLMkd0VmtzQk1ZRk0SCzJHdFZrc0JNWUZNGgsyR3RWa3NCTVlGTQ==',
5659 'id': 'sfv_audio_pivot',
5660 'title': 'sfv_audio_pivot',
5663 'playlist_mincount': 50,
5668 def suitable(cls
, url
):
5669 return False if YoutubeIE
.suitable(url
) else super().suitable(url
)
5671 _URL_RE
= re
.compile(rf
'(?P<pre>{_VALID_URL})(?(not_channel)|(?P<tab>/\w+))?(?P<post>.*)$')
5673 @YoutubeTabBaseInfoExtractor.passthrough_smuggled_data
5674 def _real_extract(self
, url
, smuggled_data
):
5675 item_id
= self
._match
_id
(url
)
5676 url
= urllib
.parse
.urlunparse(
5677 urllib
.parse
.urlparse(url
)._replace
(netloc
='www.youtube.com'))
5678 compat_opts
= self
.get_param('compat_opts', [])
5681 mobj
= self
._URL
_RE
.match(url
).groupdict()
5682 mobj
.update((k
, '') for k
, v
in mobj
.items() if v
is None)
5685 mobj
, redirect_warning
= get_mobj(url
), None
5686 # Youtube returns incomplete data if tabname is not lower case
5687 pre
, tab
, post
, is_channel
= mobj
['pre'], mobj
['tab'].lower(), mobj
['post'], not mobj
['not_channel']
5689 if smuggled_data
.get('is_music_url'):
5690 if item_id
[:2] == 'VL': # Youtube music VL channels have an equivalent playlist
5691 item_id
= item_id
[2:]
5692 pre
, tab
, post
, is_channel
= f
'https://www.youtube.com/playlist?list={item_id}', '', '', False
5693 elif item_id
[:2] == 'MP': # Resolve albums (/[channel/browse]/MP...) to their equivalent playlist
5694 mdata
= self
._extract
_tab
_endpoint
(
5695 f
'https://music.youtube.com/channel/{item_id}', item_id
, default_client
='web_music')
5696 murl
= traverse_obj(mdata
, ('microformat', 'microformatDataRenderer', 'urlCanonical'),
5697 get_all
=False, expected_type
=str)
5699 raise ExtractorError('Failed to resolve album to playlist')
5700 return self
.url_result(murl
, ie
=YoutubeTabIE
.ie_key())
5701 elif mobj
['channel_type'] == 'browse': # Youtube music /browse/ should be changed to /channel/
5702 pre
= f
'https://www.youtube.com/channel/{item_id}'
5704 original_tab_name
= tab
5705 if is_channel
and not tab
and 'no-youtube-channel-redirect' not in compat_opts
:
5706 # Home URLs should redirect to /videos/
5707 redirect_warning
= ('A channel/user page was given. All the channel\'s videos will be downloaded. '
5708 'To download only the videos in the home page, add a "/featured" to the URL')
5711 url
= ''.join((pre
, tab
, post
))
5712 mobj
= get_mobj(url
)
5714 # Handle both video/playlist URLs
5716 video_id
, playlist_id
= (qs
.get(key
, [None])[0] for key
in ('v', 'list'))
5718 if not video_id
and mobj
['not_channel'].startswith('watch'):
5720 # If there is neither video or playlist ids, youtube redirects to home page, which is undesirable
5721 raise ExtractorError('Unable to recognize tab page')
5722 # Common mistake: https://www.youtube.com/watch?list=playlist_id
5723 self
.report_warning(f
'A video URL was given without video ID. Trying to download playlist {playlist_id}')
5724 url
= f
'https://www.youtube.com/playlist?list={playlist_id}'
5725 mobj
= get_mobj(url
)
5727 if video_id
and playlist_id
:
5728 if self
.get_param('noplaylist'):
5729 self
.to_screen(f
'Downloading just video {video_id} because of --no-playlist')
5730 return self
.url_result(f
'https://www.youtube.com/watch?v={video_id}',
5731 ie
=YoutubeIE
.ie_key(), video_id
=video_id
)
5732 self
.to_screen(f
'Downloading playlist {playlist_id}; add --no-playlist to just download video {video_id}')
5734 data
, ytcfg
= self
._extract
_data
(url
, item_id
)
5736 # YouTube may provide a non-standard redirect to the regional channel
5737 # See: https://github.com/yt-dlp/yt-dlp/issues/2694
5738 redirect_url
= traverse_obj(
5739 data
, ('onResponseReceivedActions', ..., 'navigateAction', 'endpoint', 'commandMetadata', 'webCommandMetadata', 'url'), get_all
=False)
5740 if redirect_url
and 'no-youtube-channel-redirect' not in compat_opts
:
5741 redirect_url
= ''.join((
5742 urljoin('https://www.youtube.com', redirect_url
), mobj
['tab'], mobj
['post']))
5743 self
.to_screen(f
'This playlist is likely not available in your region. Following redirect to regional playlist {redirect_url}')
5744 return self
.url_result(redirect_url
, ie
=YoutubeTabIE
.ie_key())
5746 tabs
= traverse_obj(data
, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type
=list)
5748 selected_tab
= self
._extract
_selected
_tab
(tabs
)
5749 selected_tab_url
= urljoin(
5750 url
, traverse_obj(selected_tab
, ('endpoint', 'commandMetadata', 'webCommandMetadata', 'url')))
5751 translated_tab_name
= selected_tab
.get('title', '').lower()
5753 # Prefer tab name from tab url as it is always in en,
5754 # but only when preferred lang is set as it may not extract reliably in all cases.
5755 selected_tab_name
= (self
._preferred
_lang
in (None, 'en') and translated_tab_name
5756 or selected_tab_url
and get_mobj(selected_tab_url
)['tab'][1:] # primary
5757 or translated_tab_name
)
5759 if selected_tab_name
== 'home':
5760 selected_tab_name
= 'featured'
5761 requested_tab_name
= mobj
['tab'][1:]
5763 if 'no-youtube-channel-redirect' not in compat_opts
:
5764 if requested_tab_name
== 'live': # Live tab should have redirected to the video
5765 raise UserNotLive(video_id
=mobj
['id'])
5766 if requested_tab_name
not in ('', selected_tab_name
):
5767 redirect_warning
= f
'The channel does not have a {requested_tab_name} tab'
5768 if not original_tab_name
:
5769 if item_id
[:2] == 'UC':
5770 # Topic channels don't have /videos. Use the equivalent playlist instead
5771 pl_id
= f
'UU{item_id[2:]}'
5772 pl_url
= f
'https://www.youtube.com/playlist?list={pl_id}'
5774 data
, ytcfg
= self
._extract
_data
(pl_url
, pl_id
, ytcfg
=ytcfg
, fatal
=True, webpage_fatal
=True)
5775 except ExtractorError
:
5776 redirect_warning
+= ' and the playlist redirect gave error'
5778 item_id
, url
, selected_tab_name
= pl_id
, pl_url
, requested_tab_name
5779 redirect_warning
+= f
'. Redirecting to playlist {pl_id} instead'
5780 if selected_tab_name
and selected_tab_name
!= requested_tab_name
:
5781 redirect_warning
+= f
'. {selected_tab_name} tab is being downloaded instead'
5783 raise ExtractorError(redirect_warning
, expected
=True)
5785 if redirect_warning
:
5786 self
.to_screen(redirect_warning
)
5787 self
.write_debug(f
'Final URL: {url}')
5789 # YouTube sometimes provides a button to reload playlist with unavailable videos.
5790 if 'no-youtube-unavailable-videos' not in compat_opts
:
5791 data
= self
._reload
_with
_unavailable
_videos
(item_id
, data
, ytcfg
) or data
5792 self
._extract
_and
_report
_alerts
(data
, only_once
=True)
5793 tabs
= traverse_obj(data
, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs'), expected_type
=list)
5795 return self
._extract
_from
_tabs
(item_id
, ytcfg
, data
, tabs
)
5797 playlist
= traverse_obj(
5798 data
, ('contents', 'twoColumnWatchNextResults', 'playlist', 'playlist'), expected_type
=dict)
5800 return self
._extract
_from
_playlist
(item_id
, url
, data
, playlist
, ytcfg
)
5802 video_id
= traverse_obj(
5803 data
, ('currentVideoEndpoint', 'watchEndpoint', 'videoId'), expected_type
=str) or video_id
5805 if mobj
['tab'] != '/live': # live tab is expected to redirect to video
5806 self
.report_warning(f
'Unable to recognize playlist. Downloading just video {video_id}')
5807 return self
.url_result(f
'https://www.youtube.com/watch?v={video_id}',
5808 ie
=YoutubeIE
.ie_key(), video_id
=video_id
)
5810 raise ExtractorError('Unable to recognize tab page')
5813 class YoutubePlaylistIE(InfoExtractor
):
5814 IE_DESC
= 'YouTube playlists'
5815 _VALID_URL
= r
'''(?x)(?:
5820 youtube(?:kids)?\.com|
5825 (?P<id>%(playlist_id)s)
5827 'playlist_id': YoutubeBaseInfoExtractor
._PLAYLIST
_ID
_RE
,
5828 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
5830 IE_NAME
= 'youtube:playlist'
5832 'note': 'issue #673',
5833 'url': 'PLBB231211A4F62143',
5835 'title': '[OLD]Team Fortress 2 (Class-based LP)',
5836 'id': 'PLBB231211A4F62143',
5837 'uploader': 'Wickman',
5838 'uploader_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
5839 'description': 'md5:8fa6f52abb47a9552002fa3ddfc57fc2',
5841 'uploader_url': 'https://www.youtube.com/user/Wickydoo',
5842 'modified_date': r
're:\d{8}',
5843 'channel_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
5844 'channel': 'Wickman',
5846 'channel_url': 'https://www.youtube.com/user/Wickydoo',
5848 'playlist_mincount': 29,
5850 'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
5852 'title': 'YDL_safe_search',
5853 'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
5855 'playlist_count': 2,
5856 'skip': 'This playlist is private',
5859 'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
5860 'playlist_count': 4,
5863 'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
5864 'uploader': 'milan',
5865 'uploader_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
5867 'channel_url': 'https://www.youtube.com/channel/UCEI1-PVPcYXjB73Hfelbmaw',
5869 'modified_date': '20140919',
5872 'channel_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
5873 'uploader_url': 'https://www.youtube.com/channel/UCEI1-PVPcYXjB73Hfelbmaw',
5874 'availability': 'public',
5876 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5878 'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
5879 'playlist_mincount': 455,
5881 'title': '2018 Chinese New Singles (11/6 updated)',
5882 'id': 'PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
5884 'uploader_id': 'UC21nz3_MesPLqtDqwdvnoxA',
5885 'description': 'md5:da521864744d60a198e3a88af4db0d9d',
5888 'channel_url': 'https://www.youtube.com/c/愛低音的國王',
5890 'uploader_url': 'https://www.youtube.com/c/愛低音的國王',
5891 'channel_id': 'UC21nz3_MesPLqtDqwdvnoxA',
5892 'modified_date': r
're:\d{8}',
5893 'availability': 'public',
5895 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5897 'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
5898 'only_matching': True,
5900 # music album playlist
5901 'url': 'OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM',
5902 'only_matching': True,
5906 def suitable(cls
, url
):
5907 if YoutubeTabIE
.suitable(url
):
5909 from ..utils
import parse_qs
5911 if qs
.get('v', [None])[0]:
5913 return super().suitable(url
)
5915 def _real_extract(self
, url
):
5916 playlist_id
= self
._match
_id
(url
)
5917 is_music_url
= YoutubeBaseInfoExtractor
.is_music_url(url
)
5918 url
= update_url_query(
5919 'https://www.youtube.com/playlist',
5920 parse_qs(url
) or {'list': playlist_id}
)
5922 url
= smuggle_url(url
, {'is_music_url': True}
)
5923 return self
.url_result(url
, ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
5926 class YoutubeYtBeIE(InfoExtractor
):
5927 IE_DESC
= 'youtu.be'
5928 _VALID_URL
= r
'https?://youtu\.be/(?P<id>[0-9A-Za-z_-]{11})/*?.*?\blist=(?P<playlist_id>%(playlist_id)s)' % {'playlist_id': YoutubeBaseInfoExtractor._PLAYLIST_ID_RE}
5930 'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
5932 'id': 'yeWKywCrFtk',
5934 'title': 'Small Scale Baler and Braiding Rugs',
5935 'uploader': 'Backus-Page House Museum',
5936 'uploader_id': 'backuspagemuseum',
5937 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/user/backuspagemuseum',
5938 'upload_date': '20161008',
5939 'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
5940 'categories': ['Nonprofits & Activism'],
5944 'playable_in_embed': True,
5945 'thumbnail': 'https://i.ytimg.com/vi_webp/yeWKywCrFtk/maxresdefault.webp',
5946 'channel': 'Backus-Page House Museum',
5947 'channel_id': 'UCEfMCQ9bs3tjvjy1s451zaw',
5948 'live_status': 'not_live',
5950 'channel_url': 'https://www.youtube.com/channel/UCEfMCQ9bs3tjvjy1s451zaw',
5951 'availability': 'public',
5953 'comment_count': int,
5954 'channel_follower_count': int
5958 'skip_download': True,
5961 'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
5962 'only_matching': True,
5965 def _real_extract(self
, url
):
5966 mobj
= self
._match
_valid
_url
(url
)
5967 video_id
= mobj
.group('id')
5968 playlist_id
= mobj
.group('playlist_id')
5969 return self
.url_result(
5970 update_url_query('https://www.youtube.com/watch', {
5972 'list': playlist_id
,
5973 'feature': 'youtu.be',
5974 }), ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
5977 class YoutubeLivestreamEmbedIE(InfoExtractor
):
5978 IE_DESC
= 'YouTube livestream embeds'
5979 _VALID_URL
= r
'https?://(?:\w+\.)?youtube\.com/embed/live_stream/?\?(?:[^#]+&)?channel=(?P<id>[^&#]+)'
5981 'url': 'https://www.youtube.com/embed/live_stream?channel=UC2_KI6RB__jGdlnK6dvFEZA',
5982 'only_matching': True,
5985 def _real_extract(self
, url
):
5986 channel_id
= self
._match
_id
(url
)
5987 return self
.url_result(
5988 f
'https://www.youtube.com/channel/{channel_id}/live',
5989 ie
=YoutubeTabIE
.ie_key(), video_id
=channel_id
)
5992 class YoutubeYtUserIE(InfoExtractor
):
5993 IE_DESC
= 'YouTube user videos; "ytuser:" prefix'
5994 IE_NAME
= 'youtube:user'
5995 _VALID_URL
= r
'ytuser:(?P<id>.+)'
5997 'url': 'ytuser:phihag',
5998 'only_matching': True,
6001 def _real_extract(self
, url
):
6002 user_id
= self
._match
_id
(url
)
6003 return self
.url_result(
6004 'https://www.youtube.com/user/%s/videos' % user_id
,
6005 ie
=YoutubeTabIE
.ie_key(), video_id
=user_id
)
6008 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor
):
6009 IE_NAME
= 'youtube:favorites'
6010 IE_DESC
= 'YouTube liked videos; ":ytfav" keyword (requires cookies)'
6011 _VALID_URL
= r
':ytfav(?:ou?rite)?s?'
6012 _LOGIN_REQUIRED
= True
6015 'only_matching': True,
6017 'url': ':ytfavorites',
6018 'only_matching': True,
6021 def _real_extract(self
, url
):
6022 return self
.url_result(
6023 'https://www.youtube.com/playlist?list=LL',
6024 ie
=YoutubeTabIE
.ie_key())
6027 class YoutubeNotificationsIE(YoutubeTabBaseInfoExtractor
):
6028 IE_NAME
= 'youtube:notif'
6029 IE_DESC
= 'YouTube notifications; ":ytnotif" keyword (requires cookies)'
6030 _VALID_URL
= r
':ytnotif(?:ication)?s?'
6031 _LOGIN_REQUIRED
= True
6034 'only_matching': True,
6036 'url': ':ytnotifications',
6037 'only_matching': True,
6040 def _extract_notification_menu(self
, response
, continuation_list
):
6041 notification_list
= traverse_obj(
6043 ('actions', 0, 'openPopupAction', 'popup', 'multiPageMenuRenderer', 'sections', 0, 'multiPageMenuNotificationSectionRenderer', 'items'),
6044 ('actions', 0, 'appendContinuationItemsAction', 'continuationItems'),
6045 expected_type
=list) or []
6046 continuation_list
[0] = None
6047 for item
in notification_list
:
6048 entry
= self
._extract
_notification
_renderer
(item
.get('notificationRenderer'))
6051 continuation
= item
.get('continuationItemRenderer')
6053 continuation_list
[0] = continuation
6055 def _extract_notification_renderer(self
, notification
):
6056 video_id
= traverse_obj(
6057 notification
, ('navigationEndpoint', 'watchEndpoint', 'videoId'), expected_type
=str)
6058 url
= f
'https://www.youtube.com/watch?v={video_id}'
6061 browse_ep
= traverse_obj(
6062 notification
, ('navigationEndpoint', 'browseEndpoint'), expected_type
=dict)
6063 channel_id
= traverse_obj(browse_ep
, 'browseId', expected_type
=str)
6064 post_id
= self
._search
_regex
(
6065 r
'/post/(.+)', traverse_obj(browse_ep
, 'canonicalBaseUrl', expected_type
=str),
6066 'post id', default
=None)
6067 if not channel_id
or not post_id
:
6069 # The direct /post url redirects to this in the browser
6070 url
= f
'https://www.youtube.com/channel/{channel_id}/community?lb={post_id}'
6072 channel
= traverse_obj(
6073 notification
, ('contextualMenu', 'menuRenderer', 'items', 1, 'menuServiceItemRenderer', 'text', 'runs', 1, 'text'),
6075 notification_title
= self
._get
_text
(notification
, 'shortMessage')
6076 if notification_title
:
6077 notification_title
= notification_title
.replace('\xad', '') # remove soft hyphens
6078 # TODO: handle recommended videos
6079 title
= self
._search
_regex
(
6080 rf
'{re.escape(channel or "")}[^:]+: (.+)', notification_title
,
6081 'video title', default
=None)
6082 upload_date
= (strftime_or_none(self
._parse
_time
_text
(self
._get
_text
(notification
, 'sentTimeText')), '%Y%m%d')
6083 if self
._configuration
_arg
('approximate_date', ie_key
=YoutubeTabIE
.ie_key())
6088 'ie_key': (YoutubeIE
if video_id
else YoutubeTabIE
).ie_key(),
6089 'video_id': video_id
,
6091 'channel_id': channel_id
,
6093 'thumbnails': self
._extract
_thumbnails
(notification
, 'videoThumbnail'),
6094 'upload_date': upload_date
,
6097 def _notification_menu_entries(self
, ytcfg
):
6098 continuation_list
= [None]
6100 for page
in itertools
.count(1):
6101 ctoken
= traverse_obj(
6102 continuation_list
, (0, 'continuationEndpoint', 'getNotificationMenuEndpoint', 'ctoken'), expected_type
=str)
6103 response
= self
._extract
_response
(
6104 item_id
=f
'page {page}', query
={'ctoken': ctoken}
if ctoken
else {}, ytcfg
=ytcfg
,
6105 ep
='notification/get_notification_menu', check_get_keys
='actions',
6106 headers
=self
.generate_api_headers(ytcfg
=ytcfg
, visitor_data
=self
._extract
_visitor
_data
(response
)))
6107 yield from self
._extract
_notification
_menu
(response
, continuation_list
)
6108 if not continuation_list
[0]:
6111 def _real_extract(self
, url
):
6112 display_id
= 'notifications'
6113 ytcfg
= self
._download
_ytcfg
('web', display_id
) if not self
.skip_webpage
else {}
6114 self
._report
_playlist
_authcheck
(ytcfg
)
6115 return self
.playlist_result(self
._notification
_menu
_entries
(ytcfg
), display_id
, display_id
)
6118 class YoutubeSearchIE(YoutubeTabBaseInfoExtractor
, SearchInfoExtractor
):
6119 IE_DESC
= 'YouTube search'
6120 IE_NAME
= 'youtube:search'
6121 _SEARCH_KEY
= 'ytsearch'
6122 _SEARCH_PARAMS
= 'EgIQAQ%3D%3D' # Videos only
6124 'url': 'ytsearch5:youtube-dl test video',
6125 'playlist_count': 5,
6127 'id': 'youtube-dl test video',
6128 'title': 'youtube-dl test video',
6133 class YoutubeSearchDateIE(YoutubeTabBaseInfoExtractor
, SearchInfoExtractor
):
6134 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ ':date'
6135 _SEARCH_KEY
= 'ytsearchdate'
6136 IE_DESC
= 'YouTube search, newest videos first'
6137 _SEARCH_PARAMS
= 'CAISAhAB' # Videos only, sorted by date
6139 'url': 'ytsearchdate5:youtube-dl test video',
6140 'playlist_count': 5,
6142 'id': 'youtube-dl test video',
6143 'title': 'youtube-dl test video',
6148 class YoutubeSearchURLIE(YoutubeTabBaseInfoExtractor
):
6149 IE_DESC
= 'YouTube search URLs with sorting and filter support'
6150 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ '_url'
6151 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/(?:results|search)\?([^#]+&)?(?:search_query|q)=(?:[^&]+)(?:[&#]|$)'
6153 'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
6154 'playlist_mincount': 5,
6156 'id': 'youtube-dl test video',
6157 'title': 'youtube-dl test video',
6160 'url': 'https://www.youtube.com/results?search_query=python&sp=EgIQAg%253D%253D',
6161 'playlist_mincount': 5,
6167 'url': 'https://www.youtube.com/results?search_query=%23cats',
6168 'playlist_mincount': 1,
6172 # The test suite does not have support for nested playlists
6174 # 'url': r're:https://(www\.)?youtube\.com/hashtag/cats',
6179 'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
6180 'only_matching': True,
6183 def _real_extract(self
, url
):
6185 query
= (qs
.get('search_query') or qs
.get('q'))[0]
6186 return self
.playlist_result(self
._search
_results
(query
, qs
.get('sp', (None,))[0]), query
, query
)
6189 class YoutubeMusicSearchURLIE(YoutubeTabBaseInfoExtractor
):
6190 IE_DESC
= 'YouTube music search URLs with selectable sections, e.g. #songs'
6191 IE_NAME
= 'youtube:music:search_url'
6192 _VALID_URL
= r
'https?://music\.youtube\.com/search\?([^#]+&)?(?:search_query|q)=(?:[^&]+)(?:[&#]|$)'
6194 'url': 'https://music.youtube.com/search?q=royalty+free+music',
6195 'playlist_count': 16,
6197 'id': 'royalty free music',
6198 'title': 'royalty free music',
6201 'url': 'https://music.youtube.com/search?q=royalty+free+music&sp=EgWKAQIIAWoKEAoQAxAEEAkQBQ%3D%3D',
6202 'playlist_mincount': 30,
6204 'id': 'royalty free music - songs',
6205 'title': 'royalty free music - songs',
6207 'params': {'extract_flat': 'in_playlist'}
6209 'url': 'https://music.youtube.com/search?q=royalty+free+music#community+playlists',
6210 'playlist_mincount': 30,
6212 'id': 'royalty free music - community playlists',
6213 'title': 'royalty free music - community playlists',
6215 'params': {'extract_flat': 'in_playlist'}
6219 'albums': 'EgWKAQIYAWoKEAoQAxAEEAkQBQ==',
6220 'artists': 'EgWKAQIgAWoKEAoQAxAEEAkQBQ==',
6221 'community playlists': 'EgeKAQQoAEABagoQChADEAQQCRAF',
6222 'featured playlists': 'EgeKAQQoADgBagwQAxAJEAQQDhAKEAU==',
6223 'songs': 'EgWKAQIIAWoKEAoQAxAEEAkQBQ==',
6224 'videos': 'EgWKAQIQAWoKEAoQAxAEEAkQBQ==',
6227 def _real_extract(self
, url
):
6229 query
= (qs
.get('search_query') or qs
.get('q'))[0]
6230 params
= qs
.get('sp', (None,))[0]
6232 section
= next((k
for k
, v
in self
._SECTIONS
.items() if v
== params
), params
)
6234 section
= urllib
.parse
.unquote_plus((url
.split('#') + [''])[1]).lower()
6235 params
= self
._SECTIONS
.get(section
)
6238 title
= join_nonempty(query
, section
, delim
=' - ')
6239 return self
.playlist_result(self
._search
_results
(query
, params
, default_client
='web_music'), title
, title
)
6242 class YoutubeFeedsInfoExtractor(InfoExtractor
):
6244 Base class for feed extractors
6245 Subclasses must re-define the _FEED_NAME property.
6247 _LOGIN_REQUIRED
= True
6248 _FEED_NAME
= 'feeds'
6250 def _real_initialize(self
):
6251 YoutubeBaseInfoExtractor
._check
_login
_required
(self
)
6255 return f
'youtube:{self._FEED_NAME}'
6257 def _real_extract(self
, url
):
6258 return self
.url_result(
6259 f
'https://www.youtube.com/feed/{self._FEED_NAME}', ie
=YoutubeTabIE
.ie_key())
6262 class YoutubeWatchLaterIE(InfoExtractor
):
6263 IE_NAME
= 'youtube:watchlater'
6264 IE_DESC
= 'Youtube watch later list; ":ytwatchlater" keyword (requires cookies)'
6265 _VALID_URL
= r
':ytwatchlater'
6267 'url': ':ytwatchlater',
6268 'only_matching': True,
6271 def _real_extract(self
, url
):
6272 return self
.url_result(
6273 'https://www.youtube.com/playlist?list=WL', ie
=YoutubeTabIE
.ie_key())
6276 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor
):
6277 IE_DESC
= 'YouTube recommended videos; ":ytrec" keyword'
6278 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/?(?:[?#]|$)|:ytrec(?:ommended)?'
6279 _FEED_NAME
= 'recommended'
6280 _LOGIN_REQUIRED
= False
6283 'only_matching': True,
6285 'url': ':ytrecommended',
6286 'only_matching': True,
6288 'url': 'https://youtube.com',
6289 'only_matching': True,
6293 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor
):
6294 IE_DESC
= 'YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)'
6295 _VALID_URL
= r
':ytsub(?:scription)?s?'
6296 _FEED_NAME
= 'subscriptions'
6299 'only_matching': True,
6301 'url': ':ytsubscriptions',
6302 'only_matching': True,
6306 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor
):
6307 IE_DESC
= 'Youtube watch history; ":ythis" keyword (requires cookies)'
6308 _VALID_URL
= r
':ythis(?:tory)?'
6309 _FEED_NAME
= 'history'
6311 'url': ':ythistory',
6312 'only_matching': True,
6316 class YoutubeStoriesIE(InfoExtractor
):
6317 IE_DESC
= 'YouTube channel stories; "ytstories:" prefix'
6318 IE_NAME
= 'youtube:stories'
6319 _VALID_URL
= r
'ytstories:UC(?P<id>[A-Za-z0-9_-]{21}[AQgw])$'
6321 'url': 'ytstories:UCwFCb4jeqaKWnciAYM-ZVHg',
6322 'only_matching': True,
6325 def _real_extract(self
, url
):
6326 playlist_id
= f
'RLTD{self._match_id(url)}'
6327 return self
.url_result(
6328 smuggle_url(f
'https://www.youtube.com/playlist?list={playlist_id}&playnext=1', {'is_story': True}
),
6329 ie
=YoutubeTabIE
, video_id
=playlist_id
)
6332 class YoutubeShortsAudioPivotIE(InfoExtractor
):
6333 IE_DESC
= 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
6334 IE_NAME
= 'youtube:shorts:pivot:audio'
6335 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/source/(?P<id>[\w-]{11})/shorts'
6337 'url': 'https://www.youtube.com/source/Lyj-MZSAA9o/shorts',
6338 'only_matching': True,
6342 def _generate_audio_pivot_params(video_id
):
6344 Generates sfv_audio_pivot browse params for this video id
6346 pb_params
= b
'\xf2\x05+\n)\x12\'\n\x0b%b\x12\x0b%b\x1a\x0b%b' % ((video_id
.encode(),) * 3)
6347 return urllib
.parse
.quote(base64
.b64encode(pb_params
).decode())
6349 def _real_extract(self
, url
):
6350 video_id
= self
._match
_id
(url
)
6351 return self
.url_result(
6352 f
'https://www.youtube.com/feed/sfv_audio_pivot?bp={self._generate_audio_pivot_params(video_id)}',
6356 class YoutubeTruncatedURLIE(InfoExtractor
):
6357 IE_NAME
= 'youtube:truncated_url'
6358 IE_DESC
= False # Do not list
6359 _VALID_URL
= r
'''(?x)
6361 (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
6364 annotation_id=annotation_[^&]+|
6370 attribution_link\?a=[^&]+
6376 'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
6377 'only_matching': True,
6379 'url': 'https://www.youtube.com/watch?',
6380 'only_matching': True,
6382 'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
6383 'only_matching': True,
6385 'url': 'https://www.youtube.com/watch?feature=foo',
6386 'only_matching': True,
6388 'url': 'https://www.youtube.com/watch?hl=en-GB',
6389 'only_matching': True,
6391 'url': 'https://www.youtube.com/watch?t=2372',
6392 'only_matching': True,
6395 def _real_extract(self
, url
):
6396 raise ExtractorError(
6397 'Did you forget to quote the URL? Remember that & is a meta '
6398 'character in most shells, so you want to put the URL in quotes, '
6400 '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
6401 ' or simply youtube-dl BaW_jenozKc .',
6405 class YoutubeClipIE(YoutubeTabBaseInfoExtractor
):
6406 IE_NAME
= 'youtube:clip'
6407 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/clip/(?P<id>[^/?#]+)'
6409 # FIXME: Other metadata should be extracted from the clip, not from the base video
6410 'url': 'https://www.youtube.com/clip/UgytZKpehg-hEMBSn3F4AaABCQ',
6412 'id': 'UgytZKpehg-hEMBSn3F4AaABCQ',
6414 'section_start': 29.0,
6415 'section_end': 39.7,
6418 'availability': 'public',
6419 'categories': ['Gaming'],
6420 'channel': 'Scott The Woz',
6421 'channel_id': 'UC4rqhyiTs7XyuODcECvuiiQ',
6422 'channel_url': 'https://www.youtube.com/channel/UC4rqhyiTs7XyuODcECvuiiQ',
6423 'description': 'md5:7a4517a17ea9b4bd98996399d8bb36e7',
6425 'playable_in_embed': True,
6427 'thumbnail': 'https://i.ytimg.com/vi_webp/ScPX26pdQik/maxresdefault.webp',
6428 'title': 'Mobile Games on Console - Scott The Woz',
6429 'upload_date': '20210920',
6430 'uploader': 'Scott The Woz',
6431 'uploader_id': 'scottthewoz',
6432 'uploader_url': 'http://www.youtube.com/user/scottthewoz',
6434 'live_status': 'not_live',
6435 'channel_follower_count': int
6439 def _real_extract(self
, url
):
6440 clip_id
= self
._match
_id
(url
)
6441 _
, data
= self
._extract
_webpage
(url
, clip_id
)
6443 video_id
= traverse_obj(data
, ('currentVideoEndpoint', 'watchEndpoint', 'videoId'))
6445 raise ExtractorError('Unable to find video ID')
6447 clip_data
= traverse_obj(data
, (
6448 'engagementPanels', ..., 'engagementPanelSectionListRenderer', 'content', 'clipSectionRenderer',
6449 'contents', ..., 'clipAttributionRenderer', 'onScrubExit', 'commandExecutorCommand', 'commands', ...,
6450 'openPopupAction', 'popup', 'notificationActionRenderer', 'actionButton', 'buttonRenderer', 'command',
6451 'commandExecutorCommand', 'commands', ..., 'loopCommand'), get_all
=False)
6454 '_type': 'url_transparent',
6455 'url': f
'https://www.youtube.com/watch?v={video_id}',
6456 'ie_key': YoutubeIE
.ie_key(),
6458 'section_start': int(clip_data
['startTimeMs']) / 1000,
6459 'section_end': int(clip_data
['endTimeMs']) / 1000,
6463 class YoutubeTruncatedIDIE(InfoExtractor
):
6464 IE_NAME
= 'youtube:truncated_id'
6465 IE_DESC
= False # Do not list
6466 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
6469 'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
6470 'only_matching': True,
6473 def _real_extract(self
, url
):
6474 video_id
= self
._match
_id
(url
)
6475 raise ExtractorError(
6476 f
'Incomplete YouTube ID {video_id}. URL {url} looks truncated.',