├── 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 | 
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 |
71 | {% if channels|length == 0 %}
72 |
73 |
74 |
75 |
No Results Found!
76 |
No TV channels found for {{query}}!.
77 |
78 |
79 |
80 | {% endif %}
81 |
82 | {% for channel in channels %}
83 |
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 |
99 |
JIO TV
Login Page
100 |
Login To JIO TV to start watching Live TV.
101 |
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 |
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 |
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 | -
266 |
267 |
{{ song.title }}
268 | {{ ", ".join(song.artist) }}
269 |
270 |
273 |
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 |
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 | -
267 |
268 |
{{ song.title }}
269 | {{ song.artist }}
270 |
271 |
274 |
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 |
230 |
231 | Search Results for {{ query }}
232 |
233 |
234 |
235 | {% for result in results %}
236 |
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 |
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 |
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 | -
310 |
311 |
{{ song.title }}
312 | {{ song.more_info.artist }}
313 |
314 |
315 |
316 |
317 |
318 | {% endfor %}
319 |
320 |
321 |
322 |
323 |
329 |
330 | {% for album in artists_details.top_albums.albums %}
331 | -
334 |
335 |
{{ album.title }}
336 | {{ album.year }}
337 |
338 |
339 |
340 |
341 |
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 |
--------------------------------------------------------------------------------