import { ISource, SearchResult, MediaType, ContentLinks, SelectionData } from '../../src/types/source.js'; import { CONFIG } from '../../src/utils/config.js'; import { sourceRegistry } from '../../src/core/registry.js'; import { fetchSearchResults, fetchTrendingMovies, fetchTrendingSeries, fetchContentPage, fetchResolvedLink, fetchRecent } from './api.js'; import { parseSearchHTML, parseContentHTML, extractLinkFromZtProtect } from './parser.js'; /** * Normalise un titre pour la comparaison (minuscules, sans accents, sans ponctuation). */ function normalizeTitle(title: string): string { return title .toLowerCase() .normalize('NFD') .replace(/[\u0300-\u036f]/g, '') .replace(/-\s*saison\s*\d+/gi, '') .replace(/\(\s*\d{4}\s*\)/g, '') .replace(/[^a-z0-9]/g, ''); } /** * Déduplique les résultats par titre normalisé, en gardant la première occurrence. */ function deduplicateByTitle(results: SearchResult[]): SearchResult[] { const seen = new Set(); return results.filter(r => { const key = normalizeTitle(r.title); if (seen.has(key)) return false; seen.add(key); return true; }); } export class ZoneTelechargementAPI implements ISource { name = 'zt'; displayName = 'Zone-Téléchargement'; get baseUrl() { return CONFIG.ZT_URL?.replace(/\/$/, ''); } async healthCheck(): Promise { if (!this.baseUrl) { console.warn('[ZT] ⚠️ ZT_URL non définie.'); return false; } return true; } async search(query: string, mediaType: MediaType = 'movie'): Promise { if (!this.baseUrl) throw new Error('ZT_URL non configurée.'); if (!query || query.length < 4) throw new Error('La recherche nécessite au moins 4 caractères.'); const html = await fetchSearchResults(this.baseUrl, query); if (html.includes('Aucun résultat')) return []; let results = parseSearchHTML(html, this.baseUrl); if (mediaType === 'movie') { results = results.filter(r => r.type === 'movie' || r.type === 'anime'); } else { results = results.filter(r => r.type === 'series' || r.type === 'anime'); } return deduplicateByTitle(results); } async getTrending(mediaType: MediaType): Promise { if (!this.baseUrl) return []; try { const html = mediaType === 'movie' ? await fetchTrendingMovies(this.baseUrl) : await fetchTrendingSeries(this.baseUrl); const results = parseSearchHTML(html, this.baseUrl).slice(0, 40); return deduplicateByTitle(results).slice(0, 20); } catch (e: any) { console.error(`[ZT] ❌ Erreur trending ${mediaType}:`, e.message); return []; } } async getRecent(): Promise { if (!this.baseUrl) return []; try { const html = await fetchRecent(this.baseUrl); const results = parseSearchHTML(html, this.baseUrl).slice(0, 40); return deduplicateByTitle(results).slice(0, 20); } catch (e: any) { console.error(`[ZT] ❌ Erreur getRecent:`, e.message); return []; } } async getContentLinks(pageUrl: string): Promise { if (!this.baseUrl) throw new Error('ZT_URL non configurée.'); const fullUrl = pageUrl.startsWith('http') ? pageUrl : (this.baseUrl + (pageUrl.startsWith('/') ? '' : '/') + pageUrl); const html = await fetchContentPage(fullUrl); return parseContentHTML(html); } async getSelection(identifier: string, type?: string, seasonValue?: string | number): Promise { const targetUrl = seasonValue ? String(seasonValue) : identifier; const content = await this.getContentLinks(targetUrl); const isSeries = targetUrl.includes('/telecharger-serie/') || targetUrl.includes('/serie-') || (content.relatedSeasons?.length || 0) > 0; let currentSeasonLabel = "Saison (Actuelle)"; if (content.releaseNames && content.releaseNames.length > 0) { const sm = content.releaseNames[0].match(/Saison\s*\d+/i); if (sm) currentSeasonLabel = sm[0]; } const formattedSeasons = (content.relatedSeasons || []).map(s => ({ label: s.label, value: s.href })); if (isSeries) { formattedSeasons.push({ label: currentSeasonLabel, value: targetUrl }); formattedSeasons.sort((a, b) => { const numA = parseInt(a.label.replace(/\D/g, '')) || 0; const numB = parseInt(b.label.replace(/\D/g, '')) || 0; return numA - numB; }); } const allLinks = [...content.links]; if (content.relatedQualities && content.relatedQualities.length > 0) { console.log(`[ZT] Fetching ${content.relatedQualities.length} other qualities concurrently...`); const qualityPromises = content.relatedQualities.map(async (q) => { try { const qContent = await this.getContentLinks(q.href); return qContent.links; } catch (e) { console.error(`[ZT] Error fetching quality page ${q.href}:`, e); return []; } }); const otherQualitiesLinks = await Promise.all(qualityPromises); otherQualitiesLinks.forEach(links => allLinks.push(...links)); } return { links: allLinks, seasons: formattedSeasons, isSeries }; } async resolveLink(linkId: string): Promise { try { console.log(`[ZT] 🔓 Résolution du lien : ${linkId}`); const html = await fetchResolvedLink(linkId); const resolved = extractLinkFromZtProtect(html); if (!resolved) { console.warn(`[ZT] ⚠️ Impossible d'extraire le lien résolu du HTML de ZTProtect pour ${linkId}`); } return resolved; } catch (e: any) { console.error(`[ZT] ❌ Erreur resolveLink pour ${linkId}:`, e.message); return null; } } } // ── Auto-registration ── sourceRegistry.register(new ZoneTelechargementAPI());