/** * Proxy management utilities. * Extracted from server.js — centralizes all proxy configuration, agent caching, * Cloudflare Workers proxy rotation, CORS fallback, and site-specific request helpers. */ const axios = require("axios"); const http = require("http"); const https = require("https"); const { SocksProxyAgent } = require("socks-proxy-agent"); const { HttpProxyAgent } = require("http-proxy-agent"); const { HttpsProxyAgent } = require("https-proxy-agent"); const tough = require("tough-cookie"); const initCycleTLS = require("cycletls"); // === PROXY AGENT CACHES === const proxyAgentCache = new Map(); const darkinoProxyAgentCache = new Map(); const proxyRotationState = new Map(); // Initialize global agent keep-alive with socket limits http.globalAgent.keepAlive = true; http.globalAgent.maxSockets = 128; http.globalAgent.maxFreeSockets = 32; https.globalAgent.keepAlive = true; https.globalAgent.maxSockets = 128; https.globalAgent.maxFreeSockets = 32; // === PROXY CONFIGURATION FLAGS === const ENABLE_DARKINO_PROXY = true; // Passe \u00e0 false pour d\u00e9sactiver le proxy pour Darkino const ENABLE_COFLIX_PROXY = true; // Passe \u00e0 false pour d\u00e9sactiver le proxy pour Coflix const ENABLE_FRENCH_STREAM_PROXY = true; // Active/d\u00e9sactive le proxy pour French-Stream const ENABLE_LECTEURVIDEO_PROXY = true; // Active/d\u00e9sactive le proxy pour LecteurVideo const ENABLE_FSTREAM_PROXY = true; // Active/d\u00e9sactive le proxy pour FStream const ENABLE_ANIME_PROXY = true; // Active/d\u00e9sactive le proxy pour AnimeSama (via Cloudflare Workers) const ENABLE_WIFLIX_PROXY = true; // Active/d\u00e9sactive le proxy pour Wiflix const MAX_LECTEURVIDEO_PROXY_ATTEMPTS = 2; // Constante pour l'enhancement Darkino const darkiworld_premium = false; // Passe \u00e0 false pour d\u00e9sactiver l'enhancement Darkino // === DARKINO 403 COOLDOWN === // Cooldown de 5 minutes apr\u00e8s une erreur 403 (Cloudflare challenge) const DARKINO_403_COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes let darkino403CooldownUntil = 0; // Timestamp jusqu'auquel on ne fait plus de requ\u00eates // === CPASMAL CONFIGURATION === const CPASMAL_BASE_URL = "https://www.cpasmal.rip"; const cpasmalJar = new tough.CookieJar(null, { rejectPublicSuffixes: false }); // Cache pour les agents SOCKS5 Cpasmal (Keep-Alive) const cpasmalAgentCache = new Map(); function getCpasmalAgent(proxy) { if (!proxy) return null; const cacheKey = `${proxy.type}:${proxy.host}:${proxy.port}:${proxy.auth}`; if (cpasmalAgentCache.has(cacheKey)) { return cpasmalAgentCache.get(cacheKey); } // Configuration Keep-Alive optimis\u00e9e const agentOpts = { keepAlive: true, keepAliveMsecs: 15000, // 15s keep-alive maxSockets: 50, maxFreeSockets: 10, timeout: 30000, }; let agent; if (proxy.type === "socks5" || proxy.type === "socks5h") { const info = { hostname: proxy.host, port: proxy.port, protocol: "socks:", tls: { rejectUnauthorized: false }, ...agentOpts, }; // Auth handling if (proxy.auth) { const parts = proxy.auth.split(":"); info.username = parts[0]; // Correction: userId -> username info.password = parts[1]; } agent = new SocksProxyAgent(info); } if (agent) { cpasmalAgentCache.set(cacheKey, agent); } return agent; } // Fonction pour faire des requ\u00eates vers Cpasmal avec rotation de proxies SOCKS5 async function axiosCpasmalRequest(config) { const urlStr = config.url || ""; // Constante pour Cpasmal Base URL si relative const baseURL = CPASMAL_BASE_URL; // Headers par d\u00e9faut const defaultHeaders = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8", "Accept-Encoding": "gzip, deflate, br", Connection: "keep-alive", ...(config.headers || {}), }; // Gestion des cookies (r\u00e9cup\u00e9ration manuelle pour compatibilit\u00e9 avec proxy agent) if (urlStr || config.baseURL) { try { // Reconstitution approximative de l'URL pour les cookies const targetUrl = urlStr.startsWith("http") ? urlStr : (config.baseURL || baseURL) + urlStr; const cookieString = await cpasmalJar.getCookieString(targetUrl); if (cookieString) { defaultHeaders["Cookie"] = cookieString; } } catch (err) { console.error("[Cpasmal CookieJar] Error getting cookies:", err); } } // Déterminer les agents proxy à utiliser : // 1. _cpasmalAgents fournis (session scopée avec DARKINO_PROXIES HTTP) // 2. _cpasmalProxy fourni (SOCKS5 override) // 3. Sinon : proxy SOCKS5 aléatoire (legacy) let httpAgent, httpsAgent, proxyLabel; if (config._cpasmalAgents) { httpAgent = config._cpasmalAgents.httpAgent; httpsAgent = config._cpasmalAgents.httpsAgent; proxyLabel = config._cpasmalAgents._label || "HTTP proxy"; } else { const proxy = config._cpasmalProxy !== undefined ? config._cpasmalProxy : pickRandomProxyOrNone(); const agent = getCpasmalAgent(proxy); httpAgent = agent; httpsAgent = agent; proxyLabel = proxy ? `${proxy.host}:${proxy.port}` : "Direct"; } // Nettoyer les champs internes avant de passer à axios const { _cpasmalProxy, _cpasmalAgents, ...cleanConfig } = config; try { if (process.env.DEBUG_CPASMAL === "true") console.time(`[Cpasmal] Request ${urlStr} (Proxy: ${proxyLabel})`); const response = await axios({ ...cleanConfig, headers: defaultHeaders, httpAgent, httpsAgent, proxy: false, timeout: config.timeout || 5000, decompress: true, }); if (process.env.DEBUG_CPASMAL === "true") console.timeEnd(`[Cpasmal] Request ${urlStr} (Proxy: ${proxyLabel})`); // Gestion des cookies (sauvegarde manuelle) if (response.headers["set-cookie"]) { const cookies = response.headers["set-cookie"]; const url = response.config.url; try { if (Array.isArray(cookies)) { for (const cookie of cookies) { await cpasmalJar.setCookie(cookie, url); } } else { await cpasmalJar.setCookie(cookies, url); } } catch (err) { console.error("[Cpasmal CookieJar] Error setting cookies:", err); } } return response; } catch (error) { // Ajouter infos debug error.cpasmalUrl = urlStr; error.cpasmalProxy = proxyLabel; // Propager l'erreur immédiatement throw error; } } // === CLOUDFLARE WORKERS PROXIES CONFIGURATION === const CLOUDFLARE_WORKERS_PROXIES = ( process.env.CLOUDFLARE_WORKERS_PROXIES || "" ) .split(",") .map((proxy) => proxy.trim()) .filter(Boolean); // === CACHE POUR LES PROXIES CLOUDFLARE EN ERREUR === // Cache pour m\u00e9moriser les proxies en erreur (429, 500, timeout, etc.) // Les proxies en erreur seront ignor\u00e9s pendant PROXY_ERROR_COOLDOWN_MS millisecondes const proxyErrorCache = new Map(); // Map const PROXY_ERROR_COOLDOWN_MS = 60000; // 60 secondes de cooldown pour un proxy en erreur const PROXY_ERROR_COOLDOWN_429_MS = 120000; // 2 minutes de cooldown sp\u00e9cifique pour erreur 429 (rate limit) const PROXY_ERROR_COOLDOWN_5XX_MS = 90000; // 1.5 minutes pour les erreurs serveur (500, 502, 503, 504) const MAX_CONSECUTIVE_ERRORS = 3; // Nombre max d'erreurs cons\u00e9cutives avant cooldown prolong\u00e9 const PROXY_EXTENDED_COOLDOWN_MS = 300000; // 5 minutes de cooldown prolong\u00e9 si trop d'erreurs cons\u00e9cutives /** * Marque un proxy comme \u00e9tant en erreur * @param {string} proxyUrl - URL du proxy * @param {number|string} errorCode - Code d'erreur (429, 500, 'timeout', etc.) */ function markProxyAsErrored(proxyUrl, errorCode) { const existing = proxyErrorCache.get(proxyUrl); const errorCount = existing ? existing.errorCount + 1 : 1; proxyErrorCache.set(proxyUrl, { errorTime: Date.now(), errorCode, errorCount, }); // Log seulement si DEBUG activ\u00e9 if (process.env.DEBUG_PROXY) { console.log( `[PROXY CACHE] Proxy marqu\u00e9 en erreur: ${proxyUrl} (code: ${errorCode}, count: ${errorCount})`, ); } } /** * V\u00e9rifie si un proxy est actuellement en cooldown (\u00e0 \u00e9viter) * @param {string} proxyUrl - URL du proxy * @returns {boolean} - true si le proxy doit \u00eatre ignor\u00e9 */ function isProxyInCooldown(proxyUrl) { const errorInfo = proxyErrorCache.get(proxyUrl); if (!errorInfo) return false; const now = Date.now(); const timeSinceError = now - errorInfo.errorTime; // D\u00e9terminer le cooldown appropri\u00e9 selon le type d'erreur et le nombre d'erreurs let cooldownMs; if (errorInfo.errorCount >= MAX_CONSECUTIVE_ERRORS) { cooldownMs = PROXY_EXTENDED_COOLDOWN_MS; } else if (errorInfo.errorCode === 429) { cooldownMs = PROXY_ERROR_COOLDOWN_429_MS; } else if (errorInfo.errorCode >= 500 && errorInfo.errorCode < 600) { cooldownMs = PROXY_ERROR_COOLDOWN_5XX_MS; } else { cooldownMs = PROXY_ERROR_COOLDOWN_MS; } // Si le cooldown est pass\u00e9, supprimer l'entr\u00e9e du cache if (timeSinceError >= cooldownMs) { proxyErrorCache.delete(proxyUrl); return false; } return true; } /** * Retourne la liste des proxies disponibles (non en cooldown) * @param {string[]} allProxies - Liste de tous les proxies * @returns {string[]} - Liste des proxies disponibles */ function getAvailableProxies(allProxies) { const available = allProxies.filter((proxy) => !isProxyInCooldown(proxy)); // Si tous les proxies sont en cooldown, on r\u00e9initialise le cache et on retourne tous les proxies // pour \u00e9viter de bloquer compl\u00e8tement le service if (available.length === 0) { if (process.env.DEBUG_PROXY) { console.log( "[PROXY CACHE] Tous les proxies sont en cooldown, r\u00e9initialisation du cache", ); } proxyErrorCache.clear(); return allProxies; } return available; } /** * R\u00e9initialise le compteur d'erreurs d'un proxy apr\u00e8s un succ\u00e8s * @param {string} proxyUrl - URL du proxy */ function markProxyAsHealthy(proxyUrl) { proxyErrorCache.delete(proxyUrl); } // Nettoyage p\u00e9riodique du proxyErrorCache — supprime les entr\u00e9es dont le cooldown a expir\u00e9 setInterval( () => { const now = Date.now(); for (const [proxyUrl, errorInfo] of proxyErrorCache) { const maxCooldown = PROXY_EXTENDED_COOLDOWN_MS; // 5 min = cooldown le plus long if (now - errorInfo.errorTime > maxCooldown) { proxyErrorCache.delete(proxyUrl); } } }, 5 * 60 * 1000, ).unref(); // Toutes les 5 minutes — unref to not prevent process exit /** * Construit l'URL proxy en fonction du type de proxy * Les proxies se terminant par '/' attendent l'URL encod\u00e9e * Les proxies se terminant par '?' attendent l'URL non-encod\u00e9e * @param {string} proxyUrl - URL du proxy Cloudflare * @param {string} targetUrl - URL cible \u00e0 proxyer * @returns {string} - URL finale \u00e0 appeler */ function buildProxiedUrl(proxyUrl, targetUrl) { if (proxyUrl.endsWith("/")) { // Proxy qui attend l'URL encod\u00e9e (ex: cors-worker-1) return proxyUrl + encodeURIComponent(targetUrl); } else { // Proxy qui attend l'URL en query string (ex: ?url=...) return proxyUrl + targetUrl; } } // Fonction pour faire une requ\u00eate avec fallback CORS en cas d'erreur 429 async function makeRequestWithCorsFallback(targetUrl, options = {}) { const { timeout = 7000, headers = {}, decompress = true, method: requestMethod, data: requestData, ...otherOptions } = options; const method = (requestMethod || 'GET').toUpperCase(); const defaultHeaders = { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", priority: "u=1, i", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "cross-site", "sec-gpc": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", ...headers, }; // Filtrer les proxies disponibles (non en cooldown) const availableProxies = getAvailableProxies(CLOUDFLARE_WORKERS_PROXIES); if (availableProxies.length === 0) { return axios({ url: targetUrl, method, headers: defaultHeaders, timeout, decompress, ...(requestData !== undefined ? { data: requestData } : {}), ...otherOptions, }); } // Utiliser directement les proxies Cloudflare Workers disponibles let lastError = null; for (let i = 0; i < availableProxies.length; i++) { const currentProxy = availableProxies[i]; try { const finalProxyUrl = buildProxiedUrl(currentProxy, targetUrl); const response = await axios({ url: finalProxyUrl, method, headers: defaultHeaders, timeout, decompress, ...(requestData !== undefined ? { data: requestData } : {}), ...otherOptions, }); // Succ\u00e8s : marquer le proxy comme sain markProxyAsHealthy(currentProxy); return response; } catch (proxyError) { lastError = proxyError; const statusCode = proxyError.response?.status; const errorCode = statusCode || proxyError.code || "unknown"; // En cas d'erreur 400 ou 403, arr\u00eat imm\u00e9diat sans r\u00e9essayer avec d'autres proxies if (statusCode === 400 || statusCode === 403) { throw proxyError; } // Marquer le proxy en erreur pour les codes 429, 5xx, timeout, etc. if ( statusCode === 429 || (statusCode >= 500 && statusCode < 600) || proxyError.code === "ECONNABORTED" || proxyError.code === "ETIMEDOUT" ) { markProxyAsErrored(currentProxy, errorCode); } // Si c'est le dernier proxy et qu'on a une erreur, throw l'erreur if (i === availableProxies.length - 1) { throw proxyError; } // Sinon continuer avec le prochain proxy (429, etc.) } } // Si on arrive ici, tous les proxies ont \u00e9chou\u00e9 throw lastError || new Error("Tous les proxies ont \u00e9chou\u00e9"); } // Fonction pour faire une requ\u00eate Coflix avec rotation Cloudflare Workers. async function makeCoflixRequest(targetUrl, options = {}) { const { timeout = 15000, headers = {}, decompress = true, ...otherOptions } = options; // Nettoyer l'URL pour \u00e9viter les espaces ind\u00e9sirables const cleanTargetUrl = targetUrl.trim(); // Headers pour les requ\u00eates Coflix via proxy const coflixProxyHeaders = { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "accept-encoding": "gzip, deflate, br", "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", "cache-control": "no-cache", pragma: "no-cache", "sec-ch-ua": '"Chromium";v="140", "Not=A?Brand";v="24", "Brave";v="140"', "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": '"Windows"', "sec-fetch-dest": "document", "sec-fetch-mode": "navigate", "sec-fetch-site": "cross-site", "sec-gpc": "1", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", ...headers, }; // Supprimer les headers li\u00e9s \u00e0 l'IP d'origine pour \u00e9viter de les transmettre const headersToRemove = [ "X-Forwarded-For", "X-Real-IP", "X-Client-IP", "CF-Connecting-IP", "True-Client-IP", "X-Original-Forwarded-For", ]; const cleanHeaders = { ...coflixProxyHeaders }; headersToRemove.forEach((header) => { delete cleanHeaders[header]; delete cleanHeaders[header.toLowerCase()]; }); const proxyCandidates = getAvailableProxies(CLOUDFLARE_WORKERS_PROXIES); let lastError = null; if (proxyCandidates.length === 0) { return axios({ url: cleanTargetUrl, method: otherOptions.method || "GET", headers: cleanHeaders, timeout, decompress, responseType: "text", responseEncoding: "utf8", ...otherOptions, }); } // Un 403/429 peut d\u00e9pendre du worker utilise : on tente chaque worker avant d'abandonner. for (let i = 0; i < proxyCandidates.length; i++) { const cloudflareProxy = proxyCandidates[i]; const proxiedUrl = buildProxiedUrl(cloudflareProxy, cleanTargetUrl); try { const response = await axios({ url: proxiedUrl, method: otherOptions.method || "GET", headers: cleanHeaders, timeout, decompress, responseType: "text", responseEncoding: "utf8", ...otherOptions, }); markProxyAsHealthy(cloudflareProxy); return response; } catch (error) { lastError = error; const statusCode = error.response?.status; const errorCode = error.code || "unknown"; error.coflixUrl = cleanTargetUrl; error.coflixProxy = cloudflareProxy; error.coflixProxiedUrl = proxiedUrl; if (statusCode === 403 || statusCode === 429) { markProxyAsErrored(cloudflareProxy, statusCode); continue; } if (statusCode >= 500 && statusCode < 600) { markProxyAsErrored(cloudflareProxy, statusCode); continue; } if ( errorCode === "ECONNABORTED" || errorCode === "ETIMEDOUT" || errorCode === "ECONNRESET" || errorCode === "EHOSTUNREACH" || errorCode === "ENETUNREACH" ) { markProxyAsErrored(cloudflareProxy, errorCode); continue; } // Pour les erreurs fonctionnelles (400, 404, parsing, etc.), arr\u00eater imm\u00e9diatement. throw error; } } if (lastError) throw lastError; throw new Error( "Tous les Cloudflare Workers proxies ont \u00e9chou\u00e9 (Coflix)", ); } // Fonction pour faire une requete LecteurVideo avec CycleTLS. // On limite volontairement la rotation a 2 proxies max pour eviter de balayer tout le pool. // Instance CycleTLS partagee (singleton) let cycleTLSInstance = null; let cycleTLSInitializing = false; const cycleTLSWaiters = []; async function getCycleTLS() { if (cycleTLSInstance) return cycleTLSInstance; if (cycleTLSInitializing) { return new Promise((resolve) => cycleTLSWaiters.push(resolve)); } cycleTLSInitializing = true; try { cycleTLSInstance = await initCycleTLS(); cycleTLSInitializing = false; for (const waiter of cycleTLSWaiters) waiter(cycleTLSInstance); cycleTLSWaiters.length = 0; return cycleTLSInstance; } catch (err) { cycleTLSInitializing = false; // Reject all waiters so they don't leak forever for (const waiter of cycleTLSWaiters) waiter(Promise.reject(err)); cycleTLSWaiters.length = 0; throw err; } } // Graceful shutdown: kill CycleTLS Go subprocess async function shutdownCycleTLS() { if (cycleTLSInstance) { try { await cycleTLSInstance.exit(); } catch { /* ignore */ } cycleTLSInstance = null; } } // JA3 fingerprint Chrome 120+ const CHROME_JA3 = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513,29-23-24,0"; const CHROME_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"; function pickLecteurVideoProxyCandidates() { const httpPool = HTTP_PROXIES.length > 0 ? HTTP_PROXIES : parseProxyPayload(process.env.HTTP_PROXIES, "http") .map((entry) => sanitizeProxyEntry(entry, "http")) .filter(Boolean); const socksPool = PROXIES.length > 0 ? PROXIES : parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5") .map((entry) => sanitizeProxyEntry(entry, "socks5")) .filter(Boolean); const preferredPool = httpPool.length > 0 ? { proxies: httpPool, useSocks: false, label: "HTTP_PROXIES" } : { proxies: socksPool, useSocks: true, label: "SOCKS5_PROXIES" }; if (!preferredPool.proxies || preferredPool.proxies.length === 0) { return { proxies: [], useSocks: false, label: "aucun", totalAvailable: 0, }; } const selectedProxies = [...preferredPool.proxies] .sort(() => Math.random() - 0.5) .slice(0, MAX_LECTEURVIDEO_PROXY_ATTEMPTS); return { proxies: selectedProxies, useSocks: preferredPool.useSocks, label: preferredPool.label, totalAvailable: preferredPool.proxies.length, }; } async function makeLecteurVideoRequest(targetUrl, options = {}) { const { headers = {} } = options; const cleanTargetUrl = targetUrl.trim(); const { proxies, useSocks, label: proxyPoolLabel, totalAvailable, } = pickLecteurVideoProxyCandidates(); if (!proxies || proxies.length === 0) { const cycleTLS = await getCycleTLS(); const response = await cycleTLS( cleanTargetUrl, { body: "", ja3: CHROME_JA3, userAgent: CHROME_UA, headers: { Referer: "https://coflix.date/", Origin: "https://coflix.date", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.9", ...headers, }, timeout: 15, }, "get", ); const body = typeof response.body === "string" ? response.body : JSON.stringify(response.body); return { data: body, status: response.status, headers: response.headers || {}, }; } const cycleTLS = await getCycleTLS(); let lastError = null; console.log( `[LECTEURVIDEO] ${proxies.length} proxy(s) retenu(s) sur ${totalAvailable} depuis ${proxyPoolLabel}`, ); for (let i = 0; i < proxies.length; i++) { const proxy = proxies[i]; const auth = proxy.auth ? `${proxy.auth}@` : ""; const proxyUrl = useSocks ? `socks5h://${auth}${proxy.host}:${proxy.port}` : `http://${auth}${proxy.host}:${proxy.port}`; console.log( `[LECTEURVIDEO] tentative ${i + 1}/${proxies.length} via cycletls + ${proxy.host}:${proxy.port} (${proxyPoolLabel})`, ); try { const response = await cycleTLS( cleanTargetUrl, { body: "", ja3: CHROME_JA3, userAgent: CHROME_UA, proxy: proxyUrl, headers: { Referer: "https://coflix.date/", Origin: "https://coflix.date", Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.9", ...headers, }, timeout: 15, }, "get", ); const body = typeof response.body === "string" ? response.body : JSON.stringify(response.body); const title = body.match(/(.*?)<\/title>/i)?.[1] || "no title"; console.log( `[LECTEURVIDEO] ${proxy.host}:${proxy.port} -> ${response.status} | title: ${title} | ${body.length} chars`, ); if ( response.status === 403 && (body.includes("cf-wrapper") || body.includes("cloudflare")) ) { console.warn( `[LECTEURVIDEO] proxy ${i + 1}/${proxies.length} (${proxy.host}) bloque par Cloudflare, retry...`, ); continue; } return { data: body, status: response.status, headers: response.headers || {}, }; } catch (error) { console.error( `[LECTEURVIDEO] proxy ${i + 1}/${proxies.length} (${proxy.host}) echoue: ${error.message?.substring(0, 100)}`, ); lastError = error; continue; } } console.error( `[LECTEURVIDEO] tous les proxies ont echoue pour ${cleanTargetUrl}`, ); throw lastError || new Error("[LECTEURVIDEO] Tous les proxies ont echoue"); } // Fonction AnimeSama avec CycleTLS (cloudscraper-style : JA3 Chrome pour bypass Cloudflare). // Utilise le SOCKS5_PROXIES env directement (proxies dedies) -- pas le pool ProxyScrape, // pas de fallback direct sans proxy. const MAX_ANIMESAMA_CYCLETLS_ATTEMPTS = 2; const ANIMESAMA_SOCKS5_PROXIES = dedupeProxyEntries( parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5") .map((entry) => sanitizeProxyEntry(entry, "socks5")) .filter(Boolean), ); async function makeAnimeSamaRequest(targetUrl, options = {}) { const { headers = {}, timeout = 30, method = "get", body = "", } = options; const cleanTargetUrl = targetUrl.trim(); const lowerMethod = String(method).toLowerCase(); const defaultHeaders = { Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.6", ...headers, }; const pool = [...ANIMESAMA_SOCKS5_PROXIES] .sort(() => Math.random() - 0.5) .slice(0, MAX_ANIMESAMA_CYCLETLS_ATTEMPTS); if (pool.length === 0) { throw new Error( "[ANIMESAMA CYCLETLS] aucun proxy SOCKS5 defini dans SOCKS5_PROXIES", ); } const cycleTLS = await getCycleTLS(); const asAxiosLike = (response) => { const data = typeof response.body === "string" ? response.body : JSON.stringify(response.body); return { data, status: response.status, headers: response.headers || {} }; }; const isCloudflareBlock = (status, bodyStr) => status === 403 && typeof bodyStr === "string" && (bodyStr.includes("cf-wrapper") || bodyStr.includes("cloudflare")); let lastError = null; for (let i = 0; i < pool.length; i++) { const proxy = pool[i]; const auth = proxy.auth ? `${proxy.auth}@` : ""; const proxyUrl = `socks5h://${auth}${proxy.host}:${proxy.port}`; try { const response = await cycleTLS( cleanTargetUrl, { body, ja3: CHROME_JA3, userAgent: CHROME_UA, proxy: proxyUrl, headers: defaultHeaders, timeout, }, lowerMethod, ); const result = asAxiosLike(response); if (isCloudflareBlock(result.status, result.data)) { lastError = new Error( `[ANIMESAMA CYCLETLS] cloudflare block via ${proxy.host}:${proxy.port}`, ); continue; } return result; } catch (err) { lastError = err; } } throw lastError || new Error("[ANIMESAMA CYCLETLS] tous les proxies ont echoue"); } // === SOCKS5 PROXIES (from env) === const parseJsonArrayEnv = (envName, fallback = []) => { const rawValue = process.env[envName]; if (!rawValue) return fallback; try { const parsed = JSON.parse(rawValue); return Array.isArray(parsed) ? parsed : fallback; } catch { return fallback; } }; const { redis } = require("../config/redis"); const { acquireRedisLock } = require("./redisLock"); const PROXY_ROTATION_REDIS_PREFIX = "proxyscrape:rotation:v1"; const PROXY_RATE_LIMIT_REDIS_PREFIX = "proxyscrape:rate-limit:v1"; const proxyRateLimitState = new Map(); function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function buildProxyRateLimitKey(poolName, proxy) { if (!proxy || !proxy.host || !proxy.port) return null; return `${PROXY_RATE_LIMIT_REDIS_PREFIX}:${poolName}:${proxy.type || "proxy"}:${proxy.host}:${proxy.port}:${proxy.auth || ""}`; } function reserveLocalRateLimitedProxy(proxyKey, minIntervalMs) { if (!proxyKey) return false; const now = Date.now(); const nextAllowedAt = proxyRateLimitState.get(proxyKey) || 0; if (nextAllowedAt > now) { return false; } proxyRateLimitState.set(proxyKey, now + minIntervalMs); return true; } async function reserveRateLimitedProxy(poolName, proxy, minIntervalMs) { if (!proxy) return false; const safeIntervalMs = Math.max(0, Math.floor(Number(minIntervalMs) || 0)); if (safeIntervalMs <= 0) { return true; } const proxyKey = buildProxyRateLimitKey(poolName, proxy); if (!proxyKey) { return false; } try { const reserved = await redis.set( proxyKey, String(Date.now()), "PX", safeIntervalMs, "NX", ); if (reserved === "OK") { return true; } return false; } catch { // Fallback local si Redis n'est pas disponible. } return reserveLocalRateLimitedProxy(proxyKey, safeIntervalMs); } setInterval(() => { const now = Date.now(); for (const [proxyKey, nextAllowedAt] of proxyRateLimitState) { if (!Number.isFinite(nextAllowedAt) || nextAllowedAt <= now) { proxyRateLimitState.delete(proxyKey); } } }, 30 * 1000).unref(); function reserveLocalProxyWindow(poolName, poolLength, batchSize) { const existingCursor = proxyRotationState.get(poolName); const startIndex = Number.isInteger(existingCursor) ? existingCursor % poolLength : Math.floor(Math.random() * poolLength); proxyRotationState.set(poolName, (startIndex + batchSize) % poolLength); return startIndex; } async function reserveProxyWindow(poolName, poolLength, batchSize = 1) { if (!Number.isFinite(poolLength) || poolLength <= 0) { return 0; } const safeBatchSize = Math.min( poolLength, Math.max(1, Math.floor(Number(batchSize) || 1)), ); try { const rotationKey = `${PROXY_ROTATION_REDIS_PREFIX}:${poolName}`; const nextValue = await redis.incrby(rotationKey, safeBatchSize); const startIndex = Number(nextValue) - safeBatchSize; return ((startIndex % poolLength) + poolLength) % poolLength; } catch { return reserveLocalProxyWindow(poolName, poolLength, safeBatchSize); } } async function pickNextProxyBatch(pool, count = 1, poolName = "default") { if (!Array.isArray(pool) || pool.length === 0) { return []; } const safeCount = Math.min( pool.length, Math.max(1, Math.floor(Number(count) || 1)), ); const startIndex = await reserveProxyWindow(poolName, pool.length, safeCount); const selected = []; for (let offset = 0; offset < safeCount; offset++) { selected.push(pool[(startIndex + offset) % pool.length]); } return selected; } async function pickNextRateLimitedProxy(pool, options = {}) { if (!Array.isArray(pool) || pool.length === 0) { return null; } const { poolName = "default", minIntervalMs = 0, waitTimeoutMs = 0, retryDelayMs = 25, } = options; const safeMinIntervalMs = Math.max(0, Math.floor(Number(minIntervalMs) || 0)); if (safeMinIntervalMs <= 0) { const [proxy] = await pickNextProxyBatch(pool, 1, poolName); return proxy || null; } const deadline = Date.now() + Math.max(0, Math.floor(Number(waitTimeoutMs) || 0)); while (true) { const startIndex = await reserveProxyWindow(poolName, pool.length, 1); for (let offset = 0; offset < pool.length; offset++) { const proxy = pool[(startIndex + offset) % pool.length]; const reserved = await reserveRateLimitedProxy( poolName, proxy, safeMinIntervalMs, ); if (reserved) { return proxy; } } if (Date.now() >= deadline) { return null; } const remainingMs = deadline - Date.now(); await sleep(Math.max(5, Math.min(retryDelayMs, remainingMs))); } } async function pickNextSocks5Proxy(options = {}) { const proxy = await pickNextRateLimitedProxy(PROXIES, { poolName: "SOCKS5_PROXIES", ...options, }); return proxy || null; } const splitCsvEnv = (envName, fallback = []) => { const rawValue = process.env[envName]; if (!rawValue) return [...fallback]; return rawValue .split(",") .map((value) => value.trim()) .filter(Boolean); }; const parsePositiveIntEnv = (envName, fallback) => { const rawValue = process.env[envName]; const parsed = Number.parseInt(rawValue || "", 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; }; function normalizeProxyType(type, fallback = "http") { const normalized = String(type || fallback) .toLowerCase() .replace(/:$/, "") .trim(); if ( normalized === "socks" || normalized === "socks5h" || normalized.startsWith("socks5") ) { return "socks5"; } if (normalized.startsWith("http")) { return "http"; } return fallback; } function updateArrayInPlace(target, nextValues) { target.splice(0, target.length, ...nextValues); return target; } function dedupeProxyEntries(entries = []) { const seen = new Set(); return entries.filter((entry) => { if (!entry || !entry.host || !entry.port) return false; const key = `${entry.type}:${entry.host}:${entry.port}:${entry.auth || ""}`; if (seen.has(key)) return false; seen.add(key); return true; }); } function sanitizeProxyEntry(entry, fallbackType = "http") { if (!entry || !entry.host || !entry.port) return null; const port = Number.parseInt(entry.port, 10); if (!Number.isFinite(port) || port <= 0) return null; return { host: String(entry.host).trim(), port, auth: entry.auth ? String(entry.auth).trim() : undefined, type: normalizeProxyType(entry.type, fallbackType), }; } function parseProxyString(value, defaultType = "http") { if (!value || typeof value !== "string") return null; const rawValue = value.trim(); if (!rawValue) return null; if (/^[a-z]+:\/\//i.test(rawValue)) { try { const parsedUrl = new URL(rawValue); return sanitizeProxyEntry( { host: parsedUrl.hostname, port: parsedUrl.port, auth: parsedUrl.username ? `${decodeURIComponent(parsedUrl.username)}${parsedUrl.password ? `:${decodeURIComponent(parsedUrl.password)}` : ""}` : undefined, type: parsedUrl.protocol.replace(":", ""), }, defaultType, ); } catch { return null; } } const authSplitIndex = rawValue.lastIndexOf("@"); let authPart = ""; let hostPortPart = rawValue; if (authSplitIndex !== -1) { authPart = rawValue.slice(0, authSplitIndex).trim(); hostPortPart = rawValue.slice(authSplitIndex + 1).trim(); } const segments = hostPortPart .split(":") .map((segment) => segment.trim()) .filter(Boolean); if (segments.length === 2) { return sanitizeProxyEntry( { host: segments[0], port: segments[1], auth: authPart || undefined, type: defaultType, }, defaultType, ); } if (!authPart && segments.length === 4) { const [a, b, c, d] = segments; if (/^\d+$/.test(b)) { return sanitizeProxyEntry( { host: a, port: b, auth: `${c}:${d}`, type: defaultType, }, defaultType, ); } if (/^\d+$/.test(d)) { return sanitizeProxyEntry( { host: c, port: d, auth: `${a}:${b}`, type: defaultType, }, defaultType, ); } } return null; } function parseProxyPayload(payload, defaultType = "http") { if (!payload) return []; if (Array.isArray(payload)) { return dedupeProxyEntries( payload .flatMap((item) => parseProxyPayload(item, defaultType)) .filter(Boolean), ); } if (typeof payload === "string") { const trimmed = payload.trim(); if (!trimmed) return []; if (trimmed.startsWith("[") || trimmed.startsWith("{")) { try { return parseProxyPayload(JSON.parse(trimmed), defaultType); } catch { // Keep parsing as raw text below. } } return dedupeProxyEntries( trimmed .split(/\r?\n/) .map((line) => parseProxyString(line, defaultType)) .filter(Boolean), ); } if (typeof payload === "object") { if (payload.host && payload.port) { return dedupeProxyEntries( [sanitizeProxyEntry(payload, defaultType)].filter(Boolean), ); } const candidateKeys = ["proxies", "data", "items", "results", "list"]; for (const key of candidateKeys) { if (payload[key] !== undefined) { const parsed = parseProxyPayload(payload[key], defaultType); if (parsed.length > 0) return parsed; } } const arrayValues = Object.values(payload).filter(Array.isArray); for (const value of arrayValues) { const parsed = parseProxyPayload(value, defaultType); if (parsed.length > 0) return parsed; } } return []; } function proxyToUrl(proxy, forcedType) { const safeProxy = sanitizeProxyEntry( proxy, forcedType || proxy?.type || "http", ); if (!safeProxy) return null; const type = normalizeProxyType(forcedType || safeProxy.type, "http"); const auth = safeProxy.auth ? `${safeProxy.auth}@` : ""; return `${type}://${auth}${safeProxy.host}:${safeProxy.port}`; } const PROXY_POOL_REDIS_KEY = "proxyscrape:mainapi:pools:v1"; const PROXYSCRAPE_API_TOKEN = ( process.env.PROXYSCRAPE_API_TOKEN || process.env.PROXYSCRAPE_API_KEY || "" ).trim(); const PROXYSCRAPE_ACCOUNT_ID = ( process.env.PROXYSCRAPE_ACCOUNT_ID || "" ).trim(); const PROXYSCRAPE_ENABLED = Boolean( PROXYSCRAPE_API_TOKEN && PROXYSCRAPE_ACCOUNT_ID, ); const PROXY_POOL_REDIS_TTL_SECONDS = 60 * 60; const PROXYSCRAPE_ACCOUNT_BASE_URL = "https://api.proxyscrape.com"; const PROXYSCRAPE_ACCOUNT_PATH = "/v4/account/{accountId}/datacenter_shared/proxy-list"; const PROXYSCRAPE_TIMEOUT_MS = 12000; const PROXYSCRAPE_REFRESH_MS = 15 * 60 * 1000; const PROXYSCRAPE_MAX_PER_PROTOCOL = 1000; const PROXYSCRAPE_HTTP_PROTOCOLS = ["http"]; const PROXYSCRAPE_SOCKS_PROTOCOLS = ["socks"]; const PROXYSCRAPE_ALLOW_LEGACY_FALLBACK = false; const PROXYSCRAPE_DEBUG = false; const legacySocksEnv = PROXYSCRAPE_ENABLED && !PROXYSCRAPE_ALLOW_LEGACY_FALLBACK ? [] : parseJsonArrayEnv("SOCKS5_PROXIES", []); const legacyHttpEnv = PROXYSCRAPE_ENABLED && !PROXYSCRAPE_ALLOW_LEGACY_FALLBACK ? [] : parseJsonArrayEnv("HTTP_PROXIES", []); const PROXIES = dedupeProxyEntries( (legacySocksEnv.length > 0 ? legacySocksEnv : parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5") ) .map((entry) => sanitizeProxyEntry(entry, "socks5")) .filter(Boolean), ); const COFLIX_SOCKS5_PROXIES = dedupeProxyEntries( parseProxyPayload(process.env.SOCKS5_PROXIES, "socks5") .map((entry) => sanitizeProxyEntry(entry, "socks5")) .filter(Boolean), ); const HTTP_PROXIES = dedupeProxyEntries( (legacyHttpEnv.length > 0 ? legacyHttpEnv : parseProxyPayload(process.env.HTTP_PROXIES, "http") ) .map((entry) => sanitizeProxyEntry(entry, "http")) .filter(Boolean), ); const DARKINO_HTTP_PROXIES = []; const DARKINO_PROXIES = []; function syncDerivedHttpProxyPools(httpEntries) { const normalizedHttpEntries = dedupeProxyEntries( httpEntries .map((entry) => sanitizeProxyEntry(entry, "http")) .filter(Boolean), ); updateArrayInPlace(HTTP_PROXIES, normalizedHttpEntries); updateArrayInPlace( DARKINO_PROXIES, normalizedHttpEntries.map((entry) => ({ ...entry, type: "http" })), ); updateArrayInPlace( DARKINO_HTTP_PROXIES, DARKINO_PROXIES.map((proxy) => proxyToUrl(proxy, "http")).filter(Boolean), ); } syncDerivedHttpProxyPools(HTTP_PROXIES); const proxyScrapeState = { refreshPromise: null, lastRefreshAt: 0, lastRefreshError: null, lastSuccessMeta: { socks: null, http: null, }, }; function getProxyPoolAgeMs(updatedAt = proxyScrapeState.lastRefreshAt) { const timestamp = Number(updatedAt); if (!Number.isFinite(timestamp) || timestamp <= 0) { return Number.POSITIVE_INFINITY; } return Date.now() - timestamp; } function isProxyPoolFresh(updatedAt = proxyScrapeState.lastRefreshAt) { return getProxyPoolAgeMs(updatedAt) < PROXYSCRAPE_REFRESH_MS; } function buildProxyScrapeResult(source) { return { enabled: true, socks: PROXIES.length, http: HTTP_PROXIES.length, meta: proxyScrapeState.lastSuccessMeta, source, }; } function limitProxyEntries(entries) { if ( !Number.isFinite(PROXYSCRAPE_MAX_PER_PROTOCOL) || PROXYSCRAPE_MAX_PER_PROTOCOL <= 0 ) { return entries; } return entries.slice(0, PROXYSCRAPE_MAX_PER_PROTOCOL); } function buildProxyScrapeCandidates(protocol) { if (!PROXYSCRAPE_ACCOUNT_ID) { return []; } const accountPath = PROXYSCRAPE_ACCOUNT_PATH.replace( "{accountId}", PROXYSCRAPE_ACCOUNT_ID, ); return [ { url: `${PROXYSCRAPE_ACCOUNT_BASE_URL}${accountPath}`, headers: { "api-token": PROXYSCRAPE_API_TOKEN }, params: { type: "getproxies", protocol, format: "normal", }, label: `v4 account ${accountPath}`, }, ]; } function applyStoredProxyPools(storedPools) { if (!storedPools || typeof storedPools !== "object") return false; const socks = dedupeProxyEntries( (storedPools.socks || []) .map((entry) => sanitizeProxyEntry(entry, "socks5")) .filter(Boolean), ); const httpEntries = dedupeProxyEntries( (storedPools.http || []) .map((entry) => sanitizeProxyEntry(entry, "http")) .filter(Boolean), ); if (socks.length === 0 || httpEntries.length === 0) { return false; } updateArrayInPlace( PROXIES, socks.map((entry) => ({ ...entry, type: "socks5" })), ); syncDerivedHttpProxyPools(httpEntries); proxyAgentCache.clear(); darkinoProxyAgentCache.clear(); cpasmalAgentCache.clear(); return true; } async function loadProxyPoolsFromRedis() { try { const rawValue = await redis.get(PROXY_POOL_REDIS_KEY); if (!rawValue) return false; const parsed = JSON.parse(rawValue); if (!applyStoredProxyPools(parsed)) return false; proxyScrapeState.lastRefreshAt = Number(parsed.updatedAt) || proxyScrapeState.lastRefreshAt; proxyScrapeState.lastSuccessMeta = parsed.meta || proxyScrapeState.lastSuccessMeta; return true; } catch { return false; } } async function saveProxyPoolsToRedis(payload) { try { await redis.set( PROXY_POOL_REDIS_KEY, JSON.stringify(payload), "EX", PROXY_POOL_REDIS_TTL_SECONDS, ); } catch { // ignore Redis persistence failures } } async function fetchProxyScrapeProtocol(defaultType, protocols) { const attemptErrors = []; for (const protocol of protocols) { const candidates = buildProxyScrapeCandidates(protocol); for (const candidate of candidates) { try { if (PROXYSCRAPE_DEBUG) { console.log( `[PROXYSCRAPE DEBUG] tentative ${candidate.label} | protocol=${protocol} | url=${candidate.url} | params=${JSON.stringify(candidate.params)}`, ); } const response = await axios.get(candidate.url, { headers: candidate.headers, params: candidate.params, timeout: PROXYSCRAPE_TIMEOUT_MS, responseType: "text", transformResponse: [(data) => data], }); const proxies = limitProxyEntries( dedupeProxyEntries(parseProxyPayload(response.data, defaultType)), ); if (proxies.length === 0) { const message = `[PROXYSCRAPE] ${candidate.label} (${protocol}) a retourne 0 proxy`; attemptErrors.push(message); if (PROXYSCRAPE_DEBUG) { console.warn( `${message} | body=${String(response.data || "").slice(0, 300)}`, ); } continue; } if (PROXYSCRAPE_DEBUG) { console.log( `[PROXYSCRAPE DEBUG] succes ${candidate.label} (${protocol}) -> ${proxies.length} proxies`, ); } return { proxies, meta: { protocol, endpoint: candidate.label, count: proxies.length, }, }; } catch (error) { const status = error.response?.status; const message = `[PROXYSCRAPE] ${candidate.label} (${protocol}) a echoue: ${status || error.code || error.message}`; attemptErrors.push(message); if (PROXYSCRAPE_DEBUG) { console.warn( `${message} | body=${String(error.response?.data || "").slice(0, 300)}`, ); } } } } throw new Error( attemptErrors.length > 0 ? attemptErrors.join(" | ") : "[PROXYSCRAPE] Aucun endpoint n a repondu", ); } async function refreshProxyScrapeProxies(options = {}) { const { force = false, silent = false } = options; if (!PROXYSCRAPE_ENABLED) { return { enabled: false, socks: PROXIES.length, http: HTTP_PROXIES.length, }; } if (!force && proxyScrapeState.refreshPromise) { return proxyScrapeState.refreshPromise; } if (proxyScrapeState.refreshPromise) { return proxyScrapeState.refreshPromise; } if (!force) { const loadedFromRedis = await loadProxyPoolsFromRedis(); if (loadedFromRedis && isProxyPoolFresh()) { if (!silent) { console.log( `[PROXYSCRAPE] Pools frais charges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`, ); } return buildProxyScrapeResult("redis-fresh"); } } proxyScrapeState.refreshPromise = (async () => { const lock = await acquireRedisLock(PROXY_POOL_REDIS_KEY, { ttl: Math.max(10, Math.ceil(PROXYSCRAPE_TIMEOUT_MS / 1000) * 2), retries: 40, retryDelay: 500, }); if (!lock) { const loadedFromRedis = await loadProxyPoolsFromRedis(); if (loadedFromRedis) { if (!silent) { console.log( `[PROXYSCRAPE] Pools charges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`, ); } return buildProxyScrapeResult("redis"); } } try { if (!force) { const loadedFromRedis = await loadProxyPoolsFromRedis(); if (loadedFromRedis && isProxyPoolFresh()) { if (!silent) { console.log( `[PROXYSCRAPE] Pools frais recharges depuis Redis: ${PROXIES.length} SOCKS5, ${HTTP_PROXIES.length} HTTP`, ); } return buildProxyScrapeResult("redis-fresh"); } } const socksResult = await fetchProxyScrapeProtocol( "socks5", PROXYSCRAPE_SOCKS_PROTOCOLS, ); const httpResult = await fetchProxyScrapeProtocol( "http", PROXYSCRAPE_HTTP_PROTOCOLS, ); updateArrayInPlace( PROXIES, socksResult.proxies.map((proxy) => ({ ...proxy, type: "socks5" })), ); syncDerivedHttpProxyPools(httpResult.proxies); proxyAgentCache.clear(); darkinoProxyAgentCache.clear(); cpasmalAgentCache.clear(); proxyScrapeState.lastRefreshAt = Date.now(); proxyScrapeState.lastRefreshError = null; proxyScrapeState.lastSuccessMeta = { socks: socksResult.meta, http: httpResult.meta, }; await saveProxyPoolsToRedis({ updatedAt: proxyScrapeState.lastRefreshAt, socks: PROXIES, http: HTTP_PROXIES, meta: proxyScrapeState.lastSuccessMeta, }); if (!silent) { console.log( `[PROXYSCRAPE] Refresh OK: ${PROXIES.length} SOCKS5 via ${socksResult.meta.endpoint}, ${HTTP_PROXIES.length} HTTP via ${httpResult.meta.endpoint}`, ); } return { enabled: true, socks: PROXIES.length, http: HTTP_PROXIES.length, meta: proxyScrapeState.lastSuccessMeta, source: "api", }; } catch (error) { proxyScrapeState.lastRefreshError = error.message; const loadedFromRedis = await loadProxyPoolsFromRedis(); if (loadedFromRedis) { if (!silent) { console.warn( `[PROXYSCRAPE] Refresh API echoue, fallback Redis utilise: ${error.message}`, ); } return buildProxyScrapeResult("redis-fallback"); } if (!silent) { console.error(`[PROXYSCRAPE] Refresh echoue: ${error.message}`); } throw error; } finally { if (lock && typeof lock.release === "function") { await lock.release(); } } })().finally(() => { proxyScrapeState.refreshPromise = null; }); return proxyScrapeState.refreshPromise; } function scheduleProxyScrapeRefresh() { if (!PROXYSCRAPE_ENABLED) return; setTimeout(() => { loadProxyPoolsFromRedis().catch(() => {}); }, 0).unref(); setInterval(() => { refreshProxyScrapeProxies({ silent: false }).catch(() => {}); }, PROXYSCRAPE_REFRESH_MS).unref(); } scheduleProxyScrapeRefresh(); // === WIFLIX FREE PROXY LIST (ProxyScrape v4 free) === const WIFLIX_FREE_PROXY_URL = "https://api.proxyscrape.com/v4/free-proxy-list/get?request=display_proxies&proxy_format=protocolipport&format=text"; const WIFLIX_FREE_PROXY_REFRESH_MS = 5 * 60 * 1000; // 5 minutes const WIFLIX_FREE_PROXY_MAX_ATTEMPTS = 10; // Try up to 10 proxies per request const WIFLIX_FREE_PROXY_TIMEOUT = 5000; // 5s per proxy attempt (fail fast) const wiflixFreeProxies = []; // HTTP-only proxy URL strings const wiflixProxyAgentCache = new Map(); const wiflixDeadProxies = new Set(); // Proxies who echoue — survit entre les requetes, reset au refresh async function fetchWiflixFreeProxies() { try { const res = await axios.get(WIFLIX_FREE_PROXY_URL, { timeout: 15000 }); const text = typeof res.data === "string" ? res.data : ""; const lines = text .split(/\r?\n/) .map((l) => l.trim()) .filter(Boolean); const parsed = []; for (const line of lines) { // Only keep http/https — socks4 free proxies are too unreliable if (/^https?:\/\/.+:\d+$/.test(line)) { parsed.push(line); } } // Replace list in-place + reset dead list wiflixFreeProxies.length = 0; wiflixFreeProxies.push(...parsed); wiflixProxyAgentCache.clear(); wiflixDeadProxies.clear(); console.log( `[WIFLIX FREE PROXY] Refresh OK: ${wiflixFreeProxies.length} http proxies (${lines.length - parsed.length} socks ignores)`, ); } catch (err) { console.error( `[WIFLIX FREE PROXY] Refresh echoue: ${err.message}`, ); } } function getWiflixFreeProxyAgent(proxyUrl) { if (wiflixProxyAgentCache.has(proxyUrl)) { return wiflixProxyAgentCache.get(proxyUrl); } const agents = { httpAgent: new HttpProxyAgent(proxyUrl), httpsAgent: new HttpsProxyAgent(proxyUrl), }; wiflixProxyAgentCache.set(proxyUrl, agents); return agents; } /** * Wiflix request — tries free HTTP proxies first, then falls back to Cloudflare Workers. */ async function makeWiflixRequest(targetUrl, options = {}) { const { timeout = 10000, headers = {}, decompress = true, method: requestMethod, data: requestData, ...otherOptions } = options; const method = (requestMethod || "GET").toUpperCase(); const defaultHeaders = { accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "accept-language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7", "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36", ...headers, }; // --- Phase 1: free proxy list (skip already-dead ones) --- const alive = wiflixFreeProxies.filter((p) => !wiflixDeadProxies.has(p)); if (alive.length > 0) { const shuffled = alive.sort(() => Math.random() - 0.5); const candidates = shuffled.slice(0, WIFLIX_FREE_PROXY_MAX_ATTEMPTS); for (let i = 0; i < candidates.length; i++) { const proxyUrl = candidates[i]; try { const agents = getWiflixFreeProxyAgent(proxyUrl); const response = await axios({ url: targetUrl, method, headers: defaultHeaders, timeout: WIFLIX_FREE_PROXY_TIMEOUT, decompress, ...agents, ...(requestData !== undefined ? { data: requestData } : {}), ...otherOptions, }); // Verify we got a real response (not a proxy error page) const body = typeof response.data === "string" ? response.data : ""; if (response.status === 200 && body.length > 100) { console.log( `[WIFLIX FREE PROXY] OK via ${proxyUrl} (tentative ${i + 1}/${candidates.length})`, ); return response; } // Bad response — mark dead wiflixDeadProxies.add(proxyUrl); } catch { wiflixDeadProxies.add(proxyUrl); wiflixProxyAgentCache.delete(proxyUrl); } } console.log( `[WIFLIX FREE PROXY] ${candidates.length} echoues, ${alive.length - candidates.length} restants, fallback CF Workers`, ); } // --- Phase 2: fallback to Cloudflare Workers --- return makeRequestWithCorsFallback(targetUrl, options); } // Initial fetch + 5-min refresh fetchWiflixFreeProxies(); setInterval(() => { fetchWiflixFreeProxies(); }, WIFLIX_FREE_PROXY_REFRESH_MS).unref(); // Fonction utilitaire pour choisir aléatoirement un proxy (toujours utiliser un proxy) function pickRandomProxyOrNone() { // Sélectionner toujours un proxy aléatoire parmi la liste if (!PROXIES || PROXIES.length === 0) return null; const idx = Math.floor(Math.random() * PROXIES.length); return PROXIES[idx] || null; } // Sélectionne un proxy aléatoire — retourne null si la liste est vide function pickRandomProxy() { if (!PROXIES || PROXIES.length === 0) return null; const idx = Math.floor(Math.random() * PROXIES.length); return PROXIES[idx]; } // Fonction utilitaire pour cr\u00e9er un agent proxy SOCKS5 (avec cache) function getProxyAgent(proxy) { if (!proxy) return null; const auth = proxy.auth ? `${proxy.auth}@` : ""; const proxyType = normalizeProxyType(proxy.type, "socks5"); const cacheKey = `${proxyType}:${proxy.host}:${proxy.port}:${auth}`; // V\u00e9rifier le cache d'abord if (proxyAgentCache.has(cacheKey)) { return proxyAgentCache.get(cacheKey); } const protocol = proxyType === "socks5" ? "socks5h" : proxyType; const proxyUrl = `${protocol}://${auth}${proxy.host}:${proxy.port}`; const agent = new SocksProxyAgent(proxyUrl); // Mettre en cache l'agent proxyAgentCache.set(cacheKey, agent); return agent; } // Fonction utilitaire pour cr\u00e9er un agent proxy (pour Darkino) - avec cache function getDarkinoHttpProxyAgent(proxy) { if (!proxy) return null; const auth = proxy.auth ? `${proxy.auth}@` : ""; const proxyType = normalizeProxyType(proxy.type, "http"); const cacheKey = `${proxyType}:${proxy.host}:${proxy.port}:${auth}`; // V\u00e9rifier le cache d'abord if (darkinoProxyAgentCache.has(cacheKey)) { return darkinoProxyAgentCache.get(cacheKey); } let agents; if (proxyType === "socks5") { const proxyUrl = `socks5://${auth}${proxy.host}:${proxy.port}`; const agent = new SocksProxyAgent(proxyUrl); agents = { httpAgent: agent, httpsAgent: agent, }; } else { const proxyUrl = `http://${auth}${proxy.host}:${proxy.port}`; agents = { httpAgent: new HttpProxyAgent(proxyUrl), httpsAgent: new HttpsProxyAgent(proxyUrl), }; } // Mettre en cache les agents darkinoProxyAgentCache.set(cacheKey, agents); return agents; } module.exports = { // Proxy agent caches proxyAgentCache, darkinoProxyAgentCache, // Proxy configuration flags ENABLE_DARKINO_PROXY, ENABLE_COFLIX_PROXY, ENABLE_FRENCH_STREAM_PROXY, ENABLE_LECTEURVIDEO_PROXY, ENABLE_FSTREAM_PROXY, ENABLE_ANIME_PROXY, ENABLE_WIFLIX_PROXY, darkiworld_premium, // Darkino 403 cooldown (expose getter/setter since it's mutable) DARKINO_403_COOLDOWN_MS, get darkino403CooldownUntil() { return darkino403CooldownUntil; }, set darkino403CooldownUntil(val) { darkino403CooldownUntil = val; }, // Cpasmal CPASMAL_BASE_URL, cpasmalJar, getCpasmalAgent, axiosCpasmalRequest, // Cloudflare Workers proxies CLOUDFLARE_WORKERS_PROXIES, proxyErrorCache, markProxyAsErrored, isProxyInCooldown, getAvailableProxies, markProxyAsHealthy, buildProxiedUrl, // Request helpers makeRequestWithCorsFallback, makeWiflixRequest, makeCoflixRequest, makeLecteurVideoRequest, makeAnimeSamaRequest, // SOCKS5 proxies PROXIES, HTTP_PROXIES, DARKINO_HTTP_PROXIES, DARKINO_PROXIES, refreshProxyScrapeProxies, // Agent helpers pickRandomProxyOrNone, pickRandomProxy, pickNextSocks5Proxy, getProxyAgent, getDarkinoHttpProxyAgent, // Cleanup shutdownCycleTLS, };