1
0
postauto/autopost/server.js

799 lines
28 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const express = require('express');
const session = require('express-session');
const FileStore = require('session-file-store')(session);
const path = require('path');
const fs = require('fs');
const AnsiToHtml = require('ansi-to-html');
const convert = new AnsiToHtml();
const { exec } = require('child_process');
const os = require('os');
const config = require('./config');
const db = require('./db');
db.testConnection(); // vérification au démarrage
const app = express();
const port = config.port;
// Middleware pour parser les formulaires POST
app.use(express.urlencoded({ extended: true }));
app.use(session({
store: new FileStore({
path: './sessions', // dossier où stocker les fichiers
ttl: 24 * 60 * 60, // durée de vie en secondes (ici 1 jour)
retries: 0
}),
secret: config.sessionSecret,
resave: false,
saveUninitialized: false,
cookie: {
maxAge: 24 * 60 * 60 * 1000 // 1 jour en ms
}
}));
app.use(express.static('public'));
const autopostRouter = express.Router();
app.use('/js', express.static(
path.join(__dirname, '../node_modules/@tailwindcss/browser/dist'),
{ setHeaders: (res) => res.type('application/javascript') } // optionnel
));
app.use('/jquery', express.static(
path.join(__dirname, '../node_modules/jquery/dist'),
{ setHeaders: (res) => res.type('application/javascript') } // optionnel
));
// --------------------------- Auth non protégée -----------------------------
autopostRouter.get('/login', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Login</title>
<!-- Inclusion de Tailwind CSS via le CDN -->
<script src="/js/index.global.js"></script>
</head>
<body class="bg-slate-900 flex items-center justify-center min-h-screen">
<div class="bg-slate-700 p-8 rounded-lg shadow-md w-80">
<h2 class="text-center text-2xl font-bold text-white mb-4">Authentification</h2>
<form method="POST" action="/autopost/login">
<input type="text" name="username" placeholder="Identifiant" required
class="w-full p-2 mb-4 rounded border border-gray-300"/>
<input type="password" name="password" placeholder="Mot de passe" required
class="w-full p-2 mb-4 rounded border border-gray-300"/>
<button type="submit"
class="w-full p-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Se connecter
</button>
</form>
</div>
</body>
</html>
`);
});
autopostRouter.post('/login', (req, res) => {
const { username, password } = req.body;
if (username === config.auth.username && password === config.auth.password) {
req.session.authenticated = true;
res.redirect('/autopost');
} else {
res.send('Identifiants invalides. <a href="/autopost/login">Réessayer</a>');
}
});
autopostRouter.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/autopost/login');
});
function checkAuth(req, res, next) {
if (req.session && req.session.authenticated) {
next();
} else {
res.redirect(req.baseUrl + '/login');
}
}
autopostRouter.use(checkAuth);
// --------------------------- PAGE PRINCIPALE -----------------------------
autopostRouter.get('/', async (req, res) => {
const limit = 100; // enregistrements par page
const page = parseInt(req.query.page) || 1;
const offset = (page - 1) * limit;
try {
const [stats] = await db.query(`
SELECT
COUNT(*) AS total,
SUM(status = 0) AS attente,
SUM(status = 1) AS termine,
SUM(status = 2) AS erreur,
SUM(status = 3) AS deja,
SUM(status = 4) AS encours
FROM \`${config.DB_TABLE}\`
`);
// Récupérer le nombre total d'enregistrements
const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``);
const totalRecords = countResult[0].total;
const totalPages = Math.ceil(totalRecords / limit);
const [rows] = await db.query(
`SELECT nom, status, id FROM \`${config.DB_TABLE}\` ORDER BY (status = 2) DESC, id DESC LIMIT ? OFFSET ?`,
[limit, offset]
);
let html = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Suivi Autopost</title>
<script src="/js/index.global.js"></script>
<script src="/jquery/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet">
</head>
<body class="bg-slate-900 text-white font-sans p-4">
<div class="w-full px-4">
<div class="mb-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl md:text-3xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-cyan-300">
Suivi Autopost
</h1>
<a href="/autopost/logout"
class="inline-flex items-center gap-2 px-3 py-2 rounded-xl bg-red-600/90 hover:bg-red-600 text-white shadow-lg shadow-red-900/20 transition">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a2 2 0 01-2 2H6a2 2 0 01-2-2V7a2 2 0 012-2h5a2 2 0 012 2v1"/>
</svg>
Déconnexion
</a>
</div>
<p class="mt-1 text-sm text-gray-400">Vue densemble des traitements en cours et de létat des envois.</p>
</div>
<!-- Stat cards -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<!-- En attente -->
<div class="group rounded-2xl p-3 md:p-5 bg-gradient-to-br from-cyan-500 to-cyan-400 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="0">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 6v6l4 2"/>
</svg>
</div>
<div class="text-sm/5 font-medium">En attente</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].attente}</div>
</div>
<!-- Terminé -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-green-300 to-emerald-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="1">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Terminé</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].termine}</div>
</div>
<!-- Erreur -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-rose-300 to-red-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="2">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v3m0 4h.01M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Erreur</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].erreur}</div>
</div>
<!-- Déjà dispo -->
<div class="group rounded-2xl p-4 md:p-5 bg-gradient-to-br from-pink-300 to-fuchsia-300 text-black shadow-xl ring-1 ring-white/10 transition transform hover:-translate-y-0.5 hover:shadow-2xl cursor-pointer filter-card" data-status="3">
<div class="flex items-center gap-3">
<div class="rounded-xl bg-black/10 p-2">
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 7h8M8 11h8M8 15h6"/>
</svg>
</div>
<div class="text-sm/5 font-medium">Déjà dispo</div>
</div>
<div class="mt-3 text-3xl font-extrabold tabular-nums">${stats[0].deja}</div>
</div>
</div>
<!-- Bouton réinitialiser -->
<div class="mb-4 text-center">
<button id="showAll"
class="hidden px-5 py-2 rounded-xl bg-gradient-to-br from-gray-600 to-gray-800 text-white font-semibold shadow-lg hover:from-gray-500 hover:to-gray-700 transition transform hover:-translate-y-0.5">
Tout afficher
</button>
</div>
<!-- Search -->
<div class="mb-4">
<label for="searchInput" class="sr-only">Rechercher</label>
<div class="relative">
<span class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<svg class="w-5 h-5 text-gray-400" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M21 21l-4.35-4.35M10 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16z"/>
</svg>
</span>
<input id="searchInput" type="text" placeholder="Rechercher…"
class="w-full pl-10 pr-3 py-2 rounded-xl bg-gray-800/80 border border-gray-700 focus:border-blue-500 focus:ring-4 focus:ring-blue-500/20 outline-none transition placeholder:text-gray-400" />
</div>
</div>
<nav aria-label="Page navigation" class="mb-4">
<ul class="inline-flex items-center -space-x-px">
<li>
${ page > 1
? `<a href="/autopost/?page=${page - 1}" class="px-3 py-2 ml-0 leading-tight text-gray-500 bg-white border border-gray-300 rounded-l-lg hover:bg-gray-100 hover:text-gray-700">Précédent</a>`
: `<span class="px-3 py-2 ml-0 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-l-lg">Précédent</span>`
}
</li>
<li>
<span class="px-4 py-2 leading-tight text-gray-700 bg-white border border-gray-300">Page ${page} sur ${totalPages}</span>
</li>
<li>
${ page < totalPages
? `<a href="/autopost/?page=${page + 1}" class="px-3 py-2 leading-tight text-gray-500 bg-white border border-gray-300 rounded-r-lg hover:bg-gray-100 hover:text-gray-700">Suivant</a>`
: `<span class="px-3 py-2 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-r-lg">Suivant</span>`
}
</li>
</ul>
</nav>
<div class="overflow-x-auto">
<table class="min-w-full border border-gray-700 shadow-lg rounded-lg overflow-hidden">
<thead class="bg-gray-900 text-gray-300 uppercase text-sm">
<tr>
<th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Status</th>
<th class="px-4 py-2">ID</th>
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
`;
rows.forEach(row => {
let statusText = '';
let statusClass = '';
switch (parseInt(row.status)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
break;
case 1:
statusText = 'ENVOI TERMINÉ';
statusClass = 'bg-green-300 text-black font-bold';
break;
case 2:
statusText = 'ERREUR';
statusClass = 'bg-red-300 text-black font-bold';
break;
case 3:
statusText = 'DEJA DISPONIBLE';
statusClass = 'bg-pink-300 text-black font-bold';
break;
case 4:
statusText = 'EN COURS';
statusClass = 'bg-yellow-300 text-black font-bold';
break;
default:
statusText = 'INCONNU';
}
let logLink = (parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Log</a>'
: '';
let mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="'+row.nom+'">MediaInfo/BDInfo</a>'
: '';
let dlLink = row.status === 1
? ` | <a href="/autopost/dl?name=${encodeURIComponent(row.nom)}" class="dl-link text-blue-400 hover:underline">DL</a>`
: '';
html += `
<tr id="row-${row.id}" class="odd:bg-gray-800 even:bg-gray-700">
<td class="px-4 py-2 border border-gray-700">${row.nom}</td>
<td class="px-4 py-2 border border-gray-700 status-text whitespace-nowrap ${statusClass}">${statusText}</td>
<td class="px-4 py-2 border border-gray-700">${row.id}</td>
<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">
<a href="#" class="edit-link text-blue-400 hover:underline" data-id="${row.id}" data-status="${row.status}">Editer</a> |
<a href="#" class="delete-link text-blue-400 hover:underline" data-id="${row.id}">Supprimer</a>
${logLink}
${mediainfoLink}
${dlLink}
</td>
</tr>
`;
});
html += `
</tbody>
</table>
</div>
</div>
<!-- Modale d'édition -->
<div id="editModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-gray-800 p-6 rounded relative w-1/2">
<span class="absolute top-2 right-2 text-white cursor-pointer close">&times;</span>
<h2 class="text-xl font-semibold mb-4">Editer Release <span id="modalReleaseId"></span></h2>
<form id="editForm">
<label for="statusSelect" class="block mb-2">Status :</label>
<select id="statusSelect" name="status" class="w-full p-2 mb-4 rounded border border-gray-300">
<option value="0">EN ATTENTE</option>
<option value="1">ENVOI TERMINÉ</option>
<option value="2">ERREUR</option>
<option value="3">DEJA DISPONIBLE</option>
</select>
<input type="hidden" id="releaseId" name="id" value=""/>
<button type="submit" class="w-full p-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Mettre à jour
</button>
</form>
</div>
</div>
<!-- Modale pour afficher le log -->
<div id="logModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-gray-800 p-6 rounded relative w-2/3">
<span class="absolute top-2 right-2 text-white cursor-pointer close log-close">&times;</span>
<h2 class="text-xl font-semibold mb-4">Contenu du log</h2>
<pre id="logContent" class="max-h-[80vh] overflow-y-auto"></pre>
</div>
</div>
<!-- Modale pour afficher le mediainfo -->
<div id="mediainfoModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center">
<div class="bg-gray-800 p-6 rounded relative w-2/3">
<span class="absolute top-2 right-2 text-white cursor-pointer close mediainfo-close">&times;</span>
<h2 class="text-xl font-semibold mb-4">Contenu du mediainfo</h2>
<pre id="mediainfoContent" class="max-h-[80vh] overflow-y-auto"></pre>
</div>
</div>
<script>
function updateTable(rows) {
var tbody = $("table tbody");
tbody.empty();
rows.forEach(function(row) {
var statusText = '';
var statusClass = '';
switch (parseInt(row.status)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
break;
case 1:
statusText = 'ENVOI TERMINÉ';
statusClass = 'bg-green-300 text-black font-bold';
break;
case 2:
statusText = 'ERREUR';
statusClass = 'bg-red-300 text-black font-bold';
break;
case 3:
statusText = 'DEJA DISPONIBLE';
statusClass = 'bg-pink-300 text-black font-bold';
break;
case 4:
statusText = 'EN COURS';
statusClass = 'bg-yellow-300 text-black font-bold';
break;
default:
statusText = 'INCONNU';
}
var logLink = (parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Log</a>'
: '';
var mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="'+row.nom+'">Mediainfo</a>'
: '';
var dlLink = (parseInt(row.status) === 1)
? ' | <a href="/autopost/dl?name='+encodeURIComponent(row.nom)+'" class="dl-link text-blue-400 hover:underline">DL</a>'
: '';
var tr = '<tr id="row-'+row.id+'" data-status="'+row.status+'" class="odd:bg-gray-800 even:bg-gray-700">'+
'<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">'+row.nom+'</td>'+
'<td class="px-4 py-2 border border-gray-700 status-text whitespace-nowrap '+statusClass+'">'+statusText+'</td>'+
'<td class="px-4 py-2 border border-gray-700">'+row.id+'</td>'+
'<td class="px-4 py-2 border border-gray-700">'+
'<a href="#" class="edit-link text-blue-400 hover:underline" data-id="'+row.id+'" data-status="'+row.status+'">Editer</a> | '+
'<a href="#" class="delete-link text-blue-400 hover:underline" data-id="'+row.id+'">Supprimer</a>'+
logLink+mediainfoLink+dlLink+
'</td>'+
'</tr>';
tbody.append(tr);
});
}
// Filtrage combiné avec recherche
var currentFilter = null; // null => pas de filtre actif
function applyFilterAndSearch() {
var q = ($("#searchInput").val() || "").toLowerCase();
$("table tbody tr").each(function() {
var $tr = $(this);
var name = $tr.find("td:first").text().toLowerCase();
var status = parseInt($tr.data("status"), 10);
var matchFilter = (currentFilter === null) || (status === currentFilter);
var matchSearch = !q || name.indexOf(q) !== -1;
$tr.toggle(matchFilter && matchSearch);
});
}
$(document).ready(function(){
// Recherche AJAX
$("#searchInput").on("keyup", function() {
var q = $(this).val();
$.ajax({
url: '/autopost/search',
type: 'GET',
data: { q: q },
dataType: 'json',
success: function(data) {
updateTable(data);
applyFilterAndSearch(); // conserve filtre actif
},
error: function() {
alert("Erreur lors de la recherche.");
}
});
});
function toggleShowAllButton() {
if (currentFilter === null) {
$('#showAll').addClass('hidden');
} else {
$('#showAll').removeClass('hidden');
}
}
// Filtrage par clic sur une card
$(document).on("click", ".filter-card", function() {
currentFilter = parseInt($(this).data("status"), 10);
$(".filter-card").removeClass("ring-4 ring-white/40");
$(this).addClass("ring-4 ring-white/40");
applyFilterAndSearch();
toggleShowAllButton();
});
// Bouton tout afficher
$(document).on("click", "#showAll", function() {
currentFilter = null;
$(".filter-card").removeClass("ring-4 ring-white/40");
applyFilterAndSearch();
toggleShowAllButton();
});
// Edition
$(document).on("click", ".edit-link", function(e) {
e.preventDefault();
var releaseId = $(this).data('id');
var currentStatus = $(this).data('status');
$('#releaseId').val(releaseId);
$('#modalReleaseId').text(releaseId);
$('#statusSelect').val(currentStatus);
$('#editModal').removeClass('hidden').fadeIn();
});
// Suppression
$(document).on("click", ".delete-link", function(e) {
e.preventDefault();
if (confirm("Êtes-vous sûr de vouloir supprimer cet enregistrement ?")) {
var releaseId = $(this).data('id');
$.ajax({
url: '/autopost/delete/' + releaseId,
type: 'POST',
success: function(data) {
$('#row-' + releaseId).fadeOut('slow', function(){
$(this).remove();
});
},
error: function() {
alert("Erreur lors de la suppression.");
}
});
}
});
// Affichage log
$(document).on("click", ".log-link", function(e) {
e.preventDefault();
var filename = $(this).data("filename");
$.ajax({
url: '/autopost/log',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#logContent").html(data.content);
$("#logModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier log.");
}
});
});
// Affichage mediainfo
$(document).on("click", ".mediainfo-link", function(e) {
e.preventDefault();
var filename = $(this).data("filename");
$.ajax({
url: '/autopost/mediainfo',
type: 'GET',
data: { name: filename },
dataType: 'json',
success: function(data) {
$("#mediainfoContent").text(data.content);
$("#mediainfoModal").removeClass('hidden').fadeIn();
},
error: function() {
alert("Erreur lors du chargement du fichier mediainfo.");
}
});
});
// Fermeture modales
$('.close').click(function(){
$(this).closest('.fixed').fadeOut(function(){
$(this).addClass('hidden');
});
});
$('.fixed').click(function(e) {
if (e.target === this) {
$(this).fadeOut(function(){
$(this).addClass('hidden');
});
}
});
// Edition formulaire
$('#editForm').submit(function(e){
e.preventDefault();
var releaseId = $('#releaseId').val();
var newStatus = $('#statusSelect').val();
$.ajax({
url: '/autopost/edit/' + releaseId,
type: 'POST',
data: { status: newStatus },
success: function(data) {
var statusText = '';
var statusClass = '';
switch(parseInt(newStatus)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
break;
case 1:
statusText = 'ENVOI TERMINÉ';
statusClass = 'bg-green-300 text-black font-bold';
break;
case 2:
statusText = 'ERREUR';
statusClass = 'bg-red-300 text-black font-bold';
break;
case 3:
statusText = 'DEJA DISPONIBLE';
statusClass = 'bg-pink-300 text-black font-bold';
break;
case 4:
statusText = 'EN COURS';
statusClass = 'bg-yellow-300 text-black font-bold';
break;
default:
statusText = 'INCONNU';
}
var row = $('#row-' + releaseId);
row.find('.status-text')
.removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300')
.addClass(statusClass)
.text(statusText);
$('#editModal').fadeOut(function(){
$(this).addClass('hidden');
});
},
error: function() {
alert("Erreur lors de la mise à jour.");
}
});
});
});
</script>
</body>
</html>
`;
res.send(html);
} catch (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la requête DB.");
}
});
// --------------------------- Recherche -----------------------------
autopostRouter.get('/search', async (req, res) => {
const q = req.query.q || "";
const searchQuery = "%" + q + "%";
try {
const [rows] = await db.query(
`SELECT nom, status, id FROM \`${config.DB_TABLE}\` WHERE nom LIKE ? ORDER BY id DESC LIMIT 500`,
[searchQuery]
);
res.json(rows);
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la requête." });
}
});
// --------------------------- Log -----------------------------
autopostRouter.get('/log', (req, res) => {
const filename = req.query.name;
if (!filename) {
return res.status(400).json({ error: "Nom du fichier non spécifié." });
}
let base = filename.includes('.') ? filename.split('.').slice(0, -1).join('.') : filename;
const logFilePath = path.join(config.logdirectory, base + '.log');
fs.readFile(logFilePath, 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors du chargement du fichier log." });
}
const htmlContent = convert.toHtml(data);
res.json({ content: htmlContent });
});
});
// --------------------------- Mediainfo -----------------------------
autopostRouter.get('/mediainfo', (req, res) => {
const filename = req.query.name;
if (!filename) {
return res.status(400).json({ error: "Nom du fichier non spécifié." });
}
const base = filename.includes('.') ? filename.split('.').slice(0, -1).join('.') : filename;
const jsonPath = path.join(config.infodirectory, base + '.json');
const bdinfoPath = path.join(config.infodirectory, base + '.bdinfo.txt');
// Vérifie lequel des deux existe, priorité au .json
fs.access(jsonPath, fs.constants.F_OK, (errJson) => {
if (!errJson) {
// .json existe
readAndSend(jsonPath);
} else {
// .json n'existe pas, on essaie .bdinfo.txt
fs.access(bdinfoPath, fs.constants.F_OK, (errBdinfo) => {
if (!errBdinfo) {
readAndSend(bdinfoPath);
} else {
res.status(404).json({ error: "Aucun fichier mediainfo trouvé." });
}
});
}
});
function readAndSend(filePath) {
fs.readFile(filePath, 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors du chargement du fichier mediainfo." });
}
// Essaie de prettifier si JSON
try {
const obj = JSON.parse(data);
const pretty = JSON.stringify(obj, null, 2);
res.json({ content: pretty });
} catch (e) {
res.json({ content: data });
}
});
}
});
// --------------------------- Download -----------------------------
autopostRouter.get('/dl', (req, res) => {
const filename = req.query.name;
if (!filename) {
return res.status(400).send("Nom du fichier non spécifié.");
}
// Extraire le nom de base sans extension
let base = filename.includes('.') ? filename.split('.').slice(0, -1).join('.') : filename;
const subfolder = base.charAt(0).toUpperCase();
// L'archive est en .7z maintenant
const archiveFilePath = path.join(config.finishdirectory, subfolder, base + '.7z');
console.log("Tentative de téléchargement (archive 7z) :", archiveFilePath);
// Utilisation du répertoire temporaire
const tmpDir = path.join(__dirname, 'tmp');
// Le fichier extrait attendu (sans dossier interne grâce à "7z e")
const extractedFilePath = path.join(tmpDir, base + '.nzb');
// Utiliser "7z e" pour extraire sans recréer la structure de dossiers
const command = `7z e "${archiveFilePath}" -o"${tmpDir}" -y`;
exec(command, (error, stdout, stderr) => {
if (error) {
console.error(`Erreur lors de la décompression: ${error.message}`);
return res.status(500).send("Erreur lors de la décompression.");
}
// On vérifie que le fichier extrait existe
res.download(extractedFilePath, base + '.nzb', (err) => {
if (err) {
console.error("Erreur lors du téléchargement :", err);
res.status(500).send("Erreur lors du téléchargement.");
}
// Suppression du fichier temporaire après téléchargement (optionnel)
fs.unlink(extractedFilePath, (err) => {
if (err) {
console.error("Erreur lors de la suppression du fichier temporaire :", err);
}
});
});
});
});
// --------------------------- Édition -----------------------------
autopostRouter.post('/edit/:id', async (req, res) => {
const id = req.params.id;
const newStatus = req.body.status;
try {
await db.query(`UPDATE \`${config.DB_TABLE}\` SET status = ? WHERE id = ?`, [newStatus, id]);
if (req.xhr || req.headers.accept.indexOf('json') > -1) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
}
} catch (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la mise à jour.");
}
});
// --------------------------- Suppression -----------------------------
autopostRouter.post('/delete/:id', async (req, res) => {
const id = req.params.id;
try {
await db.query(`DELETE FROM \`${config.DB_TABLE}\` WHERE id = ?`, [id]);
if (req.xhr || req.headers.accept.indexOf('json') > -1) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
}
} catch (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la suppression.");
}
});
// Redirection accès direct GET /edit/:id
autopostRouter.get('/edit/:id', (req, res) => {
res.redirect("/autopost/");
});
app.use('/autopost', autopostRouter);
app.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}/autopost`);
});