import { SearchResult, MediaType, ContentLinks, VideoLink } from '../../src/types/source.js'; /** * Parse le HTML de résultats de recherche ZT. */ export function parseSearchHTML(html: string, baseUrl: string | undefined): SearchResult[] { const results: SearchResult[] = []; const coverRegex = /
]*>([\s\S]*?)(?=
]*>\s*]*>\s*([^<]+)/); if (!titleMatch) continue; const href = titleMatch[1]!.trim(); const title = titleMatch[2]!.trim(); const imgMatch = block.match(/]*src="([^"]+)"/); let image = imgMatch ? imgMatch[1]! : null; if (image && image.startsWith('/') && baseUrl) { image = baseUrl + image; } let type: 'movie' | 'series' | 'anime' = 'movie'; if (href.includes('/telecharger-serie/') || href.includes('/serie-')) { type = 'series'; } else if (href.includes('/animes')) { type = 'anime'; } let year: string | null = null; const yearMatch = title.match(/\(\s*(\d{4})\s*\)/) || href.match(/-(\d{4})-/); if (yearMatch) { year = yearMatch[1]; } results.push({ title, image, hrefPath: href, year, type, source: 'zt' }); } return results; } /** * Parse le HTML d'une page de contenu ZT pour en extraire les liens et saisons. */ export function parseContentHTML(html: string): ContentLinks { const links: VideoLink[] = []; const releaseNames: string[] = []; const releaseRegex = /([^<]+)<\/font>/g; let releaseMatch: RegExpExecArray | null; while ((releaseMatch = releaseRegex.exec(html)) !== null) { releaseNames.push(releaseMatch[1]!.trim()); } const sections = html.split(/]*href="([^"]+)"[^>]*>([^<]+)<\/a>/g; let linkMatch: RegExpExecArray | null; while ((linkMatch = linkRegex.exec(sectionHtml)) !== null) { const zoneursUrl = linkMatch[1]!; const label = linkMatch[2]!.trim(); // Extraire la taille depuis le label : "NOM.FICHIER (11.5 GO)" → "11.5 GO" const sizeRegex = /\s*\(([\d.,]+\s*(?:go|gb|mo|mb|ko|kb|to|tb))\)/i; let sizeMatch = label.match(sizeRegex); let size = sizeMatch ? sizeMatch[1]!.trim().toUpperCase() : undefined; // Si non trouvé dans le label, on cherche dans le nom de la release (qualité) if (!size && releaseNames.length > 0) { const qualityMatch = releaseNames[0].match(sizeRegex); if (qualityMatch) size = qualityMatch[1]!.trim().toUpperCase(); } // Nettoyer le label pour enlever la taille const cleanedLabel = label.replace(sizeRegex, "").trim(); // On n'utilise le label comme "épisode" que si c'est un vrai nom de fichier/épisode (pas juste "Télécharger") const isGenericLabel = /^(t\u00e9l\u00e9charger|download|cliquez ici|lien|turbobit|1fichier|rapidgator|nitroflare|send.now)/i.test(cleanedLabel); let episode = (!isGenericLabel && cleanedLabel.length > 3) ? cleanedLabel : undefined; // SI le label est générique, on cherche un texte juste avant (ex: "Episode 1") if (isGenericLabel || !episode) { const index = linkMatch.index; const prevHtml = sectionHtml.substring(Math.max(0, index - 100), index); // Cherche "Episode X", "Saison complète", etc. const epMatch = prevHtml.match(/(?:|)?(Episode\s*\d+|Saison\s*compl\u00e8te)(?:<\/b>|<\/strong>)?/i); if (epMatch) { episode = epMatch[1].trim(); } } let quality = releaseNames.length > 0 ? releaseNames[0] : 'Inconnu'; if (quality.match(sizeRegex)) quality = quality.replace(sizeRegex, ''); let langs: string[] = []; let subs: string[] = []; const textToScan = `${quality} ${cleanedLabel}`; const langMatch = textToScan.match(/\b(MULTI(?:LANGUES?)?|TRUEFRENCH|FRENCH|VOSTFR|VFF|VF)\b/gi); if (langMatch) { const seenLangs = new Set(); const seenSubs = new Set(); langMatch.forEach(l => { const up = l.toUpperCase(); if (up.includes('VOSTFR')) { seenLangs.add('VOSTFR'); seenSubs.add('French'); } else if (up.includes('TRUEFRENCH')) seenLangs.add('TrueFrench'); else if (up.includes('FRENCH') || up === 'VF' || up === 'VFF') seenLangs.add('French'); else if (up.includes('MULTI')) { seenLangs.add('MULTI'); seenSubs.add('Multi'); } }); langs = Array.from(seenLangs); subs = Array.from(seenSubs); quality = quality.replace(/\b(MULTI(?:LANGUES?)?|TRUEFRENCH|FRENCH|VOSTFR|VFF|VF)\b/gi, '').trim(); } quality = quality.replace(/[\(\)\[\]\-]+$/g, '').replace(/[\(\)\[\]]/g, '').replace(/\s+/g, ' ').trim(); if (!quality || quality.toLowerCase() === 'inconnu') quality = 'WEB'; links.push({ id: zoneursUrl, host: hostName, label: cleanedLabel, url: null, size, quality: quality, langs, subs, episode: episode, }); } } const relatedSeasons: { href: string; label: string }[] = []; const relatedQualities: { href: string; label: string }[] = []; // Chercher toutes les sections "également disponibles" const sectionRegex = /(Saisons?|Qualit(?:é|e)s?)\s*également disponibles[\s\S]*?<\/h3>([\s\S]*?)(?:|]*class="postinfo")/gi; let sSectionMatch: RegExpExecArray | null; while ((sSectionMatch = sectionRegex.exec(html)) !== null) { const type = sSectionMatch[1].toLowerCase(); const seasonBlock = sSectionMatch[2]!; const seasonRegex = /]*href="([^"]+)"[^>]*>([\s\S]*?)<\/span><\/a>/g; let sMatch: RegExpExecArray | null; while ((sMatch = seasonRegex.exec(seasonBlock)) !== null) { const label = sMatch[2]!.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim(); const href = sMatch[1]!.trim(); if (type.includes('saison')) { if (!relatedSeasons.find(rs => rs.href === href)) { relatedSeasons.push({ href, label }); } } else { if (!relatedQualities.find(rs => rs.href === href)) { relatedQualities.push({ href, label }); } } } } return { links, releaseNames, relatedSeasons, relatedQualities }; } /** * Extrait le lien final déverrouillé de la page HTML de ZTPROTECT. */ export function extractLinkFromZtProtect(html: string): string | null { // 1. Essayer de trouver la valeur de l'input result-input let match = html.match(/class="result-input"\s+value="([^"]+)"/i); if (match && match[1]) return match[1]; // 2. Essayer de trouver l'attribut href du bouton de succès match = html.match(/]*href="([^"]+)"[^>]*class="[^"]*btn-success[^"]*"/i); if (match && match[1]) return match[1]; match = html.match(/class="[^"]*btn-success[^"]*"\s+[^>]*href="([^"]+)"/i); if (match && match[1]) return match[1]; return null; }