Files
Agora/plugins/ztnews/parser.ts
T
2026-09-15 21:56:10 +02:00

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;
}