@@ -9,12 +9,13 @@ const { exec } = require('child_process');
const os = require ( 'os' ) ;
const config = require ( './config' ) ;
const db = require ( './db' ) ;
const chokidar = require ( 'chokidar' ) ;
db . testConnection ( ) ; // vérification au démarrage
const app = express ( ) ;
const port = config . port ;
const background _color = ( config ? . background _color ? ? '' ) . trim ( ) || 'slate-900' ;
// Middleware pour parser les formulaires POST
app . use ( express . urlencoded ( { extended : true } ) ) ;
@@ -57,7 +58,7 @@ autopostRouter.get('/login', (req, res) => {
<!-- 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">
<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">
<h2 class="text-center text-2xl font-bold text-white mb-4">Authentification</h2>
<form method="POST" action="/autopost/login">
@@ -100,6 +101,109 @@ function checkAuth(req, res, next) {
}
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 -----------------------------
autopostRouter . get ( '/' , async ( req , res ) => {
const limit = 100 ; // enregistrements par page
@@ -107,6 +211,7 @@ autopostRouter.get('/', async (req, res) => {
const offset = ( page - 1 ) * limit ;
try {
const [ stats ] = await db . query ( `
SELECT
COUNT(*) AS total,
@@ -128,7 +233,7 @@ autopostRouter.get('/', async (req, res) => {
) ;
let html = `
<!DOCTYPE html>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
@@ -137,8 +242,9 @@ autopostRouter.get('/', async (req, res) => {
<script src="/jquery/jquery.min.js"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700&display=swap" rel="stylesheet">
</head>
<body class="bg-slate-900 text-white font-sans p-4">
<body class="bg-${ background _color } text-white font-sans p-4">
<div class="w-full px-4">
<!-- Header -->
<div class="mb-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl md:text-3xl font-extrabold tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-cyan-300">
@@ -238,11 +344,12 @@ autopostRouter.get('/', async (req, res) => {
</div>
</div>
<nav aria-label="Page navigation" class="mb-4">
<nav id="pagination" 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> `
${
page > 1
? ` <a href="/autopost/?page= ${ page - 1 } " data-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>
@@ -250,13 +357,15 @@ autopostRouter.get('/', async (req, res) => {
<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> `
${
page < totalPages
? ` <a href="/autopost/?page= ${ page + 1 } " data-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 border border-gray-700 shadow-lg rounded-lg overflow-hidden">
<thead class="bg-gray-900 text-gray-300 uppercase text-sm">
@@ -267,8 +376,7 @@ autopostRouter.get('/', async (req, res) => {
<th class="px-4 py-2">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-700">
` ;
<tbody class="divide-y divide-gray-700"> ` ;
rows . forEach ( row => {
let statusText = '' ;
@@ -298,13 +406,13 @@ autopostRouter.get('/', async (req, res) => {
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>'
? ' | <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/BDInfo</a>'
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="' + row . nom + '">MediaInfo/BDInfo</a>'
: '' ;
let dlLink = row . status === 1
? ` | <a href="/autopost/dl?name=${ encodeURIComponent ( row . nom ) } " class="dl-link text-blue-400 hover:underline">DL</a>`
? ' | <a href="/autopost/dl?name=' + encodeURIComponent ( row . nom ) + ' " class="dl-link text-blue-400 hover:underline">DL</a>'
: '' ;
html += `
@@ -323,6 +431,7 @@ autopostRouter.get('/', async (req, res) => {
` ;
} ) ;
html += `
</tbody>
</table>
@@ -332,7 +441,6 @@ autopostRouter.get('/', async (req, res) => {
<!-- Modale d'édition -->
<div id="editModal" class="hidden fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-md transform transition-all scale-95">
<!-- Bouton fermer -->
<button type="button" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close">
×
@@ -345,7 +453,6 @@ autopostRouter.get('/', async (req, res) => {
<!-- Formulaire -->
<form id="editForm">
<!-- Label + Select -->
<label for="statusSelect" class="block mb-2 text-gray-300 font-medium">Status :</label>
<select id="statusSelect" name="status"
@@ -368,11 +475,9 @@ autopostRouter.get('/', async (req, res) => {
</div>
</div>
<!-- Modale pour afficher le log -->
<div id="logModal" class="hidden fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-7xl relative"> <!-- élargi max-w -->
<!-- Bouton fermer -->
<button type="button" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close log-close">
×
@@ -387,14 +492,12 @@ autopostRouter.get('/', async (req, res) => {
<pre id="logContent"
class="max-h-[90vh] overflow-y-auto p-6 rounded-lg bg-gray-800 text-green-400 font-mono text-sm leading-relaxed border border-gray-700 shadow-inner whitespace-pre-wrap">
</pre>
</div>
</div>
<!-- Modale pour afficher le mediainfo -->
<div id="mediainfoModal" class="hidden fixed inset-0 bg-black bg-opacity-60 backdrop-blur-sm flex items-center justify-center z-50">
<div class="bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-6xl relative">
<!-- Bouton fermer -->
<button type="button"
class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close mediainfo-close"
@@ -417,7 +520,6 @@ autopostRouter.get('/', async (req, res) => {
<pre id="mediainfoContent"
class="max-h-[80vh] overflow-y-auto p-4 rounded-lg bg-gray-800 text-yellow-300 font-mono text-sm leading-relaxed border border-gray-700 shadow-inner whitespace-pre-wrap">
</pre>
</div>
</div>
@@ -449,13 +551,29 @@ autopostRouter.get('/', async (req, res) => {
<span id="toastMsg">Supprimé.</span>
</div>
<script>
// --- State ---
const INITIAL_PAGE = ${ page } ;
const INITIAL_TOTAL_PAGES = ${ totalPages } ;
const PAGE_LIMIT = ${ limit } ;
let currentPage = INITIAL_PAGE;
let currentTotalPages = INITIAL_TOTAL_PAGES;
let activeFilter = null; // number | null
let activeQuery = ''; // string
// --- Rendering: table ---
function updateTable(rows) {
var tbody = $ (" table tbody" );
var tbody = $ (' table tbody' );
tbody.empty();
rows.forEach(function(row) {
var statusText = '';
var statusClass = '';
switch (parseInt(row.status)) {
case 0:
statusText = 'EN ATTENTE';
@@ -482,113 +600,200 @@ autopostRouter.get('/', async (req, res) => {
}
var 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>'
: '';
var 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>'
: '';
var dlLink = (parseInt(row.status) === 1)
? ' | <a href="/autopost/dl?name='+encodeURIComponent(row.nom)+'" class="dl-link text-blue-400 hover:underline">DL</a>'
? ' | <a href="#" class="log-link text-blue-400 hover:underline" data-filename="' + row.nom + '">Log</a>'
: '';
var tr = '<tr id="row-'+row.id+'" data-status="'+row.status+'" 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>'+
var 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 >'
: '';
var dlLink = (parseInt(row.status) === 1)
? ' | <a href="/autopost/dl?name=' + encodeURIComponent(row.nom) + ' " class="dl -link text-blue-400 hover:underline">DL</a>'
: '';
var tr =
'<tr id="row-' + row.id + '" data-status="' + row.status + '" 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(){
let searchTimer = null;
$ ("#searchInput").on("keyup", function() {
clearTimeout(searchTimer); // annule le timer précédent
// --- Rendering: pagination ---
function renderPaginationHTML(page, totalPages) {
var prevDisabled = page <= 1;
var nextDisabled = page >= totalPages;
var h = '';
h += '<ul class="inline-flex items-center -space-x-px">';
h += '<li>';
if (prevDisabled) {
h += '<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>';
} else {
h += '<a href="/autopost/?page=' + (page - 1) + '" data-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>';
}
h += '</li>';
h += '<li><span class="px-4 py-2 leading-tight text-gray-700 bg-white border border-gray-300">Page ' + page + ' sur ' + totalPages + '</span></li>';
h += '<li>';
if (nextDisabled) {
h += '<span class="px-3 py-2 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-r-lg">Suivant</span>';
} else {
h += '<a href="/autopost/?page=' + (page + 1) + '" data-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>';
}
h += '</li>';
h += '</ul>';
return h;
}
function updatePagination(page, totalPages) {
$ ('#pagination').html(renderPaginationHTML(page, totalPages));
}
// --- Data loading ---
function loadPage(p) {
const targetPage = Math.max(1, parseInt(p, 10) || 1);
const isFilter = activeFilter !== null;
const url = isFilter ? '/autopost/filter' : '/autopost/search';
const data = isFilter
? { status: activeFilter, page: targetPage, limit: PAGE_LIMIT }
: { q: activeQuery || '', page: targetPage, limit: PAGE_LIMIT };
$ .ajax({
url: url,
type: 'GET',
data: data,
dataType: 'json',
success: function(resp) {
updateTable(resp.rows);
currentPage = resp.page;
currentTotalPages = resp.totalPages;
updatePagination(currentPage, currentTotalPages);
},
error: function(jqXHR, textStatus, errorThrown) {
if (textStatus !== 'abort') {
console.error('Erreur AJAX :', textStatus, errorThrown);
alert('Erreur lors du chargement de la page.');
}
}
});
}
// --- 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 ---
$ (document).ready(function() {
// Recherche
let searchTimer = null;
$ ('#searchInput').on('keyup', function() {
clearTimeout(searchTimer);
let q = $ (this).val();
searchTimer = setTimeout(function() {
$ .ajax({
url: '/autopost/search',
type: 'GET',
data: { q: q },
dataType: 'json',
success: function(data) {
updateTable(data);
$ (".filter-card").removeClass("ring-4 ring-white/40");
activeQuery = q || '';
activeFilter = null; // on sort du mode filtre
$ ('.filter-card').removeClass('ring-4 ring-white/40');
loadPage(1); // charge page 1 avec pagination AJAX
toggleShowAllButton();
},
error: function(jqXHR, textStatus, errorThrown) {
// On ignore l'erreur si la requête a été annulée volontairement
if (textStatus !== "abort") {
console.error('Erreur AJAX :', textStatus, errorThrown);
alert("Erreur lors de la recherche.");
}
}
});
}, 300); // 300ms après la dernière frappe
}, 300);
});
// Pagination: intercepter et paginer en AJAX (liste, recherche ou filtre)
$ (document).on('click', '#pagination a[data-page]', function(e) {
e.preventDefault();
const p = parseInt( $ (this).data('page'), 10);
if (!isNaN(p)) loadPage(p);
});
// Affichage/masquage du bouton "Tout afficher"
function toggleShowAllButton() {
if ( $ (" .filter-card.ring-4" ).length === 0) {
if ( $ (' .filter-card.ring-4' ).length === 0) {
$ ('#showAll').addClass('hidden');
} else {
$ ('#showAll').removeClass('hidden');
}
}
// Filtrage par clic sur une card
$ (document).on(" click" , " .filter-card" , function () {
const status = parseInt( $ (this).data(" status" ), 10);
$ (document).on(' click' , ' .filter-card' , function() {
const status = parseInt( $ (this).data(' status' ), 10);
$ (" .filter-card" ).removeClass(" ring-4 ring-white/40" );
$ (this).addClass(" ring-4 ring-white/40" );
$ (' .filter-card' ).removeClass(' ring-4 ring-white/40' );
$ (this).addClass(' ring-4 ring-white/40' );
$ .ajax({
url: '/autopost/filter',
type: 'GET',
data: { status },
dataType: 'json',
success: function (data) {
updateTable(data);
activeFilter = status;
activeQuery = ''; // on sort du mode recherche
loadPage(1); // page 1 du filtre
toggleShowAllButton();
},
error: function () {
alert("Erreur lors du filtrage.");
}
});
});
// Bouton tout afficher
$ (document).on(" click" , " #showAll" , function () {
$ (" .filter-card" ).removeClass(" ring-4 ring-white/40" );
$ .ajax({
url: '/autopost/search',
type: 'GET',
data: { q: $ ("#searchInput").val() || "" },
dataType: 'json',
success: function (data) {
updateTable(data);
$ (document).on(' click' , ' #showAll' , function() {
$ (' .filter-card' ).removeClass(' ring-4 ring-white/40' );
activeFilter = null;
activeQuery = $ ('#searchInput').val() || '';
loadPage(1); // si recherche vide => liste complète, sinon recherche paginée
toggleShowAllButton();
},
error: function () {
alert("Erreur lors du chargement des données.");
}
});
});
// Edition
$ (document).on("click", ".edit-link", function(e) {
// Edition (ouvrir modale)
$ (document).on('click', '.edit-link', function(e) {
e.preventDefault();
var releaseId = $ (this).data('id');
var currentStatus = $ (this).data('status');
@@ -598,7 +803,6 @@ autopostRouter.get('/', async (req, res) => {
$ ('#editModal').removeClass('hidden').fadeIn();
});
// Suppression
// ---------- Confirmation de suppression (modale) ----------
var pendingDeleteId = null;
@@ -627,7 +831,6 @@ autopostRouter.get('/', async (req, res) => {
$ (document).on('click', '.delete-link', function(e) {
e.preventDefault();
var releaseId = $ (this).data('id');
// Récupère le nom dans la 1ère cellule de la ligne (évite d’ ajouter data-name partout)
var name = $ (this).closest('tr').find('td:first').text().trim();
openConfirmModal(releaseId, name);
});
@@ -659,8 +862,8 @@ autopostRouter.get('/', async (req, res) => {
url: '/autopost/delete/' + releaseId,
type: 'POST',
success: function() {
// Effet visuel sur la ligne puis suppression
$ ('#row-' + releaseId) .css('outline', '2px solid rgba(239,68,68,0.6)')
$ ('#row-' + releaseId)
.css('outline', '2px solid rgba(239,68,68,0.6)')
.fadeOut('300', function(){ $ (this).remove(); });
showToast('Enregistrement supprimé');
},
@@ -674,71 +877,72 @@ autopostRouter.get('/', async (req, res) => {
});
// Affichage log
$ (document).on(" click" , " .log-link" , function(e) {
$ (document).on(' click' , ' .log-link' , function(e) {
e.preventDefault();
var filename = $ (this).data(" filename" );
var 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();
$ (' #logContent' ).html(data.content);
$ (' #logModal' ).removeClass('hidden').fadeIn();
},
error: function() {
alert(" Erreur lors du chargement du fichier log." );
alert(' Erreur lors du chargement du fichier log.' );
}
});
});
// Affichage mediainfo
$ (document).on(" click" , " .mediainfo-link" , function(e) {
$ (document).on(' click' , ' .mediainfo-link' , function(e) {
e.preventDefault();
var filename = $ (this).data(" filename" );
var 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();
$ (' #mediainfoContent' ).text(data.content);
$ (' #mediainfoModal' ).removeClass('hidden').fadeIn();
},
error: function() {
alert(" Erreur lors du chargement du fichier mediainfo." );
alert(' Erreur lors du chargement du fichier mediainfo.' );
}
});
});
// Fermeture modales
$ ('.close').click(function(){
$ (this).closest('.fixed').fadeOut(function(){
// Fermeture modales (croix + clic overlay)
$ ('.close').click(function() {
$ (this).closest('.fixed').fadeOut(function() {
$ (this).addClass('hidden');
});
});
$ ('.fixed').click(function(e) {
if (e.target === this) {
$ (this).fadeOut(function(){
$ (this).fadeOut(function() {
$ (this).addClass('hidden');
});
}
});
// Edition formulaire
$ ('#editForm').submit(function(e){
// Edition: submit
$ ('#editForm').submit(function(e) {
e.preventDefault();
var releaseId = $ ('#releaseId').val();
var newStatus = $ ('#statusSelect').val();
$ .ajax({
url: '/autopost/edit/' + releaseId,
type: 'POST',
data: { status: newStatus },
success: function(data ) {
success: function() {
var statusText = '';
var statusClass = '';
switch(parseInt(newStatus)) {
switch (parseInt(newStatus)) {
case 0:
statusText = 'EN ATTENTE';
statusClass = 'bg-cyan-500 text-black font-bold';
@@ -762,29 +966,32 @@ autopostRouter.get('/', async (req, res) => {
default:
statusText = 'INCONNU';
}
var row = $ ('#row-' + releaseId);
row.find('.status-text')
.removeClass('bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300')
.addClass(statusClass)
.text(statusText);
$ ('#editModal').fadeOut(function(){
$ ('#editModal').fadeOut(function() {
$ (this).addClass('hidden');
});
},
error: function() {
alert(" Erreur lors de la mise à jour." );
alert(' Erreur lors de la mise à jour.' );
}
});
});
});
// Bouton copie du JSON
document.getElementById("c opyMediainfoBtn").addEventListener("click", function () {
const content = document.getElementById("m ediainfoContent").textContent;
// --- C opy Mediainfo JSON button ---
document.getElementById('copyM ediainfoBtn').addEventListener('click', function() {
const content = document.getElementById('mediainfoContent').textContent;
navigator.clipboard.writeText(content).then(() => {
this.textContent = " ✅ Copié !" ;
setTimeout(() => this.textContent = " 📋 Copier JSON" , 2000);
}).catch(err => {
alert(" Erreur lors de la copie : " + err);
this.textContent = ' ✅ Copié !' ;
setTimeout(() => this.textContent = ' 📋 Copier JSON' , 2000);
}).catch(function(err) {
alert(' Erreur lors de la copie : ' + err);
});
});
</script>
@@ -801,20 +1008,36 @@ autopostRouter.get('/', async (req, res) => {
// --------------------------- Recherche -----------------------------
autopostRouter . get ( '/search' , async ( req , res ) => {
const q = req . query . q || "" ;
const searchQuery = "%" + 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 [ r ows] = await db . query (
` SELECT nom, status, id FROM \` ${ config . DB _TABLE } \` WHERE nom LIKE ? ORDER BY id DESC LIMIT 500 ` ,
const [ countR ows] = await db . query (
` SELECT COUNT(*) AS total FROM \` ${ config . DB _TABLE } \` WHERE nom LIKE ? ` ,
[ searchQuery ]
) ;
res . json ( rows ) ;
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 ) {
@@ -990,24 +1213,64 @@ autopostRouter.post('/delete/:id', async (req, res) => {
}
} ) ;
// --------------------------- Fu ltrage -----------------------------
// --------------------------- Fi ltrage -----------------------------
autopostRouter . get ( '/filter' , async ( req , res ) => {
const status = req . query . status ;
if ( status === undefined ) {
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 [ r ows] = await db . query (
` SELECT nom, status, id FROM \` ${ config . DB _TABLE } \` WHERE status = ? ORDER BY id DESC LIMIT 500 ` ,
const [ countR ows] = await db . query (
` SELECT COUNT(*) AS total FROM \` ${ config . DB _TABLE } \` WHERE status = ? ` ,
[ status ]
) ;
res . json ( rows ) ;
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." } ) ;
}
} ) ;
// --------------------------- 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
autopostRouter . get ( '/edit/:id' , ( req , res ) => {
res . redirect ( "/autopost/" ) ;