1
0
mirror of https://github.com/yt-dlp/yt-dlp.git synced 2025-06-27 17:08:32 +00:00
This commit is contained in:
doe1080 2025-06-26 17:03:12 -07:00 committed by GitHub
commit 84bfc43ae7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 127 additions and 0 deletions

View File

@ -1972,6 +1972,7 @@
SproutVideoIE,
VidsIoIE,
)
from .spwn import SPWNIE
from .srgssr import (
SRGSSRIE,
SRGSSRPlayIE,

126
yt_dlp/extractor/spwn.py Normal file
View File

@ -0,0 +1,126 @@
import json
import time
from .wrestleuniverse import WrestleUniverseBaseIE
from ..utils import (
ExtractorError,
clean_html,
parse_iso8601,
parse_qs,
update_url,
url_or_none,
)
from ..utils.traversal import require, traverse_obj
class SPWNIE(WrestleUniverseBaseIE):
_BASE_URL = 'https://spwn.jp'
_LOGIN_HEADERS = {'Content-Type': 'application/json'}
_LOGIN_HINT = (
'Use --username refresh --password <refreshToken>, --username and --password, '
'--netrc-cmd, or --netrc (spwn) to provide account credentials')
_LOGIN_QUERY = {'key': 'AIzaSyC-RDWv-QnYNWsJ6geLPpFYArlo2uPWCpA'}
_NETRC_MACHINE = 'spwn'
_VALID_URL = r'https?://spwn\.jp/events/(?P<id>[^/?#]+)(/streaming)?'
_TESTS = [{
'url': 'https://spwn.jp/events/evt_wG62Svb4HkCQkK3QwDqI/streaming?vid=ki6OfjHeVlDGVKtEJwe4',
'info_dict': {
'id': 'ki6OfjHeVlDGVKtEJwe4',
'ext': 'mp4',
'title': '本編',
'categories': 'count:1',
'description': 'md5:7b6ef6bc591dc3e8641acf224a2345fe',
'release_date': '20250606',
'release_timestamp': 1749200400,
'series': '劇場版「ポールプリンセス!!」【劇場版アニメ本編配信】',
'series_id': 'evt_wG62Svb4HkCQkK3QwDqI',
'tags': 'count:2',
'thumbnail': r're:https?://public-web\.spwn\.jp/events/.+',
'timestamp': 1749200400,
'upload_date': '20250606',
},
'skip': 'Paid only',
}, {
'url': 'https://spwn.jp/events/evt_wG62Svb4HkCQkK3QwDqI',
'info_dict': {
'id': 'evt_wG62Svb4HkCQkK3QwDqI',
'title': '劇場版「ポールプリンセス!!」【劇場版アニメ本編配信】',
},
'playlist_count': 2,
'skip': 'Paid only',
}]
@WrestleUniverseBaseIE._TOKEN.getter
def _TOKEN(self):
if not self._REAL_TOKEN or self._TOKEN_EXPIRY <= int(time.time()):
if not self._REFRESH_TOKEN:
self.raise_login_required(
f'No refreshToken provided. {self._LOGIN_HINT}', method=None)
self._refresh_token()
return self._REAL_TOKEN
def _perform_login(self, username, password):
if username.lower() == 'refresh':
self._REFRESH_TOKEN = password
return self._refresh_token()
return super()._perform_login(username, password)
def _call_api(self, event_id, note, url=None, path=None):
if not url:
url = f'https://firestore.googleapis.com/v1/projects/spwn-balus/databases/(default)/documents/{path}'
return traverse_obj(self._download_json(
url, event_id, f'Downloading {note}', fatal=False,
), ('fields', {dict}), default={})
def _real_extract(self, url):
event_id = self._match_id(url)
event = self._call_api(event_id, 'event info', path=f'events/{event_id}')
info = self._download_json(
'https://us-central1-spwn-balus.cloudfunctions.net/getStreamingKey/',
event_id, headers={
'Authorization': f'Bearer {self._TOKEN}',
'Content-Type': 'application/json',
'Origin': self._BASE_URL,
}, data=json.dumps({'eid': event_id}).encode())
if traverse_obj(info, ('isError', {bool})):
msg = traverse_obj(info, ('msg', {str}, filter))
raise ExtractorError(
msg or 'API returned an error response', expected=bool(msg))
video_id = traverse_obj(parse_qs(url), ('vid', 0, {str}))
if not video_id:
return self.playlist_result([self.url_result(
f'{self._BASE_URL}/events/{event_id}/streaming?vid={vid}', SPWNIE,
) for vid in traverse_obj(info, ('videoIds', ..., {str}, filter))],
event_id, traverse_obj(event, ('title', 'stringValue', {str})))
video = self._call_api(event_id, 'video info', path=f'streaming/{event_id}/videos/{video_id}')
policy, signature, m3u8_url = traverse_obj(info, ('cookies', video_id, 'default', (
('cookie', 'CloudFront-Policy', {str}, {require('CloudFront policy')}),
('cookie', 'CloudFront-Signature', {str}, {require('CloudFront signature')}),
('url', {url_or_none}, {require('manifest URL')}),
)))
return {
'id': video_id,
'title': traverse_obj(video, ('name', 'stringValue', {str})),
'formats': self._extract_m3u8_formats(
update_url(m3u8_url, netloc='vod.spwnlive.net'), video_id,
'mp4', query={'policy': policy, 'signature': signature}),
'series_id': event_id,
**traverse_obj(event, {
'cast': ('artistRefs', 'arrayValue', 'values', ..., 'referenceValue',
{lambda x: self._call_api(event_id, 'artist info', url=f'{self._API_BASE}/{x}')},
'name', 'stringValue', {str}, filter, all, filter),
'categories': ('categories', 'arrayValue', 'values', ..., 'stringValue', {str}, filter, all, filter),
'description': ('description', 'stringValue', {clean_html}, {clean_html}, filter),
'series': ('title', 'stringValue', {str}),
'tags': ('twitterHashTag', 'arrayValue', 'values', ..., 'stringValue', {str}, filter, all, filter),
'thumbnail': ('defaultImg', 'stringValue', {url_or_none}),
}),
**traverse_obj(event, ('parts', 'arrayValue', 'values', ..., 'mapValue', 'fields', {
'release_timestamp': ('openTime', 'timestampValue', {parse_iso8601}),
'timestamp': ('startTime', 'timestampValue', {parse_iso8601}),
}, any)),
}