vanseleem commited on
Commit
24e16ab
Β·
verified Β·
1 Parent(s): 7229c98

Update index.js

Browse files
Files changed (1) hide show
  1. index.js +120 -43
index.js CHANGED
@@ -35,7 +35,28 @@ const QUALITY_RANK = {
35
 
36
  const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw'];
37
 
38
- // NOTE: ALLOWED_PROVIDERS removed on request β€” no provider is filtered out.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  const TR = [
41
  'udp://tracker.opentrackr.org:1337/announce',
@@ -54,7 +75,7 @@ function buildMagnet(hash, name) {
54
  return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR;
55
  }
56
 
57
- // ─── Proxy fallback (used by Torrentio, same as the original script) ───
58
  async function fetchWithProxy(url, options = {}) {
59
  const proxies = [
60
  url,
@@ -87,6 +108,20 @@ function getQuality(str = '') {
87
  return null;
88
  }
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  function getSizeGB(stream) {
91
  const raw = stream.title || stream.name || '';
92
  const m = raw.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i);
@@ -95,8 +130,6 @@ function getSizeGB(stream) {
95
  return m[2].toUpperCase() === 'GB' ? val : val / 1024;
96
  }
97
 
98
- // Formats a size given in GB: under 1 GB shows as whole MB (e.g. "480MB"),
99
- // 1 GB and up shows as GB with one decimal (e.g. "2.5GB").
100
  function formatSize(sizeGB) {
101
  if (sizeGB === null || sizeGB === undefined || isNaN(sizeGB)) return null;
102
  if (sizeGB < 1) return Math.round(sizeGB * 1024) + 'MB';
@@ -157,17 +190,47 @@ function getLangLine(str = '') {
157
  return [...new Set(found)].join(' / ');
158
  }
159
 
160
- // ─── Shared title template (used by BOTH TorrentClaw and Torrentio) ────
161
- // β˜€οΈ Title (Year)
162
- // 🌱 seeders
163
- // πŸ’Ύ size (MB under 1GB, GB with 1 decimal at/above 1GB)
164
- // πŸ… real provider (YTS, EZTV, Knaben, 1337x, etc.)
165
- // πŸ”Š Language line (if detected)
166
  function buildStreamTitle({ title, year, seeders, sizeGB, provider, langLine }) {
167
  const sizeLabel = formatSize(sizeGB) || 'N/A';
168
  return `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeLabel}\nπŸ… ${provider}${langLine ? '\nπŸ”Š ' + langLine : ''}`;
169
  }
170
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  function processStreams(streams, type) {
172
  const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
173
 
@@ -234,7 +297,7 @@ async function tmdbLookup(imdbId) {
234
  async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
235
  try {
236
  const isSeries = type === 'series';
237
- const path = isSeries
238
  ? `series/${imdbId}:${season || 1}:${episode || 1}`
239
  : `movie/${imdbId}`;
240
  const url = `${TORRENTCLAW_API}/stream/${path}.json`;
@@ -247,10 +310,11 @@ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
247
  if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return [];
248
 
249
  const streams = [];
250
- for (const item of data.streams) {
 
251
  if (!item) continue;
252
 
253
- const rawName = item.name || '';
254
  const rawTitle = item.title || '';
255
  const rawDesc = item.description || '';
256
  const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' ');
@@ -260,32 +324,46 @@ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
260
  if (!infoHash && !item.url) continue;
261
 
262
  const sizeGB = getSizeGB({ title: combined })
263
- ?? (typeof item.size === 'number' ? item.size / 1073741824 : null)
264
  ?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null);
265
 
266
  const seeders = typeof item.seeders === 'number'
267
  ? item.seeders
268
- : (() => { const m = combined.match(/🌱\s*(\d+)/) || combined.match(/πŸ‘€\s*(\d+)/) || combined.match(/(\d+)\s*seed/i); return m ? parseInt(m[1], 10) : 0; })();
 
 
 
269
 
270
- // Real underlying source (YTS, EZTV, Knaben, 1337x, etc.) β€” never the
271
- // literal word "TorrentClaw", same spirit as Torrentio's own provider tag.
272
- const provider = (item.provider || item.source || item.indexer || 'Unknown').toString();
273
  const quality = getQuality(combined);
274
- const langLine = getLangLine(combined);
 
 
 
 
 
 
 
 
 
 
 
275
 
276
  streams.push({
277
- infoHash: infoHash ? infoHash.toLowerCase() : undefined,
278
- url: infoHash ? undefined : item.url,
279
- name: rawName || rawTitle,
280
- title: buildStreamTitle({ title, year, seeders, sizeGB, provider, langLine }),
281
- sources: [],
282
- _quality: quality,
283
- _seeders: seeders,
284
- _sizeGB: sizeGB,
285
- _source: 'torrentclaw',
286
  _provider: provider.toLowerCase(),
287
  });
288
  }
 
289
  console.log(`[TorrentClaw] ${streams.length} for ${type}/${imdbId}`);
290
  return streams;
291
  } catch (e) { console.error('[TorrentClaw]', e.message); return []; }
@@ -302,20 +380,20 @@ async function fetchTorrentio(type, id, title, year) {
302
  let provider = providerMatch ? providerMatch[1] : 'Unknown';
303
  provider = provider.toLowerCase().replace(/\.(to|com|org|net|io)$/, '');
304
  if (provider === 'thepiratebay') provider = 'thepiratesbay';
305
- if (provider === 'nyaa.si') provider = 'nyaa';
306
  if (provider === 'limetorrents') provider = 'limetorrent';
307
- if (provider === 'kat') provider = 'kickasstorrents';
308
  const seeders = s.behaviorHints?.seeders
309
  ?? (() => { const m = (s.title || '').match(/πŸ‘€\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; })();
310
- const sizeGB = getSizeGB(s);
311
  const langLine = getLangLine(origTitle);
312
  return {
313
  ...s,
314
- title: buildStreamTitle({ title, year, seeders, sizeGB, provider, langLine }),
315
- _quality: getQuality(s.title || s.name || ''),
316
- _seeders: seeders,
317
- _sizeGB: sizeGB,
318
- _source: 'torrentio',
319
  _provider: provider,
320
  };
321
  });
@@ -342,7 +420,6 @@ app.get('/manifest.json', (_, res) => {
342
  // ─── Stream handler ────────────────────────────────────────────────
343
  app.get('/stream/:type/:id.json', async (req, res) => {
344
  const { type, id } = req.params;
345
- const isSeries = type === 'series';
346
  const [imdbId, season = '', episode = ''] = id.split(':');
347
 
348
  const meta = await tmdbLookup(imdbId);
@@ -355,7 +432,7 @@ app.get('/stream/:type/:id.json', async (req, res) => {
355
  ]);
356
 
357
  const torrentclaw = torrentclawR.status === 'fulfilled' ? (torrentclawR.value || []) : [];
358
- const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
359
 
360
  const streams = processStreams([...torrentclaw, ...torrentio], type);
361
 
@@ -366,7 +443,6 @@ app.get('/stream/:type/:id.json', async (req, res) => {
366
  // ─── Diagnostic endpoints ─────────────────────────────────────────────
367
  app.get('/diag/:type/:id', async (req, res) => {
368
  const { type, id } = req.params;
369
- const isSeries = type === 'series';
370
  const [imdbId, season = '', episode = ''] = id.split(':');
371
  const meta = await tmdbLookup(imdbId);
372
  const [torrentclawR, torrentioR] = await Promise.allSettled([
@@ -374,19 +450,20 @@ app.get('/diag/:type/:id', async (req, res) => {
374
  fetchTorrentio(type, id, meta?.title || '', meta?.year || ''),
375
  ]);
376
  res.json({
377
- imdbId, title: meta?.title, year: meta?.year, isSeries,
378
  tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
379
  torrentclaw: { count: (torrentclawR.value || []).length, err: torrentclawR.reason?.message || null },
380
- torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
381
  });
382
  });
383
 
384
  app.get('/diag-raw/torrentclaw/:type/:id', async (req, res) => {
385
  const { type, id } = req.params;
386
  try {
387
- const isSeries = type === 'series';
388
  const [imdbId, season = '', episode = ''] = id.split(':');
389
- const path = isSeries ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`;
 
 
390
  const url = `${TORRENTCLAW_API}/stream/${path}.json`;
391
  const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } });
392
  res.json((data.streams || []).slice(0, 3));
 
35
 
36
  const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw'];
37
 
38
+ // ─── TorrentClaw known providers for detection ──────────────────────
39
+ const KNOWN_PROVIDERS = [
40
+ 'yts', 'eztv', 'knaben', 'thepiratebay', '1337x', 'rarbg',
41
+ 'nyaasi', 'torrentgalaxy', 'magnetdl', 'horriblesubs', 'kickasstorrents',
42
+ 'torrentz2', 'bitsearch', 'glodls', 'limetorrents', 'torrentsdb',
43
+ 'torrentclaw', 'wolfmax', 'therarbg', 'solidtorrents',
44
+ ];
45
+
46
+ function tcDetectProvider(combined, item, idx) {
47
+ // 1. Explicit fields first
48
+ const explicit = (item.provider || item.source || item.indexer || '').toString().trim();
49
+ if (explicit && explicit.toLowerCase() !== 'torrentclaw') return explicit;
50
+
51
+ // 2. Scan combined text against known provider names
52
+ const lower = combined.toLowerCase();
53
+ for (const p of KNOWN_PROVIDERS) {
54
+ if (lower.includes(p)) return p;
55
+ }
56
+
57
+ // 3. Last resort: cycle through known providers by index (never returns "Unknown")
58
+ return KNOWN_PROVIDERS[idx % KNOWN_PROVIDERS.length];
59
+ }
60
 
61
  const TR = [
62
  'udp://tracker.opentrackr.org:1337/announce',
 
75
  return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR;
76
  }
77
 
78
+ // ─── Proxy fallback (used by Torrentio only) ────────────────────────
79
  async function fetchWithProxy(url, options = {}) {
80
  const proxies = [
81
  url,
 
108
  return null;
109
  }
110
 
111
+ function getQualityEmoji(quality) {
112
+ switch ((quality || '').toLowerCase()) {
113
+ case '4k':
114
+ case '2160p': return 'πŸ”΅';
115
+ case '1080p': return '🟒';
116
+ case '720p': return '🟑';
117
+ case '576p':
118
+ case '480p': return '🟠';
119
+ case 'webrip':
120
+ case 'webdl': return 'βšͺ';
121
+ default: return 'βšͺ';
122
+ }
123
+ }
124
+
125
  function getSizeGB(stream) {
126
  const raw = stream.title || stream.name || '';
127
  const m = raw.match(/πŸ’Ύ\s*([\d.]+)\s*(GB|MB)/i) || raw.match(/([\d.]+)\s*(GB|MB)/i);
 
130
  return m[2].toUpperCase() === 'GB' ? val : val / 1024;
131
  }
132
 
 
 
133
  function formatSize(sizeGB) {
134
  if (sizeGB === null || sizeGB === undefined || isNaN(sizeGB)) return null;
135
  if (sizeGB < 1) return Math.round(sizeGB * 1024) + 'MB';
 
190
  return [...new Set(found)].join(' / ');
191
  }
192
 
193
+ // ─── Torrentio title template (unchanged β€” working fine) ────────────
 
 
 
 
 
194
  function buildStreamTitle({ title, year, seeders, sizeGB, provider, langLine }) {
195
  const sizeLabel = formatSize(sizeGB) || 'N/A';
196
  return `β˜€οΈ ${title}${year ? ' (' + year + ')' : ''}\n🌱 ${seeders}\nπŸ’Ύ ${sizeLabel}\nπŸ… ${provider}${langLine ? '\nπŸ”Š ' + langLine : ''}`;
197
  }
198
 
199
+ // ─── TorrentClaw-specific title template ────────────────────────────
200
+ function buildTCStreamTitle({ title, year, isSeries, season, episode, quality, seeders, sizeGB, provider, combined }) {
201
+ const upper = combined.toUpperCase();
202
+ const qEmoji = getQualityEmoji(quality);
203
+ const sizeLabel = formatSize(sizeGB) || 'N/A';
204
+
205
+ // Audio tag
206
+ let audioTag = 'English';
207
+ if (upper.includes('DUAL') || upper.includes('DUAL-AUDIO')) audioTag = 'Dual-Audio';
208
+ else if (upper.includes('MULTI') || upper.includes('MULTILANG') || upper.includes('MULTI-AUDIO')) audioTag = 'Multi-Audio';
209
+ else if (upper.includes('HINDI')) audioTag = 'Hindi';
210
+
211
+ // Video tags
212
+ const tags = [];
213
+ if (upper.includes('DV') || upper.includes('DOLBY VISION')) tags.push('DV');
214
+ if (upper.includes('HDR10+')) tags.push('HDR10+');
215
+ else if (upper.includes('HDR10')) tags.push('HDR10');
216
+ else if (upper.includes('HDR')) tags.push('HDR');
217
+ if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC');
218
+ tags.push(audioTag);
219
+
220
+ const tagLine = tags.join(' β€’ ');
221
+ const headerLine = isSeries
222
+ ? `🎦 ${title} | S${season || 1} E${episode || 1}`
223
+ : `🎬 ${title} - ${year || ''}`;
224
+ const qualityLine = `${qEmoji} ${quality} | ${tagLine}`;
225
+ const statsLine = `🌱 ${seeders} | πŸ’Ύ ${sizeLabel} | πŸ”— ${provider}`;
226
+
227
+ return `${headerLine}\n${qualityLine}\n${statsLine}`;
228
+ }
229
+
230
+ function buildTCStreamName({ quality, seeders }) {
231
+ return `🦞 TorrentClaw | ${(quality || '').toUpperCase()} | 🌱${seeders}`;
232
+ }
233
+
234
  function processStreams(streams, type) {
235
  const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
236
 
 
297
  async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
298
  try {
299
  const isSeries = type === 'series';
300
+ const path = isSeries
301
  ? `series/${imdbId}:${season || 1}:${episode || 1}`
302
  : `movie/${imdbId}`;
303
  const url = `${TORRENTCLAW_API}/stream/${path}.json`;
 
310
  if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return [];
311
 
312
  const streams = [];
313
+ for (let idx = 0; idx < data.streams.length; idx++) {
314
+ const item = data.streams[idx];
315
  if (!item) continue;
316
 
317
+ const rawName = item.name || '';
318
  const rawTitle = item.title || '';
319
  const rawDesc = item.description || '';
320
  const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' ');
 
324
  if (!infoHash && !item.url) continue;
325
 
326
  const sizeGB = getSizeGB({ title: combined })
327
+ ?? (typeof item.size === 'number' ? item.size / 1073741824 : null)
328
  ?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null);
329
 
330
  const seeders = typeof item.seeders === 'number'
331
  ? item.seeders
332
+ : (() => {
333
+ const m = combined.match(/🌱\s*(\d+)/) || combined.match(/πŸ‘€\s*(\d+)/) || combined.match(/(\d+)\s*seed/i);
334
+ return m ? parseInt(m[1], 10) : 0;
335
+ })();
336
 
337
+ // ── Provider detection: explicit field β†’ text scan β†’ cycled fallback ──
338
+ const provider = tcDetectProvider(combined, item, idx);
 
339
  const quality = getQuality(combined);
340
+
341
+ const streamTitle = buildTCStreamTitle({
342
+ title, year, isSeries,
343
+ season, episode,
344
+ quality: quality || 'unknown',
345
+ seeders,
346
+ sizeGB,
347
+ provider,
348
+ combined,
349
+ });
350
+
351
+ const streamName = buildTCStreamName({ quality: quality || 'unknown', seeders });
352
 
353
  streams.push({
354
+ infoHash: infoHash ? infoHash.toLowerCase() : undefined,
355
+ url: infoHash ? undefined : item.url,
356
+ name: streamName,
357
+ title: streamTitle,
358
+ sources: [],
359
+ _quality: quality,
360
+ _seeders: seeders,
361
+ _sizeGB: sizeGB,
362
+ _source: 'torrentclaw',
363
  _provider: provider.toLowerCase(),
364
  });
365
  }
366
+
367
  console.log(`[TorrentClaw] ${streams.length} for ${type}/${imdbId}`);
368
  return streams;
369
  } catch (e) { console.error('[TorrentClaw]', e.message); return []; }
 
380
  let provider = providerMatch ? providerMatch[1] : 'Unknown';
381
  provider = provider.toLowerCase().replace(/\.(to|com|org|net|io)$/, '');
382
  if (provider === 'thepiratebay') provider = 'thepiratesbay';
383
+ if (provider === 'nyaa.si') provider = 'nyaa';
384
  if (provider === 'limetorrents') provider = 'limetorrent';
385
+ if (provider === 'kat') provider = 'kickasstorrents';
386
  const seeders = s.behaviorHints?.seeders
387
  ?? (() => { const m = (s.title || '').match(/πŸ‘€\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; })();
388
+ const sizeGB = getSizeGB(s);
389
  const langLine = getLangLine(origTitle);
390
  return {
391
  ...s,
392
+ title: buildStreamTitle({ title, year, seeders, sizeGB, provider, langLine }),
393
+ _quality: getQuality(s.title || s.name || ''),
394
+ _seeders: seeders,
395
+ _sizeGB: sizeGB,
396
+ _source: 'torrentio',
397
  _provider: provider,
398
  };
399
  });
 
420
  // ─── Stream handler ────────────────────────────────────────────────
421
  app.get('/stream/:type/:id.json', async (req, res) => {
422
  const { type, id } = req.params;
 
423
  const [imdbId, season = '', episode = ''] = id.split(':');
424
 
425
  const meta = await tmdbLookup(imdbId);
 
432
  ]);
433
 
434
  const torrentclaw = torrentclawR.status === 'fulfilled' ? (torrentclawR.value || []) : [];
435
+ const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
436
 
437
  const streams = processStreams([...torrentclaw, ...torrentio], type);
438
 
 
443
  // ─── Diagnostic endpoints ─────────────────────────────────────────────
444
  app.get('/diag/:type/:id', async (req, res) => {
445
  const { type, id } = req.params;
 
446
  const [imdbId, season = '', episode = ''] = id.split(':');
447
  const meta = await tmdbLookup(imdbId);
448
  const [torrentclawR, torrentioR] = await Promise.allSettled([
 
450
  fetchTorrentio(type, id, meta?.title || '', meta?.year || ''),
451
  ]);
452
  res.json({
453
+ imdbId, title: meta?.title, year: meta?.year,
454
  tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
455
  torrentclaw: { count: (torrentclawR.value || []).length, err: torrentclawR.reason?.message || null },
456
+ torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
457
  });
458
  });
459
 
460
  app.get('/diag-raw/torrentclaw/:type/:id', async (req, res) => {
461
  const { type, id } = req.params;
462
  try {
 
463
  const [imdbId, season = '', episode = ''] = id.split(':');
464
+ const path = type === 'series'
465
+ ? `series/${imdbId}:${season || 1}:${episode || 1}`
466
+ : `movie/${imdbId}`;
467
  const url = `${TORRENTCLAW_API}/stream/${path}.json`;
468
  const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } });
469
  res.json((data.streams || []).slice(0, 3));