radlive.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. import json
  2. from .common import InfoExtractor
  3. from ..utils import (
  4. ExtractorError,
  5. format_field,
  6. traverse_obj,
  7. try_get,
  8. unified_timestamp,
  9. )
  10. class RadLiveIE(InfoExtractor):
  11. IE_NAME = 'radlive'
  12. _VALID_URL = r'https?://(?:www\.)?rad\.live/content/(?P<content_type>feature|episode)/(?P<id>[a-f0-9-]+)'
  13. _TESTS = [{
  14. 'url': 'https://rad.live/content/feature/dc5acfbc-761b-4bec-9564-df999905116a',
  15. 'md5': '6219d5d31d52de87d21c9cf5b7cb27ff',
  16. 'info_dict': {
  17. 'id': 'dc5acfbc-761b-4bec-9564-df999905116a',
  18. 'ext': 'mp4',
  19. 'title': 'Deathpact - Digital Mirage 2 [Full Set]',
  20. 'language': 'en',
  21. 'thumbnail': 'https://static.12core.net/cb65ae077a079c68380e38f387fbc438.png',
  22. 'description': '',
  23. 'release_timestamp': 1600185600.0,
  24. 'channel': 'Proximity',
  25. 'channel_id': '9ce6dd01-70a4-4d59-afb6-d01f807cd009',
  26. 'channel_url': 'https://rad.live/content/channel/9ce6dd01-70a4-4d59-afb6-d01f807cd009',
  27. },
  28. }, {
  29. 'url': 'https://rad.live/content/episode/bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
  30. 'md5': '40b2175f347592125d93e9a344080125',
  31. 'info_dict': {
  32. 'id': 'bbcf66ec-0d02-4ca0-8dc0-4213eb2429bf',
  33. 'ext': 'mp4',
  34. 'title': 'E01: Bad Jokes 1',
  35. 'language': 'en',
  36. 'thumbnail': 'https://lsp.littlstar.com/channels/WHISTLE/BAD_JOKES/SEASON_1/BAD_JOKES_101/poster.jpg',
  37. 'description': 'Bad Jokes - Champions, Adam Pally, Super Troopers, Team Edge and 2Hype',
  38. 'episode': 'E01: Bad Jokes 1',
  39. 'episode_number': 1,
  40. 'episode_id': '336',
  41. },
  42. }]
  43. def _real_extract(self, url):
  44. content_type, video_id = self._match_valid_url(url).groups()
  45. webpage = self._download_webpage(url, video_id)
  46. content_info = json.loads(self._search_regex(
  47. r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
  48. webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
  49. video_info = content_info[content_type]
  50. if not video_info:
  51. raise ExtractorError('Unable to extract video info, make sure the URL is valid')
  52. formats = self._extract_m3u8_formats(video_info['assets']['videos'][0]['url'], video_id)
  53. data = video_info.get('structured_data', {})
  54. release_date = unified_timestamp(traverse_obj(data, ('releasedEvent', 'startDate')))
  55. channel = next(iter(content_info.get('channels', [])), {})
  56. channel_id = channel.get('lrn', '').split(':')[-1] or None
  57. result = {
  58. 'id': video_id,
  59. 'title': video_info['title'],
  60. 'formats': formats,
  61. 'language': traverse_obj(data, ('potentialAction', 'target', 'inLanguage')),
  62. 'thumbnail': traverse_obj(data, ('image', 'contentUrl')),
  63. 'description': data.get('description'),
  64. 'release_timestamp': release_date,
  65. 'channel': channel.get('name'),
  66. 'channel_id': channel_id,
  67. 'channel_url': format_field(channel_id, None, 'https://rad.live/content/channel/%s'),
  68. }
  69. if content_type == 'episode':
  70. result.update({
  71. # TODO: Get season number when downloading single episode
  72. 'episode': video_info.get('title'),
  73. 'episode_number': video_info.get('number'),
  74. 'episode_id': video_info.get('id'),
  75. })
  76. return result
  77. class RadLiveSeasonIE(RadLiveIE): # XXX: Do not subclass from concrete IE
  78. IE_NAME = 'radlive:season'
  79. _VALID_URL = r'https?://(?:www\.)?rad\.live/content/season/(?P<id>[a-f0-9-]+)'
  80. _TESTS = [{
  81. 'url': 'https://rad.live/content/season/08a290f7-c9ef-4e22-9105-c255995a2e75',
  82. 'md5': '40b2175f347592125d93e9a344080125',
  83. 'info_dict': {
  84. 'id': '08a290f7-c9ef-4e22-9105-c255995a2e75',
  85. 'title': 'Bad Jokes - Season 1',
  86. },
  87. 'playlist_mincount': 5,
  88. }]
  89. @classmethod
  90. def suitable(cls, url):
  91. return False if RadLiveIE.suitable(url) else super().suitable(url)
  92. def _real_extract(self, url):
  93. season_id = self._match_id(url)
  94. webpage = self._download_webpage(url, season_id)
  95. content_info = json.loads(self._search_regex(
  96. r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
  97. webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
  98. video_info = content_info['season']
  99. entries = [{
  100. '_type': 'url_transparent',
  101. 'id': episode['structured_data']['url'].split('/')[-1],
  102. 'url': episode['structured_data']['url'],
  103. 'series': try_get(content_info, lambda x: x['series']['title']),
  104. 'season': video_info['title'],
  105. 'season_number': video_info.get('number'),
  106. 'season_id': video_info.get('id'),
  107. 'ie_key': RadLiveIE.ie_key(),
  108. } for episode in video_info['episodes']]
  109. return self.playlist_result(entries, season_id, video_info.get('title'))
  110. class RadLiveChannelIE(RadLiveIE): # XXX: Do not subclass from concrete IE
  111. IE_NAME = 'radlive:channel'
  112. _VALID_URL = r'https?://(?:www\.)?rad\.live/content/channel/(?P<id>[a-f0-9-]+)'
  113. _TESTS = [{
  114. 'url': 'https://rad.live/content/channel/5c4d8df4-6fa0-413c-81e3-873479b49274',
  115. 'md5': '625156a08b7f2b0b849f234e664457ac',
  116. 'info_dict': {
  117. 'id': '5c4d8df4-6fa0-413c-81e3-873479b49274',
  118. 'title': 'Whistle Sports',
  119. },
  120. 'playlist_mincount': 7,
  121. }]
  122. _QUERY = '''
  123. query WebChannelListing ($lrn: ID!) {
  124. channel (id:$lrn) {
  125. name
  126. features {
  127. structured_data
  128. }
  129. }
  130. }'''
  131. @classmethod
  132. def suitable(cls, url):
  133. return False if RadLiveIE.suitable(url) else super().suitable(url)
  134. def _real_extract(self, url):
  135. channel_id = self._match_id(url)
  136. graphql = self._download_json(
  137. 'https://content.mhq.12core.net/graphql', channel_id,
  138. headers={'Content-Type': 'application/json'},
  139. data=json.dumps({
  140. 'query': self._QUERY,
  141. 'variables': {'lrn': f'lrn:12core:media:content:channel:{channel_id}'},
  142. }).encode())
  143. data = traverse_obj(graphql, ('data', 'channel'))
  144. if not data:
  145. raise ExtractorError('Unable to extract video info, make sure the URL is valid')
  146. entries = [{
  147. '_type': 'url_transparent',
  148. 'url': feature['structured_data']['url'],
  149. 'ie_key': RadLiveIE.ie_key(),
  150. } for feature in data['features']]
  151. return self.playlist_result(entries, channel_id, data.get('name'))