176 lines
7.3 KiB
TypeScript
176 lines
7.3 KiB
TypeScript
import { SearchResult, ContentLinks, VideoLink } from '../../src/types/source.js';
|
|
|
|
|
|
function decodeFnMeta(url: string): { quality?: string; langs?: string[] } {
|
|
try {
|
|
const m = url.match(/[?&]fn=([^&]+)/);
|
|
if (!m) return {};
|
|
const decoded = Buffer.from(decodeURIComponent(m[1]!), 'base64').toString('utf-8');
|
|
const qm = decoded.match(/\[([^\]]+)\]/);
|
|
const quality = qm ? qm[1]!.trim() : undefined;
|
|
// Tout après " - " jusqu'à la fin (typiquement la langue : FRENCH, MULTI, VOSTFR…)
|
|
const lm = decoded.match(/-\s+([A-Za-z]+(?:\s+[A-Za-z]+)?)$/);
|
|
const langs = lm ? [lm[1]!.trim()] : undefined;
|
|
return { quality, langs };
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
function detectType(href: string): 'movie' | 'series' | 'anime' {
|
|
if (/[?&]p=serie\b|telecharger-serie|serie-/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.startsWith('/') ? url : '/' + url);
|
|
}
|
|
|
|
/**
|
|
* Strip suffixes de qualité/langue pour dedup par titre normalisé.
|
|
*/
|
|
function normalizeTitle(title: string): string {
|
|
return title
|
|
.toLowerCase()
|
|
.normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
.replace(/\b(web-?dl|web-?rip|blu-?ray|hdtv|hdrip|dvdrip|hdlight|truefrench|french|multi(?:langues?)?|vff|vf|vostfr|x264|x265|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;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* News utilise la structure DLE classique avec cover_global / cover_infos_title / mainimg
|
|
* sur la home/listing/recherche. On peut donc partager le parser de listing.
|
|
*/
|
|
export function parseListingHTML(html: string, baseUrl: string): SearchResult[] {
|
|
const results: SearchResult[] = [];
|
|
const coverRegex = /<div class="cover_global"[^>]*>([\s\S]*?)(?=<div class="cover_global"|$)/g;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = coverRegex.exec(html)) !== null) {
|
|
const block = m[1]!;
|
|
const titleMatch = block.match(/<div class="cover_infos_title"[^>]*>\s*<a href="([^"]+)"[^>]*>\s*([^<]+)/);
|
|
if (!titleMatch) continue;
|
|
const href = absUrl(titleMatch[1]!.trim(), baseUrl);
|
|
const title = titleMatch[2]!.trim();
|
|
const imgMatch = block.match(/<img class="mainimg"[^>]*src="([^"]+)"/);
|
|
const image = imgMatch ? absUrl(imgMatch[1]!, baseUrl) : null;
|
|
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,
|
|
year,
|
|
image,
|
|
hrefPath: href,
|
|
type: detectType(titleMatch[1]!),
|
|
source: 'ztnews',
|
|
});
|
|
}
|
|
return deduplicateByTitle(results);
|
|
}
|
|
|
|
/**
|
|
* Parse la fiche film/série de news.
|
|
* Structure dans <div class="postinfo">:
|
|
* <div style="color:#XXX">HOST_NAME</div>
|
|
* <a href="dl-protect.link/SLUG?fn=...&rl=a2">Télécharger</a> (film)
|
|
* <a href="dl-protect.link/SLUG?fn=...&rl=b2">Episode N</a> (série, plusieurs liens)
|
|
*/
|
|
export function parseContentHTML(html: string, isSeries: boolean): ContentLinks {
|
|
const links: VideoLink[] = [];
|
|
|
|
const postMatch = html.match(/<div class="postinfo">([\s\S]*?)<\/div>\s*<\/center>/);
|
|
if (!postMatch) return { links };
|
|
const post = postMatch[1]!;
|
|
|
|
// Découper par hôte : chaque hôte est marqué par <div style="font-weight:bold;color:#XXX">HOST</div>
|
|
const hostSplit = post.split(/<div\s+style="font-weight:bold;color:#[0-9a-fA-F]+">([^<]+)<\/div>/);
|
|
// hostSplit[0] = pre-section, puis alterne (HOST, BLOCK)
|
|
for (let i = 1; i < hostSplit.length; i += 2) {
|
|
const host = hostSplit[i]!.trim();
|
|
const block = hostSplit[i + 1] || '';
|
|
// Tous les <a href="dl-protect.link..."> dans cette section
|
|
const linkRegex = /<a[^>]+href="(https?:\/\/dl-protect\.link\/[0-9a-fA-F]+\?[^"]*?rl=[ab]2[^"]*)"[^>]*>([^<]+)<\/a>/g;
|
|
let lm: RegExpExecArray | null;
|
|
while ((lm = linkRegex.exec(block)) !== null) {
|
|
const url = lm[1]!;
|
|
const label = lm[2]!.trim();
|
|
const epMatch = label.match(/Episode\s*(\d+|FiNAL|Final|final)/i);
|
|
const meta = decodeFnMeta(url);
|
|
|
|
let quality = meta.quality || 'Inconnu';
|
|
let langs: string[] = [];
|
|
let subs: string[] = [];
|
|
|
|
const textToScan = `${quality} ${label}`;
|
|
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: url,
|
|
host: host.toLowerCase(),
|
|
label: isSeries ? `${label} — ${host}` : host,
|
|
episode: epMatch ? epMatch[1] : undefined,
|
|
quality: quality,
|
|
langs: langs,
|
|
subs: subs,
|
|
url: null,
|
|
});
|
|
}
|
|
}
|
|
|
|
return { links };
|
|
}
|
|
|
|
/**
|
|
* Extrait les autres versions/qualités du film depuis la section "Qualités également disponibles".
|
|
*/
|
|
export function parseOtherVersions(html: string, baseUrl: string): { label: string; value: string }[] {
|
|
const out: { label: string; value: string }[] = [];
|
|
const sectionMatch = html.match(/<div class="otherversions"[\s\S]*?<\/div>/);
|
|
if (!sectionMatch) return out;
|
|
const linkRegex = /<a\s+href="([^"]+)"[^>]*>\s*<span class="otherquality">([\s\S]*?)<\/span>\s*<\/a>/g;
|
|
let m: RegExpExecArray | null;
|
|
while ((m = linkRegex.exec(sectionMatch[0])) !== null) {
|
|
const href = absUrl(m[1]!, baseUrl);
|
|
const label = m[2]!.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
if (label && !out.find(o => o.value === href)) out.push({ label, value: href });
|
|
}
|
|
return out;
|
|
}
|