r'channel|c|user|playlist|watch|w|v|embed|e|watch_popup|clip|'
r'shorts|movies|results|search|shared|hashtag|trending|explore|feed|feeds|'
r'browse|oembed|get_video_info|iframe_api|s/player|'
- r'storefront|oops|index|account|reporthistory|t/terms|about|upload|signin|logout')
+ r'storefront|oops|index|account|t/terms|about|upload|signin|logout')
_PLAYLIST_ID_RE = r'(?:(?:PL|LL|EC|UU|FL|RD|UL|TL|PU|OLAK5uy_)[0-9A-Za-z-_]{10,}|RDMM|WL|LL|LM)'
return next_continuation
contents = []
- for key in ('contents', 'items'):
+ for key in ('contents', 'items', 'rows'):
contents.extend(try_get(renderer, lambda x: x[key], list) or [])
for content in contents:
_EMBED_REGEX = [
r'''(?x)
(?:
- <iframe[^>]+?src=|
+ <(?:[0-9A-Za-z-]+?)?iframe[^>]+?src=|
data-video-url=|
<embed[^>]+?src=|
embedSWF\(?:\s*|
yield self._extract_video(renderer)
def _rich_entries(self, rich_grid_renderer):
- renderer = try_get(
- rich_grid_renderer, lambda x: x['content']['videoRenderer'], dict) or {}
+ renderer = traverse_obj(
+ rich_grid_renderer, ('content', ('videoRenderer', 'reelItemRenderer')), get_all=False) or {}
video_id = renderer.get('videoId')
if not video_id:
return
yield entry
'''
+ def _report_history_entries(self, renderer):
+ for url in traverse_obj(renderer, (
+ 'rows', ..., 'reportHistoryTableRowRenderer', 'cells', ...,
+ 'reportHistoryTableCellRenderer', 'cell', 'reportHistoryTableTextCellRenderer', 'text', 'runs', ...,
+ 'navigationEndpoint', 'commandMetadata', 'webCommandMetadata', 'url')):
+ yield self.url_result(urljoin('https://www.youtube.com', url), YoutubeIE)
+
def _extract_entries(self, parent_renderer, continuation_list):
# continuation_list is modified in-place with continuation_list = [continuation_token]
continuation_list[:] = [None]
content, 'itemSectionRenderer', 'musicShelfRenderer', 'musicShelfContinuation',
expected_type=dict)
if not is_renderer:
- renderer = content.get('richItemRenderer')
- if renderer:
- for entry in self._rich_entries(renderer):
+ if content.get('richItemRenderer'):
+ for entry in self._rich_entries(content['richItemRenderer']):
yield entry
continuation_list[0] = self._extract_continuation(parent_renderer)
+ elif content.get('reportHistorySectionRenderer'): # https://www.youtube.com/reporthistory
+ table = traverse_obj(content, ('reportHistorySectionRenderer', 'table', 'tableRenderer'))
+ yield from self._report_history_entries(table)
+ continuation_list[0] = self._extract_continuation(table)
continue
+
isr_contents = try_get(is_renderer, lambda x: x['contents'], list) or []
for isr_content in isr_contents:
if not isinstance(isr_content, dict):
'playlistVideoRenderer': (self._playlist_entries, 'contents'),
'itemSectionRenderer': (extract_entries, 'contents'), # for feeds
'richItemRenderer': (extract_entries, 'contents'), # for hashtag
- 'backstagePostThreadRenderer': (self._post_thread_continuation_entries, 'contents')
+ 'backstagePostThreadRenderer': (self._post_thread_continuation_entries, 'contents'),
+ 'reportHistoryTableRowRenderer': (self._report_history_entries, 'rows'),
}
on_response_received = dict_get(response, ('onResponseReceivedActions', 'onResponseReceivedEndpoints'))
continuation_items = try_get(
'playlist_mincount': 1,
'params': {'extractor_args': {'youtube': {'lang': ['ja']}}},
'expected_warnings': ['Preferring "ja"'],
+ }, {
+ # shorts audio pivot for 2GtVksBMYFM.
+ 'url': 'https://www.youtube.com/feed/sfv_audio_pivot?bp=8gUrCikSJwoLMkd0VmtzQk1ZRk0SCzJHdFZrc0JNWUZNGgsyR3RWa3NCTVlGTQ==',
+ 'info_dict': {
+ 'id': 'sfv_audio_pivot',
+ 'title': 'sfv_audio_pivot',
+ 'tags': [],
+ },
+ 'playlist_mincount': 50,
+
}]
@classmethod
ie=YoutubeTabIE, video_id=playlist_id)
+class YoutubeShortsAudioPivotIE(InfoExtractor):
+ IE_DESC = 'YouTube Shorts audio pivot (Shorts using audio of a given video); "ytshortsap:" prefix'
+ IE_NAME = 'youtube:shorts:pivot:audio'
+ _VALID_URL = f'(?x)^ytshortsap:{YoutubeIE._VALID_URL[5:]}'
+ _TESTS = [{
+ 'url': 'ytshortsap:https://www.youtube.com/shorts/Lyj-MZSAA9o?feature=share',
+ 'only_matching': True,
+ }, {
+ 'url': 'ytshortsap:Lyj-MZSAA9o',
+ 'only_matching': True,
+ }]
+
+ @staticmethod
+ def _generate_audio_pivot_params(video_id):
+ """
+ Generates sfv_audio_pivot browse params for this video id
+ """
+ pb_params = b'\xf2\x05+\n)\x12\'\n\x0b%b\x12\x0b%b\x1a\x0b%b' % ((video_id.encode(),) * 3)
+ return urllib.parse.quote(base64.b64encode(pb_params).decode())
+
+ def _real_extract(self, url):
+ video_id = self._match_id(url)
+ return self.url_result(
+ f'https://www.youtube.com/feed/sfv_audio_pivot?bp={self._generate_audio_pivot_params(video_id)}',
+ ie=YoutubeTabIE)
+
+
class YoutubeTruncatedURLIE(InfoExtractor):
IE_NAME = 'youtube:truncated_url'
IE_DESC = False # Do not list