variadic,
)
+
+STREAMING_DATA_CLIENT_NAME = '__yt_dlp_client'
# any clients starting with _ cannot be explicitly requested by the user
INNERTUBE_CLIENTS = {
'web': {
return client_name, base, variant[0] if variant else None
+def short_client_name(client_name):
+ main, *parts = _split_innertube_client(client_name)[0].replace('embedscreen', 'e_s').split('_')
+ return join_nonempty(main[:4], ''.join(x[0] for x in parts)).upper()
+
+
def build_innertube_clients():
THIRD_PARTY = {
'embedUrl': 'https://www.youtube.com/', # Can be any valid URL
comment = self._extract_comment(comment_renderer, parent)
if not comment:
continue
+ is_pinned = bool(traverse_obj(comment_renderer, 'pinnedCommentBadge'))
+ comment_id = comment['id']
+ if is_pinned:
+ tracker['pinned_comment_ids'].add(comment_id)
# Sometimes YouTube may break and give us infinite looping comments.
# See: https://github.com/yt-dlp/yt-dlp/issues/6290
- if comment['id'] in tracker['seen_comment_ids']:
+ if comment_id in tracker['seen_comment_ids']:
+ if comment_id in tracker['pinned_comment_ids'] and not is_pinned:
+ # Pinned comments may appear a second time in newest first sort
+ # See: https://github.com/yt-dlp/yt-dlp/issues/6712
+ continue
self.report_warning('Detected YouTube comments looping. Stopping comment extraction as we probably cannot get any more.')
yield
else:
current_page_thread=0,
total_parent_comments=0,
total_reply_comments=0,
- seen_comment_ids=set())
+ seen_comment_ids=set(),
+ pinned_comment_ids=set()
+ )
# TODO: Deprecated
# YouTube comments have a max depth of 2
self.report_warning(
f'Skipping player response from {client} client (got player response for video "{pr_video_id}" instead of "{video_id}")' + bug_reports_message())
else:
+ # Save client name for introspection later
+ name = short_client_name(client)
+ sd = traverse_obj(pr, ('streamingData', {dict})) or {}
+ sd[STREAMING_DATA_CLIENT_NAME] = name
+ for f in traverse_obj(sd, (('formats', 'adaptiveFormats'), ..., {dict})):
+ f[STREAMING_DATA_CLIENT_NAME] = name
prs.append(pr)
# creator clients can bypass AGE_VERIFICATION_REQUIRED if logged in
return live_status
def _extract_formats_and_subtitles(self, streaming_data, video_id, player_url, live_status, duration):
+ CHUNK_SIZE = 10 << 20
itags, stream_ids = collections.defaultdict(set), []
itag_qualities, res_qualities = {}, {0: None}
q = qualities([
'small', 'medium', 'large', 'hd720', 'hd1080', 'hd1440', 'hd2160', 'hd2880', 'highres'
])
streaming_formats = traverse_obj(streaming_data, (..., ('formats', 'adaptiveFormats'), ...))
+ all_formats = self._configuration_arg('include_duplicate_formats')
+
+ def build_fragments(f):
+ return LazyList({
+ 'url': update_url_query(f['url'], {
+ 'range': f'{range_start}-{min(range_start + CHUNK_SIZE - 1, f["filesize"])}'
+ })
+ } for range_start in range(0, f['filesize'], CHUNK_SIZE))
for fmt in streaming_formats:
if fmt.get('targetDurationSec'):
itag = str_or_none(fmt.get('itag'))
audio_track = fmt.get('audioTrack') or {}
stream_id = (itag, audio_track.get('id'), fmt.get('isDrc'))
- if stream_id in stream_ids:
- continue
+ if not all_formats:
+ if stream_id in stream_ids:
+ continue
quality = fmt.get('quality')
height = int_or_none(fmt.get('height'))
if is_damaged:
self.report_warning(
f'{video_id}: Some formats are possibly damaged. They will be deprioritized', only_once=True)
+
+ client_name = fmt.get(STREAMING_DATA_CLIENT_NAME)
dct = {
'asr': int_or_none(fmt.get('audioSampleRate')),
'filesize': int_or_none(fmt.get('contentLength')),
'format_id': f'{itag}{"-drc" if fmt.get("isDrc") else ""}',
'format_note': join_nonempty(
- '%s%s' % (audio_track.get('displayName') or '',
- ' (default)' if language_preference > 0 else ''),
+ join_nonempty(audio_track.get('displayName'),
+ language_preference > 0 and ' (default)', delim=''),
fmt.get('qualityLabel') or quality.replace('audio_quality_', ''),
- 'DRC' if fmt.get('isDrc') else None,
+ fmt.get('isDrc') and 'DRC',
try_get(fmt, lambda x: x['projectionType'].replace('RECTANGULAR', '').lower()),
try_get(fmt, lambda x: x['spatialAudioType'].replace('SPATIAL_AUDIO_TYPE_', '').lower()),
- throttled and 'THROTTLED', is_damaged and 'DAMAGED', delim=', '),
+ throttled and 'THROTTLED', is_damaged and 'DAMAGED',
+ (self.get_param('verbose') or all_formats) and client_name,
+ delim=', '),
# Format 22 is likely to be damaged. See https://github.com/yt-dlp/yt-dlp/issues/3372
'source_preference': -10 if throttled else -5 if itag == '22' else -1,
'fps': int_or_none(fmt.get('fps')) or None,
if mime_mobj:
dct['ext'] = mimetype2ext(mime_mobj.group(1))
dct.update(parse_codecs(mime_mobj.group(2)))
- no_audio = dct.get('acodec') == 'none'
- no_video = dct.get('vcodec') == 'none'
- if no_audio:
- dct['vbr'] = tbr
- if no_video:
- dct['abr'] = tbr
- if no_audio or no_video:
- CHUNK_SIZE = 10 << 20
- dct.update({
- 'protocol': 'http_dash_segments',
- 'fragments': [{
- 'url': update_url_query(dct['url'], {
- 'range': f'{range_start}-{min(range_start + CHUNK_SIZE - 1, dct["filesize"])}'
- })
- } for range_start in range(0, dct['filesize'], CHUNK_SIZE)]
- } if dct['filesize'] else {
- 'downloader_options': {'http_chunk_size': CHUNK_SIZE} # No longer useful?
- })
-
- if dct.get('ext'):
- dct['container'] = dct['ext'] + '_dash'
-
if itag:
itags[itag].add(('https', dct.get('language')))
stream_ids.append(stream_id)
+ single_stream = 'none' in (dct.get('acodec'), dct.get('vcodec'))
+ if single_stream and dct.get('ext'):
+ dct['container'] = dct['ext'] + '_dash'
+
+ if dct['filesize']:
+ yield {
+ **dct,
+ 'format_id': f'{dct["format_id"]}-dashy' if all_formats else dct['format_id'],
+ 'protocol': 'http_dash_segments',
+ 'fragments': build_fragments(dct),
+ }
+ if not all_formats:
+ continue
+ dct['downloader_options'] = {'http_chunk_size': CHUNK_SIZE}
yield dct
needs_live_processing = self._needs_live_processing(live_status, duration)
elif skip_bad_formats and live_status == 'is_live' and needs_live_processing != 'is_live':
skip_manifests.add('dash')
- def process_manifest_format(f, proto, itag):
+ def process_manifest_format(f, proto, client_name, itag):
key = (proto, f.get('language'))
- if key in itags[itag]:
+ if not all_formats and key in itags[itag]:
return False
itags[itag].add(key)
- if any(p != proto for p, _ in itags[itag]):
+ if itag and all_formats:
+ f['format_id'] = f'{itag}-{proto}'
+ elif any(p != proto for p, _ in itags[itag]):
f['format_id'] = f'{itag}-{proto}'
elif itag:
f['format_id'] = itag
f['quality'] = q(itag_qualities.get(try_get(f, lambda f: f['format_id'].split('-')[0]), -1))
if f['quality'] == -1 and f.get('height'):
f['quality'] = q(res_qualities[min(res_qualities, key=lambda x: abs(x - f['height']))])
+ if self.get_param('verbose'):
+ f['format_note'] = join_nonempty(f.get('format_note'), client_name, delim=', ')
return True
subtitles = {}
for sd in streaming_data:
+ client_name = sd.get(STREAMING_DATA_CLIENT_NAME)
+
hls_manifest_url = 'hls' not in skip_manifests and sd.get('hlsManifestUrl')
if hls_manifest_url:
fmts, subs = self._extract_m3u8_formats_and_subtitles(
hls_manifest_url, video_id, 'mp4', fatal=False, live=live_status == 'is_live')
subtitles = self._merge_subtitles(subs, subtitles)
for f in fmts:
- if process_manifest_format(f, 'hls', self._search_regex(
+ if process_manifest_format(f, 'hls', client_name, self._search_regex(
r'/itag/(\d+)', f['url'], 'itag', default=None)):
yield f
formats, subs = self._extract_mpd_formats_and_subtitles(dash_manifest_url, video_id, fatal=False)
subtitles = self._merge_subtitles(subs, subtitles) # Prioritize HLS subs over DASH
for f in formats:
- if process_manifest_format(f, 'dash', f['format_id']):
+ if process_manifest_format(f, 'dash', client_name, f['format_id']):
f['filesize'] = int_or_none(self._search_regex(
r'/clen/(\d+)', f.get('fragment_base_url') or f['url'], 'file size', default=None))
if needs_live_processing:
initial_data = None
if webpage:
initial_data = self.extract_yt_initial_data(video_id, webpage, fatal=False)
+ if not traverse_obj(initial_data, 'contents'):
+ self.report_warning('Incomplete data received in embedded initial data; re-fetching using API.')
+ initial_data = None
if not initial_data:
query = {'videoId': video_id}
query.update(self._get_checkok_params())
initial_data = self._extract_response(
item_id=video_id, ep='next', fatal=False,
- ytcfg=master_ytcfg, query=query,
+ ytcfg=master_ytcfg, query=query, check_get_keys='contents',
headers=self.generate_api_headers(ytcfg=master_ytcfg),
note='Downloading initial data API JSON')
info['comment_count'] = traverse_obj(initial_data, (
'contents', 'twoColumnWatchNextResults', 'results', 'results', 'contents', ..., 'itemSectionRenderer',
- 'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount', 'simpleText'
+ 'contents', ..., 'commentsEntryPointHeaderRenderer', 'commentCount'
), (
'engagementPanels', lambda _, v: v['engagementPanelSectionListRenderer']['panelIdentifier'] == 'comment-item-section',
- 'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo', 'runs', ..., 'text'
- ), expected_type=int_or_none, get_all=False)
+ 'engagementPanelSectionListRenderer', 'header', 'engagementPanelTitleHeaderRenderer', 'contextualInfo'
+ ), expected_type=self._get_count, get_all=False)
try: # This will error if there is no livechat
initial_data['contents']['twoColumnWatchNextResults']['conversationBar']['liveChatRenderer']['continuations'][0]['reloadContinuationData']['continuation']