Update index.js
Browse files
index.js
CHANGED
|
@@ -14,13 +14,9 @@ app.use((req, res, next) => {
|
|
| 14 |
next();
|
| 15 |
});
|
| 16 |
|
| 17 |
-
const TMDB_KEY
|
| 18 |
-
const UA
|
| 19 |
-
const
|
| 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;
|
|
@@ -32,7 +28,7 @@ const QUALITY_RANK = {
|
|
| 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',
|
|
@@ -46,7 +42,9 @@ const ALLOWED_PROVIDERS = [
|
|
| 46 |
'limetorrent',
|
| 47 |
'kickasstorrents',
|
| 48 |
'animetosho',
|
| 49 |
-
'tokyotosho'
|
|
|
|
|
|
|
| 50 |
];
|
| 51 |
|
| 52 |
const TR = [
|
|
@@ -62,27 +60,6 @@ const TR = [
|
|
| 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 |
}
|
|
@@ -224,111 +201,63 @@ async function tmdbLookup(imdbId) {
|
|
| 224 |
} catch (e) { console.error('[TMDB]', e.message); return null; }
|
| 225 |
}
|
| 226 |
|
| 227 |
-
|
|
|
|
| 228 |
try {
|
| 229 |
-
const
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 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
|
| 277 |
-
if (!
|
| 278 |
-
|
| 279 |
-
const
|
| 280 |
-
const
|
| 281 |
-
|
| 282 |
-
const
|
| 283 |
-
|
| 284 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
streams.push({
|
| 286 |
-
infoHash:
|
| 287 |
-
|
| 288 |
-
|
|
|
|
| 289 |
sources: [],
|
| 290 |
-
_quality: getQuality(
|
| 291 |
-
_seeders:
|
| 292 |
_sizeGB: sizeGB,
|
| 293 |
-
_source: '
|
| 294 |
-
_provider: '
|
| 295 |
});
|
| 296 |
}
|
| 297 |
-
console.log(`[
|
| 298 |
return streams;
|
| 299 |
-
} catch (e) { console.error('[
|
| 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 ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -356,19 +285,11 @@ app.get('/stream/:type/:id.json', async (req, res) => {
|
|
| 356 |
const title = meta?.title || '';
|
| 357 |
const year = meta?.year || '';
|
| 358 |
|
| 359 |
-
const
|
| 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(
|
| 370 |
|
| 371 |
-
console.log(`[stream] ${type}/${id} β
|
| 372 |
res.json({ streams });
|
| 373 |
});
|
| 374 |
|
|
@@ -378,28 +299,23 @@ app.get('/diag/:type/:id', async (req, res) => {
|
|
| 378 |
const isSeries = type === 'series';
|
| 379 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 380 |
const meta = await tmdbLookup(imdbId);
|
| 381 |
-
const
|
| 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:
|
| 389 |
-
|
| 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
|
| 399 |
-
const
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
})
|
|
|
|
| 403 |
} catch (e) { res.json({ error: e.message }); }
|
| 404 |
});
|
| 405 |
|
|
|
|
| 14 |
next();
|
| 15 |
});
|
| 16 |
|
| 17 |
+
const TMDB_KEY = '83d364331c40bfbe29858aeed82f45cc';
|
| 18 |
+
const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
| 19 |
+
const TORRENTCLAW_API = 'https://torrentclaw.com/api/stremio';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
const MAX_SIZE_GB_MOVIE = 4;
|
| 22 |
const MAX_SIZE_GB_SERIES = 1.5;
|
|
|
|
| 28 |
'4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
|
| 29 |
};
|
| 30 |
|
| 31 |
+
const PRIORITY_PROVIDERS = ['yts', 'knaben', 'torrentsdb', 'eztv', 'nyaasi', 'thepiratebay', 'torrentclaw'];
|
| 32 |
|
| 33 |
const ALLOWED_PROVIDERS = [
|
| 34 |
'yts',
|
|
|
|
| 42 |
'limetorrent',
|
| 43 |
'kickasstorrents',
|
| 44 |
'animetosho',
|
| 45 |
+
'tokyotosho',
|
| 46 |
+
'torrentclaw',
|
| 47 |
+
'unknown'
|
| 48 |
];
|
| 49 |
|
| 50 |
const TR = [
|
|
|
|
| 60 |
'https://tracker.zhuqiy.com:443/announce',
|
| 61 |
].map(t => '&tr=' + encodeURIComponent(t)).join('');
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
function buildMagnet(hash, name) {
|
| 64 |
return 'magnet:?xt=urn:btih:' + hash.toLowerCase() + '&dn=' + encodeURIComponent(name) + TR;
|
| 65 |
}
|
|
|
|
| 201 |
} catch (e) { console.error('[TMDB]', e.message); return null; }
|
| 202 |
}
|
| 203 |
|
| 204 |
+
// βββ TorrentClaw scraper (replaces YTS / Knaben / Torrentio) βββββββββββ
|
| 205 |
+
async function scrapeTorrentClaw(type, imdbId, title, year, season, episode) {
|
| 206 |
try {
|
| 207 |
+
const isSeries = type === 'series';
|
| 208 |
+
const path = isSeries
|
| 209 |
+
? `series/${imdbId}:${season || 1}:${episode || 1}`
|
| 210 |
+
: `movie/${imdbId}`;
|
| 211 |
+
const url = `${TORRENTCLAW_API}/stream/${path}.json`;
|
| 212 |
+
|
| 213 |
+
const { data } = await axios.get(url, {
|
| 214 |
+
timeout: 15000,
|
| 215 |
+
headers: { 'User-Agent': UA, 'Accept': 'application/json' },
|
| 216 |
+
});
|
| 217 |
+
|
| 218 |
+
if (!data || !Array.isArray(data.streams) || data.streams.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 220 |
const streams = [];
|
| 221 |
+
for (const item of data.streams) {
|
| 222 |
+
if (!item) continue;
|
| 223 |
+
|
| 224 |
+
const rawName = item.name || '';
|
| 225 |
+
const rawTitle = item.title || '';
|
| 226 |
+
const rawDesc = item.description || '';
|
| 227 |
+
const combined = `${rawName} ${rawTitle} ${rawDesc}`.replace(/\n/g, ' ');
|
| 228 |
+
|
| 229 |
+
const infoHashMatch = (item.url || '').match(/btih:([a-fA-F0-9]{32,40})/i);
|
| 230 |
+
const infoHash = item.infoHash || (infoHashMatch ? infoHashMatch[1] : null);
|
| 231 |
+
if (!infoHash && !item.url) continue;
|
| 232 |
+
|
| 233 |
+
const sizeGB = getSizeGB({ title: combined })
|
| 234 |
+
?? (typeof item.size === 'number' ? item.size / 1073741824 : null)
|
| 235 |
+
?? (typeof item.bytes === 'number' ? item.bytes / 1073741824 : null);
|
| 236 |
+
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A';
|
| 237 |
+
|
| 238 |
+
const seeders = typeof item.seeders === 'number'
|
| 239 |
+
? item.seeders
|
| 240 |
+
: (() => { const m = combined.match(/π±\s*(\d+)/) || combined.match(/π€\s*(\d+)/) || combined.match(/(\d+)\s*seed/i); return m ? parseInt(m[1], 10) : 0; })();
|
| 241 |
+
|
| 242 |
+
const provider = (item.provider || item.source || item.indexer || 'TorrentClaw').toString().toLowerCase();
|
| 243 |
+
const langLine = getLangLine(combined);
|
| 244 |
+
|
| 245 |
streams.push({
|
| 246 |
+
infoHash: infoHash ? infoHash.toLowerCase() : undefined,
|
| 247 |
+
url: infoHash ? undefined : item.url,
|
| 248 |
+
name: rawName || rawTitle,
|
| 249 |
+
title: `βοΈ ${title}${year ? ' (' + year + ')' : ''}\nπ± ${seeders}\nπΎ ${sizeStr}\nπ
TorrentClaw${langLine ? '\nπ ' + langLine : ''}`,
|
| 250 |
sources: [],
|
| 251 |
+
_quality: getQuality(combined),
|
| 252 |
+
_seeders: seeders,
|
| 253 |
_sizeGB: sizeGB,
|
| 254 |
+
_source: 'torrentclaw',
|
| 255 |
+
_provider: 'torrentclaw',
|
| 256 |
});
|
| 257 |
}
|
| 258 |
+
console.log(`[TorrentClaw] ${streams.length} for ${type}/${imdbId}`);
|
| 259 |
return streams;
|
| 260 |
+
} catch (e) { console.error('[TorrentClaw]', e.message); return []; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
}
|
| 262 |
|
| 263 |
// βββ Manifest ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 285 |
const title = meta?.title || '';
|
| 286 |
const year = meta?.year || '';
|
| 287 |
|
| 288 |
+
const torrentclaw = await scrapeTorrentClaw(type, imdbId, title, year, season, episode);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
+
const streams = processStreams(torrentclaw, type);
|
| 291 |
|
| 292 |
+
console.log(`[stream] ${type}/${id} β torrentclaw:${torrentclaw.length} final:${streams.length}`);
|
| 293 |
res.json({ streams });
|
| 294 |
});
|
| 295 |
|
|
|
|
| 299 |
const isSeries = type === 'series';
|
| 300 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 301 |
const meta = await tmdbLookup(imdbId);
|
| 302 |
+
const torrentclaw = await scrapeTorrentClaw(type, imdbId, meta?.title || '', meta?.year || '', season, episode);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 303 |
res.json({
|
| 304 |
imdbId, title: meta?.title, year: meta?.year, isSeries,
|
| 305 |
+
tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
|
| 306 |
+
torrentclaw: { count: torrentclaw.length },
|
|
|
|
|
|
|
| 307 |
});
|
| 308 |
});
|
| 309 |
|
| 310 |
app.get('/diag-raw/:type/:id', async (req, res) => {
|
| 311 |
const { type, id } = req.params;
|
| 312 |
try {
|
| 313 |
+
const isSeries = type === 'series';
|
| 314 |
+
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 315 |
+
const path = isSeries ? `series/${imdbId}:${season || 1}:${episode || 1}` : `movie/${imdbId}`;
|
| 316 |
+
const url = `${TORRENTCLAW_API}/stream/${path}.json`;
|
| 317 |
+
const { data } = await axios.get(url, { timeout: 15000, headers: { 'User-Agent': UA, 'Accept': 'application/json' } });
|
| 318 |
+
res.json((data.streams || []).slice(0, 3));
|
| 319 |
} catch (e) { res.json({ error: e.message }); }
|
| 320 |
});
|
| 321 |
|