const express = require('express');
const cors = require('cors');
const CryptoJS = require('crypto-js');
const app = express();
const PORT = process.env.PORT || 7860;
app.use(cors());
app.use(express.json());
const DES_KEY = CryptoJS.enc.Utf8.parse('38346591');
// Decode HTML entities
function decodeEntities(str) {
if (!str) return '';
return str
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, "'")
.replace(/ /g, ' ')
.trim();
}
// Decrypt DES-ECB media URL
function decryptMediaUrl(encryptedUrl) {
try {
const decrypted = CryptoJS.DES.decrypt(
{ ciphertext: CryptoJS.enc.Base64.parse(encryptedUrl) },
DES_KEY,
{
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
}
);
return decrypted.toString(CryptoJS.enc.Utf8);
} catch (err) {
console.error('Decryption failed:', err.message);
return null;
}
}
// Stremio Catalogs Mapping
const CATALOG_DEFAULTS = {
'jiosaavn_trending': 'Top Trending Hits',
'jiosaavn_bollywood': 'Latest Bollywood Hindi',
'jiosaavn_english': 'International Pop Hits',
'jiosaavn_punjabi': 'Top Punjabi Hits',
'jiosaavn_romantic': 'Best of Arijit Singh Romantic Hits',
'jiosaavn_south': 'South Indian Hits Telugu Tamil',
'jiosaavn_lofi': 'Hindi Lo-Fi Chill',
'jiosaavn_retro': '90s Evergreen Bollywood Hits',
'jiosaavn_party': 'Party Dance Club Hits'
};
const STREMIO_CATALOGS = [
{
type: 'music',
id: 'jiosaavn_trending',
name: '🔥 Trending Hits',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_bollywood',
name: '🎬 Bollywood & Hindi',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_english',
name: '🌍 Global Pop & English',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_punjabi',
name: '🥁 Punjabi Hits',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_romantic',
name: '❤️ Romantic Melodies',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_south',
name: '🌟 South Hits (Tamil/Telugu)',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_lofi',
name: '☕ Lo-Fi & Chill',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_retro',
name: '📻 90s Evergreen Classics',
extra: [{ name: 'search' }, { name: 'skip' }]
},
{
type: 'music',
id: 'jiosaavn_party',
name: '🎉 Party & Dance',
extra: [{ name: 'search' }, { name: 'skip' }]
}
];
// Stremio Addon Manifest
const STREMIO_MANIFEST = {
id: 'community.jiosaavn.music',
version: '1.1.0',
name: 'JioSaavn Music',
description: 'Stream 320kbps music with 9 curated catalogs (Bollywood, Global Pop, Punjabi, Romantic, South, Lo-Fi, Retro & Party).',
logo: 'https://c.saavncdn.com/871/Brahmastra-Original-Motion-Picture-Soundtrack-Hindi-2022-20221006155213-500x500.jpg',
background: 'https://c.saavncdn.com/871/Brahmastra-Original-Motion-Picture-Soundtrack-Hindi-2022-20221006155213-500x500.jpg',
resources: ['catalog', 'meta', 'stream'],
types: ['music', 'other'],
catalogs: STREMIO_CATALOGS,
idPrefixes: ['saavn:']
};
// 1. Landing Page with 1-Click Install Button
app.get('/', (req, res) => {
const host = req.get('host');
const proto = req.headers['x-forwarded-proto'] || req.protocol;
const manifestUrl = `${proto}://${host}/manifest.json`;
const stremioInstallUrl = `stremio://${host}/manifest.json`;
res.send(`
JioSaavn Stremio Addon
🎵 JioSaavn Stremio Addon
Stream high-quality 320kbps music with 9 curated catalogs:
🔥 Trending
🎬 Bollywood
🌍 Global Pop
🥁 Punjabi
❤️ Romantic
🌟 South Indian
☕ Lo-Fi
📻 90s Retro
🎉 Party Dance
Install in Stremio
${manifestUrl}
Or copy the URL above and paste it into Stremio's Addon search bar.
`);
});
// 2. Manifest endpoint
app.get('/manifest.json', (req, res) => {
res.json(STREMIO_MANIFEST);
});
// Helper to parse Stremio extra params (search=..., skip=...)
function parseExtra(extraStr) {
const extra = {};
if (!extraStr) return extra;
const clean = extraStr.replace(/\.json$/, '');
const pairs = clean.split('&');
for (const pair of pairs) {
const idx = pair.indexOf('=');
if (idx !== -1) {
const key = pair.slice(0, idx);
const val = decodeURIComponent(pair.slice(idx + 1));
extra[key] = val;
}
}
return extra;
}
// Query JioSaavn catalog
async function searchJioSaavn(query, page = 0) {
const headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json'
};
const searchQuery = query && query.trim() ? query.trim() : 'Hindi Trending';
const [autoResp, searchResp] = await Promise.allSettled([
page === 0
? fetch(`https://www.jiosaavn.com/api.php?_format=json&_marker=0&ctx=wap6dot0&__call=autocomplete.get&query=${encodeURIComponent(searchQuery)}`, { headers })
: Promise.resolve({ ok: false }),
fetch(`https://www.jiosaavn.com/api.php?_format=json&_marker=0&ctx=wap6dot0&__call=search.getResults&q=${encodeURIComponent(searchQuery)}&p=${page}&n=20`, { headers })
]);
const candidateMap = new Map();
if (searchResp.status === 'fulfilled' && searchResp.value.ok) {
const data = await searchResp.value.json().catch(() => ({}));
for (const item of (data.results || [])) {
if (!item.id || candidateMap.has(item.id)) continue;
candidateMap.set(item.id, {
id: `saavn:${item.id}`,
type: 'music',
name: decodeEntities(item.song),
poster: item.image ? item.image.replace(/150x150\.jpg$/, '500x500.jpg') : null,
description: `Artist: ${decodeEntities(item.primary_artists || item.singers || '')}\nAlbum: ${decodeEntities(item.album || '')}\nDuration: ${Math.floor((item.duration || 0) / 60)}:${String((item.duration || 0) % 60).padStart(2, '0')}`,
genres: [item.language || 'Music']
});
}
}
if (autoResp.status === 'fulfilled' && autoResp.value.ok) {
const autoData = await autoResp.value.json().catch(() => ({}));
for (const item of (autoData.songs?.data || [])) {
if (!item.id || candidateMap.has(item.id)) continue;
candidateMap.set(item.id, {
id: `saavn:${item.id}`,
type: 'music',
name: decodeEntities(item.title),
poster: item.image ? item.image.replace(/150x150\.jpg$/, '500x500.jpg') : null,
description: `Artist: ${decodeEntities(item.more_info?.primary_artists || '')}\nAlbum: ${decodeEntities(item.more_info?.album || '')}`,
genres: ['Music']
});
}
}
return Array.from(candidateMap.values());
}
// 3. Stremio Catalog Handler with full pagination & multi-catalog support
app.get('/catalog/:type/:id/:extra?.json', async (req, res) => {
const { type, id, extra: extraParam } = req.params;
const extra = parseExtra(extraParam);
const search = extra.search || '';
const skip = parseInt(extra.skip, 10) || 0;
const page = Math.floor(skip / 20);
// If search query is present, use it; otherwise use the catalog's curated query
const query = search || CATALOG_DEFAULTS[id] || 'Top Trending Hits';
console.log(`[Stremio] Catalog: id=${id}, query="${query}", page=${page} (skip=${skip})`);
try {
const metas = await searchJioSaavn(query, page);
res.json({ metas });
} catch (err) {
console.error('[Stremio] Catalog error:', err.message);
res.json({ metas: [] });
}
});
// 4. Stremio Meta Handler
app.get('/meta/:type/:id.json', async (req, res) => {
const { type, id } = req.params;
const songId = id.replace(/^saavn:/, '');
console.log(`[Stremio] Meta request for: ${id} (raw: ${songId})`);
try {
const response = await fetch(`https://www.jiosaavn.com/api.php?_format=json&_marker=0&ctx=wap6dot0&__call=song.getDetails&pids=${encodeURIComponent(songId)}`, {
headers: { 'User-Agent': 'Mozilla/5.0' }
});
const data = await response.json();
const song = (data.songs && data.songs[0]) || data[songId];
if (!song) {
return res.status(404).json({ meta: null });
}
const poster = song.image ? song.image.replace(/150x150\.jpg$/, '500x500.jpg') : null;
const artists = decodeEntities(song.primary_artists || song.singers || '');
const album = decodeEntities(song.album || '');
const title = decodeEntities(song.song || '');
res.json({
meta: {
id: `saavn:${song.id}`,
type: type || 'music',
name: title,
poster: poster,
background: poster,
logo: poster,
description: `Track: ${title}\nArtist: ${artists}\nAlbum: ${album}\nYear: ${song.year || ''}\nLabel: ${song.label || ''}`,
releaseInfo: song.year ? String(song.year) : undefined,
genres: [song.language || 'Music', 'Audio'],
director: artists ? [artists] : []
}
});
} catch (err) {
console.error('[Stremio] Meta error:', err.message);
res.status(500).json({ meta: null });
}
});
// 5. Stremio Stream Handler
app.get('/stream/:type/:id.json', async (req, res) => {
const { type, id } = req.params;
const songId = id.replace(/^saavn:/, '');
console.log(`[Stremio] Stream request for: ${id} (raw: ${songId})`);
try {
const response = await fetch(`https://www.jiosaavn.com/api.php?_format=json&_marker=0&ctx=wap6dot0&__call=song.getDetails&pids=${encodeURIComponent(songId)}`, {
headers: { 'User-Agent': 'Mozilla/5.0' }
});
const data = await response.json();
const song = (data.songs && data.songs[0]) || data[songId];
if (!song || !song.encrypted_media_url) {
return res.status(404).json({ streams: [] });
}
const decrypted = decryptMediaUrl(song.encrypted_media_url);
if (!decrypted) {
return res.status(500).json({ streams: [] });
}
const url320 = decrypted.replace('_96', '_320').replace('_160', '_320');
const url160 = decrypted.replace('_96', '_160').replace('_320', '_160');
res.json({
streams: [
{
name: 'JioSaavn [HQ]',
title: `${decodeEntities(song.song)}\n320kbps High Quality AAC`,
url: url320
},
{
name: 'JioSaavn [SQ]',
title: `${decodeEntities(song.song)}\n160kbps Standard AAC`,
url: url160
}
]
});
} catch (err) {
console.error('[Stremio] Stream error:', err.message);
res.status(500).json({ streams: [] });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`[JioSaavn Stremio Addon] Server is listening on http://0.0.0.0:${PORT}`);
});