├── Modules ├── __init__.py ├── JioSaavn.py └── JioTV.py ├── routers ├── __init__.py ├── JioSaavnRoute.py └── JiotvRoute.py ├── models ├── __init__.py ├── JioTV │ └── ExceptionModels.py └── JioSaavn │ ├── ArtistDetailsModel.py │ ├── SongDetailsModel.py │ ├── PlaylistDetailsModel.py │ ├── AlbumDetailsModel.py │ ├── SearchModel.py │ └── HomeModels.py ├── resources ├── JioTV_logo.icns └── JioTV_logo.ico ├── static ├── JioTV │ ├── css │ │ └── style.css │ └── js │ │ └── script.js └── JioSaavn │ └── css │ └── style.css ├── .vscode └── settings.json ├── requirements.txt ├── Dockerfile ├── .gitignore ├── .dockerignore ├── LICENSE ├── templates ├── JioTV │ ├── player.html │ ├── index.html │ └── otp_login.html └── JioSaavn │ ├── home.html │ ├── playlist_details.html │ ├── Album_Details.html │ ├── search_results.html │ ├── Play.html │ └── artists_details.html ├── README.md ├── .github └── workflows │ └── build.yml └── main.py /Modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /routers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from .JioTV import ExceptionModels 2 | -------------------------------------------------------------------------------- /resources/JioTV_logo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henry-richard7/JioTV-Proxy/HEAD/resources/JioTV_logo.icns -------------------------------------------------------------------------------- /resources/JioTV_logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henry-richard7/JioTV-Proxy/HEAD/resources/JioTV_logo.ico -------------------------------------------------------------------------------- /static/JioTV/css/style.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 100px auto; 3 | width: 70%; 4 | } 5 | video { 6 | width: 100%; 7 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[python]": { 3 | "editor.defaultFormatter": "ms-python.black-formatter" 4 | }, 5 | "python.formatting.provider": "none" 6 | } -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | httpx 2 | uvicorn 3 | fastapi 4 | requests 5 | m3u8 6 | nuitka 7 | jinja2 8 | pyDes 9 | pydantic 10 | scheduler 11 | uvloop; sys_platform != 'win32' 12 | winloop; sys_platform == 'win32' -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11 2 | 3 | WORKDIR /app 4 | 5 | COPY requirements.txt . 6 | RUN pip install -r requirements.txt 7 | 8 | COPY . . 9 | 10 | EXPOSE 8000 11 | 12 | CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] -------------------------------------------------------------------------------- /models/JioTV/ExceptionModels.py: -------------------------------------------------------------------------------- 1 | class JiotvUnauthorizedException(Exception): 2 | def __init__(self, name: str): 3 | self.name = name 4 | 5 | 6 | class JiotvSessionExpiredException(Exception): 7 | def __init__(self, name: str): 8 | self.name = name 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/tasks.json 4 | !.vscode/launch.json 5 | !.vscode/extensions.json 6 | !.vscode/*.code-snippets 7 | 8 | # Local History for Visual Studio Code 9 | .history/ 10 | 11 | # Built Visual Studio Code Extensions 12 | *.vsix 13 | static/.DS_Store 14 | creds.db 15 | data/jio_headers.json 16 | Modules/__pycache__/ 17 | __pycache__ 18 | output 19 | *.ipynb -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/tasks.json 4 | !.vscode/launch.json 5 | !.vscode/extensions.json 6 | !.vscode/*.code-snippets 7 | 8 | .github 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Built Visual Studio Code Extensions 13 | *.vsix 14 | static/.DS_Store 15 | creds.db 16 | data/jio_headers.json 17 | Modules/__pycache__/ 18 | __pycache__ 19 | LICENSE 20 | README.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Henry Richard J 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /models/JioSaavn/ArtistDetailsModel.py: -------------------------------------------------------------------------------- 1 | from pydantic import ( 2 | BaseModel, 3 | Field, 4 | field_validator, 5 | ValidationInfo, 6 | ) 7 | 8 | from models.JioSaavn.SearchModel import Song, Album 9 | from html import unescape 10 | 11 | 12 | class TopSongs(BaseModel): 13 | songs: list[Song] 14 | 15 | 16 | class TopAlbums(BaseModel): 17 | albums: list[Album] 18 | 19 | 20 | class ArtistDetail(BaseModel): 21 | artist_id: str = Field(..., alias="artistId") 22 | title: str = Field(..., alias="name") 23 | image: str 24 | listeners: str = Field(..., alias="subtitle") 25 | follower_count: str 26 | top_songs: TopSongs = Field(..., alias="topSongs") 27 | top_albums: TopAlbums = Field(..., alias="topAlbums") 28 | 29 | @field_validator("*", mode="before") 30 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 31 | if info.field_name == "image": 32 | return value.replace("150x150", "500x500") 33 | 34 | elif info.field_name == "listeners": 35 | return value.split(" ")[-2] 36 | 37 | elif isinstance(value, str): 38 | return unescape(value) 39 | 40 | else: 41 | return value 42 | -------------------------------------------------------------------------------- /models/JioSaavn/SongDetailsModel.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, field_validator, ValidationInfo, computed_field 2 | from html import unescape 3 | from Modules import JioSaavn 4 | 5 | 6 | class SongDetail(BaseModel): 7 | id: str 8 | title: str = Field(..., alias="song") 9 | album: str 10 | albumid: str 11 | year: str 12 | artist: list = Field(..., alias="primary_artists") 13 | artist_id: list = Field(..., alias="primary_artists_id") 14 | image: str 15 | play_count: int 16 | encrypted_media_url: str 17 | duration: str 18 | release_date: str 19 | 20 | @field_validator("*", mode="before") 21 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 22 | if info.field_name == "image": 23 | return value.replace("150x150", "500x500") 24 | 25 | elif info.field_name == "artist" or info.field_name == "artist_id": 26 | 27 | if info.field_name == "artist": 28 | return [unescape(v) for v in value.split(", ")] 29 | else: 30 | return value.split(", ") 31 | 32 | elif isinstance(value, str): 33 | return unescape(value) 34 | 35 | else: 36 | return value 37 | 38 | @computed_field 39 | @property 40 | def decoded_stream_link(self) -> str: 41 | jio_saavn_api = JioSaavn.JioSaavnApi() 42 | return jio_saavn_api.decrypt_url(self.encrypted_media_url) 43 | -------------------------------------------------------------------------------- /templates/JioTV/player.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | JioTV Player 6 | 10 | 11 | 12 | 13 | 14 | 15 | 21 |
22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JioTV-Proxy 2 | 3 | JioTV proxy developed using Python and FastAPI framework. 4 | 5 | # Total Downloads 6 | 7 | ![Downloads](https://img.shields.io/github/downloads/henry-richard7/JioTV-Proxy/total.svg?style=for-the-badge&logo=github) 8 | 9 | # How to use (From Binary) 10 | 11 | - Download the latest for your platform from [Releases](https://github.com/henry-richard7/JioTV-Proxy/releases) 12 | - Run JioTV file. 13 | - Login to your Jio Account using OTP at http://localhost:8000/jiotv/login. 14 | - To play live channels on web http://localhost:8000/jiotv. 15 | - To play live channels in media player such as vlc http://localhost:8000/jiotv/playlist.m3u 16 | - To play live channels on your local network http://:8000/jiotv/playlist.m3u (You can get this from console when running the app.) 17 | 18 | # How To Use (Using Docker) 19 | 20 | - Clone or Download this repo. 21 | - In terminal `cd JioTV-Proxy` 22 | - Next type `docker build -t jiotv-proxy .` and press enter and wait for build. 23 | - Next type `docker run -p 8000:8000 jiotv-proxy` and press enter. 24 | 25 | # How To Use (From Source) 26 | 27 | - Clone or Download this repo. 28 | - Install required dependencies `pip install -r requirements.txt` 29 | - To run the py file, on a terminal in the root folder and type `python3 main.py` or `python main.py` 30 | - Follow the above steps. 31 | 32 | # Known Issues 33 | 34 | - Sony channels will not play. 35 | 36 | # Auto Build 37 | 38 | This repo uses github actions to build binary for x86_64. 39 | -------------------------------------------------------------------------------- /models/JioSaavn/PlaylistDetailsModel.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, field_validator, ValidationInfo, computed_field 2 | from html import unescape 3 | from datetime import datetime 4 | 5 | 6 | class Song(BaseModel): 7 | id: str 8 | title: str = Field(..., alias="song") 9 | album: str 10 | album_id: str = Field(..., alias="albumid") 11 | year: str 12 | artist: list = Field(..., alias="primary_artists") 13 | artist_id: list = Field(..., alias="primary_artists_id") 14 | image: str 15 | language: str 16 | 17 | @field_validator("*", mode="before") 18 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 19 | if info.field_name == "image": 20 | return value.replace("150x150", "500x500") 21 | 22 | elif info.field_name == "artist" or info.field_name == "artist_id": 23 | 24 | if info.field_name == "artist": 25 | return [unescape(v) for v in value.split(", ")] 26 | else: 27 | return value.split(", ") 28 | 29 | elif isinstance(value, str): 30 | return unescape(value) 31 | 32 | else: 33 | return value 34 | 35 | 36 | class PlaylistDetail(BaseModel): 37 | id: str = Field(..., alias="listid") 38 | title: str = Field(..., alias="listname") 39 | image: str 40 | list_count: str 41 | songs: list[Song] 42 | last_updated: int 43 | 44 | @computed_field 45 | @property 46 | def last_update_string(self) -> str: 47 | dt_object = datetime.fromtimestamp(self.last_updated) 48 | return dt_object.strftime("%Y-%m-%d %I:%M:%S %p") 49 | 50 | @field_validator("*", mode="before") 51 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 52 | if isinstance(value, str): 53 | return unescape(value) 54 | 55 | else: 56 | return value 57 | -------------------------------------------------------------------------------- /models/JioSaavn/AlbumDetailsModel.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field, field_validator, ValidationInfo 2 | from html import unescape 3 | 4 | 5 | class AlbumInput(BaseModel): 6 | album_id: str 7 | 8 | 9 | class Song(BaseModel): 10 | id: str 11 | title: str = Field(..., alias="song") 12 | album: str 13 | albumid: str 14 | year: str 15 | artist: str = Field(..., alias="music") 16 | artist_id: str = Field(..., alias="music_id") 17 | image: str 18 | play_count: str 19 | encrypted_media_url: str 20 | duration: str 21 | release_date: str 22 | 23 | @field_validator("*", mode="before") 24 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 25 | if info.field_name == "image": 26 | return value.replace("150x150", "500x500") 27 | elif isinstance(value, str): 28 | return unescape(value) 29 | else: 30 | return value 31 | 32 | 33 | class AlbumDetail(BaseModel): 34 | title: str 35 | name: str 36 | year: str 37 | release_date: str 38 | primary_artists: list 39 | primary_artists_id: list 40 | albumid: str 41 | perma_url: str 42 | image: str 43 | 44 | @field_validator("*", mode="before") 45 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 46 | if info.field_name == "image": 47 | return value.replace("150x150", "500x500") 48 | elif ( 49 | info.field_name == "primary_artists" 50 | or info.field_name == "primary_artists_id" 51 | ): 52 | if info.field_name == "primary_artists": 53 | return [unescape(v) for v in value.split(", ")] 54 | else: 55 | return value.split(", ") 56 | else: 57 | return unescape(value) 58 | 59 | 60 | class AlbumDetails(BaseModel): 61 | album_detail: AlbumDetail 62 | songs: list[Song] 63 | -------------------------------------------------------------------------------- /static/JioTV/js/script.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | const source = document.getElementById('stream_url').value; 3 | const video = document.querySelector('video'); 4 | 5 | const defaultOptions = {}; 6 | 7 | if (!Hls.isSupported()) { 8 | video.src = source; 9 | var player = new Plyr(video, defaultOptions); 10 | } else { 11 | // For more Hls.js options, see https://github.com/dailymotion/hls.js 12 | const hls = new Hls(); 13 | hls.loadSource(source); 14 | 15 | // From the m3u8 playlist, hls parses the manifest and returns 16 | // all available video qualities. This is important, in this approach, 17 | // we will have one source on the Plyr player. 18 | hls.on(Hls.Events.MANIFEST_PARSED, function (event, data) { 19 | 20 | // Transform available levels into an array of integers (height values). 21 | const availableQualities = hls.levels.map(l => l.height); 22 | availableQualities.unshift(0); //prepend 0 to quality array 23 | 24 | // Add new qualities to option 25 | defaultOptions.quality = { 26 | default: 0, //Default - AUTO 27 | options: availableQualities, 28 | forced: true, 29 | onChange: e => updateQuality(e) }; 30 | 31 | // Add Auto Label 32 | defaultOptions.i18n = { 33 | qualityLabel: { 34 | 0: 'Auto' } }; 35 | 36 | 37 | 38 | hls.on(Hls.Events.LEVEL_SWITCHED, function (event, data) { 39 | var span = document.querySelector(".plyr__menu__container [data-plyr='quality'][value='0'] span"); 40 | if (hls.autoLevelEnabled) { 41 | span.innerHTML = `AUTO (${hls.levels[data.level].height}p)`; 42 | } else { 43 | span.innerHTML = `AUTO`; 44 | } 45 | }); 46 | 47 | // Initialize new Plyr player with quality options 48 | var player = new Plyr(video, defaultOptions); 49 | }); 50 | 51 | hls.attachMedia(video); 52 | window.hls = hls; 53 | } 54 | 55 | function updateQuality(newQuality) { 56 | if (newQuality === 0) { 57 | window.hls.currentLevel = -1; //Enable AUTO quality if option.value = 0 58 | } else { 59 | window.hls.levels.forEach((level, levelIndex) => { 60 | if (level.height === newQuality) { 61 | console.log("Found quality match with " + newQuality); 62 | window.hls.currentLevel = levelIndex; 63 | } 64 | }); 65 | } 66 | } 67 | }); -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | tags: 4 | - "v*" 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - os: macos-latest 13 | TARGET: macos 14 | CMD_BUILD: > 15 | nuitka --onefile main.py --output-dir=output --output-filename="JioTV" --remove-output --disable-ccache && 16 | cp -r templates output && 17 | cp -r data output && 18 | cp -r static output && 19 | cd output/ && 20 | zip -r9 JioTV_auto_build_MACOS_x86_64 . 21 | OUT_FILE_NAME: JioTV_auto_build_MACOS_x86_64.zip 22 | - os: windows-latest 23 | TARGET: windows 24 | CMD_BUILD: > 25 | nuitka --windows-icon-from-ico=resources\JioTV_logo.ico --onefile main.py --output-dir=output --output-filename="JioTV.exe" --remove-output --disable-ccache --follow-imports --assume-yes-for-downloads && 26 | cp -r templates output && 27 | cp -r data output && 28 | cp -r static output && 29 | cd output/ && 30 | powershell Compress-Archive -Path .\* -DestinationPath JioTV_auto_build_WINDOWS_x86_64.zip 31 | OUT_FILE_NAME: JioTV_auto_build_WINDOWS_x86_64.zip 32 | ASSET_MIME: application/zip 33 | - os: ubuntu-latest 34 | TARGET: linux 35 | CMD_BUILD: > 36 | nuitka --onefile main.py --output-dir=output --output-filename="JioTV" --remove-output --disable-ccache && 37 | cp -r templates output && 38 | cp -r data output && 39 | cp -r static output && 40 | cd output/ && 41 | zip -r9 JioTV_auto_build_LINUX_x86_64 . 42 | OUT_FILE_NAME: JioTV_auto_build_LINUX_x86_64.zip 43 | ASSET_MIME: application/zip 44 | steps: 45 | - uses: actions/checkout@v4 46 | - uses: ilammy/msvc-dev-cmd@v1 47 | - name: Set up Python 3.10.X 48 | uses: actions/setup-python@v5 49 | with: 50 | python-version: 3.10.11 51 | - name: Install dependencies 52 | run: | 53 | python -m pip install --upgrade pip 54 | pip install -r requirements.txt 55 | - name: Build with pyinstaller for ${{matrix.TARGET}} 56 | run: ${{matrix.CMD_BUILD}} 57 | - name: Create Release 58 | uses: softprops/action-gh-release@v2 59 | with: 60 | files: output/${{matrix.OUT_FILE_NAME}} 61 | env: 62 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 63 | -------------------------------------------------------------------------------- /static/JioSaavn/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: lightgreen; 3 | 4 | /* Smoothly transition the background color */ 5 | transition: background-color .5s; 6 | } 7 | 8 | .player { 9 | height: 95vh; 10 | display: flex; 11 | align-items: center; 12 | flex-direction: column; 13 | justify-content: center; 14 | } 15 | 16 | .details { 17 | display: flex; 18 | align-items: center; 19 | flex-direction: column; 20 | justify-content: center; 21 | margin-top: 25px; 22 | } 23 | 24 | .track-art { 25 | margin: 25px; 26 | height: 250px; 27 | width: 250px; 28 | background-image: url("https://images.pexels.com/photos/262034/pexels-photo-262034.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260"); 29 | background-size: cover; 30 | border-radius: 15%; 31 | } 32 | 33 | .now-playing { 34 | font-size: 1rem; 35 | } 36 | 37 | .track-name { 38 | font-size: 3rem; 39 | } 40 | 41 | .track-artist { 42 | font-size: 1.5rem; 43 | } 44 | 45 | .buttons { 46 | display: flex; 47 | flex-direction: row; 48 | align-items: center; 49 | } 50 | 51 | .playpause-track, .prev-track, .next-track { 52 | padding: 25px; 53 | opacity: 0.8; 54 | 55 | /* Smoothly transition the opacity */ 56 | transition: opacity .2s; 57 | } 58 | 59 | .playpause-track:hover, .prev-track:hover, .next-track:hover { 60 | opacity: 1.0; 61 | } 62 | 63 | .slider_container { 64 | width: 75%; 65 | max-width: 400px; 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | } 70 | 71 | /* Modify the appearance of the slider */ 72 | .seek_slider, .volume_slider { 73 | -webkit-appearance: none; 74 | -moz-appearance: none; 75 | appearance: none; 76 | height: 5px; 77 | background: black; 78 | opacity: 0.7; 79 | -webkit-transition: .2s; 80 | transition: opacity .2s; 81 | } 82 | 83 | /* Modify the appearance of the slider thumb */ 84 | .seek_slider::-webkit-slider-thumb, .volume_slider::-webkit-slider-thumb { 85 | -webkit-appearance: none; 86 | -moz-appearance: none; 87 | appearance: none; 88 | width: 15px; 89 | height: 15px; 90 | background: white; 91 | cursor: pointer; 92 | border-radius: 50%; 93 | } 94 | 95 | .seek_slider:hover, .volume_slider:hover { 96 | opacity: 1.0; 97 | } 98 | 99 | .seek_slider { 100 | width: 60%; 101 | } 102 | 103 | .volume_slider { 104 | width: 30%; 105 | } 106 | 107 | .current-time, .total-duration { 108 | padding: 10px; 109 | } 110 | 111 | i.fa-volume-down, i.fa-volume-up { 112 | padding: 10px; 113 | } 114 | 115 | i.fa-play-circle, i.fa-pause-circle, i.fa-step-forward, i.fa-step-backward { 116 | cursor: pointer; 117 | } -------------------------------------------------------------------------------- /models/JioSaavn/SearchModel.py: -------------------------------------------------------------------------------- 1 | from pydantic import ( 2 | BaseModel, 3 | Field, 4 | field_validator, 5 | ValidationInfo, 6 | ) 7 | from enum import Enum 8 | from html import unescape 9 | 10 | 11 | class SearchModes(str, Enum): 12 | SONGS = "search.getResults" 13 | ARTISTS = "search.getArtistResults" 14 | ALBUMS = "search.getAlbumResults" 15 | PLAYLISTS = "search.getPlaylistResults" 16 | 17 | class Config: 18 | use_enum_values = True 19 | 20 | 21 | class SearchInputModel(BaseModel): 22 | search_mode: SearchModes 23 | query: str 24 | 25 | 26 | class MoreInfo(BaseModel): 27 | album: str 28 | album_id: str 29 | artist: str = Field(..., alias="music") 30 | encrypted_media_url: str 31 | duration: str 32 | 33 | 34 | class Song(BaseModel): 35 | id: str 36 | title: str 37 | image: str 38 | language: str 39 | year: str 40 | play_count: str 41 | more_info: MoreInfo 42 | 43 | @field_validator("*", mode="before") 44 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 45 | if info.field_name == "image": 46 | return value.replace("150x150", "500x500") 47 | 48 | elif isinstance(value, str): 49 | return unescape(value) 50 | 51 | else: 52 | return value 53 | 54 | 55 | class Album(BaseModel): 56 | id: str 57 | title: str 58 | artist: str = Field(..., alias="subtitle") 59 | image: str 60 | language: str 61 | year: str 62 | 63 | @field_validator("*", mode="before") 64 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 65 | if info.field_name == "image": 66 | return value.replace("150x150", "500x500") 67 | 68 | elif isinstance(value, str): 69 | return unescape(value) 70 | 71 | else: 72 | return value 73 | 74 | 75 | class Artist(BaseModel): 76 | title: str = Field(..., alias="name") 77 | id: str 78 | image: str 79 | 80 | @field_validator("*", mode="before") 81 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 82 | if info.field_name == "image": 83 | return value.replace("50x50", "500x500") 84 | 85 | elif isinstance(value, str): 86 | return unescape(value) 87 | 88 | else: 89 | return value 90 | 91 | 92 | class Playlist(BaseModel): 93 | id: str 94 | title: str 95 | image: str 96 | 97 | @field_validator("*", mode="before") 98 | def image_resolution_fix(cls, value: str, info: ValidationInfo): 99 | if info.field_name == "image": 100 | return value.replace("150x150", "500x500") 101 | 102 | elif isinstance(value, str): 103 | return unescape(value) 104 | 105 | else: 106 | return value 107 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | 3 | from fastapi import FastAPI, Request, status 4 | from fastapi.responses import JSONResponse 5 | from fastapi.staticfiles import StaticFiles 6 | 7 | from routers import JiotvRoute, JioSaavnRoute 8 | from models.JioTV.ExceptionModels import ( 9 | JiotvUnauthorizedException, 10 | JiotvSessionExpiredException, 11 | ) 12 | 13 | from Modules.JioTV import JioTV 14 | import logging 15 | 16 | from sys import platform as current_platform 17 | 18 | logger = logging.getLogger("uvicorn") 19 | jiotv_obj = JioTV(logger) 20 | localip = jiotv_obj.get_local_ip() 21 | 22 | 23 | def welcome_msg(): 24 | print("Welcome to Jio Proxy") 25 | print("To Watch Live-TV:") 26 | print(f"\tTV Web Player: http://{localip}:8000/jiotv") 27 | print(f"\tPlease Login at http://{localip}:8000/jiotv/login") 28 | print(f"\tPlaylist m3u: http://{localip}:8000/jiotv/playlist.m3u") 29 | print() 30 | print("To Access Jio Saavn:") 31 | print(f"\tWeb Player: http://{localip}:8000/jio_saavn/") 32 | print(f"\tAPI Endpoints: http://{localip}:8000/jio_saavn/api/") 33 | print() 34 | 35 | 36 | app = FastAPI() 37 | app.mount("/static", StaticFiles(directory="static"), name="static") 38 | 39 | 40 | @app.exception_handler(JiotvUnauthorizedException) 41 | async def jiotv_unauthorized_exception_handler( 42 | request: Request, exc: JiotvUnauthorizedException 43 | ): 44 | return JSONResponse( 45 | status_code=status.HTTP_401_UNAUTHORIZED, 46 | content={ 47 | "status_code": status.HTTP_401_UNAUTHORIZED, 48 | "error": "Session not authenticated.", 49 | "details": f"Seems like you are not logged in, please login by going to http://{request.client.host}:8000/jiotv/login", 50 | }, 51 | ) 52 | 53 | 54 | @app.exception_handler(JiotvSessionExpiredException) 55 | async def jiotv_session_expired_exception_handler( 56 | request: Request, exc: JiotvSessionExpiredException 57 | ): 58 | return JSONResponse( 59 | status_code=status.HTTP_410_GONE, 60 | content={ 61 | "status_code": status.HTTP_410_GONE, 62 | "error": "Session Expired.", 63 | "details": f"Seems like sessions has been expired, please login again at http://{request.client.host}:8000/jiotv/login", 64 | }, 65 | ) 66 | 67 | 68 | app.include_router(JiotvRoute.router, prefix="/jiotv") 69 | app.include_router(JioSaavnRoute.router, prefix="/jio_saavn") 70 | 71 | 72 | if __name__ == "__main__": 73 | welcome_msg() 74 | config = uvicorn.Config(app=app, host="0.0.0.0", port=8000, log_level="warning") 75 | server = uvicorn.Server(config) 76 | 77 | if current_platform in ("win32", "cygwin", "cli"): 78 | import winloop 79 | 80 | winloop.install() 81 | winloop.run(server.serve()) 82 | else: 83 | import uvloop # type: ignore For UNIX based systems 84 | 85 | uvloop.install() 86 | uvloop.run(server.serve()) 87 | -------------------------------------------------------------------------------- /models/JioSaavn/HomeModels.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, computed_field, Field, field_validator, ValidationInfo 2 | from enum import Enum 3 | from html import unescape 4 | 5 | 6 | class Languages(Enum): 7 | Tamil = "tamil" 8 | Hindi = "hindi" 9 | English = "english" 10 | Telugu = "telugu" 11 | Punjabi = "punjabi" 12 | Marathi = "marathi" 13 | Gujarati = "gujarati" 14 | Bengali = "bengali" 15 | Kannada = "kannada" 16 | Bhojpuri = "bhojpuri" 17 | Malayalam = "malayalam" 18 | Urdu = "urdu" 19 | Haryanvi = "haryanvi" 20 | Rajasthani = "rajasthani" 21 | Odia = "odia" 22 | Assamese = "assamese" 23 | 24 | 25 | class HomeInput(BaseModel): 26 | language: Languages 27 | 28 | 29 | class Music(BaseModel): 30 | id: str 31 | name: str 32 | 33 | @field_validator("*", mode="before") 34 | def html_unescape(cls, value: str, info: ValidationInfo): 35 | if isinstance(value, str): 36 | return unescape(value) 37 | 38 | else: 39 | return value 40 | 41 | 42 | class Songs(BaseModel): 43 | name: str 44 | image: str 45 | 46 | @field_validator("*", mode="before") 47 | def html_unescape(cls, value: str, info: ValidationInfo): 48 | if isinstance(value, str): 49 | return unescape(value) 50 | 51 | else: 52 | return value 53 | 54 | 55 | class ChartItem(BaseModel): 56 | listid: str 57 | listname: str 58 | image: str 59 | weight: int 60 | songs: list[Songs] 61 | perma_url: str 62 | 63 | @field_validator("*", mode="before") 64 | def html_unescape(cls, value: str, info: ValidationInfo): 65 | if isinstance(value, str): 66 | return unescape(value) 67 | 68 | else: 69 | return value 70 | 71 | 72 | class NewAlbumItem(BaseModel): 73 | query: str 74 | text: str 75 | year: str 76 | image: str 77 | albumid: str 78 | title: str 79 | Artist: dict = Field(..., alias="Artist", exclude=True) 80 | weight: int 81 | language: str 82 | 83 | @computed_field 84 | @property 85 | def artists(self) -> list[Music]: 86 | return [Music(**artist) for artist in self.Artist.get("music")] 87 | 88 | @field_validator("*", mode="before") 89 | def html_unescape(cls, value: str, info: ValidationInfo): 90 | if info.field_name == "image": 91 | return value.replace("150x150", "500x500") 92 | 93 | elif isinstance(value, str): 94 | return unescape(value) 95 | 96 | else: 97 | return value 98 | 99 | 100 | class FeaturedPlaylistItem(BaseModel): 101 | listid: str 102 | secondary_subtitle: str 103 | firstname: str 104 | listname: str 105 | data_type: str 106 | count: int 107 | image: str 108 | sponsored: bool 109 | perma_url: str 110 | follower_count: str 111 | uid: str 112 | last_updated: int 113 | 114 | @field_validator("*", mode="before") 115 | def html_unescape(cls, value: str, info: ValidationInfo): 116 | if isinstance(value, str): 117 | return unescape(value) 118 | 119 | else: 120 | return value 121 | 122 | 123 | class HomePageResponse(BaseModel): 124 | new_albums: list[NewAlbumItem] 125 | featured_playlists: list[FeaturedPlaylistItem] 126 | charts: list[ChartItem] 127 | -------------------------------------------------------------------------------- /templates/JioTV/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | JioTV Proxy 7 | 13 | 14 | 15 | 20 | 48 | 49 |
50 |
51 |
52 | 61 | 68 |
69 |
70 |
71 | {% if channels|length == 0 %} 72 | 73 |
74 | 78 |
79 | 80 | {% endif %} 81 |
82 | {% for channel in channels %} 83 |
84 |
85 | ... 86 |
87 |
{{ channel.title }}
88 | Watch Live Now 94 |
95 |
96 |
97 | {% endfor %} 98 |
99 |
100 | {% if search and query.strip()%} 101 | Show all 104 | {% endif %} 105 |
106 | 107 | 108 | -------------------------------------------------------------------------------- /routers/JioSaavnRoute.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from fastapi import APIRouter, Depends, Request 4 | 5 | from fastapi.templating import Jinja2Templates 6 | 7 | from Modules.JioSaavn import JioSaavnApi 8 | 9 | from models.JioSaavn import ( 10 | HomeModels, 11 | SearchModel, 12 | SongDetailsModel, 13 | AlbumDetailsModel, 14 | ArtistDetailsModel, 15 | PlaylistDetailsModel, 16 | ) 17 | 18 | router = APIRouter(tags=["Jio Saavn"]) 19 | templates = Jinja2Templates(directory="templates/JioSaavn") 20 | 21 | 22 | @router.get("/api/home") 23 | async def api_homepage( 24 | language: HomeModels.Languages = HomeModels.Languages.Tamil, 25 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 26 | ) -> HomeModels.HomePageResponse: 27 | home_page_contents = await jio_saavn.home_page(language) 28 | return home_page_contents 29 | 30 | 31 | @router.get("/api/search") 32 | async def api_search( 33 | query: str, 34 | search_mode: SearchModel.SearchModes = SearchModel.SearchModes.SONGS, 35 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 36 | ) -> Union[ 37 | list[SearchModel.Song], 38 | list[SearchModel.Album], 39 | list[SearchModel.Artist], 40 | list[SearchModel.Playlist], 41 | ]: 42 | search_results = await jio_saavn.search(query=query, search_mode=search_mode) 43 | return search_results 44 | 45 | 46 | @router.get("/api/song_details") 47 | async def api_song_details( 48 | song_id: str, 49 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 50 | ) -> SongDetailsModel.SongDetail: 51 | song_detail = await jio_saavn.song_details(song_id=song_id) 52 | return song_detail 53 | 54 | 55 | @router.get("/api/album_details") 56 | async def api_album_details( 57 | album_id: str, 58 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 59 | ) -> AlbumDetailsModel.AlbumDetails: 60 | album_detail = await jio_saavn.album_details(album_id=album_id) 61 | return album_detail 62 | 63 | 64 | @router.get("/api/artist_details") 65 | async def api_artist_details( 66 | artists_id: str, 67 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 68 | ) -> ArtistDetailsModel.ArtistDetail: 69 | artist_detail = await jio_saavn.artist_details(artist_id=artists_id) 70 | return artist_detail 71 | 72 | 73 | @router.get("/api/playlist_details") 74 | async def api_playlist_details( 75 | playlist_id: str, 76 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 77 | ) -> PlaylistDetailsModel.PlaylistDetail: 78 | playlist_detail = await jio_saavn.playlist_details(playlist_id=playlist_id) 79 | return playlist_detail 80 | 81 | 82 | @router.get("/") 83 | async def home_ui( 84 | request: Request, 85 | language: HomeModels.Languages = HomeModels.Languages.Tamil, 86 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 87 | ): 88 | home_page_contents = await jio_saavn.home_page(language) 89 | return templates.TemplateResponse( 90 | "home.html", 91 | { 92 | "request": request, 93 | "albums": home_page_contents.new_albums, 94 | "language": language, 95 | }, 96 | ) 97 | 98 | 99 | @router.get("/album_details") 100 | async def album_details_ui( 101 | request: Request, 102 | album_id: str, 103 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 104 | ): 105 | home_page_contents = await jio_saavn.album_details(album_id=album_id) 106 | return templates.TemplateResponse( 107 | "Album_Details.html", 108 | { 109 | "request": request, 110 | "album_details": home_page_contents, 111 | }, 112 | ) 113 | 114 | 115 | @router.get("/playlists_details") 116 | async def album_details_ui( 117 | request: Request, 118 | playlist_id: str, 119 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 120 | ): 121 | home_page_contents = await jio_saavn.playlist_details(playlist_id=playlist_id) 122 | return templates.TemplateResponse( 123 | "playlist_details.html", 124 | { 125 | "request": request, 126 | "playlist_details": home_page_contents, 127 | }, 128 | ) 129 | 130 | 131 | @router.get("/artists_details") 132 | async def album_details_ui( 133 | request: Request, 134 | artist_id: str, 135 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 136 | ): 137 | home_page_contents = await jio_saavn.artist_details(artist_id=artist_id) 138 | return templates.TemplateResponse( 139 | "artists_details.html", 140 | { 141 | "request": request, 142 | "artists_details": home_page_contents, 143 | }, 144 | ) 145 | 146 | 147 | @router.get("/play_song") 148 | async def album_details_ui( 149 | request: Request, 150 | song_id: str, 151 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 152 | ): 153 | home_page_contents = await jio_saavn.song_details(song_id=song_id) 154 | return templates.TemplateResponse( 155 | "play.html", 156 | { 157 | "request": request, 158 | "song_details": home_page_contents, 159 | }, 160 | ) 161 | 162 | 163 | @router.get("/search") 164 | async def search_ui( 165 | request: Request, 166 | query: str, 167 | search_mode: SearchModel.SearchModes = SearchModel.SearchModes.SONGS, 168 | jio_saavn: JioSaavnApi = Depends(JioSaavnApi), 169 | ): 170 | home_page_contents = await jio_saavn.search(query=query, search_mode=search_mode) 171 | return templates.TemplateResponse( 172 | "search_results.html", 173 | { 174 | "request": request, 175 | "mode": search_mode.name, 176 | "results": home_page_contents, 177 | "query": query, 178 | }, 179 | ) 180 | -------------------------------------------------------------------------------- /Modules/JioSaavn.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | from httpx import AsyncClient 3 | import base64 4 | from pyDes import des, ECB, PAD_PKCS5 5 | from models.JioSaavn import ( 6 | HomeModels, 7 | AlbumDetailsModel, 8 | SearchModel, 9 | ArtistDetailsModel, 10 | SongDetailsModel, 11 | PlaylistDetailsModel, 12 | ) 13 | 14 | 15 | class JioSaavnApi: 16 | def __init__(self) -> None: 17 | self.des_cipher = des( 18 | b"38346591", ECB, b"\0\0\0\0\0\0\0\0", pad=None, padmode=PAD_PKCS5 19 | ) 20 | self.jio_api_base_url = "https://www.jiosaavn.com/api.php" 21 | 22 | def decrypt_url(self, url: str) -> str: 23 | enc_url = base64.b64decode(url.strip()) 24 | 25 | dec_url = self.des_cipher.decrypt(enc_url, padmode=PAD_PKCS5).decode("utf-8") 26 | dec_url = dec_url.replace("_96.mp4", "_320.mp4") 27 | return dec_url 28 | 29 | async def home_page( 30 | self, language: HomeModels.Languages 31 | ) -> HomeModels.HomePageResponse: 32 | cookies = {"L": language.value} 33 | 34 | request_params = {"__call": "content.getHomepageData"} 35 | async with AsyncClient() as async_client: 36 | resp = await async_client.get( 37 | self.jio_api_base_url, cookies=cookies, params=request_params 38 | ) 39 | 40 | resp = resp.json() 41 | 42 | new_albums = [ 43 | HomeModels.NewAlbumItem(**album) for album in resp.get("new_albums") 44 | ] 45 | featured_playlists = [ 46 | HomeModels.FeaturedPlaylistItem(**featured_playlist) 47 | for featured_playlist in resp.get("featured_playlists") 48 | ] 49 | charts = [HomeModels.ChartItem(**chart) for chart in resp.get("charts")] 50 | 51 | return HomeModels.HomePageResponse( 52 | new_albums=new_albums, featured_playlists=featured_playlists, charts=charts 53 | ) 54 | 55 | async def album_details(self, album_id: str) -> AlbumDetailsModel.AlbumDetails: 56 | 57 | request_params = { 58 | "__call": "content.getAlbumDetails", 59 | "albumid": album_id, 60 | } 61 | async with AsyncClient() as async_client: 62 | resp = await async_client.get(self.jio_api_base_url, params=request_params) 63 | 64 | resp = resp.json() 65 | 66 | album_details = AlbumDetailsModel.AlbumDetail(**resp) 67 | songs = [AlbumDetailsModel.Song(**song) for song in resp.get("songs")] 68 | 69 | result = AlbumDetailsModel.AlbumDetails(album_detail=album_details, songs=songs) 70 | return result 71 | 72 | async def artist_details(self, artist_id: str) -> ArtistDetailsModel.ArtistDetail: 73 | request_params = { 74 | "__call": "artist.getArtistPageDetails", 75 | "_format": "json", 76 | "_marker": "0", 77 | "api_version": "4", 78 | "sort_by": "latest", 79 | "sortOrder": "desc", 80 | "artistId": artist_id, 81 | } 82 | async with AsyncClient() as async_client: 83 | resp = await async_client.get(self.jio_api_base_url, params=request_params) 84 | 85 | resp: dict = resp.json() 86 | return ArtistDetailsModel.ArtistDetail(**resp) 87 | 88 | async def search(self, query: str, search_mode: SearchModel.SearchModes) -> Union[ 89 | list[SearchModel.Song], 90 | list[SearchModel.Album], 91 | list[SearchModel.Artist], 92 | list[SearchModel.Playlist], 93 | ]: 94 | request_params = { 95 | "__call": search_mode.value, 96 | "_format": "json", 97 | "_marker": "0", 98 | "n": "151353", 99 | "api_version": "4", 100 | "ctx": "web6dot0", 101 | "q": query, 102 | } 103 | async with AsyncClient() as async_client: 104 | resp = await async_client.get(self.jio_api_base_url, params=request_params) 105 | 106 | resp: dict = resp.json() 107 | 108 | if search_mode == SearchModel.SearchModes.SONGS: 109 | return [SearchModel.Song(**song) for song in resp.get("results")] 110 | 111 | elif search_mode == SearchModel.SearchModes.ALBUMS: 112 | return [SearchModel.Album(**album) for album in resp.get("results")] 113 | 114 | elif search_mode == SearchModel.SearchModes.ARTISTS: 115 | return [SearchModel.Artist(**artist) for artist in resp.get("results")] 116 | 117 | elif search_mode == SearchModel.SearchModes.PLAYLISTS: 118 | return [ 119 | SearchModel.Playlist(**playlist) for playlist in resp.get("results") 120 | ] 121 | 122 | return None 123 | 124 | async def song_details(self, song_id: str) -> SongDetailsModel.SongDetail: 125 | request_params = { 126 | "__call": "song.getDetails", 127 | "cc": "in", 128 | "pids": song_id, 129 | "_format": "json", 130 | "_marker": "0", 131 | } 132 | async with AsyncClient() as async_client: 133 | resp = await async_client.get(self.jio_api_base_url, params=request_params) 134 | 135 | resp: dict = resp.json() 136 | 137 | return SongDetailsModel.SongDetail(**resp[song_id]) 138 | 139 | async def playlist_details( 140 | self, playlist_id: str 141 | ) -> PlaylistDetailsModel.PlaylistDetail: 142 | request_params = { 143 | "__call": "playlist.getDetails", 144 | "cc": "in", 145 | "listid": playlist_id, 146 | "_format": "json", 147 | "_marker": "0", 148 | } 149 | async with AsyncClient() as async_client: 150 | resp = await async_client.get(self.jio_api_base_url, params=request_params) 151 | 152 | resp: dict = resp.json() 153 | 154 | return PlaylistDetailsModel.PlaylistDetail(**resp) 155 | -------------------------------------------------------------------------------- /templates/JioTV/otp_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | JIO TV Login 8 | 86 | 87 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 | 102 |
103 | 104 | 105 |
106 |
107 | 117 |
118 | 119 | 120 |
121 | 122 | 123 | 124 | 125 |
126 |
127 | 128 | 129 | 183 | 184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /templates/JioSaavn/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 27 | Latest {{language}} Album Songs 28 | 29 | 30 | 120 |
121 |
122 | 130 | 138 |
139 | 178 |
179 | Search For:   180 |
181 | 192 |
193 |
194 | 204 |
205 |
206 | 216 |
217 |
218 | 228 |
229 |
230 |
231 |
232 |
233 |
234 | {% for album in albums %} 235 |
236 |
237 | 242 |
243 |
244 | {{album.text}} 245 |
246 |

247 | Released in {{album.year}} 248 |

249 | Details 254 |
255 |
256 |
257 | {% endfor %} 258 |
259 |
260 |
261 | 262 | 263 | -------------------------------------------------------------------------------- /templates/JioSaavn/playlist_details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 27 | {{ playlist_details.title }} 28 | 29 | 30 | 120 |
121 |
122 | 130 | 138 |
139 | 178 |
179 | Search For:   180 |
181 | 192 |
193 |
194 | 204 |
205 |
206 | 216 |
217 |
218 | 228 |
229 |
230 |
231 |
232 |
233 |
234 | 240 |
241 |
242 |
243 |

{{ playlist_details.title }}

244 |

245 | Last Updated: 246 | {{playlist_details.last_update_string}} 249 |

250 | 251 |

252 | Total Songs: 253 | {{ "".join(playlist_details.list_count) }} 256 |

257 |
258 |
259 |
260 |
261 |
    262 | {% for song in playlist_details.songs %} 263 |
  1. 266 |
    267 |
    {{ song.title }}
    268 | {{ ", ".join(song.artist) }} 269 |
    270 | 273 |
  2. 274 | {% endfor %} 275 |
276 |
277 |
278 | 279 | 280 | -------------------------------------------------------------------------------- /templates/JioSaavn/Album_Details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 27 | {{ album_details.title }} 28 | 29 | 30 | 120 |
121 |
122 | 130 | 138 |
139 | 178 |
179 | Search For:   180 |
181 | 192 |
193 |
194 | 204 |
205 |
206 | 216 |
217 |
218 | 228 |
229 |
230 |
231 |
232 |
233 |
234 | 240 |
241 |
242 |
243 |

{{ title }}

244 |

245 | Released Date: 246 | {{ album_details.album_detail.release_date }} 249 |

250 | 251 |

252 | Primary Artists: 253 | {{ ", ".join(album_details.album_detail.primary_artists) 255 | }} 257 |

258 |
259 |
260 |
261 |
262 |
    263 | {% for song in album_details.songs %} 264 |
  1. 267 |
    268 |
    {{ song.title }}
    269 | {{ song.artist }} 270 |
    271 | 274 |
  2. 275 | {% endfor %} 276 |
277 |
278 |
279 | 280 | 281 | -------------------------------------------------------------------------------- /templates/JioSaavn/search_results.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 27 | Results for {{ songName }} 28 | 29 | 30 | 120 |
121 |
122 | 130 | 138 |
139 | 178 |
179 | Search For:   180 |
181 | 192 |
193 |
194 | 204 |
205 |
206 | 216 |
217 |
218 | 228 |
229 |
230 |
231 |

Search Results for {{ query }}

232 |
233 |
234 |
235 | {% for result in results %} 236 |
237 |
238 | Card image cap 244 |
245 |
246 | {{ result.title|safe }} 247 |
248 |

249 | {{ result.year }} 250 |

251 | {% if mode == "SONGS" %} 252 | 256 | 257 | Listen 258 | 259 | {% elif mode == "ALBUMS" %} 260 | 264 | 265 | Show Details 266 | 267 | {% elif mode == "ARTISTS" %} 268 | 272 | 273 | Show Details 274 | 275 | {% elif mode == "PLAYLISTS" %} 276 | 280 | 281 | Show Details 282 | 283 | {% endif %} 284 |
285 |
286 |
287 | {% endfor %} 288 |
289 |
290 |
291 | 292 | 293 | -------------------------------------------------------------------------------- /routers/JiotvRoute.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from fastapi import APIRouter, Request, Depends, FastAPI 4 | from fastapi.responses import PlainTextResponse, Response 5 | 6 | from fastapi.templating import Jinja2Templates 7 | 8 | from models.JioTV.ExceptionModels import ( 9 | JiotvUnauthorizedException, 10 | JiotvSessionExpiredException, 11 | ) 12 | 13 | from Modules.JioTV import JioTV 14 | from time import time 15 | 16 | from os import path 17 | 18 | import sqlite3 19 | import logging 20 | 21 | from contextlib import asynccontextmanager 22 | import asyncio 23 | 24 | from scheduler.asyncio import Scheduler 25 | from datetime import timedelta 26 | 27 | logger = logging.getLogger("uvicorn") 28 | jiotv_obj = JioTV(logger) 29 | localip = jiotv_obj.get_local_ip() 30 | 31 | 32 | def convert(m3u_file: str): 33 | m3u_json = [] 34 | lines = m3u_file.split("\n") 35 | for line in lines: 36 | if line.startswith("#EXTINF"): 37 | channelInfo = {} 38 | logoStartIndex = line.index('tvg-logo="') + 10 39 | logoEndIndex = line.index('"', logoStartIndex) 40 | logoUrl = line[logoStartIndex:logoEndIndex] 41 | groupTitleStartIndex = line.index('group-title="') + 13 42 | groupTitleEndIndex = line.index('"', groupTitleStartIndex) 43 | groupTitle = line[groupTitleStartIndex:groupTitleEndIndex] 44 | titleStartIndex = line.rindex(",") + 1 45 | title = line[titleStartIndex:].strip() 46 | link = lines[lines.index(line) + 1] 47 | channelInfo["logo"] = logoUrl 48 | channelInfo["group_title"] = groupTitle 49 | channelInfo["title"] = title 50 | channelInfo["link"] = link 51 | m3u_json.append(channelInfo) 52 | result = m3u_json 53 | return result 54 | 55 | 56 | def store_creds(phone_number): 57 | # Store the credentials along with expire time in sqlite 58 | expire_time = time() + 3600 59 | db = sqlite3.connect("creds.db") 60 | cursor = db.cursor() 61 | cursor.execute( 62 | """CREATE TABLE IF NOT EXISTS creds( 63 | phone_number TEXT, 64 | expire NUMERIC 65 | )""" 66 | ) 67 | cursor.execute("""INSERT INTO creds VALUES(?,?)""", (phone_number, expire_time)) 68 | db.commit() 69 | db.close() 70 | 71 | 72 | def update_expire_time(phone_number): 73 | expire_time = time() + 3600 74 | 75 | db = sqlite3.connect("creds.db") 76 | cursor = db.cursor() 77 | 78 | cursor.execute( 79 | """UPDATE creds SET expire = ? WHERE phone_number = ?""", 80 | ( 81 | expire_time, 82 | phone_number, 83 | ), 84 | ) 85 | db.commit() 86 | db.close() 87 | 88 | 89 | def get_expire(): 90 | db = sqlite3.connect("creds.db") 91 | cursor = db.cursor() 92 | cursor.execute("""SELECT expire FROM creds""") 93 | expire_time = cursor.fetchone()[0] 94 | db.close() 95 | return expire_time 96 | 97 | 98 | def get_phone_number(): 99 | db = sqlite3.connect("creds.db") 100 | cursor = db.cursor() 101 | cursor.execute("""SELECT phone_number FROM creds""") 102 | phone_number = cursor.fetchone()[0] 103 | db.close() 104 | return phone_number 105 | 106 | 107 | def clear_creds(): 108 | db = sqlite3.connect("creds.db") 109 | cursor = db.cursor() 110 | cursor.execute("""DELETE FROM creds""") 111 | db.commit() 112 | db.close() 113 | 114 | 115 | async def jiotv_auth_verify(): 116 | if not path.exists(path.join("data", "jio_headers.json")): 117 | raise JiotvUnauthorizedException(name="--") 118 | else: 119 | pass 120 | 121 | 122 | async def background_refresh_token(): 123 | refreshed_token = await jiotv_obj.refresh_token() 124 | if refreshed_token: 125 | jiotv_obj.update_headers() 126 | logger.info("[*] Session Refreshed.") 127 | 128 | update_expire_time(phone_number=get_phone_number()) 129 | else: 130 | pass 131 | 132 | 133 | @asynccontextmanager 134 | async def lifespan(app: FastAPI): 135 | await background_refresh_token() 136 | schedule = Scheduler() 137 | schedule.cyclic(timedelta(minutes=45), background_refresh_token) 138 | yield 139 | schedule.delete_jobs() 140 | 141 | 142 | router = APIRouter(lifespan=lifespan) 143 | templates = Jinja2Templates(directory="templates/JioTV") 144 | 145 | 146 | @router.get("/") 147 | async def index( 148 | request: Request, 149 | query: Optional[str] = None, 150 | auth_session=Depends(jiotv_auth_verify), 151 | ): 152 | playlist_response = await jiotv_obj.get_playlists(request.headers.get("host")) 153 | channels = convert(playlist_response) 154 | search = False 155 | 156 | if query != "" and query is not None: 157 | channels = [x for x in channels if query.lower() in x["title"].lower()] 158 | search = True 159 | 160 | return templates.TemplateResponse( 161 | "index.html", 162 | {"request": request, "channels": channels, "search": search, "query": query}, 163 | ) 164 | 165 | 166 | @router.get("/player") 167 | async def player( 168 | request: Request, 169 | stream_url, 170 | auth_session=Depends(jiotv_auth_verify), 171 | ): 172 | return templates.TemplateResponse( 173 | "player.html", {"request": request, "stream_url": stream_url} 174 | ) 175 | 176 | 177 | @router.get("/get_otp") 178 | def get_otp(phone_no): 179 | return jiotv_obj.sendOTP(phone_no.replace("+91", "")) 180 | 181 | 182 | @router.get("/createToken") 183 | def createToken(phone_number, otp): 184 | phone_number = phone_number.replace("+91", "") 185 | """ 186 | A function that creates a token for the given phone_number and otp. 187 | 188 | Parameters: 189 | phone_number (str): The phone_number of the user. 190 | otp (str): The otp of the user. 191 | 192 | Returns: 193 | str: The login response containing the token. 194 | """ 195 | if path.exists(path.join("creds.db")): 196 | logger.warning("[!] Creds already available. Clearing existing creds.") 197 | clear_creds() 198 | 199 | else: 200 | logger.info("[-] First Time Logging in.") 201 | 202 | login_response = jiotv_obj.login(phone_number, otp) 203 | if login_response == "[SUCCESS]": 204 | store_creds(phone_number) 205 | jiotv_obj.update_headers() 206 | return login_response 207 | 208 | return login_response 209 | 210 | 211 | @router.get("/login") 212 | async def loginJio(request: Request): 213 | """ 214 | Get the login page. 215 | 216 | Parameters: 217 | request (Request): The HTTP request object. 218 | 219 | Returns: 220 | TemplateResponse: The response containing the login page HTML template. 221 | """ 222 | return templates.TemplateResponse("otp_login.html", {"request": request}) 223 | 224 | 225 | @router.get("/playlist.m3u") 226 | async def get_playlist( 227 | request: Request, 228 | auth_session=Depends(jiotv_auth_verify), 229 | ): 230 | """ 231 | Retrieves a playlist in the form of an m3u file. 232 | 233 | Returns: 234 | A PlainTextResponse object containing the playlist in the specified media type. 235 | """ 236 | playlist_response = await jiotv_obj.get_playlists(request.headers.get("host")) 237 | return PlainTextResponse(playlist_response, media_type="application/x-mpegurl") 238 | 239 | 240 | @router.get("/m3u8") 241 | async def get_m3u8( 242 | cid, 243 | auth_session=Depends(jiotv_auth_verify), 244 | ): 245 | """ 246 | Retrieves the m3u8 playlist for a given channel ID. 247 | 248 | Parameters: 249 | - cid: The ID of the channel to retrieve the m3u8 playlist for. (type: any) 250 | 251 | Returns: 252 | - The m3u8 playlist for the specified channel. (type: PlainTextResponse) 253 | """ 254 | channel_response = await jiotv_obj.get_channel_url(cid) 255 | return PlainTextResponse(channel_response, media_type="application/x-mpegurl") 256 | 257 | 258 | @router.get("/get_audio") 259 | async def get_multi_audio( 260 | uri, 261 | cid, 262 | cookie, 263 | auth_session=Depends(jiotv_auth_verify), 264 | ): 265 | """ 266 | A function that handles the GET request to retrieve audio. 267 | 268 | Parameters: 269 | - uri (str): The URI of the audio. 270 | - cid (str): The CID (Client ID) of the audio. 271 | - cookie (str): The cookie associated with the audio. 272 | 273 | Returns: 274 | - Response: The response object containing the audio. 275 | """ 276 | audio_response = await jiotv_obj.get_audio(uri, cid, cookie) 277 | return Response(audio_response, media_type="application/x-mpegurl") 278 | 279 | 280 | @router.get("/get_subs") 281 | async def get_subtitles( 282 | uri, 283 | cid, 284 | cookie, 285 | auth_session=Depends(jiotv_auth_verify), 286 | ): 287 | """ 288 | Retrieves subtitles for a given URI using the specified CID and cookie. 289 | 290 | Args: 291 | uri (str): The URI for which to retrieve subtitles. 292 | cid (str): The CID associated with the URI. 293 | cookie (str): The cookie required for authentication. 294 | 295 | Returns: 296 | str: The subtitles for the specified URI. 297 | 298 | """ 299 | subs_response = await jiotv_obj.get_subs(uri, cid, cookie) 300 | return Response(subs_response) 301 | 302 | 303 | @router.get("/get_vtt") 304 | async def get_vtt_( 305 | uri, 306 | cid, 307 | cookie, 308 | auth_session=Depends(jiotv_auth_verify), 309 | ): 310 | resp = await jiotv_obj.get_vtt(uri, cid, cookie) 311 | return PlainTextResponse(resp, media_type="text/vtt") 312 | 313 | 314 | @router.get("/get_ts") 315 | async def get_tts( 316 | uri, 317 | cid, 318 | cookie, 319 | auth_session=Depends(jiotv_auth_verify), 320 | ): 321 | """ 322 | A function that handles GET requests to the "/get_ts" endpoint. 323 | 324 | Parameters: 325 | - uri (str): The URI for the request. 326 | - cid (str): The CID for the request. 327 | - cookie (str): The cookie for the request. 328 | 329 | Returns: 330 | - Response: Gets the segments from the specified URI. 331 | """ 332 | tts_response = await jiotv_obj.get_ts(uri, cid, cookie) 333 | return Response(tts_response, media_type="video/MP2T") 334 | 335 | 336 | @router.get("/get_key") 337 | async def get_keys( 338 | uri, 339 | cid, 340 | cookie, 341 | auth_session=Depends(jiotv_auth_verify), 342 | ): 343 | """ 344 | Retrieves a key from the specified URI using the provided CID and cookie. 345 | 346 | Parameters: 347 | - uri (str): The URI from which to retrieve the key. 348 | - cid (str): The CID associated with the key. 349 | - cookie (str): The cookie to use for authentication. 350 | 351 | Returns: 352 | - Response: The response containing the DRM key. 353 | """ 354 | key_response = await jiotv_obj.get_key(uri, cid, cookie) 355 | return Response(key_response, media_type="application/octet-stream") 356 | 357 | 358 | @router.get("/play") 359 | async def play( 360 | uri, 361 | cid, 362 | cookie, 363 | auth_session=Depends(jiotv_auth_verify), 364 | ): 365 | """ 366 | This function is a route handler for the "/play" endpoint of the API. It expects three parameters: 367 | 368 | - `uri`: A string representing the URI. 369 | - `cid`: A string representing the CID. 370 | - `cookie`: A string representing the cookie. 371 | 372 | The function calls the `final_play` function passing in the `uri`, `cid`, and `cookie` parameters, and returns the result as a `PlainTextResponse` object with the media type set to "application/x-mpegurl". 373 | 374 | Returns: 375 | - A `PlainTextResponse` object with the media type set to "application/x-mpegurl". 376 | """ 377 | final_play_response = await jiotv_obj.final_play(uri, cid, cookie) 378 | return PlainTextResponse(final_play_response, media_type="application/x-mpegurl") 379 | -------------------------------------------------------------------------------- /templates/JioSaavn/Play.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | 19 | 24 | 31 | Now Playing - {{song_details.title|safe}} 32 | 33 | 34 | 124 |
125 |
126 |
127 |
128 |

129 | Now Playing 130 |

131 |
132 |
133 |
Track Name
134 |
Track Artist
135 |
136 |
137 |
138 | 139 |
140 |
141 | 142 |
143 |
144 | 145 |
146 |
147 |
148 |
00:00
149 | 157 |
00:00
158 |
159 |
160 | 161 | 169 | 170 |
171 |
172 |
173 | 174 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /templates/JioSaavn/artists_details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 20 | 27 | 28 | {{ artists_details.title }} 29 | 30 | 31 | 32 | 122 | 123 | 124 |
125 |
126 | 134 | 142 |
143 | 182 |
183 | Search For:   184 |
185 | 196 |
197 |
198 | 208 |
209 |
210 | 220 |
221 |
222 | 232 |
233 |
234 |
235 | 236 | 237 |
238 | 239 |
240 |
241 | 247 |
248 |
249 |
250 |

{{ artists_details.title }}

251 |

252 | Total Followers: 253 | {{artists_details.follower_count}} 256 |

257 | 258 |

259 | Total Listeners: 260 | {{artists_details.listeners}} 261 |

262 |
263 |
264 |
265 | 266 | 267 | 295 | 296 | 297 |
298 | 299 |
305 |
    306 | {% for song in artists_details.top_songs.songs %} 307 |
  1. 310 |
    311 |
    {{ song.title }}
    312 | {{ song.more_info.artist }} 313 |
    314 | 315 | 316 | 317 |
  2. 318 | {% endfor %} 319 |
320 |
321 | 322 | 323 |
329 |
    330 | {% for album in artists_details.top_albums.albums %} 331 |
  1. 334 |
    335 |
    {{ album.title }}
    336 | {{ album.year }} 337 |
    338 | 339 | 340 | 341 |
  2. 342 | {% endfor %} 343 |
344 |
345 |
346 |
347 | 348 | 349 | -------------------------------------------------------------------------------- /Modules/JioTV.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha1 2 | from time import time 3 | from random import randint 4 | 5 | import m3u8 6 | from socket import socket, AF_INET, SOCK_DGRAM 7 | import json 8 | from httpx import post, AsyncClient 9 | from os import path 10 | from base64 import b64encode 11 | 12 | from logging import Logger 13 | 14 | # Constants 15 | IMG_PUBLIC = "https://jioimages.cdn.jio.com/imagespublic/" 16 | IMG_CATCHUP = "https://jiotv.catchup.cdn.jio.com/dare_images/images/" 17 | IMG_CATCHUP_SHOWS = "https://jiotv.catchup.cdn.jio.com/dare_images/shows/" 18 | FEATURED_SRC = ( 19 | "https://tv.media.jio.com/apis/v1.6/getdata/featurednew?start=0&limit=30&langId=6" 20 | ) 21 | CHANNELS_SRC_NEW = "https://jiotv.data.cdn.jio.com/apis/v3.0/getMobileChannelList/get/?os=android&devicetype=phone&usertype=tvYR7NSNn7rymo3F&version=285" 22 | GET_CHANNEL_URL = "https://jiotvapi.media.jio.com/playback/apis/v1/geturl?langId=6" 23 | CATCHUP_SRC = "https://jiotv.data.cdn.jio.com/apis/v1.3/getepg/get?offset={0}&channel_id={1}&langId=6" 24 | M3U_CHANNEL = '\n#EXTINF:0 tvg-id="{tvg_id}" tvg-name="{channel_name}" group-title="{group_title}" tvg-chno="{tvg_chno}" tvg-logo="{tvg_logo}"{catchup},{channel_name}\n{play_url}' 25 | DICTIONARY_URL = "https://jiotvapi.cdn.jio.com/apis/v1.3/dictionary/dictionary?langId=6" 26 | 27 | REFRESH_TOKEN_URL = ( 28 | "https://auth.media.jio.com/tokenservice/apis/v1/refreshtoken?langId=6" 29 | ) 30 | # --------------------- 31 | 32 | 33 | android_id = sha1(f"{time()}{randint(0, 99)}".encode()).hexdigest()[:16] 34 | 35 | 36 | class JioTV: 37 | def __init__(self, logger: Logger) -> None: 38 | self.logger = logger 39 | self.channel_headers = {} 40 | self.request_headers = {} 41 | 42 | if path.exists(path.join("data", "jio_headers.json")): 43 | auth_headers = json.load(open(path.join("data", "jio_headers.json"), "r")) 44 | self.request_headers = { 45 | "appkey": "NzNiMDhlYcQyNjJm", 46 | "channel_id": "144", 47 | "channelid": "144", 48 | "crmid": auth_headers["crmid"], 49 | "deviceId": auth_headers["device_id"], 50 | "devicetype": "phone", 51 | "isott": "true", 52 | "languageId": "6", 53 | "lbcookie": "1", 54 | "os": "android", 55 | "osVersion": "14", 56 | "srno": "240707144000", 57 | "ssotoken": auth_headers["ssotoken"], 58 | "subscriberId": auth_headers["subscriberid"], 59 | "uniqueId": auth_headers["uniqueid"], 60 | "appname": "RJIL_JioTV", 61 | "User-Agent": "plaYtv/7.1.3 (Linux;Android 14) ExoPlayerLib/2.11.7", 62 | "usergroup": "tvYR7NSNn7rymo3F", 63 | "versionCode": "331", 64 | } 65 | self.channel_headers = { 66 | "Content-Type": "application/x-www-form-urlencoded", 67 | "appkey": "NzNiMDhlYzQyNjJm", 68 | "userid": auth_headers["userid"], 69 | "crmid": auth_headers["crmid"], 70 | "deviceId": auth_headers["device_id"], 71 | "devicetype": "phone", 72 | "isott": "true", 73 | "languageId": "6", 74 | "lbcookie": "1", 75 | "os": "android", 76 | "dm": "Xiaomi 22101316UP", 77 | "osversion": "14", 78 | "srno": "240303144000", 79 | "accesstoken": auth_headers["accesstoken"], 80 | "subscriberid": auth_headers["subscriberid"], 81 | "uniqueId": auth_headers["uniqueid"], 82 | "usergroup": "tvYR7NSNn7rymo3F", 83 | "User-Agent": "okhttp/4.9.3", 84 | "versionCode": "331", 85 | } 86 | 87 | def update_headers(self): 88 | auth_headers = json.load(open(path.join("data", "jio_headers.json"), "r")) 89 | 90 | self.request_headers = { 91 | "appkey": "NzNiMDhlYcQyNjJm", 92 | "channel_id": "144", 93 | "channelid": "144", 94 | "crmid": auth_headers["crmid"], 95 | "deviceId": auth_headers["device_id"], 96 | "devicetype": "phone", 97 | "isott": "true", 98 | "languageId": "6", 99 | "lbcookie": "1", 100 | "os": "android", 101 | "osVersion": "14", 102 | "srno": "240707144000", 103 | "ssotoken": auth_headers["ssotoken"], 104 | "subscriberId": auth_headers["subscriberid"], 105 | "uniqueId": auth_headers["uniqueid"], 106 | "appname": "RJIL_JioTV", 107 | "User-Agent": "plaYtv/7.1.3 (Linux;Android 14) ExoPlayerLib/2.11.7", 108 | "usergroup": "tvYR7NSNn7rymo3F", 109 | "versionCode": "331", 110 | } 111 | 112 | self.channel_headers = { 113 | "Content-Type": "application/x-www-form-urlencoded", 114 | "appkey": "NzNiMDhlYzQyNjJm", 115 | "userid": auth_headers["userid"], 116 | "crmid": auth_headers["crmid"], 117 | "deviceId": auth_headers["device_id"], 118 | "devicetype": "phone", 119 | "isott": "true", 120 | "languageId": "6", 121 | "lbcookie": "1", 122 | "os": "android", 123 | "dm": "Xiaomi 22101316UP", 124 | "osversion": "14", 125 | "srno": "240303144000", 126 | "accesstoken": auth_headers["accesstoken"], 127 | "subscriberid": auth_headers["subscriberid"], 128 | "uniqueId": auth_headers["uniqueid"], 129 | "usergroup": "tvYR7NSNn7rymo3F", 130 | "User-Agent": "okhttp/4.9.3", 131 | "versionCode": "331", 132 | } 133 | 134 | def get_local_ip(self): 135 | """ 136 | Retrieves the local IP address of the machine. 137 | 138 | Returns: 139 | str: The local IP address of the machine. 140 | """ 141 | 142 | s = socket(AF_INET, SOCK_DGRAM) 143 | s.settimeout(0) 144 | try: 145 | s.connect(("8.8.8.8", 80)) 146 | IP = s.getsockname()[0] 147 | except Exception: 148 | IP = "127.0.0.1" 149 | finally: 150 | s.close() 151 | return IP 152 | 153 | async def get_channels(self): 154 | """ 155 | Retrieves channels from the specified source and saves them to a JSON file. 156 | 157 | This function sends a GET request to the CHANNELS_SRC_NEW endpoint with the specified headers. It retrieves the JSON response and extracts the 'result' field. The extracted data is then saved to a JSON file located at 'data/channels.json'. 158 | 159 | Parameters: 160 | None 161 | 162 | Returns: 163 | None 164 | """ 165 | 166 | response = await AsyncClient().get( 167 | CHANNELS_SRC_NEW, headers=self.channel_headers 168 | ) 169 | response = response.json()["result"] 170 | with open(r"data\channels.json", "w") as f: 171 | json.dump(response, f, ensure_ascii=False, indent=4) 172 | 173 | def sendOTP(self, mobile): 174 | 175 | base64_encoded_phone_number = b64encode(f"+91{mobile}".encode("ascii")).decode( 176 | "ascii" 177 | ) 178 | 179 | url = "https://jiotvapi.media.jio.com/userservice/apis/v1/loginotp/send" 180 | json_body = { 181 | "number": base64_encoded_phone_number, 182 | } 183 | headers = { 184 | "appname": "RJIL_JioTV", 185 | "os": "android", 186 | "devicetype": "phone", 187 | "content-type": "application/json", 188 | "user-agent": "okhttp/3.14.9", 189 | } 190 | 191 | resp = post(url=url, json=json_body, headers=headers) 192 | 193 | if resp.status_code == 204: 194 | return "[SUCCESS]" 195 | 196 | else: 197 | return "[FAILED]" 198 | 199 | def login(self, phone_number, otp): 200 | """ 201 | Logs in a user with the given email and password. 202 | 203 | Args: 204 | email (str): The email address of the user. 205 | password (str): The password of the user. 206 | 207 | Returns: 208 | str: The success or failure message. 209 | """ 210 | 211 | base64_encoded_phone_number = b64encode( 212 | f"+91{phone_number}".encode("ascii") 213 | ).decode("ascii") 214 | 215 | url = "https://jiotvapi.media.jio.com/userservice/apis/v1/loginotp/verify" 216 | headers = { 217 | "appname": "RJIL_JioTV", 218 | "os": "android", 219 | "devicetype": "phone", 220 | "content-type": "application/json", 221 | "user-agent": "okhttp/3.14.9", 222 | } 223 | 224 | json_body = { 225 | "number": base64_encoded_phone_number, 226 | "otp": otp, 227 | "deviceInfo": { 228 | "consumptionDeviceName": "RMX1945", 229 | "info": { 230 | "type": "android", 231 | "platform": {"name": "RMX1945"}, 232 | "androidId": android_id, 233 | }, 234 | }, 235 | } 236 | 237 | resp = post(url=url, json=json_body, headers=headers).json() 238 | 239 | if resp.get("ssoToken", "") != "": 240 | _CREDS = { 241 | "accesstoken": resp.get("authToken"), 242 | "refresh_token": resp.get("refreshToken"), 243 | "ssotoken": resp.get("ssoToken"), 244 | "jToken": resp.get("jToken"), 245 | "userid": resp.get("sessionAttributes", {}).get("user", {}).get("uid"), 246 | "uniqueid": resp.get("sessionAttributes", {}) 247 | .get("user", {}) 248 | .get("unique"), 249 | "crmid": resp.get("sessionAttributes", {}) 250 | .get("user", {}) 251 | .get("subscriberId"), 252 | "subscriberid": resp.get("sessionAttributes", {}) 253 | .get("user", {}) 254 | .get("subscriberId"), 255 | "device_id": resp.get("deviceId"), 256 | } 257 | 258 | headers = { 259 | "deviceId": resp.get("deviceId"), 260 | "devicetype": "phone", 261 | "os": "android", 262 | "osversion": "9", 263 | "user-agent": "plaYtv/7.0.8 (Linux;Android 9) ExoPlayerLib/2.11.7", 264 | "usergroup": "tvYR7NSNn7rymo3F", 265 | "versioncode": "289", 266 | "dm": "ZUK ZUK Z1", 267 | } 268 | 269 | headers.update(_CREDS) 270 | 271 | with open("data/jio_headers.json", "w") as f: 272 | json.dump(headers, f, indent=4) 273 | 274 | return "[SUCCESS]" 275 | else: 276 | return "[FAILED]" 277 | 278 | async def refresh_token(self): 279 | if path.exists(path.join("data", "jio_headers.json")): 280 | with open(path.join("data", "jio_headers.json"), "r") as auth_file: 281 | auth_headers = json.load(auth_file) 282 | 283 | post_body = { 284 | "appName": "RJIL_JioTV", 285 | "deviceId": auth_headers["deviceId"], 286 | "refreshToken": auth_headers["refresh_token"], 287 | } 288 | refresh_token_headers = { 289 | "accesstoken": auth_headers["accesstoken"], 290 | "devicetype": "phone", 291 | "versionCode": "315", 292 | "os": "android", 293 | "Content-Type": "application/json; charset=utf-8", 294 | "Host": "auth.media.jio.com", 295 | "Accept-Encoding": "gzip", 296 | "User-Agent": "okhttp/4.2.2", 297 | } 298 | 299 | async with AsyncClient() as async_client: 300 | resp = await async_client.post( 301 | REFRESH_TOKEN_URL, 302 | headers=refresh_token_headers, 303 | json=post_body, 304 | ) 305 | 306 | resp = resp.json() 307 | 308 | if resp.get("authToken"): 309 | auth_headers["accesstoken"] = resp.get("authToken") 310 | with open(path.join("data", "jio_headers.json"), "w") as f: 311 | json.dump(auth_headers, f, indent=4) 312 | 313 | return True 314 | else: 315 | return False 316 | else: 317 | return False 318 | 319 | async def get_key(self, uri, cid, cookie): 320 | """ 321 | Retrieves a key from the specified URI using the provided channel ID and cookie. 322 | 323 | Parameters: 324 | uri (str): The URI from which to retrieve the key. 325 | cid (int): The channel ID to use in the request headers. 326 | cookie (str): The cookie to include in the request headers. 327 | 328 | Returns: 329 | bytes: The retrieved key as bytes. 330 | """ 331 | headers = self.request_headers 332 | headers["channelid"] = str(cid) 333 | headers["srno"] = "240707144000" 334 | headers["cookie"] = cookie 335 | headers["Content-type"] = "application/octet-stream" 336 | 337 | resp = await AsyncClient().get( 338 | uri, 339 | headers=headers, 340 | ) 341 | resp = resp.content 342 | 343 | return resp 344 | 345 | async def get_ts(self, uri, cid, cookie): 346 | """ 347 | Retrieves the content from a given URI using the GET method and returns the response content. 348 | 349 | Args: 350 | uri (str): The URI to retrieve content from. 351 | cid (int): The channel ID. 352 | cookie (str): The cookie value. 353 | 354 | Returns: 355 | bytes: The response content. 356 | """ 357 | headers = self.request_headers 358 | headers["channelid"] = str(cid) 359 | headers["srno"] = "240707144000" 360 | headers["cookie"] = cookie 361 | 362 | resp = await AsyncClient().get( 363 | uri, 364 | headers=headers, 365 | ) 366 | return resp.content 367 | 368 | async def get_audio(self, uri, cid, cookie): 369 | """ 370 | Generate the audio m3u8 playlist with modified URLs. 371 | 372 | Args: 373 | uri (str): The URI of the original m3u8 playlist. 374 | cid (str): The cid value for the request. 375 | cookie (str): The cookie value for the request. 376 | 377 | Returns: 378 | str: The modified audio m3u8 playlist. 379 | """ 380 | base_url = uri.split("/") 381 | base_url.pop() 382 | base_url = "/".join(base_url) 383 | 384 | audio_m3u8 = await self.get_ts(uri, cid, cookie) 385 | audio_m3u8 = audio_m3u8.decode() 386 | 387 | parsed_audio_m3u8 = m3u8.loads(audio_m3u8) 388 | 389 | for segment in parsed_audio_m3u8.segments: 390 | audio_m3u8 = audio_m3u8.replace( 391 | segment.uri, 392 | f"/jiotv/get_ts?uri={base_url}/{segment.uri}&cid={cid}&cookie={cookie}", 393 | ) 394 | 395 | for key in parsed_audio_m3u8.keys: 396 | if key is not None: 397 | audio_m3u8 = audio_m3u8.replace( 398 | key.uri, f"/jiotv/get_key?uri={key.uri}&cid={cid}&cookie={cookie}" 399 | ) 400 | 401 | return audio_m3u8 402 | 403 | async def get_vtt(self, uri, cid, cookie): 404 | resp = await self.get_ts(uri, cid, cookie) 405 | resp = resp.decode() 406 | return resp 407 | 408 | async def get_subs(self, uri, cid, cookie): 409 | """ 410 | Retrieves the subdomains for a given URI. 411 | 412 | Parameters: 413 | uri (str): The URI for which to retrieve the subdomains. 414 | cid (str): The client ID associated with the request. 415 | cookie (str): The authentication cookie. 416 | 417 | Returns: 418 | str: Gets subtitles URI. 419 | """ 420 | base_url = uri.split("/") 421 | base_url.pop() 422 | base_url = "/".join(base_url) 423 | 424 | resp = await self.get_ts(uri, cid, cookie) 425 | resp = resp.decode() 426 | parsed_subs = m3u8.loads(resp) 427 | 428 | for segment in parsed_subs.segments: 429 | resp = resp.replace( 430 | segment.uri, 431 | f"/jiotv/get_vtt?uri={base_url}/{segment.uri.replace('.webvtt','.vtt')}&cid={cid}&cookie={cookie}", 432 | ) 433 | 434 | return resp 435 | 436 | async def get_channel_url(self, channel_id): 437 | """ 438 | Retrieves the URL of a channel based on its ID. 439 | 440 | Args: 441 | channel_id (int): The ID of the channel. 442 | 443 | Returns: 444 | str: The URL of the channel with various query parameters appended. 445 | 446 | Raises: 447 | None: Does not raise any exceptions. 448 | """ 449 | rjson = {"channel_id": int(channel_id), "stream_type": "Seek"} 450 | resp = await AsyncClient().post( 451 | GET_CHANNEL_URL, 452 | headers=self.channel_headers, 453 | data=rjson, 454 | ) 455 | 456 | resp = resp.json() 457 | onlyUrl = resp.get("bitrates", "").get("high", "") 458 | 459 | base_url = onlyUrl.split("?")[0].split("/") 460 | base_url.pop() 461 | base_url = "/".join(base_url) 462 | 463 | cookie = "__hdnea__" + resp.get("result", "").split("__hdnea__")[-1] 464 | first_m3u8 = await AsyncClient().get(onlyUrl, headers=self.channel_headers) 465 | first_m3u8 = first_m3u8.text 466 | 467 | firast_m3u8_parsed = m3u8.loads(first_m3u8) 468 | 469 | final_ = first_m3u8 470 | for playlist in firast_m3u8_parsed.playlists: 471 | final_ = final_.replace( 472 | playlist.uri, 473 | f"/jiotv/play?uri={base_url}/{playlist.uri}&cid={channel_id}&cookie={cookie}", 474 | ) 475 | 476 | for media in firast_m3u8_parsed.media: 477 | if media.type == "SUBTITLES": 478 | final_ = final_.replace( 479 | media.uri, 480 | f"/jiotv/get_subs?uri={base_url}/{media.uri}&cid={channel_id}&cookie={cookie}", 481 | ) 482 | 483 | for media in firast_m3u8_parsed.media: 484 | if media.type == "AUDIO" and media.uri is not None: 485 | final_ = final_.replace( 486 | media.uri, 487 | f"/jiotv/get_audio?uri={base_url}/{media.uri}&cid={channel_id}&cookie={cookie}", 488 | ) 489 | return final_ 490 | 491 | async def final_play(self, uri, cid, cookie): 492 | """ 493 | Fetches and processes a playlist file from the given URI. 494 | 495 | Args: 496 | uri (str): The URI of the playlist file. 497 | cid (str): The channel ID. 498 | cookie (str): The cookie value. 499 | 500 | Returns: 501 | str: The processed playlist file. 502 | 503 | Raises: 504 | None 505 | """ 506 | headers = self.request_headers 507 | headers["channelid"] = str(cid) 508 | headers["srno"] = "240707144000" 509 | headers["cookie"] = cookie 510 | 511 | resp = await AsyncClient().get( 512 | uri, 513 | headers=headers, 514 | ) 515 | resp = resp.text 516 | 517 | parsed_m3u8 = m3u8.loads(resp) 518 | 519 | base_url = uri.split("/") 520 | base_url.pop() 521 | base_url = "/".join(base_url) 522 | 523 | temp_text = resp 524 | 525 | for segment in parsed_m3u8.segments: 526 | temp_text = temp_text.replace( 527 | segment.uri, 528 | f"/jiotv/get_ts?uri={base_url}/{segment.uri}&cid={cid}&cookie={cookie}", 529 | ) 530 | 531 | for key in parsed_m3u8.keys: 532 | if key is not None: 533 | temp_text = temp_text.replace( 534 | key.uri, f"/jiotv/get_key?uri={key.uri}&cid={cid}&cookie={cookie}" 535 | ) 536 | 537 | return temp_text 538 | 539 | async def get_playlists(self, host): 540 | response = await AsyncClient().get( 541 | CHANNELS_SRC_NEW, headers=self.channel_headers 542 | ) 543 | response = response.json() 544 | 545 | channels = response["result"] 546 | 547 | lang_id = { 548 | 6: "English", 549 | 1: "Hindi", 550 | 2: "Marathi", 551 | 3: "Punjabi", 552 | 4: "Urdu", 553 | 5: "Bengali", 554 | 7: "Malayalam", 555 | 8: "Tamil", 556 | 9: "Gujarati", 557 | 10: "Odia", 558 | 11: "Telugu", 559 | 12: "Bhojpuri", 560 | 13: "Kannada", 561 | 14: "Assamese", 562 | 15: "Nepali", 563 | 16: "French", 564 | } 565 | 566 | genre_id = { 567 | 8: "Sports", 568 | 5: "Entertainment", 569 | 6: "Movies", 570 | 12: "News", 571 | 13: "Music", 572 | 7: "Kids", 573 | 9: "Lifestyle", 574 | 10: "Infotainment", 575 | 15: "Devotional", 576 | 0x10: "Business", 577 | 17: "Educational", 578 | 18: "Shopping", 579 | 19: "JioDarshan", 580 | } 581 | 582 | m3u8 = """#EXTM3U\n""" 583 | 584 | for channel in channels: 585 | channel_id = channel["channel_id"] 586 | channel_logo = ( 587 | "http://jiotv.catchup.cdn.jio.com/dare_images/images/" 588 | + channel["logoUrl"] 589 | ) 590 | channel_name = channel["channel_name"] 591 | channel_genre = genre_id[channel["channelCategoryId"]] 592 | m3u8 += f'#EXTINF:-1 tvg-id="{channel_id}" group-title="{channel_genre}" tvg-logo="{channel_logo}",{channel_name}\nhttp://{host}/jiotv/m3u8?cid={channel_id}\n' 593 | 594 | return m3u8.strip() 595 | --------------------------------------------------------------------------------