import { ISource, SearchResult, MediaType, ContentLinks, VideoLink, SelectionData } from '../../src/types/source.js'; import { sourceRegistry } from '../../src/core/registry.js'; import { CONFIG_HYDRACKER, apiGet, apiPost, fetchSearch, fetchDownloadPage, fetchSeriesLiens } from './api.js'; import { QUALITY_MAP, formatSize, parseSearchResults, parseTrendingResults, parseMovieLinks, parseSeasons, parsePremiumLink, getLangs, getSubs } from './parser.js'; export class HydrackerAPI implements ISource { name = 'hydracker'; displayName = 'Hydracker (Token)'; async healthCheck(): Promise { console.warn('[Hydracker] ⚠️ Plugin désactivé (Site fermé définitivement). Conservé pour archivage.'); return false; } async search(query: string, mediaType: MediaType = 'movie'): Promise { const data = await fetchSearch(query); if (!data) { console.error('[Hydracker] search: fetchSearch a retourné null pour', query); return []; } const totalRaw = (data.results || []).length; const parsed = parseSearchResults(data, mediaType); console.log(`[Hydracker] search "${query}" (${mediaType}): ${totalRaw} résultats bruts → ${parsed.length} après filtre`); return parsed; } async getTrending(mediaType: MediaType): Promise { // Channel 12 = Films, Channel 10 = Séries const channelId = mediaType === 'series' ? 10 : 12; try { const data = await apiGet(`channel/${channelId}`, { restriction: '', order: 'trending:desc', filters: '', page: 1, paginate: 'lengthAware', returnContentOnly: true }); return parseTrendingResults(data); } catch (e: any) { console.error(`[Hydracker] getTrending Error for channel ${channelId}:`, e.message); return []; } } async getRecent(): Promise { try { const data = await apiGet('titles', { order: 'created_at:desc', page: 1, paginate: 'lengthAware' }); return parseTrendingResults(data); } catch (e: any) { console.error(`[Hydracker] getRecent Error:`, e.message); return []; } } async getSelection(identifier: string, type?: string, seasonValue?: string | number): Promise { // Récupérer les infos du titre via /download pour avoir les saisons const titleData = await fetchDownloadPage(identifier); let isSeries = false; if (type) { isSeries = (type === 'series' || type === 'serie' || type === 'tv'); } else if (titleData && titleData.title) { isSeries = titleData.title.is_series === true; } // Extraire les saisons depuis la réponse /download const seasonsList: number[] = []; if (titleData && titleData.title && titleData.title.seasons) { const seasons = titleData.title.seasons; for (const s of seasons) { if (typeof s.number === 'number' && s.number > 0) { seasonsList.push(s.number); } } seasonsList.sort((a, b) => a - b); } if (seasonsList.length > 0) isSeries = true; const currentSeason = seasonValue ? parseInt(String(seasonValue), 10) : (isSeries ? 1 : 0); const content = await this.getContentLinks(identifier, currentSeason); const formattedSeasons = seasonsList.map(num => ({ label: `Saison ${num}`, value: num })); return { links: content.links, seasons: isSeries ? formattedSeasons : [], isSeries }; } async getContentLinks(titleId: string, season: number = 1): Promise { if (season === 0) { // Film : utiliser /download directement const downloadData = await fetchDownloadPage(titleId); if (!downloadData) return { links: [] }; return { links: this.parseLiensFromDownload(downloadData, season) }; } // Série : itérer sur les épisodes const rawLiens = await fetchSeriesLiens(titleId, season); const links: VideoLink[] = rawLiens.map(l => this.parseSingleLien(l, season)); return { links }; } /** * Parse les liens depuis une réponse /download (film ou épisode unique) */ private parseLiensFromDownload(downloadData: any, season: number): VideoLink[] { const allLiens: any[] = []; if (downloadData.video) allLiens.push(downloadData.video); if (downloadData.alternative_videos) { for (const av of downloadData.alternative_videos) { if (!allLiens.find(l => l.id === av.id)) { allLiens.push(av); } } } return allLiens.map(l => this.parseSingleLien(l, season)); } /** * Convertit un objet lien brut de l'API en VideoLink unifié */ private parseSingleLien(l: any, season: number): VideoLink { // Extraire le nom du host const hostName = l.host_compact?.name || l.host?.name || l.name || '?'; // Extraire la qualité const quality = l.qual?.qual || l.quality || QUALITY_MAP[l.qualite] || `id:${l.qualite}`; // Extraire les langues const langs = l.langues ? l.langues.map((la: any) => la.lang || la.name || '') : getLangs(l); // Extraire les sous-titres const subs = l.subs_compact ? l.subs_compact.map((s: any) => s.name || '') : getSubs(l); return { id: l.id, host: hostName, size: formatSize(l.taille), sizeBytes: l.taille || 0, quality, langs, subs, releaseName: l.release || l.filename || l.name || l.titre || l.titre_release || undefined, episode: (l.episode === 0 || l.episode === "0" || l.episode === "00" || l.episode === null) ? (season === 0 ? 'Film complet' : 'Saison complète') : (l.episode ? String(l.episode) : null), url: null }; } async getSeasons(titleId: string): Promise { // Utiliser /download pour récupérer les saisons (au lieu de /titles/{id} qui est redondant) const downloadData = await fetchDownloadPage(titleId); if (!downloadData || !downloadData.title || !downloadData.title.seasons) return []; return downloadData.title.seasons .map((s: any) => s.number) .filter((n: any) => typeof n === 'number' && n > 0) .sort((a: number, b: number) => a - b); } private isPremiumCache: boolean | null = null; private premiumCheckPromise: Promise | null = null; async checkPremiumStatus(): Promise { if (this.isPremiumCache !== null) return this.isPremiumCache; if (this.premiumCheckPromise) return this.premiumCheckPromise; this.premiumCheckPromise = (async () => { try { const result = await apiGet('users/me'); if (result && result.user) { this.isPremiumCache = !!result.user.IsPremium; console.log(`[Hydracker] Statut Premium vérifié: ${this.isPremiumCache ? 'OUI' : 'NON'}`); return this.isPremiumCache; } } catch (e: any) { console.error('[Hydracker] Erreur vérification Premium:', e.message); } return false; })(); return await this.premiumCheckPromise; } async resolveLink(linkId: string): Promise { // Tentative de résolution via la base locale d'abord const localDbSource = sourceRegistry.get('localdb') as any; if (localDbSource && typeof localDbSource.resolveLocalLink === 'function') { const localUrl = localDbSource.resolveLocalLink(linkId); if (localUrl) { console.log(`[Hydracker] Lien résolu via base de données locale (ID: ${linkId})`); return localUrl; } } // Tenter la résolution via l'API /content/liens/{id} try { const result = await apiGet(`content/liens/${linkId}`); if (result && (result.directDL || result.url || result.link)) { const finalUrl = result.directDL || result.url || result.link; console.log(`[Hydracker] Got final URL via API: ${finalUrl.substring(0, 80)}...`); return finalUrl; } // Vérifier aussi dans result.lien (format alternatif) if (result && result.lien && result.lien.lien) { console.log(`[Hydracker] Got final URL via result.lien.lien`); return result.lien.lien; } } catch (e: any) { console.error(`[Hydracker] Exception resolving lien ${linkId}:`, e.message); } console.log(`[Hydracker] Échec de la résolution API. Fallback automatique via Movix...`); return await this.resolveMovixLink(linkId); } async resolveMovixLink(lienId: string, titleId?: string): Promise { try { const { CONFIG } = await import('../../src/utils/config.js'); const movixBase = CONFIG.MOVIX_URL || ''; if (!movixBase) { console.warn('[Hydracker] MOVIX_URL non configurée, impossible de résoudre via Movix.'); return null; } const movixApiBase = (() => { try { const u = new URL(movixBase); return `${u.protocol}//api.${u.host}/api`; } catch { return ''; } })(); if (!movixApiBase) return null; console.log(`[Hydracker] Tentative de débridage Movix pour le lien ${lienId}...`); const url = `${movixApiBase}/darkiworld/decode/${lienId}${titleId ? `?title_id=${titleId}` : ''}`; const response = await fetch(url, { method: 'GET', headers: { 'Accept': 'application/json, text/plain, */*', 'Referer': `${movixBase}/`, 'Origin': movixBase, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 OPR/133.0.0.0' } }); const data = await response.json(); if (!response.ok || data.success === false) { console.error('[Hydracker] Erreur API Movix:', data.error || 'Erreur inconnue'); return null; } const directUrl = data.directDL || data.direct_url || (data.embed_url && (data.embed_url.directDL || data.embed_url.src || data.embed_url.lien)); if (directUrl) { console.log(`[Hydracker] Movix a résolu le lien avec succès !`); return directUrl; } return null; } catch (e: any) { console.error(`[Hydracker] Exception lors de la résolution Movix :`, e.message); return null; } } } // ── Auto-registration ── sourceRegistry.register(new HydrackerAPI());