diff --git a/addon.xml b/addon.xml index dc2d8c3..4656a94 100644 --- a/addon.xml +++ b/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/resources/lib/api.py b/resources/lib/api.py index 06e7b16..5ffddf0 100644 --- a/resources/lib/api.py +++ b/resources/lib/api.py @@ -1,27 +1,23 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals import urlquick -from xbmc import executebuiltin from xbmcgui import Dialog -from functools import reduce -from resources.lib.contants import API_BASE_URL, BASE_HEADERS, url_constructor -from resources.lib.utils import deep_get, updateQueryParams, qualityFilter +import xbmc +from resources.lib.contants import BASE_HEADERS, url_constructor +from resources.lib.utils import deep_get, updateQueryParams, qualityFilter, getAuth, guestToken from codequick import Script from codequick.script import Settings from codequick.storage import PersistentDict -from urllib.parse import quote_plus, urlparse, parse_qsl +from urllib.parse import quote_plus from urllib.request import urlopen, Request -import time -import hashlib -import hmac import json -import re from uuid import uuid4 from base64 import b64decode class HotstarAPI: + device_id = str(uuid4()) def __init__(self): self.session = urlquick.Session() @@ -30,8 +26,8 @@ def __init__(self): def getMenu(self): url = url_constructor("/o/v2/menu") resp = self.get( - url, headers={"x-country-code": "in", "x-platform-code": "ANDROID_TV"}) - return deep_get(resp, "body.results.menuItems") + url, headers={"x-country-code": "in", "x-platform-code": "firetv"}) + return deep_get(resp, "body.results.menuItems") # deep_get(resp["body"]["results"]["menuItems"][0]["subItem"][1], "subItem") def getPage(self, url): results = deep_get(self.get(url), "body.results") @@ -49,7 +45,7 @@ def getTray(self, url, search_query=None): with PersistentDict("userdata.pickle") as db: pid = db.get("udata", {}).get("pId") results = self.get(url.format(pid=pid), headers={ - "hotstarauth": self._getAuth(includeST=True, persona=True)}) + "hotstarauth": getAuth(includeST=True, persona=True)}) # ids = ",".join(map(lambda x: x.get("item_id"), # deep_get(results, "data.items"))) # url = url_constructor("/o/v1/multi/get/content?ids=" + ids) @@ -74,8 +70,6 @@ def getTray(self, url, search_query=None): totalResults = deep_get( results, "assets.totalResults") or results.get("totalResults") - offset = deep_get( - results, "assets.offset") or results.get("offset") allResultsPageUrl = None if len(items) > 0 and nextPageUrl is not None and ("/season/" in nextPageUrl or items[0].get("assetType") == "EPISODE") and totalResults is not None: allResultsPageUrl = updateQueryParams(nextPageUrl, {"size": str( @@ -88,16 +82,24 @@ def getTray(self, url, search_query=None): return [], None, None def getPlay(self, contentId, subtag, drm=False, lang=None, partner=None, ask=False): - url = url_constructor("/play/v1/playback/%scontent/%s" % - ('partner/' if partner is not None else '', contentId)) - encryption = "widevine" if drm else "plain" + # 'partner/' if partner is not None else '', + url = url_constructor("/play/v4/playback/content/%s" % (contentId)) + encryption = "widevine" # if drm else "plain" + + """ if partner: resp = self.post(url, headers=self._getPlayHeaders(extra={"X-HS-Platform": "android"}), params=self._getPlayParams( subtag, encryption), max_age=-1, json={"user_id": "", "partner_data": "x", "data": {"third_party_bundle": partner}}) else: resp = self.get(url, headers=self._getPlayHeaders( ), params=self._getPlayParams(subtag, encryption), max_age=-1) - playBackSets = deep_get(resp, "data.playBackSets") + """ + # + # data = '{"os_name":"Windows","os_version":"10","app_name":"web","app_version":"7.41.0","platform":"Chrome","platform_version":"106.0.0.0","client_capabilities":{"ads":["non_ssai"],"audio_channel":["stereo"],"dvr":["short"],"package":["dash","hls"],"dynamic_range":["sdr"],"video_codec":["h264"],"encryption":["widevine"],"ladder":["tv"],"container":["fmp4","ts"],"resolution":["hd"]},"drm_parameters":{"widevine_security_level":["SW_SECURE_DECODE","SW_SECURE_CRYPTO"],"hdcp_version":["HDCP_NO_DIGITAL_OUTPUT"]},"resolution":"auto"}' + data = '{"os_name":"Android","os_version":"7.0","app_name":"android","app_version":"7.41.0","platform":"firetv","platform_version":"7.6.0.0","client_capabilities":{"ads":["non_ssai"],"audio_channel":["stereo"],"dvr":["short"],"package":["dash","hls"],"dynamic_range":["sdr"],"video_codec":["h264"],"encryption":["widevine"],"ladder":["phone"],"container":["fmp4","ts"],"resolution":["fhd","hd"]},"drm_parameters":{"widevine_security_level":["SW_SECURE_DECODE","SW_SECURE_CRYPTO"],"hdcp_version":["HDCP_NO_DIGITAL_OUTPUT"]},"resolution":"auto"}' + resp = self.post(url, headers=self._getPlayHeaders(includeST=True), params=self._getPlayParams( + subtag, encryption), max_age=-1, data=data) + playBackSets = deep_get(resp, "data.playback_sets") if playBackSets is None: return None, None, None playbackUrl, licenceUrl, playbackProto = HotstarAPI._findPlayback( @@ -118,33 +120,54 @@ def getExtItem(self, contentId): return "com.widevine.alpha" if item.get("encrypted") else False, item.get("isSubTagged") and "subs-tag:%s|" % item.get("features")[0].get("subType"), item.get("title") def doLogin(self): - url = url_constructor( - "/in/aadhar/v2/firetv/in/users/logincode/") - resp = self.post(url, headers={"Content-Length": "0"}) - code = deep_get(resp, "description.code") - yield (code, 1) - for i in range(2, 101): - resp = self.get(url+code, max_age=-1) - Script.log(resp, lvl=Script.INFO) - token = deep_get(resp, "description.userIdentity") + with PersistentDict("userdata.pickle") as db: + if db.get("token"): + self._refreshToken() + else: + token = guestToken() + # token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ1bV9hY2Nlc3MiLCJleHAiOjE2Njk4ODU2ODEsImlhdCI6MTY2OTI4MDg4MSwiaXNzIjoiVFMiLCJqdGkiOiI1OWVjM2MzNDZkMWU0Mzc3OGQ4Y2Y2NjY1ZTcwNTNkYiIsInN1YiI6IntcImhJZFwiOlwiMDg0ZjE4NjdmODVlNGYxMDkwODdlODc2YWI4ZWIyYWVcIixcInBJZFwiOlwiZGIxYzFlN2Q2NmFhNDg1ZDg4MzdiOGRhNzAzZWUwOWFcIixcIm5hbWVcIjpcIkd1ZXN0IFVzZXJcIixcImlwXCI6XCIxMDMuMTc3LjEzLjE0NlwiLFwiY291bnRyeUNvZGVcIjpcImluXCIsXCJjdXN0b21lclR5cGVcIjpcIm51XCIsXCJ0eXBlXCI6XCJndWVzdFwiLFwiaXNFbWFpbFZlcmlmaWVkXCI6ZmFsc2UsXCJpc1Bob25lVmVyaWZpZWRcIjpmYWxzZSxcImRldmljZUlkXCI6XCI5NTE5OWEwYi1jODVhLTQwNTUtYmE4MS1hZDcyNGUwNTk5MTNcIixcInByb2ZpbGVcIjpcIkFEVUxUXCIsXCJ2ZXJzaW9uXCI6XCJ2MlwiLFwic3Vic2NyaXB0aW9uc1wiOntcImluXCI6e319LFwiaXNzdWVkQXRcIjoxNjY5MjgwODgxNDY5fSIsInZlcnNpb24iOiIxXzAifQ.NbhndhIOpaU9XiZZIg0_0jQGH4CWTKJ69QHHvW74VZM' + db.clear() + db["token"] = token + db.flush() + mobile = Dialog().numeric(0, "Enter 10 Digit mobile number") + url = url_constructor("/um/v3/users/084f1867f85e4f109087e876ab8eb2ae/register?register-by=phone_otp") + data = { + "phone_number": mobile, + "country_prefix": "91", + "device_meta": {"device_name": "Chrome Browser on Windows"} + } + data = json.dumps(data) + resp = self.put(url, headers=self._getPlayHeaders(includeST=True, includeUM=True, extra={"x-hs-device-id": self.device_id, "x-request-id": self.device_id}), data=data) + if deep_get(resp, "message") == 'User verification initiated': + OTP = Dialog().numeric(0, "Enter 4 Digit OTP") + url = url_constructor( + "/um/v3/users/login?login-by=phone_otp") + + data = { + "phone_number": mobile, + "verification_code": OTP, + "device_meta": {"device_name": "Chrome Browser on Windows"} + } + data = json.dumps(data) + resp = self.put(url, headers=self._getPlayHeaders( + includeST=True, includeUM=True, extra={"x-hs-device-id": self.device_id}), data=data) + token = deep_get(resp, "user_identity") if token: with PersistentDict("userdata.pickle") as db: db["token"] = token - db["deviceId"] = str(uuid4()) + db["deviceId"] = self.device_id db["udata"] = json.loads(json.loads( - b64decode(token.split(".")[1]+"========")).get("sub")) - if db.get("isGuest"): - del db["isGuest"] + b64decode(token.split(".")[1] + "========")).get("sub")) db.flush() - yield code, 100 - break - yield code, i + Script.notify("Login Success", "You are logged in") def doLogout(self): with PersistentDict("userdata.pickle") as db: db.clear() db.flush() + Script.notify("Logout Success", "You are logged out") + return def get(self, url, **kwargs): try: @@ -160,6 +183,14 @@ def post(self, url, **kwargs): except Exception as e: return self._handleError(e, url, "post", **kwargs) + def put(self, url, **kwargs): + try: + response = self.session.put(url, **kwargs) + # xbmc.log(json.dumps(response.json())) + return response.json() + except Exception as e: + return self._handleError(e, url, "put", **kwargs) + def _handleError(self, e, url, _rtype, **kwargs): if e.__class__.__name__ == "ValueError": Script.log("Can not parse response of request url %s" % @@ -170,9 +201,9 @@ def _handleError(self, e, url, _rtype, **kwargs): with PersistentDict("userdata.pickle") as db: if db.get("isGuest"): Script.notify( - "Login Error", "Please login to watch this content") - executebuiltin( - "RunPlugin(plugin://plugin.video.botallen.hotstar/resources/lib/main/login/)") + "Subscription Error", "Please subscribe to watch this content") + # executebuiltin( + # "RunPlugin(plugin://plugin.video.botallen.hotstar/resources/lib/main/login/)") else: Script.notify( "Subscription Error", "You don't have valid subscription to watch this content", display_time=2000) @@ -211,11 +242,11 @@ def _refreshToken(self): with PersistentDict("userdata.pickle") as db: oldToken = db.get("token") if oldToken: - resp = self.session.get(url_constructor("/in/aadhar/v2/firetv/in/users/refresh-token"), - headers={"userIdentity": oldToken, "deviceId": db.get("deviceId", str(uuid4()))}, raise_for_status=False, max_age=-1).json() + resp = self.session.get(url_constructor("/um/v3/users/refresh"), + headers=self._getPlayHeaders(includeST=True, includeUM=False, extra={"x-hs-device-id": self.device_id, "x-request-id": self.device_id}), raise_for_status=False, max_age=-1).json() if resp.get("errorCode"): return resp.get("message") - new_token = deep_get(resp, "description.userIdentity") + new_token = deep_get(resp, "user_identity") db['token'] = new_token db.flush() return new_token @@ -224,49 +255,40 @@ def _refreshToken(self): return e @staticmethod - def _getPlayHeaders(includeST=False, playbackUrl=None, extra={}): + def _getPlayHeaders(includeST=False, includeUM=False, playbackUrl=None, extra={}): with PersistentDict("userdata.pickle") as db: token = db.get("token") - auth = HotstarAPI._getAuth(includeST) + + auth = getAuth(includeST, False, includeUM) headers = { "hotstarauth": auth, - "X-Country-Code": "in", - "X-HS-AppVersion": "3.3.0", - "X-HS-Platform": "firetv", - "X-HS-UserToken": token, - "User-Agent": "Hotstar;in.startv.hotstar/3.3.0 (Android/8.1.0)", + "x-hs-platform": "firetv", + "x-hs-appversion": "7.41.0", + "content-type": "application/json", + "x-country-code": "in", + "x-platform-code": "PCTV", + "x-hs-usertoken": token, + "x-hs-request-id": HotstarAPI.device_id, + "user-agent": "Hotstar;in.startv.hotstar/3.3.0 (Android/8.1.0)", + # Mozilla/5.0 (Linux; Android 7.0; Redmi Note 4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Mobile Safari/537.36 **extra, } if playbackUrl: r = Request(playbackUrl) - r.add_header("User-Agent", headers.get("User-Agent")) + r.add_header("user-agent", headers.get("user-agent")) cookie = urlopen(r).headers.get("Set-Cookie", "").split(";")[0] if cookie: headers["Cookie"] = cookie return headers - @staticmethod - def _getAuth(includeST=False, persona=False): - _AKAMAI_ENCRYPTION_KEY = b'\x05\xfc\x1a\x01\xca\xc9\x4b\xc4\x12\xfc\x53\x12\x07\x75\xf9\xee' - if persona: - _AKAMAI_ENCRYPTION_KEY = b"\xa0\xaa\x8b\xcf\x9d\xd5\x8e\xc6\xe3\xb5\x7d\x9b\x4e\x5a\x00\x80\xb1\x45\x0d\xf7\x43\x6c\xfa\x22\xdd\x5c\xff\xdf\xea\x8e\x12\x52" - st = int(time.time()) - exp = st + 6000 - auth = 'st=%d~exp=%d~acl=/*' % (st, - exp) if includeST else 'exp=%d~acl=/*' % exp - auth += '~hmac=' + hmac.new(_AKAMAI_ENCRYPTION_KEY, - auth.encode(), hashlib.sha256).hexdigest() - return auth - @staticmethod def _getPlayParams(subTag="", encryption="widevine"): with PersistentDict("userdata.pickle") as db: - deviceId = db.get("deviceId", str(uuid4())) + deviceId = db.get("deviceId", HotstarAPI.device_id) return { - "os-name": "firetv", - "desired-config": "audio_channel:stereo|encryption:%s|ladder:tv|package:dash|%svideo_codec:h264" % (encryption, subTag or ""), "device-id": deviceId, - "os-version": "8.1.0" + "desired-config": "audio_channel:stereo|container:fmp4|dynamic_range:sdr|encryption:%s|ladder:phone|package:dash|resolution:fhd|%svideo_codec:h264" % (encryption, subTag or "") + # "desired-config": "ads:non_ssai|audio_channel:stereo|container:ts|dvr:short|dynamic_range:sdr|encryption:plain|ladder:web|language:hin|package:hls|resolution:fhd|video_codec:h264" } @staticmethod @@ -277,14 +299,14 @@ def _findPlayback(playBackSets, lang=None, ask=False): quality = {"4k": 0, "hd": 1, "sd": 2} for each in playBackSets: config = {k: v for d in map(lambda x: dict([x.split(":")]), each.get( - "tagsCombination", "a:b").split(";")) for k, v in d.items()} + "tags_combination", "a:b").split(";")) for k, v in d.items()} Script.log( f"Checking combination {config} with language {lang}", lvl=Script.DEBUG) if config.get("encryption", "") in ["plain", "widevine"] and config.get("package", "") in ["hls", "dash"]: if lang and config.get("language") and config.get("language", "") != lang: continue - config["playback"] = (each.get("playbackUrl"), each.get( - "licenceUrl"), "mpd" if config.get("package") == "dash" else "hls") + config["playback"] = (each.get("playback_url"), each.get( + "licence_url"), "mpd" if config.get("package") == "dash" else "hls") if selected is None: selected = config["playback"] if config.get("ladder"): diff --git a/resources/lib/builder.py b/resources/lib/builder.py index b960e6c..89d3a41 100644 --- a/resources/lib/builder.py +++ b/resources/lib/builder.py @@ -1,12 +1,11 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals from datetime import datetime from codequick import Listitem, Script, Resolver, Route from codequick.storage import PersistentDict -from urlquick import MAX_AGE import inputstreamhelper -from .contants import url_constructor, IMG_THUMB_H_URL, IMG_POSTER_V_URL, IMG_FANART_H_URL, MEDIA_TYPE, BASE_HEADERS, TRAY_IDENTIFIERS, PERSONA_BASE_URL, NAME +from .contants import IMG_THUMB_H_URL, IMG_POSTER_V_URL, IMG_FANART_H_URL, MEDIA_TYPE, BASE_HEADERS, TRAY_IDENTIFIERS, PERSONA_BASE_URL from .api import deep_get, HotstarAPI from .utils import updateQueryParams from urllib.parse import urlencode @@ -14,7 +13,6 @@ from binascii import hexlify import urlquick import re -import json class Builder: diff --git a/resources/lib/contants.py b/resources/lib/contants.py index 7c7aea0..3b30100 100644 --- a/resources/lib/contants.py +++ b/resources/lib/contants.py @@ -1,4 +1,4 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from codequick.utils import urljoin_partial NAME = "plugin.video.botallen.hotstar" @@ -11,7 +11,7 @@ IMG_POSTER_V_URL = IMG_BASE + "/f_auto,t_web_vl_3x/%s.jpg" IMG_THUMB_H_URL = IMG_BASE + "/f_auto,t_web_hs_3x/%s.jpg" -BASE_HEADERS = {"x-country-code": "in", "x-platform-code": "ANDROID"} +BASE_HEADERS = {"x-country-code": "IN", "x-platform-code": "ANDROID"} CONTENT_TYPE = {"MOVIE": "movies", "SHOW": "tvshows", "SEASON": "tvshows", "EPISODE": "episodes"} MEDIA_TYPE = {"MOVIE": "movie", "SHOW": "tvshow", diff --git a/resources/lib/main.py b/resources/lib/main.py index 6cc5b3a..ff03ceb 100644 --- a/resources/lib/main.py +++ b/resources/lib/main.py @@ -1,17 +1,14 @@ -# -*- coding: utf-8 -*- +# -*- coding: utf-8 -*- from __future__ import unicode_literals from codequick import Route, run, Script, Resolver import resources.lib.utils as U -from xbmcgui import DialogProgress from xbmc import executebuiltin from xbmcplugin import SORT_METHOD_EPISODE, SORT_METHOD_DATE -import time -import urlquick from .api import HotstarAPI from .builder import Builder -from .contants import BASE_HEADERS, CONTENT_TYPE +from .contants import CONTENT_TYPE @Route.register @@ -69,16 +66,7 @@ def play_ext(plugin, contentId, partner=None): @Script.register def login(plugin): - msg = "1. Go to [B]https://tv.hotstar.com[/B]\n2. Login with your hotstar account[CR]3. Enter the 4 digit code : " - pdialog = DialogProgress() - pdialog.create("Login", msg+"Loading...") - for code, i in api.doLogin(): - if pdialog.iscanceled() or i == 100: - break - else: - time.sleep(1) - pdialog.update(i, msg+"[B][UPPERCASE]%s[/UPPERCASE][/B]" % code) - pdialog.close() + api.doLogin() @Script.register diff --git a/resources/lib/utils.py b/resources/lib/utils.py index fb58234..8c89804 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -5,9 +5,11 @@ from .contants import url_constructor from urllib.parse import urlparse, parse_qsl, urlencode, urlunparse import urlquick +import json from uuid import uuid4 - -from xbmc import executebuiltin +import time +import hashlib +import hmac def deep_get(dictionary, keys, default=None): @@ -31,19 +33,51 @@ def login_wrapper(*args, **kwargs): else: # login require Script.notify( - "Login Error", "Please login to watch this content") - executebuiltin( - "RunPlugin(plugin://plugin.video.botallen.hotstar/resources/lib/main/login/)") + "Login Error", "You need valid subscription to watch this content") + # executebuiltin( + # "RunPlugin(plugin://plugin.video.botallen.hotstar/resources/lib/main/login/)") return False return login_wrapper +def getAuth(includeST=False, persona=False, includeUM=False): + _AKAMAI_ENCRYPTION_KEY = b'\x05\xfc\x1a\x01\xca\xc9\x4b\xc4\x12\xfc\x53\x12\x07\x75\xf9\xee' + if persona: + _AKAMAI_ENCRYPTION_KEY = b"\xa0\xaa\x8b\xcf\x9d\xd5\x8e\xc6\xe3\xb5\x7d\x9b\x4e\x5a\x00\x80\xb1\x45\x0d\xf7\x43\x6c\xfa\x22\xdd\x5c\xff\xdf\xea\x8e\x12\x52" + st = int(time.time()) + + um = '/um/v3' if includeUM else '' + exp = st + 6000 + auth = 'st=%d~exp=%d~acl=%s/*' % (st, exp, + um) if includeST else 'exp=%d~acl=/*' % exp + auth += '~hmac=' + hmac.new(_AKAMAI_ENCRYPTION_KEY, + auth.encode(), hashlib.sha256).hexdigest() + return auth + + def guestToken(): - resp = urlquick.post(url_constructor("/in/aadhar/v2/firetv/in/user/guest-signup"), json={ - "idType": "device", - "id": str(uuid4()), - }).json() - return deep_get(resp, "description.userIdentity") + hdr = { + 'hotstarauth': getAuth(includeST=True, persona=False, includeUM=True), + 'x-hs-platform': 'firetv', + 'x-country-code': 'IN', + 'x-hs-appversion': '7.42.0', + 'x-request-id': str(uuid4()), + 'x-hs-device-id': str(uuid4()), + 'Content-Type': 'application/json', + } + data = { + "device_ids": [{"id": "95199a0b-c85a-4055-ba81-ad724e059913", "type": "device_id"}], + "device_meta": {"network_operator": "4g - 4.6 - 50", "os_name": "Windows", "os_version": "10"} + } + data = json.dumps(data) + + # data = json.dumps( + # {"device_ids": [{"id": str(uuid4()), "type": "device_id"}]}).encode() + + resp = urlquick.post(url_constructor("/um/v3/users"), + data=data, headers=hdr).json() + + return deep_get(resp, "user_identity") def updateQueryParams(url, params):