Compare commits

..

No commits in common. "29e257037862f3b2ad65e6e8d2972f9ed89389e3" and "36b29bb3532e008a2aaf3d36d1c6fc3944137930" have entirely different histories.

4 changed files with 45 additions and 87 deletions

View File

@ -84,7 +84,6 @@ from .agora import (
) )
from .airtv import AirTVIE from .airtv import AirTVIE
from .aitube import AitubeKZVideoIE from .aitube import AitubeKZVideoIE
from .alibaba import AlibabaIE
from .aliexpress import AliExpressLiveIE from .aliexpress import AliExpressLiveIE
from .aljazeera import AlJazeeraIE from .aljazeera import AlJazeeraIE
from .allocine import AllocineIE from .allocine import AllocineIE

View File

@ -1,42 +0,0 @@
from .common import InfoExtractor
from ..utils import int_or_none, str_or_none, url_or_none
from ..utils.traversal import traverse_obj
class AlibabaIE(InfoExtractor):
_VALID_URL = r'https?://(?:www\.)?alibaba\.com/product-detail/[\w-]+_(?P<id>\d+)\.html'
_TESTS = [{
'url': 'https://www.alibaba.com/product-detail/Kids-Entertainment-Bouncer-Bouncy-Castle-Waterslide_1601271126969.html',
'info_dict': {
'id': '6000280444270',
'display_id': '1601271126969',
'ext': 'mp4',
'title': 'Kids Entertainment Bouncer Bouncy Castle Waterslide Juex Gonflables Commercial Inflatable Tropical Water Slide',
'duration': 30,
'thumbnail': 'https://sc04.alicdn.com/kf/Hc5bb391974454af18c7a4f91cbe4062bg.jpg_120x120.jpg',
},
}]
def _real_extract(self, url):
display_id = self._match_id(url)
webpage = self._download_webpage(url, display_id)
product_data = self._search_json(
r'window\.detailData\s*=', webpage, 'detail data', display_id)['globalData']['product']
return {
**traverse_obj(product_data, ('mediaItems', lambda _, v: v['type'] == 'video' and v['videoId'], any, {
'id': ('videoId', {int}, {str_or_none}),
'duration': ('duration', {int_or_none}),
'thumbnail': ('videoCoverUrl', {url_or_none}),
'formats': ('videoUrl', lambda _, v: url_or_none(v['videoUrl']), {
'url': 'videoUrl',
'format_id': ('definition', {str_or_none}),
'tbr': ('bitrate', {int_or_none}),
'width': ('width', {int_or_none}),
'height': ('height', {int_or_none}),
'filesize': ('length', {int_or_none}),
}),
})),
'title': traverse_obj(product_data, ('subject', {str})),
'display_id': display_id,
}

View File

