mirror of
https://github.com/movixcorp/MovixOpenSource.git
synced 2026-08-10 18:25:40 +00:00
Gros lot de travail synchronisé depuis le repo privé. Cette mise à jour regroupe la refonte du lecteur et du Cast, de nouvelles sources VOD/Live TV, le durcissement de l’API et des proxies, ainsi qu’une nouvelle version de l’application mobile. Lecteur, sources et Live TV - Refonte du lecteur HLS avec sélection et mémorisation de la qualité, détection des résolutions, scan des liens, choix des pistes audio et fallback contrôlé de 1080p vers 720p. - Ajoute une garde contre les boucles de changement de source, notamment lors des erreurs vidéo et des réponses 429. - Revoit SeekStreaming de bout en bout : validation des embeds, prise en charge de plusieurs candidats, regroupement par serveur, règles de headers persistantes et cache d’extraction raccourci. - Ajoute SwiftFlow pour les films et séries, avec lecteurs VF/VOSTFR, cache stale-while-revalidate, déduplication des requêtes et limite partagée côté API. - Adapte Coflix à son nouveau domaine et à son nouveau thème, ajoute un kill switch, développe les wrappers 1jour1film et actualise le domaine Wiflix. - Ajoute NorthLive et Vavoo à la Live TV, regroupe les variantes d’une même chaîne et permet de choisir le serveur ou la qualité. - Enrichit les Top 10 avec des filtres par période et par algorithme, ainsi que de nouvelles métriques de visionnage. Cast et application mobile - Unifie le contrôle Cast entre le Web Cast SDK et le pont Android : état distant, lecture, pause, seek, arrêt et reprise après reconnexion. - Ajoute sur Android un relais média LAN sécurisé, limité au Chromecast et au réseau Wi-Fi sélectionnés, avec sessions opaques et service foreground. - Prépare les sources avant diffusion et prend en charge les playlists HLS TS/fMP4, les flux progressifs, les sous-titres VTT, SeekStreaming et les segments MPEG-TS encapsulés dans des PNG. - Renforce le proxy média local : support Range, validation des redirections, liaison DNS/socket au réseau Cast et meilleure gestion des headers Fsvid. - Sécurise le bridge WebView avec contrôle de l’origine top-frame, capacités propres à chaque document et invalidation lors des navigations. - Ajoute le Picture-in-Picture Android avec lecture/pause et seek de 10 secondes depuis la fenêtre flottante. - Maintient désormais l’écran allumé uniquement pendant une lecture active. - Publie l’application Android 2.5.7, build 16, avec le nouvel APK et ses métadonnées d’intégrité. API, base de données et paiements - Remplace 13 migrations SQL isolées par un manifeste de schéma MySQL centralisé, additif et capable de détecter les divergences de structure. - Ajoute le workflow db:init avec prévisualisation, dry-run, verrou MySQL, confirmations pour les tables Wrapped et couverture de tests dédiée. - Rend configurables les limites du pool MySQL et fiabilise les advisory locks. - Revoit entièrement PayGate : références de callback séparées, nonces, validation des wallets et transactions, états monotones, déduplication des paiements et protection contre les courses ou réutilisations de txid. - Renforce l’annulation, la réconciliation et la livraison idempotente des factures VIP, avec réponses no-store sur les routes sensibles. - Distingue les JWT invalides des indisponibilités MySQL afin de renvoyer correctement 401 ou 503 sans provoquer de déconnexions inutiles. - Utilise ZZAPI/Claude Haiku pour la modération, avec OpenRouter/DeepSeek en solution de secours. - Améliore la gestion HLS et Range dans proxiesembed. Frontend, profils et infrastructure - Protège la sélection et l’hydratation des profils contre les réponses concurrentes, les intentions obsolètes et l’effacement accidentel du stockage local. - Rend VITE_SITE_URL obligatoire et l’injecte dans le runtime, les données structurées, le service worker, robots.txt et sitemap.xml. - Optimise les probes réseau du service worker avec cache court et single-flight. - Charge dynamiquement les traductions anglaises, plusieurs routes secondaires et les données Search/TMDB uniquement lorsqu’elles deviennent nécessaires. - Ajoute des caches mémoire/session pour les images et contenus TMDB et réduit les animations, calculs et rendus hors écran. - Ajoute la page publique des frais, les coins carrés persistants, une nouvelle intro Movix, les avertissements de publicités adultes et de nombreux textes Cast, qualité, Live TV et sources. - Met à jour les extensions Chrome 1.3.12 et Firefox 1.5.9, ainsi que le userscript, avec la nouvelle extraction SeekStreaming et la préparation Cast. - Ajoute une large couverture de tests Kotlin et JavaScript pour le Cast, le proxy média, le PiP, le bridge, la base de données, PayGate et le lecteur. - Ajoute bootstrap-icons et patch-package, régénère les lockfiles npm et actualise la documentation d’installation et d’initialisation MySQL. Co-authored-by: GPT-5.6 Sol <noreply@openai.com>
789 lines
23 KiB
JavaScript
789 lines
23 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: {},
|
|
COFLIX_BASE_URL: (
|
|
process.env.COFLIX_BASE_URL || "https://coflix.esq"
|
|
).replace(/\/$/, ""),
|
|
getFromCacheNoExpiration: async () => null,
|
|
saveToCache: async () => false,
|
|
formatCoflixError: (error) =>
|
|
error && error.message ? error.message : String(error),
|
|
};
|
|
|
|
function configure(injected) {
|
|
Object.assign(deps, injected);
|
|
}
|
|
|
|
// Kill switch — utile quand le domaine Coflix tombe / redirige vers leur Telegram.
|
|
// COFLIX_ENABLED=false coupe recherches, fetchs et refresh background ;
|
|
// le cache disque existant reste servi tel quel.
|
|
const COFLIX_ENABLED = !["false", "0"].includes(
|
|
String(process.env.COFLIX_ENABLED ?? "true").toLowerCase(),
|
|
);
|
|
|
|
// ---- 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;
|
|
}
|
|
|
|
// Deduit le post_type Coflix a partir du segment d'URL de la fiche.
|
|
// Le nouveau theme range les contenus sous /film|serie|animes|drames/.
|
|
function coflixPostTypeFromUrl(url) {
|
|
if (!url) return null;
|
|
if (/\/film\//i.test(url)) return "movies";
|
|
if (/\/serie\//i.test(url)) return "series";
|
|
if (/\/animes\//i.test(url)) return "animes";
|
|
if (/\/drames\//i.test(url)) return "doramas";
|
|
return null; // collections, genres, tutos... -> ignore
|
|
}
|
|
|
|
// Fonction pour rechercher un contenu sur Coflix par titre.
|
|
// Le nouveau theme n'a plus /suggest.php : on scrape la page de recherche
|
|
// WordPress /?s=<query> et sa grille .md-manga-card.
|
|
async function searchCoflixByTitle(title, mediaType, releaseYear) {
|
|
if (!COFLIX_ENABLED) return [];
|
|
try {
|
|
const normalizedTitle = normalizeCoflixQuery(title);
|
|
|
|
const response = await deps.axiosCoflixRequest({
|
|
method: "get",
|
|
url: `/?s=${encodeURIComponent(normalizedTitle)}`,
|
|
});
|
|
|
|
if (typeof response.data !== "string") {
|
|
console.error(
|
|
`La reponse de recherche Coflix n'est pas du HTML: ${deps.formatCoflixError(response.data)}`,
|
|
);
|
|
return [];
|
|
}
|
|
|
|
const $ = cheerio.load(response.data);
|
|
|
|
const results = [];
|
|
$(".md-manga-card").each((i, el) => {
|
|
const $card = $(el);
|
|
|
|
const cardTitle = $card.find(".md-manga-card-name").first().text().trim();
|
|
|
|
// URL de la fiche : on privilegie un lien /film|serie|animes|drames/.
|
|
let href = $card
|
|
.find("a[href]")
|
|
.filter((j, a) => !!coflixPostTypeFromUrl($(a).attr("href")))
|
|
.first()
|
|
.attr("href");
|
|
if (!href) href = $card.find("a[href]").first().attr("href");
|
|
|
|
const postType = coflixPostTypeFromUrl(href);
|
|
if (!cardTitle || !href || !postType) return; // skip collections & bruit
|
|
|
|
const yearText = $card.find(".md-card-badge.year").first().text().trim();
|
|
const resultYear = yearText ? parseInt(yearText, 10) : null;
|
|
|
|
results.push({
|
|
title: cardTitle,
|
|
url: href,
|
|
similarity: calculateTitleSimilarity(title, cardTitle),
|
|
year: Number.isNaN(resultYear) ? null : resultYear,
|
|
id: null,
|
|
excerpt: "",
|
|
post_type: postType,
|
|
rating: null,
|
|
});
|
|
});
|
|
|
|
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 strictYear = filteredResults.filter((r) => r.year === releaseYear);
|
|
// Les fiches series du nouveau theme n'affichent pas d'annee (seuls les
|
|
// films ont un badge annee). Sans match strict, on retombe sur les fiches
|
|
// sans annee connue et on tranche a la similarite du titre.
|
|
const yearMatchedResults = strictYear.length
|
|
? strictYear
|
|
: filteredResults.filter((r) => r.year == null);
|
|
|
|
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-site 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);
|
|
}
|
|
|
|
// Extrait la liste des embeds lecteurvideo + le token JWT d'une page Coflix
|
|
// (film ou episode). Le nouveau theme embarque, dans un <script> inline :
|
|
// var cfPlayerToken = "<jwt>";
|
|
// var cfServers = [{"nombre":"...","embed_url":"https://lecteurvideo.com/embed.php?id=..","idioma":".."}];
|
|
// Le token (lie au post, ~30 min) doit etre passe a l'embed via ?t=<token>.
|
|
function parseCoflixPlayers(html) {
|
|
const tokenMatch = html.match(/var\s+cfPlayerToken\s*=\s*["']([^"']*)["']/);
|
|
const token = tokenMatch ? tokenMatch[1] : "";
|
|
|
|
let servers = [];
|
|
const serversMatch = html.match(/var\s+cfServers\s*=\s*(\[[\s\S]*?\])\s*;/);
|
|
if (serversMatch) {
|
|
try {
|
|
servers = JSON.parse(serversMatch[1]);
|
|
} catch (e) {
|
|
servers = [];
|
|
}
|
|
}
|
|
|
|
const embeds = [];
|
|
for (const s of Array.isArray(servers) ? servers : []) {
|
|
if (!s || !s.embed_url) continue;
|
|
let embed = String(s.embed_url).trim();
|
|
if (token) {
|
|
embed +=
|
|
(embed.includes("?") ? "&" : "?") + "t=" + encodeURIComponent(token);
|
|
}
|
|
embeds.push({ embed_url: embed, idioma: s.idioma || "", nombre: s.nombre || "" });
|
|
}
|
|
|
|
return { token, embeds };
|
|
}
|
|
|
|
// Recupere les player_links (uqload/voe/lecteur6/...) depuis une page embed
|
|
// lecteurvideo.com. La page liste les hosts via `li[onclick="showVideo('<base64>')"]`.
|
|
async function scrapeLecteurVideoEmbed(embedUrl, tag) {
|
|
const playerLinks = [];
|
|
try {
|
|
const iframePageResponse = await deps.axiosLecteurVideoRequest({
|
|
method: "get",
|
|
url: embedUrl,
|
|
});
|
|
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 ${tag}] ⚠️ Aucun playerItem — iframe: ${embedUrl}, 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]) {
|
|
let decodedUrl;
|
|
try {
|
|
decodedUrl = Buffer.from(base64Match[1], "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 ${tag}] ❌ Erreur extraction player: ${errorCode}`,
|
|
);
|
|
}
|
|
});
|
|
} catch (iframePageError) {
|
|
const errorCode =
|
|
iframePageError.response?.status || iframePageError.code || "unknown";
|
|
console.error(
|
|
`[COFLIX ${tag}] ❌ Erreur requête iframe ${embedUrl} — code: ${errorCode}, message: ${iframePageError.message}`,
|
|
);
|
|
}
|
|
return playerLinks;
|
|
}
|
|
|
|
// Extrait la liste des embeds jouables d'une page Coflix deja chargee (cheerio $
|
|
// + html brut). Priorite au bloc `cfServers`, fallback sur l'iframe live
|
|
// #cfPlayerFrame (deja tokenise) puis toute iframe non-coflix.
|
|
function extractCoflixEmbeds($, html) {
|
|
const { embeds } = parseCoflixPlayers(html);
|
|
if (embeds.length) return embeds;
|
|
|
|
const iframeSrc =
|
|
$("#cfPlayerFrame").attr("src") ||
|
|
$("iframe.cf-player-frame").attr("src") ||
|
|
$("iframe").attr("src");
|
|
if (iframeSrc && !/coflix\./i.test(iframeSrc)) {
|
|
return [{ embed_url: iframeSrc, idioma: "", nombre: "" }];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
if (!COFLIX_ENABLED) {
|
|
return cachedData || { iframe_src: null, player_links: [] };
|
|
}
|
|
|
|
const relativePath = url.replace(
|
|
/^https?:\/\/(?:www\.)?coflix\.[^/]+/i,
|
|
"",
|
|
);
|
|
const response = await deps.axiosCoflixRequest({
|
|
method: "get",
|
|
url: relativePath,
|
|
});
|
|
const $ = cheerio.load(response.data);
|
|
|
|
const embeds = extractCoflixEmbeds($, response.data);
|
|
if (embeds.length === 0) {
|
|
console.log(`Aucun serveur (cfServers/iframe) trouve pour l'URL ${url}`);
|
|
}
|
|
|
|
const iframeSrc = embeds.length ? embeds[0].embed_url : null;
|
|
const playerLinks = [];
|
|
for (const embed of embeds) {
|
|
const links = await scrapeLecteurVideoEmbed(embed.embed_url, "MOVIE");
|
|
playerLinks.push(...links);
|
|
}
|
|
|
|
const result = {
|
|
iframe_src:
|
|
iframeSrc && !/coflix\./i.test(iframeSrc) ? 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: [] };
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
if (!COFLIX_ENABLED) {
|
|
return { seasons: [], current_episode: null };
|
|
}
|
|
|
|
// Le nouveau theme n'expose plus la liste des saisons/episodes de facon
|
|
// scrappable (et le front recupere les saisons via TMDB, pas via Coflix).
|
|
// On construit directement le permalien d'episode /episode/<slug>-<S>x<E>/
|
|
// (confirme par ex. /episode/franky-2x1/ pour la serie /serie/franky/).
|
|
const slugMatch = url.match(/\/(?:serie|animes|drames)\/([^/]+)/i);
|
|
const seriesSlug = slugMatch ? slugMatch[1] : "";
|
|
|
|
if (!seriesSlug || !episodeNumber) {
|
|
return { seasons: [], current_episode: null };
|
|
}
|
|
|
|
const base = (deps.COFLIX_BASE_URL || "https://coflix.esq").replace(
|
|
/\/$/,
|
|
"",
|
|
);
|
|
const episodeUrl = `${base}/episode/${seriesSlug}-${seasonNumber}x${episodeNumber}/`;
|
|
|
|
let episodeTitle = "";
|
|
let episodeIframeSrc = null;
|
|
const episodePlayerLinks = [];
|
|
|
|
try {
|
|
const episodePageResponse = await deps.axiosCoflixRequest({
|
|
method: "get",
|
|
url: episodeUrl,
|
|
});
|
|
const episodePage$ = cheerio.load(episodePageResponse.data);
|
|
|
|
episodeTitle =
|
|
episodePage$("h1.cf-movie-title").first().text().trim() ||
|
|
episodePage$("article header h1").first().text().trim();
|
|
|
|
const embeds = extractCoflixEmbeds(episodePage$, episodePageResponse.data);
|
|
episodeIframeSrc = embeds.length ? embeds[0].embed_url : null;
|
|
for (const embed of embeds) {
|
|
const links = await scrapeLecteurVideoEmbed(embed.embed_url, "TV");
|
|
episodePlayerLinks.push(...links);
|
|
}
|
|
} catch (episodeError) {
|
|
console.error(
|
|
`Erreur lors de la recuperation des donnees de l'episode: ${deps.formatCoflixError(episodeError)}`,
|
|
);
|
|
return { seasons: [], current_episode: null };
|
|
}
|
|
|
|
// `seasons` n'est utilise cote back que comme garde-fou "resultat non vide"
|
|
// (isEmptyResult dans tmdb.js) : on ne le remplit que si l'episode a des liens.
|
|
const hasPlayers = episodePlayerLinks.length > 0;
|
|
const seasons = hasPlayers
|
|
? [
|
|
{
|
|
season_number: seasonNumber,
|
|
name: `Saison ${seasonNumber}`,
|
|
episodes: [],
|
|
},
|
|
]
|
|
: [];
|
|
|
|
const result = {
|
|
seasons: seasons,
|
|
current_episode: {
|
|
season_number: seasonNumber,
|
|
episode_number: episodeNumber,
|
|
title: episodeTitle,
|
|
iframe_src:
|
|
episodeIframeSrc && !/coflix\./i.test(episodeIframeSrc)
|
|
? episodeIframeSrc
|
|
: null,
|
|
player_links: episodePlayerLinks,
|
|
},
|
|
};
|
|
|
|
if (hasPlayers) {
|
|
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,
|
|
COFLIX_ENABLED,
|
|
normalizeCoflixQuery,
|
|
searchCoflixByTitle,
|
|
calculateTitleSimilarity,
|
|
filterEmmmmbedReaders,
|
|
getMovieDataFromCoflix,
|
|
getTvDataFromCoflix,
|
|
};
|