Update index.js
Browse files
index.js
CHANGED
|
@@ -4,6 +4,7 @@ const cheerio = require('cheerio');
|
|
| 4 |
const AdmZip = require('adm-zip');
|
| 5 |
const https = require('https');
|
| 6 |
const http = require('http');
|
|
|
|
| 7 |
const zlib = require('zlib');
|
| 8 |
const { URL } = require('url');
|
| 9 |
|
|
@@ -21,9 +22,8 @@ app.use((req, res, next) => {
|
|
| 21 |
// ================================================================
|
| 22 |
// PROXY CONFIG ← edit these three lines to swap proxies
|
| 23 |
// Each takes a URL-ENCODED target appended to it. Set to '' to disable a slot.
|
| 24 |
-
//
|
| 25 |
-
//
|
| 26 |
-
// your SubDL Workers addon.
|
| 27 |
// ================================================================
|
| 28 |
const PROXY_1 = 'https://api.codetabs.com/v1/proxy?quest=';
|
| 29 |
const PROXY_2 = 'https://api.allorigins.win/raw?url=';
|
|
@@ -33,9 +33,12 @@ const PROXY_3 = '';
|
|
| 33 |
// ================================================================
|
| 34 |
// DOWNLOAD TUNING ← edit these to change retry/pacing behaviour
|
| 35 |
// ================================================================
|
| 36 |
-
const DL_RETRY_DELAY =
|
| 37 |
const DL_STEP_DELAY = 600; // ms between detail-page fetch and download fetch
|
| 38 |
-
const DL_ATTEMPTS =
|
|
|
|
|
|
|
|
|
|
| 39 |
const DEBUG_ROUTE = true; // expose GET /debug/:enc — set false to disable
|
| 40 |
// ================================================================
|
| 41 |
|
|
@@ -167,7 +170,7 @@ const diag = {
|
|
| 167 |
extractFailed: 0, // got a body back but couldn't pull a subtitle out of it
|
| 168 |
downloadsOK: 0,
|
| 169 |
lastFailStage: null, // 'detail' | 'button' | 'fetch' | 'extract'
|
| 170 |
-
lastStatuses: [], //
|
| 171 |
lastArchiveKind: null, // 'zip' | 'rar' | '7z' | 'gzip' | 'html' | 'unknown'
|
| 172 |
lastNote: null, // human-readable reason the last attempt died
|
| 173 |
};
|
|
@@ -206,9 +209,10 @@ function _cookiesFrom(res, prior) {
|
|
| 206 |
// identical from the outside.
|
| 207 |
function _request(urlStr, opts = {}, redirects = 0, jar = '') {
|
| 208 |
return new Promise((resolve, reject) => {
|
| 209 |
-
if (!urlStr || redirects > MAX_REDIRECTS) return reject(new Error('
|
| 210 |
let parsed;
|
| 211 |
try { parsed = new URL(urlStr); } catch (_) { return reject(new Error('Invalid URL')); }
|
|
|
|
| 212 |
const lib = parsed.protocol === 'https:' ? https : http;
|
| 213 |
const timeout = opts.timeout || REQ_TIMEOUT;
|
| 214 |
const maxBytes = opts.maxBytes || MAX_BODY_BYTES;
|
|
@@ -219,7 +223,9 @@ function _request(urlStr, opts = {}, redirects = 0, jar = '') {
|
|
| 219 |
// 'application/zip,...' value on binary fetches was a fingerprintable tell.
|
| 220 |
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 221 |
'Accept-Language': opts.lang === 'arabic' ? 'ar,en-US;q=0.8,en;q=0.6' : 'en-US,en;q=0.9',
|
| 222 |
-
|
|
|
|
|
|
|
| 223 |
'Referer': `${BASE}/`,
|
| 224 |
'Connection': 'keep-alive',
|
| 225 |
'Cache-Control': 'no-cache',
|
|
@@ -234,6 +240,7 @@ function _request(urlStr, opts = {}, redirects = 0, jar = '') {
|
|
| 234 |
method: 'GET',
|
| 235 |
headers,
|
| 236 |
};
|
|
|
|
| 237 |
const req = lib.request(reqOpts, res => {
|
| 238 |
const status = res.statusCode;
|
| 239 |
const carried = _cookiesFrom(res, cookie);
|
|
@@ -275,6 +282,81 @@ function _request(urlStr, opts = {}, redirects = 0, jar = '') {
|
|
| 275 |
});
|
| 276 |
}
|
| 277 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 278 |
// Returns { body, status, cookies }. body is null when the fetch didn't yield a 200.
|
| 279 |
async function getHTMLEx(url, tries = 3, lang = 'english', cookies = '') {
|
| 280 |
let status = 0, jar = cookies;
|
|
@@ -295,42 +377,91 @@ async function getHTML(url, tries = 3, lang = 'english') {
|
|
| 295 |
return (await getHTMLEx(url, tries, lang)).body;
|
| 296 |
}
|
| 297 |
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
'Sec-Fetch-Dest': 'document',
|
| 308 |
'Sec-Fetch-Mode': 'navigate',
|
| 309 |
'Sec-Fetch-Site': 'same-origin',
|
| 310 |
'Sec-Fetch-User': '?1',
|
| 311 |
};
|
| 312 |
-
if (referer)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
for (let i = 0; i < DL_ATTEMPTS; i++) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
try {
|
| 315 |
-
const r = await
|
| 316 |
-
|
| 317 |
-
|
|
|
|
| 318 |
// Only 404/503 are worth waiting out; anything else won't change on retry.
|
| 319 |
-
if (r.status !== 404 && r.status !== 503)
|
| 320 |
} catch (e) {
|
| 321 |
-
|
| 322 |
}
|
| 323 |
-
if (i < DL_ATTEMPTS - 1) await sleep(DL_RETRY_DELAY);
|
| 324 |
}
|
|
|
|
| 325 |
const enc = encodeURIComponent(url);
|
| 326 |
for (const base of [PROXY_1, PROXY_2, PROXY_3]) {
|
| 327 |
if (!base) continue;
|
| 328 |
try {
|
| 329 |
const r = await _request(`${base}${enc}`, { binary: true, timeout: DL_TIMEOUT, maxBytes: MAX_SRT_BYTES });
|
| 330 |
-
|
| 331 |
-
if (r.body && r.body.length > 50) return r.body;
|
| 332 |
-
} catch (
|
| 333 |
-
|
| 334 |
}
|
| 335 |
}
|
| 336 |
return null;
|
|
@@ -965,8 +1096,8 @@ function archiveKind(buf) {
|
|
| 965 |
// ================================================================
|
| 966 |
// Walks the same path a browser does: load the subtitle detail page, read the
|
| 967 |
// real href off its download button, then request that URL carrying the detail
|
| 968 |
-
// page as Referer
|
| 969 |
-
//
|
| 970 |
async function downloadSubtitle(pageUrl, trace = {}) {
|
| 971 |
trace.pageUrl = pageUrl;
|
| 972 |
|
|
@@ -1004,13 +1135,14 @@ async function downloadSubtitle(pageUrl, trace = {}) {
|
|
| 1004 |
|
| 1005 |
// Stage 3 — fetch the archive, pacing the request like a real click
|
| 1006 |
await sleep(DL_STEP_DELAY);
|
| 1007 |
-
const
|
| 1008 |
-
const buf = await getBinary(downloadUrl, pageUrl, detail.cookies,
|
| 1009 |
-
trace.
|
| 1010 |
-
diag.lastStatuses =
|
| 1011 |
if (!buf) {
|
| 1012 |
diag.downloadFetchFailed++;
|
| 1013 |
-
|
|
|
|
| 1014 |
trace.failedAt = 'fetch';
|
| 1015 |
return null;
|
| 1016 |
}
|
|
@@ -1091,7 +1223,7 @@ app.get('/manifest.json', (req, res) => {
|
|
| 1091 |
id: 'org.van.vansubf2mplus.steel',
|
| 1092 |
name: 'VanSubF2M Steel',
|
| 1093 |
description: 'Hardened Subf2m English & Arabic subtitles with release-aware matching and anti-AI filtering.',
|
| 1094 |
-
version: '3.
|
| 1095 |
resources: ['subtitles'],
|
| 1096 |
types: ['movie', 'series'],
|
| 1097 |
idPrefixes: ['tt'],
|
|
@@ -1106,7 +1238,7 @@ app.get('/sub/:enc.srt', async (req, res) => {
|
|
| 1106 |
let pageUrl = '';
|
| 1107 |
try { pageUrl = Buffer.from(req.params.enc, 'base64url').toString('utf8'); }
|
| 1108 |
catch (_) { return res.status(400).end('Bad request'); }
|
| 1109 |
-
// This
|
| 1110 |
// the real download link is resolved fresh from that page in fetchSRT.
|
| 1111 |
if (!/^https:\/\/subf2m\.co\/subtitles\/[^\s\/]+\/(?:english|arabic)\/\d+\/?$/i.test(pageUrl)) return res.status(403).end('Forbidden');
|
| 1112 |
const content = await fetchSRT(pageUrl);
|
|
@@ -1133,7 +1265,7 @@ if (DEBUG_ROUTE) {
|
|
| 1133 |
const trace = {};
|
| 1134 |
let content = null;
|
| 1135 |
try { content = await downloadSubtitle(pageUrl, trace); }
|
| 1136 |
-
catch (e) { trace.threw =
|
| 1137 |
res.json({ ok: !!content, trace, preview: content ? content.slice(0, 300) : null });
|
| 1138 |
});
|
| 1139 |
}
|
|
@@ -1225,7 +1357,7 @@ app.get('/health', (req, res) => {
|
|
| 1225 |
res.json({
|
| 1226 |
ok: true,
|
| 1227 |
service: 'VanSubF2M Steel',
|
| 1228 |
-
version: '3.
|
| 1229 |
debugRoute: DEBUG_ROUTE ? '/debug/<same base64url as /sub/>' : 'disabled',
|
| 1230 |
downloadDiagnostics: diag,
|
| 1231 |
});
|
|
|
|
| 4 |
const AdmZip = require('adm-zip');
|
| 5 |
const https = require('https');
|
| 6 |
const http = require('http');
|
| 7 |
+
const http2 = require('http2');
|
| 8 |
const zlib = require('zlib');
|
| 9 |
const { URL } = require('url');
|
| 10 |
|
|
|
|
| 22 |
// ================================================================
|
| 23 |
// PROXY CONFIG ← edit these three lines to swap proxies
|
| 24 |
// Each takes a URL-ENCODED target appended to it. Set to '' to disable a slot.
|
| 25 |
+
// PROXY_3 is a good slot for your own Cloudflare Worker relay — Worker egress
|
| 26 |
+
// already reaches CF-fronted subtitle sites in your SubDL addon.
|
|
|
|
| 27 |
// ================================================================
|
| 28 |
const PROXY_1 = 'https://api.codetabs.com/v1/proxy?quest=';
|
| 29 |
const PROXY_2 = 'https://api.allorigins.win/raw?url=';
|
|
|
|
| 33 |
// ================================================================
|
| 34 |
// DOWNLOAD TUNING ← edit these to change retry/pacing behaviour
|
| 35 |
// ================================================================
|
| 36 |
+
const DL_RETRY_DELAY = 2000; // ms between download attempts (404/503 backoff)
|
| 37 |
const DL_STEP_DELAY = 600; // ms between detail-page fetch and download fetch
|
| 38 |
+
const DL_ATTEMPTS = 2; // attempts per transport before moving to the next
|
| 39 |
+
const DL_HARD_TIMEOUT = 12000;// ms per download attempt — fail fast, don't hang
|
| 40 |
+
const TRY_HTTP2 = true; // attempt HTTP/2 (browsers use h2; node https is h1)
|
| 41 |
+
const TRY_INSECURE = true; // retry ignoring TLS verification errors
|
| 42 |
const DEBUG_ROUTE = true; // expose GET /debug/:enc — set false to disable
|
| 43 |
// ================================================================
|
| 44 |
|
|
|
|
| 170 |
extractFailed: 0, // got a body back but couldn't pull a subtitle out of it
|
| 171 |
downloadsOK: 0,
|
| 172 |
lastFailStage: null, // 'detail' | 'button' | 'fetch' | 'extract'
|
| 173 |
+
lastStatuses: [], // per-transport attempt records from the last download
|
| 174 |
lastArchiveKind: null, // 'zip' | 'rar' | '7z' | 'gzip' | 'html' | 'unknown'
|
| 175 |
lastNote: null, // human-readable reason the last attempt died
|
| 176 |
};
|
|
|
|
| 209 |
// identical from the outside.
|
| 210 |
function _request(urlStr, opts = {}, redirects = 0, jar = '') {
|
| 211 |
return new Promise((resolve, reject) => {
|
| 212 |
+
if (!urlStr || redirects > MAX_REDIRECTS) return reject(new Error('Redirect loop'));
|
| 213 |
let parsed;
|
| 214 |
try { parsed = new URL(urlStr); } catch (_) { return reject(new Error('Invalid URL')); }
|
| 215 |
+
if (Array.isArray(opts.chain)) opts.chain.push(urlStr);
|
| 216 |
const lib = parsed.protocol === 'https:' ? https : http;
|
| 217 |
const timeout = opts.timeout || REQ_TIMEOUT;
|
| 218 |
const maxBytes = opts.maxBytes || MAX_BODY_BYTES;
|
|
|
|
| 223 |
// 'application/zip,...' value on binary fetches was a fingerprintable tell.
|
| 224 |
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 225 |
'Accept-Language': opts.lang === 'arabic' ? 'ar,en-US;q=0.8,en;q=0.6' : 'en-US,en;q=0.9',
|
| 226 |
+
// identity on binary fetches: a zip is already compressed, and asking for
|
| 227 |
+
// br/gzip on it only adds a decode path that can stall or throw.
|
| 228 |
+
'Accept-Encoding': opts.binary ? 'identity' : 'gzip, deflate',
|
| 229 |
'Referer': `${BASE}/`,
|
| 230 |
'Connection': 'keep-alive',
|
| 231 |
'Cache-Control': 'no-cache',
|
|
|
|
| 240 |
method: 'GET',
|
| 241 |
headers,
|
| 242 |
};
|
| 243 |
+
if (opts.insecure && parsed.protocol === 'https:') reqOpts.rejectUnauthorized = false;
|
| 244 |
const req = lib.request(reqOpts, res => {
|
| 245 |
const status = res.statusCode;
|
| 246 |
const carried = _cookiesFrom(res, cookie);
|
|
|
|
| 282 |
});
|
| 283 |
}
|
| 284 |
|
| 285 |
+
// ================================================================
|
| 286 |
+
// HTTP/2 TRANSPORT
|
| 287 |
+
// ================================================================
|
| 288 |
+
// Node's https module speaks HTTP/1.1. Every real browser negotiates h2, and
|
| 289 |
+
// edge WAFs fingerprint that difference. The detail page sails through on h1
|
| 290 |
+
// while /download hangs, so h2 is worth a genuine attempt rather than a guess.
|
| 291 |
+
function _h2request(urlStr, opts = {}, redirects = 0) {
|
| 292 |
+
return new Promise((resolve, reject) => {
|
| 293 |
+
if (!urlStr || redirects > MAX_REDIRECTS) return reject(new Error('Redirect loop'));
|
| 294 |
+
let parsed;
|
| 295 |
+
try { parsed = new URL(urlStr); } catch (_) { return reject(new Error('Invalid URL')); }
|
| 296 |
+
if (parsed.protocol !== 'https:') return reject(new Error('h2 needs https'));
|
| 297 |
+
if (Array.isArray(opts.chain)) opts.chain.push(`h2:${urlStr}`);
|
| 298 |
+
const timeout = opts.timeout || DL_HARD_TIMEOUT;
|
| 299 |
+
const maxBytes = opts.maxBytes || MAX_BODY_BYTES;
|
| 300 |
+
let settled = false;
|
| 301 |
+
let session;
|
| 302 |
+
const finish = (fn, arg) => {
|
| 303 |
+
if (settled) return;
|
| 304 |
+
settled = true;
|
| 305 |
+
try { if (session) session.destroy(); } catch (_) {}
|
| 306 |
+
fn(arg);
|
| 307 |
+
};
|
| 308 |
+
try {
|
| 309 |
+
session = http2.connect(parsed.origin, {
|
| 310 |
+
rejectUnauthorized: opts.insecure ? false : true,
|
| 311 |
+
});
|
| 312 |
+
} catch (e) { return reject(e); }
|
| 313 |
+
session.setTimeout(timeout, () => finish(reject, new Error('h2 session timeout')));
|
| 314 |
+
session.on('error', e => finish(reject, e));
|
| 315 |
+
const hdrs = {
|
| 316 |
+
':method': 'GET',
|
| 317 |
+
':authority': parsed.host,
|
| 318 |
+
':scheme': 'https',
|
| 319 |
+
':path': parsed.pathname + parsed.search,
|
| 320 |
+
'user-agent': UA,
|
| 321 |
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
| 322 |
+
'accept-language': 'en-US,en;q=0.9',
|
| 323 |
+
'accept-encoding': opts.binary ? 'identity' : 'gzip, deflate',
|
| 324 |
+
...(opts.h2headers || {}),
|
| 325 |
+
};
|
| 326 |
+
let req;
|
| 327 |
+
try { req = session.request(hdrs); } catch (e) { return finish(reject, e); }
|
| 328 |
+
req.setTimeout(timeout, () => finish(reject, new Error('h2 stream timeout')));
|
| 329 |
+
let status = 0, respHeaders = {};
|
| 330 |
+
req.on('response', h => { respHeaders = h; status = Number(h[':status']) || 0; });
|
| 331 |
+
const chunks = [];
|
| 332 |
+
let total = 0;
|
| 333 |
+
req.on('data', c => {
|
| 334 |
+
total += c.length;
|
| 335 |
+
if (total > maxBytes) { req.close(http2.constants.NGHTTP2_CANCEL); return; }
|
| 336 |
+
chunks.push(c);
|
| 337 |
+
});
|
| 338 |
+
req.on('error', e => finish(reject, e));
|
| 339 |
+
req.on('end', () => {
|
| 340 |
+
let buf = Buffer.concat(chunks);
|
| 341 |
+
const loc = respHeaders['location'];
|
| 342 |
+
if (status >= 300 && status < 400 && loc) {
|
| 343 |
+
settled = true;
|
| 344 |
+
try { session.destroy(); } catch (_) {}
|
| 345 |
+
return _h2request(new URL(loc, urlStr).toString(), opts, redirects + 1).then(resolve).catch(reject);
|
| 346 |
+
}
|
| 347 |
+
if (status !== 200) return finish(resolve, { status, headers: respHeaders, body: null, cookies: '' });
|
| 348 |
+
const enc = String(respHeaders['content-encoding'] || '').toLowerCase();
|
| 349 |
+
try {
|
| 350 |
+
if (enc.includes('gzip')) buf = zlib.gunzipSync(buf);
|
| 351 |
+
else if (enc.includes('br')) buf = zlib.brotliDecompressSync(buf);
|
| 352 |
+
else if (enc.includes('deflate')) buf = zlib.inflateSync(buf);
|
| 353 |
+
} catch (e) { return finish(reject, e); }
|
| 354 |
+
finish(resolve, { status, headers: respHeaders, body: opts.binary ? buf : buf.toString('utf8'), cookies: '' });
|
| 355 |
+
});
|
| 356 |
+
req.end();
|
| 357 |
+
});
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
// Returns { body, status, cookies }. body is null when the fetch didn't yield a 200.
|
| 361 |
async function getHTMLEx(url, tries = 3, lang = 'english', cookies = '') {
|
| 362 |
let status = 0, jar = cookies;
|
|
|
|
| 377 |
return (await getHTMLEx(url, tries, lang)).body;
|
| 378 |
}
|
| 379 |
|
| 380 |
+
function _errLabel(e) {
|
| 381 |
+
if (!e) return 'unknown';
|
| 382 |
+
const code = e.code ? String(e.code) : '';
|
| 383 |
+
const msg = e.message ? String(e.message).slice(0, 120) : '';
|
| 384 |
+
return code ? `${code}${msg && msg !== code ? ' (' + msg + ')' : ''}` : (msg || 'unknown');
|
| 385 |
+
}
|
| 386 |
+
|
| 387 |
+
// Tries genuinely different transports rather than hammering the same one.
|
| 388 |
+
// Each attempt records a real status or a real error code into `attempts`, so a
|
| 389 |
+
// hang, a TLS failure and a redirect loop are never again flattened into -1.
|
| 390 |
+
async function getBinary(url, referer, cookies, attempts) {
|
| 391 |
+
const rec = a => { if (Array.isArray(attempts)) attempts.push(a); };
|
| 392 |
+
const h1headers = {
|
| 393 |
'Sec-Fetch-Dest': 'document',
|
| 394 |
'Sec-Fetch-Mode': 'navigate',
|
| 395 |
'Sec-Fetch-Site': 'same-origin',
|
| 396 |
'Sec-Fetch-User': '?1',
|
| 397 |
};
|
| 398 |
+
if (referer) h1headers.Referer = referer;
|
| 399 |
+
const h2headers = {
|
| 400 |
+
'referer': referer || `${BASE}/`,
|
| 401 |
+
'sec-fetch-dest': 'document',
|
| 402 |
+
'sec-fetch-mode': 'navigate',
|
| 403 |
+
'sec-fetch-site': 'same-origin',
|
| 404 |
+
'sec-fetch-user': '?1',
|
| 405 |
+
};
|
| 406 |
+
|
| 407 |
+
const strategies = [];
|
| 408 |
for (let i = 0; i < DL_ATTEMPTS; i++) {
|
| 409 |
+
strategies.push({
|
| 410 |
+
via: `h1#${i + 1}`,
|
| 411 |
+
run: () => _request(url, {
|
| 412 |
+
binary: true, timeout: DL_HARD_TIMEOUT, maxBytes: MAX_SRT_BYTES,
|
| 413 |
+
headers: h1headers, cookies, chain: [],
|
| 414 |
+
}),
|
| 415 |
+
});
|
| 416 |
+
}
|
| 417 |
+
if (TRY_HTTP2) {
|
| 418 |
+
strategies.push({
|
| 419 |
+
via: 'h2',
|
| 420 |
+
run: () => _h2request(url, {
|
| 421 |
+
binary: true, timeout: DL_HARD_TIMEOUT, maxBytes: MAX_SRT_BYTES, h2headers,
|
| 422 |
+
}),
|
| 423 |
+
});
|
| 424 |
+
}
|
| 425 |
+
if (TRY_INSECURE) {
|
| 426 |
+
strategies.push({
|
| 427 |
+
via: 'h1-insecure',
|
| 428 |
+
run: () => _request(url, {
|
| 429 |
+
binary: true, timeout: DL_HARD_TIMEOUT, maxBytes: MAX_SRT_BYTES,
|
| 430 |
+
headers: h1headers, cookies, insecure: true, chain: [],
|
| 431 |
+
}),
|
| 432 |
+
});
|
| 433 |
+
if (TRY_HTTP2) strategies.push({
|
| 434 |
+
via: 'h2-insecure',
|
| 435 |
+
run: () => _h2request(url, {
|
| 436 |
+
binary: true, timeout: DL_HARD_TIMEOUT, maxBytes: MAX_SRT_BYTES, h2headers, insecure: true,
|
| 437 |
+
}),
|
| 438 |
+
});
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
for (let i = 0; i < strategies.length; i++) {
|
| 442 |
+
const s = strategies[i];
|
| 443 |
try {
|
| 444 |
+
const r = await s.run();
|
| 445 |
+
rec({ via: s.via, status: r.status, bytes: r.body ? r.body.length : 0,
|
| 446 |
+
type: r.headers?.['content-type'] || null });
|
| 447 |
+
if (r.body && r.body.length > 0) return r.body;
|
| 448 |
// Only 404/503 are worth waiting out; anything else won't change on retry.
|
| 449 |
+
if (s.via.startsWith('h1#') && r.status !== 404 && r.status !== 503) continue;
|
| 450 |
} catch (e) {
|
| 451 |
+
rec({ via: s.via, error: _errLabel(e) });
|
| 452 |
}
|
| 453 |
+
if (s.via.startsWith('h1#') && i < DL_ATTEMPTS - 1) await sleep(DL_RETRY_DELAY);
|
| 454 |
}
|
| 455 |
+
|
| 456 |
const enc = encodeURIComponent(url);
|
| 457 |
for (const base of [PROXY_1, PROXY_2, PROXY_3]) {
|
| 458 |
if (!base) continue;
|
| 459 |
try {
|
| 460 |
const r = await _request(`${base}${enc}`, { binary: true, timeout: DL_TIMEOUT, maxBytes: MAX_SRT_BYTES });
|
| 461 |
+
rec({ via: `proxy ${new URL(base).hostname}`, status: r.status, bytes: r.body ? r.body.length : 0 });
|
| 462 |
+
if (r.body && r.body.length > 50 && archiveKind(r.body) !== 'html') return r.body;
|
| 463 |
+
} catch (e) {
|
| 464 |
+
rec({ via: `proxy ${new URL(base).hostname}`, error: _errLabel(e) });
|
| 465 |
}
|
| 466 |
}
|
| 467 |
return null;
|
|
|
|
| 1096 |
// ================================================================
|
| 1097 |
// Walks the same path a browser does: load the subtitle detail page, read the
|
| 1098 |
// real href off its download button, then request that URL carrying the detail
|
| 1099 |
+
// page as Referer. Every stage records what happened into `trace` so /debug can
|
| 1100 |
+
// show exactly where it broke and why.
|
| 1101 |
async function downloadSubtitle(pageUrl, trace = {}) {
|
| 1102 |
trace.pageUrl = pageUrl;
|
| 1103 |
|
|
|
|
| 1135 |
|
| 1136 |
// Stage 3 — fetch the archive, pacing the request like a real click
|
| 1137 |
await sleep(DL_STEP_DELAY);
|
| 1138 |
+
const attempts = [];
|
| 1139 |
+
const buf = await getBinary(downloadUrl, pageUrl, detail.cookies, attempts);
|
| 1140 |
+
trace.attempts = attempts;
|
| 1141 |
+
diag.lastStatuses = attempts;
|
| 1142 |
if (!buf) {
|
| 1143 |
diag.downloadFetchFailed++;
|
| 1144 |
+
const summary = attempts.map(a => `${a.via}=${a.error || a.status}`).join(', ');
|
| 1145 |
+
noteFail('fetch', summary || 'no attempts recorded');
|
| 1146 |
trace.failedAt = 'fetch';
|
| 1147 |
return null;
|
| 1148 |
}
|
|
|
|
| 1223 |
id: 'org.van.vansubf2mplus.steel',
|
| 1224 |
name: 'VanSubF2M Steel',
|
| 1225 |
description: 'Hardened Subf2m English & Arabic subtitles with release-aware matching and anti-AI filtering.',
|
| 1226 |
+
version: '3.3.0',
|
| 1227 |
resources: ['subtitles'],
|
| 1228 |
types: ['movie', 'series'],
|
| 1229 |
idPrefixes: ['tt'],
|
|
|
|
| 1238 |
let pageUrl = '';
|
| 1239 |
try { pageUrl = Buffer.from(req.params.enc, 'base64url').toString('utf8'); }
|
| 1240 |
catch (_) { return res.status(400).end('Bad request'); }
|
| 1241 |
+
// This carries the subtitle DETAIL page URL (not a pre-built /download URL) —
|
| 1242 |
// the real download link is resolved fresh from that page in fetchSRT.
|
| 1243 |
if (!/^https:\/\/subf2m\.co\/subtitles\/[^\s\/]+\/(?:english|arabic)\/\d+\/?$/i.test(pageUrl)) return res.status(403).end('Forbidden');
|
| 1244 |
const content = await fetchSRT(pageUrl);
|
|
|
|
| 1265 |
const trace = {};
|
| 1266 |
let content = null;
|
| 1267 |
try { content = await downloadSubtitle(pageUrl, trace); }
|
| 1268 |
+
catch (e) { trace.threw = _errLabel(e); }
|
| 1269 |
res.json({ ok: !!content, trace, preview: content ? content.slice(0, 300) : null });
|
| 1270 |
});
|
| 1271 |
}
|
|
|
|
| 1357 |
res.json({
|
| 1358 |
ok: true,
|
| 1359 |
service: 'VanSubF2M Steel',
|
| 1360 |
+
version: '3.3.0',
|
| 1361 |
debugRoute: DEBUG_ROUTE ? '/debug/<same base64url as /sub/>' : 'disabled',
|
| 1362 |
downloadDiagnostics: diag,
|
| 1363 |
});
|