'use strict';
const express = require('express');
const cheerio = require('cheerio');
const AdmZip = require('adm-zip');
const https = require('https');
const http = require('http');
const zlib = require('zlib');
const { URL } = require('url');
const app = express();
const PORT = process.env.PORT || 7860;
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', '*');
res.header('Access-Control-Allow-Methods', 'GET, OPTIONS');
if (req.method === 'OPTIONS') return res.sendStatus(200);
next();
});
// ================================================================
// PROXY CONFIG ← edit these three lines to swap proxies
// ================================================================
const PROXY_1 = 'https://corsproxy.io/?url=';
const PROXY_2 = 'https://api.codetabs.com/v1/proxy?quest=';
const PROXY_3 = 'https://thingproxy.freeboard.io/fetch/';
// ================================================================
const BASE = 'https://subf2m.co';
const TMDB_KEY = process.env.TMDB_KEY || '30166e2db6b420d3f230808cd1bb2c90';
const TMDB_BASE = 'https://api.themoviedb.org/3';
const MAX_PER_LANG = 4;
const CACHE_TTL = 6 * 60 * 60 * 1000;
const REQ_TIMEOUT = 18_000;
const DL_TIMEOUT = 30_000;
const MAX_DETAIL = 30;
// Full ordinal list covering seasons 1–30
const ORDINALS = [
'', 'first', 'second', 'third', 'fourth', 'fifth',
'sixth', 'seventh', 'eighth', 'ninth', 'tenth',
'eleventh', 'twelfth', 'thirteenth', 'fourteenth', 'fifteenth',
'sixteenth', 'seventeenth', 'eighteenth', 'nineteenth', 'twentieth',
'twenty-first', 'twenty-second', 'twenty-third', 'twenty-fourth', 'twenty-fifth',
'twenty-sixth', 'twenty-seventh', 'twenty-eighth', 'twenty-ninth', 'thirtieth',
];
const ALLOWED_LANGS = new Set(['english', 'arabic']);
const OTHER_LANGS = [
'french','spanish','portuguese','german','italian','turkish','persian','farsi',
'hindi','russian','polish','dutch','swedish','norwegian','danish','finnish',
'korean','japanese','chinese','malay','indonesian','vietnamese','thai','greek',
'hungarian','czech','romanian','bulgarian','hebrew','ukrainian','serbian',
'croatian','slovak','slovenian','latvian','estonian','lithuanian','albanian',
'azerbaijani','bosnian','catalan','georgian','icelandic','macedonian','mongolian',
'punjabi','sinhala','tamil','bengali','urdu','nepali','khmer','burmese',
'afrikaans','swahili',
];
const QUALITY_TIERS = [
{ bonus: 0.13, terms: [/\bbluray\b/i,/\bblu[- ]ray\b/i,/\bbdrip\b/i,/\bbdremux\b/i,/\b4k\b/i,/\b2160p\b/i,/\b1080p\b/i] },
{ bonus: 0.10, terms: [/\bweb[- ]?dl\b/i,/\bwebrip\b/i,/\bamzn\b/i,/\bnflx\b/i,/\bhbo\b/i,/\bdsnp\b/i,/\bappletv\b/i,/\batvp\b/i,/\bpcok\b/i] },
{ bonus: 0.05, terms: [/\bhdtv\b/i,/\b720p\b/i,/\bhdrip\b/i] },
{ bonus: 0.08, terms: [/\bproper\b/i,/\brepack\b/i,/\breal\.proper\b/i,/\bsynced\b/i,/\bsync\b/i,/\bfixed\b/i,/\bcorrected\b/i,/\bv2\b/i,/\bv3\b/i] },
];
const AI_PATTERNS = [
/\ba\.i\.\b/i,/\bai[\s\-_]sub(title)?s?\b/i,/\bai[\s\-_](translat|generat|correct|made|creat)/i,
/(translat|generat|creat|made|sub(title)?s?)\s+(by\s+)?ai\b/i,/machine[\s\-_]translat/i,
/auto[\s\-_]?translat/i,/auto[\s\-_]?generat/i,/auto[\s\-_]?sub(title)?s?\b/i,/\bautosub\b/i,
/\bchatgpt\b/i,/\bgpt[\s\-_]?[34o]?\b/i,/\bdeep[\s\-_]?l\b/i,/google[\s\-_]translat/i,
/yandex[\s\-_]translat/i,/amazon[\s\-_]translat/i,/microsoft[\s\-_]translat/i,
/bing[\s\-_]translat/i,/\bopenai\b/i,/\bwhisper\b/i,/\bgemini\b/i,/\bclaude\b/i,
/\bllama\b/i,/neural[\s\-_](translat|sub|net|machine)/i,/\bnmt\b/i,
/artificial[\s\-_]intelligence/i,/computer[\s\-_](translat|generat)/i,
];
const AI_QUICK_HITS = [
'a.i','chatgpt','deepl','whisper','openai','gemini','claude','llama',
'machine translat','auto translat','auto-translat','auto generat','autosub','neural',
'yandex','bing translat','google translat','amazon translat','microsoft translat','nmt',
'artificial intel',' ai sub','ai-sub','ai translat','translated by ai','generated by ai',
];
function isAISub(text) {
if (!text) return false;
const lo = text.toLowerCase();
const maybeAI = AI_QUICK_HITS.some(q => lo.includes(q)) || /\bai\b/.test(lo);
if (!maybeAI) return false;
return AI_PATTERNS.some(rx => rx.test(text));
}
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';
// ================================================================
// HTTP CORE
// ================================================================
function _request(urlStr, opts = {}) {
return new Promise((resolve, reject) => {
const parsed = new URL(urlStr);
const lib = parsed.protocol === 'https:' ? https : http;
const timeout = opts.timeout || REQ_TIMEOUT;
const reqOpts = {
hostname: parsed.hostname,
port : parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path : parsed.pathname + parsed.search,
method : 'GET',
headers : {
'User-Agent' : UA,
'Accept' : opts.binary ? 'application/zip,application/octet-stream,*/*' : 'text/html,application/xhtml+xml,*/*;q=0.8',
'Accept-Language' : 'en-US,en;q=0.9',
'Accept-Encoding' : 'gzip, deflate',
'Referer' : 'https://subf2m.co/',
'Connection' : 'keep-alive',
'Cache-Control' : 'no-cache',
'Upgrade-Insecure-Requests': '1',
...(opts.headers || {}),
},
};
const req = lib.request(reqOpts, res => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
const next = new URL(res.headers.location, urlStr).toString();
return _request(next, opts).then(resolve).catch(reject);
}
if (res.statusCode === 404) return resolve(null);
if (res.statusCode !== 200) return resolve(null);
const chunks = [];
let stream = res;
const enc = res.headers['content-encoding'] || '';
if (enc.includes('gzip')) stream = res.pipe(zlib.createGunzip());
else if (enc.includes('br')) stream = res.pipe(zlib.createBrotliDecompress());
else if (enc.includes('deflate')) stream = res.pipe(zlib.createInflate());
stream.on('data', c => chunks.push(c));
stream.on('end', () => {
const buf = Buffer.concat(chunks);
resolve(opts.binary ? buf : buf.toString('utf8'));
});
stream.on('error', reject);
});
req.setTimeout(timeout, () => { req.destroy(); reject(new Error('Timeout')); });
req.on('error', reject);
req.end();
});
}
async function getHTML(url, tries = 3) {
for (let i = 0; i < tries; i++) {
try {
const r = await _request(url, { timeout: REQ_TIMEOUT });
if (r !== null) return r;
} catch (e) {
if (i < tries - 1) await sleep(1200 * (i + 1));
}
}
return null;
}
// Binary with proxy chain fallback
async function getBinary(url, tries = 3) {
for (let i = 0; i < tries; i++) {
try {
const r = await _request(url, { binary: true, timeout: DL_TIMEOUT });
if (r !== null) return r;
} catch (_) {}
}
// Proxy chain
const enc = encodeURIComponent(url);
const proxied = [
`${PROXY_1}${enc}`,
`${PROXY_2}${enc}`,
`${PROXY_3}${url}`,
];
for (const pUrl of proxied) {
try {
const r = await _request(pUrl, { binary: true, timeout: DL_TIMEOUT });
if (r && r.length > 50) return r;
} catch (_) {}
}
return null;
}
// ================================================================
// CACHE
// ================================================================
const _cache = new Map();
function cacheGet(key) {
const item = _cache.get(key);
if (!item) return null;
if (Date.now() - item.ts > CACHE_TTL) { _cache.delete(key); return null; }
return item.val;
}
function cacheSet(key, val) {
if (_cache.size >= 1200) {
let n = 0;
for (const k of _cache.keys()) { _cache.delete(k); if (++n >= 120) break; }
}
_cache.set(key, { val, ts: Date.now() });
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
// ================================================================
// STRING UTILS
// ================================================================
function titleToSlug(title, year) {
const base = (title || '').toLowerCase()
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-')
.replace(/-+/g, '-')
.replace(/^-|-$/g, '');
return year ? `${base}-${year}` : base;
}
function _clean(t) { return (t || '').replace(/\s+/g, ' ').trim(); }
// ================================================================
// RELEASE PARSING & SCORING
// ================================================================
function parseRelease(text) {
if (!text) return {};
const tokens = {};
const seMatch = text.match(/[sS](\d{1,2})[eE](\d{1,3})/);
if (seMatch) {
tokens.season = parseInt(seMatch[1]);
tokens.episode = parseInt(seMatch[2]);
} else {
const xMatch = text.match(/\b(\d{1,2})x(\d{2,3})\b/i);
if (xMatch) { tokens.season = parseInt(xMatch[1]); tokens.episode = parseInt(xMatch[2]); }
}
const resMatch = text.match(/\b(4K|2160p|1080p|720p|480p)\b/i);
if (resMatch) tokens.resolution = resMatch[1].toLowerCase();
const srcMatch = text.match(/\b(WEB[-]?DL|WEBRip|BluRay|HDTV|AMZN|NF|Netflix|DSNP|ATVP|HBO|BRAVIA|PCOK)\b/i);
if (srcMatch) tokens.source = srcMatch[1].toUpperCase().replace(/[-]/g, '');
const grpMatch = text.match(/-([A-Za-z0-9]+)$/);
if (grpMatch) tokens.group = grpMatch[1].toUpperCase();
return tokens;
}
function qualityBonus(text) {
if (!text) return 0;
let bonus = 0;
for (const tier of QUALITY_TIERS)
if (tier.terms.some(rx => rx.test(text))) bonus += tier.bonus;
return Math.min(bonus, 0.20);
}
function scoreSubtitle(sub, streamTokens) {
if (!streamTokens) return 0;
const subText = sub.releaseInfo || sub._rawText || '';
const subTokens = parseRelease(subText);
let score = 0;
if (streamTokens.season != null && streamTokens.episode != null &&
subTokens.season === streamTokens.season && subTokens.episode === streamTokens.episode)
score += 100;
if (streamTokens.source && subTokens.source && subTokens.source === streamTokens.source)
score += 20;
if (streamTokens.resolution && subTokens.resolution === streamTokens.resolution)
score += 10;
if (streamTokens.group && subTokens.group && subTokens.group === streamTokens.group)
score += 30;
if (sub.isGood) score += 5;
score += qualityBonus(subText) * 10;
return score;
}
// ================================================================
// TMDB
// ================================================================
async function getTMDB(imdbId) {
const ck = `tmdb:${imdbId}`;
const hit = cacheGet(ck);
if (hit !== null) return hit;
try {
const raw = await _request(
`${TMDB_BASE}/find/${imdbId}?external_source=imdb_id&api_key=${TMDB_KEY}`,
{ timeout: 10_000 },
);
if (!raw) { cacheSet(ck, null); return null; }
const d = JSON.parse(raw);
let info = null;
if (d.movie_results?.length) {
const m = d.movie_results[0];
info = { title: m.title || m.original_title, year: m.release_date?.substring(0,4) || null, type: 'movie', tmdbId: m.id };
} else if (d.tv_results?.length) {
const t = d.tv_results[0];
info = { title: t.name || t.original_name, year: t.first_air_date?.substring(0,4) || null, type: 'series', tmdbId: t.id };
}
cacheSet(ck, info);
return info;
} catch (e) { cacheSet(ck, null); return null; }
}
async function getTMDBSeasonYear(tmdbId, season) {
if (!tmdbId) return null;
const ck = `tmdbsy:${tmdbId}:${season}`;
const hit = cacheGet(ck);
if (hit !== null) return hit;
try {
const raw = await _request(`${TMDB_BASE}/tv/${tmdbId}/season/${season}?api_key=${TMDB_KEY}`, { timeout: 10_000 });
if (!raw) { cacheSet(ck, null); return null; }
const year = JSON.parse(raw).air_date?.substring(0, 4) || null;
cacheSet(ck, year);
return year;
} catch (e) { cacheSet(ck, null); return null; }
}
// ================================================================
// SLUG PROBING — FIXED
// No longer requires S##E## on the page — any item list qualifies.
// ================================================================
async function _probeSlug(slug, opts = {}) {
if (!slug || slug.length > 220) return false;
try {
const lang = opts.lang || 'english';
const url = `${BASE}/subtitles/${slug}/${lang}`;
const html = await getHTML(url, 1);
if (!html || html.length < 500) return false;
// Must have subtitle items
if (!html.includes('
{
const href = $(a).attr('href') || '';
if (!href.startsWith('/subtitles/')) return;
const parts = href.split('/').filter(Boolean);
if (parts.length < 2) return;
const slug = parts[1];
if (!slug || slug === 'searchbytitle' || seen.has(slug)) return;
seen.add(slug);
const rawText = _clean($(a).text());
const slugLow = slug.toLowerCase();
// Must start with show's first word for disambiguation
if (firstWord && !slugLow.startsWith(firstWord)) return;
let score = 0;
// Check title text for season match
const seriesMatch = SERIES_RE.exec(rawText);
if (seriesMatch) {
const subOrd = (seriesMatch[2] || '').trim().toLowerCase();
const subYear = seriesMatch[5];
// Ordinal match (e.g. "first season")
if (ord && subOrd === ord) score += 200;
else if (ord && subOrd.startsWith(ord)) score += 150;
// Numeric season as ordinal
if (subOrd === String(season)) score += 100;
// Year match
if (seasonYear && subYear === seasonYear) score += 50;
else if (showYear && subYear === showYear && season === 1) score += 30;
// Any series result is better than none
if (score === 0) score += 10;
}
// Also score by slug patterns (backup)
if (ord && slugLow.includes(`-${ord}-season`)) score += Math.max(score, 80);
if (slugLow.includes(`-season-${season}-`)) score += Math.max(score, 70);
if (slugLow.includes(`-s${String(season).padStart(2,'0')}`)) score += Math.max(score, 60);
if (ord && slugLow.includes(`-${ord}season`)) score += Math.max(score, 50);
if (score > 0) results.push({ slug, score });
});
if (!results.length) return null;
results.sort((a, b) => b.score - a.score);
for (const r of results.slice(0, 8)) {
await sleep(180);
if (await _probeSlug(r.slug, { season })) return r.slug;
}
return null;
}
async function _smartPatternSlugs(title, season, seasonYear, showYear) {
const base = titleToSlug(title, null);
const ord = ORDINALS[season] || '';
const pad = String(season).padStart(2, '0');
const patterns = [];
// Tier 1 — TMDB season year exact
if (seasonYear) {
if (ord) {
patterns.push(`${base}-${ord}-season-${seasonYear}`);
patterns.push(`${base}--${ord}-season-${seasonYear}`);
patterns.push(`${base}-${ord}-season-tv-${seasonYear}`);
patterns.push(`${base}-the-${ord}-season-${seasonYear}`);
}
patterns.push(`${base}-season-${season}-${seasonYear}`);
patterns.push(`${base}-s${pad}-${seasonYear}`);
patterns.push(`${base}-${seasonYear}`);
}
// Tier 2 — no year
if (ord) {
patterns.push(`${base}-${ord}-season`);
patterns.push(`${base}--${ord}-season`);
patterns.push(`${base}-the-${ord}-season`);
patterns.push(`${base}-${ord}-series`);
}
patterns.push(`${base}-season-${season}`);
patterns.push(`${base}-s${pad}`);
// Tier 3 — adjacent years
const yearBase = seasonYear ? +seasonYear : (showYear ? +showYear : null);
if (yearBase) {
for (const yr of [yearBase - 1, yearBase + 1, yearBase - 2]) {
if (ord) {
patterns.push(`${base}-${ord}-season-${yr}`);
patterns.push(`${base}-the-${ord}-season-${yr}`);
}
patterns.push(`${base}-season-${season}-${yr}`);
patterns.push(`${base}-${yr}`);
}
}
const unique = [...new Set(patterns)].filter(Boolean);
console.log(`[slug] Probing ${unique.length} patterns for "${title}" S${season}`);
for (const s of unique) {
await sleep(120);
if (await _probeSlug(s, { season })) {
console.log(`[slug] HIT: ${s}`);
return s;
}
}
return null;
}
// ================================================================
// MOVIE SLUG RESOLUTION
// ================================================================
async function resolveMovieSlug(imdbId, titleHint, yearHint) {
const ck = `mslug2:${imdbId}`;
const hit = cacheGet(ck);
if (hit !== null) return hit;
const tmdb = await getTMDB(imdbId);
const title = tmdb?.title || titleHint;
const year = tmdb?.year || yearHint;
// Strategy 1: Direct slug (most common pattern)
if (title && year) {
const direct = titleToSlug(title, year);
if (await _probeMovieSlug(direct, title)) { cacheSet(ck, direct); return direct; }
}
// Strategy 2: Search by IMDb ID
let slug = await _searchMovieSlug(imdbId, year);
if (slug && await _probeMovieSlug(slug, title)) { cacheSet(ck, slug); return slug; }
// Strategy 3: Search by title
if (title) {
slug = await _searchMovieSlug(title, year);
if (slug && await _probeMovieSlug(slug, title)) { cacheSet(ck, slug); return slug; }
// Strategy 4: Brute-force slugs
const candidates = [];
if (year) {
candidates.push(titleToSlug(title, year));
candidates.push(titleToSlug(title, String(+year - 1)));
candidates.push(titleToSlug(title, String(+year + 1)));
}
candidates.push(titleToSlug(title, null));
for (const c of [...new Set(candidates)]) {
await sleep(250);
if (await _probeMovieSlug(c, title)) { cacheSet(ck, c); return c; }
}
}
cacheSet(ck, null);
return null;
}
async function _searchMovieSlug(query, yearHint) {
const url = `${BASE}/subtitles/searchbytitle?query=${encodeURIComponent(query)}&l=`;
const html = await getHTML(url);
if (!html) return null;
const $ = cheerio.load(html);
const results = [];
const SELS = ['ul.search-result li','.search-result li','.content ul li','section ul li','ul li','li'];
let found = false;
for (const sel of SELS) {
const items = $(sel);
if (!items.length) continue;
items.each((_, el) => {
const a = $(el).find('a[href]').first();
const href = a.attr('href') || '';
if (!href.startsWith('/subtitles/') || href === '/subtitles/searchbytitle') return;
const parts = href.split('/').filter(Boolean);
if (parts.length < 2) return;
const slug = parts[1];
if (!slug || slug === 'searchbytitle') return;
const text = _clean(a.text());
const yrMatch = text.match(/\((\d{4})\)/);
const resYear = yrMatch ? +yrMatch[1] : null;
let score = 1;
if (query.startsWith('tt')) score += 100;
if (yearHint && resYear === +yearHint) score += 10;
// Penalise series results in movie search
if (text.toLowerCase().includes('season')) score -= 5;
results.push({ slug, score });
found = true;
});
if (found) break;
}
if (!results.length) return null;
results.sort((a, b) => b.score - a.score);
return results[0].slug;
}
async function _probeMovieSlug(slug, expectedTitle) {
if (!slug) return false;
try {
const html = await getHTML(`${BASE}/subtitles/${slug}/english`);
if (!html || html.length < 1500) return false;
if (expectedTitle) {
const $ = cheerio.load(html);
const pageTitle = ($('title').text() || '').toLowerCase();
const exp = expectedTitle.toLowerCase();
// Accept if title appears in page title OR if there are subtitle items
return pageTitle.includes(exp) || html.includes('class="item"') || html.includes('class="r"');
}
return true;
} catch (_) { return false; }
}
// ================================================================
// SUBTITLE SCRAPING
// ================================================================
async function getSubsList(slug, lang, season, episode, streamTokens) {
if (!ALLOWED_LANGS.has(lang)) return [];
const ck = `list2:${slug}:${lang}:${episode || 0}`;
const hit = cacheGet(ck);
if (hit) return hit;
const url = `${BASE}/subtitles/${slug}/${lang}`;
const html = await getHTML(url);
if (!html) { cacheSet(ck, []); return []; }
const $ = cheerio.load(html);
let raw = [];
// Primary scraper
$('li.item').each((_, el) => { _extractItem($, el, lang, raw); });
// Fallback 1: table rows
if (!raw.length) {
$('tr, .subtitle-item, .item').each((_, el) => { _extractItem($, el, lang, raw); });
}
// Fallback 2: any anchor with matching lang path
if (!raw.length) {
$('a[href]').each((_, a) => {
const href = $(a).attr('href') || '';
if (!href.includes(`/${lang}/`) || !/\/\d+$/.test(href)) return;
const parts = href.split('/');
const subId = parts[parts.length - 1];
if (!subId || !/^\d+$/.test(subId)) return;
const full = href.startsWith('http') ? href : `${BASE}${href}`;
raw.push({ subId, detailUrl: full, isGood: false, releaseInfo: '', comment: '', authorNote: '', lang, goodVotes: 0, badVotes: 0, downloads: 0, _rawText: '' });
});
}
// Filter to exact episode if possible
if (season && episode && raw.length) {
const expected = `s${String(season).padStart(2,'0')}e${String(episode).padStart(2,'0')}`;
const filtered = raw.filter(item => {
const txt = (item._rawText || item.releaseInfo || '').toLowerCase();
return txt.includes(expected);
});
if (filtered.length) raw = filtered;
}
// Deduplicate
const seen = new Set();
const unique = raw.filter(s => { if (seen.has(s.subId)) return false; seen.add(s.subId); return true; });
const toEnrich = unique.slice(0, MAX_DETAIL);
const enriched = (await Promise.all(toEnrich.map(s => _enrichSub(s)))).filter(Boolean);
const human = enriched.filter(s => !isAISub(`${s.releaseInfo} ${s.comment} ${s.authorNote}`));
cacheSet(ck, human);
return human;
}
function _extractItem($, el, lang, out) {
const $el = $(el);
const rawText = _clean($el.text());
let detailHref = null;
$el.find('a').each((_, a) => {
const h = $(a).attr('href') || '';
if (h.includes(`/${lang}/`) && /\/\d+$/.test(h)) detailHref = h;
});
if (!detailHref) return;
const hLow = detailHref.toLowerCase();
if (!hLow.includes(`/${lang}/`)) return;
if (OTHER_LANGS.some(ol => hLow.includes(`/${ol}/`))) return;
const parts = detailHref.split('/');
const subId = parts[parts.length - 1];
if (!subId || !/^\d+$/.test(subId)) return;
const isGood = (
$el.find('.green-icon, .positive-icon').length > 0 ||
$el.find('img[alt="good"], img[alt="Positive"], img[alt="positive"]').length > 0 ||
$el.hasClass('good') || $el.hasClass('positive') ||
($el.attr('class') || '').toLowerCase().includes('good')
);
const comment = _clean($el.find('p.comment, .comment').first().text());
const releaseLines = $el.find('.r li, ul.release li, ul.scrolllist li, ul li')
.map((_, li) => $(li).text().trim()).get().filter(Boolean);
const releaseInfo = (releaseLines.join(' | ') || _clean($el.text())).substring(0, 130);
if (isAISub(`${comment} ${releaseInfo}`)) return;
out.push({
subId,
detailUrl: detailHref.startsWith('http') ? detailHref : `${BASE}${detailHref}`,
isGood, releaseInfo, comment, authorNote: '', lang,
goodVotes: 0, badVotes: 0, downloads: 0, _rawText: rawText,
});
}
async function _enrichSub(sub) {
const ck = `det2:${sub.subId}`;
const hit = cacheGet(ck);
if (hit !== null) { if (hit.__aiBlocked) return null; return { ...sub, ...hit }; }
const html = await getHTML(sub.detailUrl);
if (!html) return sub;
const $ = cheerio.load(html);
const txt = $.root().text();
const goodVotes = +(txt.match(/voted\s+as\s+good\s+by[:\s]*(\d+)/i)?.[1] ?? 0);
const badVotes = +(txt.match(/voted\s+as\s+bad\s+by[:\s]*(\d+)/i)?.[1] ?? 0);
const downloads = +(txt.match(/downloads?[:\s]*(\d+)/i)?.[1] ?? 0);
const total = goodVotes + badVotes;
const ratio = total >= 3 ? goodVotes / total : null;
const isGood = sub.isGood || (ratio !== null && ratio >= 0.70 && goodVotes >= 2);
const authorNote = _clean($('p.comment, .comment, .notes, .author-comment').first().text());
if (isAISub(`${authorNote} ${sub.releaseInfo} ${sub.comment}`)) {
cacheSet(ck, { __aiBlocked: true });
return null;
}
const relLines = $('ul.release li, .release-names li, ul.scrolllist li').map((_, e) => $(e).text().trim()).get();
const releaseInfo = relLines.length ? relLines.join(' | ').substring(0, 130) : sub.releaseInfo;
const enriched = { ...sub, releaseInfo, goodVotes, badVotes, downloads, isGood, authorNote };
cacheSet(ck, enriched);
return enriched;
}
// ================================================================
// DOWNLOAD & SRT EXTRACTION
// ================================================================
async function fetchSRT(downloadUrl) {
const ck = `srt2:${downloadUrl}`;
const hit = cacheGet(ck);
if (hit !== null) return hit;
try {
const buf = await getBinary(downloadUrl);
if (!buf) return null;
const content = _extractFromZip(buf) ?? _tryDirect(buf);
if (!content) return null;
cacheSet(ck, content);
return content;
} catch (_) { return null; }
}
function _extractFromZip(buf) {
try {
const zip = new AdmZip(buf);
const RANK = { '.srt': 0, '.vtt': 1, '.ass': 2, '.ssa': 3 };
const files = zip.getEntries()
.filter(e => {
const n = e.entryName.toLowerCase();
return !e.isDirectory && !n.startsWith('__macosx') && Object.keys(RANK).some(x => n.endsWith(x));
})
.sort((a, b) => (RANK[_ext(a.entryName)] ?? 9) - (RANK[_ext(b.entryName)] ?? 9));
for (const f of files) {
const raw = zip.readFile(f);
if (!raw) continue;
const text = _decode(raw);
if (_isSub(text)) return text;
}
} catch (_) {}
return null;
}
function _tryDirect(buf) { const t = _decode(buf); return _isSub(t) ? t : null; }
const _ext = n => (n.match(/(\.[^.]+)$/)?.[1]?.toLowerCase() ?? '');
const _isSub = t => typeof t === 'string' && t.includes('-->') && t.length > 50;
function _decode(buf) {
const hasBOM = buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF;
const u8 = buf.slice(hasBOM ? 3 : 0).toString('utf8');
if (_isSub(u8)) return u8;
return buf.toString('latin1');
}
// ================================================================
// SORT: index [0] = best subtitle
// Movies: sort by downloads desc (most popular = best sync).
// Series: sort by scoreSubtitle desc (exact S##E## + group match).
// ================================================================
function sortForBest(subs, streamTokens, type) {
if (type === 'series' && streamTokens) {
return subs
.map(s => ({ ...s, _score: scoreSubtitle(s, streamTokens) }))
.sort((a, b) => {
if (b._score !== a._score) return b._score - a._score;
if (b.isGood !== a.isGood) return b.isGood ? 1 : -1;
return b.downloads - a.downloads;
});
}
// Movies: isGood first, then downloads
return subs.slice().sort((a, b) => {
if (b.isGood !== a.isGood) return b.isGood ? 1 : -1;
return b.downloads - a.downloads;
});
}
// ================================================================
// ROUTES
// ================================================================
app.get('/manifest.json', (req, res) => res.json({
id : 'org.van.vansubf2mplus.v2',
name : 'VanSubF2M+',
description: 'Human-Verified Arabic & English subtitles. Anti-AI. Enhanced 100% coverage.',
version : '2.0.0',
resources : ['subtitles'],
types : ['movie', 'series'],
idPrefixes : ['tt'],
catalogs : [],
}));
app.get('/sub/:enc.srt', async (req, res) => {
let dlUrl = '';
try { dlUrl = Buffer.from(req.params.enc, 'base64url').toString('utf8'); }
catch (_) { return res.status(400).end('Bad request'); }
if (!dlUrl.startsWith('https://subf2m.co/') || !dlUrl.includes('/subtitles/') || !dlUrl.endsWith('/download'))
return res.status(403).end('Forbidden');
const content = await fetchSRT(dlUrl);
if (!content) return res.status(404).end('Not found');
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.setHeader('Content-Disposition', 'inline; filename="subtitle.srt"');
res.end(content);
});
app.get('/subtitles/:type/:id/:extra?.json', async (req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
const { type, id } = req.params;
if (!id?.startsWith('tt')) return res.json({ subtitles: [] });
const ck = `result2:${id}`;
const hit = cacheGet(ck);
if (hit) return res.json({ subtitles: hit });
try {
let imdbId = id, season = null, episode = null;
if (id.includes(':')) {
const parts = id.split(':');
imdbId = parts[0];
season = parts[1] ? +parts[1] : null;
episode = parts[2] ? +parts[2] : null;
}
if (!/^tt\d+$/.test(imdbId)) return res.json({ subtitles: [] });
const filename = req.query.filename || '';
const streamTokens = (type === 'series' && season) ? parseRelease(filename) : null;
let slug;
if (type === 'series' && season) {
slug = await resolveSeriesSlug(imdbId, season);
} else {
const tmdb = await getTMDB(imdbId);
slug = await resolveMovieSlug(imdbId, tmdb?.title || null, tmdb?.year || null);
}
if (!slug) return res.json({ subtitles: [] });
const [engRes, araRes] = await Promise.allSettled([
getSubsList(slug, 'english', season, episode, streamTokens),
getSubsList(slug, 'arabic', season, episode, streamTokens),
]);
let engSubs = (engRes.status === 'fulfilled' ? engRes.value : []);
let araSubs = (araRes.status === 'fulfilled' ? araRes.value : []);
// Sort so [0] = best, then cap at MAX_PER_LANG
engSubs = sortForBest(engSubs, streamTokens, type).slice(0, MAX_PER_LANG);
araSubs = sortForBest(araSubs, streamTokens, type).slice(0, MAX_PER_LANG);
const proto = req.headers['x-forwarded-proto'] || (req.socket?.encrypted ? 'https' : 'http');
const host = req.headers['x-forwarded-host'] || req.headers.host;
const origin = `${proto}://${host}`;
const build = (subs, langCode, flag) => subs.map(s => {
const dlUrl = `${s.detailUrl}/download`;
const enc = Buffer.from(dlUrl).toString('base64url');
const badge = s.isGood ? ' ⭐' : '';
const votes = s.goodVotes > 0 ? ` [👍${s.goodVotes}]` : '';
const dls = s.downloads > 0 ? ` [⬇${s.downloads}]` : '';
const name = `${flag}${badge}${votes}${dls} ${s.releaseInfo}`.trim().substring(0, 100);
return { id: `vsub-${langCode}-${s.subId}`, url: `${origin}/sub/${enc}.srt`, lang: langCode, name };
});
const subtitles = [...build(engSubs, 'eng', '🇺🇸 English'), ...build(araSubs, 'ara', '🇸🇦 Arabic')];
cacheSet(ck, subtitles);
res.json({ subtitles });
} catch (e) {
console.error('[route]', e.message);
res.json({ subtitles: [] });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT}`);
});