Download server.js from Deep7477474/stremio-telegram-addon: direct link, hf CLI and curl.
- Browser
- Download file 13.2 kB
-
https://huggingface.co/spaces/Deep7477474/stremio-telegram-addon/resolve/main/server.js
- Command line
-
hf download hf://spaces/Deep7477474/stremio-telegram-addon/server.js
-
curl -L -o server.js https://huggingface.co/spaces/Deep7477474/stremio-telegram-addon/resolve/main/server.js
13.2 kB
| 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(`<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>JioSaavn Stremio Addon</title> | |
| <style> | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; | |
| background: #0e1017; | |
| color: #fff; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| min-height: 100vh; | |
| margin: 0; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| text-align: center; | |
| } | |
| .card { | |
| background: #1a1d26; | |
| border: 1px solid #2d313e; | |
| border-radius: 16px; | |
| padding: 32px; | |
| max-width: 520px; | |
| width: 100%; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.4); | |
| } | |
| h1 { margin-top: 0; font-size: 26px; color: #00d26a; } | |
| p { color: #a0a6b5; line-height: 1.5; font-size: 15px; } | |
| .catalogs { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 6px; | |
| justify-content: center; | |
| margin: 16px 0; | |
| } | |
| .badge { | |
| background: #252a38; | |
| border: 1px solid #363d50; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 12px; | |
| color: #c9d1d9; | |
| } | |
| .btn { | |
| display: inline-block; | |
| background: #00d26a; | |
| color: #000; | |
| font-weight: bold; | |
| padding: 14px 28px; | |
| border-radius: 10px; | |
| text-decoration: none; | |
| font-size: 16px; | |
| margin-top: 16px; | |
| transition: background 0.2s; | |
| } | |
| .btn:hover { background: #00b85c; } | |
| .url-box { | |
| margin-top: 20px; | |
| background: #11131a; | |
| border: 1px solid #282c38; | |
| border-radius: 8px; | |
| padding: 12px; | |
| font-family: monospace; | |
| font-size: 13px; | |
| word-break: break-all; | |
| color: #798399; | |
| user-select: all; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>π΅ JioSaavn Stremio Addon</h1> | |
| <p>Stream high-quality 320kbps music with 9 curated catalogs:</p> | |
| <div class="catalogs"> | |
| <span class="badge">π₯ Trending</span> | |
| <span class="badge">π¬ Bollywood</span> | |
| <span class="badge">π Global Pop</span> | |
| <span class="badge">π₯ Punjabi</span> | |
| <span class="badge">β€οΈ Romantic</span> | |
| <span class="badge">π South Indian</span> | |
| <span class="badge">β Lo-Fi</span> | |
| <span class="badge">π» 90s Retro</span> | |
| <span class="badge">π Party Dance</span> | |
| </div> | |
| <a class="btn" href="${stremioInstallUrl}">Install in Stremio</a> | |
| <div class="url-box">${manifestUrl}</div> | |
| <p style="font-size: 12px; margin-top: 14px;">Or copy the URL above and paste it into Stremio's Addon search bar.</p> | |
| </div> | |
| </body> | |
| </html>`); | |
| }); | |
| // 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}`); | |
| }); | |