]>
Commit | Line | Data |
---|---|---|
4f7b11cc | 1 | import json |
2 | import time | |
3 | import urllib.error | |
4 | import uuid | |
5 | ||
daaaf5f5 | 6 | from .common import InfoExtractor |
a3ed14cb | 7 | from ..compat import compat_str |
e2b4808f S |
8 | from ..utils import ( |
9 | ExtractorError, | |
4f7b11cc | 10 | float_or_none, |
e2b4808f | 11 | int_or_none, |
4f7b11cc | 12 | jwt_decode_hs256, |
13 | parse_age_limit, | |
14 | traverse_obj, | |
15 | try_call, | |
e2b4808f | 16 | try_get, |
4f7b11cc | 17 | unified_strdate, |
e2b4808f | 18 | ) |
daaaf5f5 AC |
19 | |
20 | ||
4f7b11cc | 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): | |
a3ed14cb A |
84 | _VALID_URL = r'''(?x) |
85 | (?: | |
86 | voot:| | |
73f035e1 | 87 | https?://(?:www\.)?voot\.com/? |
a3ed14cb | 88 | (?: |
a4713ba9 | 89 | movies?/[^/]+/| |
a3ed14cb A |
90 | (?:shows|kids)/(?:[^/]+/){4} |
91 | ) | |
92 | ) | |
93 | (?P<id>\d{3,}) | |
94 | ''' | |
e2b4808f | 95 | _TESTS = [{ |
daaaf5f5 AC |
96 | 'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/1/360558/is-this-the-end-of-kamini-/441353', |
97 | 'info_dict': { | |
4f7b11cc | 98 | 'id': '441353', |
daaaf5f5 | 99 | 'ext': 'mp4', |
4f7b11cc | 100 | 'title': 'Is this the end of Kamini?', |
e2b4808f | 101 | 'description': 'md5:06291fbbbc4dcbe21235c40c262507c1', |
4f7b11cc | 102 | 'timestamp': 1472103000, |
e2b4808f | 103 | 'upload_date': '20160825', |
e2b4808f S |
104 | 'series': 'Ishq Ka Rang Safed', |
105 | 'season_number': 1, | |
106 | 'episode': 'Is this the end of Kamini?', | |
107 | 'episode_number': 340, | |
4f7b11cc | 108 | 'release_date': '20160825', |
109 | 'season': 'Season 1', | |
110 | 'age_limit': 13, | |
111 | 'duration': 1146.0, | |
e2b4808f | 112 | }, |
4f7b11cc | 113 | 'params': {'skip_download': 'm3u8'}, |
e2b4808f S |
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, | |
a4713ba9 AM |
120 | }, { |
121 | 'url': 'https://www.voot.com/movie/fight-club/621842', | |
122 | 'only_matching': True, | |
e2b4808f | 123 | }] |
daaaf5f5 AC |
124 | |
125 | def _real_extract(self, url): | |
126 | video_id = self._match_id(url) | |
e2b4808f | 127 | media_info = self._download_json( |
4f7b11cc | 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, urllib.error.HTTPError) and e.cause.code == 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 | ||
daaaf5f5 | 150 | return { |
4f7b11cc | 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 | })), | |
daaaf5f5 | 168 | } |
a3ed14cb A |
169 | |
170 | ||
4f7b11cc | 171 | class VootSeriesIE(VootBaseIE): |
a3ed14cb A |
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) |