Aswin ecb26968da Start support for Hotstar
Hotstar is currently throwing errors when getting tracks, will be fixed with next commit.
Fixed error when loading Widevine Device.
2025-04-01 02:41:59 +05:30

544 lines
24 KiB
Python

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/(?P<type>movies|shows)/[a-z0-9-]+-)?(?P<id>\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.")