import { CONFIG } from '../../src/utils/config.js'; export const CONFIG_HYDRACKER = { get BASE_URL() { return (CONFIG.HYDRACKER_URL || '').replace(/\/$/, ''); }, get API_KEY() { return CONFIG.HYDRACKER_API_KEY; }, get TIMEOUT() { return CONFIG.HYDRACKER_TIMEOUT || 15000; }, }; export function getHydrackerHeaders() { return { 'Accept': 'application/json', 'Authorization': `Bearer ${CONFIG_HYDRACKER.API_KEY}`, 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' }; } async function fetchWithRetry( url: string, options: RequestInit = {}, maxRetries: number = 2, initialDelay: number = 2000 ): Promise { let attempt = 0; let delay = initialDelay; while (true) { attempt++; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), CONFIG_HYDRACKER.TIMEOUT); try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timeoutId); if (res.status === 502 || res.status === 503 || res.status === 504 || res.status === 429) { if (attempt < maxRetries) { console.warn(`[Hydracker-API] Attempt ${attempt}/${maxRetries} returned HTTP ${res.status} on fetch. Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; continue; } } return res; } catch (err: any) { clearTimeout(timeoutId); const isTimeout = err.name === 'AbortError' || err.message?.includes('aborted'); if (attempt < maxRetries) { const waitTime = isTimeout ? 1000 : delay; console.warn(`[Hydracker-API] Attempt ${attempt}/${maxRetries} failed/timed out (${err.message}). Retrying in ${waitTime}ms...`); await new Promise(resolve => setTimeout(resolve, waitTime)); if (!isTimeout) delay *= 2; continue; } throw err; } } } export async function apiGet(urlPath: string, params: Record = {}) { let qs = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&'); // FIX: Hydracker API returns 401 if ':' is URL-encoded as '%3A' qs = qs.replace(/%3A/g, ':'); const url = `${CONFIG_HYDRACKER.BASE_URL}/api/v1/${urlPath}` + (qs ? `?${qs}` : ''); try { const res = await fetchWithRetry(url, { headers: getHydrackerHeaders() }); if (!res.ok) { console.error(`[Hydracker-API] apiGet HTTP ${res.status} on ${urlPath}`); return null; } return await res.json(); } catch (e: any) { console.error(`[Hydracker-API] apiGet Error on ${urlPath}:`, e.message); return null; } } export async function apiPost(urlPath: string, body: any = {}) { const url = `${CONFIG_HYDRACKER.BASE_URL}/api/v1/${urlPath}`; try { const res = await fetchWithRetry(url, { method: 'POST', headers: { ...getHydrackerHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); return { status: res.status, body: await res.text() }; } catch (e: any) { console.error(`[Hydracker-API] apiPost Error on ${urlPath}:`, e.message); return null; } } export async function fetchSearch(query: string) { const url = `${CONFIG_HYDRACKER.BASE_URL}/api/v1/titles?query=${encodeURIComponent(query)}`; try { const res = await fetchWithRetry(url, { headers: getHydrackerHeaders() }); if (!res.ok) { console.error(`[Hydracker-API] Search HTTP ${res.status} for "${query}"`); return null; } const data = await res.json(); // Transform the new API structure to match the old expected structure if (data && data.pagination && Array.isArray(data.pagination.data)) { return { results: data.pagination.data }; } return data; } catch (e: any) { console.error('[Hydracker-API] Search failed:', e.message); return null; } } export async function fetchMovieLinks(titleId: string) { const url = `${CONFIG_HYDRACKER.BASE_URL}/api/v1/titles/${titleId}/download`; try { const res = await fetchWithRetry(url, { headers: getHydrackerHeaders() }); if (!res.ok) return null; return await res.json(); } catch (e: any) { return null; } } /** * Récupère la page de download d'un titre. * - Films : GET /titles/{id}/download * - Séries : GET /titles/{id}/season/{s}/episode/{e}/download * * Retourne l'objet complet contenant: video, alternative_videos, title.seasons, last_episode, etc. */ export async function fetchDownloadPage(titleId: string, season?: number, episode?: number) { let urlPath: string; if (season && season > 0 && episode && episode > 0) { urlPath = `titles/${titleId}/season/${season}/episode/${episode}/download`; } else if (season && season > 0) { // On demande le premier épisode de la saison pour obtenir les métadonnées urlPath = `titles/${titleId}/season/${season}/episode/1/download`; } else { urlPath = `titles/${titleId}/download`; } return await apiGet(urlPath); } /** * Récupère TOUS les liens d'une saison en itérant sur chaque épisode via /download. * Utilise last_episode pour savoir combien d'épisodes ont des liens. */ export async function fetchSeriesLiens(titleId: string, season: number = 1) { // D'abord, obtenir les métadonnées pour savoir combien d'épisodes il y a const firstPage = await fetchDownloadPage(titleId, season, 1); if (!firstPage) return []; const lastEpisodeMap = firstPage.last_episode || {}; const lastEp = lastEpisodeMap[String(season)] || 0; if (lastEp === 0) return []; // Collecter les liens de tous les épisodes const allLiens: any[] = []; // Extraire les liens du premier épisode qu'on a déjà chargé const extractLiens = (downloadData: any) => { const liens: any[] = []; if (downloadData.video) liens.push(downloadData.video); if (downloadData.alternative_videos) { for (const av of downloadData.alternative_videos) { // Éviter les doublons (video est souvent dans alternative_videos aussi) if (!liens.find(l => l.id === av.id)) { liens.push(av); } } } return liens; }; allLiens.push(...extractLiens(firstPage)); // Charger les épisodes suivants (2 à lastEp) for (let ep = 2; ep <= lastEp; ep++) { const epData = await fetchDownloadPage(titleId, season, ep); if (epData) { allLiens.push(...extractLiens(epData)); } } return allLiens; }