vanseleem commited on
Commit
cc86d82
Β·
verified Β·
1 Parent(s): f5235d3

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +161 -459
index.js CHANGED
@@ -6,48 +6,7 @@ const axios = require('axios');
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 TORRENTIO_BASE = 'https://torrentsdb.com';
20
-
21
- const PROXY_1 = 'https://corsproxy.io/?url=';
22
- const PROXY_2 = 'https://api.codetabs.com/v1/proxy?quest=';
23
- const PROXY_3 = 'https://thingproxy.freeboard.io/fetch/';
24
-
25
- const MAX_SIZE_GB_MOVIE = 4;
26
- const MAX_SIZE_GB_SERIES = 1.5;
27
- const LINKS_PER_QUALITY = 5;
28
- const ALLOWED_QUALITIES = ['4k', '1080p', '720p', '576p', '480p', 'webrip'];
29
-
30
- const QUALITY_RANK = {
31
- '1080p': 1, '720p': 2, '576p': 3, '480p': 4,
32
- '4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
33
- };
34
-
35
- const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay'];
36
-
37
- const ALLOWED_PROVIDERS = [
38
- 'yts',
39
- 'knaben',
40
- 'thepiratesbay',
41
- 'thepiratebay',
42
- 'eztv',
43
- 'torrentcsv',
44
- 'nyaa',
45
- 'nyaasi',
46
- 'limetorrent',
47
- 'kickasstorrents',
48
- 'animetosho',
49
- 'tokyotosho'
50
- ];
51
 
52
  const TR = [
53
  'udp://tracker.opentrackr.org:1337/announce',
@@ -55,456 +14,199 @@ const TR = [
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
- // ─── Proxy fallback function ────────────────────────────────────────
66
- async function fetchWithProxy(url, options = {}) {
67
- const proxies = [
68
- url,
69
- PROXY_1 + encodeURIComponent(url),
70
- PROXY_2 + encodeURIComponent(url),
71
- PROXY_3 + encodeURIComponent(url),
72
- ];
73
-
74
- for (const proxyUrl of proxies) {
75
- try {
76
- const response = await axios.get(proxyUrl, { timeout: 12000, headers: { 'User-Agent': UA }, ...options });
77
- return response;
78
- } catch (e) {
79
- console.warn(`[Proxy] Failed: ${proxyUrl.substring(0, 50)}... Error: ${e.message}`);
80
- continue;
81
- }
82
- }
83
- throw new Error('All proxy attempts failed');
84
- }
85
-
86
- function buildMagnet(hash, name) {
87
- return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR;
88
- }
89
-
90
- function getQuality(str = '') {
91
- const s = str.toLowerCase();
92
- if (s.includes('4k') || s.includes('2160p') || s.includes('uhd')) return '4k';
93
- if (s.includes('1080p')) return '1080p';
94
- if (s.includes('720p')) return '720p';
95
- if (s.includes('576p')) return '576p';
96
- if (s.includes('480p')) return '480p';
97
- if (s.includes('webrip')) return 'webrip';
98
- if (s.includes('webdl') || s.includes('web-dl')) return 'webdl';
99
- return null;
100
- }
101
-
102
- function getSizeGB(stream) {
103
- const raw = stream.title || stream.name || '';
104
- const m = raw.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i);
105
- if (!m) return null;
106
- const val = parseFloat(m[1]);
107
- return m[2].toUpperCase() === 'GB' ? val : val / 1024;
108
- }
109
-
110
- function getSeeders(stream) {
111
- if (stream._seeders != null) return stream._seeders;
112
- if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
113
- const m = (stream.title || '').match(/πŸ‘€\s*(\d+)/);
114
- return m ? parseInt(m[1], 10) : 0;
115
- }
116
-
117
- function getLangLine(str = '') {
118
- const lower = str.toLowerCase();
119
- const found = [];
120
- const checks = [
121
- [/\benglish\b/, 'English'],
122
- [/\bjapanese\b/, 'Japanese'],
123
- [/\bhindi\b/, 'Hindi'],
124
- [/\bfrench\b/, 'French'],
125
- [/\bgerman\b/, 'German'],
126
- [/\bspanish\b/, 'Spanish'],
127
- [/\bitalian\b/, 'Italian'],
128
- [/\brussian\b/, 'Russian'],
129
- [/\bkorean\b/, 'Korean'],
130
- [/\bchinese\b/, 'Chinese'],
131
- [/\barabic\b/, 'Arabic'],
132
- [/\bportuguese\b/, 'Portuguese'],
133
- [/\bturkish\b/, 'Turkish'],
134
- [/\bpolish\b/, 'Polish'],
135
- [/\bdutch\b/, 'Dutch'],
136
- [/\bczech\b/, 'Czech'],
137
- [/\bswedish\b/, 'Swedish'],
138
- [/\bnorwegian\b/, 'Norwegian'],
139
- [/\bdanish\b/, 'Danish'],
140
- [/\bfinnish\b/, 'Finnish'],
141
- [/\bromanian\b/, 'Romanian'],
142
- [/\bgreek\b/, 'Greek'],
143
- [/\bhebrew\b/, 'Hebrew'],
144
- [/\bthai\b/, 'Thai'],
145
- [/\bindonesian\b/, 'Indonesian'],
146
- [/\bvietnamese\b/, 'Vietnamese'],
147
- [/\btamil\b/, 'Tamil'],
148
- [/\btelugu\b/, 'Telugu'],
149
- [/\burdu\b/, 'Urdu'],
150
- [/\bdubbed\b/, 'Dubbed'],
151
- [/\bdual[\s\-]audio\b/, 'Dual Audio'],
152
- [/\bmulti[\s\-]audio\b/, 'Multi Audio'],
153
- [/\bmulti\b/, 'Multi'],
154
- ];
155
- for (const [re, label] of checks) {
156
- if (re.test(lower)) {
157
- if (label === 'Multi' && found.includes('Multi Audio')) continue;
158
- found.push(label);
159
- }
160
- }
161
- return [...new Set(found)].join(' / ');
162
- }
163
-
164
- function processStreams(streams, type) {
165
- const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
166
-
167
- const filtered = streams.filter(s => {
168
- if (!s.infoHash && !s.url) return false;
169
- if (s._provider && !ALLOWED_PROVIDERS.includes(s._provider.toLowerCase())) return false;
170
- const q = s._quality || getQuality(s.title || s.name || '');
171
- if (!q || !ALLOWED_QUALITIES.includes(q)) return false;
172
- const sizeGB = s._sizeGB ?? getSizeGB(s);
173
- if (sizeGB !== null && sizeGB > maxSize) return false;
174
- return true;
175
- });
176
-
177
- const seen = new Set();
178
- const unique = filtered.filter(s => {
179
- const key = (s.infoHash || '').toLowerCase();
180
- if (!key) return true;
181
- if (seen.has(key)) return false;
182
- seen.add(key);
183
- return true;
184
- });
185
-
186
- const byQuality = {};
187
- unique.forEach(s => {
188
- const q = s._quality || getQuality(s.title || s.name || '') || 'unknown';
189
- if (!byQuality[q]) byQuality[q] = [];
190
- byQuality[q].push(s);
191
- });
192
-
193
- for (const q in byQuality) {
194
- byQuality[q].sort((a, b) => {
195
- const rankA = PRIORITY_PROVIDERS.indexOf((a._source || '').toLowerCase());
196
- const rankB = PRIORITY_PROVIDERS.indexOf((b._source || '').toLowerCase());
197
- const pA = rankA === -1 ? 999 : rankA;
198
- const pB = rankB === -1 ? 999 : rankB;
199
- if (pA !== pB) return pA - pB;
200
- return getSeeders(b) - getSeeders(a);
201
- });
202
- }
203
-
204
- const sortedQualities = Object.keys(byQuality).sort(
205
- (a, b) => (QUALITY_RANK[a] || 99) - (QUALITY_RANK[b] || 99)
206
- );
207
-
208
- const result = [];
209
- sortedQualities.forEach(q => result.push(...byQuality[q].slice(0, LINKS_PER_QUALITY)));
210
- return result;
211
- }
212
-
213
- async function tmdbLookup(imdbId) {
214
- try {
215
- const { data } = await axios.get(
216
- `https://api.themoviedb.org/3/find/${imdbId}?api_key=${TMDB_KEY}&external_source=imdb_id`,
217
- { timeout: 8000 }
218
- );
219
- const movie = data.movie_results?.[0];
220
- const tv = data.tv_results?.[0];
221
- if (movie) return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4) };
222
- if (tv) return { title: tv.name || tv.original_name, year: (tv.first_air_date || '').slice(0, 4) };
223
- return null;
224
- } catch (e) { console.error('[TMDB]', e.message); return null; }
225
- }
226
 
227
- async function scrapeYTS(imdbId) {
 
 
228
  try {
 
229
  const { data } = await axios.get(
230
- `https://movies-api.accel.li/api/v2/list_movies.json?query_term=${imdbId}&limit=10`,
231
- { timeout: 12000, headers: { 'User-Agent': UA } }
232
  );
233
- if (data.status !== 'ok' || !data.data?.movies?.length) return [];
 
 
234
  const streams = [];
235
- for (const movie of data.data.movies) {
236
- if (movie.imdb_code && movie.imdb_code !== imdbId) continue;
237
- for (const t of (movie.torrents || [])) {
238
- if (!t.hash) continue;
239
- const qualityStr = `${t.quality} ${t.type || ''}`.trim();
240
- const sizeGB = t.size_bytes ? t.size_bytes / 1073741824 : null;
241
- const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : (t.size || '');
242
- const langLine = getLangLine(qualityStr);
 
 
 
 
 
243
  streams.push({
244
- infoHash: t.hash.toLowerCase(),
245
- name: qualityStr,
246
- title: `β˜€οΈ ${movie.title} (${movie.year})\n🌱 ${t.seeds || 0}\nπŸ’Ύ ${sizeStr}\nπŸ… YTS${langLine ? '\nπŸ”Š ' + langLine : ''}`,
247
- sources: [],
248
- _quality: getQuality(qualityStr),
249
- _seeders: t.seeds || 0,
250
- _sizeGB: sizeGB,
251
- _source: 'yts',
252
- _provider: 'yts',
253
  });
254
  }
255
  }
256
- console.log(`[YTS] ${streams.length} for ${imdbId}`);
257
  return streams;
258
- } catch (e) { console.error('[YTS]', e.message); return []; }
 
 
 
259
  }
260
 
261
- async function scrapeKnaben(title, year, isSeries, season, episode) {
 
262
  if (!title) return [];
263
  try {
264
- let query = title;
265
- if (year && !isSeries) query += ' ' + year;
266
- if (isSeries && season && episode)
267
- query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
268
- const categories = isSeries ? [5000000, 5001000] : [2000000, 2001000];
269
- const { data } = await axios.post(
270
- 'https://api.knaben.org/v1',
271
- { search_type: '75%', search_field: 'title', query, order_by: 'seeders', order_direction: 'desc', categories, from: 0, size: 40, hide_unsafe: true, hide_xxx: true },
272
- { timeout: 14000, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': UA } }
273
  );
274
- if (!data?.hits?.length) return [];
 
 
275
  const streams = [];
276
- for (const hit of data.hits) {
277
- if (!hit.magnetUrl && !hit.hash) continue;
278
- const magnet = hit.magnetUrl || buildMagnet(hit.hash, hit.title || '');
279
- const hashMatch = magnet.match(/btih:([a-fA-F0-9]{32,40})/i);
280
- const hash = hashMatch ? hashMatch[1] : (hit.hash || '');
281
- if (!hash) continue;
282
- const sizeGB = hit.bytes ? hit.bytes / 1073741824 : null;
283
- const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : '';
284
- const langLine = getLangLine(hit.title || '');
285
  streams.push({
286
- infoHash: hash.toLowerCase(),
287
- name: hit.title || '',
288
- title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${hit.seeders || 0}\nπŸ’Ύ ${sizeStr}\nπŸ… Knaben${langLine ? '\nπŸ”Š ' + langLine : ''}`,
289
- sources: [],
290
- _quality: getQuality(hit.title || ''),
291
- _seeders: hit.seeders || 0,
292
- _sizeGB: sizeGB,
293
- _source: 'knaben',
294
- _provider: 'knaben',
295
  });
296
  }
297
- console.log(`[Knaben] ${streams.length} for "${query}"`);
298
  return streams;
299
- } catch (e) { console.error('[Knaben]', e.message); return []; }
 
 
 
300
  }
301
 
302
- async function fetchTorrentio(type, id, title, year) {
 
 
303
  try {
304
- const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
305
- const { data } = await fetchWithProxy(url);
306
- const streams = (data.streams || []).map(s => {
307
- const origTitle = s.title || '';
308
- const providerMatch = (s.title || '').match(/βš™οΈ\s*(\S+)/);
309
- let provider = providerMatch ? providerMatch[1] : 'Unknown';
310
- provider = provider.toLowerCase().replace(/\.(to|com|org|net|io)$/, '');
311
- if (provider === 'thepiratebay') provider = 'thepiratesbay';
312
- if (provider === 'nyaa.si') provider = 'nyaa';
313
- if (provider === 'limetorrents') provider = 'limetorrent';
314
- if (provider === 'kat') provider = 'kickasstorrents';
315
- const seeders = s.behaviorHints?.seeders
316
- ?? (() => { const m = (s.title || '').match(/πŸ‘€\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; })();
317
- const sizeGB = getSizeGB(s);
318
- const langLine = getLangLine(origTitle);
319
- return {
320
- ...s,
321
- title: `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A'}\nπŸ… ${provider}${langLine ? '\nπŸ”Š ' + langLine : ''}`,
322
- _quality: getQuality(s.title || s.name || ''),
323
- _seeders: seeders,
324
- _sizeGB: sizeGB,
325
- _source: 'torrentio',
326
- _provider: provider,
327
- };
328
- });
329
- console.log(`[Torrentio] ${streams.length} for ${type}/${id}`);
330
  return streams;
331
- } catch (e) { console.error('[Torrentio]', e.message); return []; }
 
 
 
332
  }
333
 
334
- // ─── Manifest ──────────────────────────────────────────────────────────
335
- app.get('/manifest.json', (_, res) => {
336
- res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
337
- res.json({
338
- id: 'org.ghostream.plus.v2',
339
- name: 'VanStreams+',
340
- description: 'Stream Everything You Want\nMade With 🀍 By Van',
341
- version: '15.0.7',
342
- resources: ['stream'],
343
- types: ['movie', 'series'],
344
- idPrefixes: ['tt'],
345
- catalogs: [],
346
- });
347
- });
348
-
349
- // ─── Stream handler ────────────────────────────────────────────────
350
- app.get('/stream/:type/:id.json', async (req, res) => {
351
- const { type, id } = req.params;
352
- const isSeries = type === 'series';
353
- const [imdbId, season = '', episode = ''] = id.split(':');
354
-
355
- const meta = await tmdbLookup(imdbId);
356
- const title = meta?.title || '';
357
- const year = meta?.year || '';
358
-
359
- const [ytsR, knabenR, torrentioR] = await Promise.allSettled([
360
- isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
361
- scrapeKnaben(title, year, isSeries, season, episode),
362
- fetchTorrentio(type, id, title, year),
363
- ]);
364
-
365
- const yts = ytsR.status === 'fulfilled' ? (ytsR.value || []) : [];
366
- const knaben = knabenR.status === 'fulfilled' ? (knabenR.value || []) : [];
367
- const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
368
-
369
- const streams = processStreams([...yts, ...knaben, ...torrentio], type);
370
-
371
- console.log(`[stream] ${type}/${id} β†’ yts:${yts.length} knaben:${knaben.length} torrentio:${torrentio.length} final:${streams.length}`);
372
- res.json({ streams });
373
- });
374
-
375
- // ─── Diagnostic endpoints ─────────────────────────────────────────────
376
- app.get('/diag/:type/:id', async (req, res) => {
377
- const { type, id } = req.params;
378
- const isSeries = type === 'series';
379
- const [imdbId, season = '', episode = ''] = id.split(':');
380
- const meta = await tmdbLookup(imdbId);
381
- const [ytsR, knabenR, torrentioR] = await Promise.allSettled([
382
- isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
383
- scrapeKnaben(meta?.title || '', meta?.year || '', isSeries, season, episode),
384
- fetchTorrentio(type, id, meta?.title || '', meta?.year || ''),
385
  ]);
 
 
 
 
 
386
  res.json({
387
- imdbId, title: meta?.title, year: meta?.year, isSeries,
388
- tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
389
- yts: { count: (ytsR.value || []).length, err: ytsR.reason?.message || null },
390
- knaben: { count: (knabenR.value || []).length, err: knabenR.reason?.message || null },
391
- torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  });
393
  });
394
 
395
- app.get('/diag-raw/:type/:id', async (req, res) => {
396
- const { type, id } = req.params;
397
- try {
398
- const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
399
- const { data } = await fetchWithProxy(url);
400
- res.json((data.streams || []).slice(0, 3).map(s => ({
401
- name: s.name, title: s.title, behaviorHints: s.behaviorHints,
402
- })));
403
- } catch (e) { res.json({ error: e.message }); }
404
- });
405
-
406
- // ══════════════════════════ LANDING PAGE ═══════════════════════════
407
  app.get('/', (req, res) => {
408
- const manifestUrl = 'https://vanseleem-vsplus.hf.space/manifest.json';
409
- const installUrl = 'stremio://vanseleem-vsplus.hf.space/manifest.json';
410
-
411
- res.type('html').send(`<!DOCTYPE html>
412
- <html lang="en">
413
- <head>
414
- <meta charset="UTF-8">
415
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
416
- <title>VanStreams+</title>
417
- <style>
418
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
419
- body {
420
- font-family: -apple-system, 'Segoe UI', system-ui, sans-serif;
421
- background: #050810;
422
- color: #dde6f0;
423
- min-height: 100vh;
424
- display: flex;
425
- align-items: center;
426
- justify-content: center;
427
- padding: 28px 20px;
428
- background-image:
429
- radial-gradient(ellipse 70% 40% at 50% 0%, rgba(255,0,0,0.05) 0%, transparent 70%),
430
- radial-gradient(ellipse 40% 30% at 80% 80%, rgba(255,0,0,0.03) 0%, transparent 60%);
431
- }
432
- .wrap { width: 100%; max-width: 560px; display: flex; flex-direction: column; gap: 20px; }
433
- .header { display: flex; flex-direction: column; gap: 12px; }
434
- .logo { font-size: 2.6rem; font-weight: 800; letter-spacing: -1px; line-height: 1; }
435
- .logo .van { color: #FF0000; }
436
- .logo .streams { color: #ffffff; }
437
- .badge-row { display: flex; gap: 8px; flex-wrap: wrap; }
438
- .badge {
439
- display: inline-flex; align-items: center; gap: 5px;
440
- background: rgba(255,0,0,0.08); border: 1px solid rgba(255,0,0,0.18);
441
- color: #ffffff; padding: 4px 11px; border-radius: 100px;
442
- font-size: 0.72rem; font-weight: 700; letter-spacing: 0.4px; text-transform: uppercase;
443
- }
444
- .badge.series { color: #ffffff; }
445
- .desc { color: #ffffff; font-size: 0.9rem; line-height: 1.6; margin-top: 2px; }
446
- .card {
447
- background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07);
448
- border-radius: 16px; padding: 20px 22px;
449
- }
450
- .card-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #ffffff; margin-bottom: 10px; }
451
- .url-box {
452
- font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 0.8rem;
453
- color: #ffffff; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,0,0,0.12);
454
- border-radius: 10px; padding: 11px 14px; word-break: break-all; line-height: 1.5;
455
- }
456
- .actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
457
- .btn { padding: 10px 22px; border-radius: 10px; font-size: 0.84rem; font-weight: 600; text-decoration: none; border: none; cursor: pointer; transition: all 0.15s; font-family: inherit; }
458
- .btn-install {
459
- background: #FF0000;
460
- color: #ffffff;
461
- }
462
- .btn-install:hover { background: #cc0000; }
463
- .btn-copy {
464
- background: #ffffff;
465
- color: #050810;
466
- border: 1px solid #ffffff;
467
- }
468
- .btn-copy:hover {
469
- background: #e6e6e6;
470
- border-color: #e6e6e6;
471
- }
472
- .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
473
- .stat { background: rgba(255,255,255,0.02); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; padding: 16px 12px; text-align: center; }
474
- .stat-val { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1; margin-bottom: 5px; }
475
- .stat-lbl { font-size: 0.68rem; color: #aaa; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; white-space: pre-line; }
476
- .made-by { text-align: center; color: #ffffff; font-size: 0.85rem; margin-top: 6px; opacity: 0.8; }
477
- </style>
478
- </head>
479
- <body>
480
- <div class="wrap">
481
- <div class="header">
482
- <div class="logo">
483
- <span class="van">Van</span> <span class="streams">Streams+</span>
484
- </div>
485
- <div class="badge-row">
486
- <span class="badge">🎬 Movies</span>
487
- <span class="badge series">πŸ“Ί Series</span>
488
- </div>
489
- <p class="desc">Get Your 🍿 Ready</p>
490
- </div>
491
- <div class="card">
492
- <div class="card-label">Stremio Install URL</div>
493
- <div class="url-box">${manifestUrl}</div>
494
- <div class="actions">
495
- <a href="${installUrl}" class="btn btn-install">Install in Stremio</a>
496
- <button class="btn btn-copy" onclick="navigator.clipboard.writeText('${manifestUrl}').then(()=>{this.textContent='βœ” Copied';setTimeout(()=>this.textContent='Copy URL',2000)})">Copy URL</button>
497
- </div>
498
- </div>
499
- <div class="stats">
500
- <div class="stat"><div class="stat-val">100K+</div><div class="stat-lbl">Movies</div></div>
501
- <div class="stat"><div class="stat-val">40K+</div><div class="stat-lbl">Series</div></div>
502
- <div class="stat"><div class="stat-val">20K+</div><div class="stat-lbl">Anime</div></div>
503
- </div>
504
- <div class="made-by">Made With 🀍 By Van</div>
505
- </div>
506
- </body>
507
- </html>`);
508
  });
509
 
510
- app.listen(PORT, '0.0.0.0', () => console.log('VanStreams+ 🎬 v7.0.5 on :' + PORT));
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  const app = express();
7
  const PORT = process.env.PORT || 7860;
8
 
9
+ 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';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  const TR = [
12
  'udp://tracker.opentrackr.org:1337/announce',
 
14
  'udp://open.demonii.com:1337/announce',
15
  'udp://tracker.torrent.eu.org:451/announce',
16
  'udp://p4p.arenabg.com:1337/announce',
 
 
 
 
 
17
  ].map(t => '&tr=' + encodeURIComponent(t)).join('');
18
 
19
+ app.use((req, res, next) => {
20
+ res.header('Access-Control-Allow-Origin', '*');
21
+ res.header('Access-Control-Allow-Headers', '*');
22
+ res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
23
+ if (req.method === 'OPTIONS') return res.sendStatus(200);
24
+ next();
25
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ // ─── SubsPlease Scraper ────────────────────────────────────────
28
+ async function scrapeSubsPlease(title) {
29
+ if (!title) return [];
30
  try {
31
+ const query = encodeURIComponent(title);
32
  const { data } = await axios.get(
33
+ `https://subsplease.org/api/?f=search&tz=&s=${query}`,
34
+ { timeout: 10000, headers: { 'User-Agent': UA } }
35
  );
36
+
37
+ if (!data || Object.keys(data).length === 0) return [];
38
+
39
  const streams = [];
40
+ for (const [showKey, showData] of Object.entries(data)) {
41
+ if (!showData.downloads || !Array.isArray(showData.downloads)) continue;
42
+
43
+ for (const dl of showData.downloads) {
44
+ if (!dl.magnet || !dl.res) continue;
45
+
46
+ const hashMatch = dl.magnet.match(/btih:([a-fA-F0-9]{32,40})/i);
47
+ const hash = hashMatch ? hashMatch[1] : '';
48
+ if (!hash) continue;
49
+
50
+ const sizeMatch = dl.magnet.match(/size=(\d+)/i);
51
+ const sizeGB = sizeMatch ? parseInt(sizeMatch[1], 10) / 1073741824 : null;
52
+
53
  streams.push({
54
+ infoHash: hash.toLowerCase(),
55
+ name: `${showData.show_name} ${dl.res}p`,
56
+ title: `SubsPlease - ${showData.show_name} ${dl.res}p`,
57
+ magnet: dl.magnet,
58
+ quality: dl.res,
59
+ size: sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A',
60
+ seeders: 'N/A',
61
+ _source: 'subsplease',
 
62
  });
63
  }
64
  }
65
+ console.log(`[SubsPlease] βœ… Found ${streams.length} results for "${title}"`);
66
  return streams;
67
+ } catch (e) {
68
+ console.error('[SubsPlease] ❌', e.message);
69
+ return [];
70
+ }
71
  }
72
 
73
+ // ─── APIBay Scraper ────────────────────────────────────────
74
+ async function scrapeAPIBay(title) {
75
  if (!title) return [];
76
  try {
77
+ const { data } = await axios.get(
78
+ `https://apibay.org/q.php?q=${encodeURIComponent(title)}&cat=208`,
79
+ { timeout: 12000, headers: { 'User-Agent': UA } }
 
 
 
 
 
 
80
  );
81
+
82
+ if (!Array.isArray(data) || data.length === 0) return [];
83
+
84
  const streams = [];
85
+ for (const item of data) {
86
+ if (!item.info_hash || !item.name) continue;
87
+
88
+ const magnet = `magnet:?xt=urn:btih:${item.info_hash.toLowerCase()}&dn=${encodeURIComponent(item.name)}${TR}`;
89
+ const sizeGB = item.size ? parseInt(item.size, 10) / 1073741824 : null;
90
+ const seeders = item.seeders ? parseInt(item.seeders, 10) : 0;
91
+
 
 
92
  streams.push({
93
+ infoHash: item.info_hash.toLowerCase(),
94
+ name: item.name,
95
+ title: `APIBay (TPB) - ${item.name.substring(0, 60)}...`,
96
+ magnet: magnet,
97
+ seeders: seeders,
98
+ size: sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A',
99
+ _source: 'apibay',
 
 
100
  });
101
  }
102
+ console.log(`[APIBay] βœ… Found ${streams.length} results for "${title}"`);
103
  return streams;
104
+ } catch (e) {
105
+ console.error('[APIBay] ❌', e.message);
106
+ return [];
107
+ }
108
  }
109
 
110
+ // ─── Torrents.csv Scraper ────────────────────────────────────────
111
+ async function scrapeTorrentsCSV(title) {
112
+ if (!title) return [];
113
  try {
114
+ const { data } = await axios.get(
115
+ `https://torrents-csv.com/service/search?q=${encodeURIComponent(title)}`,
116
+ { timeout: 12000, headers: { 'User-Agent': UA } }
117
+ );
118
+
119
+ if (!Array.isArray(data) || data.length === 0) return [];
120
+
121
+ const streams = [];
122
+ for (const item of data) {
123
+ if (!item.infohash || !item.name) continue;
124
+
125
+ const magnet = `magnet:?xt=urn:btih:${item.infohash.toLowerCase()}&dn=${encodeURIComponent(item.name)}${TR}`;
126
+ const sizeGB = item.size ? item.size / 1073741824 : null;
127
+ const seeders = item.seeders || 0;
128
+
129
+ streams.push({
130
+ infoHash: item.infohash.toLowerCase(),
131
+ name: item.name,
132
+ title: `Torrents.csv - ${item.name.substring(0, 60)}...`,
133
+ magnet: magnet,
134
+ seeders: seeders,
135
+ size: sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A',
136
+ _source: 'torrentcsv',
137
+ });
138
+ }
139
+ console.log(`[Torrents.csv] βœ… Found ${streams.length} results for "${title}"`);
140
  return streams;
141
+ } catch (e) {
142
+ console.error('[Torrents.csv] ❌', e.message);
143
+ return [];
144
+ }
145
  }
146
 
147
+ // ─── Test Endpoint ────────────────────────────────────────
148
+ app.get('/test/:query', async (req, res) => {
149
+ const { query } = req.params;
150
+ console.log(`\nπŸ” Testing with query: "${query}"\n`);
151
+
152
+ const [subspleaseR, apibayR, torrentscsvR] = await Promise.allSettled([
153
+ scrapeSubsPlease(query),
154
+ scrapeAPIBay(query),
155
+ scrapeTorrentsCSV(query),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  ]);
157
+
158
+ const subsplease = subspleaseR.status === 'fulfilled' ? subspleaseR.value : [];
159
+ const apibay = apibayR.status === 'fulfilled' ? apibayR.value : [];
160
+ const torrentscsv = torrentscsvR.status === 'fulfilled' ? torrentscsvR.value : [];
161
+
162
  res.json({
163
+ query,
164
+ results: {
165
+ subsplease: {
166
+ status: subspleaseR.status,
167
+ count: subsplease.length,
168
+ data: subsplease.slice(0, 3),
169
+ },
170
+ apibay: {
171
+ status: apibayR.status,
172
+ count: apibay.length,
173
+ data: apibay.slice(0, 3),
174
+ },
175
+ torrentscsv: {
176
+ status: torrentscsvR.status,
177
+ count: torrentscsv.length,
178
+ data: torrentscsv.slice(0, 3),
179
+ },
180
+ total: subsplease.length + apibay.length + torrentscsv.length,
181
+ },
182
  });
183
  });
184
 
185
+ // ─── Health Check ────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
186
  app.get('/', (req, res) => {
187
+ res.json({
188
+ status: 'βœ… Running',
189
+ version: '1.0.0',
190
+ endpoints: [
191
+ 'GET /test/:query - Test all three scrapers',
192
+ 'Examples:',
193
+ ' /test/Breaking%20Bad',
194
+ ' /test/Solo%20Leveling',
195
+ ' /test/The%20Office',
196
+ ],
197
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  });
199
 
200
+ app.listen(PORT, '0.0.0.0', () => {
201
+ console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
202
+ console.log(`πŸ§ͺ Test Script Running on Port ${PORT}`);
203
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
204
+ console.log(`\nπŸ“ Test URLs:\n`);
205
+ console.log(` SubsPlease (Anime):`);
206
+ console.log(` http://localhost:${PORT}/test/Solo%20Leveling\n`);
207
+ console.log(` APIBay (TV Series):`);
208
+ console.log(` http://localhost:${PORT}/test/Breaking%20Bad\n`);
209
+ console.log(` Torrents.csv (General):`);
210
+ console.log(` http://localhost:${PORT}/test/The%20Office\n`);
211
+ console.log(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`);
212
+ });