MovixOpenSource/API/Mainapi/utils/axiosHelpers.js
Movix 8657a856f8 feat: live TV FCTV, fournisseur OAuth, sources J1F/Cinestream, modération IA & durcissement
feat: live TV FCTV, fournisseur OAuth, sources J1F/Cinestream, modération IA & durcissement

Gros lot de travail synchronisé depuis le repo de dev. Les grands axes :
migration de domaine (movix.date + miroirs), deux nouvelles sources films/séries
(1jour1film, Cinestream) et une n ouvelle source livetv, un fournisseur OAuth « Se connecter avec Movix », une
modération de contenu par IA revue, plus un lot de durcissement mémoire/réseau,
sécurité et perf côté API et front.

## 🌐 Domaines & miroirs

- Domaine canonique `movix.chat`/`movix.golf`/`movix.tax` → **movix.date** partout
  (front, allow-lists CORS/domaine, Discord RPC). Miroir `movix.health` →
  **movix.online** (Footer, Header, pages d'aide CaMarchePlus/Dns/Miroirs). Lien
  Telegram du footer → `mysticsaba.com`. ~10 clés i18n mises à jour.

## 🎬 Sources de streaming (VOD)

- **J1F (1jour1film)** — nouvelle source de bout en bout (`routes/j1f.js`, monté sur
  `/api/j1f`). Résout le domaine tournant via une page `/go/`, décode les blobs base64
  `J1F_SRV`/`j1fEpsData` (Dooplay/WP), ne garde que les players uniques, ignore les
  agrégateurs (frembed/vidsrc/videasy). Cache stale-while-revalidate dédié. Players 1J1F
  lus via l'extension/userscript.
- **Cinestream** — nouvelle source films (`routes/cinestream.js`, scrape Next.js
  `cinestream.info`, match par id TMDB autoritaire). Wiflix bascule ses **films** sur
  Cinestream (bot-shield flemmix pénible sur les films) ; la **TV** reste sur flemmix.
- **Wiflix** — `flemmix.farm` → **`flemmix.golf`** ; handshake cookie
  (`makeWiflixSearchRequest`, `h_check=25`) ; sélecteurs de template corrigés.
- **Cpasmal** — axios+proxy → `makeCpasmalRequest` (CycleTLS JA3) car Cloudflare 403 ;
  recherche GET → POST form (le WAF bloque le GET).
- **Fstream** — cookie anti-bot `fsschal` ; source épisodes JS statique
  (`/static/series/<id>.js`) avant l'AJAX `episodes_p.php` ; fallback `get_seasons.php`.
- **FranceTV** — `fetchSeasonEpisodesViaDeepPage` (pagination JSON « Afficher plus »).
- **Coflix** — `coflix.date` → `coflix.trade` ; logs 429 silencés en double.
- **proxyManager** — cinq helpers de contournement Cloudflare exportés
  (`makeCinestreamRequest`, `make1j1fRequest`, `makeCpasmalRequest`, handshake Wiflix
  `makeWiflixSearchRequest`/`wiflixHandshake`, `classifyCloudflare429` worker-vs-site) ;
  CycleTLS + rotation ProxyScrape, ne throw jamais sur statut HTTP. Coflix/LecteurVideo
  quittent les Cloudflare Workers pour ProxyScrape.

## 📺 Live TV — remplacement de Sosplay/Vavoo par FCTV33

- Source sport **Sosplay → FCTV33** (API RBTV, protobuf brut) réécrite de bout en bout
  dans `liveTvRoutes.js`, **Vavoo retiré** (manifest/catalogue/stream) — tout passe par
  `/api/livetv/*`.
- Pipeline neuf : décodeur protobuf maison, signature `sfver` (bs keys + MD5),
  auto-découverte des domaines API/player tournants (`hubu.ru/fctvlink` + Nuxt SSR /
  `params` rot47, cache 30 min), scraper de matchs (`scrapeFctvMatches` :
  équipes/logos, ligue, pays, statut live, tri live-first), résolution de flux (HLS
  natif d'abord, embed en secours), token CDN (XOR keystream + base64, gate
  `rb-session`).
- Route `GET /api/livetv/fctv/playlist` : proxy m3u8 intelligent (résout le token,
  réécrit les segments, retry sur token périmé, modes `raw` extension / `proxy` VIP).
- **Matchs plus réservés aux VIP** (seul `iptv_` le reste) : flux natif proxifié pour
  les VIP, stub `_fctvLocal` résolu côté client par
  l'extension/userscript (`RESOLVE_FCTV`), fallback embed sinon.
- Front `LiveTV.tsx` : `matches` → `fctv`, UI accordéon par match (logos, badges
  ligue/pays, score/timer live, favori), disclaimer, fix icône `music`. Rotations
  d'hôtes : `witv.team`→`witv.football`, `livetv876.me`→`livetv901.me`.

## 🧩 Extensions · Userscript · Discord RPC

- **FCTV en lecture native** : handlers `SETUP_FCTV_HEADERS`/`RESOLVE_FCTV` (règle DNR
  Referer/Origin/UA sur `/cfall/s*` + résolveur local IP-bindé : protobuf-lite + rot47
  + token XOR).
- **Vavoo retiré** , `cinepulse.lol` → `purstream.mx`.
- Fix extracteur **Sibnet** (`Range: bytes=0-0`) ; fix bug userscript `matchToRegex`
  (patterns ancrés `^…$` cassant `/cfall/s` → dé-ancrés) ; override dev
  `localhost:25565`.
- Bumps de version (Chrome 1.3.3→1.3.8, Firefox 1.5.1→1.5.5, userscript 1.4.0→1.4.7) ;
  PreMid nouveau `clientId` Discord + `regExp` élargi (1.0.3→1.0.4).

## 🔑 OAuth (fournisseur « Se connecter avec Movix ») & codes d'accès

- Refonte du flux d'autorisation OAuth (`OAuthAuthorizePage.tsx`) + routes
  (`routes/authRoutes.js`, `routes/oauth.js`) et stockage des clients OAuth étoffé
  (`utils/oauthClientsDb.js`, `routes/adminOauthApps.js` pour la gestion admin des apps).
- `AccessCodeForm.tsx` retouché en cohérence avec le nouveau flux ; `checkVip.js` ajusté.
- Session centralisée : `accountAuth.ts` expose `clearStoredAuthSession()` /
  `broadcastAuthChange()` (liste `AUTH_KEYS` unifiée), désormais utilisés partout à la
  déconnexion.

## 💬 Modération de contenu (IA)

- Modèle OpenRouter `google/gemini-2.5-flash-lite` → `deepseek/deepseek-v4-flash`
  (`commentsRoutes.js`, `sharedListsRoutes.js`) ; fonctions/logs renommés
  `moderateContentWithGemini` → `moderateContentWithAI` (découplés du nom du modèle).
- Prompt durci (critères INSULTES / DEMANDE_AJOUT resserrés) pour éviter les faux
  positifs sur questions, signalements de bug et plaintes de contenu manquant.
- `AdminComments.tsx` : libellé « par Gemini » → clé i18n générique.

## 🧊 Hydracker / Darkiworld

- **Sortie du debrid** : AllDebrid/RealDebrid supprimés (`utils/hydrackerLive.js` :
  branche provider retirée ; vars `ALLDEBRID_*`/`REALDEBRID_*` sorties du `.env`) —
  hydracker renvoie directement `rawUrl`/`directDL`. Liens servis depuis les snapshots
  SQLite locaux (`utils/darkiworldSqlite.js`, `darkino-backups/`).
- **Anti-scraping** sur `/decode/:id` (`routes/darkiworld.js`) : watchlist + alerte
  webhook Discord (`DISCORD_SCRAPER_WEBHOOK`) et empoisonnement/blocage de réponse pour
  les IP listées (`SCRAPER_BLOCKED_IPS`).
- **Mémoire** : cache d'agents proxy borné (`utils/lruMap.js`) qui détruit les sockets
  des agents évincés.

## 🛠️ Admin — propriété des liens & équipe

- **Propriété des liens** : les liens streaming gagnent l'attribution `added_by` et
  l'ownership est appliquée des deux côtés (uploader = ses liens, admin = tous). GET
  public masqué (`stripLinkOwner`), `GET /admin/streaming-links/:type/:id` authentifié
  conserve le tag ; `DELETE /admin/links` par URL unique ; `PUT /admin/links` masse
  admin-only.
- **Équipe** : `GET/POST/DELETE /admin/admins` + `GET /admin/team/history` (audit),
  identité via `adminIdentity.js` (option `preferProfile` → premier profil Movix). UI
  `AdminTeam.tsx`.
- **Privilèges** (`middleware/auth.js`) : `isAdmin` rejette `role !== 'admin'` (403) —
  les uploaders ne passent plus les routes admin-only. Migration
  `add_role_to_admins.sql`.

## 🎁 Wrapped v2

- Cartes de partage : logique canvas extraite (`utils/wrappedCanvas.ts`,
  `utils/wrappedShareCards.ts`), formats de partage supplémentaires + preview.
- Nouvelles slides : jour record, répartition par jour de semaine, champion de rewatch,
  âge ciné (année de sortie médiane pondérée par le temps de visionnage).
- Perf : données depuis le payload backend (zéro re-fetch TMDB), trailer lazy,
  percentile non bloquant (réchauffe en arrière-plan, lock Redis `NX`), shuffle seedé.

## 🔐 Auth · Profils · VIP

- **Pseudo forcé** : `validateProfileName` (1–32 chars, strip control/zero-width, +
  `maxLength={32}` côté UI Profile/WatchParty) ; les pseudos OAuth trop longs →
  `requiresUsernameChange` + modale bloquante `RequireUsernameChange.tsx`.
- **Sync durci** (`ProfileContext.tsx`, `syncPolicy.js`) : anti-replay cross-user,
  filtrage des ops à clés non-syncables (`access_code`/`access_code_expires` exclues),
  « wipe guard » qui préserve le local sur profil vide inattendu, dédupe des
  `loadProfiles()` concurrents. Logout centralisé (voir OAuth).
- **VIP** : `is_vip` préservé au chargement de profil (corrige un downgrade VIP
  inter-onglets qui relançait la popup pub) ; `vipDonations.js` poll de secours
  `payment-status.php` (via `ipn_token`, colonne `paygate_ipn_token`) → plus d'invoices
  bloquées.

## ▶️ Lecteur (HLS)

- Basse latence opt-in : `isLowLatencyEnabled('movies')` (scope movies/livetv,
  `lowLatencyPref.ts`) au lieu du mode forcé ; `FloatingPlayer` aligné.
- Sous-titres cinep : wrapper m3u8 synthétique pour les URI `.vtt`/`.srt` directes,
  `isSubtitleLoadError` (une piste cassée ne tue plus la vidéo), rendu `<i>/<b>/<u>`.
- Menu sources regroupé par langue (VF/VFQ/VFF/VOSTFR).

## 📡 Cast (AirPlay / Chromecast)

- Retrait du contournement proxy darkibox (`buildApiProxyUrl`) → cast direct de l'URL ;
  `runtime.ts` supprime `API_PROXY_BASE_URL`/`buildApiProxyUrl` devenus morts.
- Paramètre `streamType` (`BUFFERED` par défaut) propagé jusqu'à
  `loadMediaOnCastWithFallback`.

## 📢 Pubs

- Modes de popup (`adPopupMode.ts`) : `normal`, `auto`, `click-anywhere` ; toggle « +18 »
  séparé (`adAdultMode.ts`). `SCRIPT_AD_MODE_ENABLED` → `false`, URL du script changée.

## 🔎 Recherche & priorité des sources

- `searchUtils.ts` : le terme original contourne le filtre « trop générique » (fix
  zéro-résultat sur titres courts : Baki, Gantz).
- `animeSignals.ts` : détection donghua (CN/TW/HK ou `zh` + Animation, hors C-drama).
- `sourceAutoSelect.ts` : `VFF`/`VFQ` normalisés en `vf` (sinon reléguées derrière
  VOSTFR). `sourcePriorityPrefs.ts` : schéma v2 → v3 (nouvel ordre par défaut + `j1f`,
  migration non destructive des ordres personnalisés). `extractionPrefs.ts` : +daddylive.

## 🔒 Sécurité

- Sanitisation d'URL avant rendu en `href` : `CommentsSection.tsx` (liens markdown
  `http(s)://`) et `Greenlight/AdminLinkSubmissions.tsx` — bloque les `javascript:`.

