1
0
postauto/autopost/server.js
2025-03-15 15:29:49 +01:00

670 lines
23 KiB
JavaScript

const express = require('express');
const session = require('express-session');
const sqlite3 = require('sqlite3').verbose();
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');
// Import de la configuration depuis config.js
const config = require('./config');
const app = express();
const port = config.port;
// Middleware pour parser les formulaires POST
app.use(express.urlencoded({ extended: true }));
// Configuration des sessions en utilisant le secret depuis la config
app.use(session({
secret: config.sessionSecret,
resave: false,
saveUninitialized: false
}));
// Servir des fichiers statiques (CSS, images, etc.)
app.use(express.static('public'));
// Chemin vers la base SQLite défini dans la config
const DB_FILE = config.dbFile;
// Création du routeur pour /autopost
const autopostRouter = express.Router();
// Servir les fichiers statiques dans le contexte du routeur
autopostRouter.use('/js', express.static(path.join(__dirname, '../node_modules/@tailwindcss/browser/dist')));
// Servir les fichiers jQuery depuis node_modules/jquery/dist
autopostRouter.use('/jquery', express.static(path.join(__dirname, '../node_modules/jquery/dist')));
/* -------------------------------------------------------------------------
Routes non protégées (login, logout) sous /autopost
------------------------------------------------------------------------- */
// Affichage du formulaire de login à l'URL /autopost/login
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>
`);
});
// Traitement du formulaire de login en utilisant la config pour vérifier les identifiants
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>');
}
});
// Déconnexion
autopostRouter.get('/logout', (req, res) => {
req.session.destroy();
res.redirect('/autopost/login');
});
/* -------------------------------------------------------------------------
Middleware de protection pour les routes suivantes
------------------------------------------------------------------------- */
function checkAuth(req, res, next) {
if (req.session && req.session.authenticated) {
next();
} else {
res.redirect(req.baseUrl + '/login');
}
}
autopostRouter.use(checkAuth);
/* -------------------------------------------------------------------------
Route GET principale pour /autopost avec pagination
------------------------------------------------------------------------- */
autopostRouter.get('/', (req, res) => {
const limit = 100; // enregistrements par page
const page = parseInt(req.query.page) || 1;
const offset = (page - 1) * limit;
let db = new sqlite3.Database(DB_FILE, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error(err.message);
return res.status(500).send("Erreur lors de l'ouverture de la base de données.");
}
});
// Récupérer le nombre total d'enregistrements
const countQuery = "SELECT COUNT(*) as total FROM release";
db.get(countQuery, [], (err, countResult) => {
if (err) {
console.error(err.message);
res.status(500).send("Erreur lors du comptage des enregistrements.");
return;
}
const totalRecords = countResult.total;
const totalPages = Math.ceil(totalRecords / limit);
const query = `
SELECT nom, status, id FROM release
ORDER BY (status = 2) DESC, id DESC
LIMIT ${limit} OFFSET ${offset};
`;
db.all(query, [], (err, rows) => {
if (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la requête.");
return;
}
let html = `
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Suivi Autopost</title>
<!-- Inclusion de Tailwind CSS et jQuery -->
<script src="js/index.global.js"></script>
<script src="jquery/jquery.min.js"></script>
</head>
<body class="bg-slate-900 text-white font-sans p-4">
<!-- Conteneur "fluid" -->
<div class="w-full px-4">
<h1 class="text-3xl font-bold mb-4">Suivi Autopost</h1>
<p class="mb-4">
<a href="/autopost/logout" class="text-blue-400 hover:underline">Déconnexion</a>
</p>
<input type="text" id="searchInput" placeholder="Rechercher..."
class="p-2 mb-4 border border-gray-300 rounded w-full"/>
<!-- Pagination avec Tailwind -->
<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 bg-gray-800">
<thead>
<tr>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Name</th>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Status</th>
<th class="px-4 py-2 border border-gray-700">ID</th>
<th class="px-4 py-2 border border-gray-700 whitespace-nowrap">Actions</th>
</tr>
</thead>
<tbody>
`;
rows.forEach(row => {
let statusText = '';
let statusClass = '';
switch (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;
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</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) {
let tbody = $("table tbody");
tbody.empty();
rows.forEach(function(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;
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</a>'
: '';
let dlLink = parseInt(row.status) === 1
? ' | <a href="/autopost/dl?name='+encodeURIComponent(row.nom)+'" class="dl-link text-blue-400 hover:underline">DL</a>'
: '';
let tr = \`
<tr id="row-\${row.id}" 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);
});
}
$(document).ready(function(){
$("#searchInput").on("keyup", function() {
let q = $(this).val();
$.ajax({
url: '/autopost/search',
type: 'GET',
data: { q: q },
dataType: 'json',
success: function(data) {
updateTable(data);
},
error: function() {
alert("Erreur lors de la recherche.");
}
});
});
$(document).on("click", ".edit-link", function(e) {
e.preventDefault();
let releaseId = $(this).data('id');
let currentStatus = $(this).data('status');
$('#releaseId').val(releaseId);
$('#modalReleaseId').text(releaseId);
$('#statusSelect').val(currentStatus);
$('#editModal').removeClass('hidden').fadeIn();
});
$(document).on("click", ".delete-link", function(e) {
e.preventDefault();
if (confirm("Êtes-vous sûr de vouloir supprimer cet enregistrement ?")) {
let 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.");
}
});
}
});
$(document).on("click", ".log-link", function(e) {
e.preventDefault();
let 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.");
}
});
});
$(document).on("click", ".mediainfo-link", function(e) {
e.preventDefault();
let 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.");
}
});
});
$('.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');
});
}
});
$('#editForm').submit(function(e){
e.preventDefault();
let releaseId = $('#releaseId').val();
let newStatus = $('#statusSelect').val();
$.ajax({
url: '/autopost/edit/' + releaseId,
type: 'POST',
data: { status: newStatus },
success: function(data) {
let statusText = '';
let 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;
default:
statusText = 'INCONNU';
}
let row = $('#row-' + releaseId);
// Mise à jour uniquement de la cellule status
row.find('.status-text')
.removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-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);
db.close();
});
});
});
/* -------------------------------------------------------------------------
Route GET pour la recherche côté serveur
------------------------------------------------------------------------- */
autopostRouter.get('/search', (req, res) => {
const q = req.query.q || "";
const searchQuery = "%" + q + "%";
let db = new sqlite3.Database(DB_FILE, sqlite3.OPEN_READONLY, (err) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors de l'ouverture de la base de données." });
}
});
db.all("SELECT nom, status, id FROM release WHERE nom LIKE ? ORDER BY id DESC LIMIT 500", [searchQuery], (err, rows) => {
if (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la requête." });
return;
}
res.json(rows);
db.close();
});
});
/* -------------------------------------------------------------------------
Route GET pour récupérer le contenu d'un fichier 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 });
});
});
/* -------------------------------------------------------------------------
Route GET pour récupérer le contenu d'un fichier 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é." });
}
let base = filename.includes('.') ? filename.split('.').slice(0, -1).join('.') : filename;
const mediainfoFilePath = path.join(config.infodirectory, base + '.json');
fs.readFile(mediainfoFilePath, 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return res.status(500).json({ error: "Erreur lors du chargement du fichier mediainfo." });
}
try {
const obj = JSON.parse(data);
const pretty = JSON.stringify(obj, null, 2);
res.json({ content: pretty });
} catch(e) {
res.json({ content: data });
}
});
});
/* -------------------------------------------------------------------------
Route GET pour télécharger le fichier NZB
------------------------------------------------------------------------- */
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 = os.tmpdir();
// 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);
}
});
});
});
});
/* -------------------------------------------------------------------------
Route POST pour mettre à jour le statut (édition)
------------------------------------------------------------------------- */
autopostRouter.post('/edit/:id', (req, res) => {
const id = req.params.id;
const newStatus = req.body.status;
let db = new sqlite3.Database(DB_FILE, sqlite3.OPEN_READWRITE, (err) => {
if (err) {
console.error(err.message);
return res.status(500).send("Erreur lors de l'ouverture de la base de données.");
}
});
db.run("UPDATE release SET status = ? WHERE id = ?", [newStatus, id], function(err) {
if (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la mise à jour.");
return;
}
if (req.xhr || req.headers.accept.indexOf('json') > -1) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
}
db.close();
});
});
/* -------------------------------------------------------------------------
Route POST pour supprimer un enregistrement
------------------------------------------------------------------------- */
autopostRouter.post('/delete/:id', (req, res) => {
const id = req.params.id;
let db = new sqlite3.Database(DB_FILE, sqlite3.OPEN_READWRITE, (err) => {
if (err) {
console.error(err.message);
return res.status(500).send("Erreur lors de l'ouverture de la base de données.");
}
});
db.run("DELETE FROM release WHERE id = ?", [id], function(err) {
if (err) {
console.error(err.message);
res.status(500).send("Erreur lors de la suppression.");
return;
}
if (req.xhr || req.headers.accept.indexOf('json') > -1) {
res.json({ success: true });
} else {
res.redirect("/autopost/");
}
db.close();
});
});
// Redirection de la route GET d'édition si accès direct
autopostRouter.get('/edit/:id', (req, res) => {
res.redirect("/autopost/");
});
// Monter le routeur sur le chemin /autopost
app.use('/autopost', autopostRouter);
app.listen(port, () => {
console.log(`Serveur démarré sur http://localhost:${port}/autopost`);
});