MovixOpenSource/API/Mainapi/routes/coflix.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

869 lines
25 KiB
JavaScript

/**
* Coflix helper module.
* Extracted from server.js -- provides Coflix search and data extraction functions.
*
* This is NOT a router. It exports plain functions used by tmdb.js and others.
*/
const cheerio = require("cheerio");
const { Buffer } = require("buffer");
const { CACHE_DIR, generateCacheKey } = require("../utils/cacheManager");
// ---- Lazy-bound dependencies injected via configure() ----
let deps = {
axiosCoflixRequest: async () => {
throw new Error("coflix not configured");
},
axiosLecteurVideoRequest: async () => {
throw new Error("coflix not configured");
},
makeCoflixRequest: async () => {
throw new Error("coflix not configured");
},
coflixHeaders: {},
getFromCacheNoExpiration: async () => null,
saveToCache: async () => false,
formatCoflixError: (error) =>
error && error.message ? error.message : String(error),
};
function configure(injected) {
Object.assign(deps, injected);
}
// ---- Functions ----
// Fonction pour normaliser les caracteres speciaux pour les requetes Coflix
function normalizeCoflixQuery(query) {
if (!query) return query;
const replacements = {
"\u00e0": "a",
"\u00e1": "a",
"\u00e2": "a",
"\u00e3": "a",
"\u00e4": "a",
"\u00e5": "a",
"\u00e8": "e",
"\u00e9": "e",
"\u00ea": "e",
"\u00eb": "e",
"\u00ec": "i",
"\u00ed": "i",
"\u00ee": "i",
"\u00ef": "i",
"\u00f2": "o",
"\u00f3": "o",
"\u00f4": "o",
"\u00f5": "o",
"\u00f6": "o",
"\u00f9": "u",
"\u00fa": "u",
"\u00fb": "u",
"\u00fc": "u",
"\u00fd": "y",
"\u00ff": "y",
"\u00f1": "n",
"\u00e7": "c",
"\u0153": "oe",
"\u00e6": "ae",
"\u00c0": "A",
"\u00c1": "A",
"\u00c2": "A",
"\u00c3": "A",
"\u00c4": "A",
"\u00c5": "A",
"\u00c8": "E",
"\u00c9": "E",
"\u00ca": "E",
"\u00cb": "E",
"\u00cc": "I",
"\u00cd": "I",
"\u00ce": "I",
"\u00cf": "I",
"\u00d2": "O",
"\u00d3": "O",
"\u00d4": "O",
"\u00d5": "O",
"\u00d6": "O",
"\u00d9": "U",
"\u00da": "U",
"\u00db": "U",
"\u00dc": "U",
"\u00dd": "Y",
"\u0178": "Y",
"\u00d1": "N",
"\u00c7": "C",
"\u0152": "OE",
"\u00c6": "AE",
};
let normalized = query;
for (const [special, normal] of Object.entries(replacements)) {
normalized = normalized.split(special).join(normal);
}
return normalized;
}
// Fonction pour rechercher un contenu sur Coflix par titre
async function searchCoflixByTitle(title, mediaType, releaseYear) {
try {
const normalizedTitle = normalizeCoflixQuery(title);
const response = await deps.axiosCoflixRequest({
method: "get",
url: `/suggest.php?query=${encodeURIComponent(normalizedTitle)}`,
});
if (!Array.isArray(response.data)) {
console.error(
`La reponse de l'API Coflix n'est pas un tableau: ${deps.formatCoflixError(response.data)}`,
);
return [];
}
const results = response.data.map((item) => {
const similarity = calculateTitleSimilarity(title, item.title);
const resultYear = item.year ? parseInt(item.year) : null;
return {
title: item.title,
url: item.url,
similarity: similarity,
year: resultYear,
id: item.ID,
excerpt: item.excerpt,
post_type: item.post_type,
rating: item.rating,
};
});
let coflixTypes = [];
if (mediaType === "movie") {
coflixTypes = ["movies"];
} else if (mediaType === "tv") {
coflixTypes = ["series", "animes", "doramas"];
} else if (mediaType === "anime") {
coflixTypes = ["animes"];
} else {
coflixTypes = ["movies", "series", "animes", "doramas"];
}
let filteredResults = mediaType
? results.filter((item) => coflixTypes.includes(item.post_type))
: results;
filteredResults.sort((a, b) => b.similarity - a.similarity);
if (releaseYear) {
const yearMatchedResults = filteredResults.filter(
(r) => r.year === releaseYear,
);
if (mediaType === "movie") {
if (
yearMatchedResults.length > 0 &&
yearMatchedResults[0].similarity >= 0.8
) {
filteredResults = [yearMatchedResults[0]];
} else {
filteredResults = [];
}
} else if (mediaType === "tv") {
if (
yearMatchedResults.length > 0 &&
yearMatchedResults[0].similarity >= 0.7
) {
filteredResults = [yearMatchedResults[0]];
} else {
filteredResults = [];
}
} else {
if (
yearMatchedResults.length > 0 &&
yearMatchedResults[0].similarity >= 0.8
) {
filteredResults = [yearMatchedResults[0]];
} else {
filteredResults = [];
}
}
} else {
if (filteredResults.length > 0 && filteredResults[0].similarity > 0.8) {
filteredResults = [filteredResults[0]];
} else {
filteredResults = [];
}
}
return filteredResults;
} catch (error) {
// Global coflix.band 429 is announced once by makeCoflixRequest's cooldown — don't spam per title.
if (!error?.coflixSiteRateLimited) {
console.error(
`Erreur lors de la recherche sur Coflix pour ${title}: ${deps.formatCoflixError(error)}`,
);
}
throw error;
}
}
// Fonction utilitaire pour calculer la similarite entre deux titres
function calculateTitleSimilarity(title1, title2) {
if (!title1 || !title2) return 0;
const t1 = title1.toLowerCase();
const t2 = title2.toLowerCase();
const normalize = (str) =>
str
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[\u00ae\u2122\u00a9]/g, "")
.replace(/[^\p{L}\p{N}\s]/gu, " ")
.replace(/\s+/g, " ")
.trim();
const norm1 = normalize(t1);
const norm2 = normalize(t2);
if (norm1 === norm2) {
return 1.0;
}
const extractNumbers = (str) => {
const numbers = str.match(/\b\d+\b/g);
return numbers ? numbers.map((n) => parseInt(n)) : [];
};
const numbers1 = extractNumbers(norm1);
const numbers2 = extractNumbers(norm2);
if (numbers1.length > 0 || numbers2.length > 0) {
const hasCommonNumbers =
numbers1.length > 0 &&
numbers2.length > 0 &&
numbers1.some((n1) => numbers2.some((n2) => n1 === n2));
if (!hasCommonNumbers) {
return 0.2;
}
}
const extractSubtitles = (str) => {
const parts = str.split(/[:|]/);
return parts.length > 1 ? parts[1].trim() : "";
};
const subtitle1 = extractSubtitles(norm1);
const subtitle2 = extractSubtitles(norm2);
if (subtitle1 && subtitle2 && subtitle1 !== subtitle2) {
const subtitleWords1 = subtitle1.split(/\s+/).filter((w) => w.length > 2);
const subtitleWords2 = subtitle2.split(/\s+/).filter((w) => w.length > 2);
const commonSubtitleWords = subtitleWords1.filter((w1) =>
subtitleWords2.some((w2) => w1.includes(w2) || w2.includes(w1)),
);
if (commonSubtitleWords.length === 0) {
return 0.3;
}
}
const locationKeywords = [
"londres",
"paris",
"new york",
"tokyo",
"berlin",
"rome",
"madrid",
"barcelone",
];
const hasLocation1 = locationKeywords.some((keyword) =>
norm1.includes(keyword),
);
const hasLocation2 = locationKeywords.some((keyword) =>
norm2.includes(keyword),
);
if (hasLocation1 !== hasLocation2) {
return 0.25;
}
// Short titles
if (norm1.length <= 10 || norm2.length <= 10) {
if (norm2.includes(norm1) || norm1.includes(norm2)) {
const shorter = norm1.length <= norm2.length ? norm1 : norm2;
const longer = norm1.length <= norm2.length ? norm2 : norm1;
const lengthRatio = shorter.length / longer.length;
if (lengthRatio >= 0.8) {
return 0.85;
} else if (lengthRatio >= 0.6) {
return 0.7;
} else if (lengthRatio >= 0.4) {
return 0.5;
}
return 0.2;
}
}
// Boost if searched title is a head token
if (norm2.startsWith(norm1 + " ") || norm1.startsWith(norm2 + " ")) {
return 0.9;
}
if (norm2.includes(norm1) || norm1.includes(norm2)) {
if (norm1.length < norm2.length && norm2.includes(norm1)) {
const lengthRatio = norm1.length / norm2.length;
if (lengthRatio >= 0.6) {
return 0.8 * lengthRatio;
} else if (lengthRatio >= 0.4) {
return 0.6 * lengthRatio;
}
return 0.3;
} else if (norm2.length < norm1.length && norm1.includes(norm2)) {
const lengthRatio = norm2.length / norm1.length;
if (lengthRatio >= 0.6) {
return 0.75 * lengthRatio;
} else if (lengthRatio >= 0.4) {
return 0.55 * lengthRatio;
}
return 0.3;
}
return 0.6;
}
const filterShortWords = (words) => words.filter((w) => w.length > 3);
const words1 = filterShortWords(norm1.split(/\s+/));
const words2 = filterShortWords(norm2.split(/\s+/));
const finalWords1 = words1.length ? words1 : norm1.split(/\s+/);
const finalWords2 = words2.length ? words2 : norm2.split(/\s+/);
let matches = 0;
let orderBonus = 0;
finalWords1.forEach((word, index) => {
const matchIndex = finalWords2.findIndex(
(w) => w.includes(word) || word.includes(w),
);
if (matchIndex !== -1) {
matches++;
if (Math.abs(index - matchIndex) < 2) {
orderBonus += 0.1;
}
}
});
const baseScore =
matches / Math.max(finalWords1.length, finalWords2.length, 1);
return Math.min(baseScore + orderBonus, 0.95);
}
// Fonction pour filtrer les lecteurs lecteur1.xtremestream.xyz des donnees
function filterEmmmmbedReaders(data) {
if (!data) return data;
function filterObject(obj) {
if (Array.isArray(obj)) {
return obj.map((item) => filterObject(item));
} else if (obj && typeof obj === "object") {
const filtered = {};
for (const [key, value] of Object.entries(obj)) {
if (key === "_coflixRefreshedAt") continue;
if (key === "player_links" && Array.isArray(value)) {
filtered[key] = value.filter(
(link) =>
!link.decoded_url ||
!link.decoded_url.includes("lecteur1.xtremestream.xyz"),
);
} else {
filtered[key] = filterObject(value);
}
}
return filtered;
}
return obj;
}
return filterObject(data);
}
// Fonction pour extraire les donnees des films depuis Coflix
async function getMovieDataFromCoflix(url) {
let cachedData = null;
let hadCache = false;
try {
const cacheKey = generateCacheKey({ url: url, type: "movie" });
cachedData = await deps.getFromCacheNoExpiration(
CACHE_DIR.COFLIX,
cacheKey,
);
hadCache = !!cachedData;
if (cachedData) {
// Ne pas utiliser le cache si player_links est vide — re-fetcher
if (!cachedData.player_links || cachedData.player_links.length === 0) {
console.log(
`[COFLIX MOVIE] Cache ignoré (player_links vide) pour ${url} — re-fetch en cours`,
);
} else {
return cachedData;
}
}
const relativePath = url.replace(
/^https?:\/\/(?:www\.)?coflix\.[^/]+/i,
"",
);
const response = await deps.axiosCoflixRequest({
method: "get",
url: relativePath,
});
const $ = cheerio.load(response.data);
let iframe = $(
"main div div div article div:nth-child(2) div:nth-child(1) aside div div iframe",
);
if (!iframe.length) {
iframe = $("article iframe");
}
if (!iframe.length) {
iframe = $("iframe");
}
let iframeSrc = null;
let playerLinks = [];
if (iframe.length > 0) {
iframeSrc = iframe.attr("src");
if (iframeSrc) {
try {
const iframePageResponse = await deps.axiosLecteurVideoRequest({
method: "get",
url: iframeSrc,
});
const iframePage$ = cheerio.load(iframePageResponse.data);
let playerItems = iframePage$('li[onclick*="showVideo"]');
if (!playerItems.length) {
playerItems = iframePage$("div li[onclick]");
}
if (playerItems.length === 0) {
const bodyHtml = iframePage$("body").html() || "";
console.warn(
`[COFLIX MOVIE] ⚠️ Aucun playerItem — iframe: ${iframeSrc}, status: ${iframePageResponse.status}, taille: ${(iframePageResponse.data || "").length} chars, aperçu HTML: ${bodyHtml.substring(0, 500)}`,
);
}
playerItems.each((i, element) => {
try {
const $element = iframePage$(element);
const onClickAttr = $element.attr("onclick") || "";
const base64Match = onClickAttr.match(
/showVideo\(['"]([^'\"]+)['"]/,
);
if (base64Match && base64Match[1]) {
const base64Url = base64Match[1];
let decodedUrl;
try {
decodedUrl = Buffer.from(base64Url, "base64").toString(
"utf-8",
);
} catch (decodeError) {
decodedUrl = null;
}
const quality = $element.find("span").text().trim();
let language = "Unknown";
const info = $element.find("p").text().trim();
if (info.toLowerCase().includes("french")) {
language = "French";
} else if (info.toLowerCase().includes("english")) {
language = "English";
} else if (info.toLowerCase().includes("vostfr")) {
language = "VOSTFR";
}
playerLinks.push({
decoded_url: decodedUrl,
quality: quality,
language: language,
});
}
} catch (playerError) {
const errorCode =
playerError.response?.status || playerError.code || "unknown";
console.error(
`[COFLIX MOVIE] ❌ Erreur extraction player: ${errorCode}`,
);
}
});
} catch (iframePageError) {
const errorCode =
iframePageError.response?.status ||
iframePageError.code ||
"unknown";
console.error(
`[COFLIX MOVIE] ❌ Erreur requête iframe ${iframeSrc} — code: ${errorCode}, message: ${iframePageError.message}`,
);
}
}
} else {
console.log(`Aucun iframe trouve pour l'URL ${url}`);
}
const result = {
iframe_src: iframeSrc && !iframeSrc.includes("coflix") ? iframeSrc : null,
player_links: playerLinks,
};
if (playerLinks.length > 0) {
await deps.saveToCache(CACHE_DIR.COFLIX, cacheKey, result);
}
return result;
} catch (error) {
if (
error &&
(error.isAxiosError || (error.response && error.response.status))
) {
console.error(
`Erreur lors de la recuperation des donnees du film Coflix: ${deps.formatCoflixError(error)}`,
);
if (hadCache) {
console.log(`[COFLIX] Cache preserve malgre l'erreur pour ${url}`);
return cachedData;
}
throw error;
}
console.error(
`Erreur lors de la recuperation des donnees du film Coflix: ${deps.formatCoflixError(error)}`,
);
if (hadCache) {
console.log(`[COFLIX] Cache preserve malgre l'erreur pour ${url}`);
return cachedData;
}
return { iframe_src: null, player_links: [] };
}
}
// Nouvelle fonction pour recuperer les episodes via l'API Coflix
async function fetchCoflixSeriesEpisodes(postId, seasonNumber) {
try {
const response = await deps.axiosCoflixRequest({
method: "get",
url: `/wp-json/apiflix/v1/series/${postId}/${seasonNumber}`,
});
if (response.data && Array.isArray(response.data.episodes)) {
return response.data;
}
return null;
} catch (error) {
console.error(
`Erreur lors de la recuperation des episodes Coflix: ${deps.formatCoflixError(error)}`,
);
return null;
}
}
// Fonction pour extraire les donnees des series depuis Coflix
async function getTvDataFromCoflix(url, seasonNumber, episodeNumber) {
let cachedData = null;
let hadCache = false;
try {
const cacheKey = generateCacheKey({
url: url,
season: seasonNumber,
episode: episodeNumber,
type: "tv",
});
cachedData = await deps.getFromCacheNoExpiration(
CACHE_DIR.COFLIX,
cacheKey,
);
hadCache = !!cachedData;
if (cachedData) {
return cachedData;
}
const relativePath = url.replace(
/^https?:\/\/(?:www\.)?coflix\.[^/]+/i,
"",
);
const response = await deps.axiosCoflixRequest({
method: "get",
url: relativePath,
});
const $ = cheerio.load(response.data);
const seasonItems = $("article section div aside div ul li");
if (!seasonItems.length) {
console.log(`[ERROR] Aucune saison trouvee pour ${url}`);
const altSelectors = [
"ul li",
".seasons li",
".season-list li",
"[data-season]",
"input[data-season]",
];
for (const selector of altSelectors) {
const altItems = $(selector);
if (altItems.length > 0) {
// found items with alternative selector
}
}
return {
seasons: [],
current_episode: null,
};
}
const seasons = [];
let targetSeason = null;
let postId = null;
for (let i = 0; i < seasonItems.length; i++) {
const $seasonElement = $(seasonItems[i]);
const $label = $seasonElement.find("label");
const $input = $label.find("input");
const sNumber = $input.attr("data-season");
const seriesId = $input.attr("data-id");
const currentPostId = $input.attr("post-id");
const seasonName = $label.find("span").text().trim();
const season = {
season_number: parseInt(sNumber),
name: seasonName,
data_id: seriesId,
post_id: currentPostId,
episodes: [],
};
seasons.push(season);
if (parseInt(sNumber) === seasonNumber) {
targetSeason = season;
postId = currentPostId;
}
}
const slugMatch = url.match(/\/serie\/([^/]+)/);
const animeMatch = url.match(/\/animes\/([^/]+)/);
const seriesSlug = slugMatch
? slugMatch[1]
: animeMatch
? animeMatch[1]
: "";
if (targetSeason && episodeNumber) {
let episodeApiData = null;
let episodeUrl = null;
if (postId) {
const apiData = await fetchCoflixSeriesEpisodes(postId, seasonNumber);
if (apiData && apiData.episodes) {
const episode = apiData.episodes.find(
(ep) => parseInt(ep.number) === parseInt(episodeNumber),
);
if (episode && episode.links) {
episodeUrl = episode.links.startsWith("https://coflix.trade")
? `${episode.links}`
: episode.links;
}
}
}
if (!episodeUrl) {
episodeUrl = `https://coflix.trade/episode/${seriesSlug}-${seasonNumber}x${episodeNumber}/`;
}
try {
const episodePageResponse = await deps.makeCoflixRequest(episodeUrl, {
headers: deps.coflixHeaders,
timeout: 15000,
});
const episodePage$ = cheerio.load(episodePageResponse.data);
const episodeTitle = episodePage$("article header h1").text().trim();
const episodePlayerLinks = [];
let episodeIframe = episodePage$("main div div div article div iframe");
if (!episodeIframe.length) {
episodeIframe = episodePage$("article iframe");
}
if (!episodeIframe.length) {
episodeIframe = episodePage$("iframe");
}
let episodeIframeSrc = null;
if (episodeIframe.length > 0) {
episodeIframeSrc = episodeIframe.attr("src");
if (episodeIframeSrc) {
try {
const iframePageResponse = await deps.axiosLecteurVideoRequest({
method: "get",
url: episodeIframeSrc,
});
const iframePage$ = cheerio.load(iframePageResponse.data);
let playerItems = iframePage$('li[onclick*="showVideo"]');
if (!playerItems.length) {
playerItems = iframePage$("div li[onclick]");
}
if (playerItems.length === 0) {
const bodyHtml = iframePage$("body").html() || "";
console.warn(
`[COFLIX TV] ⚠️ Aucun playerItem — iframe: ${episodeIframeSrc}, status: ${iframePageResponse.status}, taille: ${(iframePageResponse.data || "").length} chars, aperçu HTML: ${bodyHtml.substring(0, 500)}`,
);
}
playerItems.each((i, element) => {
try {
const $element = iframePage$(element);
const onClickAttr = $element.attr("onclick") || "";
const base64Match = onClickAttr.match(
/showVideo\(['"]([^'\"]+)['"]/,
);
if (base64Match && base64Match[1]) {
const base64Url = base64Match[1];
let decodedUrl;
try {
decodedUrl = Buffer.from(base64Url, "base64").toString(
"utf-8",
);
} catch (decodeError) {
decodedUrl = null;
}
const quality = $element.find("span").text().trim();
let language = "Unknown";
const info = $element.find("p").text().trim();
if (info.toLowerCase().includes("french")) {
language = "French";
} else if (info.toLowerCase().includes("english")) {
language = "English";
} else if (info.toLowerCase().includes("vostfr")) {
language = "VOSTFR";
}
episodePlayerLinks.push({
decoded_url: decodedUrl,
quality: quality,
language: language,
});
}
} catch (playerError) {
const errorCode =
playerError.response?.status ||
playerError.code ||
"unknown";
console.error(
`[COFLIX TV] ❌ Erreur extraction player: ${errorCode}`,
);
}
});
} catch (iframePageError) {
const errorCode =
iframePageError.response?.status ||
iframePageError.code ||
"unknown";
console.error(
`[COFLIX TV] ❌ Erreur requête iframe ${episodeIframeSrc} — code: ${errorCode}, message: ${iframePageError.message}`,
);
}
}
}
return {
seasons: seasons,
current_episode: {
season_number: seasonNumber,
episode_number: episodeNumber,
title: episodeTitle,
iframe_src:
episodeIframeSrc && !episodeIframeSrc.includes("coflix")
? episodeIframeSrc
: null,
player_links: episodePlayerLinks,
},
};
} catch (episodeError) {
console.error(
`Erreur lors de la recuperation des donnees de l'episode: ${deps.formatCoflixError(episodeError)}`,
);
return {
seasons: seasons,
current_episode: null,
};
}
}
const result = {
seasons: seasons,
current_episode: null,
};
if (seasons.length > 0) {
await deps.saveToCache(CACHE_DIR.COFLIX, cacheKey, result);
}
return result;
} catch (error) {
if (
error &&
(error.isAxiosError || (error.response && error.response.status))
) {
console.error(
`Erreur lors de la recuperation des donnees de la serie Coflix: ${deps.formatCoflixError(error)}`,
);
if (hadCache) {
console.log(`[COFLIX] Cache preserve malgre l'erreur pour ${url}`);
return cachedData;
}
throw error;
}
console.error(
`Erreur lors de la recuperation des donnees de la serie Coflix: ${deps.formatCoflixError(error)}`,
);
if (hadCache) {
console.log(`[COFLIX] Cache preserve malgre l'erreur pour ${url}`);
return cachedData;
}
return { seasons: [], current_episode: null };
}
}
module.exports = {
configure,
normalizeCoflixQuery,
searchCoflixByTitle,
calculateTitleSimilarity,
filterEmmmmbedReaders,
getMovieDataFromCoflix,
fetchCoflixSeriesEpisodes,
getTvDataFromCoflix,
};