5 from .common
import InfoExtractor
6 from ..compat
import compat_str
7 from ..networking
.exceptions
import HTTPError
21 class VootBaseIE(InfoExtractor
):
22 _NETRC_MACHINE
= 'voot'
24 _LOGIN_HINT
= 'Log in with "-u <email_address> -p <password>", or use "-u token -p <auth_token>" to login with auth token.'
27 _API_HEADERS
= {'Origin': 'https://www.voot.com', 'Referer': 'https://www.voot.com/'}
29 def _perform_login(self
, username
, password
):
30 if self
._TOKEN
and self
._EXPIRY
:
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']
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({
44 }, separators
=(',', ':')).encode(), 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',
61 }, separators
=(',', ':')).encode(), headers
={
63 'Content-Type': 'application/json;charset=utf-8',
64 }, note
='Logging in', expected_status
=400), ('data', 'authToken', {dict}
))
66 raise ExtractorError('Incorrect password', expected
=True)
67 VootBaseIE
._TOKEN
= auth_token
['accessToken']
68 VootBaseIE
._EXPIRY
= auth_token
['expirationTime']
71 raise ExtractorError(self
._LOGIN
_HINT
, expected
=True)
73 def _check_token_expiry(self
):
74 if int(time
.time()) >= self
._EXPIRY
:
75 raise ExtractorError('Access token has expired', expected
=True)
77 def _real_initialize(self
):
79 self
.raise_login_required(self
._LOGIN
_HINT
, method
=None)
80 self
._check
_token
_expiry
()
83 class VootIE(VootBaseIE
):
88 https?://(?:www\.)?voot\.com/?
91 (?:shows|kids)/(?:[^/]+/){4}
97 'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/1/360558/is-this-the-end-of-kamini-/441353',
101 'title': 'Is this the end of Kamini?',
102 'description': 'md5:06291fbbbc4dcbe21235c40c262507c1',
103 'timestamp': 1472103000,
104 'upload_date': '20160825',
105 'series': 'Ishq Ka Rang Safed',
107 'episode': 'Is this the end of Kamini?',
108 'episode_number': 340,
109 'release_date': '20160825',
110 'season': 'Season 1',
114 'params': {'skip_download': 'm3u8'}
,
116 'url': 'https://www.voot.com/kids/characters/mighty-cat-masked-niyander-e-/400478/school-bag-disappears/440925',
117 'only_matching': True,
119 'url': 'https://www.voot.com/movies/pandavas-5/424627',
120 'only_matching': True,
122 'url': 'https://www.voot.com/movie/fight-club/621842',
123 'only_matching': True,
126 def _real_extract(self
, url
):
127 video_id
= self
._match
_id
(url
)
128 media_info
= self
._download
_json
(
129 'https://psapi.voot.com/jio/voot/v1/voot-web/content/query/asset-details', video_id
,
130 query
={'ids': f'include:{video_id}
', 'responseType
': 'common
'}, headers={'accesstoken': self._TOKEN})
133 m3u8_url = self._download_json(
134 'https
://vootapi
.media
.jio
.com
/playback
/v1
/playbackrights
', video_id,
135 'Downloading playback JSON
', data=b'{}', headers={
136 **self.geo_verification_headers(),
138 'Content
-Type
': 'application
/json
;charset
=utf
-8',
139 'platform
': 'androidwebdesktop
',
141 'voottoken
': self._TOKEN,
143 except ExtractorError as e:
144 if isinstance(e.cause, HTTPError) and e.cause.status == 400:
145 self._check_token_expiry()
148 formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4
', m3u8_id='hls
')
149 self._remove_duplicate_formats(formats)
153 # '/_definst_
/smil
:vod
/' m3u8 manifests claim to have 720p+ formats but max out at 480p
154 'formats
': traverse_obj(formats, (
155 lambda _, v: '/_definst_
/smil
:vod
/' not in v['url
'] or v['height
'] <= 480)),
156 'http_headers
': self._API_HEADERS,
157 **traverse_obj(media_info, ('result
', 0, {
158 'title
': ('fullTitle
', {str}),
159 'description
': ('fullSynopsis
', {str}),
160 'series
': ('showName
', {str}),
161 'season_number
': ('season
', {int_or_none}),
162 'episode
': ('fullTitle
', {str}),
163 'episode_number
': ('episode
', {int_or_none}),
164 'timestamp
': ('uploadTime
', {int_or_none}),
165 'release_date
': ('telecastDate
', {unified_strdate}),
166 'age_limit
': ('ageNemonic
', {parse_age_limit}),
167 'duration
': ('duration
', {float_or_none}),
172 class VootSeriesIE(VootBaseIE):
174 _VALID_URL = r'https?
://(?
:www\
.)?voot\
.com
/shows
/[^
/]+/(?P
<id>\d{3,}
)'
176 'url
': 'https
://www
.voot
.com
/shows
/chakravartin
-ashoka
-samrat
/100002',
177 'playlist_mincount
': 442,
182 'url
': 'https
://www
.voot
.com
/shows
/ishq
-ka
-rang
-safed
/100003',
183 'playlist_mincount
': 341,
188 _SHOW_API = 'https
://psapi
.voot
.com
/media
/voot
/v1
/voot
-web
/content
/generic
/season
-by
-show?sort
=season
%3Aasc
&id={}&responseType
=common
'
189 _SEASON_API = 'https
://psapi
.voot
.com
/media
/voot
/v1
/voot
-web
/content
/generic
/series
-wise
-episode?sort
=episode
%3Aasc
&id={}&responseType=common&page={:d}
'
191 def _entries(self, show_id):
192 show_json = self._download_json(self._SHOW_API.format(show_id), video_id=show_id)
193 for season in show_json.get('result
', []):
195 season_id = try_get(season, lambda x: x['id'], compat_str)
196 season_json = self._download_json(self._SEASON_API.format(season_id, page_num),
198 note='Downloading JSON metadata page
%d' % page_num)
199 episodes_json = season_json.get('result
', [])
202 for episode in episodes_json:
203 video_id = episode.get('id')
204 yield self.url_result(
205 'voot
:%s' % video_id, ie=VootIE.ie_key(), video_id=video_id)
206 episodes_json = self._download_json(self._SEASON_API.format(season_id, page_num),
208 note='Downloading JSON metadata page
%d' % page_num)['result
']
210 def _real_extract(self, url):
211 show_id = self._match_id(url)
212 return self.playlist_result(self._entries(show_id), playlist_id=show_id)