Compare commits

...

7 Commits

Author SHA1 Message Date
shloop
0bac1ac219
Merge 38e08a1147 into b83ca24eb7 2024-11-10 09:19:38 +05:30
sepro
b83ca24eb7
[core] Catch broken Cryptodome installations (#11486)
Authored by: seproDev
2024-11-10 00:53:49 +01:00
bashonly
240a7d43c8
[build] Pin websockets version to >=13.0,<14 (#11488)
websockets 14.0 causes CI test failures (a lot more of them)

Authored by: bashonly
2024-11-09 23:46:47 +00:00
bashonly
f13df591d4
[build] Enable attestations for trusted publishing (#11420)
Reverts 428ffb75aa

Authored by: bashonly
2024-11-09 23:26:02 +00:00
shloop
38e08a1147 -Updates URL scheme
-Includes suggested changes
-Updates test cases
2024-08-17 13:45:46 -07:00
shloop
2aa9e75975 merges _extractors.py 2024-08-17 12:59:44 -07:00
shloop
a2f4163e25 Adds xumo extractor. 2023-02-20 17:42:13 -08:00
8 changed files with 231 additions and 8 deletions

View File

@ -504,7 +504,8 @@ jobs:
- windows32 - windows32
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/download-artifact@v4 - name: Download artifacts
uses: actions/download-artifact@v4
with: with:
path: artifact path: artifact
pattern: build-bin-* pattern: build-bin-*

View File

@ -28,3 +28,20 @@ jobs:
actions: write # For cleaning up cache actions: write # For cleaning up cache
id-token: write # mandatory for trusted publishing id-token: write # mandatory for trusted publishing
secrets: inherit secrets: inherit
publish_pypi:
needs: [release]
if: vars.MASTER_PYPI_PROJECT != ''
runs-on: ubuntu-latest
permissions:
id-token: write # mandatory for trusted publishing
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist
name: build-pypi
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true

View File

@ -41,3 +41,20 @@ jobs:
actions: write # For cleaning up cache actions: write # For cleaning up cache
id-token: write # mandatory for trusted publishing id-token: write # mandatory for trusted publishing
secrets: inherit secrets: inherit
publish_pypi:
needs: [release]
if: vars.NIGHTLY_PYPI_PROJECT != ''
runs-on: ubuntu-latest
permissions:
id-token: write # mandatory for trusted publishing
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist
name: build-pypi
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
verbose: true

View File

@ -2,10 +2,6 @@ name: Release
on: on:
workflow_call: workflow_call:
inputs: inputs:
prerelease:
required: false
default: true
type: boolean
source: source:
required: false required: false
default: '' default: ''
@ -18,6 +14,10 @@ on:
required: false required: false
default: '' default: ''
type: string type: string
prerelease:
required: false
default: true
type: boolean
workflow_dispatch: workflow_dispatch:
inputs: inputs:
source: source:
@ -278,11 +278,20 @@ jobs:
make clean-cache make clean-cache
python -m build --no-isolation . python -m build --no-isolation .
- name: Upload artifacts
if: github.event_name != 'workflow_dispatch'
uses: actions/upload-artifact@v4
with:
name: build-pypi
path: |
dist/*
compression-level: 0
- name: Publish to PyPI - name: Publish to PyPI
if: github.event_name == 'workflow_dispatch'
uses: pypa/gh-action-pypi-publish@release/v1 uses: pypa/gh-action-pypi-publish@release/v1
with: with:
verbose: true verbose: true
attestations: false # Currently doesn't work w/ reusable workflows (breaks nightly)
publish: publish:
needs: [prepare, build] needs: [prepare, build]

View File

@ -52,7 +52,7 @@ default = [
"pycryptodomex", "pycryptodomex",
"requests>=2.32.2,<3", "requests>=2.32.2,<3",
"urllib3>=1.26.17,<3", "urllib3>=1.26.17,<3",
"websockets>=13.0", "websockets>=13.0,<14",
] ]
curl-cffi = [ curl-cffi = [
"curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'", "curl-cffi==0.5.10; os_name=='nt' and implementation_name=='cpython'",

View File

@ -24,7 +24,7 @@ try:
from Crypto.Cipher import AES, PKCS1_OAEP, Blowfish, PKCS1_v1_5 # noqa: F401 from Crypto.Cipher import AES, PKCS1_OAEP, Blowfish, PKCS1_v1_5 # noqa: F401
from Crypto.Hash import CMAC, SHA1 # noqa: F401 from Crypto.Hash import CMAC, SHA1 # noqa: F401
from Crypto.PublicKey import RSA # noqa: F401 from Crypto.PublicKey import RSA # noqa: F401
except ImportError: except (ImportError, OSError):
__version__ = f'broken {__version__}'.strip() __version__ = f'broken {__version__}'.strip()

View File

@ -2491,6 +2491,7 @@ from .xinpianchang import XinpianchangIE
from .xminus import XMinusIE from .xminus import XMinusIE
from .xnxx import XNXXIE from .xnxx import XNXXIE
from .xstream import XstreamIE from .xstream import XstreamIE
from .xumo import XumoIE
from .xvideos import ( from .xvideos import (
XVideosIE, XVideosIE,
XVideosQuickiesIE, XVideosQuickiesIE,

178
yt_dlp/extractor/xumo.py Normal file
View File

@ -0,0 +1,178 @@
from .common import InfoExtractor
from ..utils import determine_ext, int_or_none, smuggle_url, traverse_obj, unsmuggle_url, url_or_none
class XumoIE(InfoExtractor):
_VALID_URL = r'https?://play\.?xumo\.com/[^?#]+/(?P<id>XM[A-Z0-9]{12})'
_TESTS = [{
# movie
'url': 'https://play.xumo.com/free-movies/a-circus-tale-and-a-love-song/XM041I5U497VD3',
'params': {
'check_formats': True,
},
'md5': 'eaac858a8db4ee5a67d6d16920c24e15',
'info_dict': {
'id': 'XM041I5U497VD3',
'title': 'A Circus Tale & A Love Song',
'ext': 'mp4',
'description': 'md5:aa6372f4785c528ff04c94a275f63446',
'duration': 6887,
'release_year': 2016,
'thumbnail': r're:^https?://.*\.jpg$',
},
}, {
# entire series
'url': 'https://play.xumo.com/tv-shows/super-mario-world/XM0AN69OG47PRN',
'params': {
'skip_download': True,
},
'playlist_count': 10,
'info_dict': {
'id': 'XM0AN69OG47PRN',
'title': 'Super Mario World',
},
}, {
# episode of series
'url': 'https://play.xumo.com/tv-shows/99991299/XM02D369HADFRR',
'md5': 'ed2f396272b39f2e0fe47f02b9ae34cc',
'info_dict': {
'id': 'XM02D369HADFRR',
'title': 'Fire Sale // Misadventures In Robin Hood Woods',
'ext': 'mp4',
'series': 'Super Mario World',
'season_number': 1,
'episode_number': 2,
'episode': 'Fire Sale // Misadventures In Robin Hood Woods',
'description': 'md5:48134d36781cf4b225ec0ee4f05356d3',
'thumbnail': r're:^https?://.*\.jpg$',
'season': 'Season 1',
'duration': 1368,
},
}, {
# video from network-based alternate URL scheme
'url': 'https://play.xumo.com/networks/fakenetworkname/99991299/XM02D369HADFRR',
'md5': 'ed2f396272b39f2e0fe47f02b9ae34cc',
'info_dict': {
'id': 'XM02D369HADFRR',
'title': 'Fire Sale // Misadventures In Robin Hood Woods',
'ext': 'mp4',
'series': 'Super Mario World',
'season_number': 1,
'episode_number': 2,
'episode': 'Fire Sale // Misadventures In Robin Hood Woods',
'description': 'md5:48134d36781cf4b225ec0ee4f05356d3',
'thumbnail': r're:^https?://.*\.jpg$',
'season': 'Season 1',
'duration': 1368,
},
}]
_INFO_URL = 'https://valencia-app-mds.xumo.com/v2/assets/asset/'
_INFO_QUERY_PARAMS = {
'f': [
'connectorId',
'title',
'providers',
'descriptions',
'runtime',
'originalReleaseYear',
'cuePoints',
'ratings',
'hasCaptions',
'availableSince',
'genres',
'season',
'episode',
'seasons',
'season:all',
'episodes.episodeTitle',
'episodes.runtime',
'episodes.descriptions',
'episodes.hasCaptions',
'episodes.ratings',
],
}
def _get_video_links(self, video_id, info_json):
formats, subtitles = [], {}
for source in traverse_obj(info_json, ('providers', ..., 'sources', ...)) or []:
fmts, subs = [], {}
format_url = url_or_none(source.get('uri'))
if not format_url:
continue
ext = determine_ext(format_url)
if ext == 'm3u8':
fmts, subs = self._extract_m3u8_formats_and_subtitles(
format_url, video_id, m3u8_id='hls', fatal=False)
elif ext == 'mpd':
fmts, subs = self._extract_mpd_formats_and_subtitles(
format_url, video_id, mpd_id='dash', fatal=False)
elif format_url.endswith('.ism/Manifest'):
fmts, subs = self._extract_ism_formats_and_subtitles(
format_url, video_id, ism_id='mss', fatal=False)
if source.get('drm'):
for f in fmts:
f['has_drm'] = True
formats.extend(fmts)
subtitles = self._merge_subtitles(subtitles, subs)
for caption in traverse_obj(info_json, ('providers', ..., 'captions', ...)):
subtitles.setdefault(caption.get('lang') or 'und', []).append({
'url': caption.get('url'),
})
return formats, subtitles
def _real_extract(self, url):
url, smuggled_data = unsmuggle_url(url)
media_id = self._match_valid_url(url).group('id')
media_metadata = self._download_json(f'{self._INFO_URL}{media_id}.json', media_id, query=self._INFO_QUERY_PARAMS)
title = media_metadata.get('title')
content_type = media_metadata['contentType']
if content_type == 'SERIES':
# series => return set of URLs pointing to individual episodes and smuggle series title to avoid extra API call for each episode
return self.playlist_result([
self.url_result(
smuggle_url(f'https://play.xumo.com/tv-shows/x/{episode["id"]}', {'series': title}),
XumoIE, episode['id'], episode.get('episodeTitle'))
for episode in traverse_obj(media_metadata, ('seasons', ..., 'episodes', ...))
], media_id, title)
# video => return video info
season_number = None
series_title = None
is_episode = content_type == 'EPISODIC'
if is_episode:
season_number = int_or_none(media_metadata.get('season'))
if smuggled_data:
series_title = traverse_obj(smuggled_data, 'series')
else:
series_data = self._download_json(f'{self._INFO_URL}{media_metadata["connectorId"]}.json', media_id, query=self._INFO_QUERY_PARAMS)
series_title = traverse_obj(series_data, 'title')
formats, subtitles = self._get_video_links(media_id, media_metadata)
return {
'id': media_id,
'title': title,
'description': traverse_obj(media_metadata, ('descriptions', ('large', 'medium', 'small', 'tiny')), get_all=False),
'release_year': media_metadata.get('originalReleaseYear'),
'duration': media_metadata.get('runtime'),
'thumbnail': f'https://image.xumo.com/v1/assets/asset/{media_id}/1024x576.jpg',
'formats': formats,
'subtitles': subtitles,
'episode_number': media_metadata.get('episode'),
'season_number': season_number,
'episode': title if is_episode else None,
'series': series_title,
}