134 lines
5.3 KiB
TypeScript
134 lines
5.3 KiB
TypeScript
import { ISource, SearchResult, MediaType, SelectionData, ContentLinks } from '../../src/types/source.js';
|
|
import { CONFIG } from '../../src/utils/config.js';
|
|
import { sourceRegistry } from '../../src/core/registry.js';
|
|
import { fetchSearch, fetchTrending, fetchPage } from './api.js';
|
|
import { parseListingHTML, parseContentHTML, parseOtherVersions } from './parser.js';
|
|
|
|
function isSeriesIdentifier(identifier: string): boolean {
|
|
return /[?&]p=serie\b|telecharger-serie/i.test(identifier);
|
|
}
|
|
|
|
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, '');
|
|
}
|
|
|
|
function deduplicateByTitle(results: SearchResult[]): SearchResult[] {
|
|
const seen = new Set<string>();
|
|
return results.filter(r => {
|
|
const key = normalizeTitle(r.title);
|
|
if (seen.has(key)) return false;
|
|
seen.add(key);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
export class ZtTeamAPI implements ISource {
|
|
name = 'ztnews';
|
|
displayName = 'Zone-Téléchargement (Team)';
|
|
get baseUrl() {
|
|
return CONFIG.ZTTEAM_URL?.replace(/\/$/, '');
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
if (!this.baseUrl) {
|
|
console.warn('[ztnews] ⚠️ ZTTEAM_URL non définie.');
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
async search(query: string, mediaType: MediaType = 'movie'): Promise<SearchResult[]> {
|
|
if (!this.baseUrl) throw new Error('ZTTEAM_URL non configurée.');
|
|
if (!query || query.length < 3) throw new Error('La recherche nécessite au moins 3 caractères.');
|
|
const html = await fetchSearch(this.baseUrl, query);
|
|
let results = parseListingHTML(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<SearchResult[]> {
|
|
if (!this.baseUrl) return [];
|
|
try {
|
|
const html = await fetchTrending(this.baseUrl, mediaType === 'series' ? 'series' : 'films');
|
|
const results = parseListingHTML(html, this.baseUrl);
|
|
return deduplicateByTitle(results).slice(0, 20);
|
|
} catch (e: any) {
|
|
console.error(`[ztnews] Erreur trending ${mediaType}:`, e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getRecent(): Promise<SearchResult[]> {
|
|
if (!this.baseUrl) return [];
|
|
try {
|
|
const html = await fetchPage(this.baseUrl);
|
|
const results = parseListingHTML(html, this.baseUrl);
|
|
return deduplicateByTitle(results).slice(0, 20);
|
|
} catch (e: any) {
|
|
console.error(`[ztnews] Erreur getRecent:`, e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getContentLinks(identifier: string): Promise<ContentLinks> {
|
|
if (!this.baseUrl) throw new Error('ZTTEAM_URL non configurée.');
|
|
const url = identifier.startsWith('http') ? identifier : `${this.baseUrl}/${identifier.replace(/^\//, '')}`;
|
|
const html = await fetchPage(url);
|
|
return parseContentHTML(html, isSeriesIdentifier(identifier));
|
|
}
|
|
|
|
async getSelection(identifier: string, type?: string, seasonValue?: string | number): Promise<SelectionData> {
|
|
if (!this.baseUrl) throw new Error('ZTTEAM_URL non configurée.');
|
|
const targetIdentifier = seasonValue ? String(seasonValue) : identifier;
|
|
const isSeries = isSeriesIdentifier(targetIdentifier);
|
|
const url = targetIdentifier.startsWith('http') ? targetIdentifier : `${this.baseUrl}/${targetIdentifier.replace(/^\//, '')}`;
|
|
const html = await fetchPage(url);
|
|
const content = parseContentHTML(html, isSeries);
|
|
|
|
const allLinks = [...content.links];
|
|
let seasons: { label: string; value: string }[] = [];
|
|
|
|
if (!isSeries) {
|
|
const otherVersions = parseOtherVersions(html, this.baseUrl);
|
|
if (otherVersions.length > 0) {
|
|
console.log(`[ztnews] Fetching ${otherVersions.length} other qualities concurrently...`);
|
|
const qualityPromises = otherVersions.map(async (q) => {
|
|
try {
|
|
const qHtml = await fetchPage(q.value);
|
|
const qContent = parseContentHTML(qHtml, isSeries);
|
|
return qContent.links;
|
|
} catch (e) {
|
|
console.error(`[ztnews] Error fetching quality page ${q.value}:`, e);
|
|
return [];
|
|
}
|
|
});
|
|
const otherQualitiesLinks = await Promise.all(qualityPromises);
|
|
otherQualitiesLinks.forEach(links => allLinks.push(...links));
|
|
}
|
|
}
|
|
|
|
return {
|
|
links: allLinks,
|
|
seasons,
|
|
isSeries,
|
|
};
|
|
}
|
|
|
|
async resolveLink(linkId: string): Promise<string | null> {
|
|
console.log(`[ztTeam] 🔗 Renvoi du lien dl-protect brut (résolution via navigateur ou JDownloader requise) : ${linkId}`);
|
|
return linkId || null;
|
|
}
|
|
}
|
|
|
|
sourceRegistry.register(new ZtTeamAPI());
|