This commit is contained in:
Michiel Sikma 2025-12-04 17:03:31 -05:00 committed by GitHub
commit dc4f0c4891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 90 additions and 3 deletions

View file

@ -2241,6 +2241,7 @@ from .twitcasting import (
from .twitch import ( from .twitch import (
TwitchClipsIE, TwitchClipsIE,
TwitchCollectionIE, TwitchCollectionIE,
TwitchDirectoryClipsIE,
TwitchStreamIE, TwitchStreamIE,
TwitchVideosClipsIE, TwitchVideosClipsIE,
TwitchVideosCollectionsIE, TwitchVideosCollectionsIE,

View file

@ -43,6 +43,7 @@ class TwitchBaseIE(InfoExtractor):
_OPERATION_HASHES = { _OPERATION_HASHES = {
'CollectionSideBar': '016e1e4ccee0eb4698eb3bf1a04dc1c077fb746c78c82bac9a8f0289658fbd1a', 'CollectionSideBar': '016e1e4ccee0eb4698eb3bf1a04dc1c077fb746c78c82bac9a8f0289658fbd1a',
'FilterableVideoTower_Videos': '67004f7881e65c297936f32c75246470629557a393788fb5a69d6d9a25a8fd5f', 'FilterableVideoTower_Videos': '67004f7881e65c297936f32c75246470629557a393788fb5a69d6d9a25a8fd5f',
'ClipsCards__Game': 'cc14976959c8f31c617e956a7c4c32216c3e04f6b586088b7bf49561c35e841b',
'ClipsCards__User': '90c33f5e6465122fba8f9371e2a97076f9ed06c6fed3788d002ab9eba8f91d88', 'ClipsCards__User': '90c33f5e6465122fba8f9371e2a97076f9ed06c6fed3788d002ab9eba8f91d88',
'ShareClipRenderStatus': '1844261bb449fa51e6167040311da4a7a5f1c34fe71c71a3e0c4f551bc30c698', 'ShareClipRenderStatus': '1844261bb449fa51e6167040311da4a7a5f1c34fe71c71a3e0c4f551bc30c698',
'ChannelCollectionsContent': '5247910a19b1cd2b760939bf4cba4dcbd3d13bdf8c266decd16956f6ef814077', 'ChannelCollectionsContent': '5247910a19b1cd2b760939bf4cba4dcbd3d13bdf8c266decd16956f6ef814077',
@ -663,11 +664,12 @@ class TwitchPlaylistBaseIE(TwitchBaseIE):
def _entries(self, channel_name, *args): def _entries(self, channel_name, *args):
""" """
Subclasses must define _make_variables() and _extract_entry(), Subclasses must define _make_variables() and _extract_entry(),
as well as set _OPERATION_NAME, _ENTRY_KIND, _EDGE_KIND, and _NODE_KIND as well as set _OPERATION_NAME, _ENTRY_KIND, _DATA_KIND, _EDGE_KIND, and _NODE_KIND
""" """
cursor = None cursor = None
variables_common = self._make_variables(channel_name, *args) variables_common = self._make_variables(channel_name, *args)
entries_key = f'{self._ENTRY_KIND}s' entries_key = f'{self._ENTRY_KIND}s'
data_key = self._DATA_KIND
for page_num in itertools.count(1): for page_num in itertools.count(1):
variables = variables_common.copy() variables = variables_common.copy()
variables['limit'] = self._PAGE_LIMIT variables['limit'] = self._PAGE_LIMIT
@ -683,7 +685,7 @@ class TwitchPlaylistBaseIE(TwitchBaseIE):
if not page: if not page:
break break
edges = try_get( edges = try_get(
page, lambda x: x[0]['data']['user'][entries_key]['edges'], list) page, lambda x: x[0]['data'][data_key][entries_key]['edges'], list)
if not edges: if not edges:
break break
for edge in edges: for edge in edges:
@ -806,6 +808,7 @@ class TwitchVideosIE(TwitchVideosBaseIE):
return (False return (False
if any(ie.suitable(url) for ie in ( if any(ie.suitable(url) for ie in (
TwitchVideosClipsIE, TwitchVideosClipsIE,
TwitchDirectoryClipsIE,
TwitchVideosCollectionsIE)) TwitchVideosCollectionsIE))
else super().suitable(url)) else super().suitable(url))
@ -827,6 +830,87 @@ class TwitchVideosIE(TwitchVideosBaseIE):
f'sorted by {self._SORTED_BY.get(sort, self._DEFAULT_SORTED_BY)}')) f'sorted by {self._SORTED_BY.get(sort, self._DEFAULT_SORTED_BY)}'))
class TwitchDirectoryClipsIE(TwitchPlaylistBaseIE):
_VALID_URL = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/directory/category/(?P<id>[^/]+)/(?:clips|videos/*?\?.*?)'
_TESTS = [{
# Clips (defaults to 7d)
'url': 'https://www.twitch.tv/directory/category/starcraft/clips?range=7d',
'info_dict': {
'id': 'starcraft',
'title': 'starcraft - Clips Top 7D',
},
'playlist_mincount': 3,
}, {
'url': 'https://www.twitch.tv/directory/category/minecraft/clips?range=30d',
'info_dict': {
'id': 'minecraft',
'title': 'minecraft - Clips Top 30D',
},
'playlist_mincount': 3,
}]
Clip = collections.namedtuple('Clip', ['filter', 'label'])
_DEFAULT_CLIP = Clip('LAST_WEEK', 'Top 7D')
_RANGE = {
'24hr': Clip('LAST_DAY', 'Top 24H'),
'7d': _DEFAULT_CLIP,
'30d': Clip('LAST_MONTH', 'Top 30D'),
'all': Clip('ALL_TIME', 'Top All'),
}
_PAGE_LIMIT = 20
_OPERATION_NAME = 'ClipsCards__Game'
_ENTRY_KIND = 'clip'
_DATA_KIND = 'game'
_EDGE_KIND = 'ClipEdge'
_NODE_KIND = 'Clip'
@staticmethod
def _make_variables(game_name, channel_filter):
return {
'categorySlug': game_name,
'limit': 20,
'criteria': {
'filter': channel_filter,
},
}
@staticmethod
def _extract_entry(node):
assert isinstance(node, dict)
slug = node.get('slug')
broadcaster_name = traverse_obj(node, ('broadcaster', 'login'))
clip_url = f'https://www.twitch.tv/{broadcaster_name}/clip/{slug}'
if not clip_url:
return
return {
'_type': 'url_transparent',
'ie_key': TwitchClipsIE.ie_key(),
'id': node.get('id'),
'url': clip_url,
'title': node.get('title'),
'thumbnail': node.get('thumbnailURL'),
'duration': float_or_none(node.get('durationSeconds')),
'timestamp': unified_timestamp(node.get('createdAt')),
'view_count': int_or_none(node.get('viewCount')),
'language': node.get('language'),
}
def _real_extract(self, url):
game_name = self._match_id(url)
qs = parse_qs(url)
date_range = qs.get('range', ['7d'])[0]
clip = self._RANGE.get(date_range, self._DEFAULT_CLIP)
return self.playlist_result(
self._entries(game_name, clip.filter),
playlist_id=game_name,
playlist_title=f'{game_name} - Clips {clip.label}')
class TwitchVideosClipsIE(TwitchPlaylistBaseIE): class TwitchVideosClipsIE(TwitchPlaylistBaseIE):
IE_NAME = 'twitch:videos:clips' IE_NAME = 'twitch:videos:clips'
_VALID_URL = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/(?P<id>[^/]+)/(?:clips|videos/*?\?.*?\bfilter=clips)' _VALID_URL = r'https?://(?:(?:www|go|m)\.)?twitch\.tv/(?P<id>[^/]+)/(?:clips|videos/*?\?.*?\bfilter=clips)'
@ -858,6 +942,7 @@ class TwitchVideosClipsIE(TwitchPlaylistBaseIE):
_OPERATION_NAME = 'ClipsCards__User' _OPERATION_NAME = 'ClipsCards__User'
_ENTRY_KIND = 'clip' _ENTRY_KIND = 'clip'
_DATA_KIND = 'user'
_EDGE_KIND = 'ClipEdge' _EDGE_KIND = 'ClipEdge'
_NODE_KIND = 'Clip' _NODE_KIND = 'Clip'
@ -922,6 +1007,7 @@ class TwitchVideosCollectionsIE(TwitchPlaylistBaseIE):
_OPERATION_NAME = 'ChannelCollectionsContent' _OPERATION_NAME = 'ChannelCollectionsContent'
_ENTRY_KIND = 'collection' _ENTRY_KIND = 'collection'
_DATA_KIND = 'user'
_EDGE_KIND = 'CollectionsItemEdge' _EDGE_KIND = 'CollectionsItemEdge'
_NODE_KIND = 'Collection' _NODE_KIND = 'Collection'
@ -961,7 +1047,7 @@ class TwitchStreamIE(TwitchVideosBaseIE):
_VALID_URL = r'''(?x) _VALID_URL = r'''(?x)
https?:// https?://
(?: (?:
(?:(?:www|go|m)\.)?twitch\.tv/| (?:(?:www|go|m)\.)?twitch\.tv/(?!directory/category/)|
player\.twitch\.tv/\?.*?\bchannel= player\.twitch\.tv/\?.*?\bchannel=
) )
(?P<id>[^/#?]+) (?P<id>[^/#?]+)