logo

oasis-root

Compiled tree of Oasis Linux based on own branch at <https://hacktivis.me/git/oasis/> git clone https://anongit.hacktivis.me/git/oasis-root.git

wrestleuniverse.py (13252B)


  1. import base64
  2. import binascii
  3. import json
  4. import time
  5. import uuid
  6. from .common import InfoExtractor
  7. from ..dependencies import Cryptodome
  8. from ..utils import (
  9. ExtractorError,
  10. int_or_none,
  11. jwt_decode_hs256,
  12. traverse_obj,
  13. try_call,
  14. url_basename,
  15. url_or_none,
  16. urlencode_postdata,
  17. variadic,
  18. )
  19. class WrestleUniverseBaseIE(InfoExtractor):
  20. _NETRC_MACHINE = 'wrestleuniverse'
  21. _VALID_URL_TMPL = r'https?://(?:www\.)?wrestle-universe\.com/(?:(?P<lang>\w{2})/)?%s/(?P<id>\w+)'
  22. _API_HOST = 'api.wrestle-universe.com'
  23. _API_PATH = None
  24. _REAL_TOKEN = None
  25. _TOKEN_EXPIRY = None
  26. _REFRESH_TOKEN = None
  27. _DEVICE_ID = None
  28. _LOGIN_QUERY = {'key': 'AIzaSyCaRPBsDQYVDUWWBXjsTrHESi2r_F3RAdA'}
  29. _LOGIN_HEADERS = {
  30. 'Accept': '*/*',
  31. 'Content-Type': 'application/json',
  32. 'X-Client-Version': 'Chrome/JsCore/9.9.4/FirebaseCore-web',
  33. 'X-Firebase-gmpid': '1:307308870738:web:820f38fe5150c8976e338b',
  34. 'Referer': 'https://www.wrestle-universe.com/',
  35. 'Origin': 'https://www.wrestle-universe.com',
  36. }
  37. @property
  38. def _TOKEN(self):
  39. if not self._REAL_TOKEN or not self._TOKEN_EXPIRY:
  40. token = try_call(lambda: self._get_cookies('https://www.wrestle-universe.com/')['token'].value)
  41. if not token and not self._REFRESH_TOKEN:
  42. self.raise_login_required()
  43. self._TOKEN = token
  44. if not self._REAL_TOKEN or self._TOKEN_EXPIRY <= int(time.time()):
  45. if not self._REFRESH_TOKEN:
  46. raise ExtractorError(
  47. 'Expired token. Refresh your cookies in browser and try again', expected=True)
  48. self._refresh_token()
  49. return self._REAL_TOKEN
  50. @_TOKEN.setter
  51. def _TOKEN(self, value):
  52. self._REAL_TOKEN = value
  53. expiry = traverse_obj(value, ({jwt_decode_hs256}, 'exp', {int_or_none}))
  54. if not expiry:
  55. raise ExtractorError('There was a problem with the auth token')
  56. self._TOKEN_EXPIRY = expiry
  57. def _perform_login(self, username, password):
  58. login = self._download_json(
  59. 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword', None,
  60. 'Logging in', query=self._LOGIN_QUERY, headers=self._LOGIN_HEADERS, data=json.dumps({
  61. 'returnSecureToken': True,
  62. 'email': username,
  63. 'password': password,
  64. }, separators=(',', ':')).encode(), expected_status=400)
  65. token = traverse_obj(login, ('idToken', {str}))
  66. if not token:
  67. raise ExtractorError(
  68. f'Unable to log in: {traverse_obj(login, ("error", "message"))}', expected=True)
  69. self._REFRESH_TOKEN = traverse_obj(login, ('refreshToken', {str}))
  70. if not self._REFRESH_TOKEN:
  71. self.report_warning('No refresh token was granted')
  72. self._TOKEN = token
  73. def _real_initialize(self):
  74. if self._DEVICE_ID:
  75. return
  76. self._DEVICE_ID = self._configuration_arg('device_id', [None], ie_key=self._NETRC_MACHINE)[0]
  77. if not self._DEVICE_ID:
  78. self._DEVICE_ID = self.cache.load(self._NETRC_MACHINE, 'device_id')
  79. if self._DEVICE_ID:
  80. return
  81. self._DEVICE_ID = str(uuid.uuid4())
  82. self.cache.store(self._NETRC_MACHINE, 'device_id', self._DEVICE_ID)
  83. def _refresh_token(self):
  84. refresh = self._download_json(
  85. 'https://securetoken.googleapis.com/v1/token', None, 'Refreshing token',
  86. query=self._LOGIN_QUERY, data=urlencode_postdata({
  87. 'grant_type': 'refresh_token',
  88. 'refresh_token': self._REFRESH_TOKEN,
  89. }), headers={
  90. **self._LOGIN_HEADERS,
  91. 'Content-Type': 'application/x-www-form-urlencoded',
  92. })
  93. if traverse_obj(refresh, ('refresh_token', {str})):
  94. self._REFRESH_TOKEN = refresh['refresh_token']
  95. token = traverse_obj(refresh, 'access_token', 'id_token', expected_type=str)
  96. if not token:
  97. raise ExtractorError('No auth token returned from refresh request')
  98. self._TOKEN = token
  99. def _call_api(self, video_id, param='', msg='API', auth=True, data=None, query={}, fatal=True):
  100. headers = {'CA-CID': ''}
  101. if data:
  102. headers['Content-Type'] = 'application/json;charset=utf-8'
  103. data = json.dumps(data, separators=(',', ':')).encode()
  104. if auth and self._TOKEN:
  105. headers['Authorization'] = f'Bearer {self._TOKEN}'
  106. return self._download_json(
  107. f'https://{self._API_HOST}/v1/{self._API_PATH}/{video_id}{param}', video_id,
  108. note=f'Downloading {msg} JSON', errnote=f'Failed to download {msg} JSON',
  109. data=data, headers=headers, query=query, fatal=fatal)
  110. def _call_encrypted_api(self, video_id, param='', msg='API', data={}, query={}, fatal=True):
  111. if not Cryptodome.RSA:
  112. raise ExtractorError('pycryptodomex not found. Please install', expected=True)
  113. private_key = Cryptodome.RSA.generate(2048)
  114. cipher = Cryptodome.PKCS1_OAEP.new(private_key, hashAlgo=Cryptodome.SHA1)
  115. def decrypt(data):
  116. if not data:
  117. return None
  118. try:
  119. return cipher.decrypt(base64.b64decode(data)).decode()
  120. except (ValueError, binascii.Error) as e:
  121. raise ExtractorError(f'Could not decrypt data: {e}')
  122. token = base64.b64encode(private_key.public_key().export_key('DER')).decode()
  123. api_json = self._call_api(video_id, param, msg, data={
  124. 'deviceId': self._DEVICE_ID,
  125. 'token': token,
  126. **data,
  127. }, query=query, fatal=fatal)
  128. return api_json, decrypt
  129. def _download_metadata(self, url, video_id, lang, props_keys):
  130. metadata = self._call_api(video_id, msg='metadata', query={'al': lang or 'ja'}, auth=False, fatal=False)
  131. if not metadata:
  132. webpage = self._download_webpage(url, video_id)
  133. nextjs_data = self._search_nextjs_data(webpage, video_id, fatal=False)
  134. metadata = traverse_obj(nextjs_data, (
  135. 'props', 'pageProps', *variadic(props_keys, (str, bytes, dict, set)), {dict})) or {}
  136. return metadata
  137. def _get_formats(self, data, path, video_id=None):
  138. hls_url = traverse_obj(data, path, get_all=False)
  139. if not hls_url and not data.get('canWatch'):
  140. self.raise_no_formats(
  141. 'This account does not have access to the requested content', expected=True)
  142. elif not hls_url:
  143. self.raise_no_formats('No supported formats found')
  144. return self._extract_m3u8_formats(hls_url, video_id, 'mp4', m3u8_id='hls', live=True)
  145. class WrestleUniverseVODIE(WrestleUniverseBaseIE):
  146. _VALID_URL = WrestleUniverseBaseIE._VALID_URL_TMPL % 'videos'
  147. _TESTS = [{
  148. 'url': 'https://www.wrestle-universe.com/en/videos/dp8mpjmcKfxzUhEHM2uFws',
  149. 'info_dict': {
  150. 'id': 'dp8mpjmcKfxzUhEHM2uFws',
  151. 'ext': 'mp4',
  152. 'title': 'The 3rd “Futari wa Princess” Max Heart Tournament',
  153. 'description': 'md5:318d5061e944797fbbb81d5c7dd00bf5',
  154. 'location': '埼玉・春日部ふれあいキューブ',
  155. 'channel': 'tjpw',
  156. 'duration': 7119,
  157. 'timestamp': 1674979200,
  158. 'upload_date': '20230129',
  159. 'thumbnail': 'https://image.asset.wrestle-universe.com/8FjD67P8rZc446RBQs5RBN/8FjD67P8rZc446RBQs5RBN',
  160. 'chapters': 'count:7',
  161. 'cast': 'count:21',
  162. },
  163. 'params': {
  164. 'skip_download': 'm3u8',
  165. },
  166. }]
  167. _API_PATH = 'videoEpisodes'
  168. def _real_extract(self, url):
  169. lang, video_id = self._match_valid_url(url).group('lang', 'id')
  170. metadata = self._download_metadata(url, video_id, lang, 'videoEpisodeFallbackData')
  171. video_data = self._call_api(video_id, ':watch', 'watch', data={'deviceId': self._DEVICE_ID})
  172. return {
  173. 'id': video_id,
  174. 'formats': self._get_formats(video_data, ('protocolHls', 'url', {url_or_none}), video_id),
  175. **traverse_obj(metadata, {
  176. 'title': ('displayName', {str}),
  177. 'description': ('description', {str}),
  178. 'channel': ('labels', 'group', {str}),
  179. 'location': ('labels', 'venue', {str}),
  180. 'timestamp': ('watchStartTime', {int_or_none}),
  181. 'thumbnail': ('keyVisualUrl', {url_or_none}),
  182. 'cast': ('casts', ..., 'displayName', {str}),
  183. 'duration': ('duration', {int}),
  184. 'chapters': ('videoChapters', lambda _, v: isinstance(v.get('start'), int), {
  185. 'title': ('displayName', {str}),
  186. 'start_time': ('start', {int}),
  187. 'end_time': ('end', {int}),
  188. }),
  189. }),
  190. }
  191. class WrestleUniversePPVIE(WrestleUniverseBaseIE):
  192. _VALID_URL = WrestleUniverseBaseIE._VALID_URL_TMPL % 'lives'
  193. _TESTS = [{
  194. 'note': 'HLS AES-128 key obtained via API',
  195. 'url': 'https://www.wrestle-universe.com/en/lives/buH9ibbfhdJAY4GKZcEuJX',
  196. 'info_dict': {
  197. 'id': 'buH9ibbfhdJAY4GKZcEuJX',
  198. 'ext': 'mp4',
  199. 'title': '【PPV】Beyond the origins, into the future',
  200. 'description': 'md5:9a872db68cd09be4a1e35a3ee8b0bdfc',
  201. 'channel': 'tjpw',
  202. 'location': '東京・Twin Box AKIHABARA',
  203. 'duration': 10098,
  204. 'timestamp': 1675076400,
  205. 'upload_date': '20230130',
  206. 'thumbnail': 'https://image.asset.wrestle-universe.com/rJs2m7cBaLXrwCcxMdQGRM/rJs2m7cBaLXrwCcxMdQGRM',
  207. 'thumbnails': 'count:3',
  208. 'hls_aes': {
  209. 'key': '5633184acd6e43f1f1ac71c6447a4186',
  210. 'iv': '5bac71beb33197d5600337ce86de7862',
  211. },
  212. },
  213. 'params': {
  214. 'skip_download': 'm3u8',
  215. },
  216. 'skip': 'No longer available',
  217. }, {
  218. 'note': 'unencrypted HLS',
  219. 'url': 'https://www.wrestle-universe.com/en/lives/wUG8hP5iApC63jbtQzhVVx',
  220. 'info_dict': {
  221. 'id': 'wUG8hP5iApC63jbtQzhVVx',
  222. 'ext': 'mp4',
  223. 'title': 'GRAND PRINCESS \'22',
  224. 'description': 'md5:e4f43d0d4262de3952ff34831bc99858',
  225. 'channel': 'tjpw',
  226. 'location': '東京・両国国技館',
  227. 'duration': 18044,
  228. 'timestamp': 1647665400,
  229. 'upload_date': '20220319',
  230. 'thumbnail': 'https://image.asset.wrestle-universe.com/i8jxSTCHPfdAKD4zN41Psx/i8jxSTCHPfdAKD4zN41Psx',
  231. 'thumbnails': 'count:3',
  232. },
  233. 'params': {
  234. 'skip_download': 'm3u8',
  235. },
  236. }, {
  237. 'note': 'manifest provides live-a (partial) and live-b (full) streams',
  238. 'url': 'https://www.wrestle-universe.com/en/lives/umc99R9XsexXrxr9VjTo9g',
  239. 'only_matching': True,
  240. }]
  241. _API_PATH = 'events'
  242. def _real_extract(self, url):
  243. lang, video_id = self._match_valid_url(url).group('lang', 'id')
  244. metadata = self._download_metadata(url, video_id, lang, 'eventFallbackData')
  245. info = {
  246. 'id': video_id,
  247. **traverse_obj(metadata, {
  248. 'title': ('displayName', {str}),
  249. 'description': ('description', {str}),
  250. 'channel': ('labels', 'group', {str}),
  251. 'location': ('labels', 'venue', {str}),
  252. 'timestamp': ('startTime', {int_or_none}),
  253. 'thumbnails': (('keyVisualUrl', 'alterKeyVisualUrl', 'heroKeyVisualUrl'), {'url': {url_or_none}}),
  254. }),
  255. }
  256. ended_time = traverse_obj(metadata, ('endedTime', {int_or_none}))
  257. if info.get('timestamp') and ended_time:
  258. info['duration'] = ended_time - info['timestamp']
  259. video_data, decrypt = self._call_encrypted_api(
  260. video_id, ':watchArchive', 'watch archive', data={'method': 1})
  261. # 'chromecastUrls' can be only partial videos, avoid
  262. info['formats'] = self._get_formats(video_data, ('hls', (('urls', ...), 'url'), {url_or_none}), video_id)
  263. for f in info['formats']:
  264. # bitrates are exaggerated in PPV playlists, so avoid wrong/huge filesize_approx values
  265. if f.get('tbr'):
  266. f['tbr'] = int(f['tbr'] / 2.5)
  267. # prefer variants with the same basename as the master playlist to avoid partial streams
  268. f['format_id'] = url_basename(f['url']).partition('.')[0]
  269. if not f['format_id'].startswith(url_basename(f['manifest_url']).partition('.')[0]):
  270. f['preference'] = -10
  271. hls_aes_key = traverse_obj(video_data, ('hls', 'key', {decrypt}))
  272. if hls_aes_key:
  273. info['hls_aes'] = {
  274. 'key': hls_aes_key,
  275. 'iv': traverse_obj(video_data, ('hls', 'iv', {decrypt})),
  276. }
  277. elif traverse_obj(video_data, ('hls', 'encryptType', {int})):
  278. self.report_warning('HLS AES-128 key was not found in API response')
  279. return info