Files
2026-09-15 21:56:10 +02:00

166 lines
5.9 KiB
TypeScript

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<SearchResult[]> {
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<SearchResult[]> {
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<SearchResult[]> {
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<SelectionData> {
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<string, string>();
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<ContentLinks> {
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<boolean> {
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<any> {
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());