Compare commits

...

4 Commits

Author SHA1 Message Date
sepro
7eff676183
[ie/twitch:stream] Fix extractor (#14988)
Closes #14987
Authored by: seproDev
2025-11-11 00:40:08 +01:00
Fahim
1ac7e6005c
[ie/floatplane] Fix extractor (#14984)
Authored by: i3p9
2025-11-10 23:50:12 +01:00
Christopher Albert
f3597cfafc
[ie/TubeTuGraz] Support alternate URL format (#14718)
Closes #14686
Authored by: krystophny
2025-11-10 23:40:16 +01:00
eientei95
3ef867451c
[ie/BunnyCdn] Support player subdomain URLs (#14979)
Authored by: einstein95
2025-11-10 22:26:06 +01:00
4 changed files with 44 additions and 32 deletions

View File

@ -16,7 +16,7 @@ from ..utils.traversal import find_element, traverse_obj
class BunnyCdnIE(InfoExtractor): class BunnyCdnIE(InfoExtractor):
_VALID_URL = r'https?://(?:iframe\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)' _VALID_URL = r'https?://(?:(?:iframe|player)\.mediadelivery\.net|video\.bunnycdn\.com)/(?:embed|play)/(?P<library_id>\d+)/(?P<id>[\da-f-]+)'
_EMBED_REGEX = [rf'<iframe[^>]+src=[\'"](?P<url>{_VALID_URL}[^\'"]*)[\'"]'] _EMBED_REGEX = [rf'<iframe[^>]+src=[\'"](?P<url>{_VALID_URL}[^\'"]*)[\'"]']
_TESTS = [{ _TESTS = [{
'url': 'https://iframe.mediadelivery.net/embed/113933/e73edec1-e381-4c8b-ae73-717a140e0924', 'url': 'https://iframe.mediadelivery.net/embed/113933/e73edec1-e381-4c8b-ae73-717a140e0924',
@ -72,6 +72,9 @@ class BunnyCdnIE(InfoExtractor):
'thumbnail': r're:^https?://.*\.b-cdn\.net/6372f5a3-68df-4ef7-a115-e1110186c477/thumbnail\.jpg', 'thumbnail': r're:^https?://.*\.b-cdn\.net/6372f5a3-68df-4ef7-a115-e1110186c477/thumbnail\.jpg',
}, },
'params': {'skip_download': True}, 'params': {'skip_download': True},
}, {
'url': 'https://player.mediadelivery.net/embed/519128/875880a9-bcc2-4038-9e05-e5024bba9b70',
'only_matching': True,
}] }]
_WEBPAGE_TESTS = [{ _WEBPAGE_TESTS = [{
# Stream requires Referer # Stream requires Referer

View File

@ -6,15 +6,15 @@ from ..utils import (
OnDemandPagedList, OnDemandPagedList,
clean_html, clean_html,
determine_ext, determine_ext,
float_or_none,
format_field, format_field,
int_or_none, int_or_none,
join_nonempty, join_nonempty,
parse_codecs,
parse_iso8601, parse_iso8601,
url_or_none, url_or_none,
urljoin, urljoin,
) )
from ..utils.traversal import traverse_obj from ..utils.traversal import require, traverse_obj
class FloatplaneBaseIE(InfoExtractor): class FloatplaneBaseIE(InfoExtractor):
@ -50,37 +50,31 @@ class FloatplaneBaseIE(InfoExtractor):
media_id = media['id'] media_id = media['id']
media_typ = media.get('type') or 'video' media_typ = media.get('type') or 'video'
metadata = self._download_json(
f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id, query={'id': media_id},
note=f'Downloading {media_typ} metadata', impersonate=self._IMPERSONATE_TARGET)
stream = self._download_json( stream = self._download_json(
f'{self._BASE_URL}/api/v2/cdn/delivery', media_id, query={ f'{self._BASE_URL}/api/v3/delivery/info', media_id,
'type': 'vod' if media_typ == 'video' else 'aod', query={'scenario': 'onDemand', 'entityId': media_id},
'guid': metadata['guid'], note=f'Downloading {media_typ} stream data',
}, note=f'Downloading {media_typ} stream data',
impersonate=self._IMPERSONATE_TARGET) impersonate=self._IMPERSONATE_TARGET)
path_template = traverse_obj(stream, ('resource', 'uri', {str})) metadata = self._download_json(
f'{self._BASE_URL}/api/v3/content/{media_typ}', media_id,
f'Downloading {media_typ} metadata', query={'id': media_id},
fatal=False, impersonate=self._IMPERSONATE_TARGET)
def format_path(params): cdn_base_url = traverse_obj(stream, (
path = path_template 'groups', 0, 'origins', ..., 'url', {url_or_none}, any, {require('cdn base url')}))
for i, val in (params or {}).items():
path = path.replace(f'{{qualityLevelParams.{i}}}', val)
return path
formats = [] formats = []
for quality in traverse_obj(stream, ('resource', 'data', 'qualityLevels', ...)): for variant in traverse_obj(stream, ('groups', 0, 'variants', lambda _, v: v['url'])):
url = urljoin(stream['cdn'], format_path(traverse_obj( format_url = urljoin(cdn_base_url, variant['url'])
stream, ('resource', 'data', 'qualityLevelParams', quality['name'], {dict})))) format_id = traverse_obj(variant, ('name', {str}))
format_id = traverse_obj(quality, ('name', {str}))
hls_aes = {} hls_aes = {}
m3u8_data = None m3u8_data = None
# If we need impersonation for the API, then we need it for HLS keys too: extract in advance # If we need impersonation for the API, then we need it for HLS keys too: extract in advance
if self._IMPERSONATE_TARGET is not None: if self._IMPERSONATE_TARGET is not None:
m3u8_data = self._download_webpage( m3u8_data = self._download_webpage(
url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS, format_url, media_id, fatal=False, impersonate=self._IMPERSONATE_TARGET, headers=self._HEADERS,
note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '), note=join_nonempty('Downloading', format_id, 'm3u8 information', delim=' '),
errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' ')) errnote=join_nonempty('Failed to download', format_id, 'm3u8 information', delim=' '))
if not m3u8_data: if not m3u8_data:
@ -98,14 +92,19 @@ class FloatplaneBaseIE(InfoExtractor):
hls_aes['key'] = urlh.read().hex() hls_aes['key'] = urlh.read().hex()
formats.append({ formats.append({
**traverse_obj(quality, { **traverse_obj(variant, {
'format_note': ('label', {str}), 'format_note': ('label', {str}),
'width': ('width', {int}), 'width': ('meta', 'video', 'width', {int_or_none}),
'height': ('height', {int}), 'height': ('meta', 'video', 'height', {int_or_none}),
'vcodec': ('meta', 'video', 'codec', {str}),
'acodec': ('meta', 'audio', 'codec', {str}),
'vbr': ('meta', 'video', 'bitrate', 'average', {int_or_none(scale=1000)}),
'abr': ('meta', 'audio', 'bitrate', 'average', {int_or_none(scale=1000)}),
'audio_channels': ('meta', 'audio', 'channelCount', {int_or_none}),
'fps': ('meta', 'video', 'fps', {float_or_none}),
}), }),
**parse_codecs(quality.get('codecs')), 'url': format_url,
'url': url, 'ext': determine_ext(format_url.partition('/chunk.m3u8')[0], 'mp4'),
'ext': determine_ext(url.partition('/chunk.m3u8')[0], 'mp4'),
'format_id': format_id, 'format_id': format_id,
'hls_media_playlist_data': m3u8_data, 'hls_media_playlist_data': m3u8_data,
'hls_aes': hls_aes or None, 'hls_aes': hls_aes or None,

View File

@ -136,8 +136,10 @@ class TubeTuGrazIE(TubeTuGrazBaseIE):
IE_DESC = 'tube.tugraz.at' IE_DESC = 'tube.tugraz.at'
_VALID_URL = r'''(?x) _VALID_URL = r'''(?x)
https?://tube\.tugraz\.at/paella/ui/watch.html\?id= https?://tube\.tugraz\.at/(?:
(?P<id>[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}) paella/ui/watch\.html\?(?:[^#]*&)?id=|
portal/watch/
)(?P<id>[0-9a-fA-F]{8}-(?:[0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12})
''' '''
_TESTS = [ _TESTS = [
{ {
@ -152,6 +154,7 @@ class TubeTuGrazIE(TubeTuGrazBaseIE):
'creator': 'Safran C', 'creator': 'Safran C',
'duration': 3295818, 'duration': 3295818,
'series_id': 'b1192fff-2aa7-4bf0-a5cf-7b15c3bd3b34', 'series_id': 'b1192fff-2aa7-4bf0-a5cf-7b15c3bd3b34',
'creators': ['Safran C'],
}, },
}, { }, {
'url': 'https://tube.tugraz.at/paella/ui/watch.html?id=2df6d787-e56a-428d-8ef4-d57f07eef238', 'url': 'https://tube.tugraz.at/paella/ui/watch.html?id=2df6d787-e56a-428d-8ef4-d57f07eef238',
@ -162,6 +165,10 @@ class TubeTuGrazIE(TubeTuGrazBaseIE):
'ext': 'mp4', 'ext': 'mp4',
}, },
'expected_warnings': ['Extractor failed to obtain "title"'], 'expected_warnings': ['Extractor failed to obtain "title"'],
}, {
# Portal URL format
'url': 'https://tube.tugraz.at/portal/watch/ab28ec60-8cbe-4f1a-9b96-a95add56c612',
'only_matching': True,
}, },
] ]

View File

@ -46,7 +46,7 @@ class TwitchBaseIE(InfoExtractor):
'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777', 'ClipsCards__User': 'b73ad2bfaecfd30a9e6c28fada15bd97032c83ec77a0440766a56fe0bd632777',
'ShareClipRenderStatus': 'e0a46b287d760c6890a39d1ccd736af5ec9479a267d02c710e9ac33326b651d2', 'ShareClipRenderStatus': 'e0a46b287d760c6890a39d1ccd736af5ec9479a267d02c710e9ac33326b651d2',
'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9', 'ChannelCollectionsContent': '447aec6a0cc1e8d0a8d7732d47eb0762c336a2294fdb009e9c9d854e49d484b9',
'StreamMetadata': 'a647c2a13599e5991e175155f798ca7f1ecddde73f7f341f39009c14dbf59962', 'StreamMetadata': 'b57f9b910f8cd1a4659d894fe7550ccc81ec9052c01e438b290fd66a040b9b93',
'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01', 'ComscoreStreamingQuery': 'e1edae8122517d013405f237ffcc124515dc6ded82480a88daef69c83b53ac01',
'VideoPreviewOverlay': '3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c', 'VideoPreviewOverlay': '3006e77e51b128d838fa4e835723ca4dc9a05c5efd4466c1085215c6e437e65c',
'VideoMetadata': '49b5b8f268cdeb259d75b58dcb0c1a748e3b575003448a2333dc5cdafd49adad', 'VideoMetadata': '49b5b8f268cdeb259d75b58dcb0c1a748e3b575003448a2333dc5cdafd49adad',
@ -1050,7 +1050,10 @@ class TwitchStreamIE(TwitchVideosBaseIE):
gql = self._download_gql( gql = self._download_gql(
channel_name, [{ channel_name, [{
'operationName': 'StreamMetadata', 'operationName': 'StreamMetadata',
'variables': {'channelLogin': channel_name}, 'variables': {
'channelLogin': channel_name,
'includeIsDJ': True,
},
}, { }, {
'operationName': 'ComscoreStreamingQuery', 'operationName': 'ComscoreStreamingQuery',
'variables': { 'variables': {