2 from __future__
import unicode_literals
7 from .common
import InfoExtractor
12 compat_urllib_request
,
23 class 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
={
29 'appid': 'viu_desktop',
32 'languageid': 'default',
33 'platform': 'desktop',
35 'useridtype': 'guest',
37 }, headers
=self
.geo_verification_headers())
38 self
._auth
_token
= viu_auth_res
.info()['X-VIU-AUTH']
40 def _call_api(self
, path
, *args
, **kwargs
):
41 headers
= self
.geo_verification_headers()
43 'X-VIU-AUTH': self
._auth
_token
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)
56 class ViuIE(ViuBaseIE
):
57 _VALID_URL
= r
'(?:viu:|https?://[^/]+\.viu\.com/[a-z]{2}/media/)(?P<id>\d+)'
59 'url': 'https://www.viu.com/en/media/1116705532?containerId=playlist-22168059',
63 'title': 'Citizen Khan - Ep 1',
64 'description': 'md5:d7ea1604f49e5ba79c212c551ce2110e',
67 'skip_download': 'm3u8 download',
69 'skip': 'Geo-restricted to India',
71 'url': 'https://www.viu.com/en/media/1130599965',
75 'title': 'Jealousy Incarnate - Episode 1',
76 'description': 'md5:d3d82375cab969415d2720b6894361e9',
79 'skip_download': 'm3u8 download',
81 'skip': 'Geo-restricted to Indonesia',
83 'url': 'https://india.viu.com/en/media/1126286865',
84 'only_matching': True,
87 def _real_extract(self
, url
):
88 video_id
= self
._match
_id
(url
)
90 video_data
= self
._call
_api
(
91 'clip/load', video_id
, 'Downloading video data', query
={
92 'appid': 'viu_desktop',
97 title
= video_data
['title']
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
)
110 # r'(/hlsc_)[a-z]+(\d+\.m3u8)',
111 # r'\1whe\2', video_data['href'])
112 m3u8_url
= video_data
['href']
113 formats
= self
._extract
_m
3u8_formats
(m3u8_url
, video_id
, 'mp4')
114 self
._sort
_formats
(formats
)
117 for key
, value
in video_data
.items():
118 mobj
= re
.match(r
'^subtitle_(?P<lang>[^_]+)_(?P<ext>(vtt|srt))', key
)
121 subtitles
.setdefault(mobj
.group('lang'), []).append({
123 'ext': mobj
.group('ext')
129 'description': video_data
.get('description'),
130 'series': video_data
.get('moviealbumshowname'),
132 'episode_number': int_or_none(video_data
.get('episodeno')),
133 'duration': int_or_none(video_data
.get('duration')),
135 'subtitles': subtitles
,
139 class ViuPlaylistIE(ViuBaseIE
):
140 IE_NAME
= 'viu:playlist'
141 _VALID_URL
= r
'https?://www\.viu\.com/[^/]+/listing/playlist-(?P<id>\d+)'
143 'url': 'https://www.viu.com/en/listing/playlist-22461380',
146 'title': 'The Good Wife',
148 'playlist_count': 16,
149 'skip': 'Geo-restricted to Indonesia',
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',
159 'id': 'playlist-' + playlist_id
163 for item
in playlist_data
.get('item', []):
164 item_id
= item
.get('id')
167 item_id
= compat_str(item_id
)
168 entries
.append(self
.url_result(
169 'viu:' + item_id
, 'Viu', item_id
))
171 return self
.playlist_result(
172 entries
, playlist_id
, playlist_data
.get('title'))
175 class ViuOTTIE(InfoExtractor
):
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+)'
180 'url': 'http://www.viu.com/ott/sg/en-us/vod/3421/The%20Prime%20Minister%20and%20I',
184 'title': 'A New Beginning',
185 'description': 'md5:1e7486a619b6399b25ba6a41c0fe5b2c',
188 'skip_download': 'm3u8 download',
191 'skip': 'Geo-restricted to Singapore',
193 'url': 'http://www.viu.com/ott/hk/zh-hk/vod/7123/%E5%A4%A7%E4%BA%BA%E5%A5%B3%E5%AD%90',
197 'title': '這就是我的生活之道',
198 'description': 'md5:4eb0d8b08cf04fcdc6bbbeb16043434f',
201 'skip_download': 'm3u8 download',
204 'skip': 'Geo-restricted to Hong Kong',
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,
213 'skip_download': 'm3u8 download',
216 'skip': 'Geo-restricted to Hong Kong',
232 def _detect_error(self
, response
):
233 code
= response
.get('status', {}).get('code')
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']
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
,
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:
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'}
,
259 'username': username
,
260 'password': password
,
261 'platform_flag_label': 'web',
263 self
._user
_info
= self
._detect
_error
(data
)['user']
265 return self
._user
_info
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()
272 'r': 'vod/ajax-detail',
273 'platform_flag_label': 'web',
274 'product_id': video_id
,
277 area_id
= self
._AREA
_ID
.get(country_code
.upper())
279 query
['area_id'] = area_id
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']
285 video_data
= product_data
.get('current_product')
287 raise ExtractorError('This video is not available in your region.', expected
=True)
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')
296 for entry
in sorted(product
, key
=lambda x
: int_or_none(x
.get('number', 0))):
297 item_id
= entry
.get('product_id')
300 item_id
= compat_str(item_id
)
301 entries
.append(self
.url_result(
303 'http://www.viu.com/ott/%s/%s/vod/%s/' % (country_code
, lang_code
, item_id
),
304 {'force_noplaylist': True}
), # prevent infinite recursion
307 entry
.get('synopsis', '').strip()))
309 return self
.playlist_result(entries
, series_id
, series
.get('name'), series
.get('description'))
311 if self
.get_param('noplaylist'):
312 self
.to_screen('Downloading just video %s because of --no-playlist' % video_id
)
314 duration_limit
= False
316 'ccs_product_id': video_data
['ccs_product_id'],
317 'language_flag_id': self
._LANGUAGE
_FLAG
.get(lang_code
.lower()) or '3',
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):
330 if video_data
.get('user_level', 0) > 0:
331 user
= self
._login
(country_code
, video_id
)
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')
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
)
347 stream_data
= self
._detect
_error
(stream_data
)['stream']
348 except (ExtractorError
, KeyError): # if still not working, give up
349 self
._raise
_login
_required
()
352 raise ExtractorError('Cannot get stream info', expected
=True)
354 stream_sizes
= stream_data
.get('size', {})
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))
360 # bypass preview 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'))
366 'duration': time_duration
if time_duration
> 0 else '9999999',
367 'duration_start': '0',
369 stream_url
= stream_url
._replace
(query
=compat_urlparse
.urlencode(query
)).geturl()
372 'format_id': vid_format
,
376 'filesize': int_or_none(stream_sizes
.get(vid_format
))
378 self
._sort
_formats
(formats
)
381 for sub
in video_data
.get('subtitle', []):
382 sub_url
= sub
.get('url')
385 subtitles
.setdefault(sub
.get('name'), []).append({
390 title
= video_data
['synopsis'].strip()
395 'description': video_data
.get('description'),
396 'series': product_data
.get('series', {}).get('name'),
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'),
402 'subtitles': subtitles
,