Update index.js
Browse files
index.js
CHANGED
|
@@ -32,7 +32,7 @@ const QUALITY_RANK = {
|
|
| 32 |
'4k': 5, '2160p': 5, 'webrip': 6, 'webdl': 6,
|
| 33 |
};
|
| 34 |
|
| 35 |
-
const PRIORITY_PROVIDERS = ['yts', 'knaben', '
|
| 36 |
|
| 37 |
const ALLOWED_PROVIDERS = [
|
| 38 |
'yts',
|
|
@@ -47,9 +47,17 @@ const ALLOWED_PROVIDERS = [
|
|
| 47 |
'kickasstorrents',
|
| 48 |
'animetosho',
|
| 49 |
'tokyotosho',
|
| 50 |
-
'torrentproject',
|
| 51 |
];
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
const TR = [
|
| 54 |
'udp://tracker.opentrackr.org:1337/announce',
|
| 55 |
'udp://open.stealth.si:80/announce',
|
|
@@ -63,7 +71,6 @@ const TR = [
|
|
| 63 |
'https://tracker.zhuqiy.com:443/announce',
|
| 64 |
].map(t => '&tr=' + encodeURIComponent(t)).join('');
|
| 65 |
|
| 66 |
-
// โโโ Proxy fallback function โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
| 67 |
async function fetchWithProxy(url, options = {}) {
|
| 68 |
const proxies = [
|
| 69 |
url,
|
|
@@ -71,14 +78,12 @@ async function fetchWithProxy(url, options = {}) {
|
|
| 71 |
PROXY_2 + encodeURIComponent(url),
|
| 72 |
PROXY_3 + encodeURIComponent(url),
|
| 73 |
];
|
| 74 |
-
|
| 75 |
for (const proxyUrl of proxies) {
|
| 76 |
try {
|
| 77 |
const response = await axios.get(proxyUrl, { timeout: 12000, headers: { 'User-Agent': UA }, ...options });
|
| 78 |
return response;
|
| 79 |
} catch (e) {
|
| 80 |
-
console.warn(`[Proxy] Failed: ${proxyUrl.substring(0, 50)}...
|
| 81 |
-
continue;
|
| 82 |
}
|
| 83 |
}
|
| 84 |
throw new Error('All proxy attempts failed');
|
|
@@ -162,6 +167,16 @@ function getLangLine(str = '') {
|
|
| 162 |
return [...new Set(found)].join(' / ');
|
| 163 |
}
|
| 164 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
function processStreams(streams, type) {
|
| 166 |
const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
|
| 167 |
|
|
@@ -172,6 +187,7 @@ function processStreams(streams, type) {
|
|
| 172 |
if (!q || !ALLOWED_QUALITIES.includes(q)) return false;
|
| 173 |
const sizeGB = s._sizeGB ?? getSizeGB(s);
|
| 174 |
if (sizeGB !== null && sizeGB > maxSize) return false;
|
|
|
|
| 175 |
return true;
|
| 176 |
});
|
| 177 |
|
|
@@ -219,8 +235,8 @@ async function tmdbLookup(imdbId) {
|
|
| 219 |
);
|
| 220 |
const movie = data.movie_results?.[0];
|
| 221 |
const tv = data.tv_results?.[0];
|
| 222 |
-
if (movie) return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4) };
|
| 223 |
-
if (tv) return { title: tv.name || tv.original_name,
|
| 224 |
return null;
|
| 225 |
} catch (e) { console.error('[TMDB]', e.message); return null; }
|
| 226 |
}
|
|
@@ -242,14 +258,14 @@ async function scrapeYTS(imdbId) {
|
|
| 242 |
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : (t.size || '');
|
| 243 |
const langLine = getLangLine(qualityStr);
|
| 244 |
streams.push({
|
| 245 |
-
infoHash:
|
| 246 |
-
name:
|
| 247 |
-
title:
|
| 248 |
-
sources:
|
| 249 |
-
_quality:
|
| 250 |
-
_seeders:
|
| 251 |
-
_sizeGB:
|
| 252 |
-
_source:
|
| 253 |
_provider: 'yts',
|
| 254 |
});
|
| 255 |
}
|
|
@@ -259,23 +275,45 @@ async function scrapeYTS(imdbId) {
|
|
| 259 |
} catch (e) { console.error('[YTS]', e.message); return []; }
|
| 260 |
}
|
| 261 |
|
| 262 |
-
async function scrapeKnaben(title, year, isSeries, season, episode) {
|
| 263 |
if (!title) return [];
|
| 264 |
try {
|
| 265 |
let query = title;
|
| 266 |
-
if (year && !isSeries) query += ' ' + year;
|
| 267 |
if (isSeries && season && episode)
|
| 268 |
query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
|
| 269 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
const { data } = await axios.post(
|
| 271 |
'https://api.knaben.org/v1',
|
| 272 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
{ timeout: 14000, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': UA } }
|
| 274 |
);
|
|
|
|
| 275 |
if (!data?.hits?.length) return [];
|
|
|
|
| 276 |
const streams = [];
|
| 277 |
for (const hit of data.hits) {
|
| 278 |
if (!hit.magnetUrl && !hit.hash) continue;
|
|
|
|
|
|
|
| 279 |
const magnet = hit.magnetUrl || buildMagnet(hit.hash, hit.title || '');
|
| 280 |
const hashMatch = magnet.match(/btih:([a-fA-F0-9]{32,40})/i);
|
| 281 |
const hash = hashMatch ? hashMatch[1] : (hit.hash || '');
|
|
@@ -284,62 +322,22 @@ async function scrapeKnaben(title, year, isSeries, season, episode) {
|
|
| 284 |
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : '';
|
| 285 |
const langLine = getLangLine(hit.title || '');
|
| 286 |
streams.push({
|
| 287 |
-
infoHash:
|
| 288 |
-
name:
|
| 289 |
-
title:
|
| 290 |
-
sources:
|
| 291 |
-
_quality:
|
| 292 |
-
_seeders:
|
| 293 |
-
_sizeGB:
|
| 294 |
-
_source:
|
| 295 |
_provider: 'knaben',
|
| 296 |
});
|
| 297 |
}
|
| 298 |
-
console.log(`[Knaben] ${streams.length} for "${query}"`);
|
| 299 |
return streams;
|
| 300 |
} catch (e) { console.error('[Knaben]', e.message); return []; }
|
| 301 |
}
|
| 302 |
|
| 303 |
-
async function scrapeTorrentProject(title, year, isSeries, season, episode) {
|
| 304 |
-
if (!title) return [];
|
| 305 |
-
try {
|
| 306 |
-
let query = title;
|
| 307 |
-
if (year && !isSeries) query += ' ' + year;
|
| 308 |
-
if (isSeries && season && episode)
|
| 309 |
-
query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
|
| 310 |
-
// filter: 2000 = video/movies, 2101 = TV series
|
| 311 |
-
const filter = isSeries ? 2101 : 2000;
|
| 312 |
-
const url = `https://torrentproject.com.se/?s=${encodeURIComponent(query)}&out=json&filter=${filter}&num=40&orderby=best`;
|
| 313 |
-
const { data } = await axios.get(url, { timeout: 14000, headers: { 'User-Agent': UA } });
|
| 314 |
-
if (!data || typeof data !== 'object') return [];
|
| 315 |
-
const streams = [];
|
| 316 |
-
for (const key of Object.keys(data)) {
|
| 317 |
-
if (key === 'total_found') continue;
|
| 318 |
-
const item = data[key];
|
| 319 |
-
if (!item.torrent_hash) continue;
|
| 320 |
-
const hash = item.torrent_hash.toLowerCase();
|
| 321 |
-
const sizeGB = item.torrent_size ? item.torrent_size / 1073741824 : null;
|
| 322 |
-
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : '';
|
| 323 |
-
const quality = getQuality(item.title || '');
|
| 324 |
-
if (!quality) continue;
|
| 325 |
-
const langLine = getLangLine(item.title || '');
|
| 326 |
-
streams.push({
|
| 327 |
-
infoHash: hash,
|
| 328 |
-
name: item.title || '',
|
| 329 |
-
title: `โ๏ธ ${title}${year ? ' (' + year + ')' : ''}\n๐ฑ ${item.seeds || 0}\n๐พ ${sizeStr}\n๐
TorrentProject${langLine ? '\n๐ ' + langLine : ''}`,
|
| 330 |
-
sources: [buildMagnet(hash, item.title || '')],
|
| 331 |
-
_quality: quality,
|
| 332 |
-
_seeders: item.seeds || 0,
|
| 333 |
-
_sizeGB: sizeGB,
|
| 334 |
-
_source: 'torrentproject',
|
| 335 |
-
_provider: 'torrentproject',
|
| 336 |
-
});
|
| 337 |
-
}
|
| 338 |
-
console.log(`[TorrentProject] ${streams.length} for "${query}"`);
|
| 339 |
-
return streams;
|
| 340 |
-
} catch (e) { console.error('[TorrentProject]', e.message); return []; }
|
| 341 |
-
}
|
| 342 |
-
|
| 343 |
async function fetchTorrentio(type, id, title, year) {
|
| 344 |
try {
|
| 345 |
const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
|
|
@@ -359,11 +357,11 @@ async function fetchTorrentio(type, id, title, year) {
|
|
| 359 |
const langLine = getLangLine(origTitle);
|
| 360 |
return {
|
| 361 |
...s,
|
| 362 |
-
title:
|
| 363 |
-
_quality:
|
| 364 |
-
_seeders:
|
| 365 |
-
_sizeGB:
|
| 366 |
-
_source:
|
| 367 |
_provider: provider,
|
| 368 |
};
|
| 369 |
});
|
|
@@ -393,25 +391,24 @@ app.get('/stream/:type/:id.json', async (req, res) => {
|
|
| 393 |
const isSeries = type === 'series';
|
| 394 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 395 |
|
| 396 |
-
const meta
|
| 397 |
-
const title
|
| 398 |
-
const year
|
|
|
|
| 399 |
|
| 400 |
-
const [ytsR, knabenR,
|
| 401 |
isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
|
| 402 |
-
scrapeKnaben(title, year, isSeries, season, episode),
|
| 403 |
-
scrapeTorrentProject(title, year, isSeries, season, episode),
|
| 404 |
fetchTorrentio(type, id, title, year),
|
| 405 |
]);
|
| 406 |
|
| 407 |
-
const yts
|
| 408 |
-
const knaben
|
| 409 |
-
const
|
| 410 |
-
const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
|
| 411 |
|
| 412 |
-
const streams = processStreams([...yts, ...knaben, ...
|
| 413 |
|
| 414 |
-
console.log(`[stream] ${type}/${id} โ yts:${yts.length} knaben:${knaben.length}
|
| 415 |
res.json({ streams });
|
| 416 |
});
|
| 417 |
|
|
@@ -420,20 +417,19 @@ app.get('/diag/:type/:id', async (req, res) => {
|
|
| 420 |
const { type, id } = req.params;
|
| 421 |
const isSeries = type === 'series';
|
| 422 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 423 |
-
const meta
|
| 424 |
-
const
|
|
|
|
| 425 |
isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
|
| 426 |
-
scrapeKnaben(meta?.title || '', meta?.year || '', isSeries, season, episode),
|
| 427 |
-
scrapeTorrentProject(meta?.title || '', meta?.year || '', isSeries, season, episode),
|
| 428 |
fetchTorrentio(type, id, meta?.title || '', meta?.year || ''),
|
| 429 |
]);
|
| 430 |
res.json({
|
| 431 |
-
imdbId, title: meta?.title, year: meta?.year, isSeries,
|
| 432 |
-
tmdb:
|
| 433 |
-
yts:
|
| 434 |
-
knaben:
|
| 435 |
-
|
| 436 |
-
torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
|
| 437 |
});
|
| 438 |
});
|
| 439 |
|
|
@@ -492,7 +488,7 @@ app.get('/', (req, res) => {
|
|
| 492 |
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07);
|
| 493 |
border-radius: 16px; padding: 20px 22px;
|
| 494 |
}
|
| 495 |
-
.card-label { font-size: 0.68rem; font-weight: 700; text-
|
| 496 |
.url-box {
|
| 497 |
font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 0.8rem;
|
| 498 |
color: #ffffff; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,0,0,0.12);
|
|
@@ -500,20 +496,10 @@ app.get('/', (req, res) => {
|
|
| 500 |
}
|
| 501 |
.actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
|
| 502 |
.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; }
|
| 503 |
-
.btn-install {
|
| 504 |
-
background: #FF0000;
|
| 505 |
-
color: #ffffff;
|
| 506 |
-
}
|
| 507 |
.btn-install:hover { background: #cc0000; }
|
| 508 |
-
.btn-copy {
|
| 509 |
-
|
| 510 |
-
color: #050810;
|
| 511 |
-
border: 1px solid #ffffff;
|
| 512 |
-
}
|
| 513 |
-
.btn-copy:hover {
|
| 514 |
-
background: #e6e6e6;
|
| 515 |
-
border-color: #e6e6e6;
|
| 516 |
-
}
|
| 517 |
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
| 518 |
.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; }
|
| 519 |
.stat-val { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1; margin-bottom: 5px; }
|
|
|
|
| 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',
|
|
|
|
| 47 |
'kickasstorrents',
|
| 48 |
'animetosho',
|
| 49 |
'tokyotosho',
|
|
|
|
| 50 |
];
|
| 51 |
|
| 52 |
+
// Knaben confirmed category IDs (from live browse URLs)
|
| 53 |
+
// Movies: 3000000 (all), 3001000 (HD), 3002000 (SD), 3003000 (UHD)
|
| 54 |
+
// TV: 2000000 (all), 2001000 (HD), 2002000 (SD), 2003000 (UHD), 2007000 (Cartoon)
|
| 55 |
+
// Anime: 6000000 (all), 6001000 (Subbed), 6002000 (Dubbed), 6003000 (Dual), 6004000 (Raw)
|
| 56 |
+
// XXX blocked via hide_xxx: true
|
| 57 |
+
const KNABEN_CATS_MOVIE = [3000000, 3001000, 3002000, 3003000];
|
| 58 |
+
const KNABEN_CATS_SERIES = [2000000, 2001000, 2002000, 2003000, 2007000];
|
| 59 |
+
const KNABEN_CATS_ANIME = [6000000, 6001000, 6002000, 6003000];
|
| 60 |
+
|
| 61 |
const TR = [
|
| 62 |
'udp://tracker.opentrackr.org:1337/announce',
|
| 63 |
'udp://open.stealth.si:80/announce',
|
|
|
|
| 71 |
'https://tracker.zhuqiy.com:443/announce',
|
| 72 |
].map(t => '&tr=' + encodeURIComponent(t)).join('');
|
| 73 |
|
|
|
|
| 74 |
async function fetchWithProxy(url, options = {}) {
|
| 75 |
const proxies = [
|
| 76 |
url,
|
|
|
|
| 78 |
PROXY_2 + encodeURIComponent(url),
|
| 79 |
PROXY_3 + encodeURIComponent(url),
|
| 80 |
];
|
|
|
|
| 81 |
for (const proxyUrl of proxies) {
|
| 82 |
try {
|
| 83 |
const response = await axios.get(proxyUrl, { timeout: 12000, headers: { 'User-Agent': UA }, ...options });
|
| 84 |
return response;
|
| 85 |
} catch (e) {
|
| 86 |
+
console.warn(`[Proxy] Failed: ${proxyUrl.substring(0, 50)}... ${e.message}`);
|
|
|
|
| 87 |
}
|
| 88 |
}
|
| 89 |
throw new Error('All proxy attempts failed');
|
|
|
|
| 167 |
return [...new Set(found)].join(' / ');
|
| 168 |
}
|
| 169 |
|
| 170 |
+
function isSexualTitle(title = '') {
|
| 171 |
+
const s = title.toLowerCase();
|
| 172 |
+
const banned = [
|
| 173 |
+
'xxx', 'porn', 'sex', 'hentai', 'erotic', 'nude', 'naked', 'adult',
|
| 174 |
+
'milf', 'anal', 'blowjob', 'cumshot', 'creampie', 'gangbang', 'lesbian sex',
|
| 175 |
+
'brazzers', 'bangbros', 'mofos', 'naughty', 'onlyfans', 'playboy',
|
| 176 |
+
];
|
| 177 |
+
return banned.some(b => s.includes(b));
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
function processStreams(streams, type) {
|
| 181 |
const maxSize = type === 'series' ? MAX_SIZE_GB_SERIES : MAX_SIZE_GB_MOVIE;
|
| 182 |
|
|
|
|
| 187 |
if (!q || !ALLOWED_QUALITIES.includes(q)) return false;
|
| 188 |
const sizeGB = s._sizeGB ?? getSizeGB(s);
|
| 189 |
if (sizeGB !== null && sizeGB > maxSize) return false;
|
| 190 |
+
if (isSexualTitle(s.name || s.title || '')) return false;
|
| 191 |
return true;
|
| 192 |
});
|
| 193 |
|
|
|
|
| 235 |
);
|
| 236 |
const movie = data.movie_results?.[0];
|
| 237 |
const tv = data.tv_results?.[0];
|
| 238 |
+
if (movie) return { title: movie.title || movie.original_title, year: (movie.release_date || '').slice(0, 4), isAnime: false };
|
| 239 |
+
if (tv) return { title: tv.name || tv.original_name, year: (tv.first_air_date || '').slice(0, 4), isAnime: tv.genre_ids?.includes(16) || false };
|
| 240 |
return null;
|
| 241 |
} catch (e) { console.error('[TMDB]', e.message); return null; }
|
| 242 |
}
|
|
|
|
| 258 |
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : (t.size || '');
|
| 259 |
const langLine = getLangLine(qualityStr);
|
| 260 |
streams.push({
|
| 261 |
+
infoHash: t.hash.toLowerCase(),
|
| 262 |
+
name: qualityStr,
|
| 263 |
+
title: `โ๏ธ ${movie.title} (${movie.year})\n๐ฑ ${t.seeds || 0}\n๐พ ${sizeStr}\n๐
YTS${langLine ? '\n๐ ' + langLine : ''}`,
|
| 264 |
+
sources: [],
|
| 265 |
+
_quality: getQuality(qualityStr),
|
| 266 |
+
_seeders: t.seeds || 0,
|
| 267 |
+
_sizeGB: sizeGB,
|
| 268 |
+
_source: 'yts',
|
| 269 |
_provider: 'yts',
|
| 270 |
});
|
| 271 |
}
|
|
|
|
| 275 |
} catch (e) { console.error('[YTS]', e.message); return []; }
|
| 276 |
}
|
| 277 |
|
| 278 |
+
async function scrapeKnaben(title, year, isSeries, isAnime, season, episode) {
|
| 279 |
if (!title) return [];
|
| 280 |
try {
|
| 281 |
let query = title;
|
|
|
|
| 282 |
if (isSeries && season && episode)
|
| 283 |
query += ` S${String(season).padStart(2, '0')}E${String(episode).padStart(2, '0')}`;
|
| 284 |
+
else if (!isSeries && year)
|
| 285 |
+
query += ' ' + year;
|
| 286 |
+
|
| 287 |
+
// Pick the right categories โ no overlap between movies/TV/anime
|
| 288 |
+
let categories;
|
| 289 |
+
if (isAnime) categories = KNABEN_CATS_ANIME;
|
| 290 |
+
else if (isSeries) categories = KNABEN_CATS_SERIES;
|
| 291 |
+
else categories = KNABEN_CATS_MOVIE;
|
| 292 |
+
|
| 293 |
const { data } = await axios.post(
|
| 294 |
'https://api.knaben.org/v1',
|
| 295 |
+
{
|
| 296 |
+
search_type: '75%',
|
| 297 |
+
search_field: 'title',
|
| 298 |
+
query,
|
| 299 |
+
order_by: 'seeders',
|
| 300 |
+
order_direction: 'desc',
|
| 301 |
+
categories,
|
| 302 |
+
from: 0,
|
| 303 |
+
size: 50,
|
| 304 |
+
hide_unsafe: true,
|
| 305 |
+
hide_xxx: true, // blocks XXX category at API level
|
| 306 |
+
},
|
| 307 |
{ timeout: 14000, headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'User-Agent': UA } }
|
| 308 |
);
|
| 309 |
+
|
| 310 |
if (!data?.hits?.length) return [];
|
| 311 |
+
|
| 312 |
const streams = [];
|
| 313 |
for (const hit of data.hits) {
|
| 314 |
if (!hit.magnetUrl && !hit.hash) continue;
|
| 315 |
+
// Post-filter: reject if title contains sexual keywords
|
| 316 |
+
if (isSexualTitle(hit.title || '')) continue;
|
| 317 |
const magnet = hit.magnetUrl || buildMagnet(hit.hash, hit.title || '');
|
| 318 |
const hashMatch = magnet.match(/btih:([a-fA-F0-9]{32,40})/i);
|
| 319 |
const hash = hashMatch ? hashMatch[1] : (hit.hash || '');
|
|
|
|
| 322 |
const sizeStr = sizeGB ? sizeGB.toFixed(2) + ' GB' : '';
|
| 323 |
const langLine = getLangLine(hit.title || '');
|
| 324 |
streams.push({
|
| 325 |
+
infoHash: hash.toLowerCase(),
|
| 326 |
+
name: hit.title || '',
|
| 327 |
+
title: `โ๏ธ ${title}${year ? ' (' + year + ')' : ''}\n๐ฑ ${hit.seeders || 0}\n๐พ ${sizeStr}\n๐
Knaben${langLine ? '\n๐ ' + langLine : ''}`,
|
| 328 |
+
sources: [],
|
| 329 |
+
_quality: getQuality(hit.title || ''),
|
| 330 |
+
_seeders: hit.seeders || 0,
|
| 331 |
+
_sizeGB: sizeGB,
|
| 332 |
+
_source: 'knaben',
|
| 333 |
_provider: 'knaben',
|
| 334 |
});
|
| 335 |
}
|
| 336 |
+
console.log(`[Knaben] ${streams.length} for "${query}" cats:[${categories}]`);
|
| 337 |
return streams;
|
| 338 |
} catch (e) { console.error('[Knaben]', e.message); return []; }
|
| 339 |
}
|
| 340 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
async function fetchTorrentio(type, id, title, year) {
|
| 342 |
try {
|
| 343 |
const url = `${TORRENTIO_BASE}/stream/${type}/${id}.json`;
|
|
|
|
| 357 |
const langLine = getLangLine(origTitle);
|
| 358 |
return {
|
| 359 |
...s,
|
| 360 |
+
title: `โ๏ธ ${title}${year ? ' (' + year + ')' : ''}\n๐ฑ ${seeders}\n๐พ ${sizeGB ? sizeGB.toFixed(2) + ' GB' : 'N/A'}\n๐
${provider}${langLine ? '\n๐ ' + langLine : ''}`,
|
| 361 |
+
_quality: getQuality(s.title || s.name || ''),
|
| 362 |
+
_seeders: seeders,
|
| 363 |
+
_sizeGB: sizeGB,
|
| 364 |
+
_source: 'torrentio',
|
| 365 |
_provider: provider,
|
| 366 |
};
|
| 367 |
});
|
|
|
|
| 391 |
const isSeries = type === 'series';
|
| 392 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 393 |
|
| 394 |
+
const meta = await tmdbLookup(imdbId);
|
| 395 |
+
const title = meta?.title || '';
|
| 396 |
+
const year = meta?.year || '';
|
| 397 |
+
const isAnime = meta?.isAnime || false;
|
| 398 |
|
| 399 |
+
const [ytsR, knabenR, torrentioR] = await Promise.allSettled([
|
| 400 |
isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
|
| 401 |
+
scrapeKnaben(title, year, isSeries, isAnime, season, episode),
|
|
|
|
| 402 |
fetchTorrentio(type, id, title, year),
|
| 403 |
]);
|
| 404 |
|
| 405 |
+
const yts = ytsR.status === 'fulfilled' ? (ytsR.value || []) : [];
|
| 406 |
+
const knaben = knabenR.status === 'fulfilled' ? (knabenR.value || []) : [];
|
| 407 |
+
const torrentio = torrentioR.status === 'fulfilled' ? (torrentioR.value || []) : [];
|
|
|
|
| 408 |
|
| 409 |
+
const streams = processStreams([...yts, ...knaben, ...torrentio], type);
|
| 410 |
|
| 411 |
+
console.log(`[stream] ${type}/${id} โ yts:${yts.length} knaben:${knaben.length} torrentio:${torrentio.length} final:${streams.length} anime:${isAnime}`);
|
| 412 |
res.json({ streams });
|
| 413 |
});
|
| 414 |
|
|
|
|
| 417 |
const { type, id } = req.params;
|
| 418 |
const isSeries = type === 'series';
|
| 419 |
const [imdbId, season = '', episode = ''] = id.split(':');
|
| 420 |
+
const meta = await tmdbLookup(imdbId);
|
| 421 |
+
const isAnime = meta?.isAnime || false;
|
| 422 |
+
const [ytsR, knabenR, torrentioR] = await Promise.allSettled([
|
| 423 |
isSeries ? Promise.resolve([]) : scrapeYTS(imdbId),
|
| 424 |
+
scrapeKnaben(meta?.title || '', meta?.year || '', isSeries, isAnime, season, episode),
|
|
|
|
| 425 |
fetchTorrentio(type, id, meta?.title || '', meta?.year || ''),
|
| 426 |
]);
|
| 427 |
res.json({
|
| 428 |
+
imdbId, title: meta?.title, year: meta?.year, isSeries, isAnime,
|
| 429 |
+
tmdb: meta ? `OK: ${meta.title} (${meta.year})` : 'FAILED',
|
| 430 |
+
yts: { count: (ytsR.value || []).length, err: ytsR.reason?.message || null },
|
| 431 |
+
knaben: { count: (knabenR.value || []).length, err: knabenR.reason?.message || null },
|
| 432 |
+
torrentio: { count: (torrentioR.value || []).length, err: torrentioR.reason?.message || null },
|
|
|
|
| 433 |
});
|
| 434 |
});
|
| 435 |
|
|
|
|
| 488 |
background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.07);
|
| 489 |
border-radius: 16px; padding: 20px 22px;
|
| 490 |
}
|
| 491 |
+
.card-label { font-size: 0.68rem; font-weight: 700; text-transform: uppercase; letter-spacing: 1px; color: #ffffff; margin-bottom: 10px; }
|
| 492 |
.url-box {
|
| 493 |
font-family: 'SF Mono','Fira Code','Consolas',monospace; font-size: 0.8rem;
|
| 494 |
color: #ffffff; background: rgba(0,0,0,0.25); border: 1px solid rgba(255,0,0,0.12);
|
|
|
|
| 496 |
}
|
| 497 |
.actions { display: flex; gap: 10px; margin-top: 14px; flex-wrap: wrap; }
|
| 498 |
.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; }
|
| 499 |
+
.btn-install { background: #FF0000; color: #ffffff; }
|
|
|
|
|
|
|
|
|
|
| 500 |
.btn-install:hover { background: #cc0000; }
|
| 501 |
+
.btn-copy { background: #ffffff; color: #050810; border: 1px solid #ffffff; }
|
| 502 |
+
.btn-copy:hover { background: #e6e6e6; border-color: #e6e6e6; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 503 |
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
| 504 |
.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; }
|
| 505 |
.stat-val { font-size: 1.5rem; font-weight: 800; color: #fff; line-height: 1; margin-bottom: 5px; }
|