]>
Commit | Line | Data |
---|---|---|
1 | import json | |
2 | ||
3 | from ..utils import ( | |
4 | ExtractorError, | |
5 | format_field, | |
6 | traverse_obj, | |
7 | try_get, | |
8 | unified_timestamp | |
9 | ) | |
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) | |
65 | self._sort_formats(formats) | |
66 | ||
67 | data = video_info.get('structured_data', {}) | |
68 | ||
69 | release_date = unified_timestamp(traverse_obj(data, ('releasedEvent', 'startDate'))) | |
70 | channel = next(iter(content_info.get('channels', [])), {}) | |
71 | channel_id = channel.get('lrn', '').split(':')[-1] or None | |
72 | ||
73 | result = { | |
74 | 'id': video_id, | |
75 | 'title': video_info['title'], | |
76 | 'formats': formats, | |
77 | 'language': traverse_obj(data, ('potentialAction', 'target', 'inLanguage')), | |
78 | 'thumbnail': traverse_obj(data, ('image', 'contentUrl')), | |
79 | 'description': data.get('description'), | |
80 | 'release_timestamp': release_date, | |
81 | 'channel': channel.get('name'), | |
82 | 'channel_id': channel_id, | |
83 | 'channel_url': format_field(channel_id, None, 'https://rad.live/content/channel/%s'), | |
84 | ||
85 | } | |
86 | if content_type == 'episode': | |
87 | result.update({ | |
88 | # TODO: Get season number when downloading single episode | |
89 | 'episode': video_info.get('title'), | |
90 | 'episode_number': video_info.get('number'), | |
91 | 'episode_id': video_info.get('id'), | |
92 | }) | |
93 | ||
94 | return result | |
95 | ||
96 | ||
97 | class RadLiveSeasonIE(RadLiveIE): | |
98 | IE_NAME = 'radlive:season' | |
99 | _VALID_URL = r'https?://(?:www\.)?rad\.live/content/season/(?P<id>[a-f0-9-]+)' | |
100 | _TESTS = [{ | |
101 | 'url': 'https://rad.live/content/season/08a290f7-c9ef-4e22-9105-c255995a2e75', | |
102 | 'md5': '40b2175f347592125d93e9a344080125', | |
103 | 'info_dict': { | |
104 | 'id': '08a290f7-c9ef-4e22-9105-c255995a2e75', | |
105 | 'title': 'Bad Jokes - Season 1', | |
106 | }, | |
107 | 'playlist_mincount': 5, | |
108 | }] | |
109 | ||
110 | @classmethod | |
111 | def suitable(cls, url): | |
112 | return False if RadLiveIE.suitable(url) else super(RadLiveSeasonIE, cls).suitable(url) | |
113 | ||
114 | def _real_extract(self, url): | |
115 | season_id = self._match_id(url) | |
116 | webpage = self._download_webpage(url, season_id) | |
117 | ||
118 | content_info = json.loads(self._search_regex( | |
119 | r'<script[^>]*type=([\'"])application/json\1[^>]*>(?P<json>{.+?})</script>', | |
120 | webpage, 'video info', group='json'))['props']['pageProps']['initialContentData'] | |
121 | video_info = content_info['season'] | |
122 | ||
123 | entries = [{ | |
124 | '_type': 'url_transparent', | |
125 | 'id': episode['structured_data']['url'].split('/')[-1], | |
126 | 'url': episode['structured_data']['url'], | |
127 | 'series': try_get(content_info, lambda x: x['series']['title']), | |
128 | 'season': video_info['title'], | |
129 | 'season_number': video_info.get('number'), | |
130 | 'season_id': video_info.get('id'), | |
131 | 'ie_key': RadLiveIE.ie_key(), | |
132 | } for episode in video_info['episodes']] | |
133 | ||
134 | return self.playlist_result(entries, season_id, video_info.get('title')) | |
135 | ||
136 | ||
137 | class RadLiveChannelIE(RadLiveIE): | |
138 | IE_NAME = 'radlive:channel' | |
139 | _VALID_URL = r'https?://(?:www\.)?rad\.live/content/channel/(?P<id>[a-f0-9-]+)' | |
140 | _TESTS = [{ | |
141 | 'url': 'https://rad.live/content/channel/5c4d8df4-6fa0-413c-81e3-873479b49274', | |
142 | 'md5': '625156a08b7f2b0b849f234e664457ac', | |
143 | 'info_dict': { | |
144 | 'id': '5c4d8df4-6fa0-413c-81e3-873479b49274', | |
145 | 'title': 'Whistle Sports', | |
146 | }, | |
147 | 'playlist_mincount': 7, | |
148 | }] | |
149 | ||
150 | _QUERY = ''' | |
151 | query WebChannelListing ($lrn: ID!) { | |
152 | channel (id:$lrn) { | |
153 | name | |
154 | features { | |
155 | structured_data | |
156 | } | |
157 | } | |
158 | }''' | |
159 | ||
160 | @classmethod | |
161 | def suitable(cls, url): | |
162 | return False if RadLiveIE.suitable(url) else super(RadLiveChannelIE, cls).suitable(url) | |
163 | ||
164 | def _real_extract(self, url): | |
165 | channel_id = self._match_id(url) | |
166 | ||
167 | graphql = self._download_json( | |
168 | 'https://content.mhq.12core.net/graphql', channel_id, | |
169 | headers={'Content-Type': 'application/json'}, | |
170 | data=json.dumps({ | |
171 | 'query': self._QUERY, | |
172 | 'variables': {'lrn': f'lrn:12core:media:content:channel:{channel_id}'} | |
173 | }).encode('utf-8')) | |
174 | ||
175 | data = traverse_obj(graphql, ('data', 'channel')) | |
176 | if not data: | |
177 | raise ExtractorError('Unable to extract video info, make sure the URL is valid') | |
178 | ||
179 | entries = [{ | |
180 | '_type': 'url_transparent', | |
181 | 'url': feature['structured_data']['url'], | |
182 | 'ie_key': RadLiveIE.ie_key(), | |
183 | } for feature in data['features']] | |
184 | ||
185 | return self.playlist_result(entries, channel_id, data.get('name')) |