vanseleem commited on
Commit
bd52c99
Β·
verified Β·
1 Parent(s): 0bddc10

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +422 -1
index.js CHANGED
@@ -1,4 +1,425 @@
1
- res.type('html').send(`<!DOCTYPE html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8">
 
1
+ 'use strict';
2
+
3
+ const express = require('express');
4
+ const axios = require('axios');
5
+
6
+ const app = express();
7
+ const PORT = process.env.PORT || 7860;
8
+
9
+ app.use((req, res, next) => {
10
+ res.header('Access-Control-Allow-Origin', '*');
11
+ res.header('Access-Control-Allow-Headers', '*');
12
+ res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
13
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
14
+ next();
15
+ });
16
+
17
+ const TMDB_KEY = '83d364331c40bfbe29858aeed82f45cc';
18
+ 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';
19
+ const TORRENTCLAW_API = 'https://torrentclaw.com/api/stremio';
20
+ const TORRENTIO_API = 'https://torrentio.strem.fun';
21
+
22
+ const MAX_SIZE_GB_MOVIE = 4;
23
+ const MAX_SIZE_GB_SERIES = 1.5;
24
+ const LINKS_PER_QUALITY = 5;
25
+ const ALLOWED_QUALITIES = ['4k', '1080p', '720p', '576p', '480p', 'webrip'];
26
+
27
+ const QUALITY_RANK = {
28
+ '1080p': 1, '720p': 2, '576p': 3, '480p': 4,
29
+ '4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
30
+ };
31
+
32
+ const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw', 'torrentio'];
33
+
34
+ const ALLOWED_PROVIDERS = [
35
+ 'yts',
36
+ 'knaben',
37
+ 'thepiratesbay',
38
+ 'thepiratebay',
39
+ 'eztv',
40
+ 'torrentcsv',
41
+ 'nyaa',
42
+ 'nyaasi',
43
+ 'limetorrent',
44
+ 'kickasstorrents',
45
+ 'animetosho',
46
+ 'tokyotosho',
47
+ 'torrentclaw',
48
+ 'torrentio',
49
+ 'unknown'
50
+ ];
51
+
52
+ const TR = [
53
+ 'udp://tracker.opentrackr.org:1337/announce',
54
+ 'udp://open.stealth.si:80/announce',
55
+ 'udp://open.demonii.com:1337/announce',
56
+ 'udp://tracker.torrent.eu.org:451/announce',
57
+ 'udp://p4p.arenabg.com:1337/announce',
58
+ 'udp://exodus.desync.com:6969/announce',
59
+ 'udp://tracker.openbittorrent.com:6969/announce',
60
+ 'udp://tracker.dler.org:6969/announce',
61
+ 'https://tracker.moeblog.cn:443/announce',
62
+ 'https://tracker.zhuqiy.com:443/announce',
63
+ ].map(t => '&tr=' + encodeURIComponent(t)).join('');
64
+
65
+ function buildMagnet(hash, name) {
66
+ return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR;
67
+ }
68
+
69
+ function getQuality(str = '') {
70
+ const s = str.toLowerCase();
71
+ if (s.includes('4k') || s.includes('2160p') || s.includes('uhd')) return '4k';
72
+ if (s.includes('1080p')) return '1080p';
73
+ if (s.includes('720p')) return '720p';
74
+ if (s.includes('576p')) return '576p';
75
+ if (s.includes('480p')) return '480p';
76
+ if (s.includes('webrip')) return 'webrip';
77
+ if (s.includes('webdl') || s.includes('web-dl')) return 'webdl';
78
+ return null;
79
+ }
80
+
81
+ function getSizeGB(stream) {
82
+ const raw = stream.title || stream.name || '';
83
+ const m = raw.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i);
84
+ if (!m) return null;
85
+ const val = parseFloat(m[1]);
86
+ return m[2].toUpperCase() === 'GB' ? val : val / 1024;
87
+ }
88
+
89
+ function getSeeders(stream) {
90
+ if (stream._seeders != null) return stream._seeders;
91
+ if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
92
+ const m = (stream.title || '').match(/πŸ‘€\s*(\d+)/);
93
+ return m ? parseInt(m[1], 10) : 0;
94
+ }
95
+
96
+ function getLangLine(str = '') {
97
+ const lower = str.toLowerCase();
98
+ const found = [];
99
+ const checks = [
100
+ [/\benglish\b/, 'English'],
101
+ [/\bjapanese\b/, 'Japanese'],
102
+ [/\bhindi\b/, 'Hindi'],
103
+ [/\bfrench\b/, 'French'],
104
+ [/\bgerman\b/, 'German'],
105
+ [/\bspanish\b/, 'Spanish'],
106
+ [/\bitalian\b/, 'Italian'],
107
+ [/\brussian\b/, 'Russian'],
108
+ [/\bkorean\b/, 'Korean'],
109
+ [/\bchinese\b/, 'Chinese'],
110
+ [/\barabic\b/, 'Arabic'],
111
+ [/\bportuguese\b/, 'Portuguese'],
112
+ [/\bturkish\b/, 'Turkish'],
113
+ [/\bpolish\b/, 'Polish'],
114
+ [/\bdutch\b/, 'Dutch'],
115
+ [/\bczech\b/, 'Czech'],
116
+ [/\bswedish\b/, 'Swedish'],
117
+ [/\bnorwegian\b/, 'Norwegian'],
118
+ [/\bdanish\b/, 'Danish'],
119
+ [/\bfinnish\b/, 'Finnish'],
120
+ [/\bromanian\b/, 'Romanian'],
121
+ [/\bgreek\b/, 'Greek'],
122
+ [/\bhebrew\b/, 'Hebrew'],
123
+ [/\bthai\b/, 'Thai'],
124
+ [/\bindonesian\b/, 'Indonesian'],
125
+ [/\bvietnamese\b/, 'Vietnamese'],
126
+ [/\btamil\b/, 'Tamil'],
127
+ [/\btelugu\b/, 'Telugu'],
128
+ [/\burdu\b/, 'Urdu'],
129
+ [/\bdubbed\b/, 'Dubbed'],
130
+ [/\bdual[\s\-]audio\b/, 'Dual Audio'],
131
+ [/\bmulti[\s\-]audio\b/, 'Multi Audio'],
132
+ [/\bmulti\b/, 'Multi'],
133
+ ];
134
+ for (const [re, label] of checks) {
135
+ if (re.test(lower)) {
136
+ if (label === 'Multi' && found.includes('Multi Audio')) continue;
137
+ found.push(label);
138
+ }
139
+ }
140
+ return [...new Set(found)].join(' / ');
141
+ }
142
+
143
+ function processStreams(streams, type) {
144
+ const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
145
+
146
+ const filtered = streams.filter(s => {
147
+ if (!s.infoHash && !s.url) return false;
148
+ if (s._provider && !ALLOWED_PROVIDERS.includes(s._provider.toLowerCase())) return false;
149
+ const q = s._quality || getQuality(s.title || s.name || '');
150
+ if (!q || !ALLOWED_QUALITIES.includes(q)) return false;
151
+ const sizeGB = s._sizeGB ?? getSizeGB(s);
152
+ if (sizeGB !== null && sizeGB > maxSize) return false;
153
+ return true;
154
+ });
155
+
156
+ const seen = new Set();
157
+ const unique = filtered.filter(s => {
158
+ const key = (s.infoHash || '').toLowerCase();
159
+ if (!key) return true;
160
+ if (seen.has(key)) return false;
161
+ seen.add(key);
162
+ return true;
163
+ });
164
+
165
+ const byQuality = {};
166
+ unique.forEach(s => {
167
+ const q = s._quality || getQuality(s.title || s.name || '') || 'unknown';
168
+ if (!byQuality[q]) byQuality[q] = [];
169
+ byQuality[q].push(s);
170
+ });
171
+
172
+ for (const q in byQuality) {
173
+ byQuality[q].sort((a, b) => {
174
+ const rankA = PRIORITY_PROVIDERS.indexOf((a._source || '').toLowerCase());
175
+ const rankB = PRIORITY_PROVIDERS.indexOf((b._source || '').toLowerCase());
176
+ const pA = rankA === -1 ? 999 : rankA;
177
+ const pB = rankB === -1 ? 999 : rankB;
178
+ if (pA !== pB) return pA - pB;
179
+ return getSeeders(b) - getSeeders(a);
180
+ });
181
+ }
182
+
183
+ const sortedQualities = Object.keys(byQuality).sort(
184
+ (a, b) => (QUALITY_RANK[a] || 99) - (QUALITY_RANK[b] || 99)
185
+ );
186
+
187
+ const result = [];
188
+ sortedQualities.forEach(q => result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY)));
189
+ return result;
190
+ }
191
+
192
+ async function tmdbLookup(imdbId) {
193
+ try {
194
+ const { data } = await axios.get(
195
+ `https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_KEY}&external_source=imdb_id`,
196
+ { timeout: 8000 }
197
+ );
198
+ const movie = data.movie_results?.[0];
199
+ const tv = data.tv_results?.[0];
200
+ if (movie) return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4) };
201
+ if (tv) return { title: tv.name || tv.original_name, year: (tv.first_air_date || '').slice(0, 4) };
202
+ return null;
203
+ } catch (e) { console.error('[TMDB]', e.message); return null; }
204
+ }
205
+
206
+ // ─── TorrentClaw scraper ───────────────────────────────────────────────
207
+ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
208
+ try {
209
+ const isSeries = type === 'series';
210
+ const path = isSeries
211
+ ? `series/${imdbId}:${season || 1}:${episode || 1}`
212
+ : `movie/${imdbId}`;
213
+ const url = `${TORRENTCLAW_API}/stream/${path}.json`;
214
+
215
+ const { data } = await axios.get(url, {
216
+ timeout: 15000,
217
+ headers: { 'User-Agent': UA, 'Accept': 'application/json' },
218
+ });
219
+
220
+ if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return [];
221
+
222
+ const streams = [];
223
+ for (const item of data.streams) {
224
+ if (!item) continue;
225
+
226
+ const rawName = item.name || '';
227
+ const rawTitle = item.title || '';
228
+ const rawDesc = item.description || '';
229
+ const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' ');
230
+
231
+ const infoHashMatch = (item.url || '').match(/btih:([a-fA-F0-9]{32,40})/i);
232
+ const infoHash = item.infoHash || (infoHashMatch ? infoHashMatch[1] : null);
233
+ if (!infoHash && !item.url) continue;
234
+
235
+ const sizeGB = getSizeGB({ title: combined })
236
+ ?? (typeof item.size === 'number' ? item.size / 1073741824 : null)
237
+ ?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null);
238
+ const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A';
239
+
240
+ const seeders = typeof item.seeders === 'number'
241
+ ? item.seeders
242
+ : (() => { const m = combined.match(/🌱\s*(\d+)/) || combined.match(/πŸ‘€\s*(\d+)/) || combined.match(/(\d+)\s*seed/i); return m ? parseInt(m[1], 10) : 0; })();
243
+
244
+ const provider = (item.provider || item.source || item.indexer || 'TorrentClaw').toString().toLowerCase();
245
+ const langLine = getLangLine(combined);
246
+
247
+ streams.push({
248
+ infoHash: infoHash ? infoHash.toLowerCase() : undefined,
249
+ url: infoHash ? undefined : item.url,
250
+ name: rawName || rawTitle,
251
+ title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeStr}\nπŸ… TorrentClaw${langLine ? '\nπŸ”Š ' + langLine : ''}`,
252
+ sources: [],
253
+ _quality: getQuality(combined),
254
+ _seeders: seeders,
255
+ _sizeGB: sizeGB,
256
+ _source: 'torrentclaw',
257
+ _provider: 'torrentclaw',
258
+ });
259
+ }
260
+ console.log(`[TorrentClaw] ${streams.length} for ${type}/${imdbId}`);
261
+ return streams;
262
+ } catch (e) { console.error('[TorrentClaw]', e.message); return []; }
263
+ }
264
+
265
+ // ─── Torrentio scraper ────────────────────────────────────────────────
266
+ async function scrapeTorrentio(type, imdbId, title, year, season, episode) {
267
+ try {
268
+ const isSeries = type === 'series';
269
+ const path = isSeries
270
+ ? `series/${imdbId}:${season || 1}:${episode || 1}`
271
+ : `movie/${imdbId}`;
272
+ const url = `${TORRENTIO_API}/stream/${path}.json`;
273
+
274
+ const { data } = await axios.get(url, {
275
+ timeout: 15000,
276
+ headers: { 'User-Agent': UA, 'Accept': 'application/json' },
277
+ });
278
+
279
+ if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return [];
280
+
281
+ const streams = [];
282
+ for (const item of data.streams) {
283
+ if (!item) continue;
284
+
285
+ const rawName = item.name || '';
286
+ const rawTitle = item.title || '';
287
+ const combined = `${rawName} ${rawTitle}`.replace(/\n/g, ' ');
288
+
289
+ const infoHashMatch = (item.url || '').match(/btih:([a-fA-F0-9]{32,40})/i);
290
+ const infoHash = item.infoHash || (infoHashMatch ? infoHashMatch[1] : null);
291
+ if (!infoHash && !item.url) continue;
292
+
293
+ // Extract size
294
+ let sizeGB = null;
295
+ let sizeStr = 'N/A';
296
+ const sizeMatch = combined.match(/([0-9.]+ ?[GM]B)/i);
297
+ if (sizeMatch) {
298
+ const val = parseFloat(sizeMatch[1]);
299
+ sizeGB = sizeMatch[2].toUpperCase() === 'GB' ? val : val / 1024;
300
+ sizeStr = sizeGB.toFixed(2) + ' GB';
301
+ }
302
+
303
+ // Extract seeders
304
+ const seedersMatch = combined.match(/πŸ‘€\s*(\d+)/);
305
+ const seeders = seedersMatch ? parseInt(seedersMatch[1], 10) : 0;
306
+
307
+ // Extract provider (matching Script 2 logic)
308
+ let provider = 'torrentio';
309
+ const providerMatch = combined.match(/βš™οΈ\s*([^πŸ‘€πŸ’Ύ\n]+)/);
310
+ if (providerMatch) {
311
+ provider = providerMatch[1].trim();
312
+ } else {
313
+ if (combined.includes('RARBG')) provider = 'RARBG';
314
+ else if (combined.includes('YTS')) provider = 'YTS';
315
+ else if (combined.includes('TPB') || combined.includes('ThePirateBay')) provider = 'ThePirateBay';
316
+ else if (combined.includes('1337X')) provider = '1337x';
317
+ else if (combined.includes('EZTV')) provider = 'EZTV';
318
+ else if (combined.includes('TGX')) provider = 'TGX';
319
+ }
320
+ provider = provider.toLowerCase();
321
+
322
+ const quality = getQuality(combined);
323
+ const langLine = getLangLine(combined);
324
+
325
+ streams.push({
326
+ infoHash: infoHash ? infoHash.toLowerCase() : undefined,
327
+ url: infoHash ? undefined : item.url,
328
+ name: rawName || 'Torrentio',
329
+ title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeStr}\nπŸ… ${provider}${langLine ? '\nπŸ”Š ' + langLine : ''}`,
330
+ sources: [],
331
+ _quality: quality,
332
+ _seeders: seeders,
333
+ _sizeGB: sizeGB,
334
+ _source: provider,
335
+ _provider: provider,
336
+ });
337
+ }
338
+ console.log(`[Torrentio] ${streams.length} for ${type}/${imdbId}`);
339
+ return streams;
340
+ } catch (e) { console.error('[Torrentio]', e.message); return []; }
341
+ }
342
+
343
+ // ─── Manifest ──────────────────────────────────────────────────────────
344
+ app.get('/manifest.json', (_, res) => {
345
+ res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
346
+ res.json({
347
+ id: 'org.ghostream.plus.v2',
348
+ name: 'VanStreams+',
349
+ description: 'Stream Everything You Want\nMade With 🀍 By Van',
350
+ version: '15.0.7',
351
+ resources: ['stream'],
352
+ types: ['movie', 'series'],
353
+ idPrefixes: ['tt'],
354
+ catalogs: [],
355
+ });
356
+ });
357
+
358
+ // ─── Stream handler ────────────────────────────────────────────────
359
+ app.get('/stream/:type/:id.json', async (req, res) => {
360
+ const { type, id } = req.params;
361
+ const [imdbId, season = '', episode = ''] = id.split(':');
362
+
363
+ const meta = await tmdbLookup(imdbId);
364
+ const title = meta?.title || '';
365
+ const year = meta?.year || '';
366
+
367
+ // Fetch from both providers in parallel
368
+ const [torrentclaw, torrentio] = await Promise.all([
369
+ scrapeTorrentClaw(type, imdbId, title, year, season, episode),
370
+ scrapeTorrentio(type, imdbId, title, year, season, episode)
371
+ ]);
372
+
373
+ const combinedStreams = [...torrentclaw, ...torrentio];
374
+ const streams = processStreams(combinedStreams, type);
375
+
376
+ console.log(`[stream] ${type}/${id} β†’ torrentclaw:${torrentclaw.length} torrentio:${torrentio.length} combined:${combinedStreams.length} final:${streams.length}`);
377
+ res.json({ streams });
378
+ });
379
+
380
+ // ─── Diagnostic endpoints ─────────────────────────────────────────────
381
+ app.get('/diag/:type/:id', async (req, res) => {
382
+ const { type, id } = req.params;
383
+ const [imdbId, season = '', episode = ''] = id.split(':');
384
+ const meta = await tmdbLookup(imdbId);
385
+
386
+ const [torrentclaw, torrentio] = await Promise.all([
387
+ scrapeTorrentClaw(type, imdbId, meta?.title || '', meta?.year || '', season, episode),
388
+ scrapeTorrentio(type, imdbId, meta?.title || '', meta?.year || '', season, episode)
389
+ ]);
390
+
391
+ res.json({
392
+ imdbId, title: meta?.title, year: meta?.year, isSeries: type === 'series',
393
+ tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
394
+ torrentclaw: { count: torrentclaw.length },
395
+ torrentio: { count: torrentio.length }
396
+ });
397
+ });
398
+
399
+ app.get('/diag-raw/:type/:id', async (req, res) => {
400
+ const { type, id } = req.params;
401
+ try {
402
+ const [imdbId, season = '', episode = ''] = id.split(':');
403
+ const path = type === 'series' ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`;
404
+
405
+ const [clawRes, tioRes] = await Promise.all([
406
+ axios.get(`${TORRENTCLAW_API}/stream/${path}.json`, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } }),
407
+ axios.get(`${TORRENTIO_API}/stream/${path}.json`, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } })
408
+ ]);
409
+
410
+ res.json({
411
+ torrentclaw: (clawRes.data?.streams || []).slice(0, 3),
412
+ torrentio: (tioRes.data?.streams || []).slice(0, 3)
413
+ });
414
+ } catch (e) { res.json({ error: e.message }); }
415
+ });
416
+
417
+ // ══════════════════════════ LANDING PAGE ═══════════════════════════
418
+ app.get('/', (req, res) => {
419
+ const manifestUrl = 'https://vanseleem-vsplus.hf.space/manifest.json';
420
+ const installUrl = 'stremio://vanseleem-vsplus.hf.space/manifest.json';
421
+
422
+ res.type('html').send(`<!DOCTYPE html>
423
  <html lang="en">
424
  <head>
425
  <meta charset="UTF-8">