import os import re import time import json import m3u8 import base64 import requests import click from vinetrimmer.objects import TextTrack, Title, Tracks from vinetrimmer.services.BaseService import BaseService class Sonyliv(BaseService): """ SonyLiv India streaming service (https://sonyliv.com). \b Authorization: Cookies + accessToken(from Browser Local Storage) Security: UHD@L3, doesn't seem to care about releases. Script By https://telegram.me/divine_404 """ ALIASES = ["SL", "sonyliv"] TITLE_RE = r"^(?:https?://(?:www\.)?sonyliv.com/(?Pmovies|shows)/[a-z0-9-]+-)?(?P\d+)" @staticmethod @click.command(name="Sonyliv", short_help="https://sonyliv.com") @click.argument("title", type=str, required=False) @click.option("-d", "--device", default="chrome", type=click.Choice(["chrome", "android", "safari"], case_sensitive=False), help="Device to use for requesting manifest.") @click.pass_context def cli(ctx, **kwargs): return Sonyliv(ctx, **kwargs) def __init__(self, ctx, title, device): super().__init__(ctx) self.m = self.parse_title(ctx, title) self.manifestDevice = device self.vcodec = ctx.parent.params["vcodec"] or "H264" self.acodec = ctx.parent.params["acodec"] or "EC3" self.range = ctx.parent.params["range_"] or "SDR" self.quality = ctx.parent.params.get("quality") or 1080 self.profile = ctx.obj.profile self.device_id = None self.app_version = None self.accessToken = None self.securityToken = None self.license_api = None self.cacheData = None self.configure() def get_titles(self): tempHeaders = self.session.headers.copy() tempHeaders.update({ "Host": "apiv2.sonyliv.com", "Security_token": self.securityToken, "Session_id": self.session.cookies.get('sessionId', None, 'apiv2.sonyliv.com'), "Device_id": self.device_id, "App_version": self.app_version, }) r = requests.get( url = self.config['endpoints']['title'].format(id=self.m['id']), headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) try: titleRes = json.loads(r.content.decode()) except json.JSONDecodeError: raise ValueError(f"Received Irrelevant Title API Response: {r.text}") for correct in titleRes['resultObj']['containers']: if int(correct['id']) == int(self.m['id']): titleRes = correct.copy() if titleRes['metadata']['objectSubtype'] == 'MOVIE_BUNDLE' or titleRes['metadata']['objectSubtype'] == 'MOVIE': return Title( id_=self.m['id'], type_=Title.Types.MOVIE, name=titleRes['metadata']['title'], year=titleRes['metadata']['emfAttributes']['release_year'] if 'release_year' in titleRes['metadata']['emfAttributes'] else titleRes['metadata']['emfAttributes']['release_date'].split('-')[0], original_lang=titleRes['metadata']['language'], source=self.ALIASES[0], service_data=titleRes, ) elif (titleRes['layout'] == "BUNDLE_ITEM"): bucket = [] if (titleRes['metadata']['objectSubtype'] == "EPISODIC_SHOW"): ep_count = titleRes['episodeCount'] r = requests.get( url = self.config['endpoints']['season'].format(id=titleRes['id'], ep_start=0, ep_end=ep_count-1), headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) try: seasonRes = json.loads(r.content.decode()) except json.JSONDecodeError: raise ValueError(f"Received Irrelevant Season API Response: {r.text}") for episode in seasonRes['resultObj']['containers'][0]['containers']: bucket.append({ "episode_id": episode['id'], "series_name": titleRes['metadata']['title'], "season_number": titleRes['metadata']['season'] if ('season' in titleRes['metadata'].keys()) else "1", "episode_number": episode['metadata']['episodeNumber'], "episode_name": episode['metadata']['episodeTitle'], "episode_org_lang": episode['metadata']['language'], "service_data": episode }) if (titleRes['metadata']['objectSubtype'] == "SHOW"): for season in titleRes['containers']: if season['metadata']['objectSubtype'] == "SEASON" and int(season['parents'][0]['parentId']) == int(self.m['id']): ep_count = season['episodeCount'] r = requests.get( url = self.config['endpoints']['season'].format(id=season['id'], ep_start=0, ep_end=ep_count-1), headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) try: seasonRes = json.loads(r.content.decode()) except json.JSONDecodeError: raise ValueError(f"Received Irrelevant Season API Response: {r.text}") for episode in seasonRes['resultObj']['containers'][0]['containers']: bucket.append({ "episode_id": episode['id'], "series_name": titleRes['metadata']['title'], "season_number": season['metadata']['season'], "episode_number": episode['metadata']['episodeNumber'], "episode_name": episode['metadata']['episodeTitle'], "episode_org_lang": episode['metadata']['language'], "service_data": episode }) elif season['metadata']['objectSubtype'] == "EPISODE_RANGE" and int(season['parents'][0]['parentId']) == int(self.m['id']): ep_count = season['episodeCount'] r = requests.get( url = self.config['endpoints']['season'].format(id=season['id'], ep_start=0, ep_end=ep_count-1), headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) try: seasonRes = json.loads(r.content.decode()) except json.JSONDecodeError: raise ValueError(f"Received Irrelevant Season API Response: {r.text}") for episode in seasonRes['resultObj']['containers'][0]['containers']: bucket.append({ "episode_id": episode['id'], "series_name": titleRes['metadata']['title'], "season_number": season['metadata']['season'], "episode_number": episode['metadata']['episodeNumber'], "episode_name": episode['metadata']['episodeTitle'], "episode_org_lang": episode['metadata']['language'], "service_data": episode }) if not bucket == []: return [Title( id_=b['episode_id'], type_=Title.Types.TV, name=b['series_name'], season=b['season_number'], episode=b['episode_number'], episode_name=b['episode_name'], original_lang=b['episode_org_lang'], source=self.ALIASES[0], service_data=b['service_data'] ) for b in bucket] else: self.log.exit(" - Title unsupported.") def get_tracks(self, title): if self.vcodec == 'H265': if self.range == 'DV': client = '{"device_make":"Amazon","device_model":"AFTMM","display_res":"2160","viewport_res":"2160","supp_codec":"HEVC,H264,AAC,EAC3,AC3,ATMOS","audio_decoder":"EAC3,AAC,AC3,ATMOS","hdr_decoder":"DOLBY_VISION","td_user_useragent":"com.onemainstream.sonyliv.android\/8.95 (Android 7.1.2; en_IN; AFTMM; Build\/NS6281 )"}' elif self.range == 'HDR10': client = '{"device_make":"Amazon","device_model":"AFTMM","display_res":"2160","viewport_res":"2160","supp_codec":"HEVC,H264,AAC,EAC3,AC3,ATMOS","audio_decoder":"EAC3,AAC,AC3,ATMOS","hdr_decoder":"HDR10","td_user_useragent":"com.onemainstream.sonyliv.android\/8.95 (Android 7.1.2; en_IN; AFTMM; Build\/NS6281 )"}' elif self.range == 'SDR': client = '{"device_make":"Amazon","device_model":"AFTMM","display_res":"2160","viewport_res":"2160","supp_codec":"HEVC,H264,AAC,EAC3,AC3,ATMOS","audio_decoder":"EAC3,AAC,AC3,ATMOS","hdr_decoder":"HLG","td_user_useragent":"com.onemainstream.sonyliv.android\/8.95 (Android 7.1.2; en_IN; AFTMM; Build\/NS6281 )"}' else: client = '{"os_name":"Mac OS","os_version":"10.15.7","device_make":"none","device_model":"none","display_res":"1470","viewport_res":"894","conn_type":"4g","supp_codec":"H264,AV1,AAC","client_throughput":"16000","td_user_agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36","hdr_decoder":"UNKNOWN","audio_decoder":"STEREO"}' tempHeaders = self.session.headers.copy() if "X-Playback-Session-Id" in tempHeaders.keys(): tempHeaders.pop("X-Playback-Session-Id") tempHeaders.update({ "Host": "apiv2.sonyliv.com", "Content-Type": "application/json", "X-Via-Device": "true", "Security_token": self.securityToken, "App_version": self.app_version, "Device_id": self.device_id, "Session_id": self.session.cookies.get('sessionId', None, 'apiv2.sonyliv.com'), "Authorization": "Bearer " + self.accessToken, "Td_client_hints": client, }) if str(title.type) == "Types.MOVIE": if 'containers' not in title.service_data.keys(): _id_ = title.service_data['metadata']['contentId'] else: for mov in title.service_data['containers']: if mov['metadata']['contentSubtype'] == "MOVIE": _id_ = mov['id'] else: _id_ = title.service_data['metadata']['contentId'] r = requests.post( url = self.config['endpoints']['manifest'].format(id=_id_, bid=self.cacheData['contactId']['id']), headers = tempHeaders, cookies = self.reqCookies, json = { "actionType": "play", "browser": 'chrome', "deviceId": self.device_id, "hasLAURLEnabled": True, "os": "Mac OS", "platform": "web", "adsParams":{ "idtype": "uuid", "rdid": self.device_id, "is_lat": 0, "ppid": self.cacheData['contactId']['PPID'] } }, proxies=self.session.proxies ) try: manifestRes = json.loads(r.content.decode()) self.log.debug(f"\n{json.dumps(manifestRes, indent=4)}") if manifestRes['resultCode'] != "OK": self.log.exit(manifestRes['message']) except json.JSONDecodeError: raise ValueError(f"Received Irrelevant Manifest API Response: {r.text}") mpd_url = manifestRes['resultObj']['videoURL'] try: self.license_api = manifestRes['resultObj']['LA_Details']['laURL'] except Exception as e: pass self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Referer": "https://www.sonyliv.com/", "X-Playback-Session-Id": self.device_id, "Origin": "https://www.sonyliv.com", }) r = self.session.get(mpd_url) if not '.m3u8' in str(mpd_url): tracks = Tracks.from_mpd( url = mpd_url, data = r.content.decode(), session = self.session, source = self.ALIASES[0], ) else: tracks = Tracks.from_m3u8( m3u8.loads(str(r.content.decode())), source = self.ALIASES[0], ) # Checking SDR/HDR/DV for video in tracks.videos: video.hdr10 = False video.dv = False video.hlg = False av_range_ = manifestRes['resultObj']['additionalDataJson']['video_quality'] if av_range_ == "HDR": for video in tracks.videos: video.hdr10 = True if av_range_ == "DOLBY_VISION": for video in tracks.videos: video.dv = True if av_range_ == "HLG": for video in tracks.videos: video.hlg = True # Adding subtitle tracks for sub in manifestRes['resultObj']['subtitle']: tracks.add(TextTrack( id_= sub['subtitleId'], source = self.ALIASES[0], url = sub['subtitleUrl'], codec = "vtt", #hardcoded language = sub['subtitleLanguageName'], ), warn_only=True) for track in tracks: track.needs_proxy = True return tracks def get_chapters(self, title): return [] def certificate(self, **_): return None def license(self, challenge: bytes, title: Title, **_): if self.license_api == None: licHeaders = self.session.headers.copy() if "X-Playback-Session-Id" in licHeaders.keys(): licHeaders.pop("X-Playback-Session-Id") licHeaders.update({ "Host": "apiv2.sonyliv.com", "Content-Type": "application/json", "Security_token": self.securityToken, "Device_id": self.device_id, "X-Via-Device": "true", "Authorization": "Bearer " + self.accessToken }) r = requests.post( url = self.config['endpoints']['license'], headers = licHeaders, json = { "platform": self.config['device'][self.manifestDevice]['platform'], "deviceId": self.device_id, "actionType": "play", "browser": self.manifestDevice, "assetId": title.service_data['metadata']['contentId'], "os": self.manifestDevice } ) try: licRes = json.loads(r.content.decode()) except json.JSONDecodeError: raise ValueError(f"Irrelevant License API Response: {r.text}") self.license_api = licRes['resultObj']['laURL'] return requests.post( url = self.license_api, data=challenge, # proxies=self.session.proxies, ).content else: return requests.post( url = self.license_api, data=challenge, # proxies=self.session.proxies, ).content def configure(self): self.session.headers.update({ "User-Agent": self.config['device'][self.manifestDevice]['user-agent'], "Accept": "*/*", "Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", "Referer": "https://www.sonyliv.com/", "Origin": "https://www.sonyliv.com", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "same-site", }) if not self.cookies: raise self.log.exit(" - Please add cookies") self.reqCookies = { "_abck": self.session.cookies.get('_abck', None, '.sonyliv.com'), "ak_bmsc": self.session.cookies.get('ak_bmsc', None, '.sonyliv.com'), "bm_sz": self.session.cookies.get('bm_sz', None, '.sonyliv.com'), } self.device_id = self.config['device'][self.manifestDevice]['device_id'] self.app_version = self.config['device'][self.manifestDevice]['app_version'] self.prepToken() def prepToken(self): cache_path = self.get_cache("{profile}_ProfileCache.json".format(profile=self.profile)) if not os.path.isfile(cache_path): self.cacheData = {"vt_profile": self.profile} self.log.info(" + Generating Cache...") self.log.info("Enter your access_token from Browser (Dev Tools -> Application -> Local Storage -> 'https://www.sonyliv.com' -> accessToken):") self.accessToken = str(input(">")) self.cacheData["accessToken"] = { "rawToken": self.accessToken, "data": json.loads(base64.b64decode(f"{str(self.accessToken.split('.')[1]) + '=='}")) } if int(self.cacheData['accessToken']['data']['exp']) < int(time.time()): raise self.log.exit(f" - Provided access_token is expired.") self.log.info("Getting security_token...") self.securityToken = self.getSecurityToken() userData = self.refresh() self.cacheData["securityToken"] = { "rawToken": self.securityToken, "data": json.loads(base64.b64decode(f"{str(self.accessToken.split('.')[1]) + '=='}")) } self.cacheData["contactId"] = { "id": userData['resultObj']['contactMessage'][0]['contactID'], "PPID": self.getHash(userData['resultObj']['contactMessage'][0]['contactID']) } os.makedirs(os.path.dirname(cache_path), exist_ok=True) with open(cache_path, "w", encoding="utf-8") as fd: json.dump(self.cacheData, fd, indent = 2) else: with open(cache_path, "r+", encoding="utf-8") as fd: self.cacheData = json.loads(fd.read()) if int(self.cacheData['accessToken']['data']['exp']) < int(time.time()): raise self.log.exit("- access_token expired. Delete cache file and update cookies.") else: self.accessToken = self.cacheData['accessToken']['rawToken'] if int(self.cacheData['securityToken']['data']['exp']) < int(time.time()): self.log.info("security_token expired, Getting a new one...") self.securityToken = self.getSecurityToken() userData = self.refresh() self.cacheData["securityToken"] = { "rawToken": self.securityToken, "data": json.loads(base64.b64decode(f"{str(self.securityToken.split('.')[1]) + '=='}")) } self.cacheData["contactId"] = { "id": userData['resultObj']['contactMessage'][0]['contactID'], "PPID": self.getHash(userData['resultObj']['contactMessage'][0]['contactID']) } else: self.securityToken = self.cacheData['securityToken']['rawToken'] userData = self.refresh() self.cacheData["contactId"] = { "id": userData['resultObj']['contactMessage'][0]['contactID'], "PPID": self.getHash(userData['resultObj']['contactMessage'][0]['contactID']) } self.log.info("Using Account Tokens from Cache.") fd.seek(0) fd.truncate() json.dump(self.cacheData, fd, indent = 2) return def getSecurityToken(self): tempHeaders = self.session.headers.copy() tempHeaders.update({ "Host": "www.sonyliv.com", "Upgrade-Insecure-Requests": "1", "Sec-Fetch-Site": "none", }) try: resp = requests.get( url="https://sonyliv.com", headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) resp = str(resp.content.decode()) scToken = resp.split('securityToken:{resultCode:"OK",message:"",errorDescription:"200-10000",resultObj:"')[1].split('",systemTime')[0] json.loads(base64.b64decode(f"{scToken.split('.')[1] + '.'}")) return scToken except Exception as e: self.log.exit(e) def refresh(self): tempHeaders = self.session.headers.copy() tempHeaders.update({ "Host": "apiv2.sonyliv.com", "X-Via-Device": "true", "Security_token": self.securityToken, "App_version": self.app_version, "Device_id": self.device_id, "Session_id": self.session.cookies.get('sessionId', None, 'apiv2.sonyliv.com'), "Authorization": self.accessToken, }) self.reqCookies.update({ "sessionId": self.session.cookies.get('sessionId', None, 'apiv2.sonyliv.com'), "bm_sv": self.session.cookies.get('bm_sv', None, '.sonyliv.com'), "AKA_A2": self.session.cookies.get('AKA_A2', None, '.sonyliv.com'), "bm_mi": self.session.cookies.get('bm_mi', None, '.sonyliv.com'), }) userData = requests.get( url = self.config['endpoints']['refresh'], headers = tempHeaders, cookies = self.reqCookies, proxies=self.session.proxies ) userData = json.loads(userData.content.decode()) if userData['resultCode'] == "OK" and userData['message'] == "SUCCESS": return userData else: self.log.error(userData) self.log.exit("Unintended API Response.") def getHash(self, contactId): tempHeaders = self.session.headers.copy() tempHeaders.update({ "Host": "apiv2.sonyliv.com", "Content-Type": "application/json", "X-Via-Device": "true", "Security_token": self.securityToken, "App_version": self.app_version, "Device_id": self.device_id, "Session_id": self.session.cookies.get('sessionId', None, 'apiv2.sonyliv.com'), "Authorization": self.accessToken, }) hashData = requests.post( url = self.config['endpoints']['hash'], headers = tempHeaders, json = { "baseId": contactId }, cookies = self.reqCookies, proxies=self.session.proxies ) hashData = json.loads(hashData.content.decode()) if hashData['resultCode'] == "OK": return hashData['resultObj']['ppId'] else: self.log.error(hashData) self.log.exit("Unintended API Response.")