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*
\s*([^<\n]+?)\s*
\s*([^<\n]+?)\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 = //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(items: T[]): T[] { const seen = new Set(); 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 { if (url.startsWith('http')) return url; const cleanedBase = baseUrl.replace(/\/$/, ''); return cleanedBase + '/' + url.replace(/^\//, ''); } /** * Format résultats de recherche :
* puis
*/ export function parseSearchResults(html: string, baseUrl: string): SearchResult[] { const results: SearchResult[] = []; const blockRegex = /\s*]+src="([^"]+)"[^>]*>[\s\S]*?\s*]*>([\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; results.push({ title, year: null, image, hrefPath: href, type: detectType(hrefRaw), source: 'freetel', }); } return deduplicateByTitle(results); } /** * Format nouveautés (/page/1.html) : Titre */ export function parseTrendingResults(html: string, baseUrl: string): SearchResult[] { const results: SearchResult[] = []; const blockRegex = /]*data-tip-b64="[^"]+"[^>]*>\s*]+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); results.push({ title, year: null, image, hrefPath: absUrl(hrefRaw, baseUrl), type: detectType(hrefRaw), source: 'freetel', }); } return deduplicateByTitle(results); } /** * Parse une fiche (film ou série). * - Film : dans la section #link, précédé d'un

HOST

* - Série : (à résoudre via resolveLink) */ export function parseContentHTML(html: string, isSeries: boolean): ContentLinks { const links: VideoLink[] = []; if (isSeries) { const episodeRegex = /]+name="lien"\s+value="(https?:\/\/liens\.free-telecharger\.cam\/[^"]+)"/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

, URL dans ) const meta = parseFilmMetadata(html); const sectionMatch = html.match(/]*>\s*([A-Za-z0-9-]+)\s*<\/p>[\s\S]{0,800}?]+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 : avec contenant [HOST] et . */ export function parseEpisodeLinks(html: string): { host: string; url: string }[] { const out: { host: string; url: string }[] = []; const tableMatch = html.match(/]*class="gridtable"[\s\S]*?<\/table>/i); if (!tableMatch) return out; const rows = tableMatch[0].match(//gi) || []; for (const row of rows) { const hostMatch = row.match(/\[([^\]]+)\]/); const aMatch = row.match(/]*href\s*=\s*["']?([^"'\s>]+)/i); if (hostMatch && aMatch) { out.push({ host: hostMatch[1]!.toLowerCase().trim(), url: aMatch[1]!.trim(), }); } } return out; }