'use strict'; const express = require('express'); const axios = require('axios'); const app = express(); const PORT = process.env.PORT || 7860; app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); res.header('Access-Control-Allow-Headers', '*'); res.header('Access-Control-Allow-Methods', 'GET, OPTIONS'); if (req.method === 'OPTIONS') return res.sendStatus(200); next(); }); const TMDB_KEY = '83d364331c40bfbe29858aeed82f45cc'; const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'; const TORRENTCLAW_API = 'https://torrentclaw.com/api/stremio'; const TORRENTIO_BASE = 'https://torrentio.strem.fun'; const MAX_SIZE_GB_MOVIE = 4; const MAX_SIZE_GB_SERIES = 1.5; const LINKS_PER_QUALITY = 5; const ALLOWED_QUALITIES = ['4k', '1080p', '720p', '576p', '480p', 'webrip']; const QUALITY_RANK = { '1080p': 1, '720p': 2, '576p': 3, '480p': 4, '4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6, }; const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw']; const KNOWN_PROVIDERS = [ 'YTS', 'EZTV', 'Knaben', 'Torrentio', 'Bitmagnet', 'Prowlarr', 'DonTorrent', 'Torrents.csv', '1337x', 'TorrentGalaxy', 'RARBG', 'ThePirateBay', 'BitSearch', 'LimeTorrents', 'Nyaa', 'KickassTorrents', ]; const TR = [ 'udp://tracker.opentrackr.org:1337/announce', 'udp://open.stealth.si:80/announce', 'udp://open.demonii.com:1337/announce', 'udp://tracker.torrent.eu.org:451/announce', 'udp://p4p.arenabg.com:1337/announce', 'udp://exodus.desync.com:6969/announce', 'udp://tracker.openbittorrent.com:6969/announce', 'udp://tracker.dler.org:6969/announce', 'https://tracker.moeblog.cn:443/announce', 'https://tracker.zhuqiy.com:443/announce', ].map(t => '&tr=' + encodeURIComponent(t)).join(''); function buildMagnet(hash, name) { return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR; } function getQuality(str = '') { const s = str.toLowerCase(); if (s.includes('4k') || s.includes('2160p') || s.includes('uhd')) return '4k'; if (s.includes('1080p')) return '1080p'; if (s.includes('720p')) return '720p'; if (s.includes('576p')) return '576p'; if (s.includes('480p')) return '480p'; if (s.includes('webrip')) return 'webrip'; if (s.includes('webdl') || s.includes('web-dl')) return 'webdl'; return null; } function getSizeGB(stream) { const raw = stream.title || stream.name || ''; const m = raw.match(/💾\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i); if (!m) return null; const val = parseFloat(m[1]); return m[2].toUpperCase() === 'GB' ? val : val / 1024; } function formatSize(sizeGB) { if (sizeGB === null || sizeGB === undefined || isNaN(sizeGB)) return 'Unknown Size'; if (sizeGB < 1) return Math.round(sizeGB * 1024) + 'MB'; return sizeGB.toFixed(1) + 'GB'; } function getSeeders(stream) { if (stream._seeders != null) return stream._seeders; if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders; const m = (stream.title || '').match(/🌱\s*(\d+)/) || (stream.title || '').match(/👤\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; } function getLangLine(str = '') { const lower = str.toLowerCase(); const found = []; const checks = [ [/\benglish\b/, 'English'], [/\bjapanese\b/, 'Japanese'], [/\bhindi\b/, 'Hindi'], [/\bfrench\b/, 'French'], [/\bgerman\b/, 'German'], [/\bspanish\b/, 'Spanish'], [/\bitalian\b/, 'Italian'], [/\brussian\b/, 'Russian'], [/\bkorean\b/, 'Korean'], [/\bchinese\b/, 'Chinese'], [/\barabic\b/, 'Arabic'], [/\bportuguese\b/, 'Portuguese'], [/\bturkish\b/, 'Turkish'], [/\bpolish\b/, 'Polish'], [/\bdutch\b/, 'Dutch'], [/\bczech\b/, 'Czech'], [/\bswedish\b/, 'Swedish'], [/\bnorwegian\b/, 'Norwegian'], [/\bdanish\b/, 'Danish'], [/\bfinnish\b/, 'Finnish'], [/\bromanian\b/, 'Romanian'], [/\bgreek\b/, 'Greek'], [/\bhebrew\b/, 'Hebrew'], [/\bthai\b/, 'Thai'], [/\bindonesian\b/, 'Indonesian'], [/\bvietnamese\b/, 'Vietnamese'], [/\btamil\b/, 'Tamil'], [/\btelugu\b/, 'Telugu'], [/\burdu\b/, 'Urdu'], [/\bdubbed\b/, 'Dubbed'], [/\bdual[\s\-]audio\b/, 'Dual Audio'], [/\bmulti[\s\-]audio\b/, 'Multi Audio'], [/\bmulti\b/, 'Multi'], ]; for (const [re, label] of checks) { if (re.test(lower)) { if (label === 'Multi' && found.includes('Multi Audio')) continue; found.push(label); } } return [...new Set(found)].join(' / '); } function detectProvider(combined, item, idx) { const upper = combined.toUpperCase(); if (item && item.provider) return String(item.provider); if (item && item.source) return String(item.source); if (item && item.indexer) return String(item.indexer); for (const provider of KNOWN_PROVIDERS) { if (upper.includes(provider.toUpperCase())) return provider; } return KNOWN_PROVIDERS[idx % KNOWN_PROVIDERS.length]; } function qualityEmoji(quality) { switch (quality) { case '4k': case '2160p': return '🔥'; case '1080p': return '🌟'; case '720p': return '💎'; case '576p': case '480p': return '📱'; case 'webrip': case 'webdl': return '🌐'; default: return '🔥'; } } function buildUnifiedTitle({ isSeries, title, year, season, episode, quality, tagLine, seeders, sizeStr, provider }) { const header = isSeries ? `📺 ${title} | S${season || 1} E${episode || 1}` : `🎬 ${title}${year ? ' - ' + year : ''}`; const qLine = `${qualityEmoji(quality)} ${quality || 'unknown'}${tagLine ? ' | ' + tagLine : ''}`; const statsLine = `🌱 ${seeders} | 💾 ${sizeStr} | 🔗 ${provider}`; return `${header}\n${qLine}\n${statsLine}`; } function buildUnifiedName({ quality, seeders, sourceLabel }) { return `${sourceLabel} | ${(quality || 'unknown').toUpperCase()} | 🌱${seeders}`; } function processStreams(streams, type) { const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE; const filtered = streams.filter(s => { if (!s.infoHash && !s.url) return false; const q = s._quality || getQuality(s.title || s.name || ''); if (!q || !ALLOWED_QUALITIES.includes(q)) return false; const sizeGB = s._sizeGB ?? getSizeGB(s); if (sizeGB !== null && sizeGB > maxSize) return false; return true; }); const seen = new Set(); const unique = filtered.filter(s => { const key = (s.infoHash || '').toLowerCase(); if (!key) return true; if (seen.has(key)) return false; seen.add(key); return true; }); const byQuality = {}; unique.forEach(s => { const q = s._quality || getQuality(s.title || s.name || '') || 'unknown'; if (!byQuality[q]) byQuality[q] = []; byQuality[q].push(s); }); for (const q in byQuality) { byQuality[q].sort((a, b) => { const rankA = PRIORITY_PROVIDERS.indexOf((a._source || '').toLowerCase()); const rankB = PRIORITY_PROVIDERS.indexOf((b._source || '').toLowerCase()); const pA = rankA === -1 ? 999 : rankA; const pB = rankB === -1 ? 999 : rankB; if (pA !== pB) return pA - pB; return getSeeders(b) - getSeeders(a); }); } const sortedQualities = Object.keys(byQuality).sort( (a, b) => (QUALITY_RANK[a] || 99) - (QUALITY_RANK[b] || 99) ); const result = []; sortedQualities.forEach(q => result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY))); return result; } async function tmdbLookup(imdbId) { try { const { data } = await axios.get( `https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_KEY}&external_source=imdb_id`, { timeout: 8000 } ); const movie = data.movie_results?.[0]; const tv = data.tv_results?.[0]; if (movie) return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4) }; if (tv) return { title: tv.name || tv.original_name, year: (tv.first_air_date || '').slice(0, 4) }; return null; } catch (e) { console.error('[TMDB]', e.message); return null; } } // ─── YTS scraper ───────────────────────────────────────────────────── async function scrapeYTS(imdbId) { try { const { data } = await axios.get( `https://movies-api.accel.li/api/v2/list_movies.json?query_term=${imdbId}&limit=10`, { timeout: 12000, headers: { 'User-Agent': UA } } ); if (data.status !== 'ok' || !data.data?.movies?.length) return []; const streams = []; for (const movie of data.data.movies) { if (movie.imdb_code && movie.imdb_code !== imdbId) continue; for (const t of (movie.torrents || [])) { if (!t.hash) continue; const qualityStr = `${t.quality} ${t.type || ''}`.trim(); const sizeGB = t.size_bytes ? t.size_bytes / 1073741824 : null; const sizeStr = formatSize(sizeGB); const quality = getQuality(qualityStr); const upper = qualityStr.toUpperCase(); const tags = []; if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC'); const langLine = getLangLine(qualityStr); if (langLine) tags.push(langLine); else tags.push('English'); const tagLine = tags.join(' • '); const unifiedTitle = buildUnifiedTitle({ isSeries: false, title: movie.title, year: String(movie.year), season: '', episode: '', quality, tagLine, seeders: t.seeds || 0, sizeStr, provider: 'YTS', }); const unifiedName = buildUnifiedName({ quality, seeders: t.seeds || 0, sourceLabel: '🍿 YTS' }); streams.push({ infoHash: t.hash.toLowerCase(), name: unifiedName, title: unifiedTitle, sources: [], _quality: quality, _seeders: t.seeds || 0, _sizeGB: sizeGB, _source: 'yts', _provider: 'yts', }); } } console.log(`[YTS] ${streams.length} for ${imdbId}`); return streams; } catch (e) { console.error('[YTS]', e.message); return []; } } // ─── TorrentClaw scraper ───────────────────────────────────────────── async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) { try { const isSeries = type === 'series'; const path = isSeries ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`; const url = `${TORRENTCLAW_API}/stream/${path}.json`; const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' }, }); if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return []; const streams = []; data.streams.forEach((item, idx) => { if (!item) return; const rawName = item.name || ''; const rawTitle = item.title || ''; const rawDesc = item.description || ''; const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' '); const upper = combined.toUpperCase(); const infoHashMatch = (item.url || '').match(/btih:([a-fA-F0-9]{32,40})/i); const infoHash = item.infoHash || (infoHashMatch ? infoHashMatch[1] : null); if (!infoHash && !item.url) return; const sizeGB = getSizeGB({ title: combined }) ?? (typeof item.size === 'number' ? item.size / 1073741824 : null) ?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null); const sizeStr = formatSize(sizeGB); const seeders = typeof item.seeders === 'number' ? item.seeders : (() => { const m = combined.match(/🌱\s*(\d+)/) || combined.match(/👤\s*(\d+)/) || combined.match(/(\d+)\s*seed/i); return m ? parseInt(m[1], 10) : 0; })(); const provider = detectProvider(combined, item, idx); const quality = getQuality(combined) || getQuality(upper); const tags = []; if (upper.includes('DV') || upper.includes('DOLBY VISION')) tags.push('DV'); if (upper.includes('HDR10+')) tags.push('HDR10+'); else if (upper.includes('HDR10')) tags.push('HDR10'); else if (upper.includes('HDR')) tags.push('HDR'); if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC'); const langLine = getLangLine(combined); if (langLine) tags.push(langLine); else tags.push('English'); const tagLine = tags.join(' • '); const unifiedTitle = buildUnifiedTitle({ isSeries: type === 'series', title, year, season, episode, quality, tagLine, seeders, sizeStr, provider, }); const unifiedName = buildUnifiedName({ quality, seeders, sourceLabel: '🦞 TorrentClaw' }); streams.push({ infoHash: infoHash ? infoHash.toLowerCase() : undefined, url: infoHash ? undefined : item.url, name: unifiedName, title: unifiedTitle, sources: [], _quality: quality, _seeders: seeders, _sizeGB: sizeGB, _source: 'torrentclaw', _provider: provider.toLowerCase(), }); }); console.log(`[TorrentClaw] ${streams.length} for ${type}/${imdbId}`); return streams; } catch (e) { console.error('[TorrentClaw]', e.message); return []; } } // ─── Torrentio scraper (direct fetch, no proxy) ────────────────────── async function fetchTorrentio(type, id, title, year, season, episode) { try { const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`; const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA } }); const streams = (data.streams || []).map(s => { const origTitle = s.title || ''; const providerMatch = origTitle.match(/⚙️\s*(\S+)/); let provider = providerMatch ? providerMatch[1] : 'Unknown'; provider = provider.toLowerCase().replace(/\.(to|com|org|net|io)$/, ''); if (provider === 'thepiratebay') provider = 'thepiratesbay'; if (provider === 'nyaa.si') provider = 'nyaa'; if (provider === 'limetorrents') provider = 'limetorrent'; if (provider === 'kat') provider = 'kickasstorrents'; const seeders = s.behaviorHints?.seeders ?? (() => { const m = origTitle.match(/👤\s*(\d+)/) || origTitle.match(/🌱\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; })(); const sizeGB = getSizeGB(s); const sizeStr = formatSize(sizeGB); const quality = getQuality(origTitle) || getQuality(s.name || ''); const upper = origTitle.toUpperCase(); const tags = []; if (upper.includes('DV') || upper.includes('DOLBY VISION')) tags.push('DV'); if (upper.includes('HDR10+')) tags.push('HDR10+'); else if (upper.includes('HDR10')) tags.push('HDR10'); else if (upper.includes('HDR')) tags.push('HDR'); if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC'); const langLine = getLangLine(origTitle); if (langLine) tags.push(langLine); else tags.push('English'); const tagLine = tags.join(' • '); const unifiedTitle = buildUnifiedTitle({ isSeries: type === 'series', title, year, season, episode, quality, tagLine, seeders, sizeStr, provider, }); const unifiedName = buildUnifiedName({ quality, seeders, sourceLabel: '🚀 Torrentio' }); return { ...s, name: unifiedName, title: unifiedTitle, _quality: quality, _seeders: seeders, _sizeGB: sizeGB, _source: 'torrentio', _provider: provider, }; }); console.log(`[Torrentio] ${streams.length} for ${type}/${id}`); return streams; } catch (e) { console.error('[Torrentio]', e.message); return []; } } // ─── Manifest ────────────────────────────────────────────────────────── app.get('/manifest.json', (_, res) => { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); res.json({ id: 'org.ghostream.plus.v2', name: 'VanStreams+', description: 'Stream Everything You Want\nMade With 🤍 By Van', version: '15.0.7', resources: ['stream'], types: ['movie', 'series'], idPrefixes: ['tt'], catalogs: [], }); }); // ─── Stream handler ──────────────────────────────────────────────── app.get('/stream/:type/:id.json', async (req, res) => { const { type, id } = req.params; const isSeries = type === 'series'; const [imdbId, season = '', episode = ''] = id.split(':'); const meta = await tmdbLookup(imdbId); const title = meta?.title || ''; const year = meta?.year || ''; const [ytsR, torrentclawR, torrentioR] = await Promise.allSettled([ isSeries ? Promise.resolve([]) : scrapeYTS(imdbId), scrapeTorrentClaw(type, imdbId, title, year, season, episode), fetchTorrentio(type, id, title, year, season, episode), ]); const yts = ytsR.status === 'fulfilled' ? (ytsR.value || []) : []; const torrentclaw = torrentclawR.status === 'fulfilled' ? (torrentclawR.value || []) : []; const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : []; const streams = processStreams([...yts, ...torrentclaw, ...torrentio], type); console.log(`[stream] ${type}/${id} → yts:${yts.length} torrentclaw:${torrentclaw.length} torrentio:${torrentio.length} final:${streams.length}`); res.json({ streams }); }); // ─── Diagnostic endpoints ───────────────────────────────────────────── app.get('/diag/:type/:id', async (req, res) => { const { type, id } = req.params; const isSeries = type === 'series'; const [imdbId, season = '', episode = ''] = id.split(':'); const meta = await tmdbLookup(imdbId); const [ytsR, torrentclawR, torrentioR] = await Promise.allSettled([ isSeries ? Promise.resolve([]) : scrapeYTS(imdbId), scrapeTorrentClaw(type, imdbId, meta?.title || '', meta?.year || '', season, episode), fetchTorrentio(type, id, meta?.title || '', meta?.year || '', season, episode), ]); res.json({ imdbId, title: meta?.title, year: meta?.year, isSeries, tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED', yts: { count: (ytsR.value || []).length, err: ytsR.reason?.message || null }, torrentclaw: { count: (torrentclawR.value || []).length, err: torrentclawR.reason?.message || null }, torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null }, }); }); app.get('/diag-raw/torrentclaw/:type/:id', async (req, res) => { const { type, id } = req.params; try { const isSeries = type === 'series'; const [imdbId, season = '', episode = ''] = id.split(':'); const path = isSeries ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`; const url = `${TORRENTCLAW_API}/stream/${path}.json`; const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } }); res.json((data.streams || []).slice(0, 3)); } catch (e) { res.json({ error: e.message }); } }); app.get('/diag-raw/torrentio/:type/:id', async (req, res) => { const { type, id } = req.params; try { const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`; const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA } }); res.json((data.streams || []).slice(0, 3).map(s => ({ name: s.name, title: s.title, behaviorHints: s.behaviorHints, }))); } catch (e) { res.json({ error: e.message }); } }); // ══════════════════════════ LANDING PAGE ═══════════════════════════ app.get('/', (req, res) => { const manifestUrl = 'https://vanseleem-vsplus.hf.space/manifest.json'; const installUrl = 'stremio://vanseleem-vsplus.hf.space/manifest.json'; res.type('html').send(` VanStreams+
🎬 Movies 📺 Series

Get Your 🍿 Ready

Stremio Install URL
${manifestUrl}
100K+
Movies
40K+
Series
20K+
Anime
Made With 🤍 By Van
`); }); app.listen(PORT, '0.0.0.0', () => console.log('VanStreams+ 🎬 v7.0.5 on :' + PORT));