]> jfr.im git - yt-dlp.git/blob - yt_dlp/extractor/voot.py
b19a279344da867e21f378bbd0bb2e7116190fca
[yt-dlp.git] / yt_dlp / extractor / voot.py
1 import json
2 import time
3 import uuid
4
5 from .common import InfoExtractor
6 from ..compat import compat_str
7 from ..networking.exceptions import HTTPError
8 from ..utils import (
9 ExtractorError,
10 float_or_none,
11 int_or_none,
12 jwt_decode_hs256,
13 parse_age_limit,
14 traverse_obj,
15 try_call,
16 try_get,
17 unified_strdate,
18 )
19
20
21 class VootBaseIE(InfoExtractor):
22 _NETRC_MACHINE = 'voot'
23 _GEO_BYPASS = False
24 _LOGIN_HINT = 'Log in with "-u <email_address> -p <password>", or use "-u token -p <auth_token>" to login with auth token.'
25 _TOKEN = None
26 _EXPIRY = 0
27 _API_HEADERS = {'Origin': 'https://www.voot.com', 'Referer': 'https://www.voot.com/'}
28
29 def _perform_login(self, username, password):
30 if self._TOKEN and self._EXPIRY:
31 return
32
33 if username.lower() == 'token' and try_call(lambda: jwt_decode_hs256(password)):
34 VootBaseIE._TOKEN = password
35 VootBaseIE._EXPIRY = jwt_decode_hs256(password)['exp']
36 self.report_login()
37
38 # Mobile number as username is not supported
39 elif not username.isdigit():
40 check_username = self._download_json(
41 'https://userauth.voot.com/usersV3/v3/checkUser', None, data=json.dumps({
42 'type': 'email',
43 'email': username
44 }, separators=(',', ':')).encode(), headers={
45 **self._API_HEADERS,
46 'Content-Type': 'application/json;charset=utf-8',
47 }, note='Checking username', expected_status=403)
48 if not traverse_obj(check_username, ('isExist', {bool})):
49 if traverse_obj(check_username, ('status', 'code', {int})) == 9999:
50 self.raise_geo_restricted(countries=['IN'])
51 raise ExtractorError('Incorrect username', expected=True)
52 auth_token = traverse_obj(self._download_json(
53 'https://userauth.voot.com/usersV3/v3/login', None, data=json.dumps({
54 'type': 'traditional',
55 'deviceId': str(uuid.uuid4()),
56 'deviceBrand': 'PC/MAC',
57 'data': {
58 'email': username,
59 'password': password
60 }
61 }, separators=(',', ':')).encode(), headers={
62 **self._API_HEADERS,
63 'Content-Type': 'application/json;charset=utf-8',
64 }, note='Logging in', expected_status=400), ('data', 'authToken', {dict}))
65 if not auth_token:
66 raise ExtractorError('Incorrect password', expected=True)
67 VootBaseIE._TOKEN = auth_token['accessToken']
68 VootBaseIE._EXPIRY = auth_token['expirationTime']
69
70 else:
71 raise ExtractorError(self._LOGIN_HINT, expected=True)
72
73 def _check_token_expiry(self):
74 if int(time.time()) >= self._EXPIRY:
75 raise ExtractorError('Access token has expired', expected=True)
76
77 def _real_initialize(self):
78 if not self._TOKEN:
79 self.raise_login_required(self._LOGIN_HINT, method=None)
80 self._check_token_expiry()
81
82
83 class VootIE(VootBaseIE):
84 _VALID_URL = r'''(?x)
85 (?:
86 voot:|
87 https?://(?:www\.)?voot\.com/?
88 (?:
89 movies?/[^/]+/|
90 (?:shows|kids)/(?:[^/]+/){4}
91 )
92 )
93 (?P<id>\d{3,})
94 '''
95 _TESTS = [{
96 'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/1/360558/is-this-the-end-of-kamini-/441353',
97 'info_dict': {
98 'id': '441353',
99 'ext': 'mp4',
100 'title': 'Is this the end of Kamini?',
101 'description': 'md5:06291fbbbc4dcbe21235c40c262507c1',
102 'timestamp': 1472103000,
103 'upload_date': '20160825',
104 'series': 'Ishq Ka Rang Safed',
105 'season_number': 1,
106 'episode': 'Is this the end of Kamini?',
107 'episode_number': 340,
108 'release_date': '20160825',
109 'season': 'Season 1',
110 'age_limit': 13,
111 'duration': 1146.0,
112 },
113 'params': {'skip_download': 'm3u8'},
114 }, {
115 'url': 'https://www.voot.com/kids/characters/mighty-cat-masked-niyander-e-/400478/school-bag-disappears/440925',
116 'only_matching': True,
117 }, {
118 'url': 'https://www.voot.com/movies/pandavas-5/424627',
119 'only_matching': True,
120 }, {
121 'url': 'https://www.voot.com/movie/fight-club/621842',
122 'only_matching': True,
123 }]
124
125 def _real_extract(self, url):
126 video_id = self._match_id(url)
127 media_info = self._download_json(
128 'https://psapi.voot.com/jio/voot/v1/voot-web/content/query/asset-details', video_id,
129 query={'ids': f'include:{video_id}', 'responseType': 'common'}, headers={'accesstoken': self._TOKEN})
130
131 try:
132 m3u8_url = self._download_json(
133 'https://vootapi.media.jio.com/playback/v1/playbackrights', video_id,
134 'Downloading playback JSON', data=b'{}', headers={
135 **self.geo_verification_headers(),
136 **self._API_HEADERS,
137 'Content-Type': 'application/json;charset=utf-8',
138 'platform': 'androidwebdesktop',
139 'vootid': video_id,
140 'voottoken': self._TOKEN,
141 })['m3u8']
142 except ExtractorError as e:
143 if isinstance(e.cause, HTTPError) and e.cause.status == 400:
144 self._check_token_expiry()
145 raise
146
147 formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4', m3u8_id='hls')
148 self._remove_duplicate_formats(formats)
149
150 return {
151 'id': video_id,
152 # '/_definst_/smil:vod/' m3u8 manifests claim to have 720p+ formats but max out at 480p
153 'formats': traverse_obj(formats, (
154 lambda _, v: '/_definst_/smil:vod/' not in v['url'] or v['height'] <= 480)),
155 'http_headers': self._API_HEADERS,
156 **traverse_obj(media_info, ('result', 0, {
157 'title': ('fullTitle', {str}),
158 'description': ('fullSynopsis', {str}),
159 'series': ('showName', {str}),
160 'season_number': ('season', {int_or_none}),
161 'episode': ('fullTitle', {str}),
162 'episode_number': ('episode', {int_or_none}),
163 'timestamp': ('uploadTime', {int_or_none}),
164 'release_date': ('telecastDate', {unified_strdate}),
165 'age_limit': ('ageNemonic', {parse_age_limit}),
166 'duration': ('duration', {float_or_none}),
167 })),
168 }
169
170
171 class VootSeriesIE(VootBaseIE):
172 _VALID_URL = r'https?://(?:www\.)?voot\.com/shows/[^/]+/(?P<id>\d{3,})'
173 _TESTS = [{
174 'url': 'https://www.voot.com/shows/chakravartin-ashoka-samrat/100002',
175 'playlist_mincount': 442,
176 'info_dict': {
177 'id': '100002',
178 },
179 }, {
180 'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/100003',
181 'playlist_mincount': 341,
182 'info_dict': {
183 'id': '100003',
184 },
185 }]
186 _SHOW_API = 'https://psapi.voot.com/media/voot/v1/voot-web/content/generic/season-by-show?sort=season%3Aasc&id={}&responseType=common'
187 _SEASON_API = 'https://psapi.voot.com/media/voot/v1/voot-web/content/generic/series-wise-episode?sort=episode%3Aasc&id={}&responseType=common&page={:d}'
188
189 def _entries(self, show_id):
190 show_json = self._download_json(self._SHOW_API.format(show_id), video_id=show_id)
191 for season in show_json.get('result', []):
192 page_num = 1
193 season_id = try_get(season, lambda x: x['id'], compat_str)
194 season_json = self._download_json(self._SEASON_API.format(season_id, page_num),
195 video_id=season_id,
196 note='Downloading JSON metadata page %d' % page_num)
197 episodes_json = season_json.get('result', [])
198 while episodes_json:
199 page_num += 1
200 for episode in episodes_json:
201 video_id = episode.get('id')
202 yield self.url_result(
203 'voot:%s' % video_id, ie=VootIE.ie_key(), video_id=video_id)
204 episodes_json = self._download_json(self._SEASON_API.format(season_id, page_num),
205 video_id=season_id,
206 note='Downloading JSON metadata page %d' % page_num)['result']
207
208 def _real_extract(self, url):
209 show_id = self._match_id(url)
210 return self.playlist_result(self._entries(show_id), playlist_id=show_id)