Update index.js
Browse files
index.js
CHANGED
|
@@ -17,7 +17,11 @@ 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 TORRENTCLAW_API = 'https://torrentclaw.com/api/stremio';
|
| 20 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
const MAX_SIZE_GB_MOVIE = 4;
|
| 23 |
const MAX_SIZE_GB_SERIES = 1.5;
|
|
@@ -29,25 +33,9 @@ const QUALITY_RANK = {
|
|
| 29 |
'4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
|
| 30 |
};
|
| 31 |
|
| 32 |
-
const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw'
|
| 33 |
-
|
| 34 |
-
|
| 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',
|
|
@@ -66,6 +54,27 @@ 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';
|
|
@@ -89,7 +98,7 @@ function getSizeGB(stream) {
|
|
| 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 |
|
|
@@ -140,12 +149,44 @@ function getLangLine(str = '') {
|
|
| 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);
|
|
@@ -203,7 +244,7 @@ async function tmdbLookup(imdbId) {
|
|
| 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';
|
|
@@ -227,6 +268,7 @@ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
|
|
| 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);
|
|
@@ -235,22 +277,40 @@ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
|
|
| 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' : '
|
| 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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 245 |
const langLine = getLangLine(combined);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
streams.push({
|
| 248 |
infoHash: infoHash ? infoHash.toLowerCase() : undefined,
|
| 249 |
url: infoHash ? undefined : item.url,
|
| 250 |
-
name:
|
| 251 |
-
title:
|
| 252 |
sources: [],
|
| 253 |
-
_quality:
|
| 254 |
_seeders: seeders,
|
| 255 |
_sizeGB: sizeGB,
|
| 256 |
_source: 'torrentclaw',
|
|
@@ -262,80 +322,58 @@ async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
|
|
| 262 |
} catch (e) { console.error('[TorrentClaw]', e.message); return []; }
|
| 263 |
}
|
| 264 |
|
| 265 |
-
// βββ Torrentio scraper β
|
| 266 |
-
async function
|
| 267 |
try {
|
| 268 |
-
const
|
| 269 |
-
const
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
const
|
| 286 |
-
|
| 287 |
-
const
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
if (
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 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 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 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:
|
| 335 |
_provider: provider,
|
| 336 |
-
}
|
| 337 |
-
}
|
| 338 |
-
console.log(`[Torrentio] ${streams.length} for ${type}/${
|
| 339 |
return streams;
|
| 340 |
} catch (e) { console.error('[Torrentio]', e.message); return []; }
|
| 341 |
}
|
|
@@ -358,59 +396,65 @@ app.get('/manifest.json', (_, res) => {
|
|
| 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 |
-
|
| 368 |
-
const [torrentclaw, torrentio] = await Promise.all([
|
| 369 |
scrapeTorrentClaw(type, imdbId, title, year, season, episode),
|
| 370 |
-
|
| 371 |
]);
|
| 372 |
|
| 373 |
-
const
|
| 374 |
-
const
|
| 375 |
|
| 376 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 389 |
]);
|
| 390 |
-
|
| 391 |
res.json({
|
| 392 |
-
imdbId, title: meta?.title, year: meta?.year, isSeries
|
| 393 |
tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
|
| 394 |
-
torrentclaw: { count:
|
| 395 |
-
torrentio: { count:
|
| 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 =
|
| 404 |
-
|
| 405 |
-
const
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
} catch (e) { res.json({ error: e.message }); }
|
| 415 |
});
|
| 416 |
|
|
|
|
| 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_BASE = 'https://torrentio.strem.fun';
|
| 21 |
+
|
| 22 |
+
const PROXY_1 = 'https://corsproxy.io/?url=';
|
| 23 |
+
const PROXY_2 = 'https://api.codetabs.com/v1/proxy?quest=';
|
| 24 |
+
const PROXY_3 = 'https://thingproxy.freeboard.io/fetch/';
|
| 25 |
|
| 26 |
const MAX_SIZE_GB_MOVIE = 4;
|
| 27 |
const MAX_SIZE_GB_SERIES = 1.5;
|
|
|
|
| 33 |
'4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
|
| 34 |
};
|
| 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 |
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,
|
| 61 |
+
PROXY_1 + encodeURIComponent(url),
|
| 62 |
+
PROXY_2 + encodeURIComponent(url),
|
| 63 |
+
PROXY_3 + encodeURIComponent(url),
|
| 64 |
+
];
|
| 65 |
+
|
| 66 |
+
for (const proxyUrl of proxies) {
|
| 67 |
+
try {
|
| 68 |
+
const response = await axios.get(proxyUrl, { timeout: 12000, headers: { 'User-Agent': UA }, ...options });
|
| 69 |
+
return response;
|
| 70 |
+
} catch (e) {
|
| 71 |
+
console.warn(`[Proxy] Failed: ${proxyUrl.substring(0, 50)}... Error: ${e.message}`);
|
| 72 |
+
continue;
|
| 73 |
+
}
|
| 74 |
+
}
|
| 75 |
+
throw new Error('All proxy attempts failed');
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
function getQuality(str = '') {
|
| 79 |
const s = str.toLowerCase();
|
| 80 |
if (s.includes('4k') || s.includes('2160p') || s.includes('uhd')) return '4k';
|
|
|
|
| 98 |
function getSeeders(stream) {
|
| 99 |
if (stream._seeders != null) return stream._seeders;
|
| 100 |
if (stream.behaviorHints?.seeders) return stream.behaviorHints.seeders;
|
| 101 |
+
const m = (stream.title || '').match(/π±\s*(\d+)/) || (stream.title || '').match(/π€\s*(\d+)/);
|
| 102 |
return m ? parseInt(m[1], 10) : 0;
|
| 103 |
}
|
| 104 |
|
|
|
|
| 149 |
return [...new Set(found)].join(' / ');
|
| 150 |
}
|
| 151 |
|
| 152 |
+
// βββ Unified TorrentClaw-style title template ββββββββββββββββββββββββββ
|
| 153 |
+
// Used by BOTH TorrentClaw and Torrentio so every stream in the list looks
|
| 154 |
+
// consistent, matching the original TorrentClaw addon's format:
|
| 155 |
+
// π¦ Title | S1 E1 (series) / π¬ Title - 2026 (movie)
|
| 156 |
+
// π₯ 1080p | HDR10 β’ HEVC β’ English
|
| 157 |
+
// π± 42 | πΎ 1.2 GB | π YTS
|
| 158 |
+
function qualityEmoji(quality) {
|
| 159 |
+
switch (quality) {
|
| 160 |
+
case '4k':
|
| 161 |
+
case '2160p': return 'π';
|
| 162 |
+
case '1080p': return 'π₯';
|
| 163 |
+
case '720p': return 'π';
|
| 164 |
+
case '576p':
|
| 165 |
+
case '480p': return 'π±';
|
| 166 |
+
case 'webrip':
|
| 167 |
+
case 'webdl': return 'π';
|
| 168 |
+
default: return 'π₯';
|
| 169 |
+
}
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
function buildUnifiedTitle({ isSeries, title, year, season, episode, quality, tagLine, seeders, sizeStr, provider }) {
|
| 173 |
+
const header = isSeries
|
| 174 |
+
? `π¦ ${title} | S${season || 1} E${episode || 1}`
|
| 175 |
+
: `π¬ ${title}${year ? ' - ' + year : ''}`;
|
| 176 |
+
const qLine = `${qualityEmoji(quality)} ${quality || 'unknown'}${tagLine ? ' | ' + tagLine : ''}`;
|
| 177 |
+
const statsLine = `π± ${seeders} | πΎ ${sizeStr} | π ${provider}`;
|
| 178 |
+
return `${header}\n${qLine}\n${statsLine}`;
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
function buildUnifiedName({ quality, seeders, sourceLabel }) {
|
| 182 |
+
return `${sourceLabel} | ${(quality || 'unknown').toUpperCase()} | π±${seeders}`;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
function processStreams(streams, type) {
|
| 186 |
const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
|
| 187 |
|
| 188 |
const filtered = streams.filter(s => {
|
| 189 |
if (!s.infoHash && !s.url) return false;
|
|
|
|
| 190 |
const q = s._quality || getQuality(s.title || s.name || '');
|
| 191 |
if (!q || !ALLOWED_QUALITIES.includes(q)) return false;
|
| 192 |
const sizeGB = s._sizeGB ?? getSizeGB(s);
|
|
|
|
| 244 |
} catch (e) { console.error('[TMDB]', e.message); return null; }
|
| 245 |
}
|
| 246 |
|
| 247 |
+
// βββ TorrentClaw scraper βββββββββββββββββββββββββββββββββββββββββββββ
|
| 248 |
async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
|
| 249 |
try {
|
| 250 |
const isSeries = type === 'series';
|
|
|
|
| 268 |
const rawTitle = item.title || '';
|
| 269 |
const rawDesc = item.description || '';
|
| 270 |
const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' ');
|
| 271 |
+
const upper = combined.toUpperCase();
|
| 272 |
|
| 273 |
const infoHashMatch = (item.url || '').match(/btih:([a-fA-F0-9]{32,40})/i);
|
| 274 |
const infoHash = item.infoHash || (infoHashMatch ? infoHashMatch[1] : null);
|
|
|
|
| 277 |
const sizeGB = getSizeGB({ title: combined })
|
| 278 |
?? (typeof item.size === 'number' ? item.size / 1073741824 : null)
|
| 279 |
?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null);
|
| 280 |
+
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'Unknown Size';
|
| 281 |
|
| 282 |
const seeders = typeof item.seeders === 'number'
|
| 283 |
? item.seeders
|
| 284 |
: (() => { const m = combined.match(/π±\s*(\d+)/) || combined.match(/π€\s*(\d+)/) || combined.match(/(\d+)\s*seed/i); return m ? parseInt(m[1], 10) : 0; })();
|
| 285 |
|
| 286 |
+
const provider = (item.provider || item.source || item.indexer || 'TorrentClaw').toString();
|
| 287 |
+
const quality = getQuality(combined) || getQuality(upper);
|
| 288 |
+
|
| 289 |
+
// Build a HEVC/HDR/DV/audio tag line, same spirit as the original TorrentClaw output
|
| 290 |
+
const tags = [];
|
| 291 |
+
if (upper.includes('DV') || upper.includes('DOLBY VISION')) tags.push('DV');
|
| 292 |
+
if (upper.includes('HDR10+')) tags.push('HDR10+');
|
| 293 |
+
else if (upper.includes('HDR10')) tags.push('HDR10');
|
| 294 |
+
else if (upper.includes('HDR')) tags.push('HDR');
|
| 295 |
+
if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC');
|
| 296 |
const langLine = getLangLine(combined);
|
| 297 |
+
if (langLine) tags.push(langLine);
|
| 298 |
+
else tags.push('English');
|
| 299 |
+
const tagLine = tags.join(' β’ ');
|
| 300 |
+
|
| 301 |
+
const unifiedTitle = buildUnifiedTitle({
|
| 302 |
+
isSeries: type === 'series', title, year, season, episode,
|
| 303 |
+
quality, tagLine, seeders, sizeStr, provider,
|
| 304 |
+
});
|
| 305 |
+
const unifiedName = buildUnifiedName({ quality, seeders, sourceLabel: 'π¦ TorrentClaw' });
|
| 306 |
|
| 307 |
streams.push({
|
| 308 |
infoHash: infoHash ? infoHash.toLowerCase() : undefined,
|
| 309 |
url: infoHash ? undefined : item.url,
|
| 310 |
+
name: unifiedName,
|
| 311 |
+
title: unifiedTitle,
|
| 312 |
sources: [],
|
| 313 |
+
_quality: quality,
|
| 314 |
_seeders: seeders,
|
| 315 |
_sizeGB: sizeGB,
|
| 316 |
_source: 'torrentclaw',
|
|
|
|
| 322 |
} catch (e) { console.error('[TorrentClaw]', e.message); return []; }
|
| 323 |
}
|
| 324 |
|
| 325 |
+
// βββ Torrentio scraper (config/logic from the original Torrentio script) β
|
| 326 |
+
async function fetchTorrentio(type, id, title, year, season, episode) {
|
| 327 |
try {
|
| 328 |
+
const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
|
| 329 |
+
const { data } = await fetchWithProxy(url);
|
| 330 |
+
|
| 331 |
+
const streams = (data.streams || []).map(s => {
|
| 332 |
+
const origTitle = s.title || '';
|
| 333 |
+
const providerMatch = origTitle.match(/βοΈ\s*(\S+)/);
|
| 334 |
+
let provider = providerMatch ? providerMatch[1] : 'Unknown';
|
| 335 |
+
provider = provider.toLowerCase().replace(/\.(to|com|org|net|io)$/, '');
|
| 336 |
+
if (provider === 'thepiratebay') provider = 'thepiratesbay';
|
| 337 |
+
if (provider === 'nyaa.si') provider = 'nyaa';
|
| 338 |
+
if (provider === 'limetorrents') provider = 'limetorrent';
|
| 339 |
+
if (provider === 'kat') provider = 'kickasstorrents';
|
| 340 |
+
|
| 341 |
+
const seeders = s.behaviorHints?.seeders
|
| 342 |
+
?? (() => { const m = origTitle.match(/π€\s*(\d+)/) || origTitle.match(/π±\s*(\d+)/); return m ? parseInt(m[1], 10) : 0; })();
|
| 343 |
+
const sizeGB = getSizeGB(s);
|
| 344 |
+
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'Unknown Size';
|
| 345 |
+
const quality = getQuality(origTitle) || getQuality(s.name || '');
|
| 346 |
+
|
| 347 |
+
const upper = origTitle.toUpperCase();
|
| 348 |
+
const tags = [];
|
| 349 |
+
if (upper.includes('DV') || upper.includes('DOLBY VISION')) tags.push('DV');
|
| 350 |
+
if (upper.includes('HDR10+')) tags.push('HDR10+');
|
| 351 |
+
else if (upper.includes('HDR10')) tags.push('HDR10');
|
| 352 |
+
else if (upper.includes('HDR')) tags.push('HDR');
|
| 353 |
+
if (upper.includes('HEVC') || upper.includes('X265') || upper.includes('H265')) tags.push('HEVC');
|
| 354 |
+
const langLine = getLangLine(origTitle);
|
| 355 |
+
if (langLine) tags.push(langLine);
|
| 356 |
+
else tags.push('English');
|
| 357 |
+
const tagLine = tags.join(' β’ ');
|
| 358 |
+
|
| 359 |
+
const unifiedTitle = buildUnifiedTitle({
|
| 360 |
+
isSeries: type === 'series', title, year, season, episode,
|
| 361 |
+
quality, tagLine, seeders, sizeStr, provider,
|
| 362 |
+
});
|
| 363 |
+
const unifiedName = buildUnifiedName({ quality, seeders, sourceLabel: 'βοΈ Torrentio' });
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
+
return {
|
| 366 |
+
...s,
|
| 367 |
+
name: unifiedName,
|
| 368 |
+
title: unifiedTitle,
|
|
|
|
|
|
|
| 369 |
_quality: quality,
|
| 370 |
_seeders: seeders,
|
| 371 |
_sizeGB: sizeGB,
|
| 372 |
+
_source: 'torrentio',
|
| 373 |
_provider: provider,
|
| 374 |
+
};
|
| 375 |
+
});
|
| 376 |
+
console.log(`[Torrentio] ${streams.length} for ${type}/${id}`);
|
| 377 |
return streams;
|
| 378 |
} catch (e) { console.error('[Torrentio]', e.message); return []; }
|
| 379 |
}
|
|
|
|
| 396 |
// βββ Stream handler ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 397 |
app.get('/stream/:type/:id.json', async (req, res) => {
|
| 398 |
const { type, id } = req.params;
|
| 399 |
+
const isSeries = type === 'series';
|
| 400 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 401 |
|
| 402 |
const meta = await tmdbLookup(imdbId);
|
| 403 |
const title = meta?.title || '';
|
| 404 |
const year = meta?.year || '';
|
| 405 |
|
| 406 |
+
const [torrentclawR, torrentioR] = await Promise.allSettled([
|
|
|
|
| 407 |
scrapeTorrentClaw(type, imdbId, title, year, season, episode),
|
| 408 |
+
fetchTorrentio(type, id, title, year, season, episode),
|
| 409 |
]);
|
| 410 |
|
| 411 |
+
const torrentclaw = torrentclawR.status === 'fulfilled' ? (torrentclawR.value || []) : [];
|
| 412 |
+
const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
|
| 413 |
|
| 414 |
+
const streams = processStreams([...torrentclaw, ...torrentio], type);
|
| 415 |
+
|
| 416 |
+
console.log(`[stream] ${type}/${id} β torrentclaw:${torrentclaw.length} torrentio:${torrentio.length} final:${streams.length}`);
|
| 417 |
res.json({ streams });
|
| 418 |
});
|
| 419 |
|
| 420 |
// βββ Diagnostic endpoints βββββββββββββββββββββββββββββββββββββββββββββ
|
| 421 |
app.get('/diag/:type/:id', async (req, res) => {
|
| 422 |
const { type, id } = req.params;
|
| 423 |
+
const isSeries = type === 'series';
|
| 424 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 425 |
const meta = await tmdbLookup(imdbId);
|
| 426 |
+
const [torrentclawR, torrentioR] = await Promise.allSettled([
|
|
|
|
| 427 |
scrapeTorrentClaw(type, imdbId, meta?.title || '', meta?.year || '', season, episode),
|
| 428 |
+
fetchTorrentio(type, id, meta?.title || '', meta?.year || '', season, episode),
|
| 429 |
]);
|
|
|
|
| 430 |
res.json({
|
| 431 |
+
imdbId, title: meta?.title, year: meta?.year, isSeries,
|
| 432 |
tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
|
| 433 |
+
torrentclaw: { count: (torrentclawR.value || []).length, err: torrentclawR.reason?.message || null },
|
| 434 |
+
torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
|
| 435 |
});
|
| 436 |
});
|
| 437 |
|
| 438 |
+
app.get('/diag-raw/torrentclaw/:type/:id', async (req, res) => {
|
| 439 |
const { type, id } = req.params;
|
| 440 |
try {
|
| 441 |
+
const isSeries = type === 'series';
|
| 442 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 443 |
+
const path = isSeries ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`;
|
| 444 |
+
const url = `${TORRENTCLAW_API}/stream/${path}.json`;
|
| 445 |
+
const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } });
|
| 446 |
+
res.json((data.streams || []).slice(0, 3));
|
| 447 |
+
} catch (e) { res.json({ error: e.message }); }
|
| 448 |
+
});
|
| 449 |
|
| 450 |
+
app.get('/diag-raw/torrentio/:type/:id', async (req, res) => {
|
| 451 |
+
const { type, id } = req.params;
|
| 452 |
+
try {
|
| 453 |
+
const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
|
| 454 |
+
const { data } = await fetchWithProxy(url);
|
| 455 |
+
res.json((data.streams || []).slice(0, 3).map(s => ({
|
| 456 |
+
name: s.name, title: s.title, behaviorHints: s.behaviorHints,
|
| 457 |
+
})));
|
| 458 |
} catch (e) { res.json({ error: e.message }); }
|
| 459 |
});
|
| 460 |
|