## ⚙️ Infra & perf

- **server.js** : recyclage périodique des workers (12h) + `[memstats]` + hook
  `SIGUSR2` → heap-snapshot.
- **app.js** : monte `/api/j1f`, retire la route statique `/oauth-icons`, passe
  `makeCpasmalRequest` au router Cpasmal, `coflix.date` → `coflix.trade`.
- **lazyWithRetry.ts / main.tsx / ErrorBoundary.tsx** : récupération chunk-load réécrite
  (retry + reload budgété), garde `removeChild/insertBefore`, écran soft « Mise à jour »
  au lieu du crash-report.
- **UI** : `TopProgressBar` écrit `transform` directement sur un ref (compositor-only,
  plus de re-render) ; `blur-text` relâche `will-change` après l'animation.
- **proxiesembed/server.py** : `curl_cffi` (JA3) pour `cinep-proxy` + retry SOCKS5 sur
  403 ; lookup clé VIP réaligné sur `checkVip.js` ; log WARNING par défaut ; SOCKS5
  forcé IPv4 ; wrapper VTT/SRT pour hls.js ; re-label `application/zip` → `video/mp2t` ;
  `cinepulse.lol` → `purstream.mx`.

## 🧹 Nettoyage & suppressions

- **Front** : carte promo « Loadix » sur Download (remplace l'encart beta),
  « 1jour1film » ajouté à « C'est quoi Movix », fallback Darkino/Nightflix gutté en
  no-op (`api.movix.chat` décommissionné), classes CSS mortes retirées, fix
  `fetchPriority` → `fetchpriority`.
- **Backend** : scrapers/utilitaires morts retirés — `routes/dragiv.js`,
  `routes/topstream.js`, `routes/proxy.js`, `utils/cloudflareSession.js`,
  `utils/hydrackerBatch.js`, `utils/hydrackerQueue.js'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 18:22:13 +02:00

1060 lines
36 KiB
JavaScript

/**
* High-level axios request helpers for each streaming site.
* Extracted from server.js — wraps proxy logic with site-specific headers and cookies.
*
* Some helpers depend on axios instances / session state that live in route modules
* (e.g. axiosFStream, axiosAnimeSama, axiosCoflix, cookieJar).
* Call `configure(deps)` once at startup to inject those dependencies.
*/
const axios = require('axios');
const fsp = require('fs').promises;
const path = require('path');
const writeFileAtomic = require('write-file-atomic');
const {
ENABLE_DARKINO_PROXY,
ENABLE_COFLIX_PROXY,
ENABLE_FRENCH_STREAM_PROXY,
ENABLE_LECTEURVIDEO_PROXY,
ENABLE_FSTREAM_PROXY,
ENABLE_ANIME_PROXY,
ENABLE_WIFLIX_PROXY,
DARKINO_PROXIES,
PROXIES,
HTTP_PROXIES,
CLOUDFLARE_WORKERS_PROXIES,
getProxyAgent,
getDarkinoHttpProxyAgent,
getAvailableProxies,
markProxyAsErrored,
markProxyAsHealthy,
buildProxiedUrl,
makeRequestWithCorsFallback,
makeCoflixRequest,
makeLecteurVideoRequest,
makeWiflixRequest,
makeAnimeSamaRequest,
} = require('./proxyManager');
// Lazy-bound dependencies injected via configure()
let deps = {
darkiHeaders: {},
coflixHeaders: {},
cookieJar: null,
axiosCoflix: null,
axiosAnimeSama: null,
axiosFStream: null,
COFLIX_BASE_URL: '',
ANIME_SAMA_URL: '',
FSTREAM_BASE_URL: '',
ensureFStreamSession: async () => {},
fstreamCookies: {},
fstreamRequestCounter: 0,
getFstreamRequestCounter: () => 0,
incrementFstreamRequestCounter: () => {}
};
const WIFLIX_BASE_URL = 'https://flemmix.golf';
const truncateForLog = (value, maxLength = 300) => {
if (typeof value !== 'string') return value;
return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
};
const summarizeRequestErrorForLog = (error) => {
const responseBody = error?.response?.data;
return {
message: error?.message,
code: error?.code,
status: error?.response?.status,
statusText: error?.response?.statusText,
responseSnippet: typeof responseBody === 'string'
? truncateForLog(responseBody, 500)
: responseBody && typeof responseBody === 'object'
? truncateForLog(JSON.stringify(responseBody), 500)
: undefined
};
};
const DARKIWORLD_BASE_URL = String(process.env.DARKIWORLD_BASE_URL || 'https://darkiworld2026.com')
.trim()
.replace(/\/+$/, '');
/**
* Inject site-specific dependencies that live outside this module.
* Call once at server startup after creating axios instances.
*
* @param {object} injected - Map of dependency names to values
*/
function configure(injected) {
Object.assign(deps, injected);
}
// === Proxy manager handle for Redis-backed Darkino cooldowns ===
const proxyManager = require('./proxyManager');
// Fonction utilitaire pour g\u00e9n\u00e9rer le referer dynamiquement
function generateDarkiReferer(url) {
const baseUrl = DARKIWORLD_BASE_URL;
if (url.includes('/season/') && url.includes('/episode/') && url.includes('/download')) {
// Pour les \u00e9pisodes de s\u00e9ries: /titles/{titleId}/season/{seasonId}/episode/{episodeId}/download
const match = url.match(/\/titles\/(\d+)\/season\/(\d+)\/episode\/(\d+)\/download/);
if (match) {
const [, titleId, seasonId, episodeId] = match;
return `${baseUrl}/titles/${titleId}/season/${seasonId}/episode/${episodeId}/download`;
}
} else if (url.includes('/titles/') && url.includes('/content/liens')) {
// Nouvel endpoint download : /api/v1/titles/{titleId}/content/liens
// Le navigateur envoie referer = /titles/{titleId}/download, sinon
// l'API retourne 401 m\u00eame avec session valide.
const match = url.match(/\/titles\/(\d+)\/content\/liens/);
if (match) {
const [, titleId] = match;
return `${baseUrl}/titles/${titleId}/download`;
}
} else if (url.includes('/titles/') && url.includes('/download')) {
// Pour les films: /titles/{id}/download
const match = url.match(/\/titles\/(\d+)\/download/);
if (match) {
const [, titleId] = match;
return `${baseUrl}/titles/${titleId}/download`;
}
} else if (url.includes('/search/')) {
// Pour la recherche
return `${baseUrl}/search`;
}
// Par d\u00e9faut, utiliser l'URL de base
return baseUrl;
}
function splitCookieHeader(headerValue) {
if (typeof headerValue !== 'string' || headerValue.trim() === '') {
return [];
}
return headerValue
.split(';')
.map(part => part.trim())
.filter(Boolean)
.map(part => {
const separatorIndex = part.indexOf('=');
if (separatorIndex <= 0) {
return null;
}
return [
part.slice(0, separatorIndex).trim(),
part.slice(separatorIndex + 1).trim()
];
})
.filter(Boolean);
}
function mergeCookieHeaders(...headerValues) {
const cookieMap = new Map();
for (const headerValue of headerValues) {
for (const [name, value] of splitCookieHeader(headerValue)) {
cookieMap.set(name, value);
}
}
return [...cookieMap.entries()]
.map(([name, value]) => `${name}=${value}`)
.join('; ');
}
function decodeCookieValue(value) {
if (typeof value !== 'string' || value === '') {
return '';
}
try {
return decodeURIComponent(value);
} catch (_) {
return value;
}
}
async function getDarkinoJarState() {
if (!deps.cookieJar) {
return {
cookieHeader: '',
xsrfToken: ''
};
}
try {
const cookies = await deps.cookieJar.getCookies(DARKIWORLD_BASE_URL);
const xsrfCookie = cookies.find(cookie => cookie.key.toUpperCase() === 'XSRF-TOKEN');
return {
cookieHeader: cookies.map(cookie => cookie.cookieString()).join('; '),
xsrfToken: xsrfCookie ? decodeCookieValue(xsrfCookie.value) : ''
};
} catch (error) {
console.warn(`[DARKINO REQUEST] Impossible de lire le cookieJar: ${error.message}`);
return {
cookieHeader: '',
xsrfToken: ''
};
}
}
async function buildDarkinoRequestHeaders(config) {
const dynamicReferer = generateDarkiReferer(config.url || '/');
const jarState = await getDarkinoJarState();
const requestHeaders = {
...deps.darkiHeaders,
referer: dynamicReferer,
...config.headers,
};
const configuredCookieHeader = requestHeaders.cookie || requestHeaders.Cookie || '';
// IMPORTANT: env cookies (DARKIWORLD_COOKIES) doivent gagner contre le
// cookieJar. Sans ça, la réponse du GET `/` (refresh session) émet un
// `set-cookie` avec une session anonyme qui écrase `darkiworld_session`
// dans le jar → toutes les requêtes suivantes vers les routes auth (ex.
// /api/v1/titles/{id}/content/liens) reçoivent 401 Unauthenticated.
// L'ordre des arguments compte : le DERNIER argument gagne dans le merge.
const mergedCookieHeader = mergeCookieHeaders(jarState.cookieHeader, configuredCookieHeader);
if (mergedCookieHeader) {
requestHeaders.cookie = mergedCookieHeader;
}
delete requestHeaders.Cookie;
// x-xsrf-token : env gagne aussi. On ne fallback sur le jar que si l'env
// ne fournit pas de token (sinon on enverrait un header qui ne matche
// pas le cookie XSRF-TOKEN env qu'on vient de privilégier).
if (jarState.xsrfToken && !requestHeaders['x-xsrf-token']) {
requestHeaders['x-xsrf-token'] = jarState.xsrfToken;
}
return requestHeaders;
}
// Fonction utilitaire pour requ\u00eates Darkino avec proxies (SOCKS5h)
async function axiosDarkinoRequest(config) {
// V\u00e9rifier si on est en cooldown apr\u00e8s une erreur 403 (cluster-wide via Redis)
const cd403Ms = await proxyManager.getDarkinoCooldownRemainingMs('403');
if (cd403Ms > 0) {
const remainingMin = Math.ceil(cd403Ms / 60000);
const error = new Error(`Darkino en cooldown (403 Cloudflare). R\u00e9essayez dans ${remainingMin} minute(s).`);
error.isDarkinoCooldown = true;
error.response = { status: 403 };
throw error;
}
// V\u00e9rifier si on est en cooldown apr\u00e8s une erreur 429 (rate limit / Cloudflare Workers daily limit)
const cd429Ms = await proxyManager.getDarkinoCooldownRemainingMs('429');
if (cd429Ms > 0) {
const remainingMin = Math.ceil(cd429Ms / 60000);
const error = new Error(`Darkino en cooldown (rate limit 429). R\u00e9essayez dans ${remainingMin} minute(s).`);
error.isDarkinoCooldown = true;
error.response = { status: 429 };
throw error;
}
// V\u00e9rifier si on est en cooldown apr\u00e8s une erreur serveur (5xx)
const cd5xxMs = await proxyManager.getDarkinoCooldownRemainingMs('5xx');
if (cd5xxMs > 0) {
const remainingMin = Math.ceil(cd5xxMs / 60000);
const error = new Error(`Darkino en cooldown (erreur serveur). R\u00e9essayez dans ${remainingMin} minute(s).`);
error.isDarkinoCooldown = true;
error.response = { status: 503 };
throw error;
}
// Cooldown apres trop de timeouts / erreurs reseau consecutifs (cluster-wide).
// Les cooldowns 403/429/5xx ne se declenchent que sur une reponse HTTP : un
// timeout (ECONNABORTED) n'a pas de error.response donc rien ne s'armait,
// l'upstream mort etait martele en boucle (RSS qui monte sans redescendre).
const cdNeterrMs = await proxyManager.getDarkinoCooldownRemainingMs('neterr');
if (cdNeterrMs > 0) {
const remainingMin = Math.ceil(cdNeterrMs / 60000);
const error = new Error(`Darkino en cooldown (trop de timeouts). Réessayez dans ${remainingMin} minute(s).`);
error.isDarkinoCooldown = true;
error.response = { status: 504 };
throw error;
}
const requestUrl = `${DARKIWORLD_BASE_URL}${config.url}`;
const darkinoRequestHeaders = await buildDarkinoRequestHeaders(config);
const darkinoAuthSnapshot = {
referer: darkinoRequestHeaders.referer,
xsrfToken: truncateForLog(darkinoRequestHeaders['x-xsrf-token'] || '', 200),
cookie: truncateForLog(darkinoRequestHeaders.cookie || '', 500),
cookieLength: (darkinoRequestHeaders.cookie || '').length,
};
if (!ENABLE_DARKINO_PROXY) {
// Si le proxy est d\u00e9sactiv\u00e9, faire la requ\u00eate directe
try {
const response = await axios({
...config,
url: requestUrl,
headers: darkinoRequestHeaders,
timeout: 5000,
withCredentials: false,
decompress: true
});
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader && deps.cookieJar) {
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
return response;
} catch (error) {
console.error(`[DARKINO REQUEST][DIRECT ERROR] ${String(config.method || 'get').toUpperCase()} ${config.url}`, {
...summarizeRequestErrorForLog(error),
auth: darkinoAuthSnapshot,
});
if (error.response && error.response.headers['set-cookie'] && deps.cookieJar) {
const setCookieHeader = error.response.headers['set-cookie'];
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
// En cas d'erreur 403, 429 ou 5xx, activer le cooldown (cluster-wide)
const directStatus = error.response?.status;
if (directStatus === 403) {
await proxyManager.armDarkinoCooldown('403');
console.log(`[DARKINO] Erreur 403 d\u00e9tect\u00e9e (direct) - Cooldown activ\u00e9`);
} else if (directStatus === 429) {
await proxyManager.armDarkinoCooldown('429');
console.log(`[DARKINO] Erreur 429 d\u00e9tect\u00e9e (direct) - Cooldown activ\u00e9`);
} else if (directStatus >= 500 && directStatus < 600) {
await proxyManager.armDarkinoCooldown('5xx');
console.log(`[DARKINO] Erreur ${directStatus} d\u00e9tect\u00e9e (direct) - Cooldown activ\u00e9`);
}
throw error;
}
}
// Utiliser les proxies HTTP sp\u00e9cifiques de Darkino avec rotation al\u00e9atoire
const darkinoProxies = [...DARKINO_PROXIES]; // Copie pour pouvoir m\u00e9langer
// M\u00e9langer al\u00e9atoirement les proxies
for (let i = darkinoProxies.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[darkinoProxies[i], darkinoProxies[j]] = [darkinoProxies[j], darkinoProxies[i]];
}
if (darkinoProxies.length === 0) {
try {
const response = await axios({
...config,
url: requestUrl,
headers: darkinoRequestHeaders,
timeout: 5000,
withCredentials: false,
decompress: true
});
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader && deps.cookieJar) {
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
return response;
} catch (error) {
console.error(`[DARKINO REQUEST][DIRECT NO PROXY ERROR] ${String(config.method || 'get').toUpperCase()} ${config.url}`, {
...summarizeRequestErrorForLog(error),
auth: darkinoAuthSnapshot,
});
if (error.response && error.response.headers['set-cookie'] && deps.cookieJar) {
const setCookieHeader = error.response.headers['set-cookie'];
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
const noProxyStatus = error.response?.status;
if (noProxyStatus === 403) {
await proxyManager.armDarkinoCooldown('403');
console.log(`[DARKINO] Erreur 403 d\u00e9tect\u00e9e (direct sans proxy) - Cooldown activ\u00e9`);
} else if (noProxyStatus === 429) {
await proxyManager.armDarkinoCooldown('429');
console.log(`[DARKINO] Erreur 429 d\u00e9tect\u00e9e (direct sans proxy) - Cooldown activ\u00e9`);
} else if (noProxyStatus >= 500 && noProxyStatus < 600) {
await proxyManager.armDarkinoCooldown('5xx');
console.log(`[DARKINO] Erreur ${noProxyStatus} d\u00e9tect\u00e9e (direct sans proxy) - Cooldown activ\u00e9`);
}
throw error;
}
}
let lastError = null;
const maxRetries = 1; // Une seule tentative
for (let i = 0; i < maxRetries; i++) {
const proxy = darkinoProxies[i];
const agents = getDarkinoHttpProxyAgent(proxy);
try {
const response = await axios({
...config,
method: config.method || 'get',
url: requestUrl,
headers: darkinoRequestHeaders,
timeout: 5000,
withCredentials: false,
decompress: true,
httpAgent: agents.httpAgent,
httpsAgent: agents.httpsAgent,
proxy: false
});
// Mettre \u00e0 jour le cookieJar avec la r\u00e9ponse
const setCookieHeader = response.headers['set-cookie'];
if (setCookieHeader && deps.cookieJar) {
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
// Requete reussie : on n'est plus en serie d'echecs reseau.
proxyManager.resetDarkinoNetFailures();
return response;
} catch (error) {
lastError = error;
const statusCode = error.response?.status;
console.warn(`[DARKINO REQUEST][PROXY ERROR] ${String(config.method || 'get').toUpperCase()} ${config.url}`, {
attempt: i + 1,
status: statusCode,
error: summarizeRequestErrorForLog(error),
auth: darkinoAuthSnapshot,
});
// Mettre \u00e0 jour les cookies m\u00eame en cas d'erreur si le header est pr\u00e9sent
if (error.response && error.response.headers['set-cookie'] && deps.cookieJar) {
const setCookieHeader = error.response.headers['set-cookie'];
await Promise.all(setCookieHeader.map(cookie => deps.cookieJar.setCookie(cookie, DARKIWORLD_BASE_URL)));
}
// En cas d'erreur 403 (Cloudflare challenge), activer le cooldown (cluster-wide)
if (statusCode === 403) {
await proxyManager.armDarkinoCooldown('403');
throw error;
}
// En cas d'erreur 429 (Too Many Requests / Cloudflare Workers daily limit),
// activer le cooldown 429 (cluster-wide) \u2014 inutile de spammer un autre proxy
// si l'upstream hydracker lui-m\u00eame rate-limit globalement.
if (statusCode === 429) {
await proxyManager.armDarkinoCooldown('429');
console.log(`[DARKINO] Erreur 429 d\u00e9tect\u00e9e (proxy) - Cooldown activ\u00e9`);
throw error;
}
// En cas d'erreur serveur (5xx), activer le cooldown 5xx (cluster-wide)
if (statusCode >= 500 && statusCode < 600) {
await proxyManager.armDarkinoCooldown('5xx');
console.log(`[DARKINO] Erreur ${statusCode} d\u00e9tect\u00e9e (proxy) - Cooldown activ\u00e9`);
throw error;
}
// Pour les autres erreurs (400, etc.), arr\u00eater imm\u00e9diatement
if (statusCode) {
throw error;
}
// Pas de statusCode = erreur reseau sans reponse HTTP (timeout
// ECONNABORTED, ECONNRESET, ...). Compte l'echec pour le circuit
// breaker cluster-wide : au seuil, le cooldown 'neterr' est arme.
await proxyManager.recordDarkinoNetFailure();
}
}
// Si on arrive ici, tous les proxies ont \u00e9chou\u00e9
console.error(`[DARKINO REQUEST][ALL FAILED] ${String(config.method || 'get').toUpperCase()} ${config.url}`, {
...summarizeRequestErrorForLog(lastError),
auth: darkinoAuthSnapshot,
});
throw lastError || new Error('Tous les proxies Darkino ont \u00e9chou\u00e9');
}
// Fonction utilitaire pour requ\u00eates Coflix avec rotation Cloudflare Workers
async function axiosCoflixRequest(config) {
// Si URL relative, on consid\u00e8re que c'est Coflix.
const targetUrl = config.url || '';
const isAbsolute = /^https?:\/\//i.test(targetUrl);
const isCoflixDomain = isAbsolute ? /^https?:\/\/(?:www\.)?coflix\.[^/]+/i.test(targetUrl) : true;
try {
if (!ENABLE_COFLIX_PROXY || !isCoflixDomain) {
return await deps.axiosCoflix({ ...config });
}
// Utiliser makeCoflixRequest avec rotation SOCKS5
const absoluteUrl = isAbsolute
? targetUrl
: `${deps.COFLIX_BASE_URL}${targetUrl.startsWith('/') ? '' : '/'}${targetUrl}`;
return await makeCoflixRequest(absoluteUrl, {
headers: { ...deps.coflixHeaders, ...(config.headers || {}) },
timeout: config.timeout || 7000,
decompress: true,
responseType: config.responseType
});
} catch (error) {
// Logger les erreurs 403
if (error.response?.status === 403) {
console.log(`[Coflix] Erreur 403 Forbidden dans axiosCoflixRequest`);
console.log(`[Coflix] URL: ${targetUrl}`);
if (error.coflixProxy) {
console.log(`[Coflix] Cloudflare Worker: ${error.coflixProxy}`);
}
if (error.coflixProxiedUrl) {
console.log(`[Coflix] Proxied URL: ${error.coflixProxiedUrl}`);
}
}
throw error;
}
}
// Fonction utilitaire pour requ\u00eates French-Stream (SPA) via CORS fallback
async function axiosFrenchStreamRequest(config) {
const targetUrl = config.url || '';
const isAbsolute = /^https?:\/\//i.test(targetUrl);
const baseMatches = config.baseURL && /french-?stream/i.test(config.baseURL);
const isFrenchStream = /french-?stream/i.test(targetUrl) || baseMatches;
if (!ENABLE_FRENCH_STREAM_PROXY || !isFrenchStream) {
return axios({ timeout: 15000, ...config });
}
const absoluteUrl = isAbsolute
? targetUrl
: (config.baseURL ? `${config.baseURL.replace(/\/$/, '')}/${targetUrl.replace(/^\//, '')}` : targetUrl);
return makeRequestWithCorsFallback(absoluteUrl, {
headers: { ...(config.headers || {}) },
timeout: config.timeout || 5000,
decompress: true,
responseType: config.responseType
});
}
// Fonction utilitaire pour requ\u00eates LecteurVideo avec proxies Wiflix
async function axiosLecteurVideoRequest(config) {
const urlStr = config.url || '';
const isLecteur = /lecteurvideo|lecteur-video|lecteur/i.test(urlStr) || (config.baseURL && /lecteurvideo|lecteur-video|lecteur/i.test(config.baseURL));
if (!ENABLE_LECTEURVIDEO_PROXY || !isLecteur) {
return axios({ timeout: 15000, ...config });
}
try {
// Construire l'URL absolue si n\u00e9cessaire
let absoluteUrl = config.url;
if (config.baseURL && !config.url.startsWith('http')) {
// Nettoyer les URLs avant la concat\u00e9nation pour \u00e9viter les espaces
const cleanBaseURL = config.baseURL.trim();
const cleanUrl = config.url.trim();
absoluteUrl = cleanBaseURL + cleanUrl;
}
// Utiliser makeLecteurVideoRequest avec les proxies Wiflix
return await makeLecteurVideoRequest(absoluteUrl, {
timeout: config.timeout || 5000,
headers: config.headers,
decompress: config.decompress !== false,
responseType: config.responseType,
responseEncoding: config.responseEncoding
});
} catch (error) {
throw error;
}
}
// Fonction utilitaire pour requ\u00eates FStream avec proxy (retry sur 429, pool SOCKS5 + Darkino)
function withOptionalFStreamCookies(headers = {}) {
if (headers['Cookie'] || headers['cookie']) {
return headers;
}
const cookieHeader = Object.entries(deps.fstreamCookies)
.filter(([, v]) => v !== '' && v != null)
.map(([k, v]) => `${k}=${v}`)
.join('; ');
if (!cookieHeader) {
return headers;
}
return {
...headers,
Cookie: cookieHeader
};
}
async function axiosFStreamRequest(config) {
const urlStr = config.url || '';
const fstreamBaseUrl = deps.FSTREAM_BASE_URL || '';
const isFStream = urlStr.includes(fstreamBaseUrl.replace('https://', '').replace('http://', '')) ||
(config.baseURL && config.baseURL.includes(fstreamBaseUrl.replace('https://', '').replace('http://', '')));
if (!ENABLE_FSTREAM_PROXY || !isFStream) {
await deps.ensureFStreamSession();
const existingHeaders = config.headers || {};
const response = await deps.axiosFStream({
...config,
timeout: 8000,
headers: withOptionalFStreamCookies(existingHeaders)
});
deps.incrementFstreamRequestCounter();
return response;
}
// Pool combine SOCKS5 + Darkino HTTP, 1 proxy aleatoire
const allProxies = [];
if (PROXIES && PROXIES.length > 0) {
PROXIES.forEach(p => allProxies.push({ proxy: p, type: 'socks5' }));
}
if (DARKINO_PROXIES && DARKINO_PROXIES.length > 0) {
DARKINO_PROXIES.forEach(p => allProxies.push({ proxy: p, type: 'darkino' }));
}
if (allProxies.length === 0) {
await deps.ensureFStreamSession();
const existingHeaders = config.headers || {};
const response = await deps.axiosFStream({
...config,
timeout: 8000,
headers: withOptionalFStreamCookies(existingHeaders)
});
deps.incrementFstreamRequestCounter();
return response;
}
const entry = allProxies[Math.floor(Math.random() * allProxies.length)];
const proxyLabel = `${entry.type}:${entry.proxy.host}:${entry.proxy.port}`;
try {
let agents;
if (entry.type === 'socks5') {
const agent = getProxyAgent(entry.proxy);
agents = { httpAgent: agent, httpsAgent: agent };
} else {
agents = getDarkinoHttpProxyAgent(entry.proxy);
}
await deps.ensureFStreamSession();
const existingHeaders = config.headers || {};
const response = await deps.axiosFStream({
...config,
timeout: 8000,
decompress: true,
httpAgent: agents.httpAgent,
httpsAgent: agents.httpsAgent,
proxy: false,
headers: withOptionalFStreamCookies(existingHeaders)
});
deps.incrementFstreamRequestCounter();
return response;
} catch (error) {
const status = error.response?.status;
const method = (config.method || 'GET').toUpperCase();
const rawUrl = config.url || '';
const isAbs = /^https?:\/\//i.test(rawUrl);
const fullUrl = isAbs
? rawUrl
: (config.baseURL ? `${config.baseURL.replace(/\/$/, '')}/${rawUrl.replace(/^\//, '')}` : rawUrl);
let qs = '';
if (config.params && typeof config.params === 'object') {
try { qs = '?' + new URLSearchParams(config.params).toString(); } catch (_) {}
}
console.error(`[FSTREAM REQUEST] Erreur ${status || error.code || 'unknown'} avec proxy ${proxyLabel}: ${error.message} | ${method} ${fullUrl}${qs}`);
throw error;
}
}
function isAnimeSamaRetriableError(error) {
const statusCode = error?.response?.status;
const errorCode = error?.code;
const message = String(error?.message || '').toLowerCase();
if (statusCode === 403 || statusCode === 407 || statusCode === 408 || statusCode === 425 || statusCode === 429) {
return true;
}
if (statusCode >= 500 && statusCode < 600) {
return true;
}
if (
errorCode === 'ECONNABORTED' ||
errorCode === 'ETIMEDOUT' ||
errorCode === 'ECONNRESET' ||
errorCode === 'ECONNREFUSED' ||
errorCode === 'EHOSTUNREACH' ||
errorCode === 'ENETUNREACH' ||
errorCode === 'EAI_AGAIN'
) {
return true;
}
return (
message.includes('socks5 authentication failed') ||
message.includes('proxy authentication') ||
message.includes('tunneling socket could not be established')
);
}
// Fonction utilitaire pour requ\u00eates AnimeSama via CycleTLS (JA3 Chrome) + proxies
async function axiosAnimeSamaRequest(config) {
const debugAnimeSama = process.env.DEBUG_ANIMESAMA === 'true';
let urlStr = config.url || '';
const isAnimeSama = urlStr.includes('anime-sama.to') || urlStr.includes('anime-sama.si') || urlStr.includes('anime-sama.fr') ||
(config.baseURL && (config.baseURL.includes('anime-sama.to') || config.baseURL.includes('anime-sama.si') || config.baseURL.includes('anime-sama.fr')));
if (!ENABLE_ANIME_PROXY || !isAnimeSama) {
return deps.axiosAnimeSama({ ...config, timeout: 30000, proxy: false });
}
// Construire l'URL compl\u00e8te
let absoluteUrl = urlStr;
if (config.baseURL && !urlStr.startsWith('http')) {
absoluteUrl = config.baseURL + urlStr;
} else if (!urlStr.startsWith('http') && !absoluteUrl.startsWith('http')) {
// Si l'URL n'est pas compl\u00e8te et qu'on n'a pas de baseURL, utiliser ANIME_SAMA_URL
absoluteUrl = deps.ANIME_SAMA_URL + (urlStr.startsWith('/') ? urlStr.substring(1) : urlStr);
}
// Headers pour les requ\u00eates Anime-Sama via proxy
const animeSamaHeaders = {
'Accept-Language': 'fr-FR,fr;q=0.6',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache',
'Priority': 'u=0, i',
'Sec-CH-UA': '"Chromium";v="142", "Brave";v="142", "Not_A Brand";v="99"',
'Sec-CH-UA-Mobile': '?0',
'Sec-CH-UA-Platform': '"Windows"',
'Sec-Fetch-Dest': 'document',
'Sec-Fetch-Mode': 'navigate',
'Sec-Fetch-Site': 'none',
'Sec-Fetch-User': '?1',
'Sec-GPC': '1',
'Upgrade-Insecure-Requests': '1',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36'
};
// 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 = { ...animeSamaHeaders };
// Si config.headers existe, le nettoyer aussi
if (config.headers) {
const configHeaders = { ...config.headers };
headersToRemove.forEach(header => {
delete configHeaders[header];
delete configHeaders[header.toLowerCase()];
});
Object.assign(cleanHeaders, configHeaders);
}
// CycleTLS (JA3 Chrome) + rotation sur pool SOCKS5/HTTP
const method = String(config.method || 'get').toLowerCase();
let cycleBody = '';
if (config.data !== undefined && config.data !== null) {
cycleBody = typeof config.data === 'string' ? config.data : JSON.stringify(config.data);
}
const res = await makeAnimeSamaRequest(absoluteUrl, {
headers: cleanHeaders,
timeout: 30,
method,
body: cycleBody,
});
if (res.status >= 200 && res.status < 400) {
return {
data: res.data,
status: res.status,
headers: res.headers,
statusText: '',
config,
};
}
const cycleErr = new Error(`[ANIMESAMA CYCLETLS] status ${res.status}`);
cycleErr.response = { data: res.data, status: res.status, headers: res.headers };
if (debugAnimeSama) {
console.warn(`[ANIMESAMA REQUEST] cycletls returned ${res.status}`, summarizeRequestErrorForLog(cycleErr));
}
throw cycleErr;
}
// Fonction utilitaire pour requ\u00eates Wiflix avec proxy
// D\u00e9l\u00e8gue \u00e0 makeWiflixRequest (free proxies -> Cloudflare Workers -> direct)
async function axiosWiflixRequest(config) {
const urlStr = config.url || '';
// Construire l'URL compl\u00e8te
let absoluteUrl = urlStr;
if (config.baseURL && !urlStr.startsWith('http')) {
absoluteUrl = config.baseURL + urlStr;
} else if (!urlStr.startsWith('http')) {
absoluteUrl = WIFLIX_BASE_URL + (urlStr.startsWith('/') ? urlStr : '/' + urlStr);
}
const { url: _, baseURL: __, headers, ...restConfig } = config;
return makeWiflixRequest(absoluteUrl, {
method: restConfig.method || 'GET',
headers: headers || {},
timeout: restConfig.timeout || 15000,
...(restConfig.data !== undefined ? { data: restConfig.data } : {}),
...(restConfig.responseType ? { responseType: restConfig.responseType } : {}),
});
}
// === UTILITY: Fusion des streaming_links par langue ===
function mergeStreamingLinks(oldLinks, newLinks) {
// oldLinks et newLinks sont des tableaux d'objets { language, players }
const merged = {};
// D'abord, copier les anciens liens
(oldLinks || []).forEach(l => {
merged[l.language] = Array.isArray(l.players) ? [...l.players] : [];
});
// Ensuite, remplacer par les nouveaux liens si disponibles
(newLinks || []).forEach(l => {
if (l.players && l.players.length > 0) {
// Si on a de nouveaux lecteurs, on remplace compl\u00e8tement les anciens
merged[l.language] = Array.isArray(l.players) ? [...l.players] : [];
}
});
// Retourne sous forme d'array d'objets
return Object.entries(merged).map(([language, players]) => ({ language, players }));
}
// === UTILITY: Migration des anciens fichiers de cache s\u00e9par\u00e9s vers le cache unifi\u00e9 ===
async function migrateOldCacheFiles(safeAnimeName, animeCacheDir) {
try {
const allCacheFiles = await fsp.readdir(animeCacheDir).catch(() => []);
const oldSeasonFiles = allCacheFiles.filter(f => f.startsWith(safeAnimeName + '_') && f.endsWith('.json'));
if (oldSeasonFiles.length === 0) return;
const migratedSeasons = {};
for (const seasonFile of oldSeasonFiles) {
try {
const seasonContent = await fsp.readFile(path.join(animeCacheDir, seasonFile), 'utf-8');
const seasonCache = JSON.parse(seasonContent);
const seasonName = seasonFile.replace(safeAnimeName + '_', '').replace('.json', '');
migratedSeasons[seasonName] = {
timestamp: seasonCache.timestamp || Date.now(),
episodes: seasonCache.episodes || []
};
} catch (e) {
}
}
if (Object.keys(migratedSeasons).length > 0) {
const unifiedCacheData = {
timestamp: Date.now(),
seasons: migratedSeasons
};
const animeCachePath = path.join(animeCacheDir, `${safeAnimeName}.json`);
await writeFileAtomic(animeCachePath, JSON.stringify(unifiedCacheData), 'utf-8');
// Nettoyer les anciens fichiers apr\u00e8s migration r\u00e9ussie
await cleanupOldCacheFiles(safeAnimeName, animeCacheDir);
}
} catch (e) {
}
}
// === UTILITY: Nettoyage des anciens fichiers de cache s\u00e9par\u00e9s ===
async function cleanupOldCacheFiles(safeAnimeName, animeCacheDir) {
try {
const allCacheFiles = await fsp.readdir(animeCacheDir).catch(() => []);
const oldSeasonFiles = allCacheFiles.filter(f => f.startsWith(safeAnimeName + '_') && f.endsWith('.json'));
for (const oldFile of oldSeasonFiles) {
try {
await fsp.unlink(path.join(animeCacheDir, oldFile));
} catch (e) {
}
}
} catch (e) {
}
}
// Fonction pour supprimer les lecteurs fsvid des r\u00e9ponses FStream
function removeFsvidPlayers(data, isVip = false) {
// Si pas de donn\u00e9es, retourner tel quel
if (!data) {
return data;
}
// Si l'utilisateur est VIP, ne pas filtrer les lecteurs fsvid
if (isVip) {
return data;
}
// Par d\u00e9faut, toujours filtrer les lecteurs fsvid (m\u00eame si remove_fsvid est false)
// Pour les films : filtrer players.organized
if (data.players && typeof data.players === 'object') {
const filteredPlayers = {};
let totalPlayers = 0;
Object.keys(data.players).forEach(playerType => {
if (Array.isArray(data.players[playerType])) {
// Filtrer les lecteurs dont l'URL contient "fsvid"
const filteredPlayerList = data.players[playerType].filter(player => {
return !player.url || !player.url.includes('fsvid');
});
if (filteredPlayerList.length > 0) {
filteredPlayers[playerType] = filteredPlayerList;
totalPlayers += filteredPlayerList.length;
}
} else {
// Si ce n'est pas un array, garder tel quel
filteredPlayers[playerType] = data.players[playerType];
}
});
return {
...data,
players: filteredPlayers,
total: totalPlayers,
metadata: {
...data.metadata,
fsvidFiltered: true
}
};
}
// Pour les s\u00e9ries : filtrer episodes
if (data.episodes && typeof data.episodes === 'object') {
const filteredEpisodes = {};
let totalPlayers = 0;
Object.keys(data.episodes).forEach(episodeKey => {
const episode = data.episodes[episodeKey];
if (episode && typeof episode === 'object') {
const filteredEpisode = { ...episode };
// V\u00e9rifier si l'\u00e9pisode a une structure "languages"
if (episode.languages && typeof episode.languages === 'object') {
const filteredLanguages = {};
Object.keys(episode.languages).forEach(languageKey => {
const languagePlayers = episode.languages[languageKey];
if (Array.isArray(languagePlayers)) {
// Filtrer les lecteurs dont l'URL contient "fsvid"
const filteredPlayerList = languagePlayers.filter(player => {
return !player.url || !player.url.includes('fsvid');
});
if (filteredPlayerList.length > 0) {
filteredLanguages[languageKey] = filteredPlayerList;
totalPlayers += filteredPlayerList.length;
}
} else {
// Si ce n'est pas un array, garder tel quel
filteredLanguages[languageKey] = languagePlayers;
}
});
filteredEpisode.languages = filteredLanguages;
} else {
// Structure ancienne sans "languages" - filtrer directement
Object.keys(episode).forEach(playerType => {
if (Array.isArray(episode[playerType])) {
// Filtrer les lecteurs dont l'URL contient "fsvid"
const filteredPlayerList = episode[playerType].filter(player => {
return !player.url || !player.url.includes('fsvid');
});
if (filteredPlayerList.length > 0) {
filteredEpisode[playerType] = filteredPlayerList;
totalPlayers += filteredPlayerList.length;
}
}
});
}
// Garder l'\u00e9pisode seulement s'il a encore des lecteurs
if (episode.languages ? Object.keys(filteredEpisode.languages).length > 0 : Object.keys(filteredEpisode).some(key => Array.isArray(filteredEpisode[key]) && filteredEpisode[key].length > 0)) {
filteredEpisodes[episodeKey] = filteredEpisode;
}
}
});
return {
...data,
episodes: filteredEpisodes,
total: totalPlayers,
metadata: {
...data.metadata,
fsvidFiltered: true
}
};
}
return data;
}
// Fonction pour formater les erreurs Coflix
function formatCoflixError(error, context = '') {
if (error && error.isAxiosError) {
const url = error.config && error.config.url ? error.config.url : '';
const statusCode = error.response ? error.response.status : '';
const statusText = error.response ? error.response.statusText : '';
// Ne pas logger les erreurs 400
if (statusCode === 400) {
return '';
}
return `[AxiosError] ${error.code || ''} ${error.message} ${statusCode ? `(${statusCode} ${statusText})` : ''} ${url}`;
} else {
const msg = error && error.message
? error.message
: (typeof error === 'string' ? error : JSON.stringify(error));
return msg;
}
}
module.exports = {
configure,
// Darkino
generateDarkiReferer,
axiosDarkinoRequest,
// Coflix
axiosCoflixRequest,
formatCoflixError,
// French-Stream
axiosFrenchStreamRequest,
// LecteurVideo
axiosLecteurVideoRequest,
// FStream
axiosFStreamRequest,
// AnimeSama
axiosAnimeSamaRequest,
// Wiflix
axiosWiflixRequest,
// Streaming link utilities
mergeStreamingLinks,
migrateOldCacheFiles,
cleanupOldCacheFiles,
// FStream player filter
removeFsvidPlayers
};