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}

Suivi Autopost ${config.name}

Déconnexion

Vue d’ensemble des traitements en cours et de l’état des envois.

En attente
${stats[0].attente}
Terminé
${stats[0].termine}
Erreur
${stats[0].erreur}
Déjà dispo
${stats[0].deja}
`; 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 += ` `; }); html += `
Name Status ID Actions
${row.nom} ${statusText} ${row.id} Editer | Supprimer ${logLink} ${mediainfoLink} ${dlLink}
`; 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`); });