v1.5.9 (Actuel)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { CONFIG } from '../../src/utils/config.js';
|
||||
|
||||
export class MovixAPI {
|
||||
private static get baseUrl(): string {
|
||||
return CONFIG.MOVIX_URL || '';
|
||||
}
|
||||
|
||||
private static get apiUrl(): string {
|
||||
if (!this.baseUrl) return '';
|
||||
try {
|
||||
const url = new URL(this.baseUrl);
|
||||
return `${url.protocol}//api.${url.host}/api`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private static getHeaders() {
|
||||
return {
|
||||
'Accept': 'application/json, text/plain, */*',
|
||||
'Origin': this.baseUrl,
|
||||
'Referer': `${this.baseUrl}/`,
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36 OPR/133.0.0.0'
|
||||
};
|
||||
}
|
||||
|
||||
public static async search(query: string): Promise<any> {
|
||||
const url = `${this.apiUrl}/search?title=${encodeURIComponent(query)}`;
|
||||
const res = await fetch(url, { headers: this.getHeaders() });
|
||||
if (!res.ok) throw new Error(`Movix Search HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
public static async getDownloadLinks(type: string, id: number | string, tmdbId: number | string): Promise<any> {
|
||||
const url = `${this.apiUrl}/darkiworld/download/${type}/${id}?tmdbId=${tmdbId}`;
|
||||
const res = await fetch(url, { headers: this.getHeaders() });
|
||||
if (!res.ok) throw new Error(`Movix Download HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
public static async decodeLink(linkId: string | number, titleId: string | number): Promise<any> {
|
||||
const url = `${this.apiUrl}/darkiworld/decode/${linkId}?title_id=${titleId}`;
|
||||
const res = await fetch(url, { headers: this.getHeaders() });
|
||||
if (!res.ok) throw new Error(`Movix Decode HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
Reference in New Issue
Block a user