v1.5.9 (Actuel)

This commit is contained in:
2026-09-15 21:56:10 +02:00
parent c21cd3ab5e
commit bff992aa4a
56 changed files with 3404 additions and 327 deletions
+207
View File
@@ -0,0 +1,207 @@
import { ISource, SearchResult, SelectionData, ContentLinks, MediaType, VideoLink } from '../../src/types/source.js';
import { sourceRegistry } from '../../src/core/registry.js';
import { MovixAPI } from './api.js';
import { CONFIG } from '../../src/utils/config.js';
class MovixSource implements ISource {
public readonly name = 'movix';
public readonly displayName = 'Movix';
public async healthCheck(): Promise<boolean> {
if (!CONFIG.MOVIX_URL) return false;
try {
// A quick check to see if the search endpoint is reachable
await MovixAPI.search('test');
return true;
} catch (e) {
console.error(`[MOVIX] Health check failed:`, e);
return false;
}
}
public async search(query: string, mediaType?: MediaType): Promise<SearchResult[]> {
try {
const response = await MovixAPI.search(query);
if (!response || !response.results) return [];
const results: SearchResult[] = [];
const searchLower = query.toLowerCase().trim();
for (const item of response.results) {
if (!item.name) continue;
// Filtre optionnel pour aligner les résultats avec la recherche
const nameLower = item.name.toLowerCase();
const originalLower = item.original_title ? item.original_title.toLowerCase() : '';
// On vérifie si la requête est incluse dans le titre ou le titre original
if (!nameLower.includes(searchLower) && !originalLower.includes(searchLower)) {
// Pour être un peu plus permissif, on vérifie si tous les mots clés y sont
const words = searchLower.split(' ');
const allWordsMatch = words.every(w => nameLower.includes(w) || originalLower.includes(w));
if (!allWordsMatch) continue;
}
// Filtrage basique par mediaType si fourni
if (mediaType) {
if (mediaType === 'movie' && item.type !== 'movie') continue;
if (mediaType === 'series' && item.type !== 'serie') continue; // Verify if it's 'serie' or 'series'
}
const hrefPath = `movix:${item.id}:${item.tmdb_id || 0}:${item.type}`;
let image = null;
if (item.poster) {
image = item.poster.startsWith('http') ? item.poster : `https://image.tmdb.org/t/p/w300/${item.poster}`;
}
results.push({
title: item.name,
year: item.year ? item.year.toString() : null,
image,
hrefPath,
type: item.type === 'movie' ? 'movie' : (item.type === 'serie' || item.type === 'series' ? 'series' : 'other'),
source: this.name
});
}
return results;
} catch (e) {
console.error(`[MOVIX] Search error:`, e);
return [];
}
}
public async getTrending(mediaType: MediaType): Promise<SearchResult[]> {
const typeStr = mediaType === 'series' ? 'tv' : 'movie';
const url = `https://api.themoviedb.org/3/trending/${typeStr}/day?api_key=f3d757824f08ea2cff45eb8f47ca3a1e&language=fr-FR`;
try {
const res = await fetch(url);
const data = await res.json();
if (!data || !data.results) return [];
return data.results.map((item: any) => {
const title = item.title || item.name;
const year = item.release_date ? item.release_date.split('-')[0] : (item.first_air_date ? item.first_air_date.split('-')[0] : null);
const image = item.poster_path ? `https://image.tmdb.org/t/p/w300${item.poster_path}` : null;
const tmdbId = item.id;
const type = mediaType === 'series' ? 'series' : 'movie';
// Identifiant spécial pour faire la recherche au moment du clic
const hrefPath = `movix:tmdb:${tmdbId}:${type}:${encodeURIComponent(title)}`;
return {
title,
year,
image,
hrefPath,
type,
source: this.name
};
});
} catch(e) {
console.error(`[MOVIX] TMDB Trending error:`, e);
return [];
}
}
public async getRecent(): Promise<SearchResult[]> {
// Fallback on movies trending as recent if no specific endpoint
return this.getTrending('movie');
}
public async getContentLinks(identifier: string, season?: number): Promise<ContentLinks> {
try {
const parts = identifier.split(':');
if (parts.length < 4) return { links: [] };
let id: string, tmdbId: string, type: string;
if (parts[1] === 'tmdb') {
tmdbId = parts[2];
type = parts[3];
const title = decodeURIComponent(parts.slice(4).join(':'));
// Recherche sur Movix pour récupérer l'ID interne
const searchRes = await MovixAPI.search(title);
const item = searchRes.results?.find((r: any) => String(r.tmdb_id) === String(tmdbId) || r.name === title);
if (!item) {
console.log(`[MOVIX] TMDB item not found on Movix: ${title}`);
return { links: [] };
}
id = item.id;
// Update type depending on what Movix returned
type = item.type === 'serie' || item.type === 'series' ? 'series' : 'movie';
} else {
id = parts[1];
tmdbId = parts[2];
type = parts[3];
}
const data = await MovixAPI.getDownloadLinks(type, id, tmdbId);
const links: VideoLink[] = [];
if (data && data.data) {
// Pour chaque host (1fichier, etc)
for (const item of data.data) {
if (!item.links || !Array.isArray(item.links)) continue;
const host = item.host || 'unknown';
const quality = item.qualite || 'Unknown';
const lang = item.langue || 'Unknown';
const size = item.size || '';
for (const linkObj of item.links) {
const linkId = linkObj.id;
if (!linkId) continue;
links.push({
id: `${linkId}|${id}`, // Store both linkId and titleId
host: host,
label: `${quality} - ${lang}`,
url: null, // Resolves later
size: size,
quality: quality,
langs: [lang]
});
}
}
}
return { links };
} catch (e) {
console.error(`[MOVIX] Error in getContentLinks:`, e);
return { links: [] };
}
}
public async getSelection(identifier: string, type?: string, seasonValue?: string | number): Promise<SelectionData> {
const content = await this.getContentLinks(identifier);
return {
links: content.links,
seasons: [],
isSeries: type === 'series'
};
}
public async resolveLink(combinedId: string): Promise<string | null> {
try {
const [linkId, titleId] = combinedId.split('|');
if (!linkId || !titleId) return null;
const res = await MovixAPI.decodeLink(linkId, titleId);
if (res && res.url) {
return res.url;
}
return null;
} catch (e) {
console.error(`[MOVIX] ResolveLink error for ${combinedId}:`, e);
return null;
}
}
}
sourceRegistry.register(new MovixSource());