Initial commit (v1.5.9)
This commit is contained in:
@@ -0,0 +1,637 @@
|
||||
import express from 'express';
|
||||
import { globalState, getActiveSources, checkSiteStatus, rebuildTrendingFromCache } from '../utils/state.js';
|
||||
import { sourceRegistry } from '../core/registry.js';
|
||||
|
||||
import apiLimiter from '../utils/rateLimiter.js';
|
||||
import authMiddleware, { requireAdmin } from '../utils/authMiddleware.js';
|
||||
import { sendToJDownloader } from '../utils/jdownloader.js';
|
||||
import { MediaType, SearchResult } from '../types/source.js';
|
||||
import { enrichSearchResults } from '../utils/tmdbEnricher.js';
|
||||
import { CONFIG } from '../utils/config.js';
|
||||
import { getAllUsers, createUserWithGeneratedPassword, deleteUser, resetPassword, updateUserPreferences } from '../utils/userStore.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// ========================= STATUS & CONFIG =========================
|
||||
|
||||
router.get('/api/status', apiLimiter, authMiddleware, (req, res) => {
|
||||
const labels: Record<string, string> = {};
|
||||
for (const source of sourceRegistry.getAllRegistered()) {
|
||||
labels[source.name] = source.displayName || source.name.toUpperCase();
|
||||
}
|
||||
res.json({
|
||||
isOffline: globalState.isSiteOffline,
|
||||
message: globalState.siteOfflineMessage,
|
||||
activeSources: globalState.activeSources,
|
||||
availableSources: sourceRegistry.getAvailableNames(),
|
||||
sourceLabels: labels
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/api/trending', apiLimiter, authMiddleware, (req, res) => {
|
||||
res.json({
|
||||
films: globalState.trendingFilms || [],
|
||||
series: globalState.trendingSeries || [],
|
||||
recent: globalState.recentItems || [],
|
||||
isOffline: globalState.isSiteOffline,
|
||||
message: globalState.siteOfflineMessage
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle sources
|
||||
router.post('/api/set-sources', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const { sources } = req.body;
|
||||
if (!Array.isArray(sources)) {
|
||||
return res.status(400).json({ error: "Format invalide, un tableau de sources est attendu." });
|
||||
}
|
||||
|
||||
const validSources = sources.filter(s => sourceRegistry.has(s));
|
||||
|
||||
// Comparer les ensembles pour voir si l'ensemble des sources actives a changé (indépendamment de l'ordre)
|
||||
const oldSet = new Set(globalState.activeSources);
|
||||
const newSet = new Set(validSources);
|
||||
const hasSetChanged = oldSet.size !== newSet.size || [...oldSet].some(s => !newSet.has(s));
|
||||
|
||||
globalState.activeSources = validSources;
|
||||
console.log(`[Source] Sources actives mises à jour: ${validSources.map(s => s.toUpperCase()).join(', ')}`);
|
||||
|
||||
// Sauvegarde persistante des sources
|
||||
import('../utils/settingsManager.js').then(({ saveSettings }) => {
|
||||
saveSettings({ activeSources: validSources });
|
||||
});
|
||||
|
||||
if (hasSetChanged) {
|
||||
// Uniquement si l'ensemble a changé, lancer un scan réseau
|
||||
await checkSiteStatus();
|
||||
} else {
|
||||
// Si seul l'ordre a changé, reconstruire les tendances depuis le cache en mémoire
|
||||
await rebuildTrendingFromCache();
|
||||
}
|
||||
|
||||
res.json({ success: true, activeSources: globalState.activeSources });
|
||||
});
|
||||
|
||||
// ========================= RECHERCHE =========================
|
||||
|
||||
router.post('/api/search', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const {
|
||||
title,
|
||||
mediaType: rawTypeInput,
|
||||
type: typeInput,
|
||||
source: reqSource,
|
||||
src: reqSrc,
|
||||
sources: reqSources,
|
||||
mergeResults: reqMergeResults,
|
||||
mergeresult: reqMergeResult
|
||||
} = req.body;
|
||||
|
||||
// Handle both naming conventions and stringified booleans
|
||||
const mergeResults = (reqMergeResults !== undefined ? reqMergeResults : reqMergeResult) !== false &&
|
||||
(reqMergeResults !== 'false' && reqMergeResult !== 'false');
|
||||
|
||||
const rawType = rawTypeInput || typeInput || 'film';
|
||||
const mediaType = rawType === 'film' ? 'movie' : (rawType === 'serie' ? 'series' : rawType);
|
||||
|
||||
if (!title) return res.status(400).json({ error: "Titre manquant." });
|
||||
|
||||
// Determine which sources to query
|
||||
let sources = getActiveSources();
|
||||
const filterSources = reqSources || reqSource || reqSrc;
|
||||
|
||||
if (filterSources) {
|
||||
const targetNames = Array.isArray(filterSources) ? filterSources : [filterSources];
|
||||
sources = sources.filter(s => targetNames.includes(s.name));
|
||||
}
|
||||
|
||||
if (sources.length === 0) return res.status(500).json({ error: "Aucune source active correspondant à la demande." });
|
||||
|
||||
console.log(`\n--- Recherche [${sources.map(s => s.name.toUpperCase()).join(', ')}]: "${title}" (${mediaType}) ---`);
|
||||
|
||||
try {
|
||||
const resultsPromises = sources.map(async (source) => {
|
||||
try {
|
||||
const results = await source.search(title, mediaType as MediaType);
|
||||
return { sourceName: source.name, results };
|
||||
} catch (e: any) {
|
||||
console.error(`Erreur recherche sur ${source.name}:`, e.message);
|
||||
return { sourceName: source.name, error: e.message };
|
||||
}
|
||||
});
|
||||
|
||||
const allResultsRaw = await Promise.all(resultsPromises);
|
||||
|
||||
if (mergeResults) {
|
||||
let allResults: SearchResult[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
allResultsRaw.forEach(item => {
|
||||
if (item.results) {
|
||||
allResults = allResults.concat(item.results);
|
||||
} else if (item.error) {
|
||||
errors.push(`${item.sourceName}: ${item.error}`);
|
||||
}
|
||||
});
|
||||
|
||||
if (!allResults.length) {
|
||||
if (errors.length > 0) {
|
||||
return res.status(500).json({ error: `Erreur(s): ${errors.join(', ')}` });
|
||||
}
|
||||
return res.status(404).json({ error: "Aucun résultat trouvé." });
|
||||
}
|
||||
|
||||
allResults = await enrichSearchResults(allResults);
|
||||
|
||||
res.json(allResults);
|
||||
} else {
|
||||
// Return grouped results
|
||||
const grouped: Record<string, SearchResult[] | { error: string }> = {};
|
||||
for (const item of allResultsRaw) {
|
||||
if (item.results) {
|
||||
grouped[item.sourceName] = await enrichSearchResults(item.results);
|
||||
} else {
|
||||
grouped[item.sourceName] = { error: item.error! };
|
||||
}
|
||||
}
|
||||
res.json(grouped);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Erreur /search:", error.message);
|
||||
res.status(500).json({ error: `Erreur serveur: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= SÉLECTION =========================
|
||||
|
||||
const handleSelectContent: express.RequestHandler = async (req, res) => {
|
||||
const { hrefPath, title, type, source } = req.body;
|
||||
if (!hrefPath || !title || !source) return res.status(400).json({ error: "Données manquantes." });
|
||||
|
||||
const activeSource = sourceRegistry.get(source);
|
||||
if (!activeSource) return res.status(500).json({ error: `Source "${source}" introuvable ou inactive.` });
|
||||
|
||||
console.log(`\n--- Sélection [${activeSource.name.toUpperCase()}]: "${title}" ---`);
|
||||
|
||||
try {
|
||||
globalState.currentTitleName = title;
|
||||
globalState.currentIdentifier = hrefPath;
|
||||
globalState.currentSelectionSource = source; // On enregistre la source de cette sélection
|
||||
globalState.directUrlMap = {};
|
||||
|
||||
const selection = await activeSource.getSelection(hrefPath, type);
|
||||
|
||||
globalState.isSeries = selection.isSeries;
|
||||
globalState.currentLiens = selection.links;
|
||||
|
||||
// Tri des liens selon l'ordre préféré
|
||||
const { CONFIG } = await import('../utils/config.js');
|
||||
const preferredHosters: string[] = CONFIG.PREFERRED_HOSTERS || [];
|
||||
|
||||
selection.links.sort((a: any, b: any) => {
|
||||
const getIndex = (host: string) => {
|
||||
if (!host) return 999;
|
||||
const h = host.toLowerCase();
|
||||
const idx = preferredHosters.findIndex(pref => h.includes(pref.toLowerCase()) || pref.toLowerCase().includes(h));
|
||||
return idx === -1 ? 999 : idx;
|
||||
};
|
||||
return getIndex(a.host) - getIndex(b.host);
|
||||
});
|
||||
|
||||
selection.links.forEach((link: any, i: number) => {
|
||||
const key = link.id != null ? String(link.id) : String(i);
|
||||
if (link.url) globalState.directUrlMap[key] = link.url;
|
||||
});
|
||||
|
||||
res.json({
|
||||
clientOptions: selection.links,
|
||||
hasNextPage: false,
|
||||
seasons: selection.seasons
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error("Erreur sélection:", error.message);
|
||||
res.status(500).json({ error: `Erreur serveur: ${error.message}` });
|
||||
}
|
||||
};
|
||||
|
||||
router.post('/api/select-movie', apiLimiter, authMiddleware, handleSelectContent);
|
||||
router.post('/api/select-trending', apiLimiter, authMiddleware, handleSelectContent);
|
||||
|
||||
// ========================= GET LINK =========================
|
||||
|
||||
router.post('/api/get-link', apiLimiter, authMiddleware, async (req, res) => {
|
||||
if (req.body.chosenId == null) return res.status(400).json({ error: "ID manquant." });
|
||||
const chosenId = String(req.body.chosenId);
|
||||
const useJD = req.body.useJD !== false && req.body.useJD !== 'false';
|
||||
|
||||
const { currentTitleName, isSeries, directUrlMap, currentSelectionSource } = globalState;
|
||||
const activeSource = currentSelectionSource ? sourceRegistry.get(currentSelectionSource) : null;
|
||||
|
||||
console.log(`\n--- Get Link [${activeSource?.name.toUpperCase()}]: ID ${chosenId} pour "${currentTitleName}" (JD: ${useJD}) ---`);
|
||||
|
||||
try {
|
||||
let finalLink: any = null;
|
||||
|
||||
if (directUrlMap[chosenId]) {
|
||||
finalLink = directUrlMap[chosenId];
|
||||
} else if (activeSource?.resolveLink) {
|
||||
finalLink = await activeSource.resolveLink(chosenId);
|
||||
}
|
||||
|
||||
if (!finalLink) throw new Error("Impossible de résoudre le lien.");
|
||||
|
||||
if (typeof finalLink === 'object' && finalLink.captcha) {
|
||||
return res.json({ status: 'challenge', challenge: finalLink });
|
||||
}
|
||||
|
||||
console.log(`🎉 Lien final: ${finalLink}`);
|
||||
|
||||
if (useJD) {
|
||||
await sendToJDownloader(finalLink, currentTitleName || 'Unknown', isSeries);
|
||||
res.json({ status: 'succès', message: 'Lien envoyé à JDownloader !', link: finalLink });
|
||||
} else {
|
||||
res.json({ status: 'succès', message: 'Lien récupéré !', link: finalLink });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Erreur /get-link:", error.message);
|
||||
res.status(500).json({ error: `Erreur serveur: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= GET LINKS BATCH =========================
|
||||
|
||||
router.post('/api/get-links-batch', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const { chosenIds, useJD } = req.body;
|
||||
if (!chosenIds || !Array.isArray(chosenIds)) return res.status(400).json({ error: "Tableau d'IDs manquant." });
|
||||
|
||||
const { currentTitleName, isSeries, directUrlMap, currentSelectionSource } = globalState;
|
||||
const activeSource = currentSelectionSource ? sourceRegistry.get(currentSelectionSource) : null;
|
||||
|
||||
console.log(`\n--- Get Links Batch [${activeSource?.name.toUpperCase()}]: ${chosenIds.length} liens pour "${currentTitleName}" (JD: ${useJD !== false}) ---`);
|
||||
|
||||
try {
|
||||
const results: string[] = [];
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const chosenId of chosenIds) {
|
||||
try {
|
||||
let finalLink: any = null;
|
||||
if (directUrlMap[String(chosenId)]) {
|
||||
finalLink = directUrlMap[String(chosenId)];
|
||||
} else if (activeSource?.resolveLink) {
|
||||
finalLink = await activeSource.resolveLink(String(chosenId));
|
||||
}
|
||||
|
||||
if (finalLink && typeof finalLink === 'object' && finalLink.captcha) {
|
||||
return res.json({ status: 'challenge', challenge: finalLink });
|
||||
}
|
||||
|
||||
if (finalLink) {
|
||||
results.push(finalLink);
|
||||
if (useJD !== false && useJD !== 'false') {
|
||||
await sendToJDownloader(finalLink, currentTitleName || 'Unknown', isSeries);
|
||||
}
|
||||
} else {
|
||||
errors.push(`ID ${chosenId} introuvable.`);
|
||||
}
|
||||
} catch (err: any) {
|
||||
errors.push(`Erreur pour ID ${chosenId}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return res.status(500).json({ error: "Aucun lien n'a pu être résolu.", details: errors });
|
||||
}
|
||||
|
||||
if (useJD !== false && useJD !== 'false') {
|
||||
res.json({ status: 'succès', message: `${results.length} lien(s) envoyé(s) à JDownloader !`, errors: errors.length > 0 ? errors : undefined });
|
||||
} else {
|
||||
res.json({ status: 'succès', message: `${results.length} lien(s) récupéré(s) !`, links: results, errors: errors.length > 0 ? errors : undefined });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Erreur /get-links-batch:", error.message);
|
||||
res.status(500).json({ error: `Erreur serveur: ${error.message}` });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= MOVIEX DECODE =========================
|
||||
|
||||
router.get('/api/movix-decode/:lienId', apiLimiter, authMiddleware, async (req, res) => {
|
||||
try {
|
||||
const { lienId } = req.params;
|
||||
const sourceHydracker = sourceRegistry.get('hydracker') as any;
|
||||
|
||||
if (!sourceHydracker) {
|
||||
return res.status(400).json({ error: "Le plugin Hydracker n'est pas actif." });
|
||||
}
|
||||
|
||||
const link = await sourceHydracker.resolveMovixLink(lienId);
|
||||
|
||||
if (link) {
|
||||
res.json({ success: true, link });
|
||||
} else {
|
||||
res.status(404).json({ error: "Impossible de débrider ce lien via Movix." });
|
||||
}
|
||||
} catch (error: any) {
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= SAISONS =========================
|
||||
|
||||
router.post('/api/select-season', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const { seasonValue } = req.body;
|
||||
const activeSource = globalState.currentSelectionSource ? sourceRegistry.get(globalState.currentSelectionSource) : null;
|
||||
|
||||
console.log(`\n--- Changement de Saison [${activeSource?.name.toUpperCase()}] (${seasonValue}) ---`);
|
||||
|
||||
try {
|
||||
if (!activeSource) throw new Error("Aucune source active.");
|
||||
|
||||
const selection = await activeSource.getSelection(
|
||||
globalState.currentIdentifier!,
|
||||
undefined,
|
||||
seasonValue
|
||||
);
|
||||
|
||||
globalState.directUrlMap = {};
|
||||
selection.links.forEach((link: any, i: number) => {
|
||||
const key = link.id != null ? String(link.id) : String(i);
|
||||
if (link.url) globalState.directUrlMap[key] = link.url;
|
||||
});
|
||||
globalState.currentLiens = selection.links;
|
||||
|
||||
res.json({ clientOptions: selection.links, hasNextPage: false });
|
||||
} catch (error: any) {
|
||||
console.error("Erreur /select-season:", error.message);
|
||||
res.status(500).json({ error: "Erreur lors du changement de saison." });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= JD DOWNLOAD STATUS =========================
|
||||
|
||||
router.get('/api/download-status', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const jdQuery = {
|
||||
params: [{ "running": true, "name": true, "bytesLoaded": true, "bytesTotal": true, "uuid": true, "packageUUID": true, "finished": true }],
|
||||
id: Date.now(), methodName: "queryLinks"
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`http://${CONFIG.JD_HOST}:${CONFIG.JD_API_PORT}/downloadsV2/queryLinks`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(jdQuery)
|
||||
});
|
||||
if (!response.ok) throw new Error(`API JD non-OK: ${response.status}`);
|
||||
const data = await response.json();
|
||||
let items: any[] = [];
|
||||
if (data && data.data) {
|
||||
items = data.data.map((item: any) => {
|
||||
let percent = 0;
|
||||
if (item.bytesTotal > 0) percent = (item.bytesLoaded / item.bytesTotal) * 100;
|
||||
if (item.bytesLoaded > 0 && item.bytesLoaded === item.bytesTotal) percent = 100;
|
||||
return { name: item.name, percent, uuid: item.uuid, packageUUID: item.packageUUID, finished: item.finished || percent >= 100 };
|
||||
});
|
||||
}
|
||||
res.json(items);
|
||||
} catch (error: any) {
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'UND_ERR_CONNECT_TIMEOUT') { res.json([]); }
|
||||
else { res.status(500).json({ error: "Erreur API JDownloader" }); }
|
||||
}
|
||||
});
|
||||
|
||||
// Suppression d'un lien JDownloader
|
||||
router.post('/api/jd/remove-link', apiLimiter, authMiddleware, async (req, res) => {
|
||||
const { linkIds } = req.body;
|
||||
if (!linkIds || !linkIds.length) return res.status(400).json({ error: 'linkIds requis.' });
|
||||
try {
|
||||
const response = await fetch(`http://${CONFIG.JD_HOST}:${CONFIG.JD_API_PORT}/downloadsV2/removeLinks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ params: [linkIds, []] })
|
||||
});
|
||||
if (!response.ok) throw new Error(`API JD non-OK: ${response.status}`);
|
||||
console.log(`[JD] Suppression de ${linkIds.length} lien(s).`);
|
||||
res.json({ success: true, message: `${linkIds.length} lien(s) supprimé(s).` });
|
||||
} catch (error: any) {
|
||||
console.error('[JD] Erreur suppression:', error.message);
|
||||
res.status(500).json({ error: 'Erreur lors de la suppression JDownloader.' });
|
||||
}
|
||||
});
|
||||
|
||||
// ========================= ADMIN — GESTION DES USERS =========================
|
||||
|
||||
/** GET /admin/users — Liste tous les utilisateurs (admin only) */
|
||||
router.get('/api/admin/users', apiLimiter, authMiddleware, requireAdmin, (req, res) => {
|
||||
try {
|
||||
const users = getAllUsers();
|
||||
res.json(users);
|
||||
} catch (error: any) {
|
||||
console.error('[Admin] Erreur liste users:', error.message);
|
||||
res.status(500).json({ error: 'Erreur serveur.' });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /admin/users — Créer un utilisateur (admin only) */
|
||||
router.post('/api/admin/users', apiLimiter, authMiddleware, requireAdmin, (req, res) => {
|
||||
const { username, role } = req.body;
|
||||
|
||||
if (!username || typeof username !== 'string') {
|
||||
return res.status(400).json({ error: "Nom d'utilisateur requis." });
|
||||
}
|
||||
|
||||
const userRole = role === 'admin' ? 'admin' : 'user';
|
||||
|
||||
try {
|
||||
const result = createUserWithGeneratedPassword(username.trim(), userRole);
|
||||
console.log(`[Admin] Utilisateur créé par ${(req.session as any).user?.username}: ${result.user.username} (${userRole})`);
|
||||
res.json({
|
||||
success: true,
|
||||
user: result.user,
|
||||
generatedPassword: result.clearPassword,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Admin] Erreur création user:', error.message);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/** DELETE /admin/users/:id — Supprimer un utilisateur (admin only) */
|
||||
router.delete('/api/admin/users/:id', apiLimiter, authMiddleware, requireAdmin, (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
const currentUser = (req.session as any).user;
|
||||
|
||||
// Interdire l'auto-suppression
|
||||
if (currentUser && currentUser.id === id) {
|
||||
return res.status(400).json({ error: 'Impossible de supprimer votre propre compte.' });
|
||||
}
|
||||
|
||||
try {
|
||||
deleteUser(id);
|
||||
console.log(`[Admin] Utilisateur supprimé par ${currentUser?.username}: ID ${id}`);
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('[Admin] Erreur suppression user:', error.message);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /admin/users/:id/reset-password — Reset le mot de passe (admin only) */
|
||||
router.post('/api/admin/users/:id/reset-password', apiLimiter, authMiddleware, requireAdmin, (req, res) => {
|
||||
const id = req.params.id as string;
|
||||
|
||||
try {
|
||||
const result = resetPassword(id);
|
||||
console.log(`[Admin] Password reset par ${(req.session as any).user?.username} pour ID ${id}`);
|
||||
res.json({
|
||||
success: true,
|
||||
generatedPassword: result.clearPassword,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('[Admin] Erreur reset password:', error.message);
|
||||
res.status(400).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /admin/plugins/save — Enregistre la config dynamique des plugins */
|
||||
router.post('/api/admin/plugins/save', apiLimiter, authMiddleware, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { config } = req.body;
|
||||
if (!config || typeof config !== 'object') {
|
||||
return res.status(400).json({ error: 'Configuration invalide.' });
|
||||
}
|
||||
|
||||
const { configManager } = await import('../utils/config.js');
|
||||
for (const [key, value] of Object.entries(config)) {
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
if (trimmed !== '') {
|
||||
configManager.set(key, trimmed);
|
||||
} else {
|
||||
configManager.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
configManager.save();
|
||||
|
||||
const { sourceRegistry } = await import('../core/registry.js');
|
||||
await sourceRegistry.initialize();
|
||||
|
||||
const { globalState } = await import('../utils/state.js');
|
||||
const availableSources = sourceRegistry.getAvailableNames();
|
||||
|
||||
// Add any newly available sources to globalState.activeSources
|
||||
for (const s of availableSources) {
|
||||
if (!globalState.activeSources.includes(s)) {
|
||||
globalState.activeSources.push(s);
|
||||
}
|
||||
}
|
||||
// Remove any sources that are no longer available
|
||||
globalState.activeSources = globalState.activeSources.filter(s => availableSources.includes(s));
|
||||
|
||||
const { clearTmdbCache } = await import('../utils/tmdbEnricher.js');
|
||||
clearTmdbCache();
|
||||
|
||||
const { checkSiteStatus } = await import('../utils/state.js');
|
||||
await checkSiteStatus(); // Re-scrape all sites and update trending
|
||||
|
||||
res.json({ success: true, message: 'Configuration enregistrée et plugins rechargés.', activeSources: availableSources });
|
||||
} catch (e: any) {
|
||||
console.error('[API] Erreur save plugins:', e.message);
|
||||
res.status(500).json({ error: "Erreur interne lors de l'enregistrement de la configuration." });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /admin/plugins/test — Teste et recharge les plugins */
|
||||
router.post('/api/admin/plugins/test', apiLimiter, authMiddleware, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { sourceRegistry } = await import('../core/registry.js');
|
||||
await sourceRegistry.initialize();
|
||||
|
||||
const { globalState } = await import('../utils/state.js');
|
||||
const active = sourceRegistry.getAvailableNames();
|
||||
|
||||
for (const s of active) {
|
||||
if (!globalState.activeSources.includes(s)) globalState.activeSources.push(s);
|
||||
}
|
||||
globalState.activeSources = globalState.activeSources.filter(s => active.includes(s));
|
||||
|
||||
res.json({ success: true, activeSources: active });
|
||||
} catch (e: any) {
|
||||
console.error('[API] Erreur test plugins:', e.message);
|
||||
res.status(500).json({ error: "Erreur interne lors du test des plugins." });
|
||||
}
|
||||
});
|
||||
|
||||
/** POST /admin/hosters/save — Enregistre l'ordre des hébergeurs préférés */
|
||||
router.post('/api/admin/hosters/save', apiLimiter, authMiddleware, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { preferredHosters } = req.body;
|
||||
if (!Array.isArray(preferredHosters)) {
|
||||
return res.status(400).json({ error: 'Format invalide.' });
|
||||
}
|
||||
|
||||
const { configManager } = await import('../utils/config.js');
|
||||
configManager.set('PREFERRED_HOSTERS', preferredHosters);
|
||||
configManager.save();
|
||||
|
||||
res.json({ success: true, message: 'Ordre enregistré.' });
|
||||
} catch (e: any) {
|
||||
console.error('[API] Erreur save hosters:', e.message);
|
||||
res.status(500).json({ error: "Erreur interne lors de l'enregistrement de l'ordre." });
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// PREFERENCES
|
||||
// ============================================================
|
||||
|
||||
router.post('/api/preferences', apiLimiter, authMiddleware, (req, res) => {
|
||||
const session = req.session as any;
|
||||
const { key, value } = req.body;
|
||||
|
||||
if (!key || typeof key !== 'string') {
|
||||
return res.status(400).json({ error: "Clé de préférence manquante ou invalide." });
|
||||
}
|
||||
|
||||
try {
|
||||
updateUserPreferences(session.user.id, { [key]: value });
|
||||
// Mettre à jour la session en mémoire
|
||||
if (!session.user.preferences) session.user.preferences = {};
|
||||
session.user.preferences[key] = value;
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (e: any) {
|
||||
console.error('[API] Erreur sauvegarde préférence:', e.message);
|
||||
res.status(500).json({ error: "Impossible de sauvegarder la préférence." });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** GET /admin/check-update — Vérifie les mises à jour depuis l'API publique */
|
||||
router.get('/api/admin/check-update', apiLimiter, authMiddleware, requireAdmin, async (req, res) => {
|
||||
try {
|
||||
const { CONFIG } = await import('../utils/config.js');
|
||||
const updateUrl = 'https://agora.nolhantirer.space/api/public/version';
|
||||
|
||||
// Pour éviter de crasher si l'URL n'existe pas, on met un timeout court.
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||
|
||||
try {
|
||||
const response = await fetch(updateUrl, { signal: controller.signal });
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
res.json({ success: true, currentVersion: req.app.locals.appVersion, latestVersion: data.version, notes: data.notes, updateInfo: data });
|
||||
} else {
|
||||
res.status(502).json({ error: 'Serveur de mise à jour injoignable (Erreur HTTP ' + response.status + ').' });
|
||||
}
|
||||
} catch (fetchErr) {
|
||||
clearTimeout(timeoutId);
|
||||
res.status(502).json({ error: 'Serveur de mise à jour injoignable ou hors ligne.' });
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[API] Erreur check update:', e.message);
|
||||
res.status(500).json({ error: "Erreur inattendue lors de la vérification de la mise à jour." });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user