'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}`); });