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