143 lines
5.9 KiB
TypeScript
143 lines
5.9 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 { parseSearchResults, parseTrendingResults, parseContentHTML, parseEpisodeLinks, parseOtherVersions } from './parser.js';
|
|
|
|
function isSeriesIdentifier(identifier: string): boolean {
|
|
return /saison|pack-series|series-(vf|vostfr|terminee)/i.test(identifier);
|
|
}
|
|
|
|
export class FreeTeleAPI implements ISource {
|
|
name = 'freetel';
|
|
displayName = 'Free-Télécharger';
|
|
private baseUrl: string | undefined;
|
|
|
|
constructor(baseUrl?: string) {
|
|
this.baseUrl = baseUrl?.replace(/\/$/, '');
|
|
}
|
|
|
|
async healthCheck(): Promise<boolean> {
|
|
if (!this.baseUrl) {
|
|
console.warn('[FreeTel] ⚠️ FT_URL non définie.');
|
|
return false;
|
|
}
|
|
try {
|
|
const res = await fetch(this.baseUrl, {
|
|
method: 'HEAD',
|
|
headers: { 'User-Agent': 'Mozilla/5.0' },
|
|
signal: AbortSignal.timeout(5000),
|
|
});
|
|
return res.ok;
|
|
} catch {
|
|
return true; // tolérant : le test réel se fait au premier scrape
|
|
}
|
|
}
|
|
|
|
async search(query: string, mediaType: MediaType = 'movie'): Promise<SearchResult[]> {
|
|
if (!this.baseUrl) throw new Error('FT_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 = parseSearchResults(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 results;
|
|
}
|
|
|
|
async getTrending(mediaType: MediaType): Promise<SearchResult[]> {
|
|
if (!this.baseUrl) return [];
|
|
try {
|
|
const html = await fetchTrending(this.baseUrl);
|
|
let results = parseTrendingResults(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 results.slice(0, 20);
|
|
} catch (e: any) {
|
|
console.error(`[FreeTel] Erreur trending ${mediaType}:`, e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getRecent(): Promise<SearchResult[]> {
|
|
if (!this.baseUrl) return [];
|
|
try {
|
|
const html = await fetchTrending(this.baseUrl);
|
|
const results = parseTrendingResults(html, this.baseUrl).slice(0, 20);
|
|
return results;
|
|
} catch (e: any) {
|
|
console.error(`[FreeTel] Erreur getRecent:`, e.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async getContentLinks(identifier: string): Promise<ContentLinks> {
|
|
if (!this.baseUrl) throw new Error('FT_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('FT_URL non configurée.');
|
|
// Si seasonValue est fournie (l'UI a cliqué sur une autre qualité), on switch de fiche
|
|
const targetIdentifier = seasonValue ? String(seasonValue) : identifier;
|
|
const url = targetIdentifier.startsWith('http') ? targetIdentifier : `${this.baseUrl}/${targetIdentifier.replace(/^\//, '')}`;
|
|
const html = await fetchPage(url);
|
|
const isSeries = isSeriesIdentifier(targetIdentifier);
|
|
const content = parseContentHTML(html, isSeries);
|
|
|
|
// Pour les films, exposer les autres qualités comme "seasons" (l'UI les affichera en dropdown)
|
|
let seasons: { label: string; value: string }[] = [];
|
|
if (!isSeries) {
|
|
seasons = parseOtherVersions(html, this.baseUrl);
|
|
// Ajouter la version courante comme première entrée (sélectionnée par défaut)
|
|
const currentQuality = content.links[0]?.quality;
|
|
if (currentQuality && currentQuality !== 'Inconnu') {
|
|
seasons.unshift({ label: currentQuality, value: targetIdentifier });
|
|
}
|
|
}
|
|
|
|
return {
|
|
links: content.links,
|
|
seasons,
|
|
isSeries,
|
|
};
|
|
}
|
|
|
|
async resolveLink(linkId: string): Promise<string | null> {
|
|
let hostUrl: string | null = null;
|
|
|
|
// Cas série : page intermédiaire liens.free-telecharger.cam/SLUG-episode_N
|
|
if (linkId.includes('liens.free-telecharger.cam')) {
|
|
try {
|
|
const html = await fetchPage(linkId);
|
|
const hosts = parseEpisodeLinks(html);
|
|
if (hosts.length === 0) {
|
|
console.warn(`[FreeTel] Aucun hôte trouvé sur ${linkId}`);
|
|
return null;
|
|
}
|
|
const preferred = hosts.find(h => /1fichier/i.test(h.host))
|
|
|| hosts.find(h => /turbobit/i.test(h.host))
|
|
|| hosts[0];
|
|
hostUrl = preferred ? preferred.url : null;
|
|
} catch (e: any) {
|
|
console.error(`[FreeTel] Erreur resolveLink:`, e.message);
|
|
return null;
|
|
}
|
|
} else if (linkId.startsWith('http')) {
|
|
// Cas film : linkId est déjà l'URL hôte (1fichier, Turbobit, …)
|
|
hostUrl = linkId;
|
|
}
|
|
|
|
return hostUrl;
|
|
}
|
|
}
|
|
|
|
sourceRegistry.register(new FreeTeleAPI(CONFIG.FT_URL));
|