]>
Commit | Line | Data |
---|---|---|
3ca3f77f U |
1 | # coding: utf-8 |
2 | from __future__ import unicode_literals | |
3 | ||
4 | import re | |
5 | ||
6 | from .common import InfoExtractor | |
7 | from ..compat import ( | |
8 | compat_kwargs, | |
9 | compat_str, | |
10 | ) | |
11 | from ..utils import ( | |
12 | ExtractorError, | |
13 | int_or_none, | |
14 | ) | |
15 | ||
16 | ||
17 | class ViuBaseIE(InfoExtractor): | |
18 | def _real_initialize(self): | |
19 | viu_auth_res = self._request_webpage( | |
20 | 'https://www.viu.com/api/apps/v2/authenticate', None, | |
21 | 'Requesting Viu auth', query={ | |
22 | 'acct': 'test', | |
23 | 'appid': 'viu_desktop', | |
24 | 'fmt': 'json', | |
25 | 'iid': 'guest', | |
26 | 'languageid': 'default', | |
27 | 'platform': 'desktop', | |
28 | 'userid': 'guest', | |
29 | 'useridtype': 'guest', | |
30 | 'ver': '1.0' | |
31 | }, headers=self.geo_verification_headers()) | |
32 | self._auth_token = viu_auth_res.info()['X-VIU-AUTH'] | |
33 | ||
34 | def _call_api(self, path, *args, **kwargs): | |
35 | headers = self.geo_verification_headers() | |
36 | headers.update({ | |
37 | 'X-VIU-AUTH': self._auth_token | |
38 | }) | |
39 | headers.update(kwargs.get('headers', {})) | |
40 | kwargs['headers'] = headers | |
41 | response = self._download_json( | |
42 | 'https://www.viu.com/api/' + path, *args, | |
43 | **compat_kwargs(kwargs))['response'] | |
44 | if response.get('status') != 'success': | |
45 | raise ExtractorError('%s said: %s' % ( | |
46 | self.IE_NAME, response['message']), expected=True) | |
47 | return response | |
48 | ||
49 | ||
50 | class ViuIE(ViuBaseIE): | |
51 | _VALID_URL = r'(?:viu:|https?://[^/]+\.viu\.com/[a-z]{2}/media/)(?P<id>\d+)' | |
52 | _TESTS = [{ | |
53 | 'url': 'https://www.viu.com/en/media/1116705532?containerId=playlist-22168059', | |
54 | 'info_dict': { | |
55 | 'id': '1116705532', | |
56 | 'ext': 'mp4', | |
57 | 'title': 'Citizen Khan - Ep 1', | |
58 | 'description': 'md5:d7ea1604f49e5ba79c212c551ce2110e', | |
59 | }, | |
60 | 'params': { | |
61 | 'skip_download': 'm3u8 download', | |
62 | }, | |
63 | 'skip': 'Geo-restricted to India', | |
64 | }, { | |
65 | 'url': 'https://www.viu.com/en/media/1130599965', | |
66 | 'info_dict': { | |
67 | 'id': '1130599965', | |
68 | 'ext': 'mp4', | |
69 | 'title': 'Jealousy Incarnate - Episode 1', | |
70 | 'description': 'md5:d3d82375cab969415d2720b6894361e9', | |
71 | }, | |
72 | 'params': { | |
73 | 'skip_download': 'm3u8 download', | |
74 | }, | |
75 | 'skip': 'Geo-restricted to Indonesia', | |
76 | }, { | |
77 | 'url': 'https://india.viu.com/en/media/1126286865', | |
78 | 'only_matching': True, | |
79 | }] | |
80 | ||
81 | def _real_extract(self, url): | |
82 | video_id = self._match_id(url) | |
83 | ||
84 | video_data = self._call_api( | |
85 | 'clip/load', video_id, 'Downloading video data', query={ | |
86 | 'appid': 'viu_desktop', | |
87 | 'fmt': 'json', | |
88 | 'id': video_id | |
89 | })['item'][0] | |
90 | ||
91 | title = video_data['title'] | |
92 | ||
93 | m3u8_url = None | |
94 | url_path = video_data.get('urlpathd') or video_data.get('urlpath') | |
95 | tdirforwhole = video_data.get('tdirforwhole') | |
96 | # #EXT-X-BYTERANGE is not supported by native hls downloader | |
97 | # and ffmpeg (#10955) | |
98 | # hls_file = video_data.get('hlsfile') | |
99 | hls_file = video_data.get('jwhlsfile') | |
100 | if url_path and tdirforwhole and hls_file: | |
101 | m3u8_url = '%s/%s/%s' % (url_path, tdirforwhole, hls_file) | |
102 | else: | |
103 | # m3u8_url = re.sub( | |
104 | # r'(/hlsc_)[a-z]+(\d+\.m3u8)', | |
105 | # r'\1whe\2', video_data['href']) | |
106 | m3u8_url = video_data['href'] | |
107 | formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4') | |
108 | self._sort_formats(formats) | |
109 | ||
110 | subtitles = {} | |
111 | for key, value in video_data.items(): | |
112 | mobj = re.match(r'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key) | |
113 | if not mobj: | |
114 | continue | |
115 | subtitles.setdefault(mobj.group('lang'), []).append({ | |
116 | 'url': value, | |
117 | 'ext': mobj.group('ext') | |
118 | }) | |
119 | ||
120 | return { | |
121 | 'id': video_id, | |
122 | 'title': title, | |
123 | 'description': video_data.get('description'), | |
124 | 'series': video_data.get('moviealbumshowname'), | |
125 | 'episode': title, | |
126 | 'episode_number': int_or_none(video_data.get('episodeno')), | |
127 | 'duration': int_or_none(video_data.get('duration')), | |
128 | 'formats': formats, | |
129 | 'subtitles': subtitles, | |
130 | } | |
131 | ||
132 | ||
133 | class ViuPlaylistIE(ViuBaseIE): | |
134 | IE_NAME = 'viu:playlist' | |
135 | _VALID_URL = r'https?://www\.viu\.com/[^/]+/listing/playlist-(?P<id>\d+)' | |
136 | _TEST = { | |
137 | 'url': 'https://www.viu.com/en/listing/playlist-22461380', | |
138 | 'info_dict': { | |
139 | 'id': '22461380', | |
140 | 'title': 'The Good Wife', | |
141 | }, | |
142 | 'playlist_count': 16, | |
143 | 'skip': 'Geo-restricted to Indonesia', | |
144 | } | |
145 | ||
146 | def _real_extract(self, url): | |
147 | playlist_id = self._match_id(url) | |
148 | playlist_data = self._call_api( | |
149 | 'container/load', playlist_id, | |
150 | 'Downloading playlist info', query={ | |
151 | 'appid': 'viu_desktop', | |
152 | 'fmt': 'json', | |
153 | 'id': 'playlist-' + playlist_id | |
154 | })['container'] | |
155 | ||
156 | entries = [] | |
157 | for item in playlist_data.get('item', []): | |
158 | item_id = item.get('id') | |
159 | if not item_id: | |
160 | continue | |
161 | item_id = compat_str(item_id) | |
162 | entries.append(self.url_result( | |
163 | 'viu:' + item_id, 'Viu', item_id)) | |
164 | ||
165 | return self.playlist_result( | |
166 | entries, playlist_id, playlist_data.get('title')) | |
167 | ||
168 | ||
169 | class ViuOTTIE(InfoExtractor): | |
170 | IE_NAME = 'viu:ott' | |
171 | _VALID_URL = r'https?://(?:www\.)?viu\.com/ott/(?P<country_code>[a-z]{2})/[a-z]{2}-[a-z]{2}/vod/(?P<id>\d+)' | |
172 | _TESTS = [{ | |
173 | 'url': 'http://www.viu.com/ott/sg/en-us/vod/3421/The%20Prime%20Minister%20and%20I', | |
174 | 'info_dict': { | |
175 | 'id': '3421', | |
176 | 'ext': 'mp4', | |
177 | 'title': 'A New Beginning', | |
178 | 'description': 'md5:1e7486a619b6399b25ba6a41c0fe5b2c', | |
179 | }, | |
180 | 'params': { | |
181 | 'skip_download': 'm3u8 download', | |
182 | }, | |
183 | 'skip': 'Geo-restricted to Singapore', | |
184 | }, { | |
185 | 'url': 'http://www.viu.com/ott/hk/zh-hk/vod/7123/%E5%A4%A7%E4%BA%BA%E5%A5%B3%E5%AD%90', | |
186 | 'info_dict': { | |
187 | 'id': '7123', | |
188 | 'ext': 'mp4', | |
189 | 'title': '這就是我的生活之道', | |
190 | 'description': 'md5:4eb0d8b08cf04fcdc6bbbeb16043434f', | |
191 | }, | |
192 | 'params': { | |
193 | 'skip_download': 'm3u8 download', | |
194 | }, | |
195 | 'skip': 'Geo-restricted to Hong Kong', | |
196 | }] | |
197 | ||
198 | _AREA_ID = { | |
199 | 'HK': 1, | |
200 | 'SG': 2, | |
201 | 'TH': 4, | |
202 | 'PH': 5, | |
203 | } | |
204 | ||
205 | def _real_extract(self, url): | |
206 | country_code, video_id = re.match(self._VALID_URL, url).groups() | |
207 | ||
208 | query = { | |
209 | 'r': 'vod/ajax-detail', | |
210 | 'platform_flag_label': 'web', | |
211 | 'product_id': video_id, | |
212 | } | |
213 | ||
214 | area_id = self._AREA_ID.get(country_code.upper()) | |
215 | if area_id: | |
216 | query['area_id'] = area_id | |
217 | ||
218 | product_data = self._download_json( | |
219 | 'http://www.viu.com/ott/%s/index.php' % country_code, video_id, | |
220 | 'Downloading video info', query=query)['data'] | |
221 | ||
222 | video_data = product_data.get('current_product') | |
223 | if not video_data: | |
224 | raise ExtractorError('This video is not available in your region.', expected=True) | |
225 | ||
226 | stream_data = self._download_json( | |
227 | 'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code, | |
228 | video_id, 'Downloading stream info', query={ | |
229 | 'ccs_product_id': video_data['ccs_product_id'], | |
230 | }, headers={ | |
231 | 'Referer': url, | |
232 | 'Origin': re.search(r'https?://[^/]+', url).group(0), | |
233 | })['data']['stream'] | |
234 | ||
235 | stream_sizes = stream_data.get('size', {}) | |
236 | formats = [] | |
237 | for vid_format, stream_url in stream_data.get('url', {}).items(): | |
238 | height = int_or_none(self._search_regex( | |
239 | r's(\d+)p', vid_format, 'height', default=None)) | |
240 | formats.append({ | |
241 | 'format_id': vid_format, | |
242 | 'url': stream_url, | |
243 | 'height': height, | |
244 | 'ext': 'mp4', | |
245 | 'filesize': int_or_none(stream_sizes.get(vid_format)) | |
246 | }) | |
247 | self._sort_formats(formats) | |
248 | ||
249 | subtitles = {} | |
250 | for sub in video_data.get('subtitle', []): | |
251 | sub_url = sub.get('url') | |
252 | if not sub_url: | |
253 | continue | |
254 | subtitles.setdefault(sub.get('name'), []).append({ | |
255 | 'url': sub_url, | |
256 | 'ext': 'srt', | |
257 | }) | |
258 | ||
259 | title = video_data['synopsis'].strip() | |
260 | ||
261 | return { | |
262 | 'id': video_id, | |
263 | 'title': title, | |
264 | 'description': video_data.get('description'), | |
265 | 'series': product_data.get('series', {}).get('name'), | |
266 | 'episode': title, | |
267 | 'episode_number': int_or_none(video_data.get('number')), | |
268 | 'duration': int_or_none(stream_data.get('duration')), | |
269 | 'thumbnail': video_data.get('cover_image_url'), | |
270 | 'formats': formats, | |
271 | 'subtitles': subtitles, | |
272 | } |