21 from .common
import InfoExtractor
, SearchInfoExtractor
22 from .openload
import PhantomJSwrapper
23 from ..compat
import functools
24 from ..jsinterp
import JSInterpreter
69 STREAMING_DATA_CLIENT_NAME
= '__yt_dlp_client'
70 # any clients starting with _ cannot be explicitly requested by the user
73 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
74 'INNERTUBE_CONTEXT': {
77 'clientVersion': '2.20220801.00.00',
80 'INNERTUBE_CONTEXT_CLIENT_NAME': 1
83 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
84 'INNERTUBE_CONTEXT': {
86 'clientName': 'WEB_EMBEDDED_PLAYER',
87 'clientVersion': '1.20220731.00.00',
90 'INNERTUBE_CONTEXT_CLIENT_NAME': 56
93 'INNERTUBE_API_KEY': 'AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30',
94 'INNERTUBE_HOST': 'music.youtube.com',
95 'INNERTUBE_CONTEXT': {
97 'clientName': 'WEB_REMIX',
98 'clientVersion': '1.20220727.01.00',
101 'INNERTUBE_CONTEXT_CLIENT_NAME': 67,
104 'INNERTUBE_API_KEY': 'AIzaSyBUPetSUmoZL-OhlxA7wSac5XinrygCqMo',
105 'INNERTUBE_CONTEXT': {
107 'clientName': 'WEB_CREATOR',
108 'clientVersion': '1.20220726.00.00',
111 'INNERTUBE_CONTEXT_CLIENT_NAME': 62,
114 'INNERTUBE_API_KEY': 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM39w',
115 'INNERTUBE_CONTEXT': {
117 'clientName': 'ANDROID',
118 'clientVersion': '17.31.35',
119 'androidSdkVersion': 30,
120 'userAgent': 'com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip'
123 'INNERTUBE_CONTEXT_CLIENT_NAME': 3,
124 'REQUIRE_JS_PLAYER': False
126 'android_embedded': {
127 'INNERTUBE_API_KEY': 'AIzaSyCjc_pVEDi4qsv5MtC2dMXzpIaDoRFLsxw',
128 'INNERTUBE_CONTEXT': {
130 'clientName': 'ANDROID_EMBEDDED_PLAYER',
131 'clientVersion': '17.31.35',
132 'androidSdkVersion': 30,
133 'userAgent': 'com.google.android.youtube/17.31.35 (Linux; U; Android 11) gzip'
136 'INNERTUBE_CONTEXT_CLIENT_NAME': 55,
137 'REQUIRE_JS_PLAYER': False
140 'INNERTUBE_API_KEY': 'AIzaSyAOghZGza2MQSZkY_zfZ370N-PUdXEo8AI',
141 'INNERTUBE_CONTEXT': {
143 'clientName': 'ANDROID_MUSIC',
144 'clientVersion': '5.16.51',
145 'androidSdkVersion': 30,
146 'userAgent': 'com.google.android.apps.youtube.music/5.16.51 (Linux; U; Android 11) gzip'
149 'INNERTUBE_CONTEXT_CLIENT_NAME': 21,
150 'REQUIRE_JS_PLAYER': False
153 'INNERTUBE_API_KEY': 'AIzaSyD_qjV8zaaUMehtLkrKFgVeSX_Iqbtyws8',
154 'INNERTUBE_CONTEXT': {
156 'clientName': 'ANDROID_CREATOR',
157 'clientVersion': '22.30.100',
158 'androidSdkVersion': 30,
159 'userAgent': 'com.google.android.apps.youtube.creator/22.30.100 (Linux; U; Android 11) gzip'
162 'INNERTUBE_CONTEXT_CLIENT_NAME': 14,
163 'REQUIRE_JS_PLAYER': False
165 # iOS clients have HLS live streams. Setting device model to get 60fps formats.
166 # See: https://github.com/TeamNewPipe/NewPipeExtractor/issues/680#issuecomment-1002724558
168 'INNERTUBE_API_KEY': 'AIzaSyB-63vPrdThhKuerbB2N_l7Kwwcxj6yUAc',
169 'INNERTUBE_CONTEXT': {
172 'clientVersion': '17.33.2',
173 'deviceModel': 'iPhone14,3',
174 'userAgent': 'com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
177 'INNERTUBE_CONTEXT_CLIENT_NAME': 5,
178 'REQUIRE_JS_PLAYER': False
181 'INNERTUBE_CONTEXT': {
183 'clientName': 'IOS_MESSAGES_EXTENSION',
184 'clientVersion': '17.33.2',
185 'deviceModel': 'iPhone14,3',
186 'userAgent': 'com.google.ios.youtube/17.33.2 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
189 'INNERTUBE_CONTEXT_CLIENT_NAME': 66,
190 'REQUIRE_JS_PLAYER': False
193 'INNERTUBE_API_KEY': 'AIzaSyBAETezhkwP0ZWA02RsqT1zu78Fpt0bC_s',
194 'INNERTUBE_CONTEXT': {
196 'clientName': 'IOS_MUSIC',
197 'clientVersion': '5.21',
198 'deviceModel': 'iPhone14,3',
199 'userAgent': 'com.google.ios.youtubemusic/5.21 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
202 'INNERTUBE_CONTEXT_CLIENT_NAME': 26,
203 'REQUIRE_JS_PLAYER': False
206 'INNERTUBE_CONTEXT': {
208 'clientName': 'IOS_CREATOR',
209 'clientVersion': '22.33.101',
210 'deviceModel': 'iPhone14,3',
211 'userAgent': 'com.google.ios.ytcreator/22.33.101 (iPhone14,3; U; CPU iOS 15_6 like Mac OS X)'
214 'INNERTUBE_CONTEXT_CLIENT_NAME': 15,
215 'REQUIRE_JS_PLAYER': False
217 # mweb has 'ultralow' formats
218 # See: https://github.com/yt-dlp/yt-dlp/pull/557
220 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
221 'INNERTUBE_CONTEXT': {
223 'clientName': 'MWEB',
224 'clientVersion': '2.20220801.00.00',
227 'INNERTUBE_CONTEXT_CLIENT_NAME': 2
229 # This client can access age restricted videos (unless the uploader has disabled the 'allow embedding' option)
230 # See: https://github.com/zerodytrash/YouTube-Internal-Clients
232 'INNERTUBE_API_KEY': 'AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8',
233 'INNERTUBE_CONTEXT': {
235 'clientName': 'TVHTML5_SIMPLY_EMBEDDED_PLAYER',
236 'clientVersion': '2.0',
239 'INNERTUBE_CONTEXT_CLIENT_NAME': 85
244 def _split_innertube_client(client_name
):
245 variant
, *base
= client_name
.rsplit('.', 1)
247 return variant
, base
[0], variant
248 base
, *variant
= client_name
.split('_', 1)
249 return client_name
, base
, variant
[0] if variant
else None
252 def short_client_name(client_name
):
253 main
, *parts
= _split_innertube_client(client_name
)[0].replace('embedscreen', 'e_s').split('_')
254 return join_nonempty(main
[:4], ''.join(x
[0] for x
in parts
)).upper()
257 def build_innertube_clients():
259 'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
261 BASE_CLIENTS
= ('android', 'web', 'tv', 'ios', 'mweb')
262 priority
= qualities(BASE_CLIENTS
[::-1])
264 for client
, ytcfg
in tuple(INNERTUBE_CLIENTS
.items()):
265 ytcfg
.setdefault('INNERTUBE_API_KEY', 'AIzaSyDCU8hByM-4DrUqRUYnGn-3llEO78bcxq8')
266 ytcfg
.setdefault('INNERTUBE_HOST', 'www.youtube.com')
267 ytcfg
.setdefault('REQUIRE_JS_PLAYER', True)
268 ytcfg
['INNERTUBE_CONTEXT']['client'].setdefault('hl', 'en')
270 _
, base_client
, variant
= _split_innertube_client(client
)
271 ytcfg
['priority'] = 10 * priority(base_client
)
274 INNERTUBE_CLIENTS
[f
'{client}_embedscreen'] = embedscreen
= copy
.deepcopy(ytcfg
)
275 embedscreen
['INNERTUBE_CONTEXT']['client']['clientScreen'] = 'EMBED'
276 embedscreen
['INNERTUBE_CONTEXT']['thirdParty'] = THIRD_PARTY
277 embedscreen
['priority'] -= 3
278 elif variant
== 'embedded':
279 ytcfg
['INNERTUBE_CONTEXT']['thirdParty'] = THIRD_PARTY
280 ytcfg
['priority'] -= 2
282 ytcfg
['priority'] -= 3
285 build_innertube_clients()
288 class BadgeType(enum
.Enum
):
289 AVAILABILITY_UNLISTED
= enum
.auto()
290 AVAILABILITY_PRIVATE
= enum
.auto()
291 AVAILABILITY_PUBLIC
= enum
.auto()
292 AVAILABILITY_PREMIUM
= enum
.auto()
293 AVAILABILITY_SUBSCRIPTION
= enum
.auto()
294 LIVE_NOW
= enum
.auto()
297 class YoutubeBaseInfoExtractor(InfoExtractor
):
298 """Provide base functions for Youtube extractors"""
301 r
'channel|c|user|playlist|watch|w|v|embed|e|live|watch_popup|clip|'
302 r
'shorts|movies|results|search|shared|hashtag|trending|explore|feed|feeds|'
303 r
'browse|oembed|get_video_info|iframe_api|s/player|source|'
304 r
'storefront|oops|index|account|t/terms|about|upload|signin|logout')
306 _PLAYLIST_ID_RE
= r
'(?:(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}|RDMM|WL|LL|LM)'
308 # _NETRC_MACHINE = 'youtube'
310 # If True it will raise an error if no login info is provided
311 _LOGIN_REQUIRED
= False
314 # invidious-redirect websites
315 r
'(?:www\.)?redirect\.invidious\.io',
316 r
'(?:(?:www|dev)\.)?invidio\.us',
317 # Invidious instances taken from https://github.com/iv-org/documentation/blob/master/docs/instances.md
318 r
'(?:www\.)?invidious\.pussthecat\.org',
319 r
'(?:www\.)?invidious\.zee\.li',
320 r
'(?:www\.)?invidious\.ethibox\.fr',
321 r
'(?:www\.)?iv\.ggtyler\.dev',
322 r
'(?:www\.)?inv\.vern\.i2p',
323 r
'(?:www\.)?am74vkcrjp2d5v36lcdqgsj2m6x36tbrkhsruoegwfcizzabnfgf5zyd\.onion',
324 r
'(?:www\.)?inv\.riverside\.rocks',
325 r
'(?:www\.)?invidious\.silur\.me',
326 r
'(?:www\.)?inv\.bp\.projectsegfau\.lt',
327 r
'(?:www\.)?invidious\.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid\.onion',
328 r
'(?:www\.)?invidious\.slipfox\.xyz',
329 r
'(?:www\.)?invidious\.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd\.onion',
330 r
'(?:www\.)?inv\.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad\.onion',
331 r
'(?:www\.)?invidious\.tiekoetter\.com',
332 r
'(?:www\.)?iv\.odysfvr23q5wgt7i456o5t3trw2cw5dgn56vbjfbq2m7xsc5vqbqpcyd\.onion',
333 r
'(?:www\.)?invidious\.nerdvpn\.de',
334 r
'(?:www\.)?invidious\.weblibre\.org',
335 r
'(?:www\.)?inv\.odyssey346\.dev',
336 r
'(?:www\.)?invidious\.dhusch\.de',
337 r
'(?:www\.)?iv\.melmac\.space',
338 r
'(?:www\.)?watch\.thekitty\.zone',
339 r
'(?:www\.)?invidious\.privacydev\.net',
340 r
'(?:www\.)?ng27owmagn5amdm7l5s3rsqxwscl5ynppnis5dqcasogkyxcfqn7psid\.onion',
341 r
'(?:www\.)?invidious\.drivet\.xyz',
342 r
'(?:www\.)?vid\.priv\.au',
343 r
'(?:www\.)?euxxcnhsynwmfidvhjf6uzptsmh4dipkmgdmcmxxuo7tunp3ad2jrwyd\.onion',
344 r
'(?:www\.)?inv\.vern\.cc',
345 r
'(?:www\.)?invidious\.esmailelbob\.xyz',
346 r
'(?:www\.)?invidious\.sethforprivacy\.com',
347 r
'(?:www\.)?yt\.oelrichsgarcia\.de',
348 r
'(?:www\.)?yt\.artemislena\.eu',
349 r
'(?:www\.)?invidious\.flokinet\.to',
350 r
'(?:www\.)?invidious\.baczek\.me',
351 r
'(?:www\.)?y\.com\.sb',
352 r
'(?:www\.)?invidious\.epicsite\.xyz',
353 r
'(?:www\.)?invidious\.lidarshield\.cloud',
354 r
'(?:www\.)?yt\.funami\.tech',
355 r
'(?:www\.)?invidious\.3o7z6yfxhbw7n3za4rss6l434kmv55cgw2vuziwuigpwegswvwzqipyd\.onion',
356 r
'(?:www\.)?osbivz6guyeahrwp2lnwyjk2xos342h4ocsxyqrlaopqjuhwn2djiiyd\.onion',
357 r
'(?:www\.)?u2cvlit75owumwpy4dj2hsmvkq7nvrclkpht7xgyye2pyoxhpmclkrad\.onion',
358 # youtube-dl invidious instances list
359 r
'(?:(?:www|no)\.)?invidiou\.sh',
360 r
'(?:(?:www|fi)\.)?invidious\.snopyta\.org',
361 r
'(?:www\.)?invidious\.kabi\.tk',
362 r
'(?:www\.)?invidious\.mastodon\.host',
363 r
'(?:www\.)?invidious\.zapashcanon\.fr',
364 r
'(?:www\.)?(?:invidious(?:-us)?|piped)\.kavin\.rocks',
365 r
'(?:www\.)?invidious\.tinfoil-hat\.net',
366 r
'(?:www\.)?invidious\.himiko\.cloud',
367 r
'(?:www\.)?invidious\.reallyancient\.tech',
368 r
'(?:www\.)?invidious\.tube',
369 r
'(?:www\.)?invidiou\.site',
370 r
'(?:www\.)?invidious\.site',
371 r
'(?:www\.)?invidious\.xyz',
372 r
'(?:www\.)?invidious\.nixnet\.xyz',
373 r
'(?:www\.)?invidious\.048596\.xyz',
374 r
'(?:www\.)?invidious\.drycat\.fr',
375 r
'(?:www\.)?inv\.skyn3t\.in',
376 r
'(?:www\.)?tube\.poal\.co',
377 r
'(?:www\.)?tube\.connect\.cafe',
378 r
'(?:www\.)?vid\.wxzm\.sx',
379 r
'(?:www\.)?vid\.mint\.lgbt',
380 r
'(?:www\.)?vid\.puffyan\.us',
381 r
'(?:www\.)?yewtu\.be',
382 r
'(?:www\.)?yt\.elukerio\.org',
383 r
'(?:www\.)?yt\.lelux\.fi',
384 r
'(?:www\.)?invidious\.ggc-project\.de',
385 r
'(?:www\.)?yt\.maisputain\.ovh',
386 r
'(?:www\.)?ytprivate\.com',
387 r
'(?:www\.)?invidious\.13ad\.de',
388 r
'(?:www\.)?invidious\.toot\.koeln',
389 r
'(?:www\.)?invidious\.fdn\.fr',
390 r
'(?:www\.)?watch\.nettohikari\.com',
391 r
'(?:www\.)?invidious\.namazso\.eu',
392 r
'(?:www\.)?invidious\.silkky\.cloud',
393 r
'(?:www\.)?invidious\.exonip\.de',
394 r
'(?:www\.)?invidious\.riverside\.rocks',
395 r
'(?:www\.)?invidious\.blamefran\.net',
396 r
'(?:www\.)?invidious\.moomoo\.de',
397 r
'(?:www\.)?ytb\.trom\.tf',
398 r
'(?:www\.)?yt\.cyberhost\.uk',
399 r
'(?:www\.)?kgg2m7yk5aybusll\.onion',
400 r
'(?:www\.)?qklhadlycap4cnod\.onion',
401 r
'(?:www\.)?axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid\.onion',
402 r
'(?:www\.)?c7hqkpkpemu6e7emz5b4vyz7idjgdvgaaa3dyimmeojqbgpea3xqjoid\.onion',
403 r
'(?:www\.)?fz253lmuao3strwbfbmx46yu7acac2jz27iwtorgmbqlkurlclmancad\.onion',
404 r
'(?:www\.)?invidious\.l4qlywnpwqsluw65ts7md3khrivpirse744un3x7mlskqauz5pyuzgqd\.onion',
405 r
'(?:www\.)?owxfohz4kjyv25fvlqilyxast7inivgiktls3th44jhk3ej3i7ya\.b32\.i2p',
406 r
'(?:www\.)?4l2dgddgsrkf2ous66i6seeyi6etzfgrue332grh2n7madpwopotugyd\.onion',
407 r
'(?:www\.)?w6ijuptxiku4xpnnaetxvnkc5vqcdu7mgns2u77qefoixi63vbvnpnqd\.onion',
408 r
'(?:www\.)?kbjggqkzv65ivcqj6bumvp337z6264huv5kpkwuv6gu5yjiskvan7fad\.onion',
409 r
'(?:www\.)?grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad\.onion',
410 r
'(?:www\.)?hpniueoejy4opn7bc4ftgazyqjoeqwlvh2uiku2xqku6zpoa4bf5ruid\.onion',
411 # piped instances from https://github.com/TeamPiped/Piped/wiki/Instances
412 r
'(?:www\.)?piped\.kavin\.rocks',
413 r
'(?:www\.)?piped\.tokhmi\.xyz',
414 r
'(?:www\.)?piped\.syncpundit\.io',
415 r
'(?:www\.)?piped\.mha\.fi',
416 r
'(?:www\.)?watch\.whatever\.social',
417 r
'(?:www\.)?piped\.garudalinux\.org',
418 r
'(?:www\.)?piped\.rivo\.lol',
419 r
'(?:www\.)?piped-libre\.kavin\.rocks',
420 r
'(?:www\.)?yt\.jae\.fi',
421 r
'(?:www\.)?piped\.mint\.lgbt',
423 r
'(?:www\.)?piped\.esmailelbob\.xyz',
424 r
'(?:www\.)?piped\.projectsegfau\.lt',
425 r
'(?:www\.)?piped\.privacydev\.net',
426 r
'(?:www\.)?piped\.palveluntarjoaja\.eu',
427 r
'(?:www\.)?piped\.smnz\.de',
428 r
'(?:www\.)?piped\.adminforge\.de',
429 r
'(?:www\.)?watch\.whatevertinfoil\.de',
430 r
'(?:www\.)?piped\.qdi\.fi',
431 r
'(?:www\.)?piped\.video',
432 r
'(?:www\.)?piped\.aeong\.one',
433 r
'(?:www\.)?piped\.moomoo\.me',
434 r
'(?:www\.)?piped\.chauvet\.pro',
435 r
'(?:www\.)?watch\.leptons\.xyz',
436 r
'(?:www\.)?pd\.vern\.cc',
437 r
'(?:www\.)?piped\.hostux\.net',
438 r
'(?:www\.)?piped\.lunar\.icu',
439 # Hyperpipe instances from https://hyperpipe.codeberg.page/
440 r
'(?:www\.)?hyperpipe\.surge\.sh',
441 r
'(?:www\.)?hyperpipe\.esmailelbob\.xyz',
442 r
'(?:www\.)?listen\.whatever\.social',
443 r
'(?:www\.)?music\.adminforge\.de',
446 # extracted from account/account_menu ep
447 # XXX: These are the supported YouTube UI and API languages,
448 # which is slightly different from languages supported for translation in YouTube studio
449 _SUPPORTED_LANG_CODES
= [
450 'af', 'az', 'id', 'ms', 'bs', 'ca', 'cs', 'da', 'de', 'et', 'en-IN', 'en-GB', 'en', 'es',
451 'es-419', 'es-US', 'eu', 'fil', 'fr', 'fr-CA', 'gl', 'hr', 'zu', 'is', 'it', 'sw', 'lv',
452 'lt', 'hu', 'nl', 'no', 'uz', 'pl', 'pt-PT', 'pt', 'ro', 'sq', 'sk', 'sl', 'sr-Latn', 'fi',
453 'sv', 'vi', 'tr', 'be', 'bg', 'ky', 'kk', 'mk', 'mn', 'ru', 'sr', 'uk', 'el', 'hy', 'iw',
454 'ur', 'ar', 'fa', 'ne', 'mr', 'hi', 'as', 'bn', 'pa', 'gu', 'or', 'ta', 'te', 'kn', 'ml',
455 'si', 'th', 'lo', 'my', 'ka', 'am', 'km', 'zh-CN', 'zh-TW', 'zh-HK', 'ja', 'ko'
458 _IGNORED_WARNINGS
= {'Unavailable videos will be hidden during playback'}
460 _YT_HANDLE_RE
= r
'@[\w.-]{3,30}' # https://support.google.com/youtube/answer/11585688?hl=en
461 _YT_CHANNEL_UCID_RE
= r
'UC[\w-]{22}'
463 def ucid_or_none(self
, ucid
):
464 return self
._search
_regex
(rf
'^({self._YT_CHANNEL_UCID_RE})$', ucid
, 'UC-id', default
=None)
466 def handle_or_none(self
, handle
):
467 return self
._search
_regex
(rf
'^({self._YT_HANDLE_RE})$', handle
, '@-handle', default
=None)
469 def handle_from_url(self
, url
):
470 return self
._search
_regex
(rf
'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_HANDLE_RE})',
471 url
, 'channel handle', default
=None)
473 def ucid_from_url(self
, url
):
474 return self
._search
_regex
(rf
'^(?:https?://(?:www\.)?youtube\.com)?/({self._YT_CHANNEL_UCID_RE})',
475 url
, 'channel id', default
=None)
477 @functools.cached_property
478 def _preferred_lang(self
):
480 Returns a language code supported by YouTube for the user preferred language.
481 Returns None if no preferred language set.
483 preferred_lang
= self
._configuration
_arg
('lang', ie_key
='Youtube', casesense
=True, default
=[''])[0]
484 if not preferred_lang
:
486 if preferred_lang
not in self
._SUPPORTED
_LANG
_CODES
:
487 raise ExtractorError(
488 f
'Unsupported language code: {preferred_lang}. Supported language codes (case-sensitive): {join_nonempty(*self._SUPPORTED_LANG_CODES, delim=", ")}.',
490 elif preferred_lang
!= 'en':
492 f
'Preferring "{preferred_lang}" translated fields. Note that some metadata extraction may fail or be incorrect.')
493 return preferred_lang
495 def _initialize_consent(self
):
496 cookies
= self
._get
_cookies
('https://www.youtube.com/')
497 if cookies
.get('__Secure-3PSID'):
500 consent
= cookies
.get('CONSENT')
502 if 'YES' in consent
.value
:
504 consent_id
= self
._search
_regex
(
505 r
'PENDING\+(\d+)', consent
.value
, 'consent', default
=None)
507 consent_id
= random
.randint(100, 999)
508 self
._set
_cookie
('.youtube.com', 'CONSENT', 'YES+cb.20210328-17-p0.en+FX+%s' % consent_id
)
510 def _initialize_pref(self
):
511 cookies
= self
._get
_cookies
('https://www.youtube.com/')
512 pref_cookie
= cookies
.get('PREF')
516 pref
= dict(urllib
.parse
.parse_qsl(pref_cookie
.value
))
518 self
.report_warning('Failed to parse user PREF cookie' + bug_reports_message())
519 pref
.update({'hl': self._preferred_lang or 'en', 'tz': 'UTC'}
)
520 self
._set
_cookie
('.youtube.com', name
='PREF', value
=urllib
.parse
.urlencode(pref
))
522 def _real_initialize(self
):
523 self
._initialize
_pref
()
524 self
._initialize
_consent
()
525 self
._check
_login
_required
()
527 def _check_login_required(self
):
528 if self
._LOGIN
_REQUIRED
and not self
._cookies
_passed
:
529 self
.raise_login_required('Login details are needed to download this content', method
='cookies')
531 _YT_INITIAL_DATA_RE
= r
'(?:window\s*\[\s*["\']ytInitialData
["\']\s*\]|ytInitialData)\s*='
532 _YT_INITIAL_PLAYER_RESPONSE_RE = r'ytInitialPlayerResponse\s*='
534 def _get_default_ytcfg(self, client='web'):
535 return copy.deepcopy(INNERTUBE_CLIENTS[client])
537 def _get_innertube_host(self, client='web'):
538 return INNERTUBE_CLIENTS[client]['INNERTUBE_HOST']
540 def _ytcfg_get_safe(self, ytcfg, getter, expected_type=None, default_client='web'):
541 # try_get but with fallback to default ytcfg client values when present
542 _func = lambda y: try_get(y, getter, expected_type)
543 return _func(ytcfg) or _func(self._get_default_ytcfg(default_client))
545 def _extract_client_name(self, ytcfg, default_client='web'):
546 return self._ytcfg_get_safe(
547 ytcfg, (lambda x: x['INNERTUBE_CLIENT_NAME'],
548 lambda x: x['INNERTUBE_CONTEXT']['client']['clientName']), str, default_client)
550 def _extract_client_version(self, ytcfg, default_client='web'):
551 return self._ytcfg_get_safe(
552 ytcfg, (lambda x: x['INNERTUBE_CLIENT_VERSION'],
553 lambda x: x['INNERTUBE_CONTEXT']['client']['clientVersion']), str, default_client)
555 def _select_api_hostname(self, req_api_hostname, default_client=None):
556 return (self._configuration_arg('innertube_host', [''], ie_key=YoutubeIE.ie_key())[0]
557 or req_api_hostname or self._get_innertube_host(default_client or 'web'))
559 def _extract_api_key(self, ytcfg=None, default_client='web'):
560 return self._ytcfg_get_safe(ytcfg, lambda x: x['INNERTUBE_API_KEY'], str, default_client)
562 def _extract_context(self, ytcfg=None, default_client='web'):
564 (ytcfg, self._get_default_ytcfg(default_client)), 'INNERTUBE_CONTEXT', expected_type=dict)
565 # Enforce language and tz for extraction
566 client_context = traverse_obj(context, 'client', expected_type=dict, default={})
567 client_context.update({'hl': self._preferred_lang or 'en', 'timeZone': 'UTC', 'utcOffsetMinutes': 0})
572 def _generate_sapisidhash_header(self, origin='https://www.youtube.com'):
573 time_now = round(time.time())
574 if self._SAPISID is None:
575 yt_cookies = self._get_cookies('https://www.youtube.com')
576 # Sometimes SAPISID cookie isn't present but __Secure-3PAPISID is.
577 # See: https://github.com/yt-dlp/yt-dlp/issues/393
578 sapisid_cookie = dict_get(
579 yt_cookies, ('__Secure-3PAPISID', 'SAPISID'))
580 if sapisid_cookie and sapisid_cookie.value:
581 self._SAPISID = sapisid_cookie.value
582 self.write_debug('Extracted SAPISID cookie')
583 # SAPISID cookie is required if not already present
584 if not yt_cookies.get('SAPISID'):
585 self.write_debug('Copying __Secure-3PAPISID cookie to SAPISID cookie')
587 '.youtube.com', 'SAPISID', self._SAPISID, secure=True, expire_time=time_now + 3600)
589 self._SAPISID = False
590 if not self._SAPISID:
592 # SAPISIDHASH algorithm from https://stackoverflow.com/a/32065323
593 sapisidhash = hashlib.sha1(
594 f'{time_now} {self._SAPISID} {origin}'.encode()).hexdigest()
595 return f'SAPISIDHASH {time_now}_{sapisidhash}'
597 def _call_api(self, ep, query, video_id, fatal=True, headers=None,
598 note='Downloading API JSON', errnote='Unable to download API page',
599 context=None, api_key=None, api_hostname=None, default_client='web'):
601 data = {'context': context} if context else {'context': self._extract_context(default_client=default_client)}
603 real_headers = self.generate_api_headers(default_client=default_client)
604 real_headers.update({'content-type': 'application/json'})
606 real_headers.update(headers)
607 api_key = (self._configuration_arg('innertube_key', [''], ie_key=YoutubeIE.ie_key(), casesense=True)[0]
608 or api_key or self._extract_api_key(default_client=default_client))
609 return self._download_json(
610 f'https://{self._select_api_hostname(api_hostname, default_client)}/youtubei/v1/{ep}',
611 video_id=video_id, fatal=fatal, note=note, errnote=errnote,
612 data=json.dumps(data).encode('utf8'), headers=real_headers,
613 query={'key': api_key, 'prettyPrint': 'false'})
615 def extract_yt_initial_data(self, item_id, webpage, fatal=True):
616 return self._search_json(self._YT_INITIAL_DATA_RE, webpage, 'yt initial data', item_id, fatal=fatal)
619 def _extract_session_index(*data):
621 Index of current account in account list.
622 See: https://github.com/yt-dlp/yt-dlp/pull/519
625 session_index = int_or_none(try_get(ytcfg, lambda x: x['SESSION_INDEX']))
626 if session_index is not None:
630 def _extract_identity_token(self, ytcfg=None, webpage=None):
632 token = try_get(ytcfg, lambda x: x['ID_TOKEN'], str)
636 return self._search_regex(
637 r'\bID_TOKEN["\']\s
*:\s
*["\'](.+?)["\']', webpage,
638 'identity token
', default=None, fatal=False)
641 def _extract_account_syncid(*args):
643 Extract syncId required to download private playlists of secondary channels
644 @params response and/or ytcfg
647 # ytcfg includes channel_syncid if on secondary channel
648 delegated_sid = try_get(data, lambda x: x['DELEGATED_SESSION_ID
'], str)
652 data, (lambda x: x['responseContext
']['mainAppWebResponseContext
']['datasyncId
'],
653 lambda x: x['DATASYNC_ID
']), str) or '').split('||
')
654 if len(sync_ids) >= 2 and sync_ids[1]:
655 # datasyncid is of the form "channel_syncid||user_syncid" for secondary channel
656 # and just "user_syncid||" for primary channel. We only want the channel_syncid
660 def _extract_visitor_data(*args):
662 Extracts visitorData from an API response or ytcfg
663 Appears to be used to track session state
666 args, [('VISITOR_DATA
', ('INNERTUBE_CONTEXT
', 'client
', 'visitorData
'), ('responseContext
', 'visitorData
'))],
669 @functools.cached_property
670 def is_authenticated(self):
671 return bool(self._generate_sapisidhash_header())
673 def extract_ytcfg(self, video_id, webpage):
676 return self._parse_json(
678 r'ytcfg\
.set\s
*\
(\s
*({.+?}
)\s
*\
)\s
*;', webpage, 'ytcfg
',
679 default='{}'), video_id, fatal=False) or {}
681 def generate_api_headers(
682 self
, *, ytcfg
=None, account_syncid
=None, session_index
=None,
683 visitor_data
=None, identity_token
=None, api_hostname
=None, default_client
='web'):
685 origin
= 'https://' + (self
._select
_api
_hostname
(api_hostname
, default_client
))
687 'X-YouTube-Client-Name': str(
688 self
._ytcfg
_get
_safe
(ytcfg
, lambda x
: x
['INNERTUBE_CONTEXT_CLIENT_NAME'], default_client
=default_client
)),
689 'X-YouTube-Client-Version': self
._extract
_client
_version
(ytcfg
, default_client
),
691 'X-Youtube-Identity-Token': identity_token
or self
._extract
_identity
_token
(ytcfg
),
692 'X-Goog-PageId': account_syncid
or self
._extract
_account
_syncid
(ytcfg
),
693 'X-Goog-Visitor-Id': visitor_data
or self
._extract
_visitor
_data
(ytcfg
),
694 'User-Agent': self
._ytcfg
_get
_safe
(ytcfg
, lambda x
: x
['INNERTUBE_CONTEXT']['client']['userAgent'], default_client
=default_client
)
696 if session_index
is None:
697 session_index
= self
._extract
_session
_index
(ytcfg
)
698 if account_syncid
or session_index
is not None:
699 headers
['X-Goog-AuthUser'] = session_index
if session_index
is not None else 0
701 auth
= self
._generate
_sapisidhash
_header
(origin
)
703 headers
['Authorization'] = auth
704 headers
['X-Origin'] = origin
705 return filter_dict(headers
)
707 def _download_ytcfg(self
, client
, video_id
):
709 'web': 'https://www.youtube.com',
710 'web_music': 'https://music.youtube.com',
711 'web_embedded': f
'https://www.youtube.com/embed/{video_id}?html5=1'
715 webpage
= self
._download
_webpage
(
716 url
, video_id
, fatal
=False, note
=f
'Downloading {client.replace("_", " ").strip()} client config')
717 return self
.extract_ytcfg(video_id
, webpage
) or {}
720 def _build_api_continuation_query(continuation
, ctp
=None):
722 'continuation': continuation
724 # TODO: Inconsistency with clickTrackingParams.
725 # Currently we have a fixed ctp contained within context (from ytcfg)
726 # and a ctp in root query for continuation.
728 query
['clickTracking'] = {'clickTrackingParams': ctp}
732 def _extract_next_continuation_data(cls
, renderer
):
733 next_continuation
= try_get(
734 renderer
, (lambda x
: x
['continuations'][0]['nextContinuationData'],
735 lambda x
: x
['continuation']['reloadContinuationData']), dict)
736 if not next_continuation
:
738 continuation
= next_continuation
.get('continuation')
741 ctp
= next_continuation
.get('clickTrackingParams')
742 return cls
._build
_api
_continuation
_query
(continuation
, ctp
)
745 def _extract_continuation_ep_data(cls
, continuation_ep
: dict):
746 if isinstance(continuation_ep
, dict):
747 continuation
= try_get(
748 continuation_ep
, lambda x
: x
['continuationCommand']['token'], str)
751 ctp
= continuation_ep
.get('clickTrackingParams')
752 return cls
._build
_api
_continuation
_query
(continuation
, ctp
)
755 def _extract_continuation(cls
, renderer
):
756 next_continuation
= cls
._extract
_next
_continuation
_data
(renderer
)
757 if next_continuation
:
758 return next_continuation
760 return traverse_obj(renderer
, (
761 ('contents', 'items', 'rows'), ..., 'continuationItemRenderer',
762 ('continuationEndpoint', ('button', 'buttonRenderer', 'command'))
763 ), get_all
=False, expected_type
=cls
._extract
_continuation
_ep
_data
)
766 def _extract_alerts(cls
, data
):
767 for alert_dict
in try_get(data
, lambda x
: x
['alerts'], list) or []:
768 if not isinstance(alert_dict
, dict):
770 for alert
in alert_dict
.values():
771 alert_type
= alert
.get('type')
774 message
= cls
._get
_text
(alert
, 'text')
776 yield alert_type
, message
778 def _report_alerts(self
, alerts
, expected
=True, fatal
=True, only_once
=False):
779 errors
, warnings
= [], []
780 for alert_type
, alert_message
in alerts
:
781 if alert_type
.lower() == 'error' and fatal
:
782 errors
.append([alert_type
, alert_message
])
783 elif alert_message
not in self
._IGNORED
_WARNINGS
:
784 warnings
.append([alert_type
, alert_message
])
786 for alert_type
, alert_message
in (warnings
+ errors
[:-1]):
787 self
.report_warning(f
'YouTube said: {alert_type} - {alert_message}', only_once
=only_once
)
789 raise ExtractorError('YouTube said: %s' % errors
[-1][1], expected
=expected
)
791 def _extract_and_report_alerts(self
, data
, *args
, **kwargs
):
792 return self
._report
_alerts
(self
._extract
_alerts
(data
), *args
, **kwargs
)
794 def _extract_badges(self
, renderer
: dict):
796 'PRIVACY_UNLISTED': BadgeType
.AVAILABILITY_UNLISTED
,
797 'PRIVACY_PRIVATE': BadgeType
.AVAILABILITY_PRIVATE
,
798 'PRIVACY_PUBLIC': BadgeType
.AVAILABILITY_PUBLIC
802 'BADGE_STYLE_TYPE_MEMBERS_ONLY': BadgeType
.AVAILABILITY_SUBSCRIPTION
,
803 'BADGE_STYLE_TYPE_PREMIUM': BadgeType
.AVAILABILITY_PREMIUM
,
804 'BADGE_STYLE_TYPE_LIVE_NOW': BadgeType
.LIVE_NOW
808 'unlisted': BadgeType
.AVAILABILITY_UNLISTED
,
809 'private': BadgeType
.AVAILABILITY_PRIVATE
,
810 'members only': BadgeType
.AVAILABILITY_SUBSCRIPTION
,
811 'live': BadgeType
.LIVE_NOW
,
812 'premium': BadgeType
.AVAILABILITY_PREMIUM
816 for badge
in traverse_obj(renderer
, ('badges', ..., 'metadataBadgeRenderer')):
818 privacy_icon_map
.get(traverse_obj(badge
, ('icon', 'iconType'), expected_type
=str))
819 or badge_style_map
.get(traverse_obj(badge
, 'style'))
822 badges
.append({'type': badge_type}
)
825 # fallback, won't work in some languages
826 label
= traverse_obj(badge
, 'label', expected_type
=str, default
='')
827 for match
, label_badge_type
in label_map
.items():
828 if match
in label
.lower():
829 badges
.append({'type': badge_type}
)
835 def _has_badge(badges
, badge_type
):
836 return bool(traverse_obj(badges
, lambda _
, v
: v
['type'] == badge_type
))
839 def _get_text(data
, *path_list
, max_runs
=None):
840 for path
in path_list
or [None]:
844 obj
= traverse_obj(data
, path
, default
=[])
845 if not any(key
is ... or isinstance(key
, (list, tuple)) for key
in variadic(path
)):
848 text
= try_get(item
, lambda x
: x
['simpleText'], str)
851 runs
= try_get(item
, lambda x
: x
['runs'], list) or []
852 if not runs
and isinstance(item
, list):
855 runs
= runs
[:min(len(runs
), max_runs
or len(runs
))]
856 text
= ''.join(traverse_obj(runs
, (..., 'text'), expected_type
=str))
860 def _get_count(self
, data
, *path_list
):
861 count_text
= self
._get
_text
(data
, *path_list
) or ''
862 count
= parse_count(count_text
)
865 self
._search
_regex
(r
'^([\d,]+)', re
.sub(r
'\s', '', count_text
), 'count', default
=None))
869 def _extract_thumbnails(data
, *path_list
):
871 Extract thumbnails from thumbnails dict
872 @param path_list: path list to level that contains 'thumbnails' key
875 for path
in path_list
or [()]:
876 for thumbnail
in traverse_obj(data
, (*variadic(path
), 'thumbnails', ...)):
877 thumbnail_url
= url_or_none(thumbnail
.get('url'))
878 if not thumbnail_url
:
880 # Sometimes youtube gives a wrong thumbnail URL. See:
881 # https://github.com/yt-dlp/yt-dlp/issues/233
882 # https://github.com/ytdl-org/youtube-dl/issues/28023
883 if 'maxresdefault' in thumbnail_url
:
884 thumbnail_url
= thumbnail_url
.split('?')[0]
886 'url': thumbnail_url
,
887 'height': int_or_none(thumbnail
.get('height')),
888 'width': int_or_none(thumbnail
.get('width')),
893 def extract_relative_time(relative_time_text
):
895 Extracts a relative time from string and converts to dt object
896 e.g. 'streamed 6 days ago', '5 seconds ago (edited)', 'updated today'
898 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
)
900 start
= mobj
.group('start')
902 return datetime_from_str(start
)
904 return datetime_from_str('now-%s%s' % (mobj
.group('time'), mobj
.group('unit')))
908 def _parse_time_text(self
, text
):
911 dt
= self
.extract_relative_time(text
)
913 if isinstance(dt
, datetime
.datetime
):
914 timestamp
= calendar
.timegm(dt
.timetuple())
916 if timestamp
is None:
918 unified_timestamp(text
) or unified_timestamp(
920 (r
'([a-z]+\s*\d{1,2},?\s*20\d{2})', r
'(?:.+|^)(?:live|premieres|ed|ing)(?:\s*(?:on|for))?\s*(.+\d)'),
921 text
.lower(), 'time text', default
=None)))
923 if text
and timestamp
is None and self
._preferred
_lang
in (None, 'en'):
925 f
'Cannot parse localized time text "{text}"', only_once
=True)
928 def _extract_response(self
, item_id
, query
, note
='Downloading API JSON', headers
=None,
929 ytcfg
=None, check_get_keys
=None, ep
='browse', fatal
=True, api_hostname
=None,
930 default_client
='web'):
931 for retry
in self
.RetryManager():
933 response
= self
._call
_api
(
934 ep
=ep
, fatal
=True, headers
=headers
,
935 video_id
=item_id
, query
=query
, note
=note
,
936 context
=self
._extract
_context
(ytcfg
, default_client
),
937 api_key
=self
._extract
_api
_key
(ytcfg
, default_client
),
938 api_hostname
=api_hostname
, default_client
=default_client
)
939 except ExtractorError
as e
:
940 if not isinstance(e
.cause
, network_exceptions
):
941 return self
._error
_or
_warning
(e
, fatal
=fatal
)
942 elif not isinstance(e
.cause
, urllib
.error
.HTTPError
):
946 first_bytes
= e
.cause
.read(512)
947 if not is_html(first_bytes
):
950 self
._webpage
_read
_content
(e
.cause
, None, item_id
, prefix
=first_bytes
) or '{}', item_id
, fatal
=False),
951 lambda x
: x
['error']['message'], str)
953 self
._report
_alerts
([('ERROR', yt_error
)], fatal
=False)
954 # Downloading page may result in intermittent 5xx HTTP error
955 # Sometimes a 404 is also recieved. See: https://github.com/ytdl-org/youtube-dl/issues/28289
956 # We also want to catch all other network exceptions since errors in later pages can be troublesome
957 # See https://github.com/yt-dlp/yt-dlp/issues/507#issuecomment-880188210
958 if e
.cause
.code
not in (403, 429):
961 return self
._error
_or
_warning
(e
, fatal
=fatal
)
964 self
._extract
_and
_report
_alerts
(response
, only_once
=True)
965 except ExtractorError
as e
:
966 # YouTube servers may return errors we want to retry on in a 200 OK response
967 # See: https://github.com/yt-dlp/yt-dlp/issues/839
968 if 'unknown error' in e
.msg
.lower():
971 return self
._error
_or
_warning
(e
, fatal
=fatal
)
972 # Youtube sometimes sends incomplete data
973 # See: https://github.com/ytdl-org/youtube-dl/issues/28194
974 if not traverse_obj(response
, *variadic(check_get_keys
)):
975 retry
.error
= ExtractorError('Incomplete data received', expected
=True)
981 def is_music_url(url
):
982 return re
.match(r
'(https?://)?music\.youtube\.com/', url
) is not None
984 def _extract_video(self
, renderer
):
985 video_id
= renderer
.get('videoId')
987 reel_header_renderer
= traverse_obj(renderer
, (
988 'navigationEndpoint', 'reelWatchEndpoint', 'overlay', 'reelPlayerOverlayRenderer',
989 'reelPlayerHeaderSupportedRenderers', 'reelPlayerHeaderRenderer'))
991 title
= self
._get
_text
(renderer
, 'title', 'headline') or self
._get
_text
(reel_header_renderer
, 'reelTitleText')
992 description
= self
._get
_text
(renderer
, 'descriptionSnippet')
994 duration
= int_or_none(renderer
.get('lengthSeconds'))
996 duration
= parse_duration(self
._get
_text
(
997 renderer
, 'lengthText', ('thumbnailOverlays', ..., 'thumbnailOverlayTimeStatusRenderer', 'text')))
999 # XXX: should write a parser to be more general to support more cases (e.g. shorts in shorts tab)
1000 duration
= parse_duration(self
._search
_regex
(
1001 r
'(?i)(ago)(?!.*\1)\s+(?P<duration>[a-z0-9 ,]+?)(?:\s+[\d,]+\s+views)?(?:\s+-\s+play\s+short)?$',
1002 traverse_obj(renderer
, ('title', 'accessibility', 'accessibilityData', 'label'), default
='', expected_type
=str),
1003 video_id
, default
=None, group
='duration'))
1005 channel_id
= traverse_obj(
1006 renderer
, ('shortBylineText', 'runs', ..., 'navigationEndpoint', 'browseEndpoint', 'browseId'),
1007 expected_type
=str, get_all
=False)
1009 channel_id
= traverse_obj(reel_header_renderer
, ('channelNavigationEndpoint', 'browseEndpoint', 'browseId'))
1011 channel_id
= self
.ucid_or_none(channel_id
)
1013 overlay_style
= traverse_obj(
1014 renderer
, ('thumbnailOverlays', ..., 'thumbnailOverlayTimeStatusRenderer', 'style'),
1015 get_all
=False, expected_type
=str)
1016 badges
= self
._extract
_badges
(renderer
)
1018 navigation_url
= urljoin('https://www.youtube.com/', traverse_obj(
1019 renderer
, ('navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url'),
1020 expected_type
=str)) or ''
1021 url
= f
'https://www.youtube.com/watch?v={video_id}'
1022 if overlay_style
== 'SHORTS' or '/shorts/' in navigation_url
:
1023 url
= f
'https://www.youtube.com/shorts/{video_id}'
1025 time_text
= (self
._get
_text
(renderer
, 'publishedTimeText', 'videoInfo')
1026 or self
._get
_text
(reel_header_renderer
, 'timestampText') or '')
1027 scheduled_timestamp
= str_to_int(traverse_obj(renderer
, ('upcomingEventData', 'startTime'), get_all
=False))
1030 'is_upcoming' if scheduled_timestamp
is not None
1031 else 'was_live' if 'streamed' in time_text
.lower()
1032 else 'is_live' if overlay_style
== 'LIVE' or self
._has
_badge
(badges
, BadgeType
.LIVE_NOW
)
1035 # videoInfo is a string like '50K views • 10 years ago'.
1036 view_count_text
= self
._get
_text
(renderer
, 'viewCountText', 'shortViewCountText', 'videoInfo') or ''
1037 view_count
= (0 if 'no views' in view_count_text
.lower()
1038 else self
._get
_count
({'simpleText': view_count_text}
))
1039 view_count_field
= 'concurrent_view_count' if live_status
in ('is_live', 'is_upcoming') else 'view_count'
1043 'ie_key': YoutubeIE
.ie_key(),
1047 'description': description
,
1048 'duration': duration
,
1049 'channel_id': channel_id
,
1050 'channel': (self
._get
_text
(renderer
, 'ownerText', 'shortBylineText')
1051 or self
._get
_text
(reel_header_renderer
, 'channelTitleText')),
1052 'channel_url': f
'https://www.youtube.com/channel/{channel_id}' if channel_id
else None,
1053 'thumbnails': self
._extract
_thumbnails
(renderer
, 'thumbnail'),
1054 'timestamp': (self
._parse
_time
_text
(time_text
)
1055 if self
._configuration
_arg
('approximate_date', ie_key
=YoutubeTabIE
)
1057 'release_timestamp': scheduled_timestamp
,
1059 'public' if self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
1060 else self
._availability
(
1061 is_private
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
) or None,
1062 needs_premium
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
) or None,
1063 needs_subscription
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
) or None,
1064 is_unlisted
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
) or None),
1065 view_count_field
: view_count
,
1066 'live_status': live_status
1070 class YoutubeIE(YoutubeBaseInfoExtractor
):
1072 _VALID_URL
= r
"""(?x)^
1074 (?:https?://|//) # http(s):// or protocol-independent URL
1075 (?:(?:(?:(?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie|kids)?\.com|
1076 (?:www\.)?deturl\.com/www\.youtube\.com|
1077 (?:www\.)?pwnyoutube\.com|
1078 (?:www\.)?hooktube\.com|
1079 (?:www\.)?yourepeat\.com|
1080 tube\.majestyc\.net|
1082 youtube\.googleapis\.com)/ # the various hostnames, with wildcard subdomains
1083 (?:.*?\#/)? # handle anchor (#/) redirect urls
1084 (?: # the various things that can precede the ID:
1085 (?:(?:v|embed|e|shorts|live)/(?!videoseries|live_stream)) # v/ or embed/ or e/ or shorts/
1086 |(?: # or the v= param in all its forms
1087 (?:(?:watch|movie)(?:_popup)?(?:\.php)?/?)? # preceding watch(_popup|.php) or nothing (like /?v=xxxx)
1088 (?:\?|\#!?) # the params delimiter ? or # or #!
1089 (?:.*?[&;])?? # any other preceding param (like /?s=tuff&v=xxxx or ?s=tuff&v=V36LpHqtcDY)
1094 youtu\.be| # just youtu.be/xxxx
1095 vid\.plus| # or vid.plus/xxxx
1096 zwearz\.com/watch| # or zwearz.com/watch/xxxx
1099 |(?:www\.)?cleanvideosearch\.com/media/action/yt/watch\?videoId=
1101 )? # all until now is optional -> you can pass the naked ID
1102 (?P<id>[0-9A-Za-z_-]{11}) # here is it! the YouTube video ID
1103 (?(1).+)? # if we found the ID, everything can follow
1105 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
1110 <(?:[0-9A-Za-z-]+?)?iframe[^>]+?src=|
1118 (?P
<url
>(?
:https?
:)?
//(?
:www\
.)?
youtube(?
:-nocookie
)?\
.com
/
1119 (?
:embed|v|p
)/[0-9A
-Za
-z_
-]{11}
.*?
)
1121 # https://wordpress.org/plugins/lazy-load-for-videos/
1123 <a\s
[^
>]*\bhref
="(?P<url>https://www\.youtube\.com/watch\?v=[0-9A-Za-z_-]{11})"
1124 \s
[^
>]*\bclass
="[^"]*\blazy
-load
-youtube
''',
1126 _RETURN_TYPE = 'video' # XXX: How to handle multifeed?
1129 r'/s/player/(?P<id>[a-zA-Z0-9_-]{8,})/player',
1130 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$',
1131 r'\b(?P<id>vfl[a-zA-Z0-9_-]+)\b.*?\.js$',
1134 '5': {'ext': 'flv', 'width': 400, 'height': 240, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1135 '6': {'ext': 'flv', 'width': 450, 'height': 270, 'acodec': 'mp3', 'abr': 64, 'vcodec': 'h263'},
1136 '13': {'ext': '3gp', 'acodec': 'aac', 'vcodec': 'mp4v'},
1137 '17': {'ext': '3gp', 'width': 176, 'height': 144, 'acodec': 'aac', 'abr': 24, 'vcodec': 'mp4v'},
1138 '18': {'ext': 'mp4', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 96, 'vcodec': 'h264'},
1139 '22': {'ext': 'mp4', 'width': 1280, 'height': 720, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1140 '34': {'ext': 'flv', 'width': 640, 'height': 360, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1141 '35': {'ext': 'flv', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1142 # itag 36 videos are either 320x180 (BaW_jenozKc) or 320x240 (__2ABJjxzNo), abr varies as well
1143 '36': {'ext': '3gp', 'width': 320, 'acodec': 'aac', 'vcodec': 'mp4v'},
1144 '37': {'ext': 'mp4', 'width': 1920, 'height': 1080, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1145 '38': {'ext': 'mp4', 'width': 4096, 'height': 3072, 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264'},
1146 '43': {'ext': 'webm', 'width': 640, 'height': 360, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1147 '44': {'ext': 'webm', 'width': 854, 'height': 480, 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8'},
1148 '45': {'ext': 'webm', 'width': 1280, 'height': 720, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1149 '46': {'ext': 'webm', 'width': 1920, 'height': 1080, 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8'},
1150 '59': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1151 '78': {'ext': 'mp4', 'width': 854, 'height': 480, 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264'},
1155 '82': {'ext': 'mp4', 'height': 360, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1156 '83': {'ext': 'mp4', 'height': 480, 'format_note': '3D', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -20},
1157 '84': {'ext': 'mp4', 'height': 720, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1158 '85': {'ext': 'mp4', 'height': 1080, 'format_note': '3D', 'acodec': 'aac', 'abr': 192, 'vcodec': 'h264', 'preference': -20},
1159 '100': {'ext': 'webm', 'height': 360, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 128, 'vcodec': 'vp8', 'preference': -20},
1160 '101': {'ext': 'webm', 'height': 480, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1161 '102': {'ext': 'webm', 'height': 720, 'format_note': '3D', 'acodec': 'vorbis', 'abr': 192, 'vcodec': 'vp8', 'preference': -20},
1163 # Apple HTTP Live Streaming
1164 '91': {'ext': 'mp4', 'height': 144, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1165 '92': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1166 '93': {'ext': 'mp4', 'height': 360, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1167 '94': {'ext': 'mp4', 'height': 480, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 128, 'vcodec': 'h264', 'preference': -10},
1168 '95': {'ext': 'mp4', 'height': 720, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1169 '96': {'ext': 'mp4', 'height': 1080, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 256, 'vcodec': 'h264', 'preference': -10},
1170 '132': {'ext': 'mp4', 'height': 240, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 48, 'vcodec': 'h264', 'preference': -10},
1171 '151': {'ext': 'mp4', 'height': 72, 'format_note': 'HLS', 'acodec': 'aac', 'abr': 24, 'vcodec': 'h264', 'preference': -10},
1174 '133': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'h264'},
1175 '134': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'h264'},
1176 '135': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1177 '136': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264'},
1178 '137': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264'},
1179 '138': {'ext': 'mp4', 'format_note': 'DASH video', 'vcodec': 'h264'}, # Height can vary (https://github.com/ytdl-org/youtube-dl/issues/4559)
1180 '160': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'h264'},
1181 '212': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'h264'},
1182 '264': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'h264'},
1183 '298': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1184 '299': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'h264', 'fps': 60},
1185 '266': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'h264'},
1188 '139': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 48, 'container': 'm4a_dash'},
1189 '140': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 128, 'container': 'm4a_dash'},
1190 '141': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'abr': 256, 'container': 'm4a_dash'},
1191 '256': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1192 '258': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'aac', 'container': 'm4a_dash'},
1193 '325': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'dtse', 'container': 'm4a_dash'},
1194 '328': {'ext': 'm4a', 'format_note': 'DASH audio', 'acodec': 'ec-3', 'container': 'm4a_dash'},
1197 '167': {'ext': 'webm', 'height': 360, 'width': 640, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1198 '168': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1199 '169': {'ext': 'webm', 'height': 720, 'width': 1280, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1200 '170': {'ext': 'webm', 'height': 1080, 'width': 1920, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1201 '218': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1202 '219': {'ext': 'webm', 'height': 480, 'width': 854, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp8'},
1203 '278': {'ext': 'webm', 'height': 144, 'format_note': 'DASH video', 'container': 'webm', 'vcodec': 'vp9'},
1204 '242': {'ext': 'webm', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1205 '243': {'ext': 'webm', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1206 '244': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1207 '245': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1208 '246': {'ext': 'webm', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1209 '247': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1210 '248': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1211 '271': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1212 # itag 272 videos are either 3840x2160 (e.g. RtoitU2A-3E) or 7680x4320 (sLprVF6d7Ug)
1213 '272': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1214 '302': {'ext': 'webm', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1215 '303': {'ext': 'webm', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1216 '308': {'ext': 'webm', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1217 '313': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9'},
1218 '315': {'ext': 'webm', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'vp9', 'fps': 60},
1221 '171': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 128},
1222 '172': {'ext': 'webm', 'acodec': 'vorbis', 'format_note': 'DASH audio', 'abr': 256},
1224 # Dash webm audio with opus inside
1225 '249': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 50},
1226 '250': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 70},
1227 '251': {'ext': 'webm', 'format_note': 'DASH audio', 'acodec': 'opus', 'abr': 160},
1230 '_rtmp': {'protocol': 'rtmp'},
1232 # av01 video only formats sometimes served with "unknown" codecs
1233 '394': {'ext': 'mp4', 'height': 144, 'format_note': 'DASH video', 'vcodec': 'av01.0.00M.08'},
1234 '395': {'ext': 'mp4', 'height': 240, 'format_note': 'DASH video', 'vcodec': 'av01.0.00M.08'},
1235 '396': {'ext': 'mp4', 'height': 360, 'format_note': 'DASH video', 'vcodec': 'av01.0.01M.08'},
1236 '397': {'ext': 'mp4', 'height': 480, 'format_note': 'DASH video', 'vcodec': 'av01.0.04M.08'},
1237 '398': {'ext': 'mp4', 'height': 720, 'format_note': 'DASH video', 'vcodec': 'av01.0.05M.08'},
1238 '399': {'ext': 'mp4', 'height': 1080, 'format_note': 'DASH video', 'vcodec': 'av01.0.08M.08'},
1239 '400': {'ext': 'mp4', 'height': 1440, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
1240 '401': {'ext': 'mp4', 'height': 2160, 'format_note': 'DASH video', 'vcodec': 'av01.0.12M.08'},
1242 _SUBTITLE_FORMATS = ('json3', 'srv1', 'srv2', 'srv3', 'ttml', 'vtt')
1249 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&t=1s&end=9',
1251 'id': 'BaW_jenozKc',
1253 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
1254 'channel': 'Philipp Hagemeister',
1255 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
1256 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
1257 'upload_date': '20121002',
1258 'description': 'md5:8fb536f4877b8a7455c2ec23794dbc22',
1259 'categories': ['Science & Technology'],
1260 'tags': ['youtube-dl'],
1264 'availability': 'public',
1265 'playable_in_embed': True,
1266 'thumbnail': 'https://i.ytimg.com/vi/BaW_jenozKc/maxresdefault.jpg',
1267 'live_status': 'not_live',
1271 'comment_count': int,
1272 'channel_follower_count': int,
1273 'uploader': 'Philipp Hagemeister',
1274 'uploader_url': 'https://www.youtube.com/@PhilippHagemeister',
1275 'uploader_id': '@PhilippHagemeister',
1276 'heatmap': 'count:100',
1280 'url': '//www.YouTube.com/watch?v=yZIXLfi8CZQ',
1281 'note': 'Embed-only video (#1746)',
1283 'id': 'yZIXLfi8CZQ',
1285 'upload_date': '20120608',
1286 'title': 'Principal Sexually Assaults A Teacher - Episode 117 - 8th June 2012',
1287 'description': 'md5:09b78bd971f1e3e289601dfba15ca4f7',
1290 'skip': 'Private video',
1293 'url': 'https://www.youtube.com/watch?v=BaW_jenozKc&v=yZIXLfi8CZQ',
1294 'note': 'Use the first video ID in the URL',
1296 'id': 'BaW_jenozKc',
1298 'title': 'youtube-dl test video "\'/\\ä↭𝕐',
1299 'channel': 'Philipp Hagemeister',
1300 'channel_id': 'UCLqxVugv74EIW3VWh2NOa3Q',
1301 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCLqxVugv74EIW3VWh2NOa3Q',
1302 'upload_date': '20121002',
1303 'description': 'md5:8fb536f4877b8a7455c2ec23794dbc22',
1304 'categories': ['Science & Technology'],
1305 'tags': ['youtube-dl'],
1309 'availability': 'public',
1310 'playable_in_embed': True,
1311 'thumbnail': 'https://i.ytimg.com/vi/BaW_jenozKc/maxresdefault.jpg',
1312 'live_status': 'not_live',
1314 'comment_count': int,
1315 'channel_follower_count': int,
1316 'uploader': 'Philipp Hagemeister',
1317 'uploader_url': 'https://www.youtube.com/@PhilippHagemeister',
1318 'uploader_id': '@PhilippHagemeister',
1321 'skip_download': True,
1325 'url': 'https://www.youtube.com/watch?v=a9LDPn-MO4I',
1326 'note': '256k DASH audio (format 141) via DASH manifest',
1328 'id': 'a9LDPn-MO4I',
1330 'upload_date': '20121002',
1332 'title': 'UHDTV TEST 8K VIDEO.mp4'
1335 'youtube_include_dash_manifest': True,
1338 'skip': 'format 141 not served anymore',
1340 # DASH manifest with encrypted signature
1342 'url': 'https://www.youtube.com/watch?v=IB3lcPjvWLA',
1344 'id': 'IB3lcPjvWLA',
1346 'title': 'Afrojack, Spree Wilson - The Spark (Official Music Video) ft. Spree Wilson',
1347 'description': 'md5:8f5e2b82460520b619ccac1f509d43bf',
1349 'upload_date': '20131011',
1352 'channel_id': 'UChuZAo1RKL85gev3Eal9_zg',
1353 'playable_in_embed': True,
1354 'channel_url': 'https://www.youtube.com/channel/UChuZAo1RKL85gev3Eal9_zg',
1356 'track': 'The Spark',
1357 'live_status': 'not_live',
1358 'thumbnail': 'https://i.ytimg.com/vi_webp/IB3lcPjvWLA/maxresdefault.webp',
1359 'channel': 'Afrojack',
1361 'availability': 'public',
1362 'categories': ['Music'],
1364 'alt_title': 'The Spark',
1365 'channel_follower_count': int,
1366 'uploader': 'Afrojack',
1367 'uploader_url': 'https://www.youtube.com/@Afrojack',
1368 'uploader_id': '@Afrojack',
1371 'youtube_include_dash_manifest': True,
1372 'format': '141/bestaudio[ext=m4a]',
1375 # Age-gate videos. See https://github.com/yt-dlp/yt-dlp/pull/575#issuecomment-888837000
1377 'note': 'Embed allowed age-gate video',
1378 'url': 'https://youtube.com/watch?v=HtVdAasjOgU',
1380 'id': 'HtVdAasjOgU',
1382 'title': 'The Witcher 3: Wild Hunt - The Sword Of Destiny Trailer',
1383 'description': r're:(?s).{100,}About the Game\n.*?The Witcher 3: Wild Hunt.{100,}',
1385 'upload_date': '20140605',
1387 'categories': ['Gaming'],
1388 'thumbnail': 'https://i.ytimg.com/vi_webp/HtVdAasjOgU/maxresdefault.webp',
1389 'availability': 'needs_auth',
1390 'channel_url': 'https://www.youtube.com/channel/UCzybXLxv08IApdjdN0mJhEg',
1392 'channel': 'The Witcher',
1393 'live_status': 'not_live',
1395 'channel_id': 'UCzybXLxv08IApdjdN0mJhEg',
1396 'playable_in_embed': True,
1398 'channel_follower_count': int,
1399 'uploader': 'The Witcher',
1400 'uploader_url': 'https://www.youtube.com/@thewitcher',
1401 'uploader_id': '@thewitcher',
1405 'note': 'Age-gate video with embed allowed in public site',
1406 'url': 'https://youtube.com/watch?v=HsUATh_Nc2U',
1408 'id': 'HsUATh_Nc2U',
1410 'title': 'Godzilla 2 (Official Video)',
1411 'description': 'md5:bf77e03fcae5529475e500129b05668a',
1412 'upload_date': '20200408',
1414 'availability': 'needs_auth',
1415 'channel_id': 'UCYQT13AtrJC0gsM1far_zJg',
1416 'channel': 'FlyingKitty',
1417 'channel_url': 'https://www.youtube.com/channel/UCYQT13AtrJC0gsM1far_zJg',
1419 'categories': ['Entertainment'],
1420 'live_status': 'not_live',
1421 'tags': ['Flyingkitty', 'godzilla 2'],
1422 'thumbnail': 'https://i.ytimg.com/vi/HsUATh_Nc2U/maxresdefault.jpg',
1425 'playable_in_embed': True,
1426 'channel_follower_count': int,
1427 'uploader': 'FlyingKitty',
1428 'uploader_url': 'https://www.youtube.com/@FlyingKitty900',
1429 'uploader_id': '@FlyingKitty900',
1430 'comment_count': int,
1434 'note': 'Age-gate video embedable only with clientScreen=EMBED',
1435 'url': 'https://youtube.com/watch?v=Tq92D6wQ1mg',
1437 'id': 'Tq92D6wQ1mg',
1438 'title': '[MMD] Adios - EVERGLOW [+Motion DL]',
1440 'upload_date': '20191228',
1441 'description': 'md5:17eccca93a786d51bc67646756894066',
1444 'availability': 'needs_auth',
1445 'channel_id': 'UC1yoRdFoFJaCY-AGfD9W0wQ',
1447 'thumbnail': 'https://i.ytimg.com/vi_webp/Tq92D6wQ1mg/sddefault.webp',
1448 'channel': 'Projekt Melody',
1449 'live_status': 'not_live',
1450 'tags': ['mmd', 'dance', 'mikumikudance', 'kpop', 'vtuber'],
1451 'playable_in_embed': True,
1452 'categories': ['Entertainment'],
1454 'channel_url': 'https://www.youtube.com/channel/UC1yoRdFoFJaCY-AGfD9W0wQ',
1455 'comment_count': int,
1456 'channel_follower_count': int,
1457 'uploader': 'Projekt Melody',
1458 'uploader_url': 'https://www.youtube.com/@ProjektMelody',
1459 'uploader_id': '@ProjektMelody',
1463 'note': 'Non-Agegated non-embeddable video',
1464 'url': 'https://youtube.com/watch?v=MeJVWBSsPAY',
1466 'id': 'MeJVWBSsPAY',
1468 'title': 'OOMPH! - Such Mich Find Mich (Lyrics)',
1469 'description': 'Fan Video. Music & Lyrics by OOMPH!.',
1470 'upload_date': '20130730',
1471 'track': 'Such mich find mich',
1473 'tags': ['oomph', 'such mich find mich', 'lyrics', 'german industrial', 'musica industrial'],
1475 'playable_in_embed': False,
1476 'creator': 'OOMPH!',
1477 'thumbnail': 'https://i.ytimg.com/vi/MeJVWBSsPAY/sddefault.jpg',
1479 'alt_title': 'Such mich find mich',
1481 'channel': 'Herr Lurik',
1482 'channel_id': 'UCdR3RSDPqub28LjZx0v9-aA',
1483 'categories': ['Music'],
1484 'availability': 'public',
1485 'channel_url': 'https://www.youtube.com/channel/UCdR3RSDPqub28LjZx0v9-aA',
1486 'live_status': 'not_live',
1488 'channel_follower_count': int,
1489 'uploader': 'Herr Lurik',
1490 'uploader_url': 'https://www.youtube.com/@HerrLurik',
1491 'uploader_id': '@HerrLurik',
1495 'note': 'Non-bypassable age-gated video',
1496 'url': 'https://youtube.com/watch?v=Cr381pDsSsA',
1497 'only_matching': True,
1499 # video_info is None (https://github.com/ytdl-org/youtube-dl/issues/4421)
1500 # YouTube Red ad is not captured for creator
1502 'url': '__2ABJjxzNo',
1504 'id': '__2ABJjxzNo',
1507 'upload_date': '20100430',
1508 'creator': 'deadmau5',
1509 'description': 'md5:6cbcd3a92ce1bc676fc4d6ab4ace2336',
1510 'title': 'Deadmau5 - Some Chords (HD)',
1511 'alt_title': 'Some Chords',
1512 'availability': 'public',
1514 'channel_id': 'UCYEK6xds6eo-3tr4xRdflmQ',
1516 'live_status': 'not_live',
1517 'channel': 'deadmau5',
1518 'thumbnail': 'https://i.ytimg.com/vi_webp/__2ABJjxzNo/maxresdefault.webp',
1520 'track': 'Some Chords',
1521 'artist': 'deadmau5',
1522 'playable_in_embed': True,
1524 'channel_url': 'https://www.youtube.com/channel/UCYEK6xds6eo-3tr4xRdflmQ',
1525 'categories': ['Music'],
1526 'album': 'Some Chords',
1527 'channel_follower_count': int,
1528 'uploader': 'deadmau5',
1529 'uploader_url': 'https://www.youtube.com/@deadmau5',
1530 'uploader_id': '@deadmau5',
1532 'expected_warnings': [
1533 'DASH manifest missing',
1536 # Olympics (https://github.com/ytdl-org/youtube-dl/issues/4431)
1538 'url': 'lqQg6PlCWgI',
1540 'id': 'lqQg6PlCWgI',
1543 'upload_date': '20150827',
1544 'description': 'md5:04bbbf3ccceb6795947572ca36f45904',
1545 'title': 'Hockey - Women - GER-AUS - London 2012 Olympic Games',
1547 'release_timestamp': 1343767800,
1548 'playable_in_embed': True,
1549 'categories': ['Sports'],
1550 'release_date': '20120731',
1551 'channel': 'Olympics',
1552 'tags': ['Hockey', '2012-07-31', '31 July 2012', 'Riverbank Arena', 'Session', 'Olympics', 'Olympic Games', 'London 2012', '2012 Summer Olympics', 'Summer Games'],
1553 'channel_id': 'UCTl3QQTvqHFjurroKxexy2Q',
1554 'thumbnail': 'https://i.ytimg.com/vi/lqQg6PlCWgI/maxresdefault.jpg',
1556 'availability': 'public',
1557 'live_status': 'was_live',
1559 'channel_url': 'https://www.youtube.com/channel/UCTl3QQTvqHFjurroKxexy2Q',
1560 'channel_follower_count': int,
1561 'uploader': 'Olympics',
1562 'uploader_url': 'https://www.youtube.com/@Olympics',
1563 'uploader_id': '@Olympics',
1566 'skip_download': 'requires avconv',
1571 'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0',
1573 'id': '_b-2C3KPAM0',
1575 'stretched_ratio': 16 / 9.,
1577 'upload_date': '20110310',
1578 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯',
1579 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人',
1580 'playable_in_embed': True,
1584 'channel_url': 'https://www.youtube.com/channel/UCS-xxCmRaA6BFdmgDPA_BIw',
1585 'channel_id': 'UCS-xxCmRaA6BFdmgDPA_BIw',
1586 'thumbnail': 'https://i.ytimg.com/vi/_b-2C3KPAM0/maxresdefault.jpg',
1588 'categories': ['People & Blogs'],
1590 'live_status': 'not_live',
1591 'availability': 'unlisted',
1592 'comment_count': int,
1593 'channel_follower_count': int,
1595 'uploader_url': 'https://www.youtube.com/@AllenMeow',
1596 'uploader_id': '@AllenMeow',
1599 # url_encoded_fmt_stream_map is empty string
1601 'url': 'qEJwOuvDf7I',
1603 'id': 'qEJwOuvDf7I',
1605 'title': 'Обсуждение судебной практики по выборам 14 сентября 2014 года в Санкт-Петербурге',
1607 'upload_date': '20150404',
1610 'skip_download': 'requires avconv',
1612 'skip': 'This live event has ended.',
1614 # Extraction from multiple DASH manifests (https://github.com/ytdl-org/youtube-dl/pull/6097)
1616 'url': 'https://www.youtube.com/watch?v=FIl7x6_3R5Y',
1618 'id': 'FIl7x6_3R5Y',
1620 'title': 'md5:7b81415841e02ecd4313668cde88737a',
1621 'description': 'md5:116377fd2963b81ec4ce64b542173306',
1623 'upload_date': '20150625',
1624 'formats': 'mincount:31',
1626 'skip': 'not actual anymore',
1628 # DASH manifest with segment_list
1630 'url': 'https://www.youtube.com/embed/CsmdDsKjzN8',
1631 'md5': '8ce563a1d667b599d21064e982ab9e31',
1633 'id': 'CsmdDsKjzN8',
1635 'upload_date': '20150501', # According to '<meta itemprop="datePublished"', but in other places it's 20150510
1636 'description': 'Retransmisión en directo de la XVIII media maratón de Zaragoza.',
1637 'title': 'Retransmisión XVIII Media maratón Zaragoza 2015',
1640 'youtube_include_dash_manifest': True,
1641 'format': '135', # bestvideo
1643 'skip': 'This live event has ended.',
1646 # Multifeed videos (multiple cameras), URL can be of any Camera
1647 # TODO: fix multifeed titles
1648 'url': 'https://www.youtube.com/watch?v=zaPI8MvL8pg',
1650 'id': 'zaPI8MvL8pg',
1651 'title': 'Terraria 1.2 Live Stream | Let\'s Play - Part 04',
1652 'description': 'md5:563ccbc698b39298481ca3c571169519',
1656 'id': 'j5yGuxZ8lLU',
1658 'title': 'Terraria 1.2 Live Stream | Let\'s Play - Part 04 (Chris)',
1659 'description': 'md5:563ccbc698b39298481ca3c571169519',
1661 'channel_follower_count': int,
1662 'channel_url': 'https://www.youtube.com/channel/UCN2XePorRokPB9TEgRZpddg',
1663 'availability': 'public',
1664 'playable_in_embed': True,
1665 'upload_date': '20131105',
1666 'categories': ['Gaming'],
1667 'live_status': 'was_live',
1669 'release_timestamp': 1383701910,
1670 'thumbnail': 'https://i.ytimg.com/vi/j5yGuxZ8lLU/maxresdefault.jpg',
1671 'comment_count': int,
1674 'channel_id': 'UCN2XePorRokPB9TEgRZpddg',
1675 'channel': 'WiiLikeToPlay',
1677 'release_date': '20131106',
1678 'uploader': 'WiiLikeToPlay',
1679 'uploader_id': '@WLTP',
1680 'uploader_url': 'https://www.youtube.com/@WLTP',
1684 'id': 'zaPI8MvL8pg',
1686 'title': 'Terraria 1.2 Live Stream | Let\'s Play - Part 04 (Tyson)',
1687 'availability': 'public',
1688 'channel_url': 'https://www.youtube.com/channel/UCN2XePorRokPB9TEgRZpddg',
1689 'channel': 'WiiLikeToPlay',
1690 'channel_follower_count': int,
1691 'description': 'md5:563ccbc698b39298481ca3c571169519',
1696 'channel_id': 'UCN2XePorRokPB9TEgRZpddg',
1697 'release_timestamp': 1383701915,
1698 'comment_count': int,
1699 'upload_date': '20131105',
1700 'thumbnail': 'https://i.ytimg.com/vi/zaPI8MvL8pg/maxresdefault.jpg',
1701 'release_date': '20131106',
1702 'playable_in_embed': True,
1703 'live_status': 'was_live',
1704 'categories': ['Gaming'],
1706 'uploader': 'WiiLikeToPlay',
1707 'uploader_id': '@WLTP',
1708 'uploader_url': 'https://www.youtube.com/@WLTP',
1712 'id': 'R7r3vfO7Hao',
1714 'title': 'Terraria 1.2 Live Stream | Let\'s Play - Part 04 (Spencer)',
1715 'thumbnail': 'https://i.ytimg.com/vi/R7r3vfO7Hao/maxresdefault.jpg',
1716 'channel_id': 'UCN2XePorRokPB9TEgRZpddg',
1718 'availability': 'public',
1719 'playable_in_embed': True,
1720 'upload_date': '20131105',
1721 'description': 'md5:563ccbc698b39298481ca3c571169519',
1722 'channel_follower_count': int,
1724 'release_date': '20131106',
1725 'comment_count': int,
1726 'channel_url': 'https://www.youtube.com/channel/UCN2XePorRokPB9TEgRZpddg',
1727 'channel': 'WiiLikeToPlay',
1728 'categories': ['Gaming'],
1729 'release_timestamp': 1383701914,
1730 'live_status': 'was_live',
1734 'uploader': 'WiiLikeToPlay',
1735 'uploader_id': '@WLTP',
1736 'uploader_url': 'https://www.youtube.com/@WLTP',
1739 'params': {'skip_download': True},
1742 # Multifeed video with comma in title (see https://github.com/ytdl-org/youtube-dl/issues/8536)
1743 'url': 'https://www.youtube.com/watch?v=gVfLd0zydlo',
1745 'id': 'gVfLd0zydlo',
1746 'title': 'DevConf.cz 2016 Day 2 Workshops 1 14:00 - 15:30',
1748 'playlist_count': 2,
1749 'skip': 'Not multifeed anymore',
1752 'url': 'https://vid.plus/FlRa-iH7PGw',
1753 'only_matching': True,
1756 'url': 'https://zwearz.com/watch/9lWxNJF-ufM/electra-woman-dyna-girl-official-trailer-grace-helbig.html',
1757 'only_matching': True,
1760 # Title with JS-like syntax "};" (see https://github.com/ytdl-org/youtube-dl/issues/7468)
1761 # Also tests cut-off URL expansion in video description (see
1762 # https://github.com/ytdl-org/youtube-dl/issues/1892,
1763 # https://github.com/ytdl-org/youtube-dl/issues/8164)
1764 'url': 'https://www.youtube.com/watch?v=lsguqyKfVQg',
1766 'id': 'lsguqyKfVQg',
1768 'title': '{dark walk}; Loki/AC/Dishonored; collab w/Elflover21',
1769 'alt_title': 'Dark Walk',
1770 'description': 'md5:8085699c11dc3f597ce0410b0dcbb34a',
1772 'upload_date': '20151119',
1773 'creator': 'Todd Haberman;\nDaniel Law Heath and Aaron Kaplan',
1774 'track': 'Dark Walk',
1775 'artist': 'Todd Haberman;\nDaniel Law Heath and Aaron Kaplan',
1776 'album': 'Position Music - Production Music Vol. 143 - Dark Walk',
1777 'thumbnail': 'https://i.ytimg.com/vi_webp/lsguqyKfVQg/maxresdefault.webp',
1778 'categories': ['Film & Animation'],
1780 'live_status': 'not_live',
1781 'channel_url': 'https://www.youtube.com/channel/UCTSRgz5jylBvFt_S7wnsqLQ',
1782 'channel_id': 'UCTSRgz5jylBvFt_S7wnsqLQ',
1784 'availability': 'public',
1785 'channel': 'IronSoulElf',
1786 'playable_in_embed': True,
1789 'channel_follower_count': int
1792 'skip_download': True,
1796 # Tags with '};' (see https://github.com/ytdl-org/youtube-dl/issues/7468)
1797 'url': 'https://www.youtube.com/watch?v=Ms7iBXnlUO8',
1798 'only_matching': True,
1801 # Video with yt:stretch=17:0
1802 'url': 'https://www.youtube.com/watch?v=Q39EVAstoRM',
1804 'id': 'Q39EVAstoRM',
1806 'title': 'Clash Of Clans#14 Dicas De Ataque Para CV 4',
1807 'description': 'md5:ee18a25c350637c8faff806845bddee9',
1808 'upload_date': '20151107',
1811 'skip_download': True,
1813 'skip': 'This video does not exist.',
1816 # Video with incomplete 'yt:stretch=16:'
1817 'url': 'https://www.youtube.com/watch?v=FRhJzUSJbGI',
1818 'only_matching': True,
1821 # Video licensed under Creative Commons
1822 'url': 'https://www.youtube.com/watch?v=M4gD1WSo5mA',
1824 'id': 'M4gD1WSo5mA',
1826 'title': 'md5:e41008789470fc2533a3252216f1c1d1',
1827 'description': 'md5:a677553cf0840649b731a3024aeff4cc',
1829 'upload_date': '20150128',
1830 'license': 'Creative Commons Attribution license (reuse allowed)',
1831 'channel_id': 'UCuLGmD72gJDBwmLw06X58SA',
1832 'channel_url': 'https://www.youtube.com/channel/UCuLGmD72gJDBwmLw06X58SA',
1835 'tags': ['Copyright (Legal Subject)', 'Law (Industry)', 'William W. Fisher (Author)'],
1836 'channel': 'The Berkman Klein Center for Internet & Society',
1837 'availability': 'public',
1839 'categories': ['Education'],
1840 'thumbnail': 'https://i.ytimg.com/vi_webp/M4gD1WSo5mA/maxresdefault.webp',
1841 'live_status': 'not_live',
1842 'playable_in_embed': True,
1843 'channel_follower_count': int,
1845 'uploader': 'The Berkman Klein Center for Internet & Society',
1846 'uploader_id': '@BKCHarvard',
1847 'uploader_url': 'https://www.youtube.com/@BKCHarvard',
1850 'skip_download': True,
1854 'url': 'https://www.youtube.com/watch?v=eQcmzGIKrzg',
1856 'id': 'eQcmzGIKrzg',
1858 'title': 'Democratic Socialism and Foreign Policy | Bernie Sanders',
1859 'description': 'md5:13a2503d7b5904ef4b223aa101628f39',
1861 'upload_date': '20151120',
1862 'license': 'Creative Commons Attribution license (reuse allowed)',
1863 'playable_in_embed': True,
1866 'channel_id': 'UCH1dpzjCEiGAt8CXkryhkZg',
1868 'availability': 'public',
1869 'categories': ['News & Politics'],
1870 'channel': 'Bernie Sanders',
1871 'thumbnail': 'https://i.ytimg.com/vi_webp/eQcmzGIKrzg/maxresdefault.webp',
1873 'live_status': 'not_live',
1874 'channel_url': 'https://www.youtube.com/channel/UCH1dpzjCEiGAt8CXkryhkZg',
1875 'comment_count': int,
1876 'channel_follower_count': int,
1878 'uploader': 'Bernie Sanders',
1879 'uploader_url': 'https://www.youtube.com/@BernieSanders',
1880 'uploader_id': '@BernieSanders',
1883 'skip_download': True,
1887 'url': 'https://www.youtube.com/watch?feature=player_embedded&amp;v=V36LpHqtcDY',
1888 'only_matching': True,
1891 # YouTube Red paid video (https://github.com/ytdl-org/youtube-dl/issues/10059)
1892 'url': 'https://www.youtube.com/watch?v=i1Ko8UG-Tdo',
1893 'only_matching': True,
1896 # Rental video preview
1897 'url': 'https://www.youtube.com/watch?v=yYr8q0y5Jfg',
1899 'id': 'uGpuVWrhIzE',
1901 'title': 'Piku - Trailer',
1902 'description': 'md5:c36bd60c3fd6f1954086c083c72092eb',
1903 'upload_date': '20150811',
1904 'license': 'Standard YouTube License',
1907 'skip_download': True,
1909 'skip': 'This video is not available.',
1912 # YouTube Red video with episode data
1913 'url': 'https://www.youtube.com/watch?v=iqKdEhx-dD4',
1915 'id': 'iqKdEhx-dD4',
1917 'title': 'Isolation - Mind Field (Ep 1)',
1918 'description': 'md5:f540112edec5d09fc8cc752d3d4ba3cd',
1920 'upload_date': '20170118',
1921 'series': 'Mind Field',
1923 'episode_number': 1,
1924 'thumbnail': 'https://i.ytimg.com/vi_webp/iqKdEhx-dD4/maxresdefault.webp',
1927 'availability': 'public',
1929 'channel': 'Vsauce',
1930 'episode': 'Episode 1',
1931 'categories': ['Entertainment'],
1932 'season': 'Season 1',
1933 'channel_id': 'UC6nSFpj9HTCZ5t-N3Rm3-HA',
1934 'channel_url': 'https://www.youtube.com/channel/UC6nSFpj9HTCZ5t-N3Rm3-HA',
1936 'playable_in_embed': True,
1937 'live_status': 'not_live',
1938 'channel_follower_count': int,
1939 'uploader': 'Vsauce',
1940 'uploader_url': 'https://www.youtube.com/@Vsauce',
1941 'uploader_id': '@Vsauce',
1944 'skip_download': True,
1946 'expected_warnings': [
1947 'Skipping DASH manifest',
1951 # The following content has been identified by the YouTube community
1952 # as inappropriate or offensive to some audiences.
1953 'url': 'https://www.youtube.com/watch?v=6SJNVb0GnPI',
1955 'id': '6SJNVb0GnPI',
1957 'title': 'Race Differences in Intelligence',
1958 'description': 'md5:5d161533167390427a1f8ee89a1fc6f1',
1960 'upload_date': '20140124',
1963 'skip_download': True,
1965 'skip': 'This video has been removed for violating YouTube\'s policy on hate speech.',
1969 'url': '1t24XAntNCY',
1970 'only_matching': True,
1973 # geo restricted to JP
1974 'url': 'sJL6WA-aGkQ',
1975 'only_matching': True,
1978 'url': 'https://invidio.us/watch?v=BaW_jenozKc',
1979 'only_matching': True,
1982 'url': 'https://redirect.invidious.io/watch?v=BaW_jenozKc',
1983 'only_matching': True,
1986 # from https://nitter.pussthecat.org/YouTube/status/1360363141947944964#m
1987 'url': 'https://redirect.invidious.io/Yh0AhrY9GjA',
1988 'only_matching': True,
1992 'url': 'https://www.youtube.com/watch?v=s7_qI6_mIXc',
1993 'only_matching': True,
1996 # Video with unsupported adaptive stream type formats
1997 'url': 'https://www.youtube.com/watch?v=Z4Vy8R84T1U',
1999 'id': 'Z4Vy8R84T1U',
2001 'title': 'saman SMAN 53 Jakarta(Sancety) opening COFFEE4th at SMAN 53 Jakarta',
2002 'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
2004 'upload_date': '20130923',
2005 'formats': 'maxcount:10',
2008 'skip_download': True,
2009 'youtube_include_dash_manifest': False,
2011 'skip': 'not actual anymore',
2014 # Youtube Music Auto-generated description
2015 # TODO: fix metadata extraction
2016 'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
2018 'id': 'MgNrAu2pzNs',
2020 'title': 'Voyeur Girl',
2021 'description': 'md5:7ae382a65843d6df2685993e90a8628f',
2022 'upload_date': '20190312',
2023 'artist': 'Stephen',
2024 'track': 'Voyeur Girl',
2025 'album': 'it\'s too much love to know my dear',
2026 'release_date': '20190313',
2027 'release_year': 2019,
2028 'alt_title': 'Voyeur Girl',
2030 'playable_in_embed': True,
2032 'categories': ['Music'],
2033 'channel_url': 'https://www.youtube.com/channel/UC-pWHpBjdGG69N9mM2auIAA',
2034 'channel': 'Stephen', # TODO: should be "Stephen - Topic"
2035 'uploader': 'Stephen',
2036 'availability': 'public',
2037 'creator': 'Stephen',
2039 'thumbnail': 'https://i.ytimg.com/vi_webp/MgNrAu2pzNs/maxresdefault.webp',
2041 'channel_id': 'UC-pWHpBjdGG69N9mM2auIAA',
2043 'live_status': 'not_live',
2044 'channel_follower_count': int
2047 'skip_download': True,
2051 'url': 'https://www.youtubekids.com/watch?v=3b8nCWDgZ6Q',
2052 'only_matching': True,
2055 # invalid -> valid video id redirection
2056 'url': 'DJztXj2GPfl',
2058 'id': 'DJztXj2GPfk',
2060 'title': 'Panjabi MC - Mundian To Bach Ke (The Dictator Soundtrack)',
2061 'description': 'md5:bf577a41da97918e94fa9798d9228825',
2062 'upload_date': '20090125',
2063 'artist': 'Panjabi MC',
2064 'track': 'Beware of the Boys (Mundian to Bach Ke) - Motivo Hi-Lectro Remix',
2065 'album': 'Beware of the Boys (Mundian To Bach Ke)',
2068 'skip_download': True,
2070 'skip': 'Video unavailable',
2073 # empty description results in an empty string
2074 'url': 'https://www.youtube.com/watch?v=x41yOUIvK2k',
2076 'id': 'x41yOUIvK2k',
2078 'title': 'IMG 3456',
2080 'upload_date': '20170613',
2082 'thumbnail': 'https://i.ytimg.com/vi_webp/x41yOUIvK2k/maxresdefault.webp',
2084 'channel_id': 'UCo03ZQPBW5U4UC3regpt1nw',
2086 'channel_url': 'https://www.youtube.com/channel/UCo03ZQPBW5U4UC3regpt1nw',
2087 'availability': 'public',
2089 'categories': ['Pets & Animals'],
2091 'playable_in_embed': True,
2092 'live_status': 'not_live',
2093 'channel': 'l\'Or Vert asbl',
2094 'channel_follower_count': int,
2095 'uploader': 'l\'Or Vert asbl',
2096 'uploader_url': 'https://www.youtube.com/@ElevageOrVert',
2097 'uploader_id': '@ElevageOrVert',
2100 'skip_download': True,
2104 # with '};' inside yt initial data (see [1])
2105 # see [2] for an example with '};' inside ytInitialPlayerResponse
2106 # 1. https://github.com/ytdl-org/youtube-dl/issues/27093
2107 # 2. https://github.com/ytdl-org/youtube-dl/issues/27216
2108 'url': 'https://www.youtube.com/watch?v=CHqg6qOn4no',
2110 'id': 'CHqg6qOn4no',
2112 'title': 'Part 77 Sort a list of simple types in c#',
2113 'description': 'md5:b8746fa52e10cdbf47997903f13b20dc',
2114 'upload_date': '20130831',
2115 'channel_id': 'UCCTVrRB5KpIiK6V2GGVsR1Q',
2117 'channel_url': 'https://www.youtube.com/channel/UCCTVrRB5KpIiK6V2GGVsR1Q',
2118 'live_status': 'not_live',
2119 'categories': ['Education'],
2120 'availability': 'public',
2121 'thumbnail': 'https://i.ytimg.com/vi/CHqg6qOn4no/sddefault.jpg',
2123 'playable_in_embed': True,
2127 'channel': 'kudvenkat',
2128 'comment_count': int,
2129 'channel_follower_count': int,
2131 'uploader': 'kudvenkat',
2132 'uploader_url': 'https://www.youtube.com/@Csharp-video-tutorialsBlogspot',
2133 'uploader_id': '@Csharp-video-tutorialsBlogspot',
2136 'skip_download': True,
2140 # another example of '};' in ytInitialData
2141 'url': 'https://www.youtube.com/watch?v=gVfgbahppCY',
2142 'only_matching': True,
2145 'url': 'https://www.youtube.com/watch_popup?v=63RmMXCd_bQ',
2146 'only_matching': True,
2149 # https://github.com/ytdl-org/youtube-dl/pull/28094
2150 'url': 'OtqTfy26tG0',
2152 'id': 'OtqTfy26tG0',
2154 'title': 'Burn Out',
2155 'description': 'md5:8d07b84dcbcbfb34bc12a56d968b6131',
2156 'upload_date': '20141120',
2157 'artist': 'The Cinematic Orchestra',
2158 'track': 'Burn Out',
2159 'album': 'Every Day',
2161 'live_status': 'not_live',
2162 'alt_title': 'Burn Out',
2166 'channel_url': 'https://www.youtube.com/channel/UCIzsJBIyo8hhpFm1NK0uLgw',
2167 'creator': 'The Cinematic Orchestra',
2168 'channel': 'The Cinematic Orchestra',
2169 'tags': ['The Cinematic Orchestra', 'Every Day', 'Burn Out'],
2170 'channel_id': 'UCIzsJBIyo8hhpFm1NK0uLgw',
2171 'availability': 'public',
2172 'thumbnail': 'https://i.ytimg.com/vi/OtqTfy26tG0/maxresdefault.jpg',
2173 'categories': ['Music'],
2174 'playable_in_embed': True,
2175 'channel_follower_count': int,
2176 'uploader': 'The Cinematic Orchestra',
2177 'comment_count': int,
2180 'skip_download': True,
2184 # controversial video, only works with bpctr when authenticated with cookies
2185 'url': 'https://www.youtube.com/watch?v=nGC3D_FkCmg',
2186 'only_matching': True,
2189 # controversial video, requires bpctr/contentCheckOk
2190 'url': 'https://www.youtube.com/watch?v=SZJvDhaSDnc',
2192 'id': 'SZJvDhaSDnc',
2194 'title': 'San Diego teen commits suicide after bullying over embarrassing video',
2195 'channel_id': 'UC-SJ6nODDmufqBzPBwCvYvQ',
2196 'upload_date': '20140716',
2197 'description': 'md5:acde3a73d3f133fc97e837a9f76b53b7',
2199 'categories': ['News & Politics'],
2201 'channel': 'CBS Mornings',
2202 'tags': ['suicide', 'bullying', 'video', 'cbs', 'news'],
2203 'thumbnail': 'https://i.ytimg.com/vi/SZJvDhaSDnc/hqdefault.jpg',
2205 'availability': 'needs_auth',
2206 'channel_url': 'https://www.youtube.com/channel/UC-SJ6nODDmufqBzPBwCvYvQ',
2208 'live_status': 'not_live',
2209 'playable_in_embed': True,
2210 'channel_follower_count': int,
2211 'uploader': 'CBS Mornings',
2212 'uploader_url': 'https://www.youtube.com/@CBSMornings',
2213 'uploader_id': '@CBSMornings',
2217 # restricted location, https://github.com/ytdl-org/youtube-dl/issues/28685
2218 'url': 'cBvYw8_A0vQ',
2220 'id': 'cBvYw8_A0vQ',
2222 'title': '4K Ueno Okachimachi Street Scenes 上野御徒町歩き',
2223 'description': 'md5:ea770e474b7cd6722b4c95b833c03630',
2224 'upload_date': '20201120',
2226 'categories': ['Travel & Events'],
2227 'channel_id': 'UC3o_t8PzBmXf5S9b7GLx1Mw',
2229 'channel': 'Walk around Japan',
2230 'tags': ['Ueno Tokyo', 'Okachimachi Tokyo', 'Ameyoko Street', 'Tokyo attraction', 'Travel in Tokyo'],
2231 'thumbnail': 'https://i.ytimg.com/vi_webp/cBvYw8_A0vQ/hqdefault.webp',
2233 'availability': 'public',
2234 'channel_url': 'https://www.youtube.com/channel/UC3o_t8PzBmXf5S9b7GLx1Mw',
2235 'live_status': 'not_live',
2236 'playable_in_embed': True,
2237 'channel_follower_count': int,
2238 'uploader': 'Walk around Japan',
2239 'uploader_url': 'https://www.youtube.com/@walkaroundjapan7124',
2240 'uploader_id': '@walkaroundjapan7124',
2243 'skip_download': True,
2246 # Has multiple audio streams
2247 'url': 'WaOKSUlf4TM',
2248 'only_matching': True
2250 # Requires Premium: has format 141 when requested using YTM url
2251 'url': 'https://music.youtube.com/watch?v=XclachpHxis',
2252 'only_matching': True
2254 # multiple subtitles with same lang_code
2255 'url': 'https://www.youtube.com/watch?v=wsQiKKfKxug',
2256 'only_matching': True,
2258 # Force use android client fallback
2259 'url': 'https://www.youtube.com/watch?v=YOelRv7fMxY',
2261 'id': 'YOelRv7fMxY',
2262 'title': 'DIGGING A SECRET TUNNEL Part 1',
2264 'upload_date': '20210624',
2265 'channel_id': 'UCp68_FLety0O-n9QU6phsgw',
2266 'channel_url': r're:https?://(?:www\.)?youtube\.com/channel/UCp68_FLety0O-n9QU6phsgw',
2267 'description': 'md5:5d5991195d599b56cd0c4148907eec50',
2269 'categories': ['Entertainment'],
2271 'channel': 'colinfurze',
2272 'tags': ['Colin', 'furze', 'Terry', 'tunnel', 'underground', 'bunker'],
2273 'thumbnail': 'https://i.ytimg.com/vi/YOelRv7fMxY/maxresdefault.jpg',
2275 'availability': 'public',
2277 'live_status': 'not_live',
2278 'playable_in_embed': True,
2279 'channel_follower_count': int,
2281 'uploader': 'colinfurze',
2282 'uploader_url': 'https://www.youtube.com/@colinfurze',
2283 'uploader_id': '@colinfurze',
2286 'format': '17', # 3gp format available on android
2287 'extractor_args': {'youtube': {'player_client': ['android']}},
2291 # Skip download of additional client configs (remix client config in this case)
2292 'url': 'https://music.youtube.com/watch?v=MgNrAu2pzNs',
2293 'only_matching': True,
2295 'extractor_args': {'youtube': {'player_skip': ['configs']}},
2299 'url': 'https://www.youtube.com/shorts/BGQWPY4IigY',
2300 'only_matching': True,
2302 'note': 'Storyboards',
2303 'url': 'https://www.youtube.com/watch?v=5KLPxDtMqe8',
2305 'id': '5KLPxDtMqe8',
2308 'title': 'Your Brain is Plastic',
2309 'description': 'md5:89cd86034bdb5466cd87c6ba206cd2bc',
2310 'upload_date': '20140324',
2312 'channel_id': 'UCZYTClx2T1of7BRZ86-8fow',
2313 'channel_url': 'https://www.youtube.com/channel/UCZYTClx2T1of7BRZ86-8fow',
2315 'thumbnail': 'https://i.ytimg.com/vi/5KLPxDtMqe8/maxresdefault.jpg',
2316 'playable_in_embed': True,
2318 'availability': 'public',
2319 'channel': 'SciShow',
2320 'live_status': 'not_live',
2322 'categories': ['Education'],
2324 'channel_follower_count': int,
2326 'uploader': 'SciShow',
2327 'uploader_url': 'https://www.youtube.com/@SciShow',
2328 'uploader_id': '@SciShow',
2329 }, 'params': {'format': 'mhtml', 'skip_download': True}
2331 # Ensure video upload_date is in UTC timezone (video was uploaded 1641170939)
2332 'url': 'https://www.youtube.com/watch?v=2NUZ8W2llS4',
2334 'id': '2NUZ8W2llS4',
2336 'title': 'The NP that test your phone performance 🙂',
2337 'description': 'md5:144494b24d4f9dfacb97c1bbef5de84d',
2338 'channel_id': 'UCRqNBSOHgilHfAczlUmlWHA',
2339 'channel_url': 'https://www.youtube.com/channel/UCRqNBSOHgilHfAczlUmlWHA',
2343 'categories': ['Gaming'],
2345 'playable_in_embed': True,
2346 'live_status': 'not_live',
2347 'upload_date': '20220103',
2349 'availability': 'public',
2350 'channel': 'Leon Nguyen',
2351 'thumbnail': 'https://i.ytimg.com/vi_webp/2NUZ8W2llS4/maxresdefault.webp',
2352 'comment_count': int,
2353 'channel_follower_count': int,
2354 'uploader': 'Leon Nguyen',
2355 'uploader_url': 'https://www.youtube.com/@LeonNguyen',
2356 'uploader_id': '@LeonNguyen',
2359 # Same video as above, but with --compat-opt no-youtube-prefer-utc-upload-date
2360 'url': 'https://www.youtube.com/watch?v=2NUZ8W2llS4',
2362 'id': '2NUZ8W2llS4',
2364 'title': 'The NP that test your phone performance 🙂',
2365 'description': 'md5:144494b24d4f9dfacb97c1bbef5de84d',
2366 'channel_id': 'UCRqNBSOHgilHfAczlUmlWHA',
2367 'channel_url': 'https://www.youtube.com/channel/UCRqNBSOHgilHfAczlUmlWHA',
2371 'categories': ['Gaming'],
2373 'playable_in_embed': True,
2374 'live_status': 'not_live',
2375 'upload_date': '20220102',
2377 'availability': 'public',
2378 'channel': 'Leon Nguyen',
2379 'thumbnail': 'https://i.ytimg.com/vi_webp/2NUZ8W2llS4/maxresdefault.webp',
2380 'comment_count': int,
2381 'channel_follower_count': int,
2382 'uploader': 'Leon Nguyen',
2383 'uploader_url': 'https://www.youtube.com/@LeonNguyen',
2384 'uploader_id': '@LeonNguyen',
2386 'params': {'compat_opts': ['no-youtube-prefer-utc-upload-date']}
2388 # date text is premiered video, ensure upload date in UTC (published 1641172509)
2389 'url': 'https://www.youtube.com/watch?v=mzZzzBU6lrM',
2391 'id': 'mzZzzBU6lrM',
2393 'title': 'I Met GeorgeNotFound In Real Life...',
2394 'description': 'md5:978296ec9783a031738b684d4ebf302d',
2395 'channel_id': 'UC_8NknAFiyhOUaZqHR3lq3Q',
2396 'channel_url': 'https://www.youtube.com/channel/UC_8NknAFiyhOUaZqHR3lq3Q',
2400 'categories': ['Entertainment'],
2402 'playable_in_embed': True,
2403 'live_status': 'not_live',
2404 'release_timestamp': 1641172509,
2405 'release_date': '20220103',
2406 'upload_date': '20220103',
2408 'availability': 'public',
2409 'channel': 'Quackity',
2410 'thumbnail': 'https://i.ytimg.com/vi/mzZzzBU6lrM/maxresdefault.jpg',
2411 'channel_follower_count': int,
2412 'uploader': 'Quackity',
2413 'uploader_id': '@Quackity',
2414 'uploader_url': 'https://www.youtube.com/@Quackity',
2417 { # continuous livestream. Microformat upload date should be preferred.
2418 # Upload date was 2021-06-19 (not UTC), while stream start is 2021-11-27
2419 'url': 'https://www.youtube.com/watch?v=kgx4WGK0oNU',
2421 'id': 'kgx4WGK0oNU',
2422 '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}',
2424 'channel_id': 'UC84whx2xxsiA1gXHXXqKGOA',
2425 'availability': 'public',
2427 'release_timestamp': 1637975704,
2428 'upload_date': '20210619',
2429 'channel_url': 'https://www.youtube.com/channel/UC84whx2xxsiA1gXHXXqKGOA',
2430 'live_status': 'is_live',
2431 'thumbnail': 'https://i.ytimg.com/vi/kgx4WGK0oNU/maxresdefault.jpg',
2432 'channel': 'Abao in Tokyo',
2433 'channel_follower_count': int,
2434 'release_date': '20211127',
2436 'categories': ['People & Blogs'],
2439 'playable_in_embed': True,
2440 'description': 'md5:2ef1d002cad520f65825346e2084e49d',
2441 'concurrent_view_count': int,
2442 'uploader': 'Abao in Tokyo',
2443 'uploader_url': 'https://www.youtube.com/@abaointokyo',
2444 'uploader_id': '@abaointokyo',
2446 'params': {'skip_download': True}
2448 # Story. Requires specific player params to work.
2449 'url': 'https://www.youtube.com/watch?v=vv8qTUWmulI',
2451 'id': 'vv8qTUWmulI',
2453 'availability': 'unlisted',
2455 'channel_id': 'UCzIZ8HrzDgc-pNQDUG6avBA',
2456 'upload_date': '20220526',
2457 'categories': ['Education'],
2459 'channel': 'IT\'S HISTORY',
2462 'playable_in_embed': True,
2464 'live_status': 'not_live',
2466 'thumbnail': 'https://i.ytimg.com/vi_webp/vv8qTUWmulI/maxresdefault.webp',
2467 'channel_url': 'https://www.youtube.com/channel/UCzIZ8HrzDgc-pNQDUG6avBA',
2469 'skip': 'stories get removed after some period of time',
2471 'url': 'https://www.youtube.com/watch?v=tjjjtzRLHvA',
2473 'id': 'tjjjtzRLHvA',
2475 'title': 'ハッシュタグ無し };if window.ytcsi',
2476 'upload_date': '20220323',
2478 'availability': 'unlisted',
2479 'channel': 'Lesmiscore',
2480 'thumbnail': r're:^https?://.*\.jpg',
2482 'categories': ['Music'],
2485 'channel_url': 'https://www.youtube.com/channel/UCdqltm_7iv1Vs6kp6Syke5A',
2486 'channel_id': 'UCdqltm_7iv1Vs6kp6Syke5A',
2487 'live_status': 'not_live',
2488 'playable_in_embed': True,
2489 'channel_follower_count': int,
2492 'uploader_id': '@lesmiscore',
2493 'uploader': 'Lesmiscore',
2494 'uploader_url': 'https://www.youtube.com/@lesmiscore',
2497 # Prefer primary title+description language metadata by default
2498 # Do not prefer translated description if primary is empty
2499 'url': 'https://www.youtube.com/watch?v=el3E4MbxRqQ',
2501 'id': 'el3E4MbxRqQ',
2503 'title': 'dlp test video 2 - primary sv no desc',
2505 'channel': 'cole-dlp-test-acc',
2508 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2510 'playable_in_embed': True,
2511 'availability': 'unlisted',
2512 'thumbnail': r're:^https?://.*\.jpg',
2515 'live_status': 'not_live',
2516 'upload_date': '20220908',
2517 'categories': ['People & Blogs'],
2518 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
2519 'uploader_url': 'https://www.youtube.com/@coletdjnz',
2520 'uploader_id': '@coletdjnz',
2521 'uploader': 'cole-dlp-test-acc',
2523 'params': {'skip_download': True}
2525 # Extractor argument: prefer translated title+description
2526 'url': 'https://www.youtube.com/watch?v=gHKT4uU8Zng',
2528 'id': 'gHKT4uU8Zng',
2530 'channel': 'cole-dlp-test-acc',
2533 'live_status': 'not_live',
2534 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
2535 'upload_date': '20220728',
2537 'categories': ['People & Blogs'],
2538 'thumbnail': r're:^https?://.*\.jpg',
2539 'title': 'dlp test video title translated (fr)',
2540 'availability': 'public',
2542 'description': 'dlp test video description translated (fr)',
2543 'playable_in_embed': True,
2544 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
2545 'uploader_url': 'https://www.youtube.com/@coletdjnz',
2546 'uploader_id': '@coletdjnz',
2547 'uploader': 'cole-dlp-test-acc',
2549 'params': {'skip_download': True, 'extractor_args': {'youtube': {'lang': ['fr']}}},
2550 'expected_warnings': [r'Preferring "fr" translated fields'],
2552 'note': '6 channel audio',
2553 'url': 'https://www.youtube.com/watch?v=zgdo7-RRjgo',
2554 'only_matching': True,
2556 'note': 'Multiple HLS formats with same itag',
2557 'url': 'https://www.youtube.com/watch?v=kX3nB4PpJko',
2559 'id': 'kX3nB4PpJko',
2561 'categories': ['Entertainment'],
2562 'description': 'md5:e8031ff6e426cdb6a77670c9b81f6fa6',
2563 'live_status': 'not_live',
2565 'channel_follower_count': int,
2566 'thumbnail': 'https://i.ytimg.com/vi_webp/kX3nB4PpJko/maxresdefault.webp',
2567 'title': 'Last To Take Hand Off Jet, Keeps It!',
2568 'channel': 'MrBeast',
2569 'playable_in_embed': True,
2571 'upload_date': '20221112',
2572 'channel_url': 'https://www.youtube.com/channel/UCX6OQ3DkcsbYNE6H8uQQuVA',
2574 'availability': 'public',
2575 'channel_id': 'UCX6OQ3DkcsbYNE6H8uQQuVA',
2578 'uploader': 'MrBeast',
2579 'uploader_url': 'https://www.youtube.com/@MrBeast',
2580 'uploader_id': '@MrBeast',
2582 'params': {'extractor_args': {'youtube': {'player_client': ['ios']}}, 'format': '233-1'},
2584 'note': 'Audio formats with Dynamic Range Compression',
2585 'url': 'https://www.youtube.com/watch?v=Tq92D6wQ1mg',
2587 'id': 'Tq92D6wQ1mg',
2589 'title': '[MMD] Adios - EVERGLOW [+Motion DL]',
2590 'channel_url': 'https://www.youtube.com/channel/UC1yoRdFoFJaCY-AGfD9W0wQ',
2591 'channel_id': 'UC1yoRdFoFJaCY-AGfD9W0wQ',
2592 'channel_follower_count': int,
2593 'description': 'md5:17eccca93a786d51bc67646756894066',
2594 'upload_date': '20191228',
2595 'tags': ['mmd', 'dance', 'mikumikudance', 'kpop', 'vtuber'],
2596 'playable_in_embed': True,
2598 'categories': ['Entertainment'],
2599 'thumbnail': 'https://i.ytimg.com/vi/Tq92D6wQ1mg/sddefault.jpg',
2601 'channel': 'Projekt Melody',
2603 'availability': 'needs_auth',
2604 'comment_count': int,
2605 'live_status': 'not_live',
2607 'uploader': 'Projekt Melody',
2608 'uploader_id': '@ProjektMelody',
2609 'uploader_url': 'https://www.youtube.com/@ProjektMelody',
2611 'params': {'extractor_args': {'youtube': {'player_client': ['tv_embedded']}}, 'format': '251-drc'},
2614 'url': 'https://www.youtube.com/live/qVv6vCqciTM',
2616 'id': 'qVv6vCqciTM',
2619 'comment_count': int,
2620 'chapters': 'count:13',
2621 'upload_date': '20221223',
2622 'thumbnail': 'https://i.ytimg.com/vi/qVv6vCqciTM/maxresdefault.jpg',
2623 'channel_url': 'https://www.youtube.com/channel/UCIdEIHpS0TdkqRkHL5OkLtA',
2625 'release_date': '20221223',
2626 'tags': ['Vtuber', '月ノ美兎', '名取さな', 'にじさんじ', 'クリスマス', '3D配信'],
2627 'title': '【 #インターネット女クリスマス 】3Dで歌ってはしゃぐインターネットの女たち【月ノ美兎/名取さな】',
2629 'playable_in_embed': True,
2631 'availability': 'public',
2632 'channel_follower_count': int,
2633 'channel_id': 'UCIdEIHpS0TdkqRkHL5OkLtA',
2634 'categories': ['Entertainment'],
2635 'live_status': 'was_live',
2636 'release_timestamp': 1671793345,
2637 'channel': 'さなちゃんねる',
2638 'description': 'md5:6aebf95cc4a1d731aebc01ad6cc9806d',
2639 'uploader': 'さなちゃんねる',
2640 'uploader_url': 'https://www.youtube.com/@sana_natori',
2641 'uploader_id': '@sana_natori',
2645 # Fallbacks when webpage and web client is unavailable
2646 'url': 'https://www.youtube.com/watch?v=wSSmNUl9Snw',
2648 'id': 'wSSmNUl9Snw',
2650 # 'categories': ['Science & Technology'],
2652 'chapters': 'count:2',
2653 'channel': 'Scott Manley',
2656 # 'availability': 'public',
2657 'channel_follower_count': int,
2658 'live_status': 'not_live',
2659 'upload_date': '20170831',
2662 'uploader_url': 'https://www.youtube.com/@scottmanley',
2663 'description': 'md5:f4bed7b200404b72a394c2f97b782c02',
2664 'uploader': 'Scott Manley',
2665 'uploader_id': '@scottmanley',
2666 'title': 'The Computer Hack That Saved Apollo 14',
2667 'channel_id': 'UCxzC4EngIsMrPmbm6Nxvb-A',
2668 'thumbnail': r're:^https?://.*\.webp',
2669 'channel_url': 'https://www.youtube.com/channel/UCxzC4EngIsMrPmbm6Nxvb-A',
2670 'playable_in_embed': True,
2673 'extractor_args': {'youtube': {'player_client': ['android'], 'player_skip': ['webpage']}},
2679 # YouTube <object> embed
2681 'url': 'http://www.improbable.com/2017/04/03/untrained-modern-youths-and-ancient-masters-in-selfie-portraits/',
2682 'md5': '873c81d308b979f0e23ee7e620b312a3',
2684 'id': 'msN87y-iEx0',
2686 'title': 'Feynman: Mirrors FUN TO IMAGINE 6',
2687 'upload_date': '20080526',
2688 'description': 'md5:873c81d308b979f0e23ee7e620b312a3',
2690 'tags': ['feynman', 'mirror', 'science', 'physics', 'imagination', 'fun', 'cool', 'puzzle'],
2691 'channel_id': 'UCCeo--lls1vna5YJABWAcVA',
2692 'playable_in_embed': True,
2693 'thumbnail': 'https://i.ytimg.com/vi/msN87y-iEx0/hqdefault.jpg',
2695 'comment_count': int,
2696 'channel': 'Christopher Sykes',
2697 'live_status': 'not_live',
2698 'channel_url': 'https://www.youtube.com/channel/UCCeo--lls1vna5YJABWAcVA',
2699 'availability': 'public',
2702 'categories': ['Science & Technology'],
2703 'channel_follower_count': int,
2704 'uploader': 'Christopher Sykes',
2705 'uploader_url': 'https://www.youtube.com/@ChristopherSykesDocumentaries',
2706 'uploader_id': '@ChristopherSykesDocumentaries',
2709 'skip_download': True,
2715 def suitable(cls, url):
2716 from ..utils import parse_qs
2719 if qs.get('list', [None])[0]:
2721 return super().suitable(url)
2723 def __init__(self, *args, **kwargs):
2724 super().__init__(*args, **kwargs)
2725 self._code_cache = {}
2726 self._player_cache = {}
2728 def _prepare_live_from_start_formats(self, formats, video_id, live_start_time, url, webpage_url, smuggled_data, is_live):
2729 lock = threading.Lock()
2730 start_time = time.time()
2731 formats = [f for f in formats if f.get('is_from_start')]
2733 def refetch_manifest(format_id, delay):
2734 nonlocal formats, start_time, is_live
2735 if time.time() <= start_time + delay:
2738 _, _, prs, player_url = self._download_player_responses(url, smuggled_data, video_id, webpage_url)
2739 video_details = traverse_obj(prs, (..., 'videoDetails'), expected_type=dict)
2740 microformats = traverse_obj(
2741 prs, (..., 'microformat', 'playerMicroformatRenderer'),
2743 _, live_status, _, formats, _ = self._list_formats(video_id, microformats, video_details, prs, player_url)
2744 is_live = live_status == 'is_live'
2745 start_time = time.time()
2747 def mpd_feed(format_id, delay):
2749 @returns (manifest_url, manifest_stream_number, is_live) or None
2751 for retry in self.RetryManager(fatal=False):
2753 refetch_manifest(format_id, delay)
2755 f = next((f for f in formats if f['format_id'] == format_id), None)
2758 retry.error = f'{video_id}: Video is no longer live'
2760 retry.error = f'Cannot find refreshed manifest for format {format_id}{bug_reports_message()}'
2762 return f['manifest_url'], f['manifest_stream_number'], is_live
2766 f['is_live'] = is_live
2767 gen = functools.partial(self._live_dash_fragments, video_id, f['format_id'],
2768 live_start_time, mpd_feed, not is_live and f.copy())
2770 f['fragments'] = gen
2771 f['protocol'] = 'http_dash_segments_generator'
2773 f['fragments'] = LazyList(gen({}))
2774 del f['is_from_start']
2776 def _live_dash_fragments(self, video_id, format_id, live_start_time, mpd_feed, manifestless_orig_fmt, ctx):
2777 FETCH_SPAN, MAX_DURATION = 5, 432000
2779 mpd_url, stream_number, is_live = None, None, True
2782 download_start_time = ctx.get('start') or time.time()
2784 lack_early_segments = download_start_time - (live_start_time or download_start_time) > MAX_DURATION
2785 if lack_early_segments:
2786 self.report_warning(bug_reports_message(
2787 'Starting download from the last 120 hours of the live stream since '
2788 'YouTube does not have data before that. If you think this is wrong,'), only_once=True)
2789 lack_early_segments = True
2791 known_idx, no_fragment_score, last_segment_url = begin_index, 0, None
2792 fragments, fragment_base_url = None, None
2794 def _extract_sequence_from_mpd(refresh_sequence, immediate):
2795 nonlocal mpd_url, stream_number, is_live, no_fragment_score, fragments, fragment_base_url
2796 # Obtain from MPD's maximum seq value
2797 old_mpd_url = mpd_url
2798 last_error = ctx.pop('last_error', None)
2799 expire_fast = immediate or last_error and isinstance(last_error, urllib.error.HTTPError) and last_error.code == 403
2800 mpd_url, stream_number, is_live = (mpd_feed(format_id, 5 if expire_fast else 18000)
2801 or (mpd_url, stream_number, False))
2802 if not refresh_sequence:
2803 if expire_fast and not is_live:
2804 return False, last_seq
2805 elif old_mpd_url == mpd_url:
2806 return True, last_seq
2807 if manifestless_orig_fmt:
2808 fmt_info = manifestless_orig_fmt
2811 fmts, _ = self._extract_mpd_formats_and_subtitles(
2812 mpd_url, None, note=False, errnote=False, fatal=False)
2813 except ExtractorError:
2816 no_fragment_score += 2
2817 return False, last_seq
2818 fmt_info = next(x for x in fmts if x['manifest_stream_number'] == stream_number)
2819 fragments = fmt_info['fragments']
2820 fragment_base_url = fmt_info['fragment_base_url']
2821 assert fragment_base_url
2823 _last_seq = int(re.search(r'(?:/|^)sq/(\d+)', fragments[-1]['path']).group(1))
2824 return True, _last_seq
2826 self.write_debug(f'[{video_id}] Generating fragments for format {format_id}')
2828 fetch_time = time.time()
2829 if no_fragment_score > 30:
2831 if last_segment_url:
2832 # Obtain from "X-Head-Seqnum" header value from each segment
2834 urlh = self._request_webpage(
2835 last_segment_url, None, note=False, errnote=False, fatal=False)
2836 except ExtractorError:
2838 last_seq = try_get(urlh, lambda x: int_or_none(x.headers['X-Head-Seqnum']))
2839 if last_seq is None:
2840 no_fragment_score += 2
2841 last_segment_url = None
2844 should_continue, last_seq = _extract_sequence_from_mpd(True, no_fragment_score > 15)
2845 no_fragment_score += 2
2846 if not should_continue:
2849 if known_idx > last_seq:
2850 last_segment_url = None
2855 if begin_index < 0 and known_idx < 0:
2856 # skip from the start when it's negative value
2857 known_idx = last_seq + begin_index
2858 if lack_early_segments:
2859 known_idx = max(known_idx, last_seq - int(MAX_DURATION // fragments[-1]['duration']))
2861 for idx in range(known_idx, last_seq):
2862 # do not update sequence here or you'll get skipped some part of it
2863 should_continue, _ = _extract_sequence_from_mpd(False, False)
2864 if not should_continue:
2866 raise ExtractorError('breaking out of outer loop')
2867 last_segment_url = urljoin(fragment_base_url, 'sq/%d' % idx)
2869 'url': last_segment_url,
2870 'fragment_count': last_seq,
2872 if known_idx == last_seq:
2873 no_fragment_score += 5
2875 no_fragment_score = 0
2876 known_idx = last_seq
2877 except ExtractorError:
2880 if manifestless_orig_fmt:
2881 # Stop at the first iteration if running for post-live manifestless;
2882 # fragment count no longer increase since it starts
2885 time.sleep(max(0, FETCH_SPAN + fetch_time - time.time()))
2887 def _extract_player_url(self, *ytcfgs, webpage=None):
2888 player_url = traverse_obj(
2889 ytcfgs, (..., 'PLAYER_JS_URL'), (..., 'WEB_PLAYER_CONTEXT_CONFIGS', ..., 'jsUrl'),
2890 get_all=False, expected_type=str)
2893 return urljoin('https://www.youtube.com', player_url)
2895 def _download_player_url(self, video_id, fatal=False):
2896 res = self._download_webpage(
2897 'https://www.youtube.com/iframe_api',
2898 note='Downloading iframe API JS', video_id=video_id, fatal=fatal)
2900 player_version = self._search_regex(
2901 r'player\\?/([0-9a-fA-F]{8})\\?/', res, 'player version', fatal=fatal)
2903 return f'https://www.youtube.com/s/player/{player_version}/player_ias.vflset/en_US/base.js'
2905 def _signature_cache_id(self, example_sig):
2906 """ Return a string representation of a signature """
2907 return '.'.join(str(len(part)) for part in example_sig.split('.'))
2910 def _extract_player_info(cls, player_url):
2911 for player_re in cls._PLAYER_INFO_RE:
2912 id_m = re.search(player_re, player_url)
2916 raise ExtractorError('Cannot identify player %r' % player_url)
2917 return id_m.group('id')
2919 def _load_player(self, video_id, player_url, fatal=True):
2920 player_id = self._extract_player_info(player_url)
2921 if player_id not in self._code_cache:
2922 code = self._download_webpage(
2923 player_url, video_id, fatal=fatal,
2924 note='Downloading player ' + player_id,
2925 errnote='Download of %s failed' % player_url)
2927 self._code_cache[player_id] = code
2928 return self._code_cache.get(player_id)
2930 def _extract_signature_function(self, video_id, player_url, example_sig):
2931 player_id = self._extract_player_info(player_url)
2933 # Read from filesystem cache
2934 func_id = f'js_{player_id}_{self._signature_cache_id(example_sig)}'
2935 assert os.path.basename(func_id) == func_id
2937 self.write_debug(f'Extracting signature function {func_id}')
2938 cache_spec, code = self.cache.load('youtube-sigfuncs', func_id), None
2941 code = self._load_player(video_id, player_url)
2943 res = self._parse_sig_js(code)
2944 test_string = ''.join(map(chr, range(len(example_sig))))
2945 cache_spec = [ord(c) for c in res(test_string)]
2946 self.cache.store('youtube-sigfuncs', func_id, cache_spec)
2948 return lambda s: ''.join(s[i] for i in cache_spec)
2950 def _print_sig_code(self, func, example_sig):
2951 if not self.get_param('youtube_print_sig_code'):
2954 def gen_sig_code(idxs):
2955 def _genslice(start, end, step):
2956 starts = '' if start == 0 else str(start)
2957 ends = (':%d' % (end + step)) if end + step >= 0 else ':'
2958 steps = '' if step == 1 else (':%d' % step)
2959 return f's[{starts}{ends}{steps}]'
2962 # Quelch pyflakes warnings - start will be set when step is set
2963 start = '(Never used)'
2964 for i, prev in zip(idxs[1:], idxs[:-1]):
2965 if step is not None:
2966 if i - prev == step:
2968 yield _genslice(start, prev, step)
2971 if i - prev in [-1, 1]:
2976 yield 's[%d]' % prev
2980 yield _genslice(start, i, step)
2982 test_string = ''.join(map(chr, range(len(example_sig))))
2983 cache_res = func(test_string)
2984 cache_spec = [ord(c) for c in cache_res]
2985 expr_code = ' + '.join(gen_sig_code(cache_spec))
2986 signature_id_tuple = '(%s)' % (
2987 ', '.join(str(len(p)) for p in example_sig.split('.')))
2988 code = ('if tuple(len(p) for p in s.split(\'.\')) == %s:\n'
2989 ' return %s\n') % (signature_id_tuple, expr_code)
2990 self.to_screen('Extracted signature function:\n' + code)
2992 def _parse_sig_js(self, jscode):
2993 funcname = self._search_regex(
2994 (r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2995 r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*encodeURIComponent\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\(',
2996 r'\bm=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(h\.s\)\)',
2997 r'\bc&&\(c=(?P<sig>[a-zA-Z0-9$]{2,})\(decodeURIComponent\(c\)\)',
2998 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+\))?',
2999 r'(?P<sig>[a-zA-Z0-9$]+)\s*=\s*function\(\s*a\s*\)\s*{\s*a\s*=\s*a\.split\(\s*""\s*\)',
3001 r'("|\')signature\1\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
3002 r'\.sig\|\|(?P<sig>[a-zA-Z0-9$]+)\(',
3003 r'yt\.akamaized\.net/\)\s*\|\|\s*.*?\s*[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?:encodeURIComponent\s*\()?\s*(?P<sig>[a-zA-Z0-9$]+)\(',
3004 r'\b[cs]\s*&&\s*[adf]\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
3005 r'\b[a-zA-Z0-9]+\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*(?P<sig>[a-zA-Z0-9$]+)\(',
3006 r'\bc\s*&&\s*[a-zA-Z0-9]+\.set\([^,]+\s*,\s*\([^)]*\)\s*\(\s*(?P<sig>[a-zA-Z0-9$]+)\('),
3007 jscode, 'Initial JS player signature function name', group='sig')
3009 jsi = JSInterpreter(jscode)
3010 initial_function = jsi.extract_function(funcname)
3011 return lambda s: initial_function([s])
3013 def _cached(self, func, *cache_id):
3014 def inner(*args, **kwargs):
3015 if cache_id not in self._player_cache:
3017 self._player_cache[cache_id] = func(*args, **kwargs)
3018 except ExtractorError as e:
3019 self._player_cache[cache_id] = e
3020 except Exception as e:
3021 self._player_cache[cache_id] = ExtractorError(traceback.format_exc(), cause=e)
3023 ret = self._player_cache[cache_id]
3024 if isinstance(ret, Exception):
3029 def _decrypt_signature(self, s, video_id, player_url):
3030 """Turn the encrypted s field into a working signature"""
3031 extract_sig = self._cached(
3032 self._extract_signature_function, 'sig', player_url, self._signature_cache_id(s))
3033 func = extract_sig(video_id, player_url, s)
3034 self._print_sig_code(func, s)
3037 def _decrypt_nsig(self, s, video_id, player_url):
3038 """Turn the encrypted n field into a working signature"""
3039 if player_url is None:
3040 raise ExtractorError('Cannot decrypt nsig without player_url')
3041 player_url = urljoin('https://www.youtube.com', player_url)
3044 jsi, player_id, func_code = self._extract_n_function_code(video_id, player_url)
3045 except ExtractorError as e:
3046 raise ExtractorError('Unable to extract nsig function code', cause=e)
3047 if self.get_param('youtube_print_sig_code'):
3048 self.to_screen(f'Extracted nsig function from {player_id}:\n{func_code[1]}\n')
3051 extract_nsig = self._cached(self._extract_n_function_from_code, 'nsig func', player_url)
3052 ret = extract_nsig(jsi, func_code)(s)
3053 except JSInterpreter.Exception as e:
3055 jsi = PhantomJSwrapper(self, timeout=5000)
3056 except ExtractorError:
3058 self.report_warning(
3059 f'Native nsig extraction failed: Trying with PhantomJS\n'
3060 f' n = {s} ; player = {player_url}', video_id)
3061 self.write_debug(e, only_once=True)
3063 args, func_body = func_code
3065 f'console.log(function({", ".join(args)}) {{ {func_body} }}({s!r}));',
3066 video_id=video_id, note='Executing signature code').strip()
3068 self.write_debug(f'Decrypted nsig {s} => {ret}')
3071 def _extract_n_function_name(self, jscode):
3072 funcname, idx = self._search_regex(
3073 r'\.get\("n"\)\)&&\(b=(?P<nfunc>[a-zA-Z0-9$]+)(?:\[(?P<idx>\d+)\])?\([a-zA-Z0-9]\)',
3074 jscode, 'Initial JS player n function name', group=('nfunc', 'idx'))
3078 return json.loads(js_to_json(self._search_regex(
3079 rf'var {re.escape(funcname)}\s*=\s*(\[.+?\]);', jscode,
3080 f'Initial JS player n function list ({funcname}.{idx})')))[int(idx)]
3082 def _extract_n_function_code(self, video_id, player_url):
3083 player_id = self._extract_player_info(player_url)
3084 func_code = self.cache.load('youtube-nsig', player_id, min_ver='2022.09.1')
3085 jscode = func_code or self._load_player(video_id, player_url)
3086 jsi = JSInterpreter(jscode)
3089 return jsi, player_id, func_code
3091 func_name = self._extract_n_function_name(jscode)
3094 func_code = self._search_regex(
3095 r'''(?xs
)%s\s
*=\s
*function\s
*\
((?P
<var
>[\w$
]+)\
)\s
*
3096 # NB: The end of the regex is intentionally kept strict
3097 {(?P<code>.+?}\s
*return\
[\w$
]+.join\
(""\
))};''' % func_name,
3098 jscode, 'nsig function', group=('var', 'code'), default=None)
3100 func_code = ([func_code[0]], func_code[1])
3102 self.write_debug('Extracting nsig function with jsinterp')
3103 func_code = jsi.extract_function_code(func_name)
3105 self.cache.store('youtube-nsig', player_id, func_code)
3106 return jsi, player_id, func_code
3108 def _extract_n_function_from_code(self, jsi, func_code):
3109 func = jsi.extract_function_from_code(*func_code)
3111 def extract_nsig(s):
3114 except JSInterpreter.Exception:
3116 except Exception as e:
3117 raise JSInterpreter.Exception(traceback.format_exc(), cause=e)
3119 if ret.startswith('enhanced_except_'):
3120 raise JSInterpreter.Exception('Signature function returned an exception')
3125 def _extract_signature_timestamp(self, video_id, player_url, ytcfg=None, fatal=False):
3127 Extract signatureTimestamp (sts)
3128 Required to tell API what sig/player version is in use.
3131 if isinstance(ytcfg, dict):
3132 sts = int_or_none(ytcfg.get('STS'))
3135 # Attempt to extract from player
3136 if player_url is None:
3137 error_msg = 'Cannot extract signature timestamp without player_url.'
3139 raise ExtractorError(error_msg)
3140 self.report_warning(error_msg)
3142 code = self._load_player(video_id, player_url, fatal=fatal)
3144 sts = int_or_none(self._search_regex(
3145 r'(?:signatureTimestamp|sts)\s*:\s*(?P<sts>[0-9]{5})', code,
3146 'JS player signature timestamp', group='sts', fatal=fatal))
3149 def _mark_watched(self, video_id, player_responses):
3150 for is_full, key in enumerate(('videostatsPlaybackUrl', 'videostatsWatchtimeUrl')):
3151 label = 'fully ' if is_full else ''
3152 url = get_first(player_responses, ('playbackTracking', key, 'baseUrl'),
3153 expected_type=url_or_none)
3155 self.report_warning(f'Unable to mark {label}watched')
3157 parsed_url = urllib.parse.urlparse(url)
3158 qs = urllib.parse.parse_qs(parsed_url.query)
3160 # cpn generation algorithm is reverse engineered from base.js.
3161 # In fact it works even with dummy cpn.
3162 CPN_ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_'
3163 cpn = ''.join(CPN_ALPHABET[random.randint(0, 256) & 63] for _ in range(0, 16))
3165 # # more consistent results setting it to right before the end
3166 video_length = [str(float((qs.get('len') or ['1.5'])[0]) - 1)]
3171 'cmt': video_length,
3172 'el': 'detailpage', # otherwise defaults to "shorts"
3176 # these seem to mark watchtime "history" in the real world
3177 # they're required, so send in a single value
3183 url = urllib.parse.urlunparse(
3184 parsed_url._replace(query=urllib.parse.urlencode(qs, True)))
3186 self._download_webpage(
3187 url, video_id, f'Marking {label}watched',
3188 'Unable to mark watched', fatal=False)
3191 def _extract_from_webpage(cls, url, webpage):
3192 # Invidious Instances
3193 # https://github.com/yt-dlp/yt-dlp/issues/195
3194 # https://github.com/iv-org/invidious/pull/1730
3196 r'<link rel="alternate" href="(?P<url>https://www\.youtube\.com/watch\?v=[0-9A-Za-z_-]{11})"',
3199 yield cls.url_result(mobj.group('url'), cls)
3200 raise cls.StopExtraction()
3202 yield from super()._extract_from_webpage(url, webpage)
3204 # lazyYT YouTube embed
3205 for id_ in re.findall(r'class="lazyYT" data-youtube-id="([^"]+)"', webpage):
3206 yield cls.url_result(unescapeHTML(id_), cls, id_)
3208 # Wordpress "YouTube Video Importer" plugin
3209 for m in re.findall(r'''(?x
)<div
[^
>]+
3210 class=(?P
<q1
>[\'"])[^\'"]*\byvii
_single
_video
_player
\b[^
\'"]*(?P=q1)[^>]+
3211 data-video_id=(?P<q2>[\'"])([^
\'"]+)(?P=q2)''', webpage):
3212 yield cls.url_result(m[-1], cls, m[-1])
3215 def extract_id(cls, url):
3216 video_id = cls.get_temp_id(url)
3218 raise ExtractorError(f'Invalid URL: {url}')
3221 def _extract_chapters_from_json(self, data, duration):
3222 chapter_list = traverse_obj(
3224 'playerOverlays', 'playerOverlayRenderer', 'decoratedPlayerBarRenderer',
3225 'decoratedPlayerBarRenderer', 'playerBar', 'chapteredPlayerBarRenderer', 'chapters'
3226 ), expected_type=list)
3228 return self._extract_chapters_helper(
3230 start_function=lambda chapter: float_or_none(
3231 traverse_obj(chapter, ('chapterRenderer', 'timeRangeStartMillis')), scale=1000),
3232 title_function=lambda chapter: traverse_obj(
3233 chapter, ('chapterRenderer', 'title', 'simpleText'), expected_type=str),
3236 def _extract_chapters_from_engagement_panel(self, data, duration):
3237 content_list = traverse_obj(
3239 ('engagementPanels', ..., 'engagementPanelSectionListRenderer', 'content', 'macroMarkersListRenderer', 'contents'),
3241 chapter_time = lambda chapter: parse_duration(self._get_text(chapter, 'timeDescription'))
3242 chapter_title = lambda chapter: self._get_text(chapter, 'title')
3244 return next(filter(None, (
3245 self._extract_chapters_helper(traverse_obj(contents, (..., 'macroMarkersListItemRenderer')),
3246 chapter_time, chapter_title, duration)
3247 for contents in content_list)), [])
3249 def _extract_heatmap_from_player_overlay(self, data):
3250 content_list = traverse_obj(data, (
3251 'playerOverlays', 'playerOverlayRenderer', 'decoratedPlayerBarRenderer', 'decoratedPlayerBarRenderer', 'playerBar',
3252 'multiMarkersPlayerBarRenderer', 'markersMap', ..., 'value', 'heatmap', 'heatmapRenderer', 'heatMarkers', {list}))
3253 return next(filter(None, (
3254 traverse_obj(contents, (..., 'heatMarkerRenderer', {
3255 'start_time': ('timeRangeStartMillis', {functools.partial(float_or_none, scale=1000)}),
3256 'end_time': {lambda x: (x['timeRangeStartMillis'] + x['markerDurationMillis']) / 1000},
3257 'value': ('heatMarkerIntensityScoreNormalized', {float_or_none}),
3258 })) for contents in content_list)), None)
3260 def _extract_comment(self, comment_renderer, parent=None):
3261 comment_id = comment_renderer.get('commentId')
3265 text = self._get_text(comment_renderer, 'contentText')
3267 # Timestamp is an estimate calculated from the current time and time_text
3268 time_text = self._get_text(comment_renderer, 'publishedTimeText') or ''
3269 timestamp = self._parse_time_text(time_text)
3271 author = self._get_text(comment_renderer, 'authorText')
3272 author_id = try_get(comment_renderer,
3273 lambda x: x['authorEndpoint']['browseEndpoint']['browseId'], str)
3275 votes = parse_count(try_get(comment_renderer, (lambda x: x['voteCount']['simpleText'],
3276 lambda x: x['likeCount']), str)) or 0
3277 author_thumbnail = try_get(comment_renderer,
3278 lambda x: x['authorThumbnail']['thumbnails'][-1]['url'], str)
3280 author_is_uploader = try_get(comment_renderer, lambda x: x['authorIsChannelOwner'], bool)
3281 is_favorited = 'creatorHeart' in (try_get(
3282 comment_renderer, lambda x: x['actionButtons']['commentActionButtonsRenderer'], dict) or {})
3286 'timestamp': timestamp,
3287 'time_text': time_text,
3288 'like_count': votes,
3289 'is_favorited': is_favorited,
3291 'author_id': author_id,
3292 'author_thumbnail': author_thumbnail,
3293 'author_is_uploader': author_is_uploader,
3294 'parent': parent or 'root'
3297 def _comment_entries(self, root_continuation_data, ytcfg, video_id, parent=None, tracker=None):
3299 get_single_config_arg = lambda c: self._configuration_arg(c, [''])[0]
3301 def extract_header(contents):
3302 _continuation = None
3303 for content in contents:
3304 comments_header_renderer = traverse_obj(content, 'commentsHeaderRenderer')
3305 expected_comment_count = self._get_count(
3306 comments_header_renderer, 'countText', 'commentsCount')
3308 if expected_comment_count:
3309 tracker['est_total'] = expected_comment_count
3310 self.to_screen(f'Downloading ~{expected_comment_count} comments')
3311 comment_sort_index = int(get_single_config_arg('comment_sort') != 'top') # 1 = new, 0 = top
3313 sort_menu_item = try_get(
3314 comments_header_renderer,
3315 lambda x: x['sortMenu']['sortFilterSubMenuRenderer']['subMenuItems'][comment_sort_index], dict) or {}
3316 sort_continuation_ep = sort_menu_item.get('serviceEndpoint') or {}
3318 _continuation = self._extract_continuation_ep_data(sort_continuation_ep) or self._extract_continuation(sort_menu_item)
3319 if not _continuation:
3322 sort_text = str_or_none(sort_menu_item.get('title'))
3324 sort_text = 'top comments' if comment_sort_index == 0 else 'newest first'
3325 self.to_screen('Sorting comments by %s' % sort_text.lower())
3327 return _continuation
3329 def extract_thread(contents):
3331 tracker['current_page_thread'] = 0
3332 for content in contents:
3333 if not parent and tracker['total_parent_comments'] >= max_parents:
3335 comment_thread_renderer = try_get(content, lambda x: x['commentThreadRenderer'])
3336 comment_renderer = get_first(
3337 (comment_thread_renderer, content), [['commentRenderer', ('comment', 'commentRenderer')]],
3338 expected_type=dict, default={})
3340 comment = self._extract_comment(comment_renderer, parent)
3343 is_pinned = bool(traverse_obj(comment_renderer, 'pinnedCommentBadge'))
3344 comment_id = comment['id']
3346 tracker['pinned_comment_ids'].add(comment_id)
3347 # Sometimes YouTube may break and give us infinite looping comments.
3348 # See: https://github.com/yt-dlp/yt-dlp/issues/6290
3349 if comment_id in tracker['seen_comment_ids']:
3350 if comment_id in tracker['pinned_comment_ids'] and not is_pinned:
3351 # Pinned comments may appear a second time in newest first sort
3352 # See: https://github.com/yt-dlp/yt-dlp/issues/6712
3354 self.report_warning('Detected YouTube comments looping. Stopping comment extraction as we probably cannot get any more.')
3357 tracker['seen_comment_ids'].add(comment['id'])
3359 tracker['running_total'] += 1
3360 tracker['total_reply_comments' if parent else 'total_parent_comments'] += 1
3363 # Attempt to get the replies
3364 comment_replies_renderer = try_get(
3365 comment_thread_renderer, lambda x: x['replies']['commentRepliesRenderer'], dict)
3367 if comment_replies_renderer:
3368 tracker['current_page_thread'] += 1
3369 comment_entries_iter = self._comment_entries(
3370 comment_replies_renderer, ytcfg, video_id,
3371 parent=comment.get('id'), tracker=tracker)
3372 yield from itertools.islice(comment_entries_iter, min(
3373 max_replies_per_thread, max(0, max_replies - tracker['total_reply_comments'])))
3375 # Keeps track of counts across recursive calls
3380 current_page_thread=0,
3381 total_parent_comments=0,
3382 total_reply_comments=0,
3383 seen_comment_ids=set(),
3384 pinned_comment_ids=set()
3388 # YouTube comments have a max depth of 2
3389 max_depth = int_or_none(get_single_config_arg('max_comment_depth'))
3391 self._downloader.deprecated_feature('[youtube] max_comment_depth extractor argument is deprecated. '
3392 'Set max replies in the max-comments extractor argument instead')
3393 if max_depth == 1 and parent:
3396 max_comments, max_parents, max_replies, max_replies_per_thread, *_ = map(
3397 lambda p: int_or_none(p, default=sys.maxsize), self._configuration_arg('max_comments', ) + [''] * 4)
3399 continuation = self._extract_continuation(root_continuation_data)
3402 is_forced_continuation = False
3403 is_first_continuation = parent is None
3404 if is_first_continuation and not continuation:
3405 # Sometimes you can get comments by generating the continuation yourself,
3406 # even if YouTube initially reports them being disabled - e.g. stories comments.
3407 # Note: if the comment section is actually disabled, YouTube may return a response with
3408 # required check_get_keys missing. So we will disable that check initially in this case.
3409 continuation = self._build_api_continuation_query(self._generate_comment_continuation(video_id))
3410 is_forced_continuation = True
3412 for page_num in itertools.count(0):
3413 if not continuation:
3415 headers = self.generate_api_headers(ytcfg=ytcfg, visitor_data=self._extract_visitor_data(response))
3416 comment_prog_str = f"({tracker['running_total']}
/{tracker['est_total']}
)"
3418 if is_first_continuation:
3419 note_prefix = 'Downloading comment section API JSON'
3421 note_prefix = ' Downloading comment API JSON reply thread %d %s' % (
3422 tracker['current_page_thread'], comment_prog_str)
3424 note_prefix = '%sDownloading comment%s API JSON page %d %s' % (
3425 ' ' if parent else '', ' replies' if parent else '',
3426 page_num, comment_prog_str)
3428 response = self._extract_response(
3429 item_id=None, query=continuation,
3430 ep='next', ytcfg=ytcfg, headers=headers, note=note_prefix,
3431 check_get_keys='onResponseReceivedEndpoints' if not is_forced_continuation else None)
3432 except ExtractorError as e:
3433 # Ignore incomplete data error for replies if retries didn't work.
3434 # This is to allow any other parent comments and comment threads to be downloaded.
3435 # See: https://github.com/yt-dlp/yt-dlp/issues/4669
3436 if 'incomplete data' in str(e).lower() and parent and self.get_param('ignoreerrors') is True:
3437 self.report_warning(
3438 'Received incomplete data for a comment reply thread and retrying did not help. '
3439 'Ignoring to let other comments be downloaded.')
3442 is_forced_continuation = False
3443 continuation_contents = traverse_obj(
3444 response, 'onResponseReceivedEndpoints', expected_type=list, default=[])
3447 for continuation_section in continuation_contents:
3448 continuation_items = traverse_obj(
3449 continuation_section,
3450 (('reloadContinuationItemsCommand', 'appendContinuationItemsAction'), 'continuationItems'),
3451 get_all=False, expected_type=list) or []
3452 if is_first_continuation:
3453 continuation = extract_header(continuation_items)
3454 is_first_continuation = False
3459 for entry in extract_thread(continuation_items):
3463 continuation = self._extract_continuation({'contents': continuation_items})
3467 message = self._get_text(root_continuation_data, ('contents', ..., 'messageRenderer', 'text'), max_runs=1)
3468 if message and not parent and tracker['running_total'] == 0:
3469 self.report_warning(f'Youtube said: {message}', video_id=video_id, only_once=True)
3470 raise self.CommentsDisabled
3473 def _generate_comment_continuation(video_id):
3475 Generates initial comment section continuation token from given video id
3477 token = f'\x12\r\x12\x0b{video_id}\x18\x062\'"\x11"\x0b{video_id}0\x00x\x020\x00B\x10comments-section'
3478 return base64.b64encode(token.encode()).decode()
3480 def _get_comments(self, ytcfg, video_id, contents, webpage):
3481 """Entry for comment extraction"""
3482 def _real_comment_extract(contents):
3484 item for item in traverse_obj(contents, (..., 'itemSectionRenderer'), default={})
3485 if item.get('sectionIdentifier') == 'comment-item-section'), None)
3486 yield from self._comment_entries(renderer, ytcfg, video_id)
3488 max_comments = int_or_none(self._configuration_arg('max_comments', [''])[0])
3489 return itertools.islice(_real_comment_extract(contents), 0, max_comments)
3492 def _get_checkok_params():
3493 return {'contentCheckOk': True, 'racyCheckOk': True}
3496 def _generate_player_context(cls, sts=None):
3498 'html5Preference': 'HTML5_PREF_WANTS',
3501 context['signatureTimestamp'] = sts
3503 'playbackContext': {
3504 'contentPlaybackContext': context
3506 **cls._get_checkok_params()
3510 def _is_agegated(player_response):
3511 if traverse_obj(player_response, ('playabilityStatus', 'desktopLegacyAgeGateReason')):
3514 reasons = traverse_obj(player_response, ('playabilityStatus', ('status', 'reason')))
3515 AGE_GATE_REASONS = (
3516 'confirm your age', 'age-restricted', 'inappropriate', # reason
3517 'age_verification_required', 'age_check_required', # status
3519 return any(expected in reason for expected in AGE_GATE_REASONS for reason in reasons)
3522 def _is_unplayable(player_response):
3523 return traverse_obj(player_response, ('playabilityStatus', 'status')) == 'UNPLAYABLE'
3525 _STORY_PLAYER_PARAMS = '8AEB'
3527 def _extract_player_response(self, client, video_id, master_ytcfg, player_ytcfg, player_url, initial_pr, smuggled_data):
3529 session_index = self._extract_session_index(player_ytcfg, master_ytcfg)
3530 syncid = self._extract_account_syncid(player_ytcfg, master_ytcfg, initial_pr)
3531 sts = self._extract_signature_timestamp(video_id, player_url, master_ytcfg, fatal=False) if player_url else None
3532 headers = self.generate_api_headers(
3533 ytcfg=player_ytcfg, account_syncid=syncid, session_index=session_index, default_client=client)
3536 'videoId': video_id,
3538 if smuggled_data.get('is_story') or _split_innertube_client(client)[0] == 'android':
3539 yt_query['params'] = self._STORY_PLAYER_PARAMS
3541 yt_query.update(self._generate_player_context(sts))
3542 return self._extract_response(
3543 item_id=video_id, ep='player', query=yt_query,
3544 ytcfg=player_ytcfg, headers=headers, fatal=True,
3545 default_client=client,
3546 note='Downloading %s player API JSON' % client.replace('_', ' ').strip()
3549 def _get_requested_clients(self, url, smuggled_data):
3550 requested_clients = []
3551 default = ['android', 'web']
3552 allowed_clients = sorted(
3553 (client for client in INNERTUBE_CLIENTS.keys() if client[:1] != '_'),
3554 key=lambda client: INNERTUBE_CLIENTS[client]['priority'], reverse=True)
3555 for client in self._configuration_arg('player_client'):
3556 if client in allowed_clients:
3557 requested_clients.append(client)
3558 elif client == 'default':
3559 requested_clients.extend(default)
3560 elif client == 'all':
3561 requested_clients.extend(allowed_clients)
3563 self.report_warning(f'Skipping unsupported client {client}')
3564 if not requested_clients:
3565 requested_clients = default
3567 if smuggled_data.get('is_music_url') or self.is_music_url(url):
3568 requested_clients.extend(
3569 f'{client}_music' for client in requested_clients if f'{client}_music' in INNERTUBE_CLIENTS)
3571 return orderedSet(requested_clients)
3573 def _extract_player_responses(self, clients, video_id, webpage, master_ytcfg, smuggled_data):
3576 initial_pr = self._search_json(
3577 self._YT_INITIAL_PLAYER_RESPONSE_RE, webpage, 'initial player response', video_id, fatal=False)
3579 all_clients = set(clients)
3580 clients = clients[::-1]
3583 def append_client(*client_names):
3584 """ Append the first client name that exists but not already used """
3585 for client_name in client_names:
3586 actual_client = _split_innertube_client(client_name)[0]
3587 if actual_client in INNERTUBE_CLIENTS:
3588 if actual_client not in all_clients:
3589 clients.append(client_name)
3590 all_clients.add(actual_client)
3593 # Android player_response does not have microFormats which are needed for
3594 # extraction of some data. So we return the initial_pr with formats
3595 # stripped out even if not requested by the user
3596 # See: https://github.com/yt-dlp/yt-dlp/issues/501
3598 pr = dict(initial_pr)
3599 pr['streamingData'] = None
3603 tried_iframe_fallback = False
3606 client, base_client, variant = _split_innertube_client(clients.pop())
3607 player_ytcfg = master_ytcfg if client == 'web' else {}
3608 if 'configs' not in self._configuration_arg('player_skip') and client != 'web':
3609 player_ytcfg = self._download_ytcfg(client, video_id) or player_ytcfg
3611 player_url = player_url or self._extract_player_url(master_ytcfg, player_ytcfg, webpage=webpage)
3612 require_js_player = self._get_default_ytcfg(client).get('REQUIRE_JS_PLAYER')
3613 if 'js' in self._configuration_arg('player_skip'):
3614 require_js_player = False
3617 if not player_url and not tried_iframe_fallback and require_js_player:
3618 player_url = self._download_player_url(video_id)
3619 tried_iframe_fallback = True
3622 pr = initial_pr if client == 'web' and initial_pr else self._extract_player_response(
3623 client, video_id, player_ytcfg or master_ytcfg, player_ytcfg, player_url if require_js_player else None, initial_pr, smuggled_data)
3624 except ExtractorError as e:
3626 self.report_warning(last_error)
3631 # YouTube may return a different video player response than expected.
3632 # See: https://github.com/TeamNewPipe/NewPipe/issues/8713
3633 pr_video_id = traverse_obj(pr, ('videoDetails', 'videoId'))
3634 if pr_video_id and pr_video_id != video_id:
3635 self.report_warning(
3636 f'Skipping player response from {client} client (got player response for video "{pr_video_id}
" instead of "{video_id}
")' + bug_reports_message())
3638 # Save client name for introspection later
3639 name = short_client_name(client)
3640 sd = traverse_obj(pr, ('streamingData', {dict})) or {}
3641 sd[STREAMING_DATA_CLIENT_NAME] = name
3642 for f in traverse_obj(sd, (('formats', 'adaptiveFormats'), ..., {dict})):
3643 f[STREAMING_DATA_CLIENT_NAME] = name
3646 # creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
3647 if variant == 'embedded' and self._is_unplayable(pr) and self.is_authenticated:
3648 append_client(f'{base_client}_creator')
3649 elif self._is_agegated(pr):
3650 if variant == 'tv_embedded':
3651 append_client(f'{base_client}_embedded')
3653 append_client(f'tv_embedded.{base_client}', f'{base_client}_embedded')
3658 self.report_warning(last_error)
3659 return prs, player_url
3661 def _needs_live_processing(self, live_status, duration):
3662 if (live_status == 'is_live' and self.get_param('live_from_start')
3663 or live_status == 'post_live' and (duration or 0) > 4 * 3600):
3666 def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
3667 CHUNK_SIZE = 10 << 20
3668 itags, stream_ids = collections.defaultdict(set), []
3669 itag_qualities, res_qualities = {}, {0: None}
3671 # Normally tiny is the smallest video-only formats. But
3672 # audio-only formats with unknown quality may get tagged as tiny
3674 'audio_quality_ultralow', 'audio_quality_low', 'audio_quality_medium', 'audio_quality_high', # Audio only formats
3675 'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
3677 streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...))
3678 all_formats = self._configuration_arg('include_duplicate_formats')
3680 def build_fragments(f):
3682 'url': update_url_query(f['url'], {
3683 'range': f'{range_start}-{min(range_start + CHUNK_SIZE - 1, f["filesize"])}'
3685 } for range_start in range(0, f['filesize'], CHUNK_SIZE))
3687 for fmt in streaming_formats:
3688 if fmt.get('targetDurationSec'):
3691 itag = str_or_none(fmt.get('itag'))
3692 audio_track = fmt.get('audioTrack') or {}
3693 stream_id = (itag, audio_track.get('id'), fmt.get('isDrc'))
3695 if stream_id in stream_ids:
3698 quality = fmt.get('quality')
3699 height = int_or_none(fmt.get('height'))
3700 if quality == 'tiny' or not quality:
3701 quality = fmt.get('audioQuality', '').lower() or quality
3702 # The 3gp format (17) in android client has a quality of "small
",
3703 # but is actually worse than other formats
3708 itag_qualities[itag] = quality
3710 res_qualities[height] = quality
3711 # FORMAT_STREAM_TYPE_OTF(otf=1) requires downloading the init fragment
3712 # (adding `&sq=0` to the URL) and parsing emsg box to determine the
3713 # number of fragment that would subsequently requested with (`&sq=N`)
3714 if fmt.get('type') == 'FORMAT_STREAM_TYPE_OTF':
3717 fmt_url = fmt.get('url')
3719 sc = urllib.parse.parse_qs(fmt.get('signatureCipher'))
3720 fmt_url = url_or_none(try_get(sc, lambda x: x['url'][0]))
3721 encrypted_sig = try_get(sc, lambda x: x['s'][0])
3722 if not all((sc, fmt_url, player_url, encrypted_sig)):
3725 fmt_url += '&%s=%s' % (
3726 traverse_obj(sc, ('sp', -1)) or 'signature',
3727 self._decrypt_signature(encrypted_sig, video_id, player_url)
3729 except ExtractorError as e:
3730 self.report_warning('Signature extraction failed: Some formats may be missing',
3731 video_id=video_id, only_once=True)
3732 self.write_debug(e, only_once=True)
3735 query = parse_qs(fmt_url)
3739 decrypt_nsig = self._cached(self._decrypt_nsig, 'nsig', query['n'][0])
3740 fmt_url = update_url_query(fmt_url, {
3741 'n': decrypt_nsig(query['n'][0], video_id, player_url)
3743 except ExtractorError as e:
3745 if isinstance(e, JSInterpreter.Exception):
3746 phantomjs_hint = (f' Install {self._downloader._format_err("PhantomJS", self._downloader.Styles.EMPHASIS)} '
3747 f'to workaround the issue. {PhantomJSwrapper.INSTALL_HINT}\n')
3749 self.report_warning(
3750 f'nsig extraction failed: You may experience throttling for some formats\n{phantomjs_hint}'
3751 f' n = {query["n"][0]} ; player = {player_url}', video_id=video_id, only_once=True)
3752 self.write_debug(e, only_once=True)
3754 self.report_warning(
3755 'Cannot decrypt nsig without player_url: You may experience throttling for some formats',
3756 video_id=video_id, only_once=True)
3759 tbr = float_or_none(fmt.get('averageBitrate') or fmt.get('bitrate'), 1000)
3760 language_preference = (
3761 10 if audio_track.get('audioIsDefault') and 10
3762 else -10 if 'descriptive' in (audio_track.get('displayName') or '').lower() and -10
3764 # Some formats may have much smaller duration than others (possibly damaged during encoding)
3765 # E.g. 2-nOtRESiUc Ref: https://github.com/yt-dlp/yt-dlp/issues/2823
3766 # Make sure to avoid false positives with small duration differences.
3767 # E.g. __2ABJjxzNo, ySuUZEjARPY
3768 is_damaged = try_get(fmt, lambda x: float(x['approxDurationMs']) / duration < 500)
3770 self.report_warning(
3771 f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
3773 client_name = fmt.get(STREAMING_DATA_CLIENT_NAME)
3775 'asr': int_or_none(fmt.get('audioSampleRate')),
3776 'filesize': int_or_none(fmt.get('contentLength')),
3777 'format_id': f'{itag}{"-drc" if fmt.get("isDrc") else ""}',
3778 'format_note': join_nonempty(
3779 join_nonempty(audio_track.get('displayName'),
3780 language_preference > 0 and ' (default)', delim=''),
3781 fmt.get('qualityLabel') or quality.replace('audio_quality_', ''),
3782 fmt.get('isDrc') and 'DRC',
3783 try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
3784 try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
3785 throttled and 'THROTTLED', is_damaged and 'DAMAGED',
3786 (self.get_param('verbose') or all_formats) and client_name,
3788 # Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
3789 'source_preference': -10 if throttled else -5 if itag == '22' else -1,
3790 'fps': int_or_none(fmt.get('fps')) or None,
3791 'audio_channels': fmt.get('audioChannels'),
3793 'quality': q(quality) - bool(fmt.get('isDrc')) / 2,
3794 'has_drm': bool(fmt.get('drmFamilies')),
3797 'width': int_or_none(fmt.get('width')),
3798 'language': join_nonempty(audio_track.get('id', '').split('.')[0],
3799 'desc' if language_preference < -1 else '') or None,
3800 'language_preference': language_preference,
3801 # Strictly de-prioritize damaged and 3gp formats
3802 'preference': -10 if is_damaged else -2 if itag == '17' else None,
3804 mime_mobj = re.match(
3805 r'((?:[^/]+)/(?:[^;]+))(?:;\s*codecs="([^
"]+)")?
', fmt.get('mimeType
') or '')
3807 dct['ext
'] = mimetype2ext(mime_mobj.group(1))
3808 dct.update(parse_codecs(mime_mobj.group(2)))
3810 itags[itag].add(('https
', dct.get('language
')))
3811 stream_ids.append(stream_id)
3812 single_stream = 'none
' in (dct.get('acodec
'), dct.get('vcodec
'))
3813 if single_stream and dct.get('ext
'):
3814 dct['container
'] = dct['ext
'] + '_dash
'
3816 if all_formats and dct['filesize
']:
3819 'format_id
': f'{dct["format_id"]}
-dashy
' if all_formats else dct['format_id
'],
3820 'protocol
': 'http_dash_segments
',
3821 'fragments
': build_fragments(dct),
3823 dct['downloader_options
'] = {'http_chunk_size': CHUNK_SIZE}
3826 needs_live_processing = self._needs_live_processing(live_status, duration)
3827 skip_bad_formats = not self._configuration_arg('include_incomplete_formats
')
3829 skip_manifests = set(self._configuration_arg('skip
'))
3830 if (not self.get_param('youtube_include_hls_manifest
', True)
3831 or needs_live_processing == 'is_live
' # These will be filtered out by YoutubeDL anyway
3832 or needs_live_processing and skip_bad_formats):
3833 skip_manifests.add('hls
')
3835 if not self.get_param('youtube_include_dash_manifest
', True):
3836 skip_manifests.add('dash
')
3837 if self._configuration_arg('include_live_dash
'):
3838 self._downloader.deprecated_feature('[youtube
] include_live_dash extractor argument
is deprecated
. '
3839 'Use include_incomplete_formats extractor argument instead
')
3840 elif skip_bad_formats and live_status == 'is_live
' and needs_live_processing != 'is_live
':
3841 skip_manifests.add('dash
')
3843 def process_manifest_format(f, proto, client_name, itag):
3844 key = (proto, f.get('language
'))
3845 if not all_formats and key in itags[itag]:
3847 itags[itag].add(key)
3849 if itag and all_formats:
3850 f['format_id
'] = f'{itag}
-{proto}
'
3851 elif any(p != proto for p, _ in itags[itag]):
3852 f['format_id
'] = f'{itag}
-{proto}
'
3854 f['format_id
'] = itag
3856 f['quality
'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id
'].split('-')[0]), -1))
3857 if f['quality
'] == -1 and f.get('height
'):
3858 f['quality
'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height
']))])
3859 if self.get_param('verbose
'):
3860 f['format_note
'] = join_nonempty(f.get('format_note
'), client_name, delim=', ')
3864 for sd in streaming_data:
3865 client_name = sd.get(STREAMING_DATA_CLIENT_NAME)
3867 hls_manifest_url = 'hls
' not in skip_manifests and sd.get('hlsManifestUrl
')
3868 if hls_manifest_url:
3869 fmts, subs = self._extract_m3u8_formats_and_subtitles(
3870 hls_manifest_url, video_id, 'mp4
', fatal=False, live=live_status == 'is_live
')
3871 subtitles = self._merge_subtitles(subs, subtitles)
3873 if process_manifest_format(f, 'hls
', client_name, self._search_regex(
3874 r'/itag
/(\d
+)', f['url
'], 'itag
', default=None)):
3877 dash_manifest_url = 'dash
' not in skip_manifests and sd.get('dashManifestUrl
')
3878 if dash_manifest_url:
3879 formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
3880 subtitles = self._merge_subtitles(subs, subtitles) # Prioritize HLS subs over DASH
3882 if process_manifest_format(f, 'dash
', client_name, f['format_id
']):
3883 f['filesize
'] = int_or_none(self._search_regex(
3884 r'/clen
/(\d
+)', f.get('fragment_base_url
') or f['url
'], 'file size
', default=None))
3885 if needs_live_processing:
3886 f['is_from_start
'] = True
3891 def _extract_storyboard(self, player_responses, duration):
3893 player_responses, ('storyboards
', 'playerStoryboardSpecRenderer
', 'spec
'), default='').split('|
')[::-1]
3894 base_url = url_or_none(urljoin('https
://i
.ytimg
.com
/', spec.pop() or None))
3898 for i, args in enumerate(spec):
3899 args = args.split('#')
3900 counts
= list(map(int_or_none
, args
[:5]))
3901 if len(args
) != 8 or not all(counts
):
3902 self
.report_warning(f
'Malformed storyboard {i}: {"#".join(args)}{bug_reports_message()}')
3904 width
, height
, frame_count
, cols
, rows
= counts
3907 url
= base_url
.replace('$L', str(L
- i
)).replace('$N', N
) + f
'&sigh={sigh}'
3908 fragment_count
= frame_count
/ (cols
* rows
)
3909 fragment_duration
= duration
/ fragment_count
3911 'format_id': f
'sb{i}',
3912 'format_note': 'storyboard',
3914 'protocol': 'mhtml',
3920 'fps': frame_count
/ duration
,
3924 'url': url
.replace('$M', str(j
)),
3925 'duration': min(fragment_duration
, duration
- (j
* fragment_duration
)),
3926 } for j
in range(math
.ceil(fragment_count
))],
3929 def _download_player_responses(self
, url
, smuggled_data
, video_id
, webpage_url
):
3931 if 'webpage' not in self
._configuration
_arg
('player_skip'):
3932 query
= {'bpctr': '9999999999', 'has_verified': '1'}
3933 if smuggled_data
.get('is_story'):
3934 query
['pp'] = self
._STORY
_PLAYER
_PARAMS
3935 webpage
= self
._download
_webpage
(
3936 webpage_url
, video_id
, fatal
=False, query
=query
)
3938 master_ytcfg
= self
.extract_ytcfg(video_id
, webpage
) or self
._get
_default
_ytcfg
()
3940 player_responses
, player_url
= self
._extract
_player
_responses
(
3941 self
._get
_requested
_clients
(url
, smuggled_data
),
3942 video_id
, webpage
, master_ytcfg
, smuggled_data
)
3944 return webpage
, master_ytcfg
, player_responses
, player_url
3946 def _list_formats(self
, video_id
, microformats
, video_details
, player_responses
, player_url
, duration
=None):
3947 live_broadcast_details
= traverse_obj(microformats
, (..., 'liveBroadcastDetails'))
3948 is_live
= get_first(video_details
, 'isLive')
3950 is_live
= get_first(live_broadcast_details
, 'isLiveNow')
3951 live_content
= get_first(video_details
, 'isLiveContent')
3952 is_upcoming
= get_first(video_details
, 'isUpcoming')
3953 post_live
= get_first(video_details
, 'isPostLiveDvr')
3954 live_status
= ('post_live' if post_live
3955 else 'is_live' if is_live
3956 else 'is_upcoming' if is_upcoming
3957 else 'was_live' if live_content
3958 else 'not_live' if False in (is_live
, live_content
)
3960 streaming_data
= traverse_obj(player_responses
, (..., 'streamingData'))
3961 *formats
, subtitles
= self
._extract
_formats
_and
_subtitles
(streaming_data
, video_id
, player_url
, live_status
, duration
)
3963 return live_broadcast_details
, live_status
, streaming_data
, formats
, subtitles
3965 def _real_extract(self
, url
):
3966 url
, smuggled_data
= unsmuggle_url(url
, {})
3967 video_id
= self
._match
_id
(url
)
3969 base_url
= self
.http_scheme() + '//www.youtube.com/'
3970 webpage_url
= base_url
+ 'watch?v=' + video_id
3972 webpage
, master_ytcfg
, player_responses
, player_url
= self
._download
_player
_responses
(url
, smuggled_data
, video_id
, webpage_url
)
3974 playability_statuses
= traverse_obj(
3975 player_responses
, (..., 'playabilityStatus'), expected_type
=dict)
3977 trailer_video_id
= get_first(
3978 playability_statuses
,
3979 ('errorScreen', 'playerLegacyDesktopYpcTrailerRenderer', 'trailerVideoId'),
3981 if trailer_video_id
:
3982 return self
.url_result(
3983 trailer_video_id
, self
.ie_key(), trailer_video_id
)
3985 search_meta
= ((lambda x
: self
._html
_search
_meta
(x
, webpage
, default
=None))
3986 if webpage
else (lambda x
: None))
3988 video_details
= traverse_obj(player_responses
, (..., 'videoDetails'), expected_type
=dict)
3989 microformats
= traverse_obj(
3990 player_responses
, (..., 'microformat', 'playerMicroformatRenderer'),
3993 translated_title
= self
._get
_text
(microformats
, (..., 'title'))
3994 video_title
= (self
._preferred
_lang
and translated_title
3995 or get_first(video_details
, 'title') # primary
3997 or search_meta(['og:title', 'twitter:title', 'title']))
3998 translated_description
= self
._get
_text
(microformats
, (..., 'description'))
3999 original_description
= get_first(video_details
, 'shortDescription')
4000 video_description
= (
4001 self
._preferred
_lang
and translated_description
4002 # If original description is blank, it will be an empty string.
4003 # Do not prefer translated description in this case.
4004 or original_description
if original_description
is not None else translated_description
)
4006 multifeed_metadata_list
= get_first(
4008 ('multicamera', 'playerLegacyMulticameraRenderer', 'metadataList'),
4010 if multifeed_metadata_list
and not smuggled_data
.get('force_singlefeed'):
4011 if self
.get_param('noplaylist'):
4012 self
.to_screen('Downloading just video %s because of --no-playlist' % video_id
)
4016 for feed
in multifeed_metadata_list
.split(','):
4017 # Unquote should take place before split on comma (,) since textual
4018 # fields may contain comma as well (see
4019 # https://github.com/ytdl-org/youtube-dl/issues/8536)
4020 feed_data
= urllib
.parse
.parse_qs(
4021 urllib
.parse
.unquote_plus(feed
))
4023 def feed_entry(name
):
4025 feed_data
, lambda x
: x
[name
][0], str)
4027 feed_id
= feed_entry('id')
4030 feed_title
= feed_entry('title')
4033 title
+= ' (%s)' % feed_title
4035 '_type': 'url_transparent',
4036 'ie_key': 'Youtube',
4038 '%swatch?v=%s' % (base_url
, feed_data
['id'][0]),
4039 {'force_singlefeed': True}
),
4042 feed_ids
.append(feed_id
)
4044 'Downloading multifeed video (%s) - add --no-playlist to just download video %s'
4045 % (', '.join(feed_ids
), video_id
))
4046 return self
.playlist_result(
4047 entries
, video_id
, video_title
, video_description
)
4049 duration
= (int_or_none(get_first(video_details
, 'lengthSeconds'))
4050 or int_or_none(get_first(microformats
, 'lengthSeconds'))
4051 or parse_duration(search_meta('duration')) or None)
4053 live_broadcast_details
, live_status
, streaming_data
, formats
, automatic_captions
= \
4054 self
._list
_formats
(video_id
, microformats
, video_details
, player_responses
, player_url
, duration
)
4055 if live_status
== 'post_live':
4056 self
.write_debug(f
'{video_id}: Video is in Post-Live Manifestless mode')
4059 if not self
.get_param('allow_unplayable_formats') and traverse_obj(streaming_data
, (..., 'licenseInfos')):
4060 self
.report_drm(video_id
)
4062 playability_statuses
,
4063 ('errorScreen', 'playerErrorMessageRenderer'), expected_type
=dict) or {}
4064 reason
= self
._get
_text
(pemr
, 'reason') or get_first(playability_statuses
, 'reason')
4065 subreason
= clean_html(self
._get
_text
(pemr
, 'subreason') or '')
4067 if subreason
== 'The uploader has not made this video available in your country.':
4068 countries
= get_first(microformats
, 'availableCountries')
4070 regions_allowed
= search_meta('regionsAllowed')
4071 countries
= regions_allowed
.split(',') if regions_allowed
else None
4072 self
.raise_geo_restricted(subreason
, countries
, metadata_available
=True)
4073 reason
+= f
'. {subreason}'
4075 self
.raise_no_formats(reason
, expected
=True)
4077 keywords
= get_first(video_details
, 'keywords', expected_type
=list) or []
4078 if not keywords
and webpage
:
4080 unescapeHTML(m
.group('content'))
4081 for m
in re
.finditer(self
._meta
_regex
('og:video:tag'), webpage
)]
4082 for keyword
in keywords
:
4083 if keyword
.startswith('yt:stretch='):
4084 mobj
= re
.search(r
'(\d+)\s*:\s*(\d+)', keyword
)
4086 # NB: float is intentional for forcing float division
4087 w
, h
= (float(v
) for v
in mobj
.groups())
4091 if f
.get('vcodec') != 'none':
4092 f
['stretched_ratio'] = ratio
4094 thumbnails
= self
._extract
_thumbnails
((video_details
, microformats
), (..., ..., 'thumbnail'))
4095 thumbnail_url
= search_meta(['og:image', 'twitter:image'])
4098 'url': thumbnail_url
,
4100 original_thumbnails
= thumbnails
.copy()
4102 # The best resolution thumbnails sometimes does not appear in the webpage
4103 # See: https://github.com/yt-dlp/yt-dlp/issues/340
4104 # List of possible thumbnails - Ref: <https://stackoverflow.com/a/20542029>
4106 # While the *1,*2,*3 thumbnails are just below their corresponding "*default" variants
4107 # in resolution, these are not the custom thumbnail. So de-prioritize them
4108 'maxresdefault', 'hq720', 'sddefault', 'hqdefault', '0', 'mqdefault', 'default',
4109 'sd1', 'sd2', 'sd3', 'hq1', 'hq2', 'hq3', 'mq1', 'mq2', 'mq3', '1', '2', '3'
4111 n_thumbnail_names
= len(thumbnail_names
)
4113 'url': 'https://i.ytimg.com/vi{webp}/{video_id}/{name}{live}.{ext}'.format(
4114 video_id
=video_id
, name
=name
, ext
=ext
,
4115 webp
='_webp' if ext
== 'webp' else '', live
='_live' if live_status
== 'is_live' else ''),
4116 } for name
in thumbnail_names
for ext
in ('webp', 'jpg'))
4117 for thumb
in thumbnails
:
4118 i
= next((i
for i
, t
in enumerate(thumbnail_names
) if f
'/{video_id}/{t}' in thumb
['url']), n_thumbnail_names
)
4119 thumb
['preference'] = (0 if '.webp' in thumb
['url'] else -1) - (2 * i
)
4120 self
._remove
_duplicate
_formats
(thumbnails
)
4121 self
._downloader
._sort
_thumbnails
(original_thumbnails
)
4123 category
= get_first(microformats
, 'category') or search_meta('genre')
4124 channel_id
= self
.ucid_or_none(str_or_none(
4125 get_first(video_details
, 'channelId')
4126 or get_first(microformats
, 'externalChannelId')
4127 or search_meta('channelId')))
4128 owner_profile_url
= get_first(microformats
, 'ownerProfileUrl')
4130 live_start_time
= parse_iso8601(get_first(live_broadcast_details
, 'startTimestamp'))
4131 live_end_time
= parse_iso8601(get_first(live_broadcast_details
, 'endTimestamp'))
4132 if not duration
and live_end_time
and live_start_time
:
4133 duration
= live_end_time
- live_start_time
4135 needs_live_processing
= self
._needs
_live
_processing
(live_status
, duration
)
4137 def is_bad_format(fmt
):
4138 if needs_live_processing
and not fmt
.get('is_from_start'):
4140 elif (live_status
== 'is_live' and needs_live_processing
!= 'is_live'
4141 and fmt
.get('protocol') == 'http_dash_segments'):
4144 for fmt
in filter(is_bad_format
, formats
):
4145 fmt
['preference'] = (fmt
.get('preference') or -1) - 10
4146 fmt
['format_note'] = join_nonempty(fmt
.get('format_note'), '(Last 4 hours)', delim
=' ')
4148 if needs_live_processing
:
4149 self
._prepare
_live
_from
_start
_formats
(
4150 formats
, video_id
, live_start_time
, url
, webpage_url
, smuggled_data
, live_status
== 'is_live')
4152 formats
.extend(self
._extract
_storyboard
(player_responses
, duration
))
4154 channel_handle
= self
.handle_from_url(owner_profile_url
)
4158 'title': video_title
,
4160 'thumbnails': thumbnails
,
4161 # The best thumbnail that we are sure exists. Prevents unnecessary
4162 # URL checking if user don't care about getting the best possible thumbnail
4163 'thumbnail': traverse_obj(original_thumbnails
, (-1, 'url')),
4164 'description': video_description
,
4165 'channel_id': channel_id
,
4166 'channel_url': format_field(channel_id
, None, 'https://www.youtube.com/channel/%s', default
=None),
4167 'duration': duration
,
4168 'view_count': int_or_none(
4169 get_first((video_details
, microformats
), (..., 'viewCount'))
4170 or search_meta('interactionCount')),
4171 'average_rating': float_or_none(get_first(video_details
, 'averageRating')),
4172 'age_limit': 18 if (
4173 get_first(microformats
, 'isFamilySafe') is False
4174 or search_meta('isFamilyFriendly') == 'false'
4175 or search_meta('og:restrictions:age') == '18+') else 0,
4176 'webpage_url': webpage_url
,
4177 'categories': [category
] if category
else None,
4179 'playable_in_embed': get_first(playability_statuses
, 'playableInEmbed'),
4180 'live_status': live_status
,
4181 'release_timestamp': live_start_time
,
4182 '_format_sort_fields': ( # source_preference is lower for throttled/potentially damaged formats
4183 'quality', 'res', 'fps', 'hdr:12', 'source', 'vcodec:vp9.2', 'channels', 'acodec', 'lang', 'proto')
4187 pctr
= traverse_obj(player_responses
, (..., 'captions', 'playerCaptionsTracklistRenderer'), expected_type
=dict)
4189 def get_lang_code(track
):
4190 return (remove_start(track
.get('vssId') or '', '.').replace('.', '-')
4191 or track
.get('languageCode'))
4193 # Converted into dicts to remove duplicates
4195 get_lang_code(sub
): sub
4196 for sub
in traverse_obj(pctr
, (..., 'captionTracks', ...))}
4197 translation_languages
= {
4198 lang
.get('languageCode'): self
._get
_text
(lang
.get('languageName'), max_runs
=1)
4199 for lang
in traverse_obj(pctr
, (..., 'translationLanguages', ...))}
4201 def process_language(container
, base_url
, lang_code
, sub_name
, query
):
4202 lang_subs
= container
.setdefault(lang_code
, [])
4203 for fmt
in self
._SUBTITLE
_FORMATS
:
4209 'url': urljoin('https://www.youtube.com', update_url_query(base_url
, query
)),
4213 # NB: Constructing the full subtitle dictionary is slow
4214 get_translated_subs
= 'translated_subs' not in self
._configuration
_arg
('skip') and (
4215 self
.get_param('writeautomaticsub', False) or self
.get_param('listsubtitles'))
4216 for lang_code
, caption_track
in captions
.items():
4217 base_url
= caption_track
.get('baseUrl')
4218 orig_lang
= parse_qs(base_url
).get('lang', [None])[-1]
4221 lang_name
= self
._get
_text
(caption_track
, 'name', max_runs
=1)
4222 if caption_track
.get('kind') != 'asr':
4226 subtitles
, base_url
, lang_code
, lang_name
, {})
4227 if not caption_track
.get('isTranslatable'):
4229 for trans_code
, trans_name
in translation_languages
.items():
4232 orig_trans_code
= trans_code
4233 if caption_track
.get('kind') != 'asr' and trans_code
!= 'und':
4234 if not get_translated_subs
:
4236 trans_code
+= f
'-{lang_code}'
4237 trans_name
+= format_field(lang_name
, None, ' from %s')
4238 # Add an "-orig" label to the original language so that it can be distinguished.
4239 # The subs are returned without "-orig" as well for compatibility
4240 if lang_code
== f
'a-{orig_trans_code}':
4242 automatic_captions
, base_url
, f
'{trans_code}-orig', f
'{trans_name} (Original)', {})
4243 # Setting tlang=lang returns damaged subtitles.
4244 process_language(automatic_captions
, base_url
, trans_code
, trans_name
,
4245 {} if orig_lang == orig_trans_code else {'tlang': trans_code}
)
4247 info
['automatic_captions'] = automatic_captions
4248 info
['subtitles'] = subtitles
4250 parsed_url
= urllib
.parse
.urlparse(url
)
4251 for component
in [parsed_url
.fragment
, parsed_url
.query
]:
4252 query
= urllib
.parse
.parse_qs(component
)
4253 for k
, v
in query
.items():
4254 for d_k
, s_ks
in [('start', ('start', 't')), ('end', ('end',))]:
4256 if d_k
not in info
and k
in s_ks
:
4257 info
[d_k
] = parse_duration(query
[k
][0])
4259 # Youtube Music Auto-generated description
4260 if video_description
:
4263 (?P<track>[^·\n]+)·(?P<artist>[^\n]+)\n+
4265 (?:.+?℗\s*(?P<release_year>\d{4})(?!\d))?
4266 (?:.+?Released on\s*:\s*(?P<release_date>\d{4}-\d{2}-\d{2}))?
4267 (.+?\nArtist\s*:\s*(?P<clean_artist>[^\n]+))?
4268 .+\nAuto-generated\ by\ YouTube\.\s*$
4269 ''', video_description
)
4271 release_year
= mobj
.group('release_year')
4272 release_date
= mobj
.group('release_date')
4274 release_date
= release_date
.replace('-', '')
4275 if not release_year
:
4276 release_year
= release_date
[:4]
4278 'album': mobj
.group('album'.strip()),
4279 'artist': mobj
.group('clean_artist') or ', '.join(a
.strip() for a
in mobj
.group('artist').split('·')),
4280 'track': mobj
.group('track').strip(),
4281 'release_date': release_date
,
4282 'release_year': int_or_none(release_year
),
4287 initial_data
= self
.extract_yt_initial_data(video_id
, webpage
, fatal
=False)
4288 if not traverse_obj(initial_data
, 'contents'):
4289 self
.report_warning('Incomplete data received in embedded initial data; re-fetching using API.')
4291 if not initial_data
:
4292 query
= {'videoId': video_id}
4293 query
.update(self
._get
_checkok
_params
())
4294 initial_data
= self
._extract
_response
(
4295 item_id
=video_id
, ep
='next', fatal
=False,
4296 ytcfg
=master_ytcfg
, query
=query
, check_get_keys
='contents',
4297 headers
=self
.generate_api_headers(ytcfg
=master_ytcfg
),
4298 note
='Downloading initial data API JSON')
4300 info
['comment_count'] = traverse_obj(initial_data
, (
4301 'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'itemSectionRenderer',
4302 'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount'
4304 'engagementPanels', lambda _
, v
: v
['engagementPanelSectionListRenderer']['panelIdentifier'] == 'comment-item-section',
4305 'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo'
4306 ), expected_type
=self
._get
_count
, get_all
=False)
4308 try: # This will error if there is no livechat
4309 initial_data
['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']
4310 except (KeyError, IndexError, TypeError):
4313 info
.setdefault('subtitles', {})['live_chat'] = [{
4314 # url is needed to set cookies
4315 'url': f
'https://www.youtube.com/watch?v={video_id}&bpctr=9999999999&has_verified=1',
4316 'video_id': video_id
,
4318 'protocol': ('youtube_live_chat' if live_status
in ('is_live', 'is_upcoming')
4319 else 'youtube_live_chat_replay'),
4323 info
['chapters'] = (
4324 self
._extract
_chapters
_from
_json
(initial_data
, duration
)
4325 or self
._extract
_chapters
_from
_engagement
_panel
(initial_data
, duration
)
4326 or self
._extract
_chapters
_from
_description
(video_description
, duration
)
4329 info
['heatmap'] = self
._extract
_heatmap
_from
_player
_overlay
(initial_data
)
4331 contents
= traverse_obj(
4332 initial_data
, ('contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents'),
4333 expected_type
=list, default
=[])
4335 vpir
= get_first(contents
, 'videoPrimaryInfoRenderer')
4337 stl
= vpir
.get('superTitleLink')
4339 stl
= self
._get
_text
(stl
)
4342 lambda x
: x
['superTitleIcon']['iconType']) == 'LOCATION_PIN':
4343 info
['location'] = stl
4345 mobj
= re
.search(r
'(.+?)\s*S(\d+)\s*•?\s*E(\d+)', stl
)
4348 'series': mobj
.group(1),
4349 'season_number': int(mobj
.group(2)),
4350 'episode_number': int(mobj
.group(3)),
4352 for tlb
in (try_get(
4354 lambda x
: x
['videoActions']['menuRenderer']['topLevelButtons'],
4358 tlb
, ('toggleButtonRenderer', ...),
4359 ('segmentedLikeDislikeButtonRenderer', ..., 'toggleButtonRenderer')))
4361 for getter
, regex
in [(
4362 lambda x
: x
['defaultText']['accessibility']['accessibilityData'],
4363 r
'(?P<count>[\d,]+)\s*(?P<type>(?:dis)?like)'), ([
4364 lambda x
: x
['accessibility'],
4365 lambda x
: x
['accessibilityData']['accessibilityData'],
4366 ], r
'(?P<type>(?:dis)?like) this video along with (?P<count>[\d,]+) other people')]:
4367 label
= (try_get(tbr
, getter
, dict) or {}).get('label')
4369 mobj
= re
.match(regex
, label
)
4371 info
[mobj
.group('type') + '_count'] = str_to_int(mobj
.group('count'))
4373 sbr_tooltip
= try_get(
4374 vpir
, lambda x
: x
['sentimentBar']['sentimentBarRenderer']['tooltip'])
4376 like_count
, dislike_count
= sbr_tooltip
.split(' / ')
4378 'like_count': str_to_int(like_count
),
4379 'dislike_count': str_to_int(dislike_count
),
4381 vcr
= traverse_obj(vpir
, ('viewCount', 'videoViewCountRenderer'))
4383 vc
= self
._get
_count
(vcr
, 'viewCount')
4384 # Upcoming premieres with waiting count are treated as live here
4385 if vcr
.get('isLive'):
4386 info
['concurrent_view_count'] = vc
4387 elif info
.get('view_count') is None:
4388 info
['view_count'] = vc
4390 vsir
= get_first(contents
, 'videoSecondaryInfoRenderer')
4392 vor
= traverse_obj(vsir
, ('owner', 'videoOwnerRenderer'))
4394 'channel': self
._get
_text
(vor
, 'title'),
4395 'channel_follower_count': self
._get
_count
(vor
, 'subscriberCountText')})
4397 if not channel_handle
:
4398 channel_handle
= self
.handle_from_url(
4400 ('navigationEndpoint', ('title', 'runs', ..., 'navigationEndpoint')),
4401 (('commandMetadata', 'webCommandMetadata', 'url'), ('browseEndpoint', 'canonicalBaseUrl')),
4402 {str}
), get_all
=False))
4406 lambda x
: x
['metadataRowContainer']['metadataRowContainerRenderer']['rows'],
4408 multiple_songs
= False
4410 if try_get(row
, lambda x
: x
['metadataRowRenderer']['hasDividerLine']) is True:
4411 multiple_songs
= True
4414 mrr
= row
.get('metadataRowRenderer') or {}
4415 mrr_title
= mrr
.get('title')
4418 mrr_title
= self
._get
_text
(mrr
, 'title')
4419 mrr_contents_text
= self
._get
_text
(mrr
, ('contents', 0))
4420 if mrr_title
== 'License':
4421 info
['license'] = mrr_contents_text
4422 elif not multiple_songs
:
4423 if mrr_title
== 'Album':
4424 info
['album'] = mrr_contents_text
4425 elif mrr_title
== 'Artist':
4426 info
['artist'] = mrr_contents_text
4427 elif mrr_title
== 'Song':
4428 info
['track'] = mrr_contents_text
4431 'uploader': info
.get('channel'),
4432 'uploader_id': channel_handle
,
4433 'uploader_url': format_field(channel_handle
, None, 'https://www.youtube.com/%s', default
=None),
4435 # The upload date for scheduled, live and past live streams / premieres in microformats
4436 # may be different from the stream date. Although not in UTC, we will prefer it in this case.
4437 # See: https://github.com/yt-dlp/yt-dlp/pull/2223#issuecomment-1008485139
4439 unified_strdate(get_first(microformats
, 'uploadDate'))
4440 or unified_strdate(search_meta('uploadDate')))
4441 if not upload_date
or (
4442 live_status
in ('not_live', None)
4443 and 'no-youtube-prefer-utc-upload-date' not in self
.get_param('compat_opts', [])
4445 upload_date
= strftime_or_none(
4446 self
._parse
_time
_text
(self
._get
_text
(vpir
, 'dateText')), '%Y%m%d') or upload_date
4447 info
['upload_date'] = upload_date
4449 for s_k
, d_k
in [('artist', 'creator'), ('track', 'alt_title')]:
4454 badges
= self
._extract
_badges
(traverse_obj(contents
, (..., 'videoPrimaryInfoRenderer'), get_all
=False))
4456 is_private
= (self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
)
4457 or get_first(video_details
, 'isPrivate', expected_type
=bool))
4459 info
['availability'] = (
4460 'public' if self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
4461 else self
._availability
(
4462 is_private
=is_private
,
4464 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
)
4465 or False if initial_data
and is_private
is not None else None),
4466 needs_subscription
=(
4467 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
)
4468 or False if initial_data
and is_private
is not None else None),
4469 needs_auth
=info
['age_limit'] >= 18,
4470 is_unlisted
=None if is_private
is None else (
4471 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
)
4472 or get_first(microformats
, 'isUnlisted', expected_type
=bool))))
4474 info
['__post_extractor'] = self
.extract_comments(master_ytcfg
, video_id
, contents
, webpage
)
4476 self
.mark_watched(video_id
, player_responses
)
4481 class YoutubeTabBaseInfoExtractor(YoutubeBaseInfoExtractor
):
4483 def passthrough_smuggled_data(func
):
4484 def _smuggle(info
, smuggled_data
):
4485 if info
.get('_type') not in ('url', 'url_transparent'):
4487 if smuggled_data
.get('is_music_url'):
4488 parsed_url
= urllib
.parse
.urlparse(info
['url'])
4489 if parsed_url
.netloc
in ('www.youtube.com', 'music.youtube.com'):
4490 smuggled_data
.pop('is_music_url')
4491 info
['url'] = urllib
.parse
.urlunparse(parsed_url
._replace
(netloc
='music.youtube.com'))
4493 info
['url'] = smuggle_url(info
['url'], smuggled_data
)
4496 @functools.wraps(func
)
4497 def wrapper(self
, url
):
4498 url
, smuggled_data
= unsmuggle_url(url
, {})
4499 if self
.is_music_url(url
):
4500 smuggled_data
['is_music_url'] = True
4501 info_dict
= func(self
, url
, smuggled_data
)
4503 _smuggle(info_dict
, smuggled_data
)
4504 if info_dict
.get('entries'):
4505 info_dict
['entries'] = (_smuggle(i
, smuggled_data
.copy()) for i
in info_dict
['entries'])
4510 def _extract_basic_item_renderer(item
):
4511 # Modified from _extract_grid_item_renderer
4512 known_basic_renderers
= (
4513 'playlistRenderer', 'videoRenderer', 'channelRenderer', 'showRenderer', 'reelItemRenderer'
4515 for key
, renderer
in item
.items():
4516 if not isinstance(renderer
, dict):
4518 elif key
in known_basic_renderers
:
4520 elif key
.startswith('grid') and key
.endswith('Renderer'):
4523 def _extract_channel_renderer(self
, renderer
):
4524 channel_id
= self
.ucid_or_none(renderer
['channelId'])
4525 title
= self
._get
_text
(renderer
, 'title')
4526 channel_url
= format_field(channel_id
, None, 'https://www.youtube.com/channel/%s', default
=None)
4527 # As of 2023-03-01 YouTube doesn't use the channel handles on these renderers yet.
4528 # However we can expect them to change that in the future.
4529 channel_handle
= self
.handle_from_url(
4530 traverse_obj(renderer
, (
4531 'navigationEndpoint', (('commandMetadata', 'webCommandMetadata', 'url'),
4532 ('browseEndpoint', 'canonicalBaseUrl')),
4533 {str}
), get_all
=False))
4538 'ie_key': YoutubeTabIE
.ie_key(),
4541 'channel_id': channel_id
,
4542 'channel_url': channel_url
,
4544 'uploader_id': channel_handle
,
4545 'uploader_url': format_field(channel_handle
, None, 'https://www.youtube.com/%s', default
=None),
4546 'channel_follower_count': self
._get
_count
(renderer
, 'subscriberCountText'),
4547 'thumbnails': self
._extract
_thumbnails
(renderer
, 'thumbnail'),
4548 'playlist_count': self
._get
_count
(renderer
, 'videoCountText'),
4549 'description': self
._get
_text
(renderer
, 'descriptionSnippet'),
4552 def _grid_entries(self
, grid_renderer
):
4553 for item
in grid_renderer
['items']:
4554 if not isinstance(item
, dict):
4556 renderer
= self
._extract
_basic
_item
_renderer
(item
)
4557 if not isinstance(renderer
, dict):
4559 title
= self
._get
_text
(renderer
, 'title')
4562 playlist_id
= renderer
.get('playlistId')
4564 yield self
.url_result(
4565 'https://www.youtube.com/playlist?list=%s' % playlist_id
,
4566 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
4570 video_id
= renderer
.get('videoId')
4572 yield self
._extract
_video
(renderer
)
4575 channel_id
= renderer
.get('channelId')
4577 yield self
._extract
_channel
_renderer
(renderer
)
4579 # generic endpoint URL support
4580 ep_url
= urljoin('https://www.youtube.com/', try_get(
4581 renderer
, lambda x
: x
['navigationEndpoint']['commandMetadata']['webCommandMetadata']['url'],
4584 for ie
in (YoutubeTabIE
, YoutubePlaylistIE
, YoutubeIE
):
4585 if ie
.suitable(ep_url
):
4586 yield self
.url_result(
4587 ep_url
, ie
=ie
.ie_key(), video_id
=ie
._match
_id
(ep_url
), video_title
=title
)
4590 def _music_reponsive_list_entry(self
, renderer
):
4591 video_id
= traverse_obj(renderer
, ('playlistItemData', 'videoId'))
4593 title
= traverse_obj(renderer
, (
4594 'flexColumns', 0, 'musicResponsiveListItemFlexColumnRenderer',
4595 'text', 'runs', 0, 'text'))
4596 return self
.url_result(f
'https://music.youtube.com/watch?v={video_id}',
4597 ie
=YoutubeIE
.ie_key(), video_id
=video_id
, title
=title
)
4598 playlist_id
= traverse_obj(renderer
, ('navigationEndpoint', 'watchEndpoint', 'playlistId'))
4600 video_id
= traverse_obj(renderer
, ('navigationEndpoint', 'watchEndpoint', 'videoId'))
4602 return self
.url_result(f
'https://music.youtube.com/watch?v={video_id}&list={playlist_id}',
4603 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4604 return self
.url_result(f
'https://music.youtube.com/playlist?list={playlist_id}',
4605 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4606 browse_id
= traverse_obj(renderer
, ('navigationEndpoint', 'browseEndpoint', 'browseId'))
4608 return self
.url_result(f
'https://music.youtube.com/browse/{browse_id}',
4609 ie
=YoutubeTabIE
.ie_key(), video_id
=browse_id
)
4611 def _shelf_entries_from_content(self
, shelf_renderer
):
4612 content
= shelf_renderer
.get('content')
4613 if not isinstance(content
, dict):
4615 renderer
= content
.get('gridRenderer') or content
.get('expandedShelfContentsRenderer')
4617 # TODO: add support for nested playlists so each shelf is processed
4618 # as separate playlist
4619 # TODO: this includes only first N items
4620 yield from self
._grid
_entries
(renderer
)
4621 renderer
= content
.get('horizontalListRenderer')
4626 def _shelf_entries(self
, shelf_renderer
, skip_channels
=False):
4628 shelf_renderer
, lambda x
: x
['endpoint']['commandMetadata']['webCommandMetadata']['url'],
4630 shelf_url
= urljoin('https://www.youtube.com', ep
)
4632 # Skipping links to another channels, note that checking for
4633 # endpoint.commandMetadata.webCommandMetadata.webPageTypwebPageType == WEB_PAGE_TYPE_CHANNEL
4635 if skip_channels
and '/channels?' in shelf_url
:
4637 title
= self
._get
_text
(shelf_renderer
, 'title')
4638 yield self
.url_result(shelf_url
, video_title
=title
)
4639 # Shelf may not contain shelf URL, fallback to extraction from content
4640 yield from self
._shelf
_entries
_from
_content
(shelf_renderer
)
4642 def _playlist_entries(self
, video_list_renderer
):
4643 for content
in video_list_renderer
['contents']:
4644 if not isinstance(content
, dict):
4646 renderer
= content
.get('playlistVideoRenderer') or content
.get('playlistPanelVideoRenderer')
4647 if not isinstance(renderer
, dict):
4649 video_id
= renderer
.get('videoId')
4652 yield self
._extract
_video
(renderer
)
4654 def _rich_entries(self
, rich_grid_renderer
):
4655 renderer
= traverse_obj(
4657 ('content', ('videoRenderer', 'reelItemRenderer', 'playlistRenderer')), get_all
=False) or {}
4658 video_id
= renderer
.get('videoId')
4660 yield self
._extract
_video
(renderer
)
4662 playlist_id
= renderer
.get('playlistId')
4664 yield self
.url_result(
4665 f
'https://www.youtube.com/playlist?list={playlist_id}',
4666 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
4667 video_title
=self
._get
_text
(renderer
, 'title'))
4670 def _video_entry(self
, video_renderer
):
4671 video_id
= video_renderer
.get('videoId')
4673 return self
._extract
_video
(video_renderer
)
4675 def _hashtag_tile_entry(self
, hashtag_tile_renderer
):
4676 url
= urljoin('https://youtube.com', traverse_obj(
4677 hashtag_tile_renderer
, ('onTapCommand', 'commandMetadata', 'webCommandMetadata', 'url')))
4679 return self
.url_result(
4680 url
, ie
=YoutubeTabIE
.ie_key(), title
=self
._get
_text
(hashtag_tile_renderer
, 'hashtag'))
4682 def _post_thread_entries(self
, post_thread_renderer
):
4683 post_renderer
= try_get(
4684 post_thread_renderer
, lambda x
: x
['post']['backstagePostRenderer'], dict)
4685 if not post_renderer
:
4688 video_renderer
= try_get(
4689 post_renderer
, lambda x
: x
['backstageAttachment']['videoRenderer'], dict) or {}
4690 video_id
= video_renderer
.get('videoId')
4692 entry
= self
._extract
_video
(video_renderer
)
4695 # playlist attachment
4696 playlist_id
= try_get(
4697 post_renderer
, lambda x
: x
['backstageAttachment']['playlistRenderer']['playlistId'], str)
4699 yield self
.url_result(
4700 'https://www.youtube.com/playlist?list=%s' % playlist_id
,
4701 ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
4702 # inline video links
4703 runs
= try_get(post_renderer
, lambda x
: x
['contentText']['runs'], list) or []
4705 if not isinstance(run
, dict):
4708 run
, lambda x
: x
['navigationEndpoint']['urlEndpoint']['url'], str)
4711 if not YoutubeIE
.suitable(ep_url
):
4713 ep_video_id
= YoutubeIE
._match
_id
(ep_url
)
4714 if video_id
== ep_video_id
:
4716 yield self
.url_result(ep_url
, ie
=YoutubeIE
.ie_key(), video_id
=ep_video_id
)
4718 def _post_thread_continuation_entries(self
, post_thread_continuation
):
4719 contents
= post_thread_continuation
.get('contents')
4720 if not isinstance(contents
, list):
4722 for content
in contents
:
4723 renderer
= content
.get('backstagePostThreadRenderer')
4724 if isinstance(renderer
, dict):
4725 yield from self
._post
_thread
_entries
(renderer
)
4727 renderer
= content
.get('videoRenderer')
4728 if isinstance(renderer
, dict):
4729 yield self
._video
_entry
(renderer
)
4732 def _rich_grid_entries(self, contents):
4733 for content in contents:
4734 video_renderer = try_get(content, lambda x: x['richItemRenderer']['content']['videoRenderer'], dict)
4736 entry = self._video_entry(video_renderer)
4741 def _report_history_entries(self
, renderer
):
4742 for url
in traverse_obj(renderer
, (
4743 'rows', ..., 'reportHistoryTableRowRenderer', 'cells', ...,
4744 'reportHistoryTableCellRenderer', 'cell', 'reportHistoryTableTextCellRenderer', 'text', 'runs', ...,
4745 'navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url')):
4746 yield self
.url_result(urljoin('https://www.youtube.com', url
), YoutubeIE
)
4748 def _extract_entries(self
, parent_renderer
, continuation_list
):
4749 # continuation_list is modified in-place with continuation_list = [continuation_token]
4750 continuation_list
[:] = [None]
4751 contents
= try_get(parent_renderer
, lambda x
: x
['contents'], list) or []
4752 for content
in contents
:
4753 if not isinstance(content
, dict):
4755 is_renderer
= traverse_obj(
4756 content
, 'itemSectionRenderer', 'musicShelfRenderer', 'musicShelfContinuation',
4759 if content
.get('richItemRenderer'):
4760 for entry
in self
._rich
_entries
(content
['richItemRenderer']):
4762 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
4763 elif content
.get('reportHistorySectionRenderer'): # https://www.youtube.com/reporthistory
4764 table
= traverse_obj(content
, ('reportHistorySectionRenderer', 'table', 'tableRenderer'))
4765 yield from self
._report
_history
_entries
(table
)
4766 continuation_list
[0] = self
._extract
_continuation
(table
)
4769 isr_contents
= try_get(is_renderer
, lambda x
: x
['contents'], list) or []
4770 for isr_content
in isr_contents
:
4771 if not isinstance(isr_content
, dict):
4775 'playlistVideoListRenderer': self
._playlist
_entries
,
4776 'gridRenderer': self
._grid
_entries
,
4777 'reelShelfRenderer': self
._grid
_entries
,
4778 'shelfRenderer': self
._shelf
_entries
,
4779 'musicResponsiveListItemRenderer': lambda x
: [self
._music
_reponsive
_list
_entry
(x
)],
4780 'backstagePostThreadRenderer': self
._post
_thread
_entries
,
4781 'videoRenderer': lambda x
: [self
._video
_entry
(x
)],
4782 'playlistRenderer': lambda x
: self
._grid
_entries
({'items': [{'playlistRenderer': x}
]}),
4783 'channelRenderer': lambda x
: self
._grid
_entries
({'items': [{'channelRenderer': x}
]}),
4784 'hashtagTileRenderer': lambda x
: [self
._hashtag
_tile
_entry
(x
)]
4786 for key
, renderer
in isr_content
.items():
4787 if key
not in known_renderers
:
4789 for entry
in known_renderers
[key
](renderer
):
4792 continuation_list
[0] = self
._extract
_continuation
(renderer
)
4795 if not continuation_list
[0]:
4796 continuation_list
[0] = self
._extract
_continuation
(is_renderer
)
4798 if not continuation_list
[0]:
4799 continuation_list
[0] = self
._extract
_continuation
(parent_renderer
)
4801 def _entries(self
, tab
, item_id
, ytcfg
, account_syncid
, visitor_data
):
4802 continuation_list
= [None]
4803 extract_entries
= lambda x
: self
._extract
_entries
(x
, continuation_list
)
4804 tab_content
= try_get(tab
, lambda x
: x
['content'], dict)
4808 try_get(tab_content
, lambda x
: x
['sectionListRenderer'], dict)
4809 or try_get(tab_content
, lambda x
: x
['richGridRenderer'], dict) or {})
4810 yield from extract_entries(parent_renderer
)
4811 continuation
= continuation_list
[0]
4813 for page_num
in itertools
.count(1):
4814 if not continuation
:
4816 headers
= self
.generate_api_headers(
4817 ytcfg
=ytcfg
, account_syncid
=account_syncid
, visitor_data
=visitor_data
)
4818 response
= self
._extract
_response
(
4819 item_id
=f
'{item_id} page {page_num}',
4820 query
=continuation
, headers
=headers
, ytcfg
=ytcfg
,
4821 check_get_keys
=('continuationContents', 'onResponseReceivedActions', 'onResponseReceivedEndpoints'))
4825 # Extracting updated visitor data is required to prevent an infinite extraction loop in some cases
4826 # See: https://github.com/ytdl-org/youtube-dl/issues/28702
4827 visitor_data
= self
._extract
_visitor
_data
(response
) or visitor_data
4830 'videoRenderer': (self
._grid
_entries
, 'items'), # for membership tab
4831 'gridPlaylistRenderer': (self
._grid
_entries
, 'items'),
4832 'gridVideoRenderer': (self
._grid
_entries
, 'items'),
4833 'gridChannelRenderer': (self
._grid
_entries
, 'items'),
4834 'playlistVideoRenderer': (self
._playlist
_entries
, 'contents'),
4835 'itemSectionRenderer': (extract_entries
, 'contents'), # for feeds
4836 'richItemRenderer': (extract_entries
, 'contents'), # for hashtag
4837 'backstagePostThreadRenderer': (self
._post
_thread
_continuation
_entries
, 'contents'),
4838 'reportHistoryTableRowRenderer': (self
._report
_history
_entries
, 'rows'),
4839 'playlistVideoListContinuation': (self
._playlist
_entries
, None),
4840 'gridContinuation': (self
._grid
_entries
, None),
4841 'itemSectionContinuation': (self
._post
_thread
_continuation
_entries
, None),
4842 'sectionListContinuation': (extract_entries
, None), # for feeds
4845 continuation_items
= traverse_obj(response
, (
4846 ('onResponseReceivedActions', 'onResponseReceivedEndpoints'), ...,
4847 'appendContinuationItemsAction', 'continuationItems'
4848 ), 'continuationContents', get_all
=False)
4849 continuation_item
= traverse_obj(continuation_items
, 0, None, expected_type
=dict, default
={})
4851 video_items_renderer
= None
4852 for key
in continuation_item
.keys():
4853 if key
not in known_renderers
:
4855 func
, parent_key
= known_renderers
[key
]
4856 video_items_renderer
= {parent_key: continuation_items}
if parent_key
else continuation_items
4857 continuation_list
= [None]
4858 yield from func(video_items_renderer
)
4859 continuation
= continuation_list
[0] or self
._extract
_continuation
(video_items_renderer
)
4861 if not video_items_renderer
:
4865 def _extract_selected_tab(tabs
, fatal
=True):
4866 for tab_renderer
in tabs
:
4867 if tab_renderer
.get('selected'):
4870 raise ExtractorError('Unable to find selected tab')
4873 def _extract_tab_renderers(response
):
4874 return traverse_obj(
4875 response
, ('contents', 'twoColumnBrowseResultsRenderer', 'tabs', ..., ('tabRenderer', 'expandableTabRenderer')), expected_type
=dict)
4877 def _extract_from_tabs(self
, item_id
, ytcfg
, data
, tabs
):
4878 metadata
= self
._extract
_metadata
_from
_tabs
(item_id
, data
)
4880 selected_tab
= self
._extract
_selected
_tab
(tabs
)
4881 metadata
['title'] += format_field(selected_tab
, 'title', ' - %s')
4882 metadata
['title'] += format_field(selected_tab
, 'expandedText', ' - %s')
4884 return self
.playlist_result(
4886 selected_tab
, metadata
['id'], ytcfg
,
4887 self
._extract
_account
_syncid
(ytcfg
, data
),
4888 self
._extract
_visitor
_data
(data
, ytcfg
)),
4891 def _extract_metadata_from_tabs(self
, item_id
, data
):
4892 info
= {'id': item_id}
4894 metadata_renderer
= traverse_obj(data
, ('metadata', 'channelMetadataRenderer'), expected_type
=dict)
4895 if metadata_renderer
:
4896 channel_id
= traverse_obj(metadata_renderer
, ('externalId', {self.ucid_or_none}
),
4897 ('channelUrl', {self.ucid_from_url}
))
4899 'channel': metadata_renderer
.get('title'),
4900 'channel_id': channel_id
,
4902 if info
['channel_id']:
4903 info
['id'] = info
['channel_id']
4905 metadata_renderer
= traverse_obj(data
, ('metadata', 'playlistMetadataRenderer'), expected_type
=dict)
4907 # We can get the uncropped banner/avatar by replacing the crop params with '=s0'
4908 # See: https://github.com/yt-dlp/yt-dlp/issues/2237#issuecomment-1013694714
4909 def _get_uncropped(url
):
4910 return url_or_none((url
or '').split('=')[0] + '=s0')
4912 avatar_thumbnails
= self
._extract
_thumbnails
(metadata_renderer
, 'avatar')
4913 if avatar_thumbnails
:
4914 uncropped_avatar
= _get_uncropped(avatar_thumbnails
[0]['url'])
4915 if uncropped_avatar
:
4916 avatar_thumbnails
.append({
4917 'url': uncropped_avatar
,
4918 'id': 'avatar_uncropped',
4922 channel_banners
= self
._extract
_thumbnails
(
4923 data
, ('header', ..., ('banner', 'mobileBanner', 'tvBanner')))
4924 for banner
in channel_banners
:
4925 banner
['preference'] = -10
4928 uncropped_banner
= _get_uncropped(channel_banners
[0]['url'])
4929 if uncropped_banner
:
4930 channel_banners
.append({
4931 'url': uncropped_banner
,
4932 'id': 'banner_uncropped',
4936 # Deprecated - remove primary_sidebar_renderer when layout discontinued
4937 primary_sidebar_renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarPrimaryInfoRenderer')
4938 playlist_header_renderer
= traverse_obj(data
, ('header', 'playlistHeaderRenderer'), expected_type
=dict)
4940 primary_thumbnails
= self
._extract
_thumbnails
(
4941 primary_sidebar_renderer
, ('thumbnailRenderer', ('playlistVideoThumbnailRenderer', 'playlistCustomThumbnailRenderer'), 'thumbnail'))
4942 playlist_thumbnails
= self
._extract
_thumbnails
(
4943 playlist_header_renderer
, ('playlistHeaderBanner', 'heroPlaylistThumbnailRenderer', 'thumbnail'))
4946 'title': (traverse_obj(metadata_renderer
, 'title')
4947 or self
._get
_text
(data
, ('header', 'hashtagHeaderRenderer', 'hashtag'))
4949 'availability': self
._extract
_availability
(data
),
4950 'channel_follower_count': self
._get
_count
(data
, ('header', ..., 'subscriberCountText')),
4951 'description': try_get(metadata_renderer
, lambda x
: x
.get('description', '')),
4952 'tags': try_get(metadata_renderer
or {}, lambda x
: x
.get('keywords', '').split()),
4953 'thumbnails': (primary_thumbnails
or playlist_thumbnails
) + avatar_thumbnails
+ channel_banners
,
4957 traverse_obj(metadata_renderer
, (('vanityChannelUrl', ('ownerUrls', ...)), {self.handle_from_url}
), get_all
=False)
4958 or traverse_obj(data
, ('header', ..., 'channelHandleText', {self.handle_or_none}
), get_all
=False))
4962 'uploader_id': channel_handle
,
4963 'uploader_url': format_field(channel_handle
, None, 'https://www.youtube.com/%s', default
=None),
4965 # Playlist stats is a text runs array containing [video count, view count, last updated].
4966 # last updated or (view count and last updated) may be missing.
4967 playlist_stats
= get_first(
4968 (primary_sidebar_renderer
, playlist_header_renderer
), (('stats', 'briefStats', 'numVideosText'), ))
4970 last_updated_unix
= self
._parse
_time
_text
(
4971 self
._get
_text
(playlist_stats
, 2) # deprecated, remove when old layout discontinued
4972 or self
._get
_text
(playlist_header_renderer
, ('byline', 1, 'playlistBylineRenderer', 'text')))
4973 info
['modified_date'] = strftime_or_none(last_updated_unix
, '%Y%m%d')
4975 info
['view_count'] = self
._get
_count
(playlist_stats
, 1)
4976 if info
['view_count'] is None: # 0 is allowed
4977 info
['view_count'] = self
._get
_count
(playlist_header_renderer
, 'viewCountText')
4978 if info
['view_count'] is None:
4979 info
['view_count'] = self
._get
_count
(data
, (
4980 'contents', 'twoColumnBrowseResultsRenderer', 'tabs', ..., 'tabRenderer', 'content', 'sectionListRenderer',
4981 'contents', ..., 'itemSectionRenderer', 'contents', ..., 'channelAboutFullMetadataRenderer', 'viewCountText'))
4983 info
['playlist_count'] = self
._get
_count
(playlist_stats
, 0)
4984 if info
['playlist_count'] is None: # 0 is allowed
4985 info
['playlist_count'] = self
._get
_count
(playlist_header_renderer
, ('byline', 0, 'playlistBylineRenderer', 'text'))
4987 if not info
.get('channel_id'):
4988 owner
= traverse_obj(playlist_header_renderer
, 'ownerText')
4989 if not owner
: # Deprecated
4990 owner
= traverse_obj(
4991 self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarSecondaryInfoRenderer'),
4992 ('videoOwner', 'videoOwnerRenderer', 'title'))
4993 owner_text
= self
._get
_text
(owner
)
4994 browse_ep
= traverse_obj(owner
, ('runs', 0, 'navigationEndpoint', 'browseEndpoint')) or {}
4996 'channel': self
._search
_regex
(r
'^by (.+) and \d+ others?$', owner_text
, 'uploader', default
=owner_text
),
4997 'channel_id': self
.ucid_or_none(browse_ep
.get('browseId')),
4998 'uploader_id': self
.handle_from_url(urljoin('https://www.youtube.com', browse_ep
.get('canonicalBaseUrl')))
5002 'uploader': info
['channel'],
5003 'channel_url': format_field(info
.get('channel_id'), None, 'https://www.youtube.com/channel/%s', default
=None),
5004 'uploader_url': format_field(info
.get('uploader_id'), None, 'https://www.youtube.com/%s', default
=None),
5009 def _extract_inline_playlist(self
, playlist
, playlist_id
, data
, ytcfg
):
5010 first_id
= last_id
= response
= None
5011 for page_num
in itertools
.count(1):
5012 videos
= list(self
._playlist
_entries
(playlist
))
5015 start
= next((i
for i
, v
in enumerate(videos
) if v
['id'] == last_id
), -1) + 1
5016 if start
>= len(videos
):
5018 yield from videos
[start
:]
5019 first_id
= first_id
or videos
[0]['id']
5020 last_id
= videos
[-1]['id']
5021 watch_endpoint
= try_get(
5022 playlist
, lambda x
: x
['contents'][-1]['playlistPanelVideoRenderer']['navigationEndpoint']['watchEndpoint'])
5023 headers
= self
.generate_api_headers(
5024 ytcfg
=ytcfg
, account_syncid
=self
._extract
_account
_syncid
(ytcfg
, data
),
5025 visitor_data
=self
._extract
_visitor
_data
(response
, data
, ytcfg
))
5027 'playlistId': playlist_id
,
5028 'videoId': watch_endpoint
.get('videoId') or last_id
,
5029 'index': watch_endpoint
.get('index') or len(videos
),
5030 'params': watch_endpoint
.get('params') or 'OAE%3D'
5032 response
= self
._extract
_response
(
5033 item_id
='%s page %d' % (playlist_id
, page_num
),
5034 query
=query
, ep
='next', headers
=headers
, ytcfg
=ytcfg
,
5035 check_get_keys
='contents'
5038 response
, lambda x
: x
['contents']['twoColumnWatchNextResults']['playlist']['playlist'], dict)
5040 def _extract_from_playlist(self
, item_id
, url
, data
, playlist
, ytcfg
):
5041 title
= playlist
.get('title') or try_get(
5042 data
, lambda x
: x
['titleText']['simpleText'], str)
5043 playlist_id
= playlist
.get('playlistId') or item_id
5045 # Delegating everything except mix playlists to regular tab-based playlist URL
5046 playlist_url
= urljoin(url
, try_get(
5047 playlist
, lambda x
: x
['endpoint']['commandMetadata']['webCommandMetadata']['url'],
5050 # Some playlists are unviewable but YouTube still provides a link to the (broken) playlist page [1]
5051 # [1] MLCT, RLTDwFCb4jeqaKWnciAYM-ZVHg
5052 is_known_unviewable
= re
.fullmatch(r
'MLCT|RLTD[\w-]{22}', playlist_id
)
5054 if playlist_url
and playlist_url
!= url
and not is_known_unviewable
:
5055 return self
.url_result(
5056 playlist_url
, ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
,
5059 return self
.playlist_result(
5060 self
._extract
_inline
_playlist
(playlist
, playlist_id
, data
, ytcfg
),
5061 playlist_id
=playlist_id
, playlist_title
=title
)
5063 def _extract_availability(self
, data
):
5065 Gets the availability of a given playlist/tab.
5066 Note: Unless YouTube tells us explicitly, we do not assume it is public
5067 @param data: response
5069 sidebar_renderer
= self
._extract
_sidebar
_info
_renderer
(data
, 'playlistSidebarPrimaryInfoRenderer') or {}
5070 playlist_header_renderer
= traverse_obj(data
, ('header', 'playlistHeaderRenderer')) or {}
5071 player_header_privacy
= playlist_header_renderer
.get('privacy')
5073 badges
= self
._extract
_badges
(sidebar_renderer
)
5075 # Personal playlists, when authenticated, have a dropdown visibility selector instead of a badge
5076 privacy_setting_icon
= get_first(
5077 (playlist_header_renderer
, sidebar_renderer
),
5078 ('privacyForm', 'dropdownFormFieldRenderer', 'dropdown', 'dropdownRenderer', 'entries',
5079 lambda _
, v
: v
['privacyDropdownItemRenderer']['isSelected'], 'privacyDropdownItemRenderer', 'icon', 'iconType'),
5082 microformats_is_unlisted
= traverse_obj(
5083 data
, ('microformat', 'microformatDataRenderer', 'unlisted'), expected_type
=bool)
5087 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PUBLIC
)
5088 or player_header_privacy
== 'PUBLIC'
5089 or privacy_setting_icon
== 'PRIVACY_PUBLIC')
5090 else self
._availability
(
5092 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PRIVATE
)
5093 or player_header_privacy
== 'PRIVATE' if player_header_privacy
is not None
5094 else privacy_setting_icon
== 'PRIVACY_PRIVATE' if privacy_setting_icon
is not None else None),
5096 self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_UNLISTED
)
5097 or player_header_privacy
== 'UNLISTED' if player_header_privacy
is not None
5098 else privacy_setting_icon
== 'PRIVACY_UNLISTED' if privacy_setting_icon
is not None
5099 else microformats_is_unlisted
if microformats_is_unlisted
is not None else None),
5100 needs_subscription
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_SUBSCRIPTION
) or None,
5101 needs_premium
=self
._has
_badge
(badges
, BadgeType
.AVAILABILITY_PREMIUM
) or None,
5105 def _extract_sidebar_info_renderer(data
, info_renderer
, expected_type
=dict):
5106 sidebar_renderer
= try_get(
5107 data
, lambda x
: x
['sidebar']['playlistSidebarRenderer']['items'], list) or []
5108 for item
in sidebar_renderer
:
5109 renderer
= try_get(item
, lambda x
: x
[info_renderer
], expected_type
)
5113 def _reload_with_unavailable_videos(self
, item_id
, data
, ytcfg
):
5115 Reload playlists with unavailable videos (e.g. private videos, region blocked, etc.)
5117 is_playlist
= bool(traverse_obj(
5118 data
, ('metadata', 'playlistMetadataRenderer'), ('header', 'playlistHeaderRenderer')))
5121 headers
= self
.generate_api_headers(
5122 ytcfg
=ytcfg
, account_syncid
=self
._extract
_account
_syncid
(ytcfg
, data
),
5123 visitor_data
=self
._extract
_visitor
_data
(data
, ytcfg
))
5125 'params': 'wgYCCAA=',
5126 'browseId': f
'VL{item_id}'
5128 return self
._extract
_response
(
5129 item_id
=item_id
, headers
=headers
, query
=query
,
5130 check_get_keys
='contents', fatal
=False, ytcfg
=ytcfg
,
5131 note
='Redownloading playlist API JSON with unavailable videos')
5133 @functools.cached_property
5134 def skip_webpage(self
):
5135 return 'webpage' in self
._configuration
_arg
('skip', ie_key
=YoutubeTabIE
.ie_key())
5137 def _extract_webpage(self
, url
, item_id
, fatal
=True):
5138 webpage
, data
= None, None
5139 for retry
in self
.RetryManager(fatal
=fatal
):
5141 webpage
= self
._download
_webpage
(url
, item_id
, note
='Downloading webpage')
5142 data
= self
.extract_yt_initial_data(item_id
, webpage
or '', fatal
=fatal
) or {}
5143 except ExtractorError
as e
:
5144 if isinstance(e
.cause
, network_exceptions
):
5145 if not isinstance(e
.cause
, urllib
.error
.HTTPError
) or e
.cause
.code
not in (403, 429):
5148 self
._error
_or
_warning
(e
, fatal
=fatal
)
5152 self
._extract
_and
_report
_alerts
(data
)
5153 except ExtractorError
as e
:
5154 self
._error
_or
_warning
(e
, fatal
=fatal
)
5157 # Sometimes youtube returns a webpage with incomplete ytInitialData
5158 # See: https://github.com/yt-dlp/yt-dlp/issues/116
5159 if not traverse_obj(data
, 'contents', 'currentVideoEndpoint', 'onResponseReceivedActions'):
5160 retry
.error
= ExtractorError('Incomplete yt initial data received')
5163 return webpage
, data
5165 def _report_playlist_authcheck(self
, ytcfg
, fatal
=True):
5166 """Use if failed to extract ytcfg (and data) from initial webpage"""
5167 if not ytcfg
and self
.is_authenticated
:
5168 msg
= 'Playlists that require authentication may not extract correctly without a successful webpage download'
5169 if 'authcheck' not in self
._configuration
_arg
('skip', ie_key
=YoutubeTabIE
.ie_key()) and fatal
:
5170 raise ExtractorError(
5171 f
'{msg}. If you are not downloading private content, or '
5172 'your cookies are only for the first account and channel,'
5173 ' pass "--extractor-args youtubetab:skip=authcheck" to skip this check',
5175 self
.report_warning(msg
, only_once
=True)
5177 def _extract_data(self
, url
, item_id
, ytcfg
=None, fatal
=True, webpage_fatal
=False, default_client
='web'):
5179 if not self
.skip_webpage
:
5180 webpage
, data
= self
._extract
_webpage
(url
, item_id
, fatal
=webpage_fatal
)
5181 ytcfg
= ytcfg
or self
.extract_ytcfg(item_id
, webpage
)
5182 # Reject webpage data if redirected to home page without explicitly requesting
5183 selected_tab
= self
._extract
_selected
_tab
(self
._extract
_tab
_renderers
(data
), fatal
=False) or {}
5184 if (url
!= 'https://www.youtube.com/feed/recommended'
5185 and selected_tab
.get('tabIdentifier') == 'FEwhat_to_watch' # Home page
5186 and 'no-youtube-channel-redirect' not in self
.get_param('compat_opts', [])):
5187 msg
= 'The channel/playlist does not exist and the URL redirected to youtube.com home page'
5189 raise ExtractorError(msg
, expected
=True)
5190 self
.report_warning(msg
, only_once
=True)
5192 self
._report
_playlist
_authcheck
(ytcfg
, fatal
=fatal
)
5193 data
= self
._extract
_tab
_endpoint
(url
, item_id
, ytcfg
, fatal
=fatal
, default_client
=default_client
)
5196 def _extract_tab_endpoint(self
, url
, item_id
, ytcfg
=None, fatal
=True, default_client
='web'):
5197 headers
= self
.generate_api_headers(ytcfg
=ytcfg
, default_client
=default_client
)
5198 resolve_response
= self
._extract
_response
(
5199 item_id
=item_id
, query
={'url': url}
, check_get_keys
='endpoint', headers
=headers
, ytcfg
=ytcfg
, fatal
=fatal
,
5200 ep
='navigation/resolve_url', note
='Downloading API parameters API JSON', default_client
=default_client
)
5201 endpoints
= {'browseEndpoint': 'browse', 'watchEndpoint': 'next'}
5202 for ep_key
, ep
in endpoints
.items():
5203 params
= try_get(resolve_response
, lambda x
: x
['endpoint'][ep_key
], dict)
5205 return self
._extract
_response
(
5206 item_id
=item_id
, query
=params
, ep
=ep
, headers
=headers
,
5207 ytcfg
=ytcfg
, fatal
=fatal
, default_client
=default_client
,
5208 check_get_keys
=('contents', 'currentVideoEndpoint', 'onResponseReceivedActions'))
5209 err_note
= 'Failed to resolve url (does the playlist exist?)'
5211 raise ExtractorError(err_note
, expected
=True)
5212 self
.report_warning(err_note
, item_id
)
5214 _SEARCH_PARAMS
= None
5216 def _search_results(self
, query
, params
=NO_DEFAULT
, default_client
='web'):
5217 data
= {'query': query}
5218 if params
is NO_DEFAULT
:
5219 params
= self
._SEARCH
_PARAMS
5221 data
['params'] = params
5224 ('contents', 'twoColumnSearchResultsRenderer', 'primaryContents', 'sectionListRenderer', 'contents'),
5225 ('onResponseReceivedCommands', 0, 'appendContinuationItemsAction', 'continuationItems'),
5227 ('contents', 'tabbedSearchResultsRenderer', 'tabs', 0, 'tabRenderer', 'content', 'sectionListRenderer', 'contents'),
5228 ('continuationContents', ),
5230 display_id
= f
'query "{query}"'
5231 check_get_keys
= tuple({keys[0] for keys in content_keys}
)
5232 ytcfg
= self
._download
_ytcfg
(default_client
, display_id
) if not self
.skip_webpage
else {}
5233 self
._report
_playlist
_authcheck
(ytcfg
, fatal
=False)
5235 continuation_list
= [None]
5237 for page_num
in itertools
.count(1):
5238 data
.update(continuation_list
[0] or {})
5239 headers
= self
.generate_api_headers(
5240 ytcfg
=ytcfg
, visitor_data
=self
._extract
_visitor
_data
(search
), default_client
=default_client
)
5241 search
= self
._extract
_response
(
5242 item_id
=f
'{display_id} page {page_num}', ep
='search', query
=data
,
5243 default_client
=default_client
, check_get_keys
=check_get_keys
, ytcfg
=ytcfg
, headers
=headers
)
5244 slr_contents
= traverse_obj(search
, *content_keys
)
5245 yield from self
._extract
_entries
({'contents': list(variadic(slr_contents))}
, continuation_list
)
5246 if not continuation_list
[0]:
5250 class YoutubeTabIE(YoutubeTabBaseInfoExtractor
):
5251 IE_DESC
= 'YouTube Tabs'
5252 _VALID_URL
= r
'''(?x:
5254 (?!consent\.)(?:\w+\.)?
5256 youtube(?:kids)?\.com|
5260 (?P<channel_type>channel|c|user|browse)/|
5263 (?:playlist|watch)\?.*?\blist=
5265 (?!(?:%(reserved_names)s)\b) # Direct URLs
5269 'reserved_names': YoutubeBaseInfoExtractor
._RESERVED
_NAMES
,
5270 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
5272 IE_NAME
= 'youtube:tab'
5275 'note': 'playlists, multipage',
5276 'url': 'https://www.youtube.com/c/ИгорьКлейнер/playlists?view=1&flow=grid',
5277 'playlist_mincount': 94,
5279 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
5280 'title': 'Igor Kleiner - Playlists',
5281 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
5282 'uploader': 'Igor Kleiner',
5283 'uploader_id': '@IgorDataScience',
5284 'uploader_url': 'https://www.youtube.com/@IgorDataScience',
5285 'channel': 'Igor Kleiner',
5286 'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
5287 'tags': ['"критическое', 'мышление"', '"наука', 'просто"', 'математика', '"анализ', 'данных"'],
5288 'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
5289 'channel_follower_count': int
5292 'note': 'playlists, multipage, different order',
5293 'url': 'https://www.youtube.com/user/igorkle1/playlists?view=1&sort=dd',
5294 'playlist_mincount': 94,
5296 'id': 'UCqj7Cz7revf5maW9g5pgNcg',
5297 'title': 'Igor Kleiner - Playlists',
5298 'description': 'md5:be97ee0f14ee314f1f002cf187166ee2',
5299 'uploader': 'Igor Kleiner',
5300 'uploader_id': '@IgorDataScience',
5301 'uploader_url': 'https://www.youtube.com/@IgorDataScience',
5302 'tags': ['"критическое', 'мышление"', '"наука', 'просто"', 'математика', '"анализ', 'данных"'],
5303 'channel_id': 'UCqj7Cz7revf5maW9g5pgNcg',
5304 'channel': 'Igor Kleiner',
5305 'channel_url': 'https://www.youtube.com/channel/UCqj7Cz7revf5maW9g5pgNcg',
5306 'channel_follower_count': int
5309 'note': 'playlists, series',
5310 'url': 'https://www.youtube.com/c/3blue1brown/playlists?view=50&sort=dd&shelf_id=3',
5311 'playlist_mincount': 5,
5313 'id': 'UCYO_jab_esuFRV4b17AJtAw',
5314 'title': '3Blue1Brown - Playlists',
5315 'description': 'md5:e1384e8a133307dd10edee76e875d62f',
5316 'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
5317 'channel': '3Blue1Brown',
5318 'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
5319 'uploader_id': '@3blue1brown',
5320 'uploader_url': 'https://www.youtube.com/@3blue1brown',
5321 'uploader': '3Blue1Brown',
5322 'tags': ['Mathematics'],
5323 'channel_follower_count': int
5326 'note': 'playlists, singlepage',
5327 'url': 'https://www.youtube.com/user/ThirstForScience/playlists',
5328 'playlist_mincount': 4,
5330 'id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
5331 'title': 'ThirstForScience - Playlists',
5332 'description': 'md5:609399d937ea957b0f53cbffb747a14c',
5333 'uploader': 'ThirstForScience',
5334 'uploader_url': 'https://www.youtube.com/@ThirstForScience',
5335 'uploader_id': '@ThirstForScience',
5336 'channel_id': 'UCAEtajcuhQ6an9WEzY9LEMQ',
5337 'channel_url': 'https://www.youtube.com/channel/UCAEtajcuhQ6an9WEzY9LEMQ',
5339 'channel': 'ThirstForScience',
5340 'channel_follower_count': int
5343 'url': 'https://www.youtube.com/c/ChristophLaimer/playlists',
5344 'only_matching': True,
5346 'note': 'basic, single video playlist',
5347 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5349 'id': 'PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5350 'title': 'youtube-dl public playlist',
5354 'modified_date': '20201130',
5355 'channel': 'Sergey M.',
5356 'channel_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5357 'channel_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5358 'availability': 'public',
5359 'uploader': 'Sergey M.',
5360 'uploader_url': 'https://www.youtube.com/@sergeym.6173',
5361 'uploader_id': '@sergeym.6173',
5363 'playlist_count': 1,
5365 'note': 'empty playlist',
5366 'url': 'https://www.youtube.com/playlist?list=PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
5368 'id': 'PL4lCao7KL_QFodcLWhDpGCYnngnHtQ-Xf',
5369 'title': 'youtube-dl empty playlist',
5371 'channel': 'Sergey M.',
5373 'modified_date': '20160902',
5374 'channel_id': 'UCmlqkdCBesrv2Lak1mF_MxA',
5375 'channel_url': 'https://www.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5376 'availability': 'public',
5377 'uploader_url': 'https://www.youtube.com/@sergeym.6173',
5378 'uploader_id': '@sergeym.6173',
5379 'uploader': 'Sergey M.',
5381 'playlist_count': 0,
5384 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/featured',
5386 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5387 'title': 'lex will - Home',
5388 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5389 'uploader': 'lex will',
5390 'uploader_id': '@lexwill718',
5391 'channel': 'lex will',
5392 'tags': ['bible', 'history', 'prophesy'],
5393 'uploader_url': 'https://www.youtube.com/@lexwill718',
5394 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5395 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5396 'channel_follower_count': int
5398 'playlist_mincount': 2,
5400 'note': 'Videos tab',
5401 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos',
5403 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5404 'title': 'lex will - Videos',
5405 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5406 'uploader': 'lex will',
5407 'uploader_id': '@lexwill718',
5408 'tags': ['bible', 'history', 'prophesy'],
5409 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5410 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5411 'uploader_url': 'https://www.youtube.com/@lexwill718',
5412 'channel': 'lex will',
5413 'channel_follower_count': int
5415 'playlist_mincount': 975,
5417 'note': 'Videos tab, sorted by popular',
5418 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/videos?view=0&sort=p&flow=grid',
5420 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5421 'title': 'lex will - Videos',
5422 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5423 'uploader': 'lex will',
5424 'uploader_id': '@lexwill718',
5425 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5426 'uploader_url': 'https://www.youtube.com/@lexwill718',
5427 'channel': 'lex will',
5428 'tags': ['bible', 'history', 'prophesy'],
5429 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5430 'channel_follower_count': int
5432 'playlist_mincount': 199,
5434 'note': 'Playlists tab',
5435 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/playlists',
5437 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5438 'title': 'lex will - Playlists',
5439 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5440 'uploader': 'lex will',
5441 'uploader_id': '@lexwill718',
5442 'uploader_url': 'https://www.youtube.com/@lexwill718',
5443 'channel': 'lex will',
5444 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5445 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5446 'tags': ['bible', 'history', 'prophesy'],
5447 'channel_follower_count': int
5449 'playlist_mincount': 17,
5451 'note': 'Community tab',
5452 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/community',
5454 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5455 'title': 'lex will - Community',
5456 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5457 'channel': 'lex will',
5458 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5459 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5460 'tags': ['bible', 'history', 'prophesy'],
5461 'channel_follower_count': int,
5462 'uploader_url': 'https://www.youtube.com/@lexwill718',
5463 'uploader_id': '@lexwill718',
5464 'uploader': 'lex will',
5466 'playlist_mincount': 18,
5468 'note': 'Channels tab',
5469 'url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w/channels',
5471 'id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5472 'title': 'lex will - Channels',
5473 'description': 'md5:2163c5d0ff54ed5f598d6a7e6211e488',
5474 'channel': 'lex will',
5475 'channel_url': 'https://www.youtube.com/channel/UCKfVa3S1e4PHvxWcwyMMg8w',
5476 'channel_id': 'UCKfVa3S1e4PHvxWcwyMMg8w',
5477 'tags': ['bible', 'history', 'prophesy'],
5478 'channel_follower_count': int,
5479 'uploader_url': 'https://www.youtube.com/@lexwill718',
5480 'uploader_id': '@lexwill718',
5481 'uploader': 'lex will',
5483 'playlist_mincount': 12,
5485 'note': 'Search tab',
5486 'url': 'https://www.youtube.com/c/3blue1brown/search?query=linear%20algebra',
5487 'playlist_mincount': 40,
5489 'id': 'UCYO_jab_esuFRV4b17AJtAw',
5490 'title': '3Blue1Brown - Search - linear algebra',
5491 'description': 'md5:e1384e8a133307dd10edee76e875d62f',
5492 'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
5493 'tags': ['Mathematics'],
5494 'channel': '3Blue1Brown',
5495 'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
5496 'channel_follower_count': int,
5497 'uploader_url': 'https://www.youtube.com/@3blue1brown',
5498 'uploader_id': '@3blue1brown',
5499 'uploader': '3Blue1Brown',
5502 'url': 'https://invidio.us/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5503 'only_matching': True,
5505 'url': 'https://www.youtubekids.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5506 'only_matching': True,
5508 'url': 'https://music.youtube.com/channel/UCmlqkdCBesrv2Lak1mF_MxA',
5509 'only_matching': True,
5511 'note': 'Playlist with deleted videos (#651). As a bonus, the video #51 is also twice in this list.',
5512 'url': 'https://www.youtube.com/playlist?list=PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
5514 'title': '29C3: Not my department',
5515 'id': 'PLwP_SiAcdui0KVebT0mU9Apz359a4ubsC',
5516 'description': 'md5:a14dc1a8ef8307a9807fe136a0660268',
5519 'modified_date': '20150605',
5520 'channel_id': 'UCEPzS1rYsrkqzSLNp76nrcg',
5521 'channel_url': 'https://www.youtube.com/channel/UCEPzS1rYsrkqzSLNp76nrcg',
5522 'channel': 'Christiaan008',
5523 'availability': 'public',
5524 'uploader_id': '@ChRiStIaAn008',
5525 'uploader': 'Christiaan008',
5526 'uploader_url': 'https://www.youtube.com/@ChRiStIaAn008',
5528 'playlist_count': 96,
5530 'note': 'Large playlist',
5531 'url': 'https://www.youtube.com/playlist?list=UUBABnxM4Ar9ten8Mdjj1j0Q',
5533 'title': 'Uploads from Cauchemar',
5534 'id': 'UUBABnxM4Ar9ten8Mdjj1j0Q',
5535 'channel_url': 'https://www.youtube.com/channel/UCBABnxM4Ar9ten8Mdjj1j0Q',
5537 'modified_date': r
're:\d{8}',
5538 'channel': 'Cauchemar',
5541 'channel_id': 'UCBABnxM4Ar9ten8Mdjj1j0Q',
5542 'availability': 'public',
5543 'uploader_id': '@Cauchemar89',
5544 'uploader': 'Cauchemar',
5545 'uploader_url': 'https://www.youtube.com/@Cauchemar89',
5547 'playlist_mincount': 1123,
5548 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5550 'note': 'even larger playlist, 8832 videos',
5551 'url': 'http://www.youtube.com/user/NASAgovVideo/videos',
5552 'only_matching': True,
5554 'note': 'Buggy playlist: the webpage has a "Load more" button but it doesn\'t have more videos',
5555 'url': 'https://www.youtube.com/playlist?list=UUXw-G3eDE9trcvY2sBMM_aA',
5557 'title': 'Uploads from Interstellar Movie',
5558 'id': 'UUXw-G3eDE9trcvY2sBMM_aA',
5561 'channel_id': 'UCXw-G3eDE9trcvY2sBMM_aA',
5562 'channel_url': 'https://www.youtube.com/channel/UCXw-G3eDE9trcvY2sBMM_aA',
5563 'channel': 'Interstellar Movie',
5565 'modified_date': r
're:\d{8}',
5566 'availability': 'public',
5567 'uploader_id': '@InterstellarMovie',
5568 'uploader': 'Interstellar Movie',
5569 'uploader_url': 'https://www.youtube.com/@InterstellarMovie',
5571 'playlist_mincount': 21,
5573 'note': 'Playlist with "show unavailable videos" button',
5574 'url': 'https://www.youtube.com/playlist?list=UUTYLiWFZy8xtPwxFwX9rV7Q',
5576 'title': 'Uploads from Phim Siêu Nhân Nhật Bản',
5577 'id': 'UUTYLiWFZy8xtPwxFwX9rV7Q',
5579 'channel': 'Phim Siêu Nhân Nhật Bản',
5582 'channel_url': 'https://www.youtube.com/channel/UCTYLiWFZy8xtPwxFwX9rV7Q',
5583 'channel_id': 'UCTYLiWFZy8xtPwxFwX9rV7Q',
5584 'modified_date': r
're:\d{8}',
5585 'availability': 'public',
5586 'uploader_url': 'https://www.youtube.com/@phimsieunhannhatban',
5587 'uploader_id': '@phimsieunhannhatban',
5588 'uploader': 'Phim Siêu Nhân Nhật Bản',
5590 'playlist_mincount': 200,
5591 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5593 'note': 'Playlist with unavailable videos in page 7',
5594 'url': 'https://www.youtube.com/playlist?list=UU8l9frL61Yl5KFOl87nIm2w',
5596 'title': 'Uploads from BlankTV',
5597 'id': 'UU8l9frL61Yl5KFOl87nIm2w',
5598 'channel': 'BlankTV',
5599 'channel_url': 'https://www.youtube.com/channel/UC8l9frL61Yl5KFOl87nIm2w',
5600 'channel_id': 'UC8l9frL61Yl5KFOl87nIm2w',
5603 'modified_date': r
're:\d{8}',
5605 'availability': 'public',
5606 'uploader_id': '@blanktv',
5607 'uploader': 'BlankTV',
5608 'uploader_url': 'https://www.youtube.com/@blanktv',
5610 'playlist_mincount': 1000,
5611 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
5613 'note': 'https://github.com/ytdl-org/youtube-dl/issues/21844',
5614 'url': 'https://www.youtube.com/playlist?list=PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
5616 'title': 'Data Analysis with Dr Mike Pound',
5617 'id': 'PLzH6n4zXuckpfMu_4Ff8E7Z1behQks5ba',
5618 'description': 'md5:7f567c574d13d3f8c0954d9ffee4e487',
5621 'channel_id': 'UC9-y-6csu5WGm29I7JiwpnA',
5622 'channel_url': 'https://www.youtube.com/channel/UC9-y-6csu5WGm29I7JiwpnA',
5623 'channel': 'Computerphile',
5624 'availability': 'public',
5625 'modified_date': '20190712',
5626 'uploader_id': '@Computerphile',
5627 'uploader': 'Computerphile',
5628 'uploader_url': 'https://www.youtube.com/@Computerphile',
5630 'playlist_mincount': 11,
5632 'url': 'https://invidio.us/playlist?list=PL4lCao7KL_QFVb7Iudeipvc2BCavECqzc',
5633 'only_matching': True,
5635 'note': 'Playlist URL that does not actually serve a playlist',
5636 'url': 'https://www.youtube.com/watch?v=FqZTN594JQw&list=PLMYEtVRpaqY00V9W81Cwmzp6N6vZqfUKD4',
5638 'id': 'FqZTN594JQw',
5640 'title': "Smiley's People 01 detective, Adventure Series, Action",
5641 'upload_date': '20150526',
5642 'license': 'Standard YouTube License',
5643 'description': 'md5:507cdcb5a49ac0da37a920ece610be80',
5644 'categories': ['People & Blogs'],
5650 'skip_download': True,
5652 'skip': 'This video is not available.',
5653 'add_ie': [YoutubeIE
.ie_key()],
5655 'url': 'https://www.youtubekids.com/watch?v=Agk7R8I8o5U&list=PUZ6jURNr1WQZCNHF0ao-c0g',
5656 'only_matching': True,
5658 'url': 'https://www.youtube.com/watch?v=MuAGGZNfUkU&list=RDMM',
5659 'only_matching': True,
5661 'url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ/live',
5663 'id': 'AlTsmyW4auo', # This will keep changing
5666 'upload_date': r
're:\d{8}',
5668 'categories': ['News & Politics'],
5671 'release_timestamp': int,
5672 'channel': 'Sky News',
5673 'channel_id': 'UCoMdktPbSTixAyNGwb-UYkQ',
5676 'thumbnail': r
're:https?://i\.ytimg\.com/vi/[^/]+/maxresdefault(?:_live)?\.jpg',
5677 'playable_in_embed': True,
5678 'release_date': r
're:\d+',
5679 'availability': 'public',
5680 'live_status': 'is_live',
5681 'channel_url': 'https://www.youtube.com/channel/UCoMdktPbSTixAyNGwb-UYkQ',
5682 'channel_follower_count': int,
5683 'concurrent_view_count': int,
5684 'uploader_url': 'https://www.youtube.com/@SkyNews',
5685 'uploader_id': '@SkyNews',
5686 'uploader': 'Sky News',
5689 'skip_download': True,
5691 'expected_warnings': ['Ignoring subtitle tracks found in '],
5693 'url': 'https://www.youtube.com/user/TheYoungTurks/live',
5695 'id': 'a48o2S1cPoo',
5697 'title': 'The Young Turks - Live Main Show',
5698 'upload_date': '20150715',
5699 'license': 'Standard YouTube License',
5700 'description': 'md5:438179573adcdff3c97ebb1ee632b891',
5701 'categories': ['News & Politics'],
5702 'tags': ['Cenk Uygur (TV Program Creator)', 'The Young Turks (Award-Winning Work)', 'Talk Show (TV Genre)'],
5706 'skip_download': True,
5708 'only_matching': True,
5710 'url': 'https://www.youtube.com/channel/UC1yBKRuGpC1tSM73A0ZjYjQ/live',
5711 'only_matching': True,
5713 'url': 'https://www.youtube.com/c/CommanderVideoHq/live',
5714 'only_matching': True,
5716 'note': 'A channel that is not live. Should raise error',
5717 'url': 'https://www.youtube.com/user/numberphile/live',
5718 'only_matching': True,
5720 'url': 'https://www.youtube.com/feed/trending',
5721 'only_matching': True,
5723 'url': 'https://www.youtube.com/feed/library',
5724 'only_matching': True,
5726 'url': 'https://www.youtube.com/feed/history',
5727 'only_matching': True,
5729 'url': 'https://www.youtube.com/feed/subscriptions',
5730 'only_matching': True,
5732 'url': 'https://www.youtube.com/feed/watch_later',
5733 'only_matching': True,
5735 'note': 'Recommended - redirects to home page.',
5736 'url': 'https://www.youtube.com/feed/recommended',
5737 'only_matching': True,
5739 'note': 'inline playlist with not always working continuations',
5740 'url': 'https://www.youtube.com/watch?v=UC6u0Tct-Fo&list=PL36D642111D65BE7C',
5741 'only_matching': True,
5743 'url': 'https://www.youtube.com/course',
5744 'only_matching': True,
5746 'url': 'https://www.youtube.com/zsecurity',
5747 'only_matching': True,
5749 'url': 'http://www.youtube.com/NASAgovVideo/videos',
5750 'only_matching': True,
5752 'url': 'https://www.youtube.com/TheYoungTurks/live',
5753 'only_matching': True,
5755 'url': 'https://www.youtube.com/hashtag/cctv9',
5761 'playlist_mincount': 300, # not consistent but should be over 300
5763 'url': 'https://www.youtube.com/watch?list=PLW4dVinRY435CBE_JD3t-0SRXKfnZHS1P&feature=youtu.be&v=M9cJMXmQ_ZU',
5764 'only_matching': True,
5766 'note': 'Requires Premium: should request additional YTM-info webpage (and have format 141) for videos in playlist',
5767 'url': 'https://music.youtube.com/playlist?list=PLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5768 'only_matching': True
5770 'note': '/browse/ should redirect to /channel/',
5771 'url': 'https://music.youtube.com/browse/UC1a8OFewdjuLq6KlF8M_8Ng',
5772 'only_matching': True
5774 'note': 'VLPL, should redirect to playlist?list=PL...',
5775 'url': 'https://music.youtube.com/browse/VLPLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5777 'id': 'PLRBp0Fe2GpgmgoscNFLxNyBVSFVdYmFkq',
5778 'description': 'Providing you with copyright free / safe music for gaming, live streaming, studying and more!',
5779 'title': 'NCS : All Releases 💿',
5780 'channel_url': 'https://www.youtube.com/channel/UC_aEa8K-EOJ3D6gOs7HcyNg',
5781 'modified_date': r
're:\d{8}',
5783 'channel_id': 'UC_aEa8K-EOJ3D6gOs7HcyNg',
5785 'channel': 'NoCopyrightSounds',
5786 'availability': 'public',
5787 'uploader_url': 'https://www.youtube.com/@NoCopyrightSounds',
5788 'uploader': 'NoCopyrightSounds',
5789 'uploader_id': '@NoCopyrightSounds',
5791 'playlist_mincount': 166,
5792 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden', 'YouTube Music is not directly supported'],
5794 # TODO: fix 'unviewable' issue with this playlist when reloading with unavailable videos
5795 'note': 'Topic, should redirect to playlist?list=UU...',
5796 'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
5798 'id': 'UU9ALqqC4aIeG5iDs7i90Bfw',
5799 'title': 'Uploads from Royalty Free Music - Topic',
5801 'channel_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5802 'channel': 'Royalty Free Music - Topic',
5804 'channel_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5805 'modified_date': r
're:\d{8}',
5807 'availability': 'public',
5808 'uploader': 'Royalty Free Music - Topic',
5810 'playlist_mincount': 101,
5811 'expected_warnings': ['YouTube Music is not directly supported', r
'[Uu]navailable videos (are|will be) hidden'],
5813 # Destination channel with only a hidden self tab (tab id is UCtFRv9O2AHqOZjjynzrv-xg)
5814 # Treat as a general feed
5815 'url': 'https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg',
5817 'id': 'UCtFRv9O2AHqOZjjynzrv-xg',
5818 'title': 'UCtFRv9O2AHqOZjjynzrv-xg',
5821 'playlist_mincount': 9,
5823 'note': 'Youtube music Album',
5824 'url': 'https://music.youtube.com/browse/MPREb_gTAcphH99wE',
5826 'id': 'OLAK5uy_l1m0thk3g31NmIIz_vMIbWtyv7eZixlH0',
5827 'title': 'Album - Royalty Free Music Library V2 (50 Songs)',
5831 'availability': 'unlisted',
5832 'modified_date': r
're:\d{8}',
5834 'playlist_count': 50,
5835 'expected_warnings': ['YouTube Music is not directly supported'],
5837 'note': 'unlisted single video playlist',
5838 'url': 'https://www.youtube.com/playlist?list=PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
5840 'id': 'PLwL24UFy54GrB3s2KMMfjZscDi1x5Dajf',
5841 'title': 'yt-dlp unlisted playlist test',
5842 'availability': 'unlisted',
5844 'modified_date': '20220418',
5845 'channel': 'colethedj',
5848 'channel_id': 'UC9zHu_mHU96r19o-wV5Qs1Q',
5849 'channel_url': 'https://www.youtube.com/channel/UC9zHu_mHU96r19o-wV5Qs1Q',
5850 'uploader_url': 'https://www.youtube.com/@colethedj1894',
5851 'uploader_id': '@colethedj1894',
5852 'uploader': 'colethedj',
5854 'playlist_count': 1,
5856 'note': 'API Fallback: Recommended - redirects to home page. Requires visitorData',
5857 'url': 'https://www.youtube.com/feed/recommended',
5859 'id': 'recommended',
5860 'title': 'recommended',
5863 'playlist_mincount': 50,
5865 'skip_download': True,
5866 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5869 'note': 'API Fallback: /videos tab, sorted by oldest first',
5870 'url': 'https://www.youtube.com/user/theCodyReeder/videos?view=0&sort=da&flow=grid',
5872 'id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
5873 'title': 'Cody\'sLab - Videos',
5874 'description': 'md5:d083b7c2f0c67ee7a6c74c3e9b4243fa',
5875 'channel': 'Cody\'sLab',
5876 'channel_id': 'UCu6mSoMNzHQiBIOCkHUa2Aw',
5878 'channel_url': 'https://www.youtube.com/channel/UCu6mSoMNzHQiBIOCkHUa2Aw',
5879 'channel_follower_count': int
5881 'playlist_mincount': 650,
5883 'skip_download': True,
5884 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5886 'skip': 'Query for sorting no longer works',
5888 'note': 'API Fallback: Topic, should redirect to playlist?list=UU...',
5889 'url': 'https://music.youtube.com/browse/UC9ALqqC4aIeG5iDs7i90Bfw',
5891 'id': 'UU9ALqqC4aIeG5iDs7i90Bfw',
5892 'title': 'Uploads from Royalty Free Music - Topic',
5893 'modified_date': r
're:\d{8}',
5894 'channel_id': 'UC9ALqqC4aIeG5iDs7i90Bfw',
5896 'channel_url': 'https://www.youtube.com/channel/UC9ALqqC4aIeG5iDs7i90Bfw',
5898 'channel': 'Royalty Free Music - Topic',
5900 'availability': 'public',
5901 'uploader': 'Royalty Free Music - Topic',
5903 'playlist_mincount': 101,
5905 'skip_download': True,
5906 'extractor_args': {'youtubetab': {'skip': ['webpage']}
}
5908 'expected_warnings': ['YouTube Music is not directly supported', r
'[Uu]navailable videos (are|will be) hidden'],
5910 'note': 'non-standard redirect to regional channel',
5911 'url': 'https://www.youtube.com/channel/UCwVVpHQ2Cs9iGJfpdFngePQ',
5912 'only_matching': True
5914 'note': 'collaborative playlist (uploader name in the form "by <uploader> and x other(s)")',
5915 'url': 'https://www.youtube.com/playlist?list=PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
5917 'id': 'PLx-_-Kk4c89oOHEDQAojOXzEzemXxoqx6',
5918 'modified_date': '20220407',
5919 'channel_url': 'https://www.youtube.com/channel/UCKcqXmCcyqnhgpA5P0oHH_Q',
5921 'availability': 'unlisted',
5922 'channel_id': 'UCKcqXmCcyqnhgpA5P0oHH_Q',
5923 'channel': 'pukkandan',
5924 'description': 'Test for collaborative playlist',
5925 'title': 'yt-dlp test - collaborative playlist',
5927 'uploader_url': 'https://www.youtube.com/@pukkandan',
5928 'uploader_id': '@pukkandan',
5929 'uploader': 'pukkandan',
5931 'playlist_mincount': 2
5933 'note': 'translated tab name',
5934 'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/playlists',
5936 'id': 'UCiu-3thuViMebBjw_5nWYrA',
5938 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5939 'description': 'test description',
5940 'title': 'cole-dlp-test-acc - 再生リスト',
5941 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
5942 'channel': 'cole-dlp-test-acc',
5943 'uploader_url': 'https://www.youtube.com/@coletdjnz',
5944 'uploader_id': '@coletdjnz',
5945 'uploader': 'cole-dlp-test-acc',
5947 'playlist_mincount': 1,
5948 'params': {'extractor_args': {'youtube': {'lang': ['ja']}
}},
5949 'expected_warnings': ['Preferring "ja"'],
5951 # XXX: this should really check flat playlist entries, but the test suite doesn't support that
5952 'note': 'preferred lang set with playlist with translated video titles',
5953 'url': 'https://www.youtube.com/playlist?list=PLt5yu3-wZAlQAaPZ5Z-rJoTdbT-45Q7c0',
5955 'id': 'PLt5yu3-wZAlQAaPZ5Z-rJoTdbT-45Q7c0',
5958 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
5959 'channel': 'cole-dlp-test-acc',
5960 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
5961 'description': 'test',
5962 'title': 'dlp test playlist',
5963 'availability': 'public',
5964 'uploader_url': 'https://www.youtube.com/@coletdjnz',
5965 'uploader_id': '@coletdjnz',
5966 'uploader': 'cole-dlp-test-acc',
5968 'playlist_mincount': 1,
5969 'params': {'extractor_args': {'youtube': {'lang': ['ja']}
}},
5970 'expected_warnings': ['Preferring "ja"'],
5972 # shorts audio pivot for 2GtVksBMYFM.
5973 'url': 'https://www.youtube.com/feed/sfv_audio_pivot?bp=8gUrCikSJwoLMkd0VmtzQk1ZRk0SCzJHdFZrc0JNWUZNGgsyR3RWa3NCTVlGTQ==',
5975 'id': 'sfv_audio_pivot',
5976 'title': 'sfv_audio_pivot',
5979 'playlist_mincount': 50,
5982 # Channel with a real live tab (not to be mistaken with streams tab)
5983 # Do not treat like it should redirect to live stream
5984 'url': 'https://www.youtube.com/channel/UCEH7P7kyJIkS_gJf93VYbmg/live',
5986 'id': 'UCEH7P7kyJIkS_gJf93VYbmg',
5987 'title': 'UCEH7P7kyJIkS_gJf93VYbmg - Live',
5990 'playlist_mincount': 20,
5992 # Tab name is not the same as tab id
5993 'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/letsplay',
5995 'id': 'UCQvWX73GQygcwXOTSf_VDVg',
5996 'title': 'UCQvWX73GQygcwXOTSf_VDVg - Let\'s play',
5999 'playlist_mincount': 8,
6001 # Home tab id is literally home. Not to get mistaken with featured
6002 'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/home',
6004 'id': 'UCQvWX73GQygcwXOTSf_VDVg',
6005 'title': 'UCQvWX73GQygcwXOTSf_VDVg - Home',
6008 'playlist_mincount': 8,
6010 # Should get three playlists for videos, shorts and streams tabs
6011 'url': 'https://www.youtube.com/channel/UCK9V2B22uJYu3N7eR_BT9QA',
6013 'id': 'UCK9V2B22uJYu3N7eR_BT9QA',
6014 'title': 'Polka Ch. 尾丸ポルカ',
6015 'channel_follower_count': int,
6016 'channel_id': 'UCK9V2B22uJYu3N7eR_BT9QA',
6017 'channel_url': 'https://www.youtube.com/channel/UCK9V2B22uJYu3N7eR_BT9QA',
6018 'description': 'md5:e56b74b5bb7e9c701522162e9abfb822',
6019 'channel': 'Polka Ch. 尾丸ポルカ',
6021 'uploader_url': 'https://www.youtube.com/@OmaruPolka',
6022 'uploader': 'Polka Ch. 尾丸ポルカ',
6023 'uploader_id': '@OmaruPolka',
6025 'playlist_count': 3,
6027 # Shorts tab with channel with handle
6028 # TODO: fix channel description
6029 'url': 'https://www.youtube.com/@NotJustBikes/shorts',
6031 'id': 'UC0intLFzLaudFG-xAvUEO-A',
6032 'title': 'Not Just Bikes - Shorts',
6034 'channel_url': 'https://www.youtube.com/channel/UC0intLFzLaudFG-xAvUEO-A',
6035 'description': 'md5:26bc55af26855a608a5cf89dfa595c8d',
6036 'channel_follower_count': int,
6037 'channel_id': 'UC0intLFzLaudFG-xAvUEO-A',
6038 'channel': 'Not Just Bikes',
6039 'uploader_url': 'https://www.youtube.com/@NotJustBikes',
6040 'uploader': 'Not Just Bikes',
6041 'uploader_id': '@NotJustBikes',
6043 'playlist_mincount': 10,
6046 'url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig/streams',
6048 'id': 'UC3eYAvjCVwNHgkaGbXX3sig',
6049 'title': '中村悠一 - Live',
6051 'channel_id': 'UC3eYAvjCVwNHgkaGbXX3sig',
6052 'channel_url': 'https://www.youtube.com/channel/UC3eYAvjCVwNHgkaGbXX3sig',
6054 'channel_follower_count': int,
6055 'description': 'md5:e744f6c93dafa7a03c0c6deecb157300',
6056 'uploader_url': 'https://www.youtube.com/@Yuichi-Nakamura',
6057 'uploader_id': '@Yuichi-Nakamura',
6060 'playlist_mincount': 60,
6062 # Channel with no uploads and hence no videos, streams, shorts tabs or uploads playlist. This should fail.
6063 # See test_youtube_lists
6064 'url': 'https://www.youtube.com/channel/UC2yXPzFejc422buOIzn_0CA',
6065 'only_matching': True,
6067 # No uploads and no UCID given. Should fail with no uploads error
6068 # See test_youtube_lists
6069 'url': 'https://www.youtube.com/news',
6070 'only_matching': True
6072 # No videos tab but has a shorts tab
6073 'url': 'https://www.youtube.com/c/TKFShorts',
6075 'id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
6076 'title': 'Shorts Break - Shorts',
6078 'channel_id': 'UCgJ5_1F6yJhYLnyMszUdmUg',
6079 'channel': 'Shorts Break',
6080 'description': 'md5:6de33c5e7ba686e5f3efd4e19c7ef499',
6081 'channel_follower_count': int,
6082 'channel_url': 'https://www.youtube.com/channel/UCgJ5_1F6yJhYLnyMszUdmUg',
6083 'uploader_url': 'https://www.youtube.com/@ShortsBreak_Official',
6084 'uploader': 'Shorts Break',
6085 'uploader_id': '@ShortsBreak_Official',
6087 'playlist_mincount': 30,
6089 # Trending Now Tab. tab id is empty
6090 'url': 'https://www.youtube.com/feed/trending',
6093 'title': 'trending - Now',
6096 'playlist_mincount': 30,
6098 # Trending Gaming Tab. tab id is empty
6099 'url': 'https://www.youtube.com/feed/trending?bp=4gIcGhpnYW1pbmdfY29ycHVzX21vc3RfcG9wdWxhcg%3D%3D',
6102 'title': 'trending - Gaming',
6105 'playlist_mincount': 30,
6107 # Shorts url result in shorts tab
6108 # TODO: Fix channel id extraction
6109 'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/shorts',
6111 'id': 'UCiu-3thuViMebBjw_5nWYrA',
6112 'title': 'cole-dlp-test-acc - Shorts',
6113 'channel': 'cole-dlp-test-acc',
6114 'description': 'test description',
6115 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
6116 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
6118 'uploader_url': 'https://www.youtube.com/@coletdjnz',
6119 'uploader_id': '@coletdjnz',
6120 'uploader': 'cole-dlp-test-acc',
6124 # Channel data is not currently available for short renderers (as of 2023-03-01)
6126 'ie_key': 'Youtube',
6127 'url': 'https://www.youtube.com/shorts/sSM9J5YH_60',
6128 'id': 'sSM9J5YH_60',
6129 'title': 'SHORT short',
6134 'params': {'extract_flat': True}
,
6136 # Live video status should be extracted
6137 'url': 'https://www.youtube.com/channel/UCQvWX73GQygcwXOTSf_VDVg/live',
6139 'id': 'UCQvWX73GQygcwXOTSf_VDVg',
6140 'title': 'UCQvWX73GQygcwXOTSf_VDVg - Live', # TODO, should be Minecraft - Live or Minecraft - Topic - Live
6146 'ie_key': 'Youtube',
6147 'url': 'startswith:https://www.youtube.com/watch?v=',
6150 'live_status': 'is_live',
6153 'concurrent_view_count': int,
6157 'params': {'extract_flat': True, 'playlist_items': '1'}
,
6158 'playlist_mincount': 1
6160 # Channel renderer metadata. Contains number of videos on the channel
6161 'url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA/channels',
6163 'id': 'UCiu-3thuViMebBjw_5nWYrA',
6164 'title': 'cole-dlp-test-acc - Channels',
6165 'channel': 'cole-dlp-test-acc',
6166 'description': 'test description',
6167 'channel_id': 'UCiu-3thuViMebBjw_5nWYrA',
6168 'channel_url': 'https://www.youtube.com/channel/UCiu-3thuViMebBjw_5nWYrA',
6170 'uploader_url': 'https://www.youtube.com/@coletdjnz',
6171 'uploader_id': '@coletdjnz',
6172 'uploader': 'cole-dlp-test-acc',
6177 'ie_key': 'YoutubeTab',
6178 'url': 'https://www.youtube.com/channel/UC-lHJZR3Gqxm24_Vd_AJ5Yw',
6179 'id': 'UC-lHJZR3Gqxm24_Vd_AJ5Yw',
6180 'channel_id': 'UC-lHJZR3Gqxm24_Vd_AJ5Yw',
6181 'title': 'PewDiePie',
6182 'channel': 'PewDiePie',
6183 'channel_url': 'https://www.youtube.com/channel/UC-lHJZR3Gqxm24_Vd_AJ5Yw',
6185 'channel_follower_count': int,
6186 'playlist_count': int,
6187 'uploader': 'PewDiePie',
6188 'uploader_url': 'https://www.youtube.com/@PewDiePie',
6189 'uploader_id': '@PewDiePie',
6192 'params': {'extract_flat': True}
,
6194 'url': 'https://www.youtube.com/@3blue1brown/about',
6196 'id': 'UCYO_jab_esuFRV4b17AJtAw',
6197 'tags': ['Mathematics'],
6198 'title': '3Blue1Brown - About',
6199 'channel_follower_count': int,
6200 'channel_id': 'UCYO_jab_esuFRV4b17AJtAw',
6201 'channel': '3Blue1Brown',
6203 'channel_url': 'https://www.youtube.com/channel/UCYO_jab_esuFRV4b17AJtAw',
6204 'description': 'md5:e1384e8a133307dd10edee76e875d62f',
6205 'uploader_url': 'https://www.youtube.com/@3blue1brown',
6206 'uploader_id': '@3blue1brown',
6207 'uploader': '3Blue1Brown',
6209 'playlist_count': 0,
6211 # Podcasts tab, with rich entry playlistRenderers
6212 'url': 'https://www.youtube.com/@99percentinvisiblepodcast/podcasts',
6214 'id': 'UCVMF2HD4ZgC0QHpU9Yq5Xrw',
6215 'channel_id': 'UCVMF2HD4ZgC0QHpU9Yq5Xrw',
6216 'uploader_url': 'https://www.youtube.com/@99percentinvisiblepodcast',
6217 'description': 'md5:3a0ed38f1ad42a68ef0428c04a15695c',
6218 'title': '99 Percent Invisible - Podcasts',
6219 'uploader': '99 Percent Invisible',
6220 'channel_follower_count': int,
6221 'channel_url': 'https://www.youtube.com/channel/UCVMF2HD4ZgC0QHpU9Yq5Xrw',
6223 'channel': '99 Percent Invisible',
6224 'uploader_id': '@99percentinvisiblepodcast',
6226 'playlist_count': 1,
6228 # Releases tab, with rich entry playlistRenderers (same as Podcasts tab)
6229 'url': 'https://www.youtube.com/@AHimitsu/releases',
6231 'id': 'UCgFwu-j5-xNJml2FtTrrB3A',
6232 'channel': 'A Himitsu',
6233 'uploader_url': 'https://www.youtube.com/@AHimitsu',
6234 'title': 'A Himitsu - Releases',
6235 'uploader_id': '@AHimitsu',
6236 'uploader': 'A Himitsu',
6237 'channel_id': 'UCgFwu-j5-xNJml2FtTrrB3A',
6239 'description': 'I make music',
6240 'channel_url': 'https://www.youtube.com/channel/UCgFwu-j5-xNJml2FtTrrB3A',
6241 'channel_follower_count': int,
6243 'playlist_mincount': 10,
6247 def suitable(cls
, url
):
6248 return False if YoutubeIE
.suitable(url
) else super().suitable(url
)
6250 _URL_RE
= re
.compile(rf
'(?P<pre>{_VALID_URL})(?(not_channel)|(?P<tab>/[^?#/]+))?(?P<post>.*)$')
6252 def _get_url_mobj(self
, url
):
6253 mobj
= self
._URL
_RE
.match(url
).groupdict()
6254 mobj
.update((k
, '') for k
, v
in mobj
.items() if v
is None)
6257 def _extract_tab_id_and_name(self
, tab
, base_url
='https://www.youtube.com'):
6258 tab_name
= (tab
.get('title') or '').lower()
6259 tab_url
= urljoin(base_url
, traverse_obj(
6260 tab
, ('endpoint', 'commandMetadata', 'webCommandMetadata', 'url')))
6262 tab_id
= (tab_url
and self
._get
_url
_mobj
(tab_url
)['tab'][1:]
6263 or traverse_obj(tab
, 'tabIdentifier', expected_type
=str))
6266 'TAB_ID_SPONSORSHIPS': 'membership',
6267 }.get(tab_id
, tab_id
), tab_name
6269 # Fallback to tab name if we cannot get the tab id.
6270 # XXX: should we strip non-ascii letters? e.g. in case of 'let's play' tab example on special gaming channel
6271 # Note that in the case of translated tab name this may result in an empty string, which we don't want.
6273 self
.write_debug(f
'Falling back to selected tab name: {tab_name}')
6277 }.get(tab_name
, tab_name
), tab_name
6279 def _has_tab(self
, tabs
, tab_id
):
6280 return any(self
._extract
_tab
_id
_and
_name
(tab
)[0] == tab_id
for tab
in tabs
)
6282 @YoutubeTabBaseInfoExtractor.passthrough_smuggled_data
6283 def _real_extract(self
, url
, smuggled_data
):
6284 item_id
= self
._match
_id
(url
)
6285 url
= urllib
.parse
.urlunparse(
6286 urllib
.parse
.urlparse(url
)._replace
(netloc
='www.youtube.com'))
6287 compat_opts
= self
.get_param('compat_opts', [])
6289 mobj
= self
._get
_url
_mobj
(url
)
6290 pre
, tab
, post
, is_channel
= mobj
['pre'], mobj
['tab'], mobj
['post'], not mobj
['not_channel']
6291 if is_channel
and smuggled_data
.get('is_music_url'):
6292 if item_id
[:2] == 'VL': # Youtube music VL channels have an equivalent playlist
6293 return self
.url_result(
6294 f
'https://music.youtube.com/playlist?list={item_id[2:]}', YoutubeTabIE
, item_id
[2:])
6295 elif item_id
[:2] == 'MP': # Resolve albums (/[channel/browse]/MP...) to their equivalent playlist
6296 mdata
= self
._extract
_tab
_endpoint
(
6297 f
'https://music.youtube.com/channel/{item_id}', item_id
, default_client
='web_music')
6298 murl
= traverse_obj(mdata
, ('microformat', 'microformatDataRenderer', 'urlCanonical'),
6299 get_all
=False, expected_type
=str)
6301 raise ExtractorError('Failed to resolve album to playlist')
6302 return self
.url_result(murl
, YoutubeTabIE
)
6303 elif mobj
['channel_type'] == 'browse': # Youtube music /browse/ should be changed to /channel/
6304 return self
.url_result(
6305 f
'https://music.youtube.com/channel/{item_id}{tab}{post}', YoutubeTabIE
, item_id
)
6307 original_tab_id
, display_id
= tab
[1:], f
'{item_id}{tab}'
6308 if is_channel
and not tab
and 'no-youtube-channel-redirect' not in compat_opts
:
6309 url
= f
'{pre}/videos{post}'
6310 if smuggled_data
.get('is_music_url'):
6311 self
.report_warning(f
'YouTube Music is not directly supported. Redirecting to {url}')
6313 # Handle both video/playlist URLs
6315 video_id
, playlist_id
= [traverse_obj(qs
, (key
, 0)) for key
in ('v', 'list')]
6316 if not video_id
and mobj
['not_channel'].startswith('watch'):
6318 # If there is neither video or playlist ids, youtube redirects to home page, which is undesirable
6319 raise ExtractorError('A video URL was given without video ID', expected
=True)
6320 # Common mistake: https://www.youtube.com/watch?list=playlist_id
6321 self
.report_warning(f
'A video URL was given without video ID. Trying to download playlist {playlist_id}')
6322 return self
.url_result(
6323 f
'https://www.youtube.com/playlist?list={playlist_id}', YoutubeTabIE
, playlist_id
)
6325 if not self
._yes
_playlist
(playlist_id
, video_id
):
6326 return self
.url_result(
6327 f
'https://www.youtube.com/watch?v={video_id}', YoutubeIE
, video_id
)
6329 data
, ytcfg
= self
._extract
_data
(url
, display_id
)
6331 # YouTube may provide a non-standard redirect to the regional channel
6332 # See: https://github.com/yt-dlp/yt-dlp/issues/2694
6333 # https://support.google.com/youtube/answer/2976814#zippy=,conditional-redirects
6334 redirect_url
= traverse_obj(
6335 data
, ('onResponseReceivedActions', ..., 'navigateAction', 'endpoint', 'commandMetadata', 'webCommandMetadata', 'url'), get_all
=False)
6336 if redirect_url
and 'no-youtube-channel-redirect' not in compat_opts
:
6337 redirect_url
= ''.join((urljoin('https://www.youtube.com', redirect_url
), tab
, post
))
6338 self
.to_screen(f
'This playlist is likely not available in your region. Following conditional redirect to {redirect_url}')
6339 return self
.url_result(redirect_url
, YoutubeTabIE
)
6341 tabs
, extra_tabs
= self
._extract
_tab
_renderers
(data
), []
6342 if is_channel
and tabs
and 'no-youtube-channel-redirect' not in compat_opts
:
6343 selected_tab
= self
._extract
_selected
_tab
(tabs
)
6344 selected_tab_id
, selected_tab_name
= self
._extract
_tab
_id
_and
_name
(selected_tab
, url
) # NB: Name may be translated
6345 self
.write_debug(f
'Selected tab: {selected_tab_id!r} ({selected_tab_name}), Requested tab: {original_tab_id!r}')
6347 if not original_tab_id
and selected_tab_name
:
6348 self
.to_screen('Downloading all uploads of the channel. '
6349 'To download only the videos in a specific tab, pass the tab\'s URL')
6350 if self
._has
_tab
(tabs
, 'streams'):
6351 extra_tabs
.append(''.join((pre
, '/streams', post
)))
6352 if self
._has
_tab
(tabs
, 'shorts'):
6353 extra_tabs
.append(''.join((pre
, '/shorts', post
)))
6354 # XXX: Members-only tab should also be extracted
6356 if not extra_tabs
and selected_tab_id
!= 'videos':
6357 # Channel does not have streams, shorts or videos tabs
6358 if item_id
[:2] != 'UC':
6359 raise ExtractorError('This channel has no uploads', expected
=True)
6361 # Topic channels don't have /videos. Use the equivalent playlist instead
6362 pl_id
= f
'UU{item_id[2:]}'
6363 pl_url
= f
'https://www.youtube.com/playlist?list={pl_id}'
6365 data
, ytcfg
= self
._extract
_data
(pl_url
, pl_id
, ytcfg
=ytcfg
, fatal
=True, webpage_fatal
=True)
6366 except ExtractorError
:
6367 raise ExtractorError('This channel has no uploads', expected
=True)
6369 item_id
, url
= pl_id
, pl_url
6371 f
'The channel does not have a videos, shorts, or live tab. Redirecting to playlist {pl_id} instead')
6373 elif extra_tabs
and selected_tab_id
!= 'videos':
6374 # When there are shorts/live tabs but not videos tab
6375 url
, data
= f
'{pre}{post}', None
6377 elif (original_tab_id
or 'videos') != selected_tab_id
:
6378 if original_tab_id
== 'live':
6379 # Live tab should have redirected to the video
6380 # Except in the case the channel has an actual live tab
6381 # Example: https://www.youtube.com/channel/UCEH7P7kyJIkS_gJf93VYbmg/live
6382 raise UserNotLive(video_id
=item_id
)
6383 elif selected_tab_name
:
6384 raise ExtractorError(f
'This channel does not have a {original_tab_id} tab', expected
=True)
6386 # For channels such as https://www.youtube.com/channel/UCtFRv9O2AHqOZjjynzrv-xg
6387 url
= f
'{pre}{post}'
6389 # YouTube sometimes provides a button to reload playlist with unavailable videos.
6390 if 'no-youtube-unavailable-videos' not in compat_opts
:
6391 data
= self
._reload
_with
_unavailable
_videos
(display_id
, data
, ytcfg
) or data
6392 self
._extract
_and
_report
_alerts
(data
, only_once
=True)
6394 tabs
, entries
= self
._extract
_tab
_renderers
(data
), []
6396 entries
= [self
._extract
_from
_tabs
(item_id
, ytcfg
, data
, tabs
)]
6398 'extractor_key': YoutubeTabIE
.ie_key(),
6399 'extractor': YoutubeTabIE
.IE_NAME
,
6402 if self
.get_param('playlist_items') == '0':
6403 entries
.extend(self
.url_result(u
, YoutubeTabIE
) for u
in extra_tabs
)
6404 else: # Users expect to get all `video_id`s even with `--flat-playlist`. So don't return `url_result`
6405 entries
.extend(map(self
._real
_extract
, extra_tabs
))
6407 if len(entries
) == 1:
6410 metadata
= self
._extract
_metadata
_from
_tabs
(item_id
, data
)
6411 uploads_url
= 'the Uploads (UU) playlist URL'
6412 if try_get(metadata
, lambda x
: x
['channel_id'].startswith('UC')):
6413 uploads_url
= f
'https://www.youtube.com/playlist?list=UU{metadata["channel_id"][2:]}'
6415 'Downloading as multiple playlists, separated by tabs. '
6416 f
'To download as a single playlist instead, pass {uploads_url}')
6417 return self
.playlist_result(entries
, item_id
, **metadata
)
6420 playlist
= traverse_obj(
6421 data
, ('contents', 'twoColumnWatchNextResults', 'playlist', 'playlist'), expected_type
=dict)
6423 return self
._extract
_from
_playlist
(item_id
, url
, data
, playlist
, ytcfg
)
6425 video_id
= traverse_obj(
6426 data
, ('currentVideoEndpoint', 'watchEndpoint', 'videoId'), expected_type
=str) or video_id
6428 if tab
!= '/live': # live tab is expected to redirect to video
6429 self
.report_warning(f
'Unable to recognize playlist. Downloading just video {video_id}')
6430 return self
.url_result(f
'https://www.youtube.com/watch?v={video_id}', YoutubeIE
, video_id
)
6432 raise ExtractorError('Unable to recognize tab page')
6435 class YoutubePlaylistIE(InfoExtractor
):
6436 IE_DESC
= 'YouTube playlists'
6437 _VALID_URL
= r
'''(?x)(?:
6442 youtube(?:kids)?\.com|
6447 (?P<id>%(playlist_id)s)
6449 'playlist_id': YoutubeBaseInfoExtractor
._PLAYLIST
_ID
_RE
,
6450 'invidious': '|'.join(YoutubeBaseInfoExtractor
._INVIDIOUS
_SITES
),
6452 IE_NAME
= 'youtube:playlist'
6454 'note': 'issue #673',
6455 'url': 'PLBB231211A4F62143',
6457 'title': '[OLD]Team Fortress 2 (Class-based LP)',
6458 'id': 'PLBB231211A4F62143',
6459 'uploader': 'Wickman',
6460 'uploader_id': '@WickmanVT',
6461 'description': 'md5:8fa6f52abb47a9552002fa3ddfc57fc2',
6463 'uploader_url': 'https://www.youtube.com/@WickmanVT',
6464 'modified_date': r
're:\d{8}',
6465 'channel_id': 'UCKSpbfbl5kRQpTdL7kMc-1Q',
6466 'channel': 'Wickman',
6468 'channel_url': 'https://www.youtube.com/channel/UCKSpbfbl5kRQpTdL7kMc-1Q',
6469 'availability': 'public',
6471 'playlist_mincount': 29,
6473 'url': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
6475 'title': 'YDL_safe_search',
6476 'id': 'PLtPgu7CB4gbY9oDN3drwC3cMbJggS7dKl',
6478 'playlist_count': 2,
6479 'skip': 'This playlist is private',
6482 'url': 'https://www.youtube.com/embed/videoseries?list=PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
6483 'playlist_count': 4,
6486 'id': 'PL6IaIsEjSbf96XFRuNccS_RuEXwNdsoEu',
6487 'uploader': 'milan',
6488 'uploader_id': '@milan5503',
6490 'channel_url': 'https://www.youtube.com/channel/UCEI1-PVPcYXjB73Hfelbmaw',
6492 'modified_date': '20140919',
6495 'channel_id': 'UCEI1-PVPcYXjB73Hfelbmaw',
6496 'uploader_url': 'https://www.youtube.com/@milan5503',
6497 'availability': 'public',
6499 'expected_warnings': [r
'[Uu]navailable videos? (is|are|will be) hidden'],
6501 'url': 'http://www.youtube.com/embed/_xDOZElKyNU?list=PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
6502 'playlist_mincount': 455,
6504 'title': '2018 Chinese New Singles (11/6 updated)',
6505 'id': 'PLsyOSbh5bs16vubvKePAQ1x3PhKavfBIl',
6507 'uploader_id': '@music_king',
6508 'description': 'md5:da521864744d60a198e3a88af4db0d9d',
6511 'channel_url': 'https://www.youtube.com/channel/UC21nz3_MesPLqtDqwdvnoxA',
6513 'uploader_url': 'https://www.youtube.com/@music_king',
6514 'channel_id': 'UC21nz3_MesPLqtDqwdvnoxA',
6515 'modified_date': r
're:\d{8}',
6516 'availability': 'public',
6518 'expected_warnings': [r
'[Uu]navailable videos (are|will be) hidden'],
6520 'url': 'TLGGrESM50VT6acwMjAyMjAxNw',
6521 'only_matching': True,
6523 # music album playlist
6524 'url': 'OLAK5uy_m4xAFdmMC5rX3Ji3g93pQe3hqLZw_9LhM',
6525 'only_matching': True,
6529 def suitable(cls
, url
):
6530 if YoutubeTabIE
.suitable(url
):
6532 from ..utils
import parse_qs
6534 if qs
.get('v', [None])[0]:
6536 return super().suitable(url
)
6538 def _real_extract(self
, url
):
6539 playlist_id
= self
._match
_id
(url
)
6540 is_music_url
= YoutubeBaseInfoExtractor
.is_music_url(url
)
6541 url
= update_url_query(
6542 'https://www.youtube.com/playlist',
6543 parse_qs(url
) or {'list': playlist_id}
)
6545 url
= smuggle_url(url
, {'is_music_url': True}
)
6546 return self
.url_result(url
, ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
6549 class YoutubeYtBeIE(InfoExtractor
):
6550 IE_DESC
= 'youtu.be'
6551 _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}
6553 'url': 'https://youtu.be/yeWKywCrFtk?list=PL2qgrgXsNUG5ig9cat4ohreBjYLAPC0J5',
6555 'id': 'yeWKywCrFtk',
6557 'title': 'Small Scale Baler and Braiding Rugs',
6558 'uploader': 'Backus-Page House Museum',
6559 'uploader_id': '@backuspagemuseum',
6560 'uploader_url': r
're:https?://(?:www\.)?youtube\.com/@backuspagemuseum',
6561 'upload_date': '20161008',
6562 'description': 'md5:800c0c78d5eb128500bffd4f0b4f2e8a',
6563 'categories': ['Nonprofits & Activism'],
6567 'playable_in_embed': True,
6568 'thumbnail': r
're:^https?://.*\.webp',
6569 'channel': 'Backus-Page House Museum',
6570 'channel_id': 'UCEfMCQ9bs3tjvjy1s451zaw',
6571 'live_status': 'not_live',
6573 'channel_url': 'https://www.youtube.com/channel/UCEfMCQ9bs3tjvjy1s451zaw',
6574 'availability': 'public',
6576 'comment_count': int,
6577 'channel_follower_count': int
6581 'skip_download': True,
6584 'url': 'https://youtu.be/uWyaPkt-VOI?list=PL9D9FC436B881BA21',
6585 'only_matching': True,
6588 def _real_extract(self
, url
):
6589 mobj
= self
._match
_valid
_url
(url
)
6590 video_id
= mobj
.group('id')
6591 playlist_id
= mobj
.group('playlist_id')
6592 return self
.url_result(
6593 update_url_query('https://www.youtube.com/watch', {
6595 'list': playlist_id
,
6596 'feature': 'youtu.be',
6597 }), ie
=YoutubeTabIE
.ie_key(), video_id
=playlist_id
)
6600 class YoutubeLivestreamEmbedIE(InfoExtractor
):
6601 IE_DESC
= 'YouTube livestream embeds'
6602 _VALID_URL
= r
'https?://(?:\w+\.)?youtube\.com/embed/live_stream/?\?(?:[^#]+&)?channel=(?P<id>[^&#]+)'
6604 'url': 'https://www.youtube.com/embed/live_stream?channel=UC2_KI6RB__jGdlnK6dvFEZA',
6605 'only_matching': True,
6608 def _real_extract(self
, url
):
6609 channel_id
= self
._match
_id
(url
)
6610 return self
.url_result(
6611 f
'https://www.youtube.com/channel/{channel_id}/live',
6612 ie
=YoutubeTabIE
.ie_key(), video_id
=channel_id
)
6615 class YoutubeYtUserIE(InfoExtractor
):
6616 IE_DESC
= 'YouTube user videos; "ytuser:" prefix'
6617 IE_NAME
= 'youtube:user'
6618 _VALID_URL
= r
'ytuser:(?P<id>.+)'
6620 'url': 'ytuser:phihag',
6621 'only_matching': True,
6624 def _real_extract(self
, url
):
6625 user_id
= self
._match
_id
(url
)
6626 return self
.url_result(f
'https://www.youtube.com/user/{user_id}', YoutubeTabIE
, user_id
)
6629 class YoutubeFavouritesIE(YoutubeBaseInfoExtractor
):
6630 IE_NAME
= 'youtube:favorites'
6631 IE_DESC
= 'YouTube liked videos; ":ytfav" keyword (requires cookies)'
6632 _VALID_URL
= r
':ytfav(?:ou?rite)?s?'
6633 _LOGIN_REQUIRED
= True
6636 'only_matching': True,
6638 'url': ':ytfavorites',
6639 'only_matching': True,
6642 def _real_extract(self
, url
):
6643 return self
.url_result(
6644 'https://www.youtube.com/playlist?list=LL',
6645 ie
=YoutubeTabIE
.ie_key())
6648 class YoutubeNotificationsIE(YoutubeTabBaseInfoExtractor
):
6649 IE_NAME
= 'youtube:notif'
6650 IE_DESC
= 'YouTube notifications; ":ytnotif" keyword (requires cookies)'
6651 _VALID_URL
= r
':ytnotif(?:ication)?s?'
6652 _LOGIN_REQUIRED
= True
6655 'only_matching': True,
6657 'url': ':ytnotifications',
6658 'only_matching': True,
6661 def _extract_notification_menu(self
, response
, continuation_list
):
6662 notification_list
= traverse_obj(
6664 ('actions', 0, 'openPopupAction', 'popup', 'multiPageMenuRenderer', 'sections', 0, 'multiPageMenuNotificationSectionRenderer', 'items'),
6665 ('actions', 0, 'appendContinuationItemsAction', 'continuationItems'),
6666 expected_type
=list) or []
6667 continuation_list
[0] = None
6668 for item
in notification_list
:
6669 entry
= self
._extract
_notification
_renderer
(item
.get('notificationRenderer'))
6672 continuation
= item
.get('continuationItemRenderer')
6674 continuation_list
[0] = continuation
6676 def _extract_notification_renderer(self
, notification
):
6677 video_id
= traverse_obj(
6678 notification
, ('navigationEndpoint', 'watchEndpoint', 'videoId'), expected_type
=str)
6679 url
= f
'https://www.youtube.com/watch?v={video_id}'
6682 browse_ep
= traverse_obj(
6683 notification
, ('navigationEndpoint', 'browseEndpoint'), expected_type
=dict)
6684 channel_id
= self
.ucid_or_none(traverse_obj(browse_ep
, 'browseId', expected_type
=str))
6685 post_id
= self
._search
_regex
(
6686 r
'/post/(.+)', traverse_obj(browse_ep
, 'canonicalBaseUrl', expected_type
=str),
6687 'post id', default
=None)
6688 if not channel_id
or not post_id
:
6690 # The direct /post url redirects to this in the browser
6691 url
= f
'https://www.youtube.com/channel/{channel_id}/community?lb={post_id}'
6693 channel
= traverse_obj(
6694 notification
, ('contextualMenu', 'menuRenderer', 'items', 1, 'menuServiceItemRenderer', 'text', 'runs', 1, 'text'),
6696 notification_title
= self
._get
_text
(notification
, 'shortMessage')
6697 if notification_title
:
6698 notification_title
= notification_title
.replace('\xad', '') # remove soft hyphens
6699 # TODO: handle recommended videos
6700 title
= self
._search
_regex
(
6701 rf
'{re.escape(channel or "")}[^:]+: (.+)', notification_title
,
6702 'video title', default
=None)
6703 timestamp
= (self
._parse
_time
_text
(self
._get
_text
(notification
, 'sentTimeText'))
6704 if self
._configuration
_arg
('approximate_date', ie_key
=YoutubeTabIE
)
6709 'ie_key': (YoutubeIE
if video_id
else YoutubeTabIE
).ie_key(),
6710 'video_id': video_id
,
6712 'channel_id': channel_id
,
6714 'uploader': channel
,
6715 'thumbnails': self
._extract
_thumbnails
(notification
, 'videoThumbnail'),
6716 'timestamp': timestamp
,
6719 def _notification_menu_entries(self
, ytcfg
):
6720 continuation_list
= [None]
6722 for page
in itertools
.count(1):
6723 ctoken
= traverse_obj(
6724 continuation_list
, (0, 'continuationEndpoint', 'getNotificationMenuEndpoint', 'ctoken'), expected_type
=str)
6725 response
= self
._extract
_response
(
6726 item_id
=f
'page {page}', query
={'ctoken': ctoken}
if ctoken
else {}, ytcfg
=ytcfg
,
6727 ep
='notification/get_notification_menu', check_get_keys
='actions',
6728 headers
=self
.generate_api_headers(ytcfg
=ytcfg
, visitor_data
=self
._extract
_visitor
_data
(response
)))
6729 yield from self
._extract
_notification
_menu
(response
, continuation_list
)
6730 if not continuation_list
[0]:
6733 def _real_extract(self
, url
):
6734 display_id
= 'notifications'
6735 ytcfg
= self
._download
_ytcfg
('web', display_id
) if not self
.skip_webpage
else {}
6736 self
._report
_playlist
_authcheck
(ytcfg
)
6737 return self
.playlist_result(self
._notification
_menu
_entries
(ytcfg
), display_id
, display_id
)
6740 class YoutubeSearchIE(YoutubeTabBaseInfoExtractor
, SearchInfoExtractor
):
6741 IE_DESC
= 'YouTube search'
6742 IE_NAME
= 'youtube:search'
6743 _SEARCH_KEY
= 'ytsearch'
6744 _SEARCH_PARAMS
= 'EgIQAQ%3D%3D' # Videos only
6746 'url': 'ytsearch5:youtube-dl test video',
6747 'playlist_count': 5,
6749 'id': 'youtube-dl test video',
6750 'title': 'youtube-dl test video',
6755 class YoutubeSearchDateIE(YoutubeTabBaseInfoExtractor
, SearchInfoExtractor
):
6756 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ ':date'
6757 _SEARCH_KEY
= 'ytsearchdate'
6758 IE_DESC
= 'YouTube search, newest videos first'
6759 _SEARCH_PARAMS
= 'CAISAhAB' # Videos only, sorted by date
6761 'url': 'ytsearchdate5:youtube-dl test video',
6762 'playlist_count': 5,
6764 'id': 'youtube-dl test video',
6765 'title': 'youtube-dl test video',
6770 class YoutubeSearchURLIE(YoutubeTabBaseInfoExtractor
):
6771 IE_DESC
= 'YouTube search URLs with sorting and filter support'
6772 IE_NAME
= YoutubeSearchIE
.IE_NAME
+ '_url'
6773 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/(?:results|search)\?([^#]+&)?(?:search_query|q)=(?:[^&]+)(?:[&#]|$)'
6775 'url': 'https://www.youtube.com/results?baz=bar&search_query=youtube-dl+test+video&filters=video&lclk=video',
6776 'playlist_mincount': 5,
6778 'id': 'youtube-dl test video',
6779 'title': 'youtube-dl test video',
6782 'url': 'https://www.youtube.com/results?search_query=python&sp=EgIQAg%253D%253D',
6783 'playlist_mincount': 5,
6789 'url': 'https://www.youtube.com/results?search_query=%23cats',
6790 'playlist_mincount': 1,
6794 # The test suite does not have support for nested playlists
6796 # 'url': r're:https://(www\.)?youtube\.com/hashtag/cats',
6802 'url': 'https://www.youtube.com/results?search_query=kurzgesagt&sp=EgIQAg%253D%253D',
6805 'title': 'kurzgesagt',
6810 'id': 'UCsXVk37bltHxD1rDPwtNM8Q',
6811 'url': 'https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q',
6812 'ie_key': 'YoutubeTab',
6813 'channel': 'Kurzgesagt – In a Nutshell',
6814 'description': 'md5:4ae48dfa9505ffc307dad26342d06bfc',
6815 'title': 'Kurzgesagt – In a Nutshell',
6816 'channel_id': 'UCsXVk37bltHxD1rDPwtNM8Q',
6817 'playlist_count': int, # XXX: should have a way of saying > 1
6818 'channel_url': 'https://www.youtube.com/channel/UCsXVk37bltHxD1rDPwtNM8Q',
6820 'uploader_id': '@kurzgesagt',
6821 'uploader_url': 'https://www.youtube.com/@kurzgesagt',
6822 'uploader': 'Kurzgesagt – In a Nutshell',
6825 'params': {'extract_flat': True, 'playlist_items': '1'}
,
6826 'playlist_mincount': 1,
6828 'url': 'https://www.youtube.com/results?q=test&sp=EgQIBBgB',
6829 'only_matching': True,
6832 def _real_extract(self
, url
):
6834 query
= (qs
.get('search_query') or qs
.get('q'))[0]
6835 return self
.playlist_result(self
._search
_results
(query
, qs
.get('sp', (None,))[0]), query
, query
)
6838 class YoutubeMusicSearchURLIE(YoutubeTabBaseInfoExtractor
):
6839 IE_DESC
= 'YouTube music search URLs with selectable sections, e.g. #songs'
6840 IE_NAME
= 'youtube:music:search_url'
6841 _VALID_URL
= r
'https?://music\.youtube\.com/search\?([^#]+&)?(?:search_query|q)=(?:[^&]+)(?:[&#]|$)'
6843 'url': 'https://music.youtube.com/search?q=royalty+free+music',
6844 'playlist_count': 16,
6846 'id': 'royalty free music',
6847 'title': 'royalty free music',
6850 'url': 'https://music.youtube.com/search?q=royalty+free+music&sp=EgWKAQIIAWoKEAoQAxAEEAkQBQ%3D%3D',
6851 'playlist_mincount': 30,
6853 'id': 'royalty free music - songs',
6854 'title': 'royalty free music - songs',
6856 'params': {'extract_flat': 'in_playlist'}
6858 'url': 'https://music.youtube.com/search?q=royalty+free+music#community+playlists',
6859 'playlist_mincount': 30,
6861 'id': 'royalty free music - community playlists',
6862 'title': 'royalty free music - community playlists',
6864 'params': {'extract_flat': 'in_playlist'}
6868 'albums': 'EgWKAQIYAWoKEAoQAxAEEAkQBQ==',
6869 'artists': 'EgWKAQIgAWoKEAoQAxAEEAkQBQ==',
6870 'community playlists': 'EgeKAQQoAEABagoQChADEAQQCRAF',
6871 'featured playlists': 'EgeKAQQoADgBagwQAxAJEAQQDhAKEAU==',
6872 'songs': 'EgWKAQIIAWoKEAoQAxAEEAkQBQ==',
6873 'videos': 'EgWKAQIQAWoKEAoQAxAEEAkQBQ==',
6876 def _real_extract(self
, url
):
6878 query
= (qs
.get('search_query') or qs
.get('q'))[0]
6879 params
= qs
.get('sp', (None,))[0]
6881 section
= next((k
for k
, v
in self
._SECTIONS
.items() if v
== params
), params
)
6883 section
= urllib
.parse
.unquote_plus((url
.split('#') + [''])[1]).lower()
6884 params
= self
._SECTIONS
.get(section
)
6887 title
= join_nonempty(query
, section
, delim
=' - ')
6888 return self
.playlist_result(self
._search
_results
(query
, params
, default_client
='web_music'), title
, title
)
6891 class YoutubeFeedsInfoExtractor(InfoExtractor
):
6893 Base class for feed extractors
6894 Subclasses must re-define the _FEED_NAME property.
6896 _LOGIN_REQUIRED
= True
6897 _FEED_NAME
= 'feeds'
6899 def _real_initialize(self
):
6900 YoutubeBaseInfoExtractor
._check
_login
_required
(self
)
6904 return f
'youtube:{self._FEED_NAME}'
6906 def _real_extract(self
, url
):
6907 return self
.url_result(
6908 f
'https://www.youtube.com/feed/{self._FEED_NAME}', ie
=YoutubeTabIE
.ie_key())
6911 class YoutubeWatchLaterIE(InfoExtractor
):
6912 IE_NAME
= 'youtube:watchlater'
6913 IE_DESC
= 'Youtube watch later list; ":ytwatchlater" keyword (requires cookies)'
6914 _VALID_URL
= r
':ytwatchlater'
6916 'url': ':ytwatchlater',
6917 'only_matching': True,
6920 def _real_extract(self
, url
):
6921 return self
.url_result(
6922 'https://www.youtube.com/playlist?list=WL', ie
=YoutubeTabIE
.ie_key())
6925 class YoutubeRecommendedIE(YoutubeFeedsInfoExtractor
):
6926 IE_DESC
= 'YouTube recommended videos; ":ytrec" keyword'
6927 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/?(?:[?#]|$)|:ytrec(?:ommended)?'
6928 _FEED_NAME
= 'recommended'
6929 _LOGIN_REQUIRED
= False
6932 'only_matching': True,
6934 'url': ':ytrecommended',
6935 'only_matching': True,
6937 'url': 'https://youtube.com',
6938 'only_matching': True,
6942 class YoutubeSubscriptionsIE(YoutubeFeedsInfoExtractor
):
6943 IE_DESC
= 'YouTube subscriptions feed; ":ytsubs" keyword (requires cookies)'
6944 _VALID_URL
= r
':ytsub(?:scription)?s?'
6945 _FEED_NAME
= 'subscriptions'
6948 'only_matching': True,
6950 'url': ':ytsubscriptions',
6951 'only_matching': True,
6955 class YoutubeHistoryIE(YoutubeFeedsInfoExtractor
):
6956 IE_DESC
= 'Youtube watch history; ":ythis" keyword (requires cookies)'
6957 _VALID_URL
= r
':ythis(?:tory)?'
6958 _FEED_NAME
= 'history'
6960 'url': ':ythistory',
6961 'only_matching': True,
6965 class YoutubeStoriesIE(InfoExtractor
):
6966 IE_DESC
= 'YouTube channel stories; "ytstories:" prefix'
6967 IE_NAME
= 'youtube:stories'
6968 _VALID_URL
= r
'ytstories:UC(?P<id>[A-Za-z0-9_-]{21}[AQgw])$'
6970 'url': 'ytstories:UCwFCb4jeqaKWnciAYM-ZVHg',
6971 'only_matching': True,
6974 def _real_extract(self
, url
):
6975 playlist_id
= f
'RLTD{self._match_id(url)}'
6976 return self
.url_result(
6977 smuggle_url(f
'https://www.youtube.com/playlist?list={playlist_id}&playnext=1', {'is_story': True}
),
6978 ie
=YoutubeTabIE
, video_id
=playlist_id
)
6981 class YoutubeShortsAudioPivotIE(InfoExtractor
):
6982 IE_DESC
= 'YouTube Shorts audio pivot (Shorts using audio of a given video)'
6983 IE_NAME
= 'youtube:shorts:pivot:audio'
6984 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/source/(?P<id>[\w-]{11})/shorts'
6986 'url': 'https://www.youtube.com/source/Lyj-MZSAA9o/shorts',
6987 'only_matching': True,
6991 def _generate_audio_pivot_params(video_id
):
6993 Generates sfv_audio_pivot browse params for this video id
6995 pb_params
= b
'\xf2\x05+\n)\x12\'\n\x0b%b\x12\x0b%b\x1a\x0b%b' % ((video_id
.encode(),) * 3)
6996 return urllib
.parse
.quote(base64
.b64encode(pb_params
).decode())
6998 def _real_extract(self
, url
):
6999 video_id
= self
._match
_id
(url
)
7000 return self
.url_result(
7001 f
'https://www.youtube.com/feed/sfv_audio_pivot?bp={self._generate_audio_pivot_params(video_id)}',
7005 class YoutubeTruncatedURLIE(InfoExtractor
):
7006 IE_NAME
= 'youtube:truncated_url'
7007 IE_DESC
= False # Do not list
7008 _VALID_URL
= r
'''(?x)
7010 (?:\w+\.)?[yY][oO][uU][tT][uU][bB][eE](?:-nocookie)?\.com/
7013 annotation_id=annotation_[^&]+|
7019 attribution_link\?a=[^&]+
7025 'url': 'https://www.youtube.com/watch?annotation_id=annotation_3951667041',
7026 'only_matching': True,
7028 'url': 'https://www.youtube.com/watch?',
7029 'only_matching': True,
7031 'url': 'https://www.youtube.com/watch?x-yt-cl=84503534',
7032 'only_matching': True,
7034 'url': 'https://www.youtube.com/watch?feature=foo',
7035 'only_matching': True,
7037 'url': 'https://www.youtube.com/watch?hl=en-GB',
7038 'only_matching': True,
7040 'url': 'https://www.youtube.com/watch?t=2372',
7041 'only_matching': True,
7044 def _real_extract(self
, url
):
7045 raise ExtractorError(
7046 'Did you forget to quote the URL? Remember that & is a meta '
7047 'character in most shells, so you want to put the URL in quotes, '
7049 '"https://www.youtube.com/watch?feature=foo&v=BaW_jenozKc" '
7050 ' or simply youtube-dl BaW_jenozKc .',
7054 class YoutubeClipIE(YoutubeTabBaseInfoExtractor
):
7055 IE_NAME
= 'youtube:clip'
7056 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/clip/(?P<id>[^/?#]+)'
7058 # FIXME: Other metadata should be extracted from the clip, not from the base video
7059 'url': 'https://www.youtube.com/clip/UgytZKpehg-hEMBSn3F4AaABCQ',
7061 'id': 'UgytZKpehg-hEMBSn3F4AaABCQ',
7063 'section_start': 29.0,
7064 'section_end': 39.7,
7067 'availability': 'public',
7068 'categories': ['Gaming'],
7069 'channel': 'Scott The Woz',
7070 'channel_id': 'UC4rqhyiTs7XyuODcECvuiiQ',
7071 'channel_url': 'https://www.youtube.com/channel/UC4rqhyiTs7XyuODcECvuiiQ',
7072 'description': 'md5:7a4517a17ea9b4bd98996399d8bb36e7',
7074 'playable_in_embed': True,
7076 'thumbnail': 'https://i.ytimg.com/vi_webp/ScPX26pdQik/maxresdefault.webp',
7077 'title': 'Mobile Games on Console - Scott The Woz',
7078 'upload_date': '20210920',
7079 'uploader': 'Scott The Woz',
7080 'uploader_id': '@ScottTheWoz',
7081 'uploader_url': 'https://www.youtube.com/@ScottTheWoz',
7083 'live_status': 'not_live',
7084 'channel_follower_count': int,
7085 'chapters': 'count:20',
7089 def _real_extract(self
, url
):
7090 clip_id
= self
._match
_id
(url
)
7091 _
, data
= self
._extract
_webpage
(url
, clip_id
)
7093 video_id
= traverse_obj(data
, ('currentVideoEndpoint', 'watchEndpoint', 'videoId'))
7095 raise ExtractorError('Unable to find video ID')
7097 clip_data
= traverse_obj(data
, (
7098 'engagementPanels', ..., 'engagementPanelSectionListRenderer', 'content', 'clipSectionRenderer',
7099 'contents', ..., 'clipAttributionRenderer', 'onScrubExit', 'commandExecutorCommand', 'commands', ...,
7100 'openPopupAction', 'popup', 'notificationActionRenderer', 'actionButton', 'buttonRenderer', 'command',
7101 'commandExecutorCommand', 'commands', ..., 'loopCommand'), get_all
=False)
7104 '_type': 'url_transparent',
7105 'url': f
'https://www.youtube.com/watch?v={video_id}',
7106 'ie_key': YoutubeIE
.ie_key(),
7108 'section_start': int(clip_data
['startTimeMs']) / 1000,
7109 'section_end': int(clip_data
['endTimeMs']) / 1000,
7113 class YoutubeConsentRedirectIE(YoutubeBaseInfoExtractor
):
7114 IE_NAME
= 'youtube:consent'
7115 IE_DESC
= False # Do not list
7116 _VALID_URL
= r
'https?://consent\.youtube\.com/m\?'
7118 'url': 'https://consent.youtube.com/m?continue=https%3A%2F%2Fwww.youtube.com%2Flive%2FqVv6vCqciTM%3Fcbrd%3D1&gl=NL&m=0&pc=yt&hl=en&src=1',
7120 'id': 'qVv6vCqciTM',
7123 'uploader_id': '@sana_natori',
7124 'comment_count': int,
7125 'chapters': 'count:13',
7126 'upload_date': '20221223',
7127 'thumbnail': 'https://i.ytimg.com/vi/qVv6vCqciTM/maxresdefault.jpg',
7128 'channel_url': 'https://www.youtube.com/channel/UCIdEIHpS0TdkqRkHL5OkLtA',
7129 'uploader_url': 'https://www.youtube.com/@sana_natori',
7131 'release_date': '20221223',
7132 'tags': ['Vtuber', '月ノ美兎', '名取さな', 'にじさんじ', 'クリスマス', '3D配信'],
7133 'title': '【 #インターネット女クリスマス 】3Dで歌ってはしゃぐインターネットの女たち【月ノ美兎/名取さな】',
7135 'playable_in_embed': True,
7137 'availability': 'public',
7138 'channel_follower_count': int,
7139 'channel_id': 'UCIdEIHpS0TdkqRkHL5OkLtA',
7140 'categories': ['Entertainment'],
7141 'live_status': 'was_live',
7142 'release_timestamp': 1671793345,
7143 'channel': 'さなちゃんねる',
7144 'description': 'md5:6aebf95cc4a1d731aebc01ad6cc9806d',
7145 'uploader': 'さなちゃんねる',
7147 'add_ie': ['Youtube'],
7148 'params': {'skip_download': 'Youtube'}
,
7151 def _real_extract(self
, url
):
7152 redirect_url
= url_or_none(parse_qs(url
).get('continue', [None])[-1])
7153 if not redirect_url
:
7154 raise ExtractorError('Invalid cookie consent redirect URL', expected
=True)
7155 return self
.url_result(redirect_url
)
7158 class YoutubeTruncatedIDIE(InfoExtractor
):
7159 IE_NAME
= 'youtube:truncated_id'
7160 IE_DESC
= False # Do not list
7161 _VALID_URL
= r
'https?://(?:www\.)?youtube\.com/watch\?v=(?P<id>[0-9A-Za-z_-]{1,10})$'
7164 'url': 'https://www.youtube.com/watch?v=N_708QY7Ob',
7165 'only_matching': True,
7168 def _real_extract(self
, url
):
7169 video_id
= self
._match
_id
(url
)
7170 raise ExtractorError(
7171 f
'Incomplete YouTube ID {video_id}. URL {url} looks truncated.',