Start support for Hotstar
Hotstar is currently throwing errors when getting tracks, will be fixed with next commit. Fixed error when loading Widevine Device.
This commit is contained in:
parent
ddcb82a853
commit
ecb26968da
BIN
Vinetrimmer - CHROME CDM version.zip
Normal file
BIN
Vinetrimmer - CHROME CDM version.zip
Normal file
Binary file not shown.
@ -48,9 +48,9 @@ Unidecode = "^1.2.0"
|
||||
validators = "^0.18.2"
|
||||
websocket-client = "^1.1.0"
|
||||
xmltodict = "^0.14.0"
|
||||
yt-dlp = "^2022.11.11"
|
||||
yt-dlp = "^2024.11.11"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
flake8 = "^3.8.4"
|
||||
isort = "^5.9.2"
|
||||
pyinstaller = "^4.4"
|
||||
|
16
sonyliv.yml
Normal file
16
sonyliv.yml
Normal file
@ -0,0 +1,16 @@
|
||||
endpoints:
|
||||
refresh: 'https://apiv2.sonyliv.com/AGL/2.9/A/ENG/WEB/IN/TN/GETPROFILE?channelPartnerID=MSMIND'
|
||||
hash: 'https://apiv2.sonyliv.com/AGL/2.0/SR/ENG/WEB/IN/TN/GETHASHVALUE'
|
||||
title: 'https://apiv2.sonyliv.com/AGL/2.6/R/ENG/WEB/IN/MH/DETAIL-V2/{id}'
|
||||
season: 'https://apiv2.sonyliv.com/AGL/2.6/SR/ENG/WEB/IN/MH/CONTENT/DETAIL/BUNDLE/{id}?from={ep_start}&to={ep_end}&orderBy=episodeNumber&sortOrder=asc&kids_safe=false'
|
||||
# manifest: 'https://apiv2.sonyliv.com/AGL/3.8/SR/ENG/WEB/IN/MH/CONTENT/VIDEOURL/VOD/{id}?contactId={bid}'
|
||||
manifest: 'https://apiv2.sonyliv.com/AGL/3.8/SR/ENG/SONY_ANDROID_TV/IN/MH/CONTENT/VIDEOURL/VOD/{id}?contactId={bid}'
|
||||
license: 'https://apiv2.sonyliv.com/AGL/1.4/SR/ENG/WEB/IN/CONTENT/GETLAURL'
|
||||
|
||||
device:
|
||||
chrome:
|
||||
device_id: '577f872ae8554c14a16ac1ef3d11c0e9-1712427232539'
|
||||
app_version: '3.5.63'
|
||||
platform: 'web'
|
||||
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'
|
||||
td_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"}'
|
@ -278,9 +278,9 @@ def dl(ctx, profile, cdm, *_, **__):
|
||||
except ValueError as e:
|
||||
raise log.exit(f" - {e}")
|
||||
|
||||
device_name = device.system_id if "set_service_certificate" in dir(device) else device.get_name()
|
||||
device_name = device.system_id if "vmp" in dir(device) else device.get_name()
|
||||
log.info(f" + Loaded {device.__class__.__name__}: {device_name} (L{device.security_level})")
|
||||
cdm = Cdm(device) if "set_service_certificate" in dir(device) else CdmPr.from_device(device)
|
||||
cdm = Cdm.from_device(device) if "vmp" in dir(device) else CdmPr.from_device(device)
|
||||
|
||||
if profile:
|
||||
cookies = get_cookie_jar(service, profile)
|
||||
|
16
vinetrimmer/config/Services/hotstar.yml
Normal file
16
vinetrimmer/config/Services/hotstar.yml
Normal file
@ -0,0 +1,16 @@
|
||||
endpoints:
|
||||
login: 'https://api.hotstar.com/in/aadhar/v2/web/th/user/login'
|
||||
refresh: 'https://www.hotstar.com/api/internal/bff/v2/start'
|
||||
tv_title: 'https://api.hotstar.com/o/v1/show/detail'
|
||||
tv_episodes: 'https://api.hotstar.com/o/v1/tray/g/1/detail'
|
||||
movie_title: 'https://api.hotstar.com/o/v1/movie/detail'
|
||||
manifest: 'https://api.hotstar.com/play/v4/playback/content/{id}'
|
||||
|
||||
device:
|
||||
os:
|
||||
name: 'Windows'
|
||||
version: 10
|
||||
|
||||
platform:
|
||||
name: 'web'
|
||||
version: '7.35.0'
|
BIN
vinetrimmer/devices/xiaomi_mi_a1_15.0.0_60ceee88_8159_l3.wvd
Normal file
BIN
vinetrimmer/devices/xiaomi_mi_a1_15.0.0_60ceee88_8159_l3.wvd
Normal file
Binary file not shown.
Binary file not shown.
@ -11,6 +11,11 @@ from vinetrimmer.services.appletvplus import AppleTVPlus
|
||||
from vinetrimmer.services.max import Max
|
||||
from vinetrimmer.services.netflix import Netflix
|
||||
from vinetrimmer.services.peacock import Peacock
|
||||
from vinetrimmer.services.hotstar import Hotstar
|
||||
from vinetrimmer.services.jio import Jio
|
||||
from vinetrimmer.services.moviesanywhere import MoviesAnywhere
|
||||
from vinetrimmer.services.sonyliv import Sonyliv
|
||||
|
||||
|
||||
# Above is necessary since dynamic imports like below fuck up nuitak
|
||||
|
||||
|
449
vinetrimmer/services/hotstar.py
Normal file
449
vinetrimmer/services/hotstar.py
Normal file
@ -0,0 +1,449 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import uuid
|
||||
import re
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
from urllib.request import urlopen, Request
|
||||
import http.cookiejar as cookiejar
|
||||
|
||||
import click
|
||||
|
||||
from vinetrimmer.objects import Title, Tracks
|
||||
from vinetrimmer.services.BaseService import BaseService
|
||||
from vinetrimmer.config import config, directories
|
||||
|
||||
class Hotstar(BaseService):
|
||||
"""
|
||||
Service code for Star India's Hotstar (aka Disney+ Hotstar) streaming service (https://hotstar.com).
|
||||
|
||||
\b
|
||||
Authorization: Credentials
|
||||
Security: UHD@L3, doesn't seem to care about releases.
|
||||
|
||||
\b
|
||||
Tips: - The library of contents can be viewed without logging in at https://hotstar.com
|
||||
- The homepage hosts domestic programming; Disney+ content is at https://hotstar.com/in/disneyplus
|
||||
"""
|
||||
|
||||
ALIASES = ["HS", "hotstar"]
|
||||
#GEOFENCE = ["in"]
|
||||
TITLE_RE = r"^(?:https?://(?:www\.)?hotstar\.com/[a-z0-9/-]+/)(?P<id>\d+)"
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="Hotstar", short_help="https://hotstar.com")
|
||||
@click.argument("title", type=str, required=False)
|
||||
@click.option("-q", "--quality", default="fhd",
|
||||
type=click.Choice(["4k", "fhd", "hd", "sd"], case_sensitive=False),
|
||||
help="Manifest quality to request.")
|
||||
@click.option("-c", "--channels", default="5.1", type=click.Choice(["5.1", "2.0", "atmos"], case_sensitive=False),
|
||||
help="Audio Codec")
|
||||
@click.option("-rg", "--region", default="in", type=click.Choice(["in", "id", "th"], case_sensitive=False),
|
||||
help="Account region")
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return Hotstar(ctx, **kwargs)
|
||||
|
||||
def __init__(self, ctx, title, quality, channels, region):
|
||||
super().__init__(ctx)
|
||||
self.parse_title(ctx, title)
|
||||
self.quality = quality
|
||||
self.channels = channels
|
||||
self.region = region.lower()
|
||||
|
||||
assert ctx.parent is not None
|
||||
|
||||
self.vcodec = ctx.parent.params["vcodec"]
|
||||
self.acodec = ctx.parent.params["acodec"] or "EC3"
|
||||
self.range = ctx.parent.params["range_"]
|
||||
|
||||
|
||||
self.profile = ctx.obj.profile
|
||||
|
||||
self.device_id = None
|
||||
self.hotstar_auth = None
|
||||
self.token = None
|
||||
self.license_api = None
|
||||
|
||||
self.configure()
|
||||
|
||||
def get_titles(self):
|
||||
headers = {
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-GB,en;q=0.5",
|
||||
"hotstarauth": self.hotstar_auth,
|
||||
"X-HS-UserToken": self.token,
|
||||
"X-HS-Platform": self.config["device"]["platform"]["name"],
|
||||
"X-HS-AppVersion": self.config["device"]["platform"]["version"],
|
||||
"X-Country-Code": self.region,
|
||||
"x-platform-code": "PCTV"
|
||||
}
|
||||
try:
|
||||
r = self.session.get(
|
||||
url=self.config["endpoints"]["movie_title"],
|
||||
headers=headers,
|
||||
params={"contentId": self.title}
|
||||
)
|
||||
try:
|
||||
res = r.json()["body"]["results"]["item"]
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Failed to load title manifest: {res.text}")
|
||||
except:
|
||||
r = self.session.get(
|
||||
url=self.config["endpoints"]["tv_title"],
|
||||
headers=headers,
|
||||
params={"contentId": self.title}
|
||||
)
|
||||
try:
|
||||
res = r.json()["body"]["results"]["item"]
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Failed to load title manifest: {res.text}")
|
||||
|
||||
if res["assetType"] == "MOVIE":
|
||||
return Title(
|
||||
id_=self.title,
|
||||
type_=Title.Types.MOVIE,
|
||||
name=res["title"],
|
||||
year=res["year"],
|
||||
original_lang=res["langObjs"][0]["iso3code"],
|
||||
source=self.ALIASES[0],
|
||||
service_data=res,
|
||||
)
|
||||
else:
|
||||
r = self.session.get(
|
||||
url=self.config["endpoints"]["tv_episodes"],
|
||||
headers=headers,
|
||||
params={
|
||||
"eid": res["id"],
|
||||
"etid": "2",
|
||||
"tao": "0",
|
||||
"tas": "1000"
|
||||
}
|
||||
)
|
||||
try:
|
||||
res = r.json()["body"]["results"]["assets"]["items"]
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Failed to load episodes list: {r.text}")
|
||||
return [Title(
|
||||
id_=self.title,
|
||||
type_=Title.Types.TV,
|
||||
name=x.get("showShortTitle"),
|
||||
year=x.get("year"),
|
||||
season=x.get("seasonNo"),
|
||||
episode=x.get("episodeNo"),
|
||||
episode_name=x.get("title"),
|
||||
original_lang=x["langObjs"][0]["iso3code"],
|
||||
source=self.ALIASES[0],
|
||||
service_data=x
|
||||
) for x in res]
|
||||
|
||||
def get_tracks(self, title):
|
||||
if title.service_data.get("parentalRating", 0) > 2:
|
||||
body = json.dumps({
|
||||
"devices": [{
|
||||
"id": self.device_id,
|
||||
"name": "Chrome Browser on Windows",
|
||||
"consentProvided": True
|
||||
}]
|
||||
})
|
||||
|
||||
self.session.post(
|
||||
url="https://api.hotstar.com/play/v1/consent/content/{id}?".format(id=title.service_data["contentId"]),
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"Content-Type": "application/json",
|
||||
"hotstarauth": self.hotstar_auth,
|
||||
"X-HS-UserToken": self.token,
|
||||
"X-HS-Platform": self.config["device"]["platform"]["name"],
|
||||
"X-HS-AppVersion": self.config["device"]["platform"]["version"],
|
||||
"X-HS-Request-Id": str(uuid.uuid4()),
|
||||
"X-Country-Code": self.region
|
||||
},
|
||||
data=body
|
||||
).json()
|
||||
akamai_cdn=True
|
||||
count = 1
|
||||
while akamai_cdn:
|
||||
r = self.session.post(
|
||||
url=self.config["endpoints"]["manifest"].format(id=title.service_data["contentId"]),
|
||||
params={
|
||||
# TODO: Perhaps set up desired-config to actual desired playback set values?
|
||||
"desired-config": "|".join([
|
||||
"audio_channel:stereo",
|
||||
"container:fmp4",
|
||||
"dynamic_range:sdr",
|
||||
"encryption:widevine",
|
||||
"ladder:tv",
|
||||
"package:dash",
|
||||
"resolution:fhd",
|
||||
"video_codec:h264"
|
||||
]),
|
||||
"device-id": self.device_id,
|
||||
"type": "paid",
|
||||
},
|
||||
headers={
|
||||
"Accept": "*/*",
|
||||
"hotstarauth": self.hotstar_auth,
|
||||
"x-hs-usertoken": self.token,
|
||||
"x-hs-request-id": self.device_id,
|
||||
"x-country-code": self.region
|
||||
},
|
||||
json={
|
||||
"os_name": "Windows",
|
||||
"os_version": "10",
|
||||
"app_name": "web",
|
||||
"app_version": "7.34.1",
|
||||
"platform": "Chrome",
|
||||
"platform_version": "99.0.4844.82",
|
||||
"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_V2_2", "HDCP_V2_1", "HDCP_V2", "HDCP_V1"]
|
||||
},
|
||||
"resolution": "auto",
|
||||
"type": "paid",
|
||||
}
|
||||
)
|
||||
try:
|
||||
playback_sets = r.json()["data"]["playback_sets"]
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Manifest fetch failed: {r.text}")
|
||||
|
||||
# transform tagsCombination into `tags` key-value dictionary for easier usage
|
||||
playback_sets = [dict(
|
||||
**x,
|
||||
tags=dict(y.split(":") for y in x["tags_combination"].lower().split(";"))
|
||||
) for x in playback_sets]
|
||||
|
||||
playback_set = next((
|
||||
x for x in playback_sets
|
||||
if x["tags"].get("encryption") == "widevine" or x["tags"].get("encryption") == "plain" # widevine, fairplay, playready
|
||||
if x["tags"].get("package") == "dash" # dash, hls
|
||||
if x["tags"].get("container") == "fmp4br" # fmp4, fmp4br, ts
|
||||
if x["tags"].get("ladder") == "tv" # tv, phone
|
||||
if x["tags"].get("video_codec").endswith(self.vcodec.lower()) # dvh265, h265, h264 - vp9?
|
||||
# user defined, may not be available in the tags list:
|
||||
if x["tags"].get("resolution") in [self.quality, None] # max is fine, -q can choose lower if wanted
|
||||
if x["tags"].get("dynamic_range") in [self.range.lower(), None] # dv, hdr10, sdr - hdr10+?
|
||||
if x["tags"].get("audio_codec") in [self.acodec.lower(), None] # ec3, aac - atmos?
|
||||
if x["tags"].get("audio_channel") in [{"5.1": "dolby51", "2.0": "stereo", "atmos": "atmos"}[self.channels], None]
|
||||
), None)
|
||||
if not playback_set:
|
||||
playback_set = next((
|
||||
x for x in playback_sets
|
||||
if x["tags"].get("encryption") == "widevine" or x["tags"].get("encryption") == "plain" # widevine, fairplay, playready
|
||||
if x["tags"].get("package") == "dash" # dash, hls
|
||||
if x["tags"].get("ladder") == "tv" # tv, phone
|
||||
if x["tags"].get("resolution") in [self.quality, None]
|
||||
), None)
|
||||
if not playback_set:
|
||||
raise ValueError("Wanted playback set is unavailable for this title...")
|
||||
if "licence_url" in playback_set: self.license_api = playback_set["licence_url"]
|
||||
if playback_set['token_algorithm'] == 'airtel-qwilt-vod' or playback_set['token_algorithm'] == 'AKAMAI-HMAC':
|
||||
self.log.info(f'Gotcha!')
|
||||
akamai_cdn = False
|
||||
else:
|
||||
self.log.info(f'Finding MPD... {count}')
|
||||
count += 1
|
||||
|
||||
r = Request(playback_set["playback_url"])
|
||||
r.add_header("user-agent", "Hotstar;in.startv.hotstar/3.3.0 (Android/8.1.0)")
|
||||
data = urlopen(r).read()
|
||||
|
||||
|
||||
mpd_url = playback_set["playback_url"] #.replace(".hotstar.com", ".akamaized.net")
|
||||
|
||||
self.session.headers.update({
|
||||
"Cookie": self.hdntl
|
||||
})
|
||||
|
||||
tracks = Tracks.from_mpd(
|
||||
url=mpd_url,
|
||||
data=data,
|
||||
session=self.session,
|
||||
source=self.ALIASES[0]
|
||||
)
|
||||
for track in tracks:
|
||||
track.needs_proxy = True
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title):
|
||||
return []
|
||||
|
||||
def certificate(self, **_):
|
||||
return None # will use common privacy cert
|
||||
|
||||
def license(self, challenge, **_):
|
||||
return self.session.post(
|
||||
url=self.license_api,
|
||||
data=challenge # expects bytes
|
||||
).content
|
||||
|
||||
# Service specific functions
|
||||
|
||||
def configure(self):
|
||||
self.session.headers.update({
|
||||
"Origin": "https://www.hotstar.com",
|
||||
"Referer": f'"https://www.hotstar.com/{self.region}"'
|
||||
})
|
||||
self.log.info("Logging into Hotstar")
|
||||
self.log.info(f'Setting region to "{self.region}"')
|
||||
self.hotstar_auth = self.get_akamai()
|
||||
self.log.info(f" + Calculated HotstarAuth: {self.hotstar_auth}")
|
||||
try:
|
||||
if self.cookies:
|
||||
hdntl_cookies = [cookie for cookie in self.session.cookies if cookie.name == 'hdntl']
|
||||
self.hdntl = f"hdntl={hdntl_cookies[-1].value}"
|
||||
self.device_id = self.session.cookies.get("deviceId")
|
||||
self.log.info(f" + Using Device ID: {self.device_id}")
|
||||
except:
|
||||
self.device_id = str(uuid.uuid4())
|
||||
self.log.info(f" + Created Device ID: {self.device_id}")
|
||||
self.token = self.get_token()
|
||||
self.log.info(" + Obtained tokens")
|
||||
|
||||
@staticmethod
|
||||
def get_akamai():
|
||||
enc_key = b"\x05\xfc\x1a\x01\xca\xc9\x4b\xc4\x12\xfc\x53\x12\x07\x75\xf9\xee"
|
||||
st = int(time.time())
|
||||
exp = st + 6000
|
||||
res = f"st={st}~exp={exp}~acl=/*"
|
||||
res += "~hmac=" + hmac.new(enc_key, res.encode(), hashlib.sha256).hexdigest()
|
||||
return res
|
||||
|
||||
def get_token(self):
|
||||
token_cache_path = self.get_cache("token_{profile}.json".format(profile=self.profile))
|
||||
if os.path.isfile(token_cache_path):
|
||||
with open(token_cache_path, encoding="utf-8") as fd:
|
||||
token = json.load(fd)
|
||||
if token.get("exp", 0) > int(time.time()):
|
||||
# not expired, lets use
|
||||
self.log.info(" + Using cached auth tokens...")
|
||||
return token["uid"]
|
||||
else:
|
||||
# expired, refresh
|
||||
self.log.info(" + Refreshing and using cached auth tokens...")
|
||||
return self.save_token(self.refresh(token["uid"], token["sub"]["deviceId"]), token_cache_path)
|
||||
# get new token
|
||||
if self.cookies:
|
||||
token = self.session.cookies.get("sessionUserUP", None, 'www.hotstar.com', '/' + self.region)
|
||||
else:
|
||||
raise self.log.exit(f" - Please add cookies")
|
||||
# token = self.login()
|
||||
return self.save_token(token, token_cache_path)
|
||||
|
||||
@staticmethod
|
||||
def save_token(token, to):
|
||||
# Decode the JWT data component
|
||||
data = json.loads(base64.b64decode(token.split(".")[1] + "===").decode("utf-8"))
|
||||
data["uid"] = token
|
||||
data["sub"] = json.loads(data["sub"])
|
||||
|
||||
os.makedirs(os.path.dirname(to), exist_ok=True)
|
||||
with open(to, "w", encoding="utf-8") as fd:
|
||||
json.dump(data, fd)
|
||||
|
||||
return token
|
||||
|
||||
def refresh(self, user_id_token, device_id):
|
||||
json_data = {
|
||||
'deeplink_url': f'/{self.region}?client_capabilities=%7B%22ads%22%3A%5B%22non_ssai%22%5D%2C%22audio_channel%22%3A%5B%22stereo%22%5D%2C%22container%22%3A%5B%22fmp4%22%2C%22ts%22%5D%2C%22dvr%22%3A%5B%22short%22%5D%2C%22dynamic_range%22%3A%5B%22sdr%22%5D%2C%22encryption%22%3A%5B%22widevine%22%2C%22plain%22%5D%2C%22ladder%22%3A%5B%22web%22%2C%22tv%22%2C%22phone%22%5D%2C%22package%22%3A%5B%22dash%22%2C%22hls%22%5D%2C%22resolution%22%3A%5B%22sd%22%2C%22hd%22%5D%2C%22video_codec%22%3A%5B%22h264%22%5D%2C%22true_resolution%22%3A%5B%22sd%22%2C%22hd%22%2C%22fhd%22%5D%7D&drm_parameters=%7B%22hdcp_version%22%3A%5B%22HDCP_V2_2%22%5D%2C%22widevine_security_level%22%3A%5B%22SW_SECURE_DECODE%22%5D%2C%22playready_security_level%22%3A%5B%5D%7D',
|
||||
'app_launch_count': 1,
|
||||
}
|
||||
r = self.session.post(
|
||||
url=self.config["endpoints"]["refresh"],
|
||||
headers={
|
||||
'x-hs-usertoken': user_id_token,
|
||||
'X-HS-Platform': self.config["device"]["platform"]["name"],
|
||||
'X-Country-Code': self.region,
|
||||
'X-HS-Accept-language': 'eng',
|
||||
'X-Request-Id': str(uuid.uuid4()),
|
||||
'x-hs-device-id': device_id,
|
||||
'X-HS-Client-Targeting': f'ad_id:{device_id};user_lat:false',
|
||||
'x-hs-request-id': str(uuid.uuid4()),
|
||||
'X-HS-Client': 'platform:web;app_version:23.06.23.3;browser:Firefox;schema_version:0.0.911',
|
||||
'Origin': 'https://www.hotstar.com',
|
||||
'Referer': f'https://www.hotstar.com/{self.region}',
|
||||
},
|
||||
json=json_data
|
||||
)
|
||||
for cookie in self.cookies:
|
||||
if cookie.name == 'sessionUserUP' and cookie.path == f"/{self.region}" and cookie.domain == 'www.hotstar.com':
|
||||
cookie.value = r.headers["x-hs-usertoken"]
|
||||
for x in self.ALIASES:
|
||||
cookie_file = os.path.join(directories.cookies, x.lower(), f"{self.profile}.txt")
|
||||
if not os.path.isfile(cookie_file):
|
||||
cookie_file = os.path.join(directories.cookies, x, f"{self.profile}.txt")
|
||||
if os.path.isfile(cookie_file):
|
||||
self.cookies.save(cookie_file, ignore_discard=True, ignore_expires=True)
|
||||
break
|
||||
return r.headers["x-hs-usertoken"]
|
||||
|
||||
def login(self):
|
||||
"""
|
||||
Log in to HOTSTAR and return a JWT User Identity token.
|
||||
:returns: JWT User Identity token.
|
||||
"""
|
||||
if self.credentials.username == "username" and self.credentials.password == "password":
|
||||
logincode_url = f"https://api.hotstar.com/{self.region}/aadhar/v2/firetv/{self.region}/users/logincode/"
|
||||
logincode_headers = {
|
||||
"Content-Length": "0",
|
||||
"User-Agent": "Hotstar;in.startv.hotstar/3.3.0 (Android/8.1.0)"
|
||||
}
|
||||
logincode = self.session.post(
|
||||
url = logincode_url,
|
||||
headers = logincode_headers
|
||||
).json()["description"]["code"]
|
||||
print(f"Go to tv.hotstar.com and put {logincode}")
|
||||
logincode_choice = input('Did you put as informed above? (y/n): ')
|
||||
if logincode_choice.lower() == 'y':
|
||||
res = self.session.get(
|
||||
url = logincode_url+logincode,
|
||||
headers = logincode_headers
|
||||
)
|
||||
else:
|
||||
self.log.exit(" - Exited.")
|
||||
raise
|
||||
else:
|
||||
res = self.session.post(
|
||||
url=self.config["endpoints"]["login"],
|
||||
json={
|
||||
"isProfileRequired": "false",
|
||||
"userData": {
|
||||
"deviceId": self.device_id,
|
||||
"password": self.credentials.password,
|
||||
"username": self.credentials.username,
|
||||
"usertype": "email"
|
||||
},
|
||||
"verification": {}
|
||||
},
|
||||
headers={
|
||||
"hotstarauth": self.hotstar_auth,
|
||||
"content-type": "application/json"
|
||||
}
|
||||
)
|
||||
try:
|
||||
data = res.json()
|
||||
except json.JSONDecodeError:
|
||||
self.log.exit(f" - Failed to get auth token, response was not JSON: {res.text}")
|
||||
raise
|
||||
if "errorCode" in data:
|
||||
self.log.exit(f" - Login failed: {data['description']} [{data['errorCode']}]")
|
||||
raise
|
||||
return data["description"]["userIdentity"]
|
343
vinetrimmer/services/jio.py
Normal file
343
vinetrimmer/services/jio.py
Normal file
@ -0,0 +1,343 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re as rex
|
||||
import os
|
||||
import requests
|
||||
from langcodes import Language
|
||||
#from typing import Any, Optional, Union
|
||||
import datetime
|
||||
import click
|
||||
|
||||
from vinetrimmer.objects import MenuTrack, TextTrack, Title, Tracks, Track
|
||||
from vinetrimmer.services.BaseService import BaseService
|
||||
|
||||
|
||||
class Jio(BaseService):
|
||||
"""
|
||||
Service code for Viacom18's JioCinema streaming service (https://www.jiocinema.com/).
|
||||
|
||||
\b
|
||||
Authorization: Token
|
||||
Security: UHD@L3 FHD@L3
|
||||
|
||||
"""
|
||||
|
||||
PHONE_NUMBER = "" # Add number with country code
|
||||
|
||||
ALIASES = ["JIO", "JioCinema"]
|
||||
#GEOFENCE = ["in2"]
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="Jio", short_help="https://www.jiocinema.com")
|
||||
@click.argument("title", type=str)
|
||||
@click.option("-m", "--movie", is_flag=True, default=False, help="Title is a Movie.")
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return Jio(ctx, **kwargs)
|
||||
|
||||
def __init__(self, ctx, title: str, movie: bool):
|
||||
self.title = title
|
||||
self.movie = movie
|
||||
super().__init__(ctx)
|
||||
|
||||
assert ctx.parent is not None
|
||||
|
||||
self.vcodec = ctx.parent.params["vcodec"]
|
||||
self.acodec = ctx.parent.params["acodec"]
|
||||
self.range = ctx.parent.params["range_"]
|
||||
|
||||
self.profile = ctx.obj.profile
|
||||
|
||||
self.token: str
|
||||
self.refresh_token: str
|
||||
self.license_api = None
|
||||
|
||||
self.configure()
|
||||
|
||||
def get_titles(self):
|
||||
titles = []
|
||||
if self.movie:
|
||||
res = self.session.get(
|
||||
url='https://content-jiovoot.voot.com/psapi/voot/v1/voot-tv/content/query/asset-details?ids=include%3A{id}&devicePlatformType=androidtv&responseType=common&page=1'.format(id=self.title)
|
||||
)
|
||||
try:
|
||||
data = res.json()['result'][0]
|
||||
self.log.debug(json.dumps(data, indent=4))
|
||||
except json.JSONDecodeError:
|
||||
raise ValueError(f"Failed to load title manifest: {res.text}")
|
||||
|
||||
titles.append(Title(
|
||||
id_=data['id'],
|
||||
type_=Title.Types.MOVIE,
|
||||
name=rex.sub(r'\([^)]*\)', '', data["fullTitle"]).strip(),
|
||||
year=data.get("releaseYear"),
|
||||
original_lang=Language.find(data['languages'][0]),
|
||||
source=self.ALIASES[0],
|
||||
service_data=data
|
||||
))
|
||||
|
||||
else:
|
||||
def get_recursive_episodes(season_id):
|
||||
total_attempts = 1
|
||||
recursive_episodes = []
|
||||
season_params = {
|
||||
'sort': 'episode:asc',
|
||||
'responseType': 'common',
|
||||
'id': season_id,
|
||||
'page': 1
|
||||
}
|
||||
while True:
|
||||
episode = self.session.get(url='https://content-jiovoot.voot.com/psapi/voot/v1/voot-web/content/generic/series-wise-episode', params=season_params).json()
|
||||
if any(episode["result"]):
|
||||
total_attempts += 1
|
||||
recursive_episodes.extend(episode["result"])
|
||||
season_params.update({'page': total_attempts})
|
||||
else:
|
||||
break
|
||||
return recursive_episodes
|
||||
# params = {
|
||||
# 'sort': 'season:asc',
|
||||
# 'id': self.title,
|
||||
# 'responseType': 'common'
|
||||
# }
|
||||
re = self.session.get(url='https://content-jiovoot.voot.com/psapi/voot/v1/voot-tv/view/show/{id}?devicePlatformType=androidtv&responseType=common&page=1'.format(id=self.title)).json()['trays'][1]
|
||||
self.log.debug(json.dumps(re, indent=4))
|
||||
for season in re['trayTabs']:
|
||||
season_id = season["id"]
|
||||
recursive_episodes = get_recursive_episodes(season_id)
|
||||
self.log.debug(json.dumps(recursive_episodes, indent=4))
|
||||
for episodes in recursive_episodes:
|
||||
titles.append(Title(
|
||||
id_=episodes["id"],
|
||||
type_=Title.Types.TV,
|
||||
name=rex.sub(r'\([^)]*\)', '', episodes["showName"]).strip(),
|
||||
season=int(float(episodes["season"])),
|
||||
episode=int(float(episodes["episode"])),
|
||||
episode_name=episodes["fullTitle"],
|
||||
original_lang=Language.find(episodes['languages'][0]),
|
||||
source=self.ALIASES[0],
|
||||
service_data=episodes
|
||||
))
|
||||
|
||||
return titles
|
||||
|
||||
def get_tracks(self, title: Title) -> Tracks:
|
||||
#self.log.debug(json.dumps(title.service_data, indent=4))
|
||||
json_data = {
|
||||
'4k': True,
|
||||
'ageGroup': '18+',
|
||||
'appVersion': '4.0.9',
|
||||
'bitrateProfile': 'xxxhdpi',
|
||||
'capability': {
|
||||
'drmCapability': {
|
||||
'aesSupport': 'yes',
|
||||
'fairPlayDrmSupport': 'yes',
|
||||
'playreadyDrmSupport': 'yes',
|
||||
'widevineDRMSupport': 'L1',
|
||||
},
|
||||
'frameRateCapability': [
|
||||
{
|
||||
'frameRateSupport': '60fps',
|
||||
'videoQuality': '2160p',
|
||||
},
|
||||
],
|
||||
},
|
||||
'continueWatchingRequired': False,
|
||||
'dolby': True,
|
||||
'downloadRequest': False,
|
||||
'hevc': False,
|
||||
'kidsSafe': False,
|
||||
'manufacturer': 'NVIDIA',
|
||||
'model': 'SHIELDTV',
|
||||
'multiAudioRequired': True,
|
||||
'osVersion': '12',
|
||||
'parentalPinValid': True,
|
||||
'x-apisignatures': 'o668nxgzwff',
|
||||
}
|
||||
|
||||
try:
|
||||
res = self.session.post(
|
||||
url = f'https://apis-jiovoot.voot.com/playbackjv/v3/{title.id}',
|
||||
json=json_data,
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
self.refresh()
|
||||
try:
|
||||
res = self.session.post(
|
||||
url = f'https://apis-jiovoot.voot.com/playbackjv/v3/{title.id}',
|
||||
json=json_data
|
||||
)
|
||||
except requests.exceptions.RequestException:
|
||||
self.log.exit("Unable to retrive manifest")
|
||||
|
||||
res = res.json()
|
||||
self.log.debug(json.dumps(res, indent=4))
|
||||
self.license_api = res['data']['playbackUrls'][0].get('licenseurl')
|
||||
vid_url = res['data']['playbackUrls'][0].get('url')
|
||||
|
||||
if "mpd" in vid_url:
|
||||
tracks = Tracks.from_mpd(
|
||||
url=vid_url,
|
||||
session=self.session,
|
||||
#lang=title.original_lang,
|
||||
source=self.ALIASES[0]
|
||||
)
|
||||
else:
|
||||
self.log.exit('No mpd found')
|
||||
|
||||
self.log.info(f"Getting audio from Various manifests for potential higher bitrate or better codec")
|
||||
for device in ['androidtablet']: #'androidmobile', 'androidweb' ==> what more devices ?
|
||||
self.session.headers.update({'x-platform': device})
|
||||
audio_mpd_url = self.session.post(url=f'https://apis-jiovoot.voot.com/playbackjv/v3/{title.id}', json=json_data)
|
||||
if audio_mpd_url.status_code != 200:
|
||||
self.log.warning("Unable to retrive manifest")
|
||||
else:
|
||||
audio_mpd_url = audio_mpd_url.json()['data']['playbackUrls'][0].get('url')
|
||||
if "mpd" in audio_mpd_url:
|
||||
audio_mpd = Tracks([
|
||||
x for x in iter(Tracks.from_mpd(
|
||||
url=audio_mpd_url,
|
||||
session=self.session,
|
||||
source=self.ALIASES[0],
|
||||
#lang=title.original_lang,
|
||||
))
|
||||
])
|
||||
tracks.add(audio_mpd.audios)
|
||||
else:
|
||||
self.log.warning('No mpd found')
|
||||
|
||||
for track in tracks:
|
||||
#track.language = Language.get('ta')
|
||||
track.needs_proxy = True
|
||||
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title: Title) -> list[MenuTrack]:
|
||||
return []
|
||||
|
||||
def certificate(self, **kwargs) -> None:
|
||||
return self.license(**kwargs)
|
||||
|
||||
def license(self, challenge: bytes, **_) -> bytes:
|
||||
assert self.license_api is not None
|
||||
self.session.headers.update({
|
||||
'x-playbackid': '5ec82c75-6fda-4b47-b2a5-84b8d9079675',
|
||||
'x-feature-code': 'ytvjywxwkn',
|
||||
'origin': 'https://www.jiocinema.com',
|
||||
'referer': 'https://www.jiocinema.com/',
|
||||
})
|
||||
return self.session.post(
|
||||
url=self.license_api,
|
||||
data=challenge, # expects bytes
|
||||
).content
|
||||
|
||||
def refresh(self) -> None:
|
||||
self.log.info(" + Refreshing auth tokens...")
|
||||
res = self.session.post(
|
||||
url="https://auth-jiocinema.voot.com/tokenservice/apis/v4/refreshtoken",
|
||||
json={
|
||||
'appName': 'RJIL_JioCinema',
|
||||
'deviceId': '332536276',
|
||||
'refreshToken': self.refresh_token,
|
||||
'appVersion': '5.6.0'
|
||||
}
|
||||
)
|
||||
if res.status_code != 200:
|
||||
return self.log.warning('Tokens cannot be Refreshed. Something went wrong..')
|
||||
|
||||
self.token = res.json()["authToken"]
|
||||
self.refresh_token = res.json()["refreshTokenId"]
|
||||
self.session.headers.update({'accesstoken': self.token})
|
||||
token_cache_path = self.get_cache("token_{profile}.json".format(profile=self.profile))
|
||||
old_data = json.load(open(token_cache_path, "r", encoding="utf-8"))
|
||||
old_data.update({
|
||||
'authToken': self.token,
|
||||
'refreshToken': self.refresh_token
|
||||
})
|
||||
json.dump(old_data, open(token_cache_path, "w", encoding="utf-8"), indent=4)
|
||||
|
||||
def login(self):
|
||||
self.log.info(' + Logging into JioCinema')
|
||||
if not self.PHONE_NUMBER:
|
||||
self.log.exit('Please provide Jiocinema registered Phone number....')
|
||||
guest = self.session.post(
|
||||
url="https://auth-jiocinema.voot.com/tokenservice/apis/v4/guest",
|
||||
json={
|
||||
'appName': 'RJIL_JioCinema',
|
||||
'deviceType': 'phone',
|
||||
'os': 'ios',
|
||||
'deviceId': '332536276',
|
||||
'freshLaunch': False,
|
||||
'adId': '332536276',
|
||||
'appVersion': '5.6.0',
|
||||
}
|
||||
)
|
||||
headers = {
|
||||
'accesstoken': guest.json()["authToken"],
|
||||
'appname': 'RJIL_JioCinema',
|
||||
'devicetype': 'phone',
|
||||
'os': 'ios'
|
||||
}
|
||||
send = self.session.post(
|
||||
url="https://auth-jiocinema.voot.com/userservice/apis/v4/loginotp/send",
|
||||
headers=headers,
|
||||
json={
|
||||
'number': '{}'.format(base64.b64encode(self.PHONE_NUMBER.encode("utf-8")).decode("utf-8")),
|
||||
'appVersion': '5.6.0'
|
||||
}
|
||||
)
|
||||
if send.status_code != 200:
|
||||
self.log.exit("OTP Send Failed!")
|
||||
else:
|
||||
self.log.info("OTP has been sent. Please write it down below and press Enter")
|
||||
otp = input()
|
||||
verify_data = {
|
||||
'deviceInfo': {
|
||||
'consumptionDeviceName': 'iPhone',
|
||||
'info': {
|
||||
'platform': {
|
||||
'name': 'iPhone OS',
|
||||
},
|
||||
'androidId': '332536276',
|
||||
'type': 'iOS',
|
||||
},
|
||||
},
|
||||
'appVersion': '5.6.0',
|
||||
'number': '{}'.format(base64.b64encode(self.PHONE_NUMBER.encode("utf-8")).decode("utf-8")),
|
||||
'otp': '{}'.format(otp)
|
||||
}
|
||||
verify = self.session.post(
|
||||
url="https://auth-jiocinema.voot.com/userservice/apis/v4/loginotp/verify",
|
||||
headers=headers,
|
||||
json=verify_data
|
||||
)
|
||||
if verify.status_code != 200:
|
||||
self.log.exit("Cannot be verified")
|
||||
self.log.info(" + Verified!")
|
||||
|
||||
return verify.json()
|
||||
|
||||
def configure(self) -> None:
|
||||
token_cache_path = self.get_cache("token_{profile}.json".format(profile=self.profile))
|
||||
if os.path.isfile(token_cache_path):
|
||||
tokens = json.load(open(token_cache_path, "r", encoding="utf-8"))
|
||||
self.log.info(" + Using cached auth tokens...")
|
||||
else:
|
||||
tokens = self.login()
|
||||
os.makedirs(os.path.dirname(token_cache_path), exist_ok=True)
|
||||
with open(token_cache_path, "w", encoding="utf-8") as file:
|
||||
json.dump(tokens, file, indent=4)
|
||||
self.token = tokens["authToken"]
|
||||
self.refresh_token = tokens["refreshToken"]
|
||||
self.session.headers.update({
|
||||
'deviceid': '332536276',
|
||||
'accesstoken': self.token,
|
||||
'appname': 'RJIL_JioCinema',
|
||||
'uniqueid': 'be277ebe-e50b-441e-bc37-bd803286f3d5',
|
||||
'user-agent': 'Dalvik/2.1.0 (Linux; U; Android 9; SHIELD Android TV Build/PPR1.180610.011)',
|
||||
'x-apisignatures': 'o668nxgzwff',
|
||||
'x-platform': 'androidtv', # base device
|
||||
'x-platform-token': 'android',
|
||||
})
|
248
vinetrimmer/services/moviesanywhere.py
Normal file
248
vinetrimmer/services/moviesanywhere.py
Normal file
@ -0,0 +1,248 @@
|
||||
import base64
|
||||
import json
|
||||
import click
|
||||
import re
|
||||
from requests import JSONDecodeError
|
||||
from httpx import URL
|
||||
import uuid
|
||||
import xmltodict
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from langcodes import Language
|
||||
from vinetrimmer.objects import AudioTrack, TextTrack, Title, Tracks, VideoTrack
|
||||
from vinetrimmer.services.BaseService import BaseService
|
||||
from vinetrimmer.vendor.pymp4.parser import Box
|
||||
|
||||
|
||||
class MoviesAnywhere(BaseService):
|
||||
"""
|
||||
Service code for US' streaming service MoviesAnywhere (https://moviesanywhere.com).
|
||||
|
||||
\b
|
||||
Authorization: Cookies
|
||||
Security: SD-HD@L3, FHD SDR@L1 (any active device), FHD-UHD HDR-DV@L1 (whitelisted devices).
|
||||
|
||||
NOTE: Can be accessed from any region, it does not seem to care.
|
||||
Accounts can only mount services when its US based though.
|
||||
|
||||
"""
|
||||
ALIASES = ["MA", "moviesanywhere"]
|
||||
|
||||
TITLE_RE = r"https://moviesanywhere\.com(?P<id>.+)"
|
||||
|
||||
VIDEO_CODEC_MAP = {
|
||||
"H264": ["avc"],
|
||||
"H265": ["hvc", "hev", "dvh"]
|
||||
}
|
||||
AUDIO_CODEC_MAP = {
|
||||
"AAC": ["mp4a", "HE", "stereo"],
|
||||
"AC3": ["ac3"],
|
||||
"EC3": ["ec3", "atmos"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
@click.command(name="MoviesAnywhere", short_help="moviesanywhere.com")
|
||||
@click.argument("title", type=str)
|
||||
|
||||
@click.pass_context
|
||||
def cli(ctx, **kwargs):
|
||||
return MoviesAnywhere(ctx, **kwargs)
|
||||
|
||||
def __init__(self, ctx, title):
|
||||
super().__init__(ctx)
|
||||
self.parse_title(ctx, title)
|
||||
self.configure()
|
||||
|
||||
self.atmos = ctx.parent.params["atmos"]
|
||||
self.vcodec = ctx.parent.params["vcodec"]
|
||||
self.acodec = ctx.parent.params["acodec"]
|
||||
|
||||
def get_titles(self):
|
||||
self.headers={
|
||||
"authorization": f"Bearer {self.access_token}",
|
||||
"install-id": self.install_id,
|
||||
}
|
||||
res = self.session.post(
|
||||
url="https://gateway.moviesanywhere.com/graphql",
|
||||
json={
|
||||
"platform": "web",
|
||||
"variables": {"slug": self.title}, # Does not seem to care which platform will be used to give the best tracks available
|
||||
"extensions": '{"persistedQuery":{"sha256Hash":"5cb001491262214406acf8237ea2b8b46ca6dbcf37e70e791761402f4f74336e","version":1}}', # ONE_GRAPH_PERSIST_QUERY_TOKEN
|
||||
},
|
||||
headers={
|
||||
"authorization": f"Bearer {self.access_token}",
|
||||
"install-id": self.install_id,
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
self.content = res.json()
|
||||
except JSONDecodeError:
|
||||
self.log.exit(" - Not able to return title information")
|
||||
|
||||
title_data = self.content["data"]["page"]
|
||||
|
||||
title_info = [
|
||||
x
|
||||
for x in title_data["components"]
|
||||
if x["__typename"] == "MovieMarqueeComponent"
|
||||
][0]
|
||||
|
||||
title_info["title"] = re.sub(r" \(.+?\)", "", title_info["title"])
|
||||
|
||||
title_data = self.content["data"]["page"]
|
||||
try:
|
||||
Id = title_data["components"][0]["mainAction"]["playerData"]["playable"]["id"]
|
||||
except KeyError:
|
||||
self.log.exit(" - Account does not seem to own this title")
|
||||
|
||||
return Title(
|
||||
id_=Id,
|
||||
type_=Title.Types.MOVIE,
|
||||
name=title_info["title"],
|
||||
year=title_info["year"],
|
||||
original_lang="en",
|
||||
source=self.ALIASES[0],
|
||||
service_data=title_data,
|
||||
)
|
||||
|
||||
def get_pssh_init(self, url):
|
||||
import os, yt_dlp
|
||||
from pathlib import Path
|
||||
init = 'init.mp4'
|
||||
|
||||
files_to_delete = [init]
|
||||
for file_name in files_to_delete:
|
||||
if os.path.exists(file_name):
|
||||
os.remove(file_name)
|
||||
|
||||
def read_pssh(path: str):
|
||||
raw = Path(path).read_bytes()
|
||||
wv = raw.rfind(bytes.fromhex('edef8ba979d64acea3c827dcd51d21ed'))
|
||||
if wv == -1: return None
|
||||
return base64.b64encode(raw[wv-12:wv-12+raw[wv-9]]).decode('utf-8')
|
||||
|
||||
ydl_opts = {
|
||||
'format': 'bestvideo[ext=mp4]/bestaudio[ext=m4a]/best',
|
||||
'allow_unplayable_formats': True,
|
||||
'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3',
|
||||
'no_warnings': True,
|
||||
'quiet': True,
|
||||
'outtmpl': init,
|
||||
'no_merge': True,
|
||||
'test': True,
|
||||
}
|
||||
|
||||
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||
info_dict = ydl.extract_info(url, download=True)
|
||||
url = info_dict.get("url", None)
|
||||
if url is None:
|
||||
raise ValueError("Failed to download the video")
|
||||
video_file_name = ydl.prepare_filename(info_dict)
|
||||
|
||||
pssh = read_pssh(init)
|
||||
|
||||
for file_name in files_to_delete:
|
||||
if os.path.exists(file_name):
|
||||
os.remove(file_name)
|
||||
return pssh
|
||||
|
||||
def get_tracks(self, title):
|
||||
player_data = self.content["data"]["page"]["components"][0]["mainAction"]["playerData"]["playable"]
|
||||
|
||||
videos = []
|
||||
audios = []
|
||||
for cr in player_data["videoAssets"]["dash"].values():
|
||||
if not cr:
|
||||
continue
|
||||
for manifest in cr:
|
||||
tracks = Tracks.from_mpd(
|
||||
url=manifest["url"],
|
||||
source=self.ALIASES[0],
|
||||
session=self.session,
|
||||
)
|
||||
|
||||
for video in tracks.videos:
|
||||
pssh = self.get_pssh_init(manifest["url"])
|
||||
video_pssh = Box.parse(base64.b64decode(pssh))
|
||||
video.pssh = video_pssh
|
||||
video.license_url = manifest["widevineLaUrl"]
|
||||
video.contentId = URL(video.license_url).params._dict["ContentId"][
|
||||
0
|
||||
]
|
||||
videos += [video]
|
||||
# Extract Atmos audio track if available.
|
||||
for audio in tracks.audios:
|
||||
audio.pssh = video_pssh
|
||||
audio.license_url = manifest["widevineLaUrl"]
|
||||
audio.contentId = URL(audio.license_url).params._dict["ContentId"][
|
||||
0
|
||||
]
|
||||
if "atmos" in audio.url:
|
||||
audio.atmos = True
|
||||
audios += [audio]
|
||||
|
||||
corrected_video_list = []
|
||||
for res in ("uhd", "hdp", "hd", "sd"):
|
||||
for video in videos:
|
||||
if f"_{res}_video" not in video.url or not video.url.endswith(
|
||||
f"&r={res}"
|
||||
):
|
||||
continue
|
||||
|
||||
if corrected_video_list and any(
|
||||
video.id == vid.id for vid in corrected_video_list
|
||||
):
|
||||
continue
|
||||
|
||||
if "dash_hevc_hdr" in video.url:
|
||||
video.hdr10 = True
|
||||
if "dash_hevc_dolbyvision" in video.url:
|
||||
video.dv = True
|
||||
|
||||
corrected_video_list += [video]
|
||||
|
||||
tracks.add(corrected_video_list)
|
||||
tracks.audios = audios
|
||||
tracks.videos = [x for x in tracks.videos if (x.codec or "")[:3] in self.VIDEO_CODEC_MAP[self.vcodec]]
|
||||
|
||||
return tracks
|
||||
|
||||
def get_chapters(self, title):
|
||||
return []
|
||||
|
||||
def certificate(self, **_):
|
||||
return None # will use common privacy cert
|
||||
|
||||
def license(self, challenge: bytes, track: Tracks, **_) -> bytes:
|
||||
license_message = self.session.post(
|
||||
url=track.license_url,
|
||||
data=challenge, # expects bytes
|
||||
)
|
||||
|
||||
if "errorCode" in license_message.text:
|
||||
self.log.exit(f" - Cannot complete license request: {license_message.text}")
|
||||
|
||||
return license_message.content
|
||||
|
||||
|
||||
def configure(self):
|
||||
access_token = None
|
||||
install_id = None
|
||||
for cookie in self.cookies:
|
||||
if cookie.name == "secure_access_token":
|
||||
access_token = cookie.value
|
||||
elif cookie.name == "install_id":
|
||||
install_id = cookie.value
|
||||
|
||||
self.access_token = access_token
|
||||
self.install_id = install_id
|
||||
|
||||
self.session.headers.update(
|
||||
{
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||
"Origin": "https://moviesanywhere.com",
|
||||
"Authorization": f"Bearer {self.access_token}",
|
||||
}
|
||||
)
|
544
vinetrimmer/services/sonyliv.py
Normal file
544
vinetrimmer/services/sonyliv.py
Normal file
@ -0,0 +1,544 @@
|
||||
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.")
|
@ -9,6 +9,7 @@ aria2c:
|
||||
cdm:
|
||||
default: 'hisense_smarttv_he55a7000euwts_sl3000'
|
||||
Amazon: 'hisense_smarttv_he55a7000euwts_sl3000'
|
||||
Hotstar: 'xiaomi_mi_a1_15.0.0_60ceee88_8159_l3'
|
||||
|
||||
cdm_api:
|
||||
- name: 'playready'
|
||||
|
@ -1,78 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
import click
|
||||
import coloredlogs
|
||||
|
||||
from vinetrimmer.config import directories, filenames # isort: split
|
||||
from vinetrimmer.commands import dl
|
||||
|
||||
|
||||
@click.command(context_settings=dict(
|
||||
allow_extra_args=True,
|
||||
ignore_unknown_options=True,
|
||||
max_content_width=116, # max PEP8 line-width, -4 to adjust for initial indent
|
||||
))
|
||||
@click.option("--debug", is_flag=True, default=False,
|
||||
help="Enable DEBUG level logs on the console. This is always enabled for log files.")
|
||||
def main(debug):
|
||||
"""
|
||||
vinetrimmer is the most convenient command-line program to
|
||||
download videos from Widevine DRM-protected video platforms.
|
||||
"""
|
||||
LOG_FORMAT = "{asctime} [{levelname[0]}] {name} : {message}"
|
||||
LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
LOG_STYLE = "{"
|
||||
|
||||
def log_exit(self, msg, *args, **kwargs):
|
||||
self.critical(msg, *args, **kwargs)
|
||||
sys.exit(1)
|
||||
|
||||
logging.Logger.exit = log_exit
|
||||
|
||||
os.makedirs(directories.logs, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format=LOG_FORMAT,
|
||||
datefmt=LOG_DATE_FORMAT,
|
||||
style=LOG_STYLE,
|
||||
handlers=[logging.FileHandler(
|
||||
os.path.join(directories.logs, filenames.log.format(time=datetime.now().strftime("%Y%m%d-%H%M%S"))),
|
||||
encoding='utf-8'
|
||||
)]
|
||||
)
|
||||
|
||||
coloredlogs.install(
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
fmt=LOG_FORMAT,
|
||||
datefmt=LOG_DATE_FORMAT,
|
||||
style=LOG_STYLE,
|
||||
handlers=[logging.StreamHandler()],
|
||||
)
|
||||
|
||||
log = logging.getLogger("vt")
|
||||
|
||||
log.info("vinetrimmer - Widevine DRM downloader and decrypter")
|
||||
log.info(f"[Root Config] : {filenames.user_root_config}")
|
||||
log.info(f"[Service Configs] : {directories.service_configs}")
|
||||
log.info(f"[Cookies] : {directories.cookies}")
|
||||
log.info(f"[CDM Devices] : {directories.devices}")
|
||||
log.info(f"[Cache] : {directories.cache}")
|
||||
log.info(f"[Logs] : {directories.logs}")
|
||||
log.info(f"[Temp Files] : {directories.temp}")
|
||||
log.info(f"[Downloads] : {directories.downloads}")
|
||||
|
||||
os.environ['PATH'] = os.path.abspath('./binaries')
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1].lower() == "dl":
|
||||
sys.argv.pop(1)
|
||||
|
||||
dl()
|
||||
|
||||
# D:\PlayReady-Amazon-Tool-main\.venv\Scripts\python.exe -X pycache_prefix=C:\Users\Aswin\AppData\Local\JetBrains\PyCharm2024.3\cpython-cache "C:/Program Files (x86)/JetBrains/PyCharm 2024.2.4/plugins/python-ce/helpers/pydev/pydevd.py" --port 42000 --module --multiprocess --save-signatures --qt-support=auto --file poetry run vt dl --no-cache --keys AMZN 0H7LY5ZKKBM1MIW0244WE9O2C4
|
||||
# Above seems to work
|
||||
if __name__ == "__main__":
|
||||
#sys.argv = ["vinetrimmer", "dl", "--no-cache", "--keys", "AMZN", "0H7LY5ZKKBM1MIW0244WE9O2C4"]
|
||||
main()
|
Loading…
x
Reference in New Issue
Block a user