import { ISource, SearchResult, SelectionData, ContentLinks, MediaType } from '../../src/types/source.js'; import { CONFIG } from '../../src/utils/config.js'; export class LoadixSource implements ISource { name = 'loadix'; displayName = 'Loadix'; private get frontUrl() { return (CONFIG.LOADIX_URL || '').replace(/\/+$/, ''); } private get baseUrl() { const urlObj = new URL(this.frontUrl); return `https://api.${urlObj.host}/api`; } private tmdbImageBase = 'https://image.tmdb.org/t/p/w500'; private mapType(type: string): MediaType { if (type === 'series') return 'series'; if (type === 'anime') return 'anime'; return 'movie'; } private formatSearchResult(hit: any): SearchResult { return { title: hit.title, year: hit.year ? hit.year.toString() : null, image: hit.posterPath ? `${this.tmdbImageBase}${hit.posterPath}` : null, hrefPath: `${this.frontUrl}/media/${hit.id}`, type: this.mapType(hit.type), source: this.name }; } async search(query: string, mediaType?: MediaType): Promise { const url = `${this.baseUrl}/media/search?q=${encodeURIComponent(query)}&page=1&pageSize=30`; const res = await fetch(url); const data = await res.json(); let hits = data.hits || []; if (mediaType && mediaType !== 'other') { hits = hits.filter((h: any) => this.mapType(h.type) === mediaType); } return hits.map((h: any) => this.formatSearchResult(h)); } async getTrending(mediaType: MediaType): Promise { const url = `${this.baseUrl}/media/search?q=&page=1&pageSize=30&sort=click_count_desc`; const res = await fetch(url); const data = await res.json(); let hits = data.hits || []; if (mediaType && mediaType !== 'other') { hits = hits.filter((h: any) => this.mapType(h.type) === mediaType); } return hits.map((h: any) => this.formatSearchResult(h)); } async getRecent(): Promise { const url = `${this.baseUrl}/media/recent?limit=24`; const res = await fetch(url); const data = await res.json(); const items = data.items || []; return items.map((h: any) => this.formatSearchResult(h)); } async getSelection(identifier: string, type?: string, seasonValue?: string | number): Promise { const idMatch = identifier.match(/media\/([a-f0-9\-]+)/); if (!idMatch) throw new Error("URL Loadix invalide."); const mediaId = idMatch[1]; // Fetch links const url = `${this.baseUrl}/media/${mediaId}/links?page=1&perPage=100&sort=scope_asc`; const res = await fetch(url); const data = await res.json(); const items = data.items || []; const links = items.map((item: any) => { let episode = null; if (item.scope === 'season' && item.seasonNumber) { episode = `S${String(item.seasonNumber).padStart(2, '0')}`; } else if (item.scope === 'episode' && item.seasonNumber && item.episodeNumber) { episode = `S${String(item.seasonNumber).padStart(2, '0')}E${String(item.episodeNumber).padStart(2, '0')}`; } return { id: `${identifier}|${item.id}`, host: item.provider || 'unknown', quality: item.quality, langs: item.language ? [item.language] : [], sizeBytes: item.sizeBytes ? parseInt(item.sizeBytes) : undefined, size: item.sizeHuman, releaseName: item.releaseGroup, episode: episode, url: null // Protected by Turnstile, resolved later by direct redirect }; }); // Check if there are any episodes/seasons to determine if it's a series const isSeries = links.some((l: any) => l.episode); // Extract seasons (just based on found links) const seasonsMap = new Map(); if (isSeries) { items.forEach((item: any) => { if (item.seasonNumber) { const seasonStr = `Saison ${item.seasonNumber}`; seasonsMap.set(String(item.seasonNumber), seasonStr); } }); } const seasons = Array.from(seasonsMap.entries()).map(([val, label]) => ({ value: val, label: label })); return { links, seasons, isSeries }; } async getContentLinks(identifier: string, season?: number): Promise { const selection = await this.getSelection(identifier); let links = selection.links; if (season) { const seasonPrefix = `S${String(season).padStart(2, '0')}`; links = links.filter(l => l.episode && l.episode.startsWith(seasonPrefix)); } return { links }; } async healthCheck(): Promise { if (!this.frontUrl) { console.warn('[Loadix] ⚠️ LOADIX_URL non définie.'); return false; } try { const results = await this.getRecent(); return results.length > 0; } catch (e: any) { console.error(`[Loadix] Healthcheck failed: ${e.message}`); return false; } } async resolveLink(linkId: string, extraData?: any): Promise { const [url] = linkId.split('|'); // Like Flixart, Turnstile cannot be solved on localhost. // We directly return the manual redirection challenge to open Loadix. return { captcha: 'turnstile', url: url, sourceName: 'Loadix' }; } } // Auto-registration import { sourceRegistry } from '../../src/core/registry.js'; sourceRegistry.register(new LoadixSource());