214 lines
8.0 KiB
TypeScript
214 lines
8.0 KiB
TypeScript
import { SearchResult, ContentLinks, VideoLink } from '../../src/types/source.js';
|
|
|
|
interface FilmMetadata {
|
|
quality?: string;
|
|
size?: string;
|
|
langs?: string[];
|
|
}
|
|
|
|
function parseFilmMetadata(html: string): FilmMetadata {
|
|
const meta: FilmMetadata = {};
|
|
const q = html.match(/Qualit[ée][^:]*:\s*<\/b>\s*([^<\n]+?)\s*<br/i);
|
|
if (q) meta.quality = q[1]!.trim();
|
|
const t = html.match(/Taille[^:]*:\s*<\/b>\s*([^<\n]+?)\s*<br/i);
|
|
if (t) meta.size = t[1]!.trim();
|
|
const l = html.match(/Langue[^:]*:\s*<\/b>\s*([^<\n]+?)\s*<br/i);
|
|
if (l) meta.langs = l[1]!.trim().split(/[,\/]/).map(s => s.trim()).filter(Boolean);
|
|
return meta;
|
|
}
|
|
|
|
/**
|
|
* Extrait les autres versions/qualités disponibles pour le même film.
|
|
* Section "Autres versions disponibles pour ..."
|
|
*/
|
|
export function parseOtherVersions(html: string, baseUrl: string): { label: string; value: string }[] {
|
|
const out: { label: string; value: string }[] = [];
|
|
const sectionMatch = html.match(/Autres versions disponibles[\s\S]+?<\/div>\s*<\/div>/i);
|
|
if (!sectionMatch) return out;
|
|
const linkRegex = /<a\s+href="([^"]+)"[\s\S]*?🎞️\s*([^<]+?)<\/a>/gi;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = linkRegex.exec(sectionMatch[0])) !== null) {
|
|
const href = absUrl(m[1]!, baseUrl);
|
|
const label = m[2]!.replace(/\s+/g, ' ').trim();
|
|
if (!out.find(o => o.value === href)) out.push({ label, value: href });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
|
|
function normalizeTitle(title: string): string {
|
|
return title
|
|
.toLowerCase()
|
|
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
.replace(/\b(web-?dl|web-?rip|blu-?ray|full-?blu-?ray|hdtv|hdrip|dvdrip|bdrip|hdlight|ultra-?hdlight|truefrench|french|multi(?:langues?)?|vff|vfq|vfi|vf|vostfr|english|hdts|cam|ts|r5|dvdscr|x264|x265|h\.?264|h\.?265|hevc)\b/g, '')
|
|
.replace(/\b(720p|1080p|2160p|4k|uhd|3d|sd|hd)\b/g, '')
|
|
.replace(/\(\s*\d{4}\s*\)/g, '')
|
|
.replace(/-\s*saison\s*\d+/gi, '')
|
|
.replace(/[^a-z0-9]/g, '');
|
|
}
|
|
|
|
function deduplicateByTitle<T extends { title: string }>(items: T[]): T[] {
|
|
const seen = new Set<string>();
|
|
return items.filter(it => {
|
|
const k = normalizeTitle(it.title);
|
|
if (!k || seen.has(k)) return false;
|
|
seen.add(k);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
function detectType(href: string): 'movie' | 'series' | 'anime' {
|
|
if (/saison|pack-series|series-(vf|vostfr|terminee)/i.test(href)) return 'series';
|
|
if (/animes?/i.test(href)) return 'anime';
|
|
return 'movie';
|
|
}
|
|
|
|
function absUrl(url: string, baseUrl: string): string {
|
|
let path = url;
|
|
if (url.startsWith('http')) {
|
|
try {
|
|
const u = new URL(url);
|
|
path = u.pathname + u.search + u.hash;
|
|
} catch {
|
|
return url;
|
|
}
|
|
}
|
|
const cleanedBase = baseUrl.replace(/\/$/, '');
|
|
return cleanedBase + '/' + path.replace(/^\//, '');
|
|
}
|
|
|
|
/**
|
|
* Format résultats de recherche : <div class="image-container"><img/></div>
|
|
* puis <div class="content"><div class="titre1"><A href="...">Titre</A></div>
|
|
*/
|
|
export function parseSearchResults(html: string, baseUrl: string): SearchResult[] {
|
|
const results: SearchResult[] = [];
|
|
const blockRegex = /<div\s+class="image-container">\s*<img[^>]+src="([^"]+)"[^>]*>[\s\S]*?<div\s+class="titre1">\s*<a\s+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = blockRegex.exec(html)) !== null) {
|
|
const image = absUrl(m[1]!, baseUrl);
|
|
const hrefRaw = m[2]!;
|
|
const href = absUrl(hrefRaw, baseUrl);
|
|
const title = m[3]!.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
if (!title) continue;
|
|
let year: string | null = null;
|
|
const yearMatch = title.match(/\(\s*(\d{4})\s*\)/) || hrefRaw.match(/-(\d{4})-/);
|
|
if (yearMatch) {
|
|
year = yearMatch[1];
|
|
}
|
|
|
|
results.push({
|
|
title,
|
|
year,
|
|
image,
|
|
hrefPath: href,
|
|
type: detectType(hrefRaw),
|
|
source: 'freetel',
|
|
});
|
|
}
|
|
return deduplicateByTitle(results);
|
|
}
|
|
|
|
/**
|
|
* Format nouveautés (/page/1.html) : <a href="..." data-tip-b64="..."><img alt="Titre" src="..."/></a>
|
|
*/
|
|
export function parseTrendingResults(html: string, baseUrl: string): SearchResult[] {
|
|
const results: SearchResult[] = [];
|
|
const blockRegex = /<a\s+href="((?:films?-|saison-|pack-series|series-)[^"]+\.html)"[^>]*data-tip-b64="[^"]+"[^>]*>\s*<img\s+alt="([^"]+)"[^>]+src="([^"]+)"/gi;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = blockRegex.exec(html)) !== null) {
|
|
const hrefRaw = m[1]!;
|
|
const title = m[2]!.trim();
|
|
const image = absUrl(m[3]!, baseUrl);
|
|
let year: string | null = null;
|
|
const yearMatch = title.match(/\(\s*(\d{4})\s*\)/) || hrefRaw.match(/-(\d{4})-/);
|
|
if (yearMatch) {
|
|
year = yearMatch[1];
|
|
}
|
|
|
|
results.push({
|
|
title,
|
|
year,
|
|
image,
|
|
hrefPath: absUrl(hrefRaw, baseUrl),
|
|
type: detectType(hrefRaw),
|
|
source: 'freetel',
|
|
});
|
|
}
|
|
return deduplicateByTitle(results);
|
|
}
|
|
|
|
/**
|
|
* Parse une fiche (film ou série).
|
|
* - Film : <input name="lien" value="https://turbobit.net/..."> dans la section #link, précédé d'un <p>HOST</p>
|
|
* - Série : <input name="lien" value="https://liens.free-telecharger.cam/SLUG-episode_N"> (à résoudre via resolveLink)
|
|
*/
|
|
export function parseContentHTML(html: string, isSeries: boolean): ContentLinks {
|
|
const links: VideoLink[] = [];
|
|
|
|
if (isSeries) {
|
|
const episodeRegex = /<input[^>]+name="lien"\s+value="(https?:\/\/liens\.free-telecharger\.[a-z]+\/[^"]+)"/gi;
|
|
let m: RegExpExecArray | null;
|
|
let idx = 0;
|
|
while ((m = episodeRegex.exec(html)) !== null) {
|
|
const url = m[1]!;
|
|
const epMatch = url.match(/episode_(\d+|final|complet)/i);
|
|
const episode = epMatch ? epMatch[1] : null;
|
|
links.push({
|
|
id: url,
|
|
host: 'multi',
|
|
label: episode ? `Épisode ${episode}` : `Lien ${idx + 1}`,
|
|
episode: episode || undefined,
|
|
quality: 'multi',
|
|
url: null,
|
|
});
|
|
idx++;
|
|
}
|
|
} else {
|
|
// Films : section #link contient des blocs (Host name dans <p>, URL dans <input hidden lien>)
|
|
const meta = parseFilmMetadata(html);
|
|
const sectionMatch = html.match(/<div\s+id="link"[\s\S]+/);
|
|
const sec = sectionMatch ? sectionMatch[0] : html;
|
|
const pairRegex = /<p[^>]*>\s*([A-Za-z0-9-]+)\s*<\/p>[\s\S]{0,800}?<input[^>]+name="lien"\s+value="([^"]+)"/gi;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = pairRegex.exec(sec)) !== null) {
|
|
const host = m[1]!.trim();
|
|
const url = m[2]!;
|
|
if (/free-telecharger|trustzone|get-trust-zone/i.test(url)) continue;
|
|
links.push({
|
|
id: url,
|
|
host: host.toLowerCase(),
|
|
label: host,
|
|
quality: meta.quality || 'Inconnu',
|
|
size: meta.size,
|
|
langs: meta.langs,
|
|
url: url,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { links };
|
|
}
|
|
|
|
/**
|
|
* Parse la page intermédiaire d'un épisode (liens.free-telecharger.cam/...).
|
|
* Structure : <table class="gridtable"> avec <tr> contenant [HOST] et <a href="URL">.
|
|
*/
|
|
export function parseEpisodeLinks(html: string): { host: string; url: string }[] {
|
|
const out: { host: string; url: string }[] = [];
|
|
const tableMatch = html.match(/<table[^>]*class="gridtable"[\s\S]*?<\/table>/i);
|
|
if (!tableMatch) return out;
|
|
const rows = tableMatch[0].match(/<tr[\s\S]*?<\/tr>/gi) || [];
|
|
for (const row of rows) {
|
|
const hostMatch = row.match(/\[([^\]]+)\]/);
|
|
const aMatch = row.match(/<a\s+[^>]*href\s*=\s*["']?([^"'\s>]+)/i);
|
|
if (hostMatch && aMatch) {
|
|
out.push({
|
|
host: hostMatch[1]!.toLowerCase().trim(),
|
|
url: aMatch[1]!.trim(),
|
|
});
|
|
}
|
|
}
|
|
return out;
|
|
}
|