1# coding: utf-8
2from __future__ import unicode_literals
3
4import json
5import re
6
7from .common import InfoExtractor
8from ..compat import (
9    compat_kwargs,
10    compat_str,
11    compat_urlparse,
12    compat_urllib_request,
13)
14from ..utils import (
15    ExtractorError,
16    int_or_none,
17    try_get,
18    smuggle_url,
19    unsmuggle_url,
20)
21
22
23class ViuBaseIE(InfoExtractor):
24    def _real_initialize(self):
25        viu_auth_res = self._request_webpage(
26            'https://www.viu.com/api/apps/v2/authenticate', None,
27            'Requesting Viu auth', query={
28                'acct': 'test',
29                'appid': 'viu_desktop',
30                'fmt': 'json',
31                'iid': 'guest',
32                'languageid': 'default',
33                'platform': 'desktop',
34                'userid': 'guest',
35                'useridtype': 'guest',
36                'ver': '1.0'
37            }, headers=self.geo_verification_headers())
38        self._auth_token = viu_auth_res.info()['X-VIU-AUTH']
39
40    def _call_api(self, path, *args, **kwargs):
41        headers = self.geo_verification_headers()
42        headers.update({
43            'X-VIU-AUTH': self._auth_token
44        })
45        headers.update(kwargs.get('headers', {}))
46        kwargs['headers'] = headers
47        response = self._download_json(
48            'https://www.viu.com/api/' + path, *args,
49            **compat_kwargs(kwargs))['response']
50        if response.get('status') != 'success':
51            raise ExtractorError('%s said: %s' % (
52                self.IE_NAME, response['message']), expected=True)
53        return response
54
55
56class ViuIE(ViuBaseIE):
57    _VALID_URL = r'(?:viu:|https?://[^/]+\.viu\.com/[a-z]{2}/media/)(?P<id>\d+)'
58    _TESTS = [{
59        'url': 'https://www.viu.com/en/media/1116705532?containerId=playlist-22168059',
60        'info_dict': {
61            'id': '1116705532',
62            'ext': 'mp4',
63            'title': 'Citizen Khan - Ep 1',
64            'description': 'md5:d7ea1604f49e5ba79c212c551ce2110e',
65        },
66        'params': {
67            'skip_download': 'm3u8 download',
68        },
69        'skip': 'Geo-restricted to India',
70    }, {
71        'url': 'https://www.viu.com/en/media/1130599965',
72        'info_dict': {
73            'id': '1130599965',
74            'ext': 'mp4',
75            'title': 'Jealousy Incarnate - Episode 1',
76            'description': 'md5:d3d82375cab969415d2720b6894361e9',
77        },
78        'params': {
79            'skip_download': 'm3u8 download',
80        },
81        'skip': 'Geo-restricted to Indonesia',
82    }, {
83        'url': 'https://india.viu.com/en/media/1126286865',
84        'only_matching': True,
85    }]
86
87    def _real_extract(self, url):
88        video_id = self._match_id(url)
89
90        video_data = self._call_api(
91            'clip/load', video_id, 'Downloading video data', query={
92                'appid': 'viu_desktop',
93                'fmt': 'json',
94                'id': video_id
95            })['item'][0]
96
97        title = video_data['title']
98
99        m3u8_url = None
100        url_path = video_data.get('urlpathd') or video_data.get('urlpath')
101        tdirforwhole = video_data.get('tdirforwhole')
102        # #EXT-X-BYTERANGE is not supported by native hls downloader
103        # and ffmpeg (#10955)
104        # hls_file = video_data.get('hlsfile')
105        hls_file = video_data.get('jwhlsfile')
106        if url_path and tdirforwhole and hls_file:
107            m3u8_url = '%s/%s/%s' % (url_path, tdirforwhole, hls_file)
108        else:
109            # m3u8_url = re.sub(
110            #     r'(/hlsc_)[a-z]+(\d+\.m3u8)',
111            #     r'\1whe\2', video_data['href'])
112            m3u8_url = video_data['href']
113        formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4')
114        self._sort_formats(formats)
115
116        subtitles = {}
117        for key, value in video_data.items():
118            mobj = re.match(r'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key)
119            if not mobj:
120                continue
121            subtitles.setdefault(mobj.group('lang'), []).append({
122                'url': value,
123                'ext': mobj.group('ext')
124            })
125
126        return {
127            'id': video_id,
128            'title': title,
129            'description': video_data.get('description'),
130            'series': video_data.get('moviealbumshowname'),
131            'episode': title,
132            'episode_number': int_or_none(video_data.get('episodeno')),
133            'duration': int_or_none(video_data.get('duration')),
134            'formats': formats,
135            'subtitles': subtitles,
136        }
137
138
139class ViuPlaylistIE(ViuBaseIE):
140    IE_NAME = 'viu:playlist'
141    _VALID_URL = r'https?://www\.viu\.com/[^/]+/listing/playlist-(?P<id>\d+)'
142    _TEST = {
143        'url': 'https://www.viu.com/en/listing/playlist-22461380',
144        'info_dict': {
145            'id': '22461380',
146            'title': 'The Good Wife',
147        },
148        'playlist_count': 16,
149        'skip': 'Geo-restricted to Indonesia',
150    }
151
152    def _real_extract(self, url):
153        playlist_id = self._match_id(url)
154        playlist_data = self._call_api(
155            'container/load', playlist_id,
156            'Downloading playlist info', query={
157                'appid': 'viu_desktop',
158                'fmt': 'json',
159                'id': 'playlist-' + playlist_id
160            })['container']
161
162        entries = []
163        for item in playlist_data.get('item', []):
164            item_id = item.get('id')
165            if not item_id:
166                continue
167            item_id = compat_str(item_id)
168            entries.append(self.url_result(
169                'viu:' + item_id, 'Viu', item_id))
170
171        return self.playlist_result(
172            entries, playlist_id, playlist_data.get('title'))
173
174
175class ViuOTTIE(InfoExtractor):
176    IE_NAME = 'viu:ott'
177    _NETRC_MACHINE = 'viu'
178    _VALID_URL = r'https?://(?:www\.)?viu\.com/ott/(?P<country_code>[a-z]{2})/(?P<lang_code>[a-z]{2}-[a-z]{2})/vod/(?P<id>\d+)'
179    _TESTS = [{
180        'url': 'http://www.viu.com/ott/sg/en-us/vod/3421/The%20Prime%20Minister%20and%20I',
181        'info_dict': {
182            'id': '3421',
183            'ext': 'mp4',
184            'title': 'A New Beginning',
185            'description': 'md5:1e7486a619b6399b25ba6a41c0fe5b2c',
186        },
187        'params': {
188            'skip_download': 'm3u8 download',
189            'noplaylist': True,
190        },
191        'skip': 'Geo-restricted to Singapore',
192    }, {
193        'url': 'http://www.viu.com/ott/hk/zh-hk/vod/7123/%E5%A4%A7%E4%BA%BA%E5%A5%B3%E5%AD%90',
194        'info_dict': {
195            'id': '7123',
196            'ext': 'mp4',
197            'title': '這就是我的生活之道',
198            'description': 'md5:4eb0d8b08cf04fcdc6bbbeb16043434f',
199        },
200        'params': {
201            'skip_download': 'm3u8 download',
202            'noplaylist': True,
203        },
204        'skip': 'Geo-restricted to Hong Kong',
205    }, {
206        'url': 'https://www.viu.com/ott/hk/zh-hk/vod/68776/%E6%99%82%E5%B0%9A%E5%AA%BD%E5%92%AA',
207        'playlist_count': 12,
208        'info_dict': {
209            'id': '3916',
210            'title': '時尚媽咪',
211        },
212        'params': {
213            'skip_download': 'm3u8 download',
214            'noplaylist': False,
215        },
216        'skip': 'Geo-restricted to Hong Kong',
217    }]
218
219    _AREA_ID = {
220        'HK': 1,
221        'SG': 2,
222        'TH': 4,
223        'PH': 5,
224    }
225    _LANGUAGE_FLAG = {
226        'zh-hk': 1,
227        'zh-cn': 2,
228        'en-us': 3,
229    }
230    _user_info = None
231
232    def _detect_error(self, response):
233        code = response.get('status', {}).get('code')
234        if code > 0:
235            message = try_get(response, lambda x: x['status']['message'])
236            raise ExtractorError('%s said: %s (%s)' % (
237                self.IE_NAME, message, code), expected=True)
238        return response['data']
239
240    def _raise_login_required(self):
241        raise ExtractorError(
242            'This video requires login. '
243            'Specify --username and --password or --netrc (machine: %s) '
244            'to provide account credentials.' % self._NETRC_MACHINE,
245            expected=True)
246
247    def _login(self, country_code, video_id):
248        if not self._user_info:
249            username, password = self._get_login_info()
250            if username is None or password is None:
251                return
252
253            data = self._download_json(
254                compat_urllib_request.Request(
255                    'https://www.viu.com/ott/%s/index.php' % country_code, method='POST'),
256                video_id, 'Logging in', errnote=False, fatal=False,
257                query={'r': 'user/login'},
258                data=json.dumps({
259                    'username': username,
260                    'password': password,
261                    'platform_flag_label': 'web',
262                }).encode())
263            self._user_info = self._detect_error(data)['user']
264
265        return self._user_info
266
267    def _real_extract(self, url):
268        url, idata = unsmuggle_url(url, {})
269        country_code, lang_code, video_id = self._match_valid_url(url).groups()
270
271        query = {
272            'r': 'vod/ajax-detail',
273            'platform_flag_label': 'web',
274            'product_id': video_id,
275        }
276
277        area_id = self._AREA_ID.get(country_code.upper())
278        if area_id:
279            query['area_id'] = area_id
280
281        product_data = self._download_json(
282            'http://www.viu.com/ott/%s/index.php' % country_code, video_id,
283            'Downloading video info', query=query)['data']
284
285        video_data = product_data.get('current_product')
286        if not video_data:
287            raise ExtractorError('This video is not available in your region.', expected=True)
288
289        series_id = video_data.get('series_id')
290        if not self.get_param('noplaylist') and not idata.get('force_noplaylist'):
291            self.to_screen('Downloading playlist %s - add --no-playlist to just download video' % series_id)
292            series = product_data.get('series', {})
293            product = series.get('product')
294            if product:
295                entries = []
296                for entry in sorted(product, key=lambda x: int_or_none(x.get('number', 0))):
297                    item_id = entry.get('product_id')
298                    if not item_id:
299                        continue
300                    item_id = compat_str(item_id)
301                    entries.append(self.url_result(
302                        smuggle_url(
303                            'http://www.viu.com/ott/%s/%s/vod/%s/' % (country_code, lang_code, item_id),
304                            {'force_noplaylist': True}),  # prevent infinite recursion
305                        'ViuOTT',
306                        item_id,
307                        entry.get('synopsis', '').strip()))
308
309                return self.playlist_result(entries, series_id, series.get('name'), series.get('description'))
310
311        if self.get_param('noplaylist'):
312            self.to_screen('Downloading just video %s because of --no-playlist' % video_id)
313
314        duration_limit = False
315        query = {
316            'ccs_product_id': video_data['ccs_product_id'],
317            'language_flag_id': self._LANGUAGE_FLAG.get(lang_code.lower()) or '3',
318        }
319        headers = {
320            'Referer': url,
321            'Origin': url,
322        }
323        try:
324            stream_data = self._download_json(
325                'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
326                video_id, 'Downloading stream info', query=query, headers=headers)
327            stream_data = self._detect_error(stream_data)['stream']
328        except (ExtractorError, KeyError):
329            stream_data = None
330            if video_data.get('user_level', 0) > 0:
331                user = self._login(country_code, video_id)
332                if user:
333                    query['identity'] = user['identity']
334                    stream_data = self._download_json(
335                        'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
336                        video_id, 'Downloading stream info', query=query, headers=headers)
337                    stream_data = self._detect_error(stream_data).get('stream')
338                else:
339                    # preview is limited to 3min for non-members
340                    # try to bypass the duration limit
341                    duration_limit = True
342                    query['duration'] = '180'
343                    stream_data = self._download_json(
344                        'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
345                        video_id, 'Downloading stream info', query=query, headers=headers)
346                    try:
347                        stream_data = self._detect_error(stream_data)['stream']
348                    except (ExtractorError, KeyError):  # if still not working, give up
349                        self._raise_login_required()
350
351        if not stream_data:
352            raise ExtractorError('Cannot get stream info', expected=True)
353
354        stream_sizes = stream_data.get('size', {})
355        formats = []
356        for vid_format, stream_url in stream_data.get('url', {}).items():
357            height = int_or_none(self._search_regex(
358                r's(\d+)p', vid_format, 'height', default=None))
359
360            # bypass preview duration limit
361            if duration_limit:
362                stream_url = compat_urlparse.urlparse(stream_url)
363                query = dict(compat_urlparse.parse_qsl(stream_url.query, keep_blank_values=True))
364                time_duration = int_or_none(video_data.get('time_duration'))
365                query.update({
366                    'duration': time_duration if time_duration > 0 else '9999999',
367                    'duration_start': '0',
368                })
369                stream_url = stream_url._replace(query=compat_urlparse.urlencode(query)).geturl()
370
371            formats.append({
372                'format_id': vid_format,
373                'url': stream_url,
374                'height': height,
375                'ext': 'mp4',
376                'filesize': int_or_none(stream_sizes.get(vid_format))
377            })
378        self._sort_formats(formats)
379
380        subtitles = {}
381        for sub in video_data.get('subtitle', []):
382            sub_url = sub.get('url')
383            if not sub_url:
384                continue
385            subtitles.setdefault(sub.get('name'), []).append({
386                'url': sub_url,
387                'ext': 'srt',
388            })
389
390        title = video_data['synopsis'].strip()
391
392        return {
393            'id': video_id,
394            'title': title,
395            'description': video_data.get('description'),
396            'series': product_data.get('series', {}).get('name'),
397            'episode': title,
398            'episode_number': int_or_none(video_data.get('number')),
399            'duration': int_or_none(stream_data.get('duration')),
400            'thumbnail': video_data.get('cover_image_url'),
401            'formats': formats,
402            'subtitles': subtitles,
403        }
404