@ -8,11 +8,10 @@ from ..utils import (
class SportDeutschlandIE(InfoExtractor): class SportDeutschlandIE(InfoExtractor):
IE_NAME = 'sporteurope' _VALID_URL = r'https?://(?:player\.)?sportdeutschland\.tv/(?P<id>(?:[^/?#]+/)?[^?#/&]+)'
_VALID_URL = r'https?://(?:player\.)?sporteurope\.tv/(?P<id>(?:[^/?#]+/)?[^?#/&]+)'
_TESTS = [{ _TESTS = [{
# Single-part video, direct link # Single-part video, direct link
'url': 'https://sporteurope.tv/rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates', 'url': 'https://sportdeutschland.tv/rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates',
'md5': '35c11a19395c938cdd076b93bda54cde', 'md5': '35c11a19395c938cdd076b93bda54cde',
'info_dict': { 'info_dict': {
'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a', 'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a',
@ -20,9 +19,9 @@ class SportDeutschlandIE(InfoExtractor):
'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates', 'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates',
'display_id': 'rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates', 'display_id': 'rostock-griffins/gfl2-rostock-griffins-vs-elmshorn-fighting-pirates',
'channel': 'Rostock Griffins', 'channel': 'Rostock Griffins',
'channel_url': 'https://sporteurope.tv/rostock-griffins', 'channel_url': 'https://sportdeutschland.tv/rostock-griffins',
'live_status': 'was_live', 'live_status': 'was_live',
'description': r're:Video-Livestream des Spiels Rostock Griffins vs\. Elmshorn Fighting Pirates.+', 'description': 'md5:60cb00067e55dafa27b0933a43d72862',
'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b', 'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b',
'timestamp': 1749913117, 'timestamp': 1749913117,
'upload_date': '20250614', 'upload_date': '20250614',
@ -30,16 +29,16 @@ class SportDeutschlandIE(InfoExtractor):
}, },
}, { }, {
# Single-part video, embedded player link # Single-part video, embedded player link
'url': 'https://player.sporteurope.tv/9e9619c4-7d77-43c4-926d-49fb57dc06dc', 'url': 'https://player.sportdeutschland.tv/9e9619c4-7d77-43c4-926d-49fb57dc06dc',
'info_dict': { 'info_dict': {
'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a', 'id': '9f27a97d-1544-4d0b-aa03-48d92d17a03a',
'ext': 'mp4', 'ext': 'mp4',
'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates', 'title': 'GFL2: Rostock Griffins vs. Elmshorn Fighting Pirates',
'display_id': '9e9619c4-7d77-43c4-926d-49fb57dc06dc', 'display_id': '9e9619c4-7d77-43c4-926d-49fb57dc06dc',
'channel': 'Rostock Griffins', 'channel': 'Rostock Griffins',
'channel_url': 'https://sporteurope.tv/rostock-griffins', 'channel_url': 'https://sportdeutschland.tv/rostock-griffins',
'live_status': 'was_live', 'live_status': 'was_live',
'description': r're:Video-Livestream des Spiels Rostock Griffins vs\. Elmshorn Fighting Pirates.+', 'description': 'md5:60cb00067e55dafa27b0933a43d72862',
'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b', 'channel_id': '9635f21c-3f67-4584-9ce4-796e9a47276b',
'timestamp': 1749913117, 'timestamp': 1749913117,
'upload_date': '20250614', 'upload_date': '20250614',
@ -48,7 +47,7 @@ class SportDeutschlandIE(InfoExtractor):
'params': {'skip_download': True}, 'params': {'skip_download': True},
}, { }, {
# Multi-part video # Multi-part video
'url': 'https://sporteurope.tv/rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2', 'url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2',
'info_dict': { 'info_dict': {
'id': '9f63d737-2444-4e3a-a1ea-840df73fd481', 'id': '9f63d737-2444-4e3a-a1ea-840df73fd481',
'display_id': 'rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2', 'display_id': 'rhine-ruhr-2025-fisu-world-university-games/volleyball-w-japan-vs-brasilien-halbfinale-2',
@ -56,7 +55,7 @@ class SportDeutschlandIE(InfoExtractor):
'description': 'md5:0a17da15e48a687e6019639c3452572b', 'description': 'md5:0a17da15e48a687e6019639c3452572b',
'channel': 'Rhine-Ruhr 2025 FISU World University Games', 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334', 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
'channel_url': 'https://sporteurope.tv/rhine-ruhr-2025-fisu-world-university-games', 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
'live_status': 'was_live', 'live_status': 'was_live',
}, },
'playlist_count': 2, 'playlist_count': 2,
@ -67,7 +66,7 @@ class SportDeutschlandIE(InfoExtractor):
'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 1', 'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 1',
'channel': 'Rhine-Ruhr 2025 FISU World University Games', 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334', 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
'channel_url': 'https://sporteurope.tv/rhine-ruhr-2025-fisu-world-university-games', 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
'duration': 14773.0, 'duration': 14773.0,
'timestamp': 1753085197, 'timestamp': 1753085197,
'upload_date': '20250721', 'upload_date': '20250721',
@ -80,17 +79,16 @@ class SportDeutschlandIE(InfoExtractor):
'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 2', 'title': 'Volleyball w: Japan vs. Braslien - Halbfinale 2 Part 2',
'channel': 'Rhine-Ruhr 2025 FISU World University Games', 'channel': 'Rhine-Ruhr 2025 FISU World University Games',
'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334', 'channel_id': '9f5216be-a49d-470b-9a30-4fe9df993334',
'channel_url': 'https://sporteurope.tv/rhine-ruhr-2025-fisu-world-university-games', 'channel_url': 'https://sportdeutschland.tv/rhine-ruhr-2025-fisu-world-university-games',
'duration': 14773.0, 'duration': 14773.0,
'timestamp': 1753128421, 'timestamp': 1753128421,
'upload_date': '20250721', 'upload_date': '20250721',
'live_status': 'was_live', 'live_status': 'was_live',
}, },
}], }],
'skip': '404 Not Found',
}, { }, {
# Livestream # Livestream
'url': 'https://sporteurope.tv/dtb/gymnastik-international-tag-1', 'url': 'https://sportdeutschland.tv/dtb/gymnastik-international-tag-1',
'info_dict': { 'info_dict': {
'id': '95d71b8a-370a-4b87-ad16-94680da18528', 'id': '95d71b8a-370a-4b87-ad16-94680da18528',
'ext': 'mp4', 'ext': 'mp4',
@ -98,7 +96,7 @@ class SportDeutschlandIE(InfoExtractor):
'display_id': 'dtb/gymnastik-international-tag-1', 'display_id': 'dtb/gymnastik-international-tag-1',
'channel_id': '936ecef1-2f4a-4e08-be2f-68073cb7ecab', 'channel_id': '936ecef1-2f4a-4e08-be2f-68073cb7ecab',
'channel': 'Deutscher Turner-Bund', 'channel': 'Deutscher Turner-Bund',
'channel_url': 'https://sporteurope.tv/dtb', 'channel_url': 'https://sportdeutschland.tv/dtb',
'description': 'md5:07a885dde5838a6f0796ee21dc3b0c52', 'description': 'md5:07a885dde5838a6f0796ee21dc3b0c52',
'live_status': 'is_live', 'live_status': 'is_live',
}, },
@ -108,9 +106,9 @@ class SportDeutschlandIE(InfoExtractor):
def _process_video(self, asset_id, video): def _process_video(self, asset_id, video):
is_live = video['type'] == 'mux_live' is_live = video['type'] == 'mux_live'
token = self._download_json( token = self._download_json(
f'https://api.sporteurope.tv/api/web/personal/asset-token/{asset_id}', f'https://api.sportdeutschland.tv/api/web/personal/asset-token/{asset_id}',
video['id'], query={'type': video['type'], 'playback_id': video['src']}, video['id'], query={'type': video['type'], 'playback_id': video['src']},
headers={'Referer': 'https://sporteurope.tv/'})['token'] headers={'Referer': 'https://sportdeutschland.tv/'})['token']
formats, subtitles = self._extract_m3u8_formats_and_subtitles( formats, subtitles = self._extract_m3u8_formats_and_subtitles(
f'https://stream.mux.com/{video["src"]}.m3u8?token={token}', video['id'], live=is_live) f'https://stream.mux.com/{video["src"]}.m3u8?token={token}', video['id'], live=is_live)
@ -128,7 +126,7 @@ class SportDeutschlandIE(InfoExtractor):
def _real_extract(self, url): def _real_extract(self, url):
display_id = self._match_id(url) display_id = self._match_id(url)
meta = self._download_json( meta = self._download_json(
f'https://api.sporteurope.tv/api/stateless/frontend/assets/{display_id}', f'https://api.sportdeutschland.tv/api/stateless/frontend/assets/{display_id}',
display_id, query={'access_token': 'true'}) display_id, query={'access_token': 'true'})
info = { info = {
@ -141,7 +139,7 @@ class SportDeutschlandIE(InfoExtractor):
'channel_id': ('profile', 'id'), 'channel_id': ('profile', 'id'),
'is_live': 'currently_live', 'is_live': 'currently_live',
'was_live': 'was_live', 'was_live': 'was_live',
'channel_url': ('profile', 'slug', {lambda x: f'https://sporteurope.tv/{x}'}), 'channel_url': ('profile', 'slug', {lambda x: f'https://sportdeutschland.tv/{x}'}),
}, get_all=False), }, get_all=False),
} }

View File

@ -1,6 +1,8 @@
import base64
import codecs
import itertools import itertools
import re import re
import urllib.parse import string
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import ( from ..utils import (
@ -14,6 +16,7 @@ from ..utils import (
join_nonempty, join_nonempty,
parse_duration, parse_duration,
str_or_none, str_or_none,
try_call,
try_get, try_get,
unified_strdate, unified_strdate,
url_or_none, url_or_none,
@ -29,7 +32,7 @@ class _ByteGenerator:
try: try:
self._algorithm = getattr(self, f'_algo{algo_id}') self._algorithm = getattr(self, f'_algo{algo_id}')
except AttributeError: except AttributeError:
raise ExtractorError(f'Unknown algorithm ID "{algo_id}"') raise ExtractorError(f'Unknown algorithm ID: {algo_id}')
self._s = to_signed_32(seed) self._s = to_signed_32(seed)
def _algo1(self, s): def _algo1(self, s):
@ -213,28 +216,32 @@ class XHamsterIE(InfoExtractor):
'only_matching': True, 'only_matching': True,
}] }]
_XOR_KEY = b'xh7999'
def _decipher_format_url(self, format_url, format_id): def _decipher_format_url(self, format_url, format_id):
parsed_url = urllib.parse.urlparse(format_url) if all(char in string.hexdigits for char in format_url):
byte_data = bytes.fromhex(format_url)
hex_string, path_remainder = self._search_regex( seed = int.from_bytes(byte_data[1:5], byteorder='little', signed=True)
r'^/(?P<hex>[0-9a-fA-F]{12,})(?P<rem>[/,].+)$', parsed_url.path, 'url components',
default=(None, None), group=('hex', 'rem'))
if not hex_string:
self.report_warning(f'Skipping format "{format_id}": unsupported URL format')
return None
byte_data = bytes.fromhex(hex_string)
seed = int.from_bytes(byte_data[1:5], byteorder='little', signed=True)
try:
byte_gen = _ByteGenerator(byte_data[0], seed) byte_gen = _ByteGenerator(byte_data[0], seed)
except ExtractorError as e: return bytearray(byte ^ next(byte_gen) for byte in byte_data[5:]).decode('latin-1')
self.report_warning(f'Skipping format "{format_id}": {e.msg}')
cipher_type, _, ciphertext = try_call(
lambda: base64.b64decode(format_url).decode().partition('_')) or [None] * 3
if not cipher_type or not ciphertext:
self.report_warning(f'Skipping format "{format_id}": failed to decipher URL')
return None return None
deciphered = bytearray(byte ^ next(byte_gen) for byte in byte_data[5:]).decode('latin-1') if cipher_type == 'xor':
return bytes(
a ^ b for a, b in
zip(ciphertext.encode(), itertools.cycle(self._XOR_KEY))).decode()
return parsed_url._replace(path=f'/{deciphered}{path_remainder}').geturl() if cipher_type == 'rot13':
return codecs.decode(ciphertext, cipher_type)
self.report_warning(f'Skipping format "{format_id}": unsupported cipher type "{cipher_type}"')
return None
def _fixup_formats(self, formats): def _fixup_formats(self, formats):
for f in formats: for f in formats:
@ -357,11 +364,8 @@ class XHamsterIE(InfoExtractor):
'height': get_height(quality), 'height': get_height(quality),
'filesize': format_sizes.get(quality), 'filesize': format_sizes.get(quality),
'http_headers': { 'http_headers': {
'Referer': urlh.url, 'Referer': standard_url,
}, },
# HTTP formats return "Wrong key" error even when deciphered by site JS
# TODO: Remove this when resolved on the site's end
'__needs_testing': True,
}) })
categories_list = video.get('categories') categories_list = video.get('categories')
@ -398,8 +402,7 @@ class XHamsterIE(InfoExtractor):
'age_limit': age_limit if age_limit is not None else 18, 'age_limit': age_limit if age_limit is not None else 18,
'categories': categories, 'categories': categories,
'formats': self._fixup_formats(formats), 'formats': self._fixup_formats(formats),
# TODO: Revert to ('res', 'proto', 'tbr') when HTTP formats problem is resolved '_format_sort_fields': ('res', 'proto', 'tbr'),
'_format_sort_fields': ('res', 'proto:m3u8', 'tbr'),
} }
# Old layout fallback # Old layout fallback