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(`
Login ${config.name}
Authentification
`);
});
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. Réessayer');
}
});
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 = `
Suivi Autopost ${config.name}
Vue d’ensemble des traitements en cours et de l’état des envois.
| Name |
Status |
ID |
Actions |
`;
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)
? ' | Log'
: '';
let mediainfoLink = (parseInt(row.status) === 0 || parseInt(row.status) === 1 || parseInt(row.status) === 2)
? ' | MediaInfo/BDInfo'
: '';
let dlLink = row.status === 1
? ` | DL`
: '';
html += `
| ${row.nom} |
${statusText} |
${row.id} |
Editer |
Supprimer
${logLink}
${mediainfoLink}
${dlLink}
|
`;
});
html += `
Supprimer cet enregistrement ?
Vous êtes sur le point de supprimer . Cette action est irréversible.
Supprimé.
`;
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 page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 100;
const offset = (page - 1) * limit;
const searchQuery = `%${q}%`;
try {
const [countRows] = await db.query(
`SELECT COUNT(*) AS total FROM \`${config.DB_TABLE}\` WHERE nom LIKE ?`,
[searchQuery]
);
const total = countRows[0].total;
const totalPages = Math.max(1, Math.ceil(total / limit));
const [rows] = await db.query(
`SELECT nom, status, id
FROM \`${config.DB_TABLE}\`
WHERE nom LIKE ?
ORDER BY (status = 2) DESC, id DESC
LIMIT ? OFFSET ?`,
[searchQuery, limit, offset]
);
res.json({ rows, page, limit, total, totalPages });
} 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." });
}
// 1) enlever les codes curseur (début de ligne/clear) mais garder les couleurs (…m)
// ex: \x1b[0G, \x1b[0K, etc. — ce sont EUX qui “collent” les blocs
let s = data.replace(/\x1b\[[0-9;]*[GK]/g, '');
// 2) supprimer TOUT segment de progression, même s’ils sont enchaînés sur UNE SEULE ligne
// forme couverte : "12.34% [======] 487.86 MiB/s, ETA 00:01"
// + variantes “ETA -”, et même le cas minimal "0.00% [ ]"
const ANSI_M = '(?:\\x1b\\[[0-9;]*m)*';
const NUM = '\\d{1,3}(?:\\.\\d+)?';
const PROG = new RegExp(
// pourcentage
ANSI_M + '\\s*' + NUM + '\\s*%' +
// barre
'\\s*' + ANSI_M + '\\[[^\\]]*\\]' +
// (option) vitesse + ETA
'(?:\\s*' + ANSI_M + NUM + '\\s*MiB\\/s,\\s*ETA\\s*[^\\x1bG\\n]*)?',
'g'
);
s = s.replace(PROG, '');
// 3) nettoyer les résidus "-G" / "G" laissés juste avant G[INFO] (quand une progression était collée)
s = s.replace(/-?\s*G(?=\[INFO\])/g, '');
// 4) petite normalisation (sans casser les couleurs)
s = s.replace(/[ \t]+/g, ' ')
.replace(/\r/g, '')
.replace(/\n{3,}/g, '\n\n')
// Retour à la ligne avant chaque mot-clé (sauf début)
.replace(/(?!^)(?=\[INFO\])/gm, '\n')
.replace(/(?!^)Calculating\s*:/gm, '\nCalculating :')
.replace(/(?!^)Writing\s*:/gm, '\nWriting :')
.replace(/(?!^)Finalizing\s*:/gm, '\nFinalizing :')
.replace(/(?!^)Finished\s*:/gm, '\nFinished :')
.trim();
const htmlContent = convert.toHtml(s); // conserve les couleurs ANSI
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.");
}
});
// --------------------------- Filtrage -----------------------------
autopostRouter.get('/filter', async (req, res) => {
const status = Number.isFinite(parseInt(req.query.status, 10))
? parseInt(req.query.status, 10)
: null;
if (status === null) {
return res.status(400).json({ error: "Status non fourni." });
}
const page = parseInt(req.query.page, 10) || 1;
const limit = parseInt(req.query.limit, 10) || 100;
const offset = (page - 1) * limit;
try {
const [countRows] = await db.query(
`SELECT COUNT(*) AS total FROM \`${config.DB_TABLE}\` WHERE status = ?`,
[status]
);
const total = countRows[0].total;
const totalPages = Math.max(1, Math.ceil(total / limit));
const [rows] = await db.query(
`SELECT nom, status, id
FROM \`${config.DB_TABLE}\`
WHERE status = ?
ORDER BY id DESC
LIMIT ? OFFSET ?`,
[status, limit, offset]
);
res.json({ rows, page, limit, total, totalPages });
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors de la requête." });
}
});
// 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`);
});