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
):
87 https?://(?:www\.)?voot\.com/?
90 (?:shows|kids)/(?:[^/]+/){4}
96 'url': 'https://www.voot.com/shows/ishq-ka-rang-safed/1/360558/is-this-the-end-of-kamini-/441353',
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',
106 'episode': 'Is this the end of Kamini?',
107 'episode_number': 340,
108 'release_date': '20160825',
109 'season': 'Season 1',
113 'params': {'skip_download': 'm3u8'}
,
115 'url': 'https://www.voot.com/kids/characters/mighty-cat-masked-niyander-e-/400478/school-bag-disappears/440925',
116 'only_matching': True,
118 'url': 'https://www.voot.com/movies/pandavas-5/424627',
119 'only_matching': True,
121 'url': 'https://www.voot.com/movie/fight-club/621842',
122 'only_matching': True,
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})
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(),
137 'Content
-Type
': 'application
/json
;charset
=utf
-8',
138 'platform
': 'androidwebdesktop
',
140 'voottoken
': self._TOKEN,
142 except ExtractorError as e:
143 if isinstance(e.cause, HTTPError) and e.cause.status == 400:
144 self._check_token_expiry()
147 formats = self._extract_m3u8_formats(m3u8_url, video_id, 'mp4
', m3u8_id='hls
')
148 self._remove_duplicate_formats(formats)
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}),
171 class VootSeriesIE(VootBaseIE):
172 _VALID_URL = r'https?
://(?
:www\
.)?voot\
.com
/shows
/[^
/]+/(?P
<id>\d{3,}
)'
174 'url
': 'https
://www
.voot
.com
/shows
/chakravartin
-ashoka
-samrat
/100002',
175 'playlist_mincount
': 442,
180 'url
': 'https
://www
.voot
.com
/shows
/ishq
-ka
-rang
-safed
/100003',
181 'playlist_mincount
': 341,
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}
'
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
', []):
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),
196 note='Downloading JSON metadata page
%d' % page_num)
197 episodes_json = season_json.get('result
', [])
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),
206 note='Downloading JSON metadata page
%d' % page_num)['result
']
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)