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}`); });