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 { 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 { 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 { 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 { // Fallback on movies trending as recent if no specific endpoint return this.getTrending('movie'); } public async getContentLinks(identifier: string, season?: number): Promise { 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 { const content = await this.getContentLinks(identifier); return { links: content.links, seasons: [], isSeries: type === 'series' }; } public async resolveLink(combinedId: string): Promise { 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());