Hotstar is currently throwing errors when getting tracks, will be fixed with next commit. Fixed error when loading Widevine Device.
343 lines
13 KiB
Python
343 lines
13 KiB
Python
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',
|
|
}) |