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