viu.py (9267B)
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 }