radlive.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import json
  2. from ..utils import (
  3. ExtractorError,
  4. format_field,
  5. traverse_obj,
  6. try_get,
  7. unified_timestamp
  8. )
  9. from .common import InfoExtractor
  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. 'release_timestamp': None,
  39. 'channel': None,
  40. 'channel_id': None,
  41. 'channel_url': None,
  42. 'episode': 'E01: Bad Jokes 1',
  43. 'episode_number': 1,
  44. 'episode_id': '336',
  45. },
  46. }]
  47. def _real_extract(self, url):
  48. content_type, video_id = self._match_valid_url(url).groups()
  49. webpage = self._download_webpage(url, video_id)
  50. content_info = json.loads(self._search_regex(
  51. r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
  52. webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
  53. video_info = content_info[content_type]
  54. if not video_info:
  55. raise ExtractorError('Unable to extract video info, make sure the URL is valid')
  56. formats = self._extract_m3u8_formats(video_info['assets']['videos'][0]['url'], video_id)
  57. data = video_info.get('structured_data', {})
  58. release_date = unified_timestamp(traverse_obj(data, ('releasedEvent', 'startDate')))
  59. channel = next(iter(content_info.get('channels', [])), {})
  60. channel_id = channel.get('lrn', '').split(':')[-1] or None
  61. result = {
  62. 'id': video_id,
  63. 'title': video_info['title'],
  64. 'formats': formats,
  65. 'language': traverse_obj(data, ('potentialAction', 'target', 'inLanguage')),
  66. 'thumbnail': traverse_obj(data, ('image', 'contentUrl')),
  67. 'description': data.get('description'),
  68. 'release_timestamp': release_date,
  69. 'channel': channel.get('name'),
  70. 'channel_id': channel_id,
  71. 'channel_url': format_field(channel_id, None, 'https://rad.live/content/channel/%s'),
  72. }
  73. if content_type == 'episode':
  74. result.update({
  75. # TODO: Get season number when downloading single episode
  76. 'episode': video_info.get('title'),
  77. 'episode_number': video_info.get('number'),
  78. 'episode_id': video_info.get('id'),
  79. })
  80. return result
  81. class RadLiveSeasonIE(RadLiveIE): # XXX: Do not subclass from concrete IE
  82. IE_NAME = 'radlive:season'
  83. _VALID_URL = r'https?://(?:www\.)?rad\.live/content/season/(?P<id>[a-f0-9-]+)'
  84. _TESTS = [{
  85. 'url': 'https://rad.live/content/season/08a290f7-c9ef-4e22-9105-c255995a2e75',
  86. 'md5': '40b2175f347592125d93e9a344080125',
  87. 'info_dict': {
  88. 'id': '08a290f7-c9ef-4e22-9105-c255995a2e75',
  89. 'title': 'Bad Jokes - Season 1',
  90. },
  91. 'playlist_mincount': 5,
  92. }]
  93. @classmethod
  94. def suitable(cls, url):
  95. return False if RadLiveIE.suitable(url) else super(RadLiveSeasonIE, cls).suitable(url)
  96. def _real_extract(self, url):
  97. season_id = self._match_id(url)
  98. webpage = self._download_webpage(url, season_id)
  99. content_info = json.loads(self._search_regex(
  100. r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>',
  101. webpage, 'video info', group='json'))['props']['pageProps']['initialContentData']
  102. video_info = content_info['season']
  103. entries = [{
  104. '_type': 'url_transparent',
  105. 'id': episode['structured_data']['url'].split('/')[-1],
  106. 'url': episode['structured_data']['url'],
  107. 'series': try_get(content_info, lambda x: x['series']['title']),
  108. 'season': video_info['title'],
  109. 'season_number': video_info.get('number'),
  110. 'season_id': video_info.get('id'),
  111. 'ie_key': RadLiveIE.ie_key(),
  112. } for episode in video_info['episodes']]
  113. return self.playlist_result(entries, season_id, video_info.get('title'))
  114. class RadLiveChannelIE(RadLiveIE): # XXX: Do not subclass from concrete IE
  115. IE_NAME = 'radlive:channel'
  116. _VALID_URL = r'https?://(?:www\.)?rad\.live/content/channel/(?P<id>[a-f0-9-]+)'
  117. _TESTS = [{
  118. 'url': 'https://rad.live/content/channel/5c4d8df4-6fa0-413c-81e3-873479b49274',
  119. 'md5': '625156a08b7f2b0b849f234e664457ac',
  120. 'info_dict': {
  121. 'id': '5c4d8df4-6fa0-413c-81e3-873479b49274',
  122. 'title': 'Whistle Sports',
  123. },
  124. 'playlist_mincount': 7,
  125. }]
  126. _QUERY = '''
  127. query WebChannelListing ($lrn: ID!) {
  128. channel (id:$lrn) {
  129. name
  130. features {
  131. structured_data
  132. }
  133. }
  134. }'''
  135. @classmethod
  136. def suitable(cls, url):
  137. return False if RadLiveIE.suitable(url) else super(RadLiveChannelIE, cls).suitable(url)
  138. def _real_extract(self, url):
  139. channel_id = self._match_id(url)
  140. graphql = self._download_json(
  141. 'https://content.mhq.12core.net/graphql', channel_id,
  142. headers={'Content-Type': 'application/json'},
  143. data=json.dumps({
  144. 'query': self._QUERY,
  145. 'variables': {'lrn': f'lrn:12core:media:content:channel:{channel_id}'}
  146. }).encode('utf-8'))
  147. data = traverse_obj(graphql, ('data', 'channel'))
  148. if not data:
  149. raise ExtractorError('Unable to extract video info, make sure the URL is valid')
  150. entries = [{
  151. '_type': 'url_transparent',
  152. 'url': feature['structured_data']['url'],
  153. 'ie_key': RadLiveIE.ie_key(),
  154. } for feature in data['features']]
  155. return self.playlist_result(entries, channel_id, data.get('name'))