1
0

Ajout refresh auto lors de l'ajout d'un fichier dans le dossier mediainfo

This commit is contained in:
unfr 2025-08-12 11:32:02 +02:00
parent d916f33b91
commit 01c2f2d35e

View File

@ -9,12 +9,13 @@ const { exec } = require('child_process');
const os = require('os'); const os = require('os');
const config = require('./config'); const config = require('./config');
const db = require('./db'); const db = require('./db');
const chokidar = require('chokidar');
db.testConnection(); // vérification au démarrage db.testConnection(); // vérification au démarrage
const app = express(); const app = express();
const port = config.port; const port = config.port;
const background_color = (config?.background_color ?? '').trim() || 'slate-900';
// Middleware pour parser les formulaires POST // Middleware pour parser les formulaires POST
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
@ -57,7 +58,7 @@ autopostRouter.get('/login', (req, res) => {
<!-- Inclusion de Tailwind CSS via le CDN --> <!-- Inclusion de Tailwind CSS via le CDN -->
<script src="/js/index.global.js"></script> <script src="/js/index.global.js"></script>
</head> </head>
<body class="bg-slate-900 flex items-center justify-center min-h-screen"> <body class="bg-${background_color} flex items-center justify-center min-h-screen">
<div class="bg-slate-700 p-8 rounded-lg shadow-md w-80"> <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> <h2 class="text-center text-2xl font-bold text-white mb-4">Authentification</h2>
<form method="POST" action="/autopost/login"> <form method="POST" action="/autopost/login">
@ -100,6 +101,109 @@ function checkAuth(req, res, next) {
} }
autopostRouter.use(checkAuth); autopostRouter.use(checkAuth);
// ---- Long-polling refresh (robuste) ----
let lpVersion = 1;
const lpWaiters = new Set();
const LP_TIMEOUT_MS = 25000;
function lpNotify(source, filePath) {
lpVersion += 1;
console.log('[LP] notify', { version: lpVersion, source, file: filePath });
for (const waiter of lpWaiters) {
clearTimeout(waiter.timer);
try { waiter.res.json({ version: lpVersion }); } catch (_) {}
}
lpWaiters.clear();
}
// Endpoint long-polling
autopostRouter.get('/updates', (req, res) => {
const since = parseInt(req.query.since, 10) || 0;
if (lpVersion > since) {
return res.json({ version: lpVersion });
}
const waiter = {
res,
timer: setTimeout(() => {
lpWaiters.delete(waiter);
res.json({ version: lpVersion }); // heartbeat/timeout
}, LP_TIMEOUT_MS)
};
lpWaiters.add(waiter);
req.on('close', () => {
clearTimeout(waiter.timer);
lpWaiters.delete(waiter);
});
});
// Watcher fichiers (JSON / BDINFO) + fallback scan
const infoDir = path.resolve(config.infodirectory);
const watchPatterns = [
path.join(infoDir, '*.json'),
path.join(infoDir, '*.JSON'),
path.join(infoDir, '*.bdinfo.txt'),
path.join(infoDir, '*.BDINFO.TXT')
];
const lpWatcher = chokidar.watch(watchPatterns, {
ignoreInitial: true,
awaitWriteFinish: { stabilityThreshold: 1200, pollInterval: 250 },
usePolling: true, // important pour NFS/SMB/Docker
interval: 1000,
depth: 0,
ignorePermissionErrors: true,
persistent: true
});
lpWatcher.on('add', (filePath) => lpNotify('add', filePath));
lpWatcher.on('change', (filePath) => lpNotify('change', filePath));
lpWatcher.on('ready', () => console.log('[LP] watcher ready on', watchPatterns));
lpWatcher.on('error', (err) => console.error('[LP] watcher error:', err));
// Fallback scan (toutes les 2s) : signature (nb fichiers + dernier mtime)
const fsp = fs.promises; // réutilise ton import fs existant
let lastSig = null;
async function computeSignature() {
try {
const names = await fsp.readdir(infoDir);
const files = names.filter(n => /\.json$/i.test(n) || /\.bdinfo\.txt$/i.test(n));
let latest = 0;
await Promise.all(files.map(async (n) => {
try {
const st = await fsp.stat(path.join(infoDir, n));
if (st.mtimeMs > latest) latest = st.mtimeMs;
} catch (_) {}
}));
return files.length + ':' + Math.floor(latest);
} catch {
return '0:0';
}
}
async function periodicScan() {
try {
const sig = await computeSignature();
if (lastSig === null) {
lastSig = sig; // baseline
} else if (sig !== lastSig) {
lastSig = sig;
lpNotify('scan', infoDir);
}
} catch (e) {
console.error('[LP] periodic scan error:', e.message || e);
} finally {
setTimeout(periodicScan, 2000);
}
}
periodicScan();
// --------------------------- PAGE PRINCIPALE ----------------------------- // --------------------------- PAGE PRINCIPALE -----------------------------
autopostRouter.get('/', async (req, res) => { autopostRouter.get('/', async (req, res) => {
const limit = 100; // enregistrements par page const limit = 100; // enregistrements par page
@ -107,6 +211,7 @@ autopostRouter.get('/', async (req, res) => {
const offset = (page - 1) * limit; const offset = (page - 1) * limit;
try { try {
const [stats] = await db.query(` const [stats] = await db.query(`
SELECT SELECT
COUNT(*) AS total, COUNT(*) AS total,
@ -116,7 +221,7 @@ autopostRouter.get('/', async (req, res) => {
SUM(status = 3) AS deja, SUM(status = 3) AS deja,
SUM(status = 4) AS encours SUM(status = 4) AS encours
FROM \`${config.DB_TABLE}\` FROM \`${config.DB_TABLE}\`
`); `);
// Récupérer le nombre total d'enregistrements // Récupérer le nombre total d'enregistrements
const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``); const [countResult] = await db.query(`SELECT COUNT(*) as total FROM \`${config.DB_TABLE}\``);
const totalRecords = countResult[0].total; const totalRecords = countResult[0].total;
@ -133,11 +238,11 @@ autopostRouter.get('/', async (req, res) => {
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Suivi Autopost ${config.name}</title> <title>Suivi Autopost ${config.name}</title>
<script src="js/index.global.js"></script> <script src="/js/index.global.js"></script>
<script src="jquery/jquery.min.js"></script> <script src="/jquery/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet">
</head> </head>
<body class="bg-sky-950 text-white font-sans p-4"> <body class="bg-${background_color} text-white font-sans p-4">
<div class="w-full px-4"> <div class="w-full px-4">
<!-- Header --> <!-- Header -->
<div class="mb-6"> <div class="mb-6">
@ -272,7 +377,7 @@ autopostRouter.get('/', async (req, res) => {
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-gray-700">`; <tbody class="divide-y divide-gray-700">`;
rows.forEach(row => { rows.forEach(row => {
let statusText = ''; let statusText = '';
let statusClass = ''; let statusClass = '';
@ -325,8 +430,8 @@ autopostRouter.get('/', async (req, res) => {
</tr> </tr>
`; `;
}); });
html += ` html += `
</tbody> </tbody>
</table> </table>
@ -445,6 +550,9 @@ autopostRouter.get('/', async (req, res) => {
<div id="toast" class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 px-4 py-2 rounded-lg bg-gray-900/95 text-white shadow-lg z-[60]"> <div id="toast" class="hidden fixed bottom-6 left-1/2 -translate-x-1/2 px-4 py-2 rounded-lg bg-gray-900/95 text-white shadow-lg z-[60]">
<span id="toastMsg">Supprimé.</span> <span id="toastMsg">Supprimé.</span>
</div> </div>
<script> <script>
// --- State --- // --- State ---
const INITIAL_PAGE = ${page}; const INITIAL_PAGE = ${page};
@ -582,6 +690,52 @@ autopostRouter.get('/', async (req, res) => {
}); });
} }
// --- Long polling: refresh table when new mediainfo file appears ---
(function () {
var lastVersion = null;
function poll() {
$.ajax({
url: '/autopost/updates',
type: 'GET',
data: { since: lastVersion || 0 },
timeout: 30000,
success: function (resp) {
if (resp && typeof resp.version === 'number') {
if (lastVersion === null) {
// 1ère synchro: on se cale, pas de refresh
lastVersion = resp.version;
} else if (resp.version > lastVersion) {
lastVersion = resp.version;
if (typeof loadPage === 'function') loadPage(currentPage || 1);
// Si tu as la route /stats + updateStatsUI(...)
if (typeof updateStatsUI === 'function') {
$.getJSON('/autopost/stats', function (s) { if (s) updateStatsUI(s); });
}
}
}
setTimeout(poll, 100); // réenchaîne direct
},
error: function () {
setTimeout(poll, 2000); // backoff réseau
}
});
}
$(document).ready(poll);
})();
function updateStatsUI(s) {
$('.filter-card[data-status="0"] .tabular-nums').text(s.attente || 0);
$('.filter-card[data-status="1"] .tabular-nums').text(s.termine || 0);
$('.filter-card[data-status="2"] .tabular-nums').text(s.erreur || 0);
$('.filter-card[data-status="3"] .tabular-nums').text(s.deja || 0);
}
function refreshAfterChange() {
loadPage(currentPage || 1);
$.getJSON('/autopost/stats', function(s) { updateStatsUI(s); });
}
// --- DOM bindings --- // --- DOM bindings ---
$(document).ready(function() { $(document).ready(function() {
@ -884,7 +1038,6 @@ autopostRouter.get('/search', async (req, res) => {
}); });
// --------------------------- Log -----------------------------
autopostRouter.get('/log', (req, res) => { autopostRouter.get('/log', (req, res) => {
const filename = req.query.name; const filename = req.query.name;
if (!filename) { if (!filename) {
@ -1097,6 +1250,27 @@ autopostRouter.get('/filter', async (req, res) => {
} }
}); });
// --------------------------- STATS -----------------------------
autopostRouter.get('/stats', async (req, res) => {
try {
const [rows] = 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}\`
`);
res.json(rows[0]);
} catch (e) {
console.error(e);
res.status(500).json({ error: 'Erreur stats' });
}
});
// Redirection accès direct GET /edit/:id // Redirection accès direct GET /edit/:id
autopostRouter.get('/edit/:id', (req, res) => { autopostRouter.get('/edit/:id', (req, res) => {
res.redirect("/autopost/"); res.redirect("/autopost/");