MovixOpenSource/API/Mainapi/routes/coflix.js
Movix cbc3f1dad9 Merge depuis le repo privé après tests
Corrections de vulnérabilités sur les commentaires (IDOR)
Rajouts de ratelimit sur les commentaires
Correction de pas de son sur hlsplayer
Livetvplayer détecte le type du flux automatiquement
Amélioration du scraper darkiworld (très chiant)
Correction du détection de l'extension

Correction de la vérification du VIP sur server.py (proxy)
Correction du scraper seekstreaming proxy

Nouvelle url streamonsport et coflix
Correction du scraper fstream (get seasons) car la route fonctionne plus sur leur site
Correction du scraper francetv
Ajout de hydrackerBatch (c'étais pour des tests)
2026-05-03 12:47:41 +02:00

866 lines
24 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) {
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.date")
? `${episode.links}`
: episode.links;
}
}
}
if (!episodeUrl) {
episodeUrl = `https://coflix.date/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,
};