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',
|
||
|
})
|