Ajout refresh auto lors de l'ajout d'un fichier dans le dossier mediainfo
This commit is contained in:
parent
d916f33b91
commit
01c2f2d35e
@ -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/");
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user