186 lines
7.9 KiB
TypeScript
186 lines
7.9 KiB
TypeScript
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 = /<div class="cover_global"[^>]*>([\s\S]*?)(?=<div class="cover_global"|$)/g;
|
|
let match: RegExpExecArray | null;
|
|
|
|
while ((match = coverRegex.exec(html)) !== null) {
|
|
const block = match[1]!;
|
|
|
|
const titleMatch = block.match(/<div class="cover_infos_title"[^>]*>\s*<a href="([^"]+)"[^>]*>\s*([^<]+)/);
|
|
if (!titleMatch) continue;
|
|
|
|
const href = titleMatch[1]!.trim();
|
|
const title = titleMatch[2]!.trim();
|
|
|
|
const imgMatch = block.match(/<img class="mainimg"[^>]*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';
|
|
}
|
|
|
|
results.push({ title, image, hrefPath: href, year: null, 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 color=red>([^<]+)<\/font>/g;
|
|
let releaseMatch: RegExpExecArray | null;
|
|
while ((releaseMatch = releaseRegex.exec(html)) !== null) {
|
|
releaseNames.push(releaseMatch[1]!.trim());
|
|
}
|
|
|
|
const sections = html.split(/<img src='\/img\/([^']+)'/);
|
|
|
|
for (let i = 1; i < sections.length; i += 2) {
|
|
const hostImg = sections[i]!;
|
|
const hostName = hostImg.replace('.png', '').replace('.jpg', '').replace('.webp', '');
|
|
const sectionHtml = sections[i + 1] || '';
|
|
|
|
const linkRegex = /<a class="btnToLink"[^>]*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|uptobox|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(/(?:<b>|<strong>)?(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<string>();
|
|
const seenSubs = new Set<string>();
|
|
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]*?)(?:<h3|<\/div>|<div[^>]*class="postinfo")/gi;
|
|
let sSectionMatch: RegExpExecArray | null;
|
|
while ((sSectionMatch = sectionRegex.exec(html)) !== null) {
|
|
const type = sSectionMatch[1].toLowerCase();
|
|
const seasonBlock = sSectionMatch[2]!;
|
|
const seasonRegex = /<a[^>]*href="([^"]+)"[^>]*><span class="otherquality">([\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(/<a\s+[^>]*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;
|
|
}
|
|
|