vanseleem commited on
Commit
ac47323
·
verified ·
1 Parent(s): 84786dd

Update index.js

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