mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2025-06-28 01:18:30 +00:00
Add clips directory extractor for Twitch
This commit is contained in:
parent
4e7c1ea346
commit
3951482e1e
@ -2253,6 +2253,7 @@
|
|||||||
from .twitch import (
|
from .twitch import (
|
||||||
TwitchClipsIE,
|
TwitchClipsIE,
|
||||||
TwitchCollectionIE,
|
TwitchCollectionIE,
|
||||||
|
TwitchDirectoryClipsIE,
|
||||||
TwitchStreamIE,
|
TwitchStreamIE,
|
||||||
TwitchVideosClipsIE,
|
TwitchVideosClipsIE,
|
||||||
TwitchVideosCollectionsIE,
|
TwitchVideosCollectionsIE,
|
||||||
|
@ -42,6 +42,7 @@ class TwitchBaseIE(InfoExtractor):
|
|||||||
_OPERATION_HASHES = {
|
_OPERATION_HASHES = {
|
||||||
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
|
'CollectionSideBar': '27111f1b382effad0b6def325caef1909c733fe6a4fbabf54f8d491ef2cf2f14',
|
||||||
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
|
'FilterableVideoTower_Videos': 'a937f1d22e269e39a03b509f65a7490f9fc247d7f83d6ac1421523e3b68042cb',
|
||||||
|
'ClipsCards__Game': 'ebcf54afb9aa5d6cec8aad2c35b84e2737a109dac5b184308aae73a27d176707',
|
||||||
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
|
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
|
||||||
'ShareClipRenderStatus': 'f130048a462a0ac86bb54d653c968c514e9ab9ca94db52368c1179e97b0f16eb',
|
'ShareClipRenderStatus': 'f130048a462a0ac86bb54d653c968c514e9ab9ca94db52368c1179e97b0f16eb',
|
||||||
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
|
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
|
||||||
@ -639,11 +640,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
|
||||||
@ -659,7 +661,7 @@ def _entries(self, channel_name, *args):
|
|||||||
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:
|
||||||
@ -779,6 +781,7 @@ def suitable(cls, url):
|
|||||||
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))
|
||||||
|
|
||||||
@ -800,6 +803,87 @@ def _real_extract(self, url):
|
|||||||
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):
|
||||||
_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)'
|
||||||
|
|
||||||
@ -831,6 +915,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'
|
||||||
|
|
||||||
@ -895,6 +980,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'
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user