mirror of
https://github.com/yt-dlp/yt-dlp
synced 2024-12-26 21:59:08 +01:00
[viu] Fixed extractor (#3136)
Closes #3133 Authored by: zackmark29, pukkandan
This commit is contained in:
parent
a3f2445e29
commit
1fb707badb
1 changed files with 103 additions and 113 deletions
|
@ -1,55 +1,32 @@
|
||||||
# coding: utf-8
|
# coding: utf-8
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import json
|
|
||||||
import re
|
import re
|
||||||
|
import json
|
||||||
|
import uuid
|
||||||
|
import random
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..compat import (
|
from ..compat import compat_str
|
||||||
compat_kwargs,
|
|
||||||
compat_str,
|
|
||||||
compat_urlparse,
|
|
||||||
compat_urllib_request,
|
|
||||||
)
|
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
ExtractorError,
|
ExtractorError,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
|
strip_or_none,
|
||||||
try_get,
|
try_get,
|
||||||
smuggle_url,
|
smuggle_url,
|
||||||
unsmuggle_url,
|
unsmuggle_url,
|
||||||
|
url_or_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ViuBaseIE(InfoExtractor):
|
class ViuBaseIE(InfoExtractor):
|
||||||
def _real_initialize(self):
|
def _call_api(self, path, *args, headers={}, **kwargs):
|
||||||
viu_auth_res = self._request_webpage(
|
|
||||||
'https://www.viu.com/api/apps/v2/authenticate', None,
|
|
||||||
'Requesting Viu auth', query={
|
|
||||||
'acct': 'test',
|
|
||||||
'appid': 'viu_desktop',
|
|
||||||
'fmt': 'json',
|
|
||||||
'iid': 'guest',
|
|
||||||
'languageid': 'default',
|
|
||||||
'platform': 'desktop',
|
|
||||||
'userid': 'guest',
|
|
||||||
'useridtype': 'guest',
|
|
||||||
'ver': '1.0'
|
|
||||||
}, headers=self.geo_verification_headers())
|
|
||||||
self._auth_token = viu_auth_res.info()['X-VIU-AUTH']
|
|
||||||
|
|
||||||
def _call_api(self, path, *args, **kwargs):
|
|
||||||
headers = self.geo_verification_headers()
|
|
||||||
headers.update({
|
|
||||||
'X-VIU-AUTH': self._auth_token
|
|
||||||
})
|
|
||||||
headers.update(kwargs.get('headers', {}))
|
|
||||||
kwargs['headers'] = headers
|
|
||||||
response = self._download_json(
|
response = self._download_json(
|
||||||
'https://www.viu.com/api/' + path, *args,
|
f'https://www.viu.com/api/{path}', *args, **kwargs,
|
||||||
**compat_kwargs(kwargs))['response']
|
headers={**self.geo_verification_headers(), **headers})['response']
|
||||||
if response.get('status') != 'success':
|
if response.get('status') != 'success':
|
||||||
raise ExtractorError('%s said: %s' % (
|
raise ExtractorError(f'{self.IE_NAME} said: {response["message"]}', expected=True)
|
||||||
self.IE_NAME, response['message']), expected=True)
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -101,6 +78,7 @@ class ViuIE(ViuBaseIE):
|
||||||
tdirforwhole = video_data.get('tdirforwhole')
|
tdirforwhole = video_data.get('tdirforwhole')
|
||||||
# #EXT-X-BYTERANGE is not supported by native hls downloader
|
# #EXT-X-BYTERANGE is not supported by native hls downloader
|
||||||
# and ffmpeg (#10955)
|
# and ffmpeg (#10955)
|
||||||
|
# FIXME: It is supported in yt-dlp
|
||||||
# hls_file = video_data.get('hlsfile')
|
# hls_file = video_data.get('hlsfile')
|
||||||
hls_file = video_data.get('jwhlsfile')
|
hls_file = video_data.get('jwhlsfile')
|
||||||
if url_path and tdirforwhole and hls_file:
|
if url_path and tdirforwhole and hls_file:
|
||||||
|
@ -227,42 +205,63 @@ class ViuOTTIE(InfoExtractor):
|
||||||
'zh-cn': 2,
|
'zh-cn': 2,
|
||||||
'en-us': 3,
|
'en-us': 3,
|
||||||
}
|
}
|
||||||
_user_info = None
|
|
||||||
|
_user_token = None
|
||||||
|
_auth_codes = {}
|
||||||
|
|
||||||
def _detect_error(self, response):
|
def _detect_error(self, response):
|
||||||
code = response.get('status', {}).get('code')
|
code = try_get(response, lambda x: x['status']['code'])
|
||||||
if code > 0:
|
if code and code > 0:
|
||||||
message = try_get(response, lambda x: x['status']['message'])
|
message = try_get(response, lambda x: x['status']['message'])
|
||||||
raise ExtractorError('%s said: %s (%s)' % (
|
raise ExtractorError(f'{self.IE_NAME} said: {message} ({code})', expected=True)
|
||||||
self.IE_NAME, message, code), expected=True)
|
return response.get('data') or {}
|
||||||
return response['data']
|
|
||||||
|
|
||||||
def _raise_login_required(self):
|
|
||||||
raise ExtractorError(
|
|
||||||
'This video requires login. '
|
|
||||||
'Specify --username and --password or --netrc (machine: %s) '
|
|
||||||
'to provide account credentials.' % self._NETRC_MACHINE,
|
|
||||||
expected=True)
|
|
||||||
|
|
||||||
def _login(self, country_code, video_id):
|
def _login(self, country_code, video_id):
|
||||||
if not self._user_info:
|
if self._user_token is None:
|
||||||
username, password = self._get_login_info()
|
username, password = self._get_login_info()
|
||||||
if username is None or password is None:
|
if username is None:
|
||||||
return
|
return
|
||||||
|
headers = {
|
||||||
|
'Authorization': f'Bearer {self._auth_codes[country_code]}',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
data = self._download_json(
|
||||||
|
'https://api-gateway-global.viu.com/api/account/validate',
|
||||||
|
video_id, 'Validating email address', headers=headers,
|
||||||
|
data=json.dumps({
|
||||||
|
'principal': username,
|
||||||
|
'provider': 'email'
|
||||||
|
}).encode())
|
||||||
|
if not data.get('exists'):
|
||||||
|
raise ExtractorError('Invalid email address')
|
||||||
|
|
||||||
data = self._download_json(
|
data = self._download_json(
|
||||||
compat_urllib_request.Request(
|
'https://api-gateway-global.viu.com/api/auth/login',
|
||||||
'https://www.viu.com/ott/%s/index.php' % country_code, method='POST'),
|
video_id, 'Logging in', headers=headers,
|
||||||
video_id, 'Logging in', errnote=False, fatal=False,
|
|
||||||
query={'r': 'user/login'},
|
|
||||||
data=json.dumps({
|
data=json.dumps({
|
||||||
'username': username,
|
'email': username,
|
||||||
'password': password,
|
'password': password,
|
||||||
'platform_flag_label': 'web',
|
'provider': 'email',
|
||||||
}).encode())
|
}).encode())
|
||||||
self._user_info = self._detect_error(data)['user']
|
self._detect_error(data)
|
||||||
|
self._user_token = data.get('identity')
|
||||||
|
# need to update with valid user's token else will throw an error again
|
||||||
|
self._auth_codes[country_code] = data.get('token')
|
||||||
|
return self._user_token
|
||||||
|
|
||||||
return self._user_info
|
def _get_token(self, country_code, video_id):
|
||||||
|
rand = ''.join(random.choice('0123456789') for _ in range(10))
|
||||||
|
return self._download_json(
|
||||||
|
f'https://api-gateway-global.viu.com/api/auth/token?v={rand}000', video_id,
|
||||||
|
headers={'Content-Type': 'application/json'}, note='Getting bearer token',
|
||||||
|
data=json.dumps({
|
||||||
|
'countryCode': country_code.upper(),
|
||||||
|
'platform': 'browser',
|
||||||
|
'platformFlagLabel': 'web',
|
||||||
|
'language': 'en',
|
||||||
|
'uuid': str(uuid.uuid4()),
|
||||||
|
'carrierId': '0'
|
||||||
|
}).encode('utf-8'))['token']
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
url, idata = unsmuggle_url(url, {})
|
url, idata = unsmuggle_url(url, {})
|
||||||
|
@ -279,16 +278,16 @@ class ViuOTTIE(InfoExtractor):
|
||||||
query['area_id'] = area_id
|
query['area_id'] = area_id
|
||||||
|
|
||||||
product_data = self._download_json(
|
product_data = self._download_json(
|
||||||
'http://www.viu.com/ott/%s/index.php' % country_code, video_id,
|
f'http://www.viu.com/ott/{country_code}/index.php', video_id,
|
||||||
'Downloading video info', query=query)['data']
|
'Downloading video info', query=query)['data']
|
||||||
|
|
||||||
video_data = product_data.get('current_product')
|
video_data = product_data.get('current_product')
|
||||||
if not video_data:
|
if not video_data:
|
||||||
raise ExtractorError('This video is not available in your region.', expected=True)
|
self.raise_geo_restricted()
|
||||||
|
|
||||||
series_id = video_data.get('series_id')
|
series_id = video_data.get('series_id')
|
||||||
if self._yes_playlist(series_id, video_id, idata):
|
if self._yes_playlist(series_id, video_id, idata):
|
||||||
series = product_data.get('series', {})
|
series = product_data.get('series') or {}
|
||||||
product = series.get('product')
|
product = series.get('product')
|
||||||
if product:
|
if product:
|
||||||
entries = []
|
entries = []
|
||||||
|
@ -296,14 +295,10 @@ class ViuOTTIE(InfoExtractor):
|
||||||
item_id = entry.get('product_id')
|
item_id = entry.get('product_id')
|
||||||
if not item_id:
|
if not item_id:
|
||||||
continue
|
continue
|
||||||
item_id = compat_str(item_id)
|
|
||||||
entries.append(self.url_result(
|
entries.append(self.url_result(
|
||||||
smuggle_url(
|
smuggle_url(f'http://www.viu.com/ott/{country_code}/{lang_code}/vod/{item_id}/',
|
||||||
'http://www.viu.com/ott/%s/%s/vod/%s/' % (country_code, lang_code, item_id),
|
{'force_noplaylist': True}),
|
||||||
{'force_noplaylist': True}), # prevent infinite recursion
|
ViuOTTIE, str(item_id), entry.get('synopsis', '').strip()))
|
||||||
'ViuOTT',
|
|
||||||
item_id,
|
|
||||||
entry.get('synopsis', '').strip()))
|
|
||||||
|
|
||||||
return self.playlist_result(entries, series_id, series.get('name'), series.get('description'))
|
return self.playlist_result(entries, series_id, series.get('name'), series.get('description'))
|
||||||
|
|
||||||
|
@ -312,69 +307,65 @@ class ViuOTTIE(InfoExtractor):
|
||||||
'ccs_product_id': video_data['ccs_product_id'],
|
'ccs_product_id': video_data['ccs_product_id'],
|
||||||
'language_flag_id': self._LANGUAGE_FLAG.get(lang_code.lower()) or '3',
|
'language_flag_id': self._LANGUAGE_FLAG.get(lang_code.lower()) or '3',
|
||||||
}
|
}
|
||||||
headers = {
|
|
||||||
'Referer': url,
|
|
||||||
'Origin': url,
|
|
||||||
}
|
|
||||||
try:
|
|
||||||
stream_data = self._download_json(
|
|
||||||
'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
|
|
||||||
video_id, 'Downloading stream info', query=query, headers=headers)
|
|
||||||
stream_data = self._detect_error(stream_data)['stream']
|
|
||||||
except (ExtractorError, KeyError):
|
|
||||||
stream_data = None
|
|
||||||
if video_data.get('user_level', 0) > 0:
|
|
||||||
user = self._login(country_code, video_id)
|
|
||||||
if user:
|
|
||||||
query['identity'] = user['identity']
|
|
||||||
stream_data = self._download_json(
|
|
||||||
'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
|
|
||||||
video_id, 'Downloading stream info', query=query, headers=headers)
|
|
||||||
stream_data = self._detect_error(stream_data).get('stream')
|
|
||||||
else:
|
|
||||||
# preview is limited to 3min for non-members
|
|
||||||
# try to bypass the duration limit
|
|
||||||
duration_limit = True
|
|
||||||
query['duration'] = '180'
|
|
||||||
stream_data = self._download_json(
|
|
||||||
'https://d1k2us671qcoau.cloudfront.net/distribute_web_%s.php' % country_code,
|
|
||||||
video_id, 'Downloading stream info', query=query, headers=headers)
|
|
||||||
try:
|
|
||||||
stream_data = self._detect_error(stream_data)['stream']
|
|
||||||
except (ExtractorError, KeyError): # if still not working, give up
|
|
||||||
self._raise_login_required()
|
|
||||||
|
|
||||||
|
def download_playback():
|
||||||
|
stream_data = self._download_json(
|
||||||
|
'https://api-gateway-global.viu.com/api/playback/distribute',
|
||||||
|
video_id=video_id, query=query, fatal=False, note='Downloading stream info',
|
||||||
|
headers={
|
||||||
|
'Authorization': f'Bearer {self._auth_codes[country_code]}',
|
||||||
|
'Referer': url,
|
||||||
|
'Origin': url
|
||||||
|
})
|
||||||
|
return self._detect_error(stream_data).get('stream')
|
||||||
|
|
||||||
|
if not self._auth_codes.get(country_code):
|
||||||
|
self._auth_codes[country_code] = self._get_token(country_code, video_id)
|
||||||
|
|
||||||
|
stream_data = None
|
||||||
|
try:
|
||||||
|
stream_data = download_playback()
|
||||||
|
except (ExtractorError, KeyError):
|
||||||
|
token = self._login(country_code, video_id)
|
||||||
|
if token is not None:
|
||||||
|
query['identity'] = token
|
||||||
|
else:
|
||||||
|
# preview is limited to 3min for non-members. But we can try to bypass it
|
||||||
|
duration_limit, query['duration'] = True, '180'
|
||||||
|
try:
|
||||||
|
stream_data = download_playback()
|
||||||
|
except (ExtractorError, KeyError):
|
||||||
|
if token is not None:
|
||||||
|
raise
|
||||||
|
self.raise_login_required(method='password')
|
||||||
if not stream_data:
|
if not stream_data:
|
||||||
raise ExtractorError('Cannot get stream info', expected=True)
|
raise ExtractorError('Cannot get stream info', expected=True)
|
||||||
|
|
||||||
stream_sizes = stream_data.get('size', {})
|
|
||||||
formats = []
|
formats = []
|
||||||
for vid_format, stream_url in stream_data.get('url', {}).items():
|
for vid_format, stream_url in (stream_data.get('url') or {}).items():
|
||||||
height = int_or_none(self._search_regex(
|
height = int(self._search_regex(r's(\d+)p', vid_format, 'height', default=None))
|
||||||
r's(\d+)p', vid_format, 'height', default=None))
|
|
||||||
|
|
||||||
# bypass preview duration limit
|
# bypass preview duration limit
|
||||||
if duration_limit:
|
if duration_limit:
|
||||||
stream_url = compat_urlparse.urlparse(stream_url)
|
stream_url = urllib.parse.urlparse(stream_url)
|
||||||
query = dict(compat_urlparse.parse_qsl(stream_url.query, keep_blank_values=True))
|
|
||||||
time_duration = int_or_none(video_data.get('time_duration'))
|
|
||||||
query.update({
|
query.update({
|
||||||
'duration': time_duration if time_duration > 0 else '9999999',
|
'duration': video_data.get('time_duration') or '9999999',
|
||||||
'duration_start': '0',
|
'duration_start': '0',
|
||||||
})
|
})
|
||||||
stream_url = stream_url._replace(query=compat_urlparse.urlencode(query)).geturl()
|
stream_url = stream_url._replace(query=urllib.parse.urlencode(dict(
|
||||||
|
urllib.parse.parse_qsl(stream_url.query, keep_blank_values=True)))).geturl()
|
||||||
|
|
||||||
formats.append({
|
formats.append({
|
||||||
'format_id': vid_format,
|
'format_id': vid_format,
|
||||||
'url': stream_url,
|
'url': stream_url,
|
||||||
'height': height,
|
'height': height,
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'filesize': int_or_none(stream_sizes.get(vid_format))
|
'filesize': try_get(stream_data, lambda x: x['size'][vid_format], int)
|
||||||
})
|
})
|
||||||
self._sort_formats(formats)
|
self._sort_formats(formats)
|
||||||
|
|
||||||
subtitles = {}
|
subtitles = {}
|
||||||
for sub in video_data.get('subtitle', []):
|
for sub in video_data.get('subtitle') or []:
|
||||||
sub_url = sub.get('url')
|
sub_url = sub.get('url')
|
||||||
if not sub_url:
|
if not sub_url:
|
||||||
continue
|
continue
|
||||||
|
@ -383,17 +374,16 @@ class ViuOTTIE(InfoExtractor):
|
||||||
'ext': 'srt',
|
'ext': 'srt',
|
||||||
})
|
})
|
||||||
|
|
||||||
title = video_data['synopsis'].strip()
|
title = strip_or_none(video_data.get('synopsis'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'title': title,
|
'title': title,
|
||||||
'description': video_data.get('description'),
|
'description': video_data.get('description'),
|
||||||
'series': product_data.get('series', {}).get('name'),
|
'series': try_get(product_data, lambda x: x['series']['name']),
|
||||||
'episode': title,
|
'episode': title,
|
||||||
'episode_number': int_or_none(video_data.get('number')),
|
'episode_number': int_or_none(video_data.get('number')),
|
||||||
'duration': int_or_none(stream_data.get('duration')),
|
'duration': int_or_none(stream_data.get('duration')),
|
||||||
'thumbnail': video_data.get('cover_image_url'),
|
'thumbnail': url_or_none(video_data.get('cover_image_url')),
|
||||||
'formats': formats,
|
'formats': formats,
|
||||||
'subtitles': subtitles,
|
'subtitles': subtitles,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue