├── .gitignore ├── LICENSE ├── README.md ├── creds.txt ├── main.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | creds.txt 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JC-dl 2 | Simple Content downloader for Indian OTT JioCinema (https://www.jiocinema.com/). 3 | 4 | *(Currently only supports movies, I haven't looked at TV shows, so if someone wants to open a PR, feel free)* 5 | 6 | ## Archived 7 | Project archived due to the death of JioCinema. 8 | 9 | ## Purpose 10 | JioCinema offers many rare and old HQ streams of Indian content which is unfortunately hidden behind DRM and cannot be easily saved for archival purposes. This tool bypasses the DRM restrictions and grabs the **Non-DRM** streams (which are ironically sometimes superior to their DRM counterparts) from JioCinema which can be downloaded directly. 11 | 12 | ### Prerequisites 13 | An account on JioCinema and the most basic skills. 14 | 15 | ### WARNING 16 | This tool shall not be abused for purposes which are not archival or educational. Use at your own risk. 17 | 18 | ## Usage 19 | * Install python and run `pip install -r requirements.txt` in your shell 20 | * Download yt-dlp (from https://github.com/yt-dlp/yt-dlp), aria2c (from https://aria2.github.io/) and the ffmpeg suite (from https://www.ffmpeg.org/download.html) 21 | (required for [FixupM3u8] task (see https://github.com/yt-dlp/yt-dlp/blob/e04b003e6469db220131812b4894ac2a1d5ee083/yt_dlp/postprocessor/ffmpeg.py#L872))) and place the binaries in the root directory 22 | * Login with your Mobile Number and OTP (on first run) as prompted 23 | * run `python main.py` 24 | * VideoID can be obtained from the Movie URL (for example: in https://www.jiocinema.com/movies/jaya-ganga?type=0&id=74f26cb06e0111ecb736133f7a349447, `74f26cb06e0111ecb736133f7a349447` is the VideoID). 25 | * Let yt-dlp and aria2c download the stream (defaults to the most superior stream) and check the `out` folder for the downloaded movie. 26 | 27 | ## The Workaround 28 | Apparently JioCinema hosts their unprotected content on their `jiobeats` CDN, which was apparently also used for JioMusic (??? See https://github.com/vikas5914/JioMusic-API#listen-song). 29 | Makes no sense for them to have this open but this workaround has been out for quite some time. 30 | 31 | ## P. S 32 | I'm really terrible at coding so consider this as the worst code. Please feel free to open PRs. 33 | 34 | ## Thanks 35 | to yt-dlp for basically solving the m3u8 parsing cause I can't write a parser. 36 | 37 | ### To-Do 38 | * Add subtitle support 39 | * ~~Simplify the config process~~ 40 | * Add logging 41 | * Add custom quality support 42 | -------------------------------------------------------------------------------- /creds.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/astravaganza/JC-dl/27f5d76911341019bba1a083d9daad8297a0b2ec/creds.txt -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import requests, json 2 | import os, sys 3 | import base64 4 | 5 | # define paths 6 | currentFile = __file__ 7 | realPath = os.path.realpath(currentFile) 8 | dirPath = os.path.dirname(realPath) 9 | dirName = os.path.basename(dirPath) 10 | ytdl_path = dirPath + "\yt-dlp.exe" 11 | 12 | # define 13 | def load_config(): 14 | global accesstoken, devid 15 | with open ("creds.txt", "r") as f: 16 | try: 17 | Creds = json.load(f) 18 | accesstoken = Creds['accesstoken'] 19 | devid = Creds['deviceid'] 20 | except json.JSONDecodeError: 21 | accesstoken = '' 22 | devid = '' 23 | 24 | Request_URL = "https://apis-jiovoot.voot.com/playbackjv/v4/" 25 | Meta_URL = "https://prod.media.jio.com/apis/common/v3/metamore/get/" 26 | OTPSendURL = "https://auth-jiocinema.voot.com/userservice/apis/v4/loginotp/send" 27 | OTPVerifyURL = "https://auth-jiocinema.voot.com/userservice/apis/v4/loginotp/verify" 28 | IdURL = "https://cs-jv.voot.com/clickstream/v1/get-id" 29 | GuestURL = "https://auth-jiocinema.voot.com/tokenservice/apis/v4/guest" 30 | 31 | def get_accesstoken(): 32 | id = requests.get(url=IdURL).json()['id'] 33 | 34 | token = requests.post(url=GuestURL, json={ 35 | 'adId': id, 36 | "appName": "RJIL_JioCinema", 37 | "appVersion": "23.10.13.0-841c2bc7", 38 | "deviceId": id, 39 | "deviceType": "phone", 40 | "freshLaunch": True, 41 | "os": "ios" 42 | }, headers={ 43 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" 44 | }).json() 45 | 46 | return token["authToken"], id 47 | 48 | def login(mobile_number): 49 | accesstoken, id = get_accesstoken() 50 | 51 | send = requests.post(url=OTPSendURL, json={ 52 | "number": base64.b64encode(f"+91{mobile_number}".encode()).decode(), 53 | "appVersion": "23.10.13.0-841c2bc7" 54 | }, headers = { 55 | 'accesstoken': accesstoken, 56 | 'appname': 'RJIL_JioCinema', 57 | 'cache-control': 'no-cache', 58 | 'devicetype': 'phone', 59 | 'os': 'ios', 60 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36', 61 | }) 62 | print(send.content) 63 | if 'karix' in str(send.content): 64 | OTP = input ('Enter OTP Received: ') 65 | verify = requests.post(url = OTPVerifyURL, headers = { 66 | 'accesstoken': accesstoken, 67 | 'appname': 'RJIL_JioCinema', 68 | 'cache-control': 'no-cache', 69 | 'devicetype': 'phone', 70 | 'os': 'ios', 71 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36', 72 | }, json={ 73 | "appVersion": "23.10.13.0-841c2bc7", 74 | "deviceInfo": { 75 | "consumptionDeviceName": "iPhone", 76 | "info": { 77 | "androidId": id, 78 | "platform": { 79 | "name": "iPhone OS" 80 | }, 81 | "type": "iOS" 82 | } 83 | }, 84 | "number": base64.b64encode(f"+91{mobile_number}".encode()).decode(), 85 | "otp": OTP 86 | }) 87 | creds = json.loads(verify.content) 88 | load_creds(creds) 89 | else: 90 | print ("Wrong/Unregistered Mobile Number (ensure there's no +91 or 0 in the beginning)") 91 | sys.exit() 92 | 93 | def load_creds(creds): 94 | try: 95 | accesstoken = creds['authToken'] 96 | devid = creds['deviceId'] 97 | except KeyError: 98 | print ("Wrong OTP, Try again!") 99 | sys.exit() 100 | Creds = { 101 | "accesstoken" : accesstoken, 102 | "deviceid" : devid 103 | } 104 | with open("creds.txt", "w") as f: 105 | f.write(json.dumps(Creds)) 106 | 107 | def get_manifest(VideoID): 108 | headers = { 109 | "Accesstoken": accesstoken, 110 | "Appname": "RJIL_JioCinema", 111 | "Versioncode": "2310130", 112 | "Deviceid": devid, 113 | "x-apisignatures": "o668nxgzwff", 114 | "X-Platform": "androidweb", 115 | "X-Platform-Token": "web", 116 | "user-agent": 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36', 117 | } 118 | response = requests.post(url=Request_URL + VideoID, headers=headers, json={ 119 | "4k": False, 120 | "ageGroup": "18+", 121 | "appVersion": "3.4.0", 122 | "bitrateProfile": "xhdpi", 123 | "capability": { 124 | "drmCapability": { 125 | "aesSupport": "yes", 126 | "fairPlayDrmSupport": "none", 127 | "playreadyDrmSupport": "none", 128 | "widevineDRMSupport": "none" 129 | }, 130 | "frameRateCapability": [ 131 | { 132 | "frameRateSupport": "60fps", 133 | "videoQuality": "2160p" 134 | } 135 | ] 136 | }, 137 | "continueWatchingRequired": True, 138 | "dolby": True, 139 | "downloadRequest": False, 140 | "hevc": False, # adjust accordingly 141 | "kidsSafe": False, 142 | "manufacturer": "Windows", 143 | "model": "Windows", 144 | "multiAudioRequired": True, 145 | "osVersion": "10", 146 | "parentalPinValid": True, 147 | "x-apisignatures": "o668nxgzwff" 148 | }) 149 | return json.loads(response.text) 150 | 151 | def get_m3u8(manifest): 152 | m3u8 = manifest['data']['playbackUrls'][1]['url'] 153 | return m3u8 154 | 155 | def mod_m3u8(url): 156 | mod = url.replace("jiovod.cdn.jio.com", "jiobeats.cdn.jio.com") 157 | lst = mod.split("/") 158 | lst[-1] = "chunklist.m3u8" 159 | mod = "/".join(lst) 160 | return mod 161 | 162 | print ('JioCinema Content Downloading Tool') 163 | load_config() 164 | if accesstoken == "" and devid == "": 165 | M_No = input ('Enter Mobile Number: ') 166 | login (M_No) 167 | load_config() 168 | VideoID = input ('Enter VideoID: ') 169 | manifest = get_manifest(VideoID) 170 | 171 | try: 172 | content_name = manifest['data']['name'] 173 | except KeyError: 174 | print ("Incorrect/Malformed VideoID") 175 | sys.exit() 176 | print (f'Downloading: {content_name} | {manifest["data"]["defaultLanguage"]}') 177 | # print (f'Subtitles available: {metadata["subtitle"]}') 178 | fileName = f'{content_name}.mp4' 179 | 180 | def get_streams(m3u8): 181 | print ("Downloading A/V") 182 | os.system(f'{ytdl_path} {m3u8} --allow-unplayable-formats --downloader aria2c --user-agent "JioOnDemand/1.5.2.1 (Linux;Android 4.4.2) Jio" -q --no-warnings') # + -P TEMP:{cachePath} -P HOME:{outPath} 183 | os.rename(f'{dirPath}\chunklist [chunklist].mp4', fileName) 184 | print ("\nSuccessfully downloaded the stream!") 185 | 186 | 187 | m3u8_url = get_m3u8(manifest) 188 | nonDRM_m3u8_url = mod_m3u8(m3u8_url) 189 | get_streams(nonDRM_m3u8_url) 190 | 191 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | --------------------------------------------------------------------------------