ci: fix pipeline, update readme and anonymize ZT references
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { CONFIG } from '../../src/utils/config.js';
|
||||
|
||||
export const CONFIG_HYDRACKER = {
|
||||
BASE_URL: (CONFIG.HYDRACKER_URL || '').replace(/\/$/, ''), // Supprime le slash final
|
||||
API_KEY: CONFIG.HYDRACKER_API_KEY,
|
||||
TIMEOUT: CONFIG.HYDRACKER_TIMEOUT || 15000,
|
||||
};
|
||||
|
||||
const HYDRACKER_HEADERS = {
|
||||
'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'
|
||||
};
|
||||
|
||||
const TIMEOUT = CONFIG_HYDRACKER.TIMEOUT; // 30 secondes par défaut (configurable)
|
||||
|
||||
async function fetchWithRetry(
|
||||
url: string,
|
||||
options: RequestInit = {},
|
||||
maxRetries: number = 2,
|
||||
initialDelay: number = 2000
|
||||
): Promise<Response> {
|
||||
let attempt = 0;
|
||||
let delay = initialDelay;
|
||||
|
||||
while (true) {
|
||||
attempt++;
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 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<string, any> = {}) {
|
||||
const qs = Object.entries(params).map(([k, v]) => `${k}=${encodeURIComponent(v)}`).join('&');
|
||||
const url = `${CONFIG_HYDRACKER.BASE_URL}/api/v1/${urlPath}` + (qs ? `?${qs}` : '');
|
||||
try {
|
||||
const res = await fetchWithRetry(url, {
|
||||
headers: HYDRACKER_HEADERS
|
||||
});
|
||||
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: { ...HYDRACKER_HEADERS, '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/search/${encodeURIComponent(query)}?loader=searchAutocomplete`;
|
||||
try {
|
||||
const res = await fetchWithRetry(url, {
|
||||
headers: HYDRACKER_HEADERS
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.error(`[Hydracker-API] Search HTTP ${res.status} for "${query}"`);
|
||||
return null;
|
||||
}
|
||||
return await res.json();
|
||||
} 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: HYDRACKER_HEADERS
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
return await res.json();
|
||||
} catch (e: any) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchSeriesLiens(titleId: string, season: number = 1) {
|
||||
const allLiens: any[] = [];
|
||||
let page = 1;
|
||||
while (true) {
|
||||
const result = await apiGet('liens', {
|
||||
title_id: titleId, loader: 'linksdl', season,
|
||||
perPage: 500, page, filters: '', paginate: 'lengthAware'
|
||||
});
|
||||
if (!result || result.error) break;
|
||||
const pagination = result.pagination || {};
|
||||
const data = pagination.data || [];
|
||||
if (!data.length) break;
|
||||
allLiens.push(...data);
|
||||
const lastPage = pagination.last_page || pagination.lastPage || 1;
|
||||
if (page >= lastPage) break;
|
||||
page++;
|
||||
}
|
||||
return allLiens;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
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, fetchMovieLinks, 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<boolean> {
|
||||
if (!CONFIG_HYDRACKER.BASE_URL || !CONFIG_HYDRACKER.API_KEY) {
|
||||
console.warn('[Hydracker] ⚠️ HYDRACKER_URL ou HYDRACKER_API_KEY manquante.');
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
const res = await fetch(CONFIG_HYDRACKER.BASE_URL, {
|
||||
headers: { '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' },
|
||||
signal: AbortSignal.timeout(CONFIG_HYDRACKER.TIMEOUT)
|
||||
});
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: string, mediaType: MediaType = 'movie'): Promise<SearchResult[]> {
|
||||
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<SearchResult[]> {
|
||||
const type = mediaType === 'series' ? 'series' : 'movie';
|
||||
try {
|
||||
const data = await apiGet('titles', { order: 'trending:desc', type, page: 1, paginate: 'lengthAware' });
|
||||
return parseTrendingResults(data);
|
||||
} catch (e: any) {
|
||||
console.error(`[Hydracker] getTrending Error for ${type}:`, e.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async getRecent(): Promise<SearchResult[]> {
|
||||
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<SelectionData> {
|
||||
const seasonsList = await this.getSeasons(identifier);
|
||||
|
||||
let isSeries = false;
|
||||
if (type) {
|
||||
isSeries = (type === 'series' || type === 'serie' || type === 'tv');
|
||||
} else {
|
||||
isSeries = seasonsList.length > 0;
|
||||
}
|
||||
|
||||
const currentSeason = seasonValue ? parseInt(String(seasonValue), 10) : 1;
|
||||
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<ContentLinks> {
|
||||
// Essai film en premier
|
||||
const movieData = await fetchMovieLinks(titleId);
|
||||
if (movieData) {
|
||||
const movieLinks = parseMovieLinks(movieData);
|
||||
if (movieLinks.length > 0) return { links: movieLinks };
|
||||
}
|
||||
|
||||
// Fallback série
|
||||
const rawLiens = await fetchSeriesLiens(titleId, season);
|
||||
const links: VideoLink[] = rawLiens.map(l => ({
|
||||
id: l.id,
|
||||
host: (l.host && l.host.name) || '?',
|
||||
size: formatSize(l.taille),
|
||||
sizeBytes: l.taille || 0,
|
||||
quality: QUALITY_MAP[l.qualite] || `id:${l.qualite}`,
|
||||
langs: getLangs(l),
|
||||
subs: getSubs(l),
|
||||
releaseName: l.release || l.name || l.titre || l.titre_release || undefined,
|
||||
episode: (l.episode === 0 || l.episode === "0" || l.episode === "00")
|
||||
? 'Saison complète'
|
||||
: (l.episode ? String(l.episode) : null),
|
||||
url: null
|
||||
}));
|
||||
|
||||
return { links };
|
||||
}
|
||||
|
||||
async getSeasons(titleId: string): Promise<number[]> {
|
||||
const result = await apiGet(`titles/${titleId}/seasons`);
|
||||
return parseSeasons(result);
|
||||
}
|
||||
|
||||
private isPremiumCache: boolean | null = null;
|
||||
private premiumCheckPromise: Promise<boolean> | null = null;
|
||||
|
||||
async checkPremiumStatus(): Promise<boolean> {
|
||||
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<string | null> {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
const isPremium = await this.checkPremiumStatus();
|
||||
|
||||
if (!isPremium) {
|
||||
console.log(`[Hydracker] Compte non Premium détecté. Bypass de Hydracker, passage direct à Movix...`);
|
||||
return await this.resolveMovixLink(linkId);
|
||||
}
|
||||
|
||||
const maxRetries = 4;
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt > 1) {
|
||||
console.log(`[Hydracker] Retry ${attempt}/${maxRetries} for lien ${linkId}`);
|
||||
await new Promise(r => setTimeout(r, 4000));
|
||||
}
|
||||
|
||||
const result = await apiGet(`content/liens/${linkId}`);
|
||||
if (!result) continue;
|
||||
|
||||
const finalUrl = result.directDL || result.url || result.link || '';
|
||||
if (!finalUrl) continue;
|
||||
|
||||
console.log(`[Hydracker] Got final URL: ${finalUrl.substring(0, 80)}...`);
|
||||
|
||||
return finalUrl;
|
||||
} catch (e: any) {
|
||||
console.error(`[Hydracker] Exception resolving lien ${linkId} (attempt ${attempt}):`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[Hydracker] Échec de la résolution classique (Erreur). Fallback automatique via Movix...`);
|
||||
return await this.resolveMovixLink(linkId);
|
||||
}
|
||||
|
||||
async resolveMovixLink(lienId: string, titleId?: string): Promise<string | null> {
|
||||
try {
|
||||
console.log(`[Hydracker] Tentative de débridage Movix pour le lien ${lienId}...`);
|
||||
const url = `https://api.movix.cloud/api/darkiworld/decode/${lienId}${titleId ? `?title_id=${titleId}` : ''}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Referer': 'https://movix.cloud/',
|
||||
'Origin': 'https://movix.cloud',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || data.success === false) {
|
||||
console.error('[Hydracker] Erreur API Movix:', data.error || 'Erreur inconnue');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Récupération du lien direct selon le format de réponse Movix
|
||||
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());
|
||||
@@ -0,0 +1,167 @@
|
||||
import { SearchResult, MediaType, VideoLink } from '../../src/types/source.js';
|
||||
|
||||
export const QUALITY_MAP: Record<number, string> = {
|
||||
89: "REMUX UHD", 57: "REMUX BLURAY", 92: "REMUX DVD",
|
||||
17: "Blu-Ray 1080p", 76: "Blu-Ray 1080p (x265)", 16: "Blu-Ray 720p", 18: "Blu-Ray 3D",
|
||||
52: "HD 1080p", 31: "HD 720p",
|
||||
50: "HDLight 1080p", 86: "HDLight 1080p (x265)", 49: "HDLight 720p",
|
||||
60: "Ultra HDLight (x265)", 53: "ULTRA HD (x265)",
|
||||
55: "WEB 1080p", 83: "WEB 1080p (x265)", 94: "WEB 1080p Light", 54: "WEB 720p", 4: "WEB",
|
||||
62: "HDTV 1080p", 61: "HDTV 720p", 14: "HDTV",
|
||||
15: "HDRip", 1: "DVDRIP", 51: "DVDRIP MKV",
|
||||
13: "ISO", 12: "IMG", 10: "DVD-R", 11: "Full-DVD",
|
||||
};
|
||||
|
||||
export const LANGUAGE_MAP: Record<number, string> = {
|
||||
1: "MULTI", 2: "Arab", 3: "Bengali", 4: "Chinese", 5: "English", 6: "French", 7: "French (Canada)",
|
||||
8: "TrueFrench", 9: "German", 10: "Hindi", 11: "Italian", 12: "Japanese", 13: "Korean",
|
||||
14: "Mandarin", 15: "Portuguese", 16: "Russian", 17: "Spanish", 18: "Turkish", 19: "unknown",
|
||||
23: "Danish", 28: "Finnish", 33: "Swedish", 35: "Bulgarian", 40: "Dutch", 41: "Persian",
|
||||
42: "Indonesian", 43: "Hebrew", 44: "Thai", 49: "Czech", 53: "Albanian", 57: "Greek",
|
||||
61: "Hungarian", 65: "Malaysian", 66: "Norwegian", 68: "Polish", 71: "Lithuanian",
|
||||
78: "Croatian", 84: "Malay", 90: "Romanian", 96: "Ukrainian", 102: "Vietnamese",
|
||||
105: "Sámegiella", 106: "Muet", 108: "Georgian", 110: "Nigerian", 113: "Maasai",
|
||||
117: "Estonian", 120: "Serbian", 123: "Slovak", 124: "Slovenian", 125: "Amharic",
|
||||
126: "Belarusian", 127: "Bosnian", 128: "Burmese", 129: "Dzongkha", 137: "Icelandic",
|
||||
138: "Kazakh", 139: "Kurdish", 140: "Latin", 141: "Latvian", 142: "Macedonian", 143: "Maori",
|
||||
144: "Mongolian", 145: "Norwegian Bokmål", 146: "Serbo-Croatian", 148: "Tagalog", 149: "Tibetan",
|
||||
150: "Walloon", 151: "Wolof", 152: "Yoruba", 154: "Moore", 155: "Quechuan", 156: "Rwanda",
|
||||
160: "Filipino", 161: "VO", 165: "Afrikaans", 171: "Créole", 174: "Gujarati", 175: "Cantonese",
|
||||
177: "FRENCH AD"
|
||||
};
|
||||
|
||||
export const SUB_MAP: Record<number, string> = {
|
||||
1: "Arab", 2: "Bengali", 3: "Chinese", 4: "English", 5: "French", 6: "German", 7: "Hindi",
|
||||
8: "Italian", 9: "Japanese", 10: "Korean", 11: "Mandarin", 12: "Portuguese", 13: "Russian",
|
||||
14: "Spanish", 15: "Turkish", 16: "Inconnu", 17: "Multi", 23: "Danish", 28: "Finnish",
|
||||
33: "Swedish", 35: "Bulgare", 36: "Persian", 37: "Hebrew", 40: "Dutch", 42: "Indonesian",
|
||||
50: "Thai", 53: "Greek", 61: "Hungarian", 65: "Malaysian", 66: "Norwegian", 68: "Polish",
|
||||
71: "Lithuanian", 76: "Czech", 82: "Croatian", 88: "Malay", 94: "Romanian", 100: "Ukrainian",
|
||||
106: "Vietnamese", 112: "Sámegiella", 115: "Estonian", 120: "Serbian", 123: "Slovak",
|
||||
127: "Slovenian", 128: "Afrikaans", 129: "Albanian", 130: "Amharic", 131: "Armenian",
|
||||
132: "Azerbaijani", 133: "Basque", 134: "Belarusian", 135: "Bosnian", 136: "Catalan",
|
||||
137: "Cebuano", 138: "Chichewa", 139: "Corsican", 140: "Esperanto", 141: "Frisian",
|
||||
142: "Galician", 143: "Georgian", 144: "Gujarati", 145: "Haitian Creole", 146: "Hausa",
|
||||
147: "Hawaiian", 148: "Icelandic", 149: "Igbo", 150: "Irish", 151: "Javanese", 152: "Kannada",
|
||||
153: "Kazakh", 154: "Khmer", 155: "Kurdish", 156: "Kyrgyz", 157: "Lao", 158: "Latin",
|
||||
159: "Latvian", 160: "Luxembourgish", 161: "Macedonian", 162: "Malagasy", 163: "Maltese",
|
||||
164: "Maori", 165: "Marathi", 166: "Mongolian", 167: "Myanmar", 168: "Nepali", 169: "Pashto",
|
||||
170: "Punjabi", 171: "Sindhi", 172: "Sinhala", 173: "Somali", 174: "Swahili", 175: "Tajik",
|
||||
176: "Tamil", 177: "Telugu", 178: "Uzbek", 179: "Welsh", 180: "Xhosa", 181: "Yiddish",
|
||||
182: "Yoruba", 183: "Zulu", 184: "Filipino"
|
||||
};
|
||||
|
||||
export function getLangs(l: any): string[] {
|
||||
if (l.langues_compact && l.langues_compact.length) {
|
||||
return l.langues_compact.map((la: any) => la.name || '');
|
||||
}
|
||||
if (l.langues && Array.isArray(l.langues)) {
|
||||
return l.langues.map((id: any) => LANGUAGE_MAP[id] || '');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getSubs(l: any): string[] {
|
||||
if (l.subs_compact && l.subs_compact.length) {
|
||||
return l.subs_compact.map((la: any) => la.name || '');
|
||||
}
|
||||
if (l.subs && Array.isArray(l.subs)) {
|
||||
return l.subs.map((id: any) => SUB_MAP[id] || '');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function formatSize(bytes: number): string {
|
||||
if (!bytes || bytes === 0) return 'N/A';
|
||||
const gb = bytes / (1024 ** 3);
|
||||
if (gb >= 1) return `${gb.toFixed(2)} Go`;
|
||||
const mb = bytes / (1024 ** 2);
|
||||
return `${mb.toFixed(0)} Mo`;
|
||||
}
|
||||
|
||||
export function parseSearchResults(data: any, mediaType: MediaType): SearchResult[] {
|
||||
const EXCLUDED_TYPES = ['games', 'music', 'app', 'ebook', 'emulation'];
|
||||
// Accepte à la fois les entrées avec model_type === 'title' et celles sans ce champ
|
||||
const results = (data.results || []).filter((r: any) =>
|
||||
(!r.model_type || r.model_type === 'title') &&
|
||||
!EXCLUDED_TYPES.includes((r.type || '').toLowerCase())
|
||||
);
|
||||
|
||||
const filtered = results.filter((r: any) => {
|
||||
const rType = (r.type || (r.is_series ? 'series' : 'movie')).toLowerCase();
|
||||
if (mediaType === 'movie') {
|
||||
return rType === 'movie' || rType === 'animes' || rType === 'anime' || rType === 'doc' || rType === 'other';
|
||||
}
|
||||
// Pour les séries
|
||||
return rType === 'series' || rType === 'serie' || rType === 'animes' || rType === 'anime' || rType === 'doc' || rType === 'other';
|
||||
});
|
||||
|
||||
return filtered.map((r: any) => ({
|
||||
title: r.name,
|
||||
year: r.year || (r.release_date ? r.release_date.substring(0, 4) : 'N/A'),
|
||||
image: r.poster || r.image || null,
|
||||
hrefPath: String(r.id),
|
||||
type: r.type || (r.is_series ? 'series' : 'movie'),
|
||||
source: 'hydracker',
|
||||
hydrackerId: String(r.id)
|
||||
}));
|
||||
}
|
||||
|
||||
export function parseTrendingResults(data: any): SearchResult[] {
|
||||
if (!data) return [];
|
||||
const results = (data.pagination || {}).data || data.data || [];
|
||||
return results.map((r: any) => ({
|
||||
title: r.name,
|
||||
year: r.year || (r.release_date ? r.release_date.substring(0, 4) : 'N/A'),
|
||||
image: r.poster || r.image || null,
|
||||
hrefPath: String(r.id),
|
||||
type: r.type || (r.is_series ? 'series' : 'movie'),
|
||||
source: 'hydracker',
|
||||
hydrackerId: String(r.id)
|
||||
})).slice(0, 19);
|
||||
}
|
||||
|
||||
export function parseMovieLinks(data: any): VideoLink[] {
|
||||
const all: any[] = [];
|
||||
if (data.video) all.push(data.video);
|
||||
if (Array.isArray(data.alternative_videos)) all.push(...data.alternative_videos);
|
||||
|
||||
return all.filter(l => l.lien).map(l => ({
|
||||
id: l.id,
|
||||
host: (l.host && l.host.name) ? l.host.name : 'Inconnu',
|
||||
url: l.lien || data.directDL,
|
||||
size: formatSize(l.taille),
|
||||
sizeBytes: l.taille || 0,
|
||||
quality: l.quality || QUALITY_MAP[l.qualite] || 'Inconnu',
|
||||
langs: getLangs(l),
|
||||
subs: getSubs(l),
|
||||
releaseName: l.release || l.name || l.titre || l.titre_release || undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
export function parseSeasons(result: any): number[] {
|
||||
if (result && !result.error) {
|
||||
const seasons = result.seasons || (result.pagination || {}).data || [];
|
||||
if (Array.isArray(seasons) && seasons.length) {
|
||||
return seasons
|
||||
.map((s: any) => typeof s === 'object' ? (s.number || s) : s)
|
||||
.filter((n: any) => typeof n === 'number' && n > 0)
|
||||
.sort((a: number, b: number) => a - b);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function parsePremiumLink(body: string): string | null {
|
||||
let data;
|
||||
try { data = JSON.parse(body); } catch { return null; }
|
||||
|
||||
let lienData = null;
|
||||
if (data.liens && Array.isArray(data.liens) && data.liens.length > 0) {
|
||||
lienData = data.liens[0];
|
||||
} else {
|
||||
lienData = data.lien || data;
|
||||
}
|
||||
|
||||
return lienData.lien || lienData.url || lienData.link || null;
|
||||
}
|
||||
Reference in New Issue
Block a user