@ -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" > < / s c r i p t >
< script src = "/js/index.global.js" > < / s c r i p t >
< / h e a d >
< / h e a d >
< 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 < / h 2 >
< h2 class = "text-center text-2xl font-bold text-white mb-4" > Authentification < / h 2 >
< 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 ,
@ -128,7 +233,7 @@ autopostRouter.get('/', async (req, res) => {
) ;
) ;
let html = `
let html = `
< ! DOCTYPE html >
< ! DOCTYPE html >
< html lang = "fr" >
< html lang = "fr" >
< head >
< head >
< meta charset = "UTF-8" >
< meta charset = "UTF-8" >
@ -137,8 +242,9 @@ autopostRouter.get('/', async (req, res) => {
< script src = "/jquery/jquery.min.js" > < / s c r i p t >
< script src = "/jquery/jquery.min.js" > < / s c r i p t >
< 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" >
< / h e a d >
< / h e a d >
< 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" >
< div class = "w-full px-4" >
<!-- Header -- >
< div class = "mb-6" >
< div class = "mb-6" >
< div class = "flex items-center justify-between" >
< 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" >
< 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) => {
< / d i v >
< / d i v >
< / d i v >
< / d i v >
< 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" >
< ul class = "inline-flex items-center -space-x-px" >
< li >
< 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> `
: ` <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> `
}
}
< / l i >
< / l i >
@ -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 } < / s p a n >
< span class = "px-4 py-2 leading-tight text-gray-700 bg-white border border-gray-300" > Page $ { page } sur $ { totalPages } < / s p a n >
< / l i >
< / l i >
< 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> `
: ` <span class="px-3 py-2 leading-tight text-gray-500 bg-gray-200 border border-gray-300 rounded-r-lg">Suivant</span> `
}
}
< / l i >
< / l i >
< / u l >
< / u l >
< / n a v >
< / n a v >
< div class = "overflow-x-auto" >
< div class = "overflow-x-auto" >
< table class = "min-w-full border border-gray-700 shadow-lg rounded-lg overflow-hidden" >
< 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" >
< 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 < / t h >
< th class = "px-4 py-2" > Actions < / t h >
< / t r >
< / t r >
< / t h e a d >
< / t h e a d >
< tbody class = "divide-y divide-gray-700" >
< tbody class = "divide-y divide-gray-700" > ` ;
` ;
rows . forEach ( row => {
rows . forEach ( row => {
let statusText = '' ;
let statusText = '' ;
@ -298,13 +406,13 @@ autopostRouter.get('/', async (req, res) => {
statusText = 'INCONNU' ;
statusText = 'INCONNU' ;
}
}
let logLink = ( parseInt ( row . status ) === 1 || parseInt ( row . status ) === 2 )
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 )
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
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 += `
html += `
@ -323,6 +431,7 @@ autopostRouter.get('/', async (req, res) => {
` ;
` ;
} ) ;
} ) ;
html += `
html += `
< / t b o d y >
< / t b o d y >
< / t a b l e >
< / t a b l e >
@ -332,7 +441,6 @@ autopostRouter.get('/', async (req, res) => {
<!-- Modale d ' édition -- >
<!-- 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 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" >
< div class = "bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-md transform transition-all scale-95" >
<!-- Bouton fermer -- >
<!-- Bouton fermer -- >
< button type = "button" class = "absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close" >
< button type = "button" class = "absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close" >
& times ;
& times ;
@ -345,7 +453,6 @@ autopostRouter.get('/', async (req, res) => {
<!-- Formulaire -- >
<!-- Formulaire -- >
< form id = "editForm" >
< form id = "editForm" >
<!-- Label + Select -- >
<!-- Label + Select -- >
< label for = "statusSelect" class = "block mb-2 text-gray-300 font-medium" > Status : < / l a b e l >
< label for = "statusSelect" class = "block mb-2 text-gray-300 font-medium" > Status : < / l a b e l >
< select id = "statusSelect" name = "status"
< select id = "statusSelect" name = "status"
@ -368,11 +475,9 @@ autopostRouter.get('/', async (req, res) => {
< / d i v >
< / d i v >
< / d i v >
< / d i v >
<!-- Modale pour afficher le log -- >
<!-- 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 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 -- >
< div class = "bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-7xl relative" > <!-- élargi max - w -- >
<!-- Bouton fermer -- >
<!-- Bouton fermer -- >
< button type = "button" class = "absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close log-close" >
< button type = "button" class = "absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close log-close" >
& times ;
& times ;
@ -387,14 +492,12 @@ autopostRouter.get('/', async (req, res) => {
< pre id = "logContent"
< 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" >
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" >
< / p r e >
< / p r e >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
<!-- Modale pour afficher le mediainfo -- >
<!-- 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 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" >
< div class = "bg-gray-900 p-6 rounded-2xl shadow-2xl w-full max-w-6xl relative" >
<!-- Bouton fermer -- >
<!-- Bouton fermer -- >
< button type = "button"
< button type = "button"
class = "absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold close mediainfo-close"
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"
< 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" >
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" >
< / p r e >
< / p r e >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
@ -449,13 +551,29 @@ autopostRouter.get('/', async (req, res) => {
< span id = "toastMsg" > Supprimé . < / s p a n >
< span id = "toastMsg" > Supprimé . < / s p a n >
< / d i v >
< / d i v >
< script >
< 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 ) {
function updateTable ( rows ) {
var tbody = $ ( "table tbody" ) ;
var tbody = $ ( 'table tbody' ) ;
tbody . empty ( ) ;
tbody . empty ( ) ;
rows . forEach ( function ( row ) {
rows . forEach ( function ( row ) {
var statusText = '' ;
var statusText = '' ;
var statusClass = '' ;
var statusClass = '' ;
switch ( parseInt ( row . status ) ) {
switch ( parseInt ( row . status ) ) {
case 0 :
case 0 :
statusText = 'EN ATTENTE' ;
statusText = 'EN ATTENTE' ;
@ -482,113 +600,200 @@ autopostRouter.get('/', async (req, res) => {
}
}
var logLink = ( parseInt ( row . status ) === 1 || parseInt ( row . status ) === 2 )
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>'
? ' | <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>'
: '' ;
: '' ;
var tr = '<tr id="row-' + row . id + '" data-status="' + row . status + '" class="odd:bg-gray-800 even:bg-gray-700">' +
var mediainfoLink = ( parseInt ( row . status ) === 0 || parseInt ( row . status ) === 1 || parseInt ( row . status ) === 2 )
'<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">' + row . nom + '</td>' +
? ' | <a href="#" class="mediainfo-link text-blue-400 hover:underline" data-filename="' + row . nom + '">Mediainfo</a>'
'<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">' +
var dlLink = ( parseInt ( row . status ) === 1 )
'<a href="#" class="edit-link text-blue-400 hover:underline" data-id="' + row . id + '" data-status="' + row . status + '">Editer</a> | ' +
? ' | <a href="/autopost/dl?name=' + encodeURIComponent ( row . nom ) + '" class="dl-link text-blue-400 hover:underline">DL</a>'
'<a href="#" class="delete-link text-blue-400 hover:underline" data-id="' + row . id + '">Supprimer</a>' +
: '' ;
logLink + mediainfoLink + dlLink +
'</td>' +
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>' ;
'</tr>' ;
tbody . append ( 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 ( ) ;
let q = $ ( this ) . val ( ) ;
searchTimer = setTimeout ( function ( ) {
searchTimer = setTimeout ( function ( ) {
$ . ajax ( {
activeQuery = q || '' ;
url : '/autopost/search' ,
activeFilter = null ; // on sort du mode filtre
type : 'GET' ,
$ ( '.filter-card' ) . removeClass ( 'ring-4 ring-white/40' ) ;
data : { q : q } ,
loadPage ( 1 ) ; // charge page 1 avec pagination AJAX
dataType : 'json' ,
success : function ( data ) {
updateTable ( data ) ;
$ ( ".filter-card" ) . removeClass ( "ring-4 ring-white/40" ) ;
toggleShowAllButton ( ) ;
toggleShowAllButton ( ) ;
} ,
} , 300 ) ;
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
} ) ;
} ) ;
// 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 ( ) {
function toggleShowAllButton ( ) {
if ( $ ( ".filter-card.ring-4" ) . length === 0 ) {
if ( $ ( '.filter-card.ring-4' ) . length === 0 ) {
$ ( '#showAll' ) . addClass ( 'hidden' ) ;
$ ( '#showAll' ) . addClass ( 'hidden' ) ;
} else {
} else {
$ ( '#showAll' ) . removeClass ( 'hidden' ) ;
$ ( '#showAll' ) . removeClass ( 'hidden' ) ;
}
}
}
}
// Filtrage par clic sur une card
// Filtrage par clic sur une card
$ ( document ) . on ( "click" , ".filter-card" , function ( ) {
$ ( document ) . on ( 'click' , '.filter-card' , function ( ) {
const status = parseInt ( $ ( this ) . data ( "status" ) , 10 ) ;
const status = parseInt ( $ ( this ) . data ( 'status' ) , 10 ) ;
$ ( ".filter-card" ) . removeClass ( "ring-4 ring-white/40" ) ;
$ ( '.filter-card' ) . removeClass ( 'ring-4 ring-white/40' ) ;
$ ( this ) . addClass ( "ring-4 ring-white/40" ) ;
$ ( this ) . addClass ( 'ring-4 ring-white/40' ) ;
$ . ajax ( {
activeFilter = status ;
url : '/autopost/filter' ,
activeQuery = '' ; // on sort du mode recherche
type : 'GET' ,
loadPage ( 1 ) ; // page 1 du filtre
data : { status } ,
dataType : 'json' ,
success : function ( data ) {
updateTable ( data ) ;
toggleShowAllButton ( ) ;
toggleShowAllButton ( ) ;
} ,
error : function ( ) {
alert ( "Erreur lors du filtrage." ) ;
}
} ) ;
} ) ;
} ) ;
// Bouton tout afficher
// Bouton tout afficher
$ ( document ) . on ( "click" , "#showAll" , function ( ) {
$ ( document ) . on ( 'click' , '#showAll' , function ( ) {
$ ( ".filter-card" ) . removeClass ( "ring-4 ring-white/40" ) ;
$ ( '.filter-card' ) . removeClass ( 'ring-4 ring-white/40' ) ;
activeFilter = null ;
$ . ajax ( {
activeQuery = $ ( '#searchInput' ) . val ( ) || '' ;
url : '/autopost/search' ,
loadPage ( 1 ) ; // si recherche vide => liste complète, sinon recherche paginée
type : 'GET' ,
data : { q : $ ( "#searchInput" ) . val ( ) || "" } ,
dataType : 'json' ,
success : function ( data ) {
updateTable ( data ) ;
toggleShowAllButton ( ) ;
toggleShowAllButton ( ) ;
} ,
error : function ( ) {
alert ( "Erreur lors du chargement des données." ) ;
}
} ) ;
} ) ;
} ) ;
// Edition (ouvrir modale)
// Edition
$ ( document ) . on ( 'click' , '.edit-link' , function ( e ) {
$ ( document ) . on ( "click" , ".edit-link" , function ( e ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
var releaseId = $ ( this ) . data ( 'id' ) ;
var releaseId = $ ( this ) . data ( 'id' ) ;
var currentStatus = $ ( this ) . data ( 'status' ) ;
var currentStatus = $ ( this ) . data ( 'status' ) ;
@ -598,7 +803,6 @@ autopostRouter.get('/', async (req, res) => {
$ ( '#editModal' ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
$ ( '#editModal' ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
} ) ;
} ) ;
// Suppression
// ---------- Confirmation de suppression (modale) ----------
// ---------- Confirmation de suppression (modale) ----------
var pendingDeleteId = null ;
var pendingDeleteId = null ;
@ -627,7 +831,6 @@ autopostRouter.get('/', async (req, res) => {
$ ( document ) . on ( 'click' , '.delete-link' , function ( e ) {
$ ( document ) . on ( 'click' , '.delete-link' , function ( e ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
var releaseId = $ ( this ) . data ( 'id' ) ;
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 ( ) ;
var name = $ ( this ) . closest ( 'tr' ) . find ( 'td:first' ) . text ( ) . trim ( ) ;
openConfirmModal ( releaseId , name ) ;
openConfirmModal ( releaseId , name ) ;
} ) ;
} ) ;
@ -659,8 +862,8 @@ autopostRouter.get('/', async (req, res) => {
url : '/autopost/delete/' + releaseId ,
url : '/autopost/delete/' + releaseId ,
type : 'POST' ,
type : 'POST' ,
success : function ( ) {
success : function ( ) {
// Effet visuel sur la ligne puis suppression
$ ( '#row-' + releaseId )
$ ( '#row-' + releaseId ) . css ( 'outline' , '2px solid rgba(239,68,68,0.6)' )
. css ( 'outline' , '2px solid rgba(239,68,68,0.6)' )
. fadeOut ( '300' , function ( ) { $ ( this ) . remove ( ) ; } ) ;
. fadeOut ( '300' , function ( ) { $ ( this ) . remove ( ) ; } ) ;
showToast ( 'Enregistrement supprimé' ) ;
showToast ( 'Enregistrement supprimé' ) ;
} ,
} ,
@ -674,71 +877,72 @@ autopostRouter.get('/', async (req, res) => {
} ) ;
} ) ;
// Affichage log
// Affichage log
$ ( document ) . on ( "click" , ".log-link" , function ( e ) {
$ ( document ) . on ( 'click' , '.log-link' , function ( e ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
var filename = $ ( this ) . data ( "filename" ) ;
var filename = $ ( this ) . data ( 'filename' ) ;
$ . ajax ( {
$ . ajax ( {
url : '/autopost/log' ,
url : '/autopost/log' ,
type : 'GET' ,
type : 'GET' ,
data : { name : filename } ,
data : { name : filename } ,
dataType : 'json' ,
dataType : 'json' ,
success : function ( data ) {
success : function ( data ) {
$ ( "#logContent" ) . html ( data . content ) ;
$ ( '#logContent' ) . html ( data . content ) ;
$ ( "#logModal" ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
$ ( '#logModal' ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
} ,
} ,
error : function ( ) {
error : function ( ) {
alert ( "Erreur lors du chargement du fichier log." ) ;
alert ( 'Erreur lors du chargement du fichier log.' ) ;
}
}
} ) ;
} ) ;
} ) ;
} ) ;
// Affichage mediainfo
// Affichage mediainfo
$ ( document ) . on ( "click" , ".mediainfo-link" , function ( e ) {
$ ( document ) . on ( 'click' , '.mediainfo-link' , function ( e ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
var filename = $ ( this ) . data ( "filename" ) ;
var filename = $ ( this ) . data ( 'filename' ) ;
$ . ajax ( {
$ . ajax ( {
url : '/autopost/mediainfo' ,
url : '/autopost/mediainfo' ,
type : 'GET' ,
type : 'GET' ,
data : { name : filename } ,
data : { name : filename } ,
dataType : 'json' ,
dataType : 'json' ,
success : function ( data ) {
success : function ( data ) {
$ ( "#mediainfoContent" ) . text ( data . content ) ;
$ ( '#mediainfoContent' ) . text ( data . content ) ;
$ ( "#mediainfoModal" ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
$ ( '#mediainfoModal' ) . removeClass ( 'hidden' ) . fadeIn ( ) ;
} ,
} ,
error : function ( ) {
error : function ( ) {
alert ( "Erreur lors du chargement du fichier mediainfo." ) ;
alert ( 'Erreur lors du chargement du fichier mediainfo.' ) ;
}
}
} ) ;
} ) ;
} ) ;
} ) ;
// Fermeture modales
// Fermeture modales (croix + clic overlay)
$ ( '.close' ) . click ( function ( ) {
$ ( '.close' ) . click ( function ( ) {
$ ( this ) . closest ( '.fixed' ) . fadeOut ( function ( ) {
$ ( this ) . closest ( '.fixed' ) . fadeOut ( function ( ) {
$ ( this ) . addClass ( 'hidden' ) ;
$ ( this ) . addClass ( 'hidden' ) ;
} ) ;
} ) ;
} ) ;
} ) ;
$ ( '.fixed' ) . click ( function ( e ) {
$ ( '.fixed' ) . click ( function ( e ) {
if ( e . target === this ) {
if ( e . target === this ) {
$ ( this ) . fadeOut ( function ( ) {
$ ( this ) . fadeOut ( function ( ) {
$ ( this ) . addClass ( 'hidden' ) ;
$ ( this ) . addClass ( 'hidden' ) ;
} ) ;
} ) ;
}
}
} ) ;
} ) ;
// Edition formulaire
// Edition: submit
$ ( '#editForm' ) . submit ( function ( e ) {
$ ( '#editForm' ) . submit ( function ( e ) {
e . preventDefault ( ) ;
e . preventDefault ( ) ;
var releaseId = $ ( '#releaseId' ) . val ( ) ;
var releaseId = $ ( '#releaseId' ) . val ( ) ;
var newStatus = $ ( '#statusSelect' ) . val ( ) ;
var newStatus = $ ( '#statusSelect' ) . val ( ) ;
$ . ajax ( {
$ . ajax ( {
url : '/autopost/edit/' + releaseId ,
url : '/autopost/edit/' + releaseId ,
type : 'POST' ,
type : 'POST' ,
data : { status : newStatus } ,
data : { status : newStatus } ,
success : function ( data ) {
success : function ( ) {
var statusText = '' ;
var statusText = '' ;
var statusClass = '' ;
var statusClass = '' ;
switch ( parseInt ( newStatus ) ) {
switch ( parseInt ( newStatus ) ) {
case 0 :
case 0 :
statusText = 'EN ATTENTE' ;
statusText = 'EN ATTENTE' ;
statusClass = 'bg-cyan-500 text-black font-bold' ;
statusClass = 'bg-cyan-500 text-black font-bold' ;
@ -762,29 +966,32 @@ autopostRouter.get('/', async (req, res) => {
default :
default :
statusText = 'INCONNU' ;
statusText = 'INCONNU' ;
}
}
var row = $ ( '#row-' + releaseId ) ;
var row = $ ( '#row-' + releaseId ) ;
row . find ( '.status-text' )
row . find ( '.status-text' )
. removeClass ( 'bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300' )
. removeClass ( 'bg-cyan-500 bg-green-300 bg-red-300 bg-pink-300 bg-yellow-300' )
. addClass ( statusClass )
. addClass ( statusClass )
. text ( statusText ) ;
. text ( statusText ) ;
$ ( '#editModal' ) . fadeOut ( function ( ) {
$ ( '#editModal' ) . fadeOut ( function ( ) {
$ ( this ) . addClass ( 'hidden' ) ;
$ ( this ) . addClass ( 'hidden' ) ;
} ) ;
} ) ;
} ,
} ,
error : function ( ) {
error : function ( ) {
alert ( "Erreur lors de la mise à jour." ) ;
alert ( 'Erreur lors de la mise à jour.' ) ;
}
}
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
} ) ;
// Bouton copie du JSON
document . getElementById ( "copyMediainfoBtn" ) . addEventListener ( "click" , function ( ) {
// --- Copy Mediainfo JSON button ---
const content = document . getElementById ( "mediainfoContent" ) . textContent ;
document . getElementById ( 'copyMediainfoBtn' ) . addEventListener ( 'click' , function ( ) {
const content = document . getElementById ( 'mediainfoContent' ) . textContent ;
navigator . clipboard . writeText ( content ) . then ( ( ) => {
navigator . clipboard . writeText ( content ) . then ( ( ) => {
this . textContent = "✅ Copié !" ;
this . textContent = '✅ Copié !' ;
setTimeout ( ( ) => this . textContent = "📋 Copier JSON" , 2000 ) ;
setTimeout ( ( ) => this . textContent = '📋 Copier JSON' , 2000 ) ;
} ) . catch ( err => {
} ) . catch ( function ( err ) {
alert ( "Erreur lors de la copie : " + err ) ;
alert ( 'Erreur lors de la copie : ' + err ) ;
} ) ;
} ) ;
} ) ;
} ) ;
< / s c r i p t >
< / s c r i p t >
@ -801,20 +1008,36 @@ autopostRouter.get('/', async (req, res) => {
// --------------------------- Recherche -----------------------------
// --------------------------- Recherche -----------------------------
autopostRouter . get ( '/search' , async ( req , res ) => {
autopostRouter . get ( '/search' , async ( req , res ) => {
const q = req . query . q || "" ;
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 {
try {
const [ rows ] = await db . query (
const [ countR ows] = await db . query (
` SELECT nom, status, id FROM \` ${ config . DB _TABLE } \` WHERE nom LIKE ? ORDER BY id DESC LIMIT 500 ` ,
` SELECT COUNT(*) AS total FROM \` ${ config . DB _TABLE } \` WHERE nom LIKE ? ` ,
[ searchQuery ]
[ 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 ) {
} catch ( err ) {
console . error ( err . message ) ;
console . error ( err . message ) ;
res . status ( 500 ) . json ( { error : "Erreur lors de la requête." } ) ;
res . status ( 500 ) . json ( { error : "Erreur lors de la requête." } ) ;
}
}
} ) ;
} ) ;
// --------------------------- 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 ) {
@ -990,24 +1213,64 @@ autopostRouter.post('/delete/:id', async (req, res) => {
}
}
} ) ;
} ) ;
// --------------------------- F u ltrage -----------------------------
// --------------------------- F i ltrage -----------------------------
autopostRouter . get ( '/filter' , async ( req , res ) => {
autopostRouter . get ( '/filter' , async ( req , res ) => {
const status = req . query . status ;
const status = Number . isFinite ( parseInt ( req . query . status , 10 ) )
if ( status === undefined ) {
? parseInt ( req . query . status , 10 )
: null ;
if ( status === null ) {
return res . status ( 400 ) . json ( { error : "Status non fourni." } ) ;
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 {
try {
const [ rows ] = await db . query (
const [ countR ows] = await db . query (
` SELECT nom, status, id FROM \` ${ config . DB _TABLE } \` WHERE status = ? ORDER BY id DESC LIMIT 500 ` ,
` SELECT COUNT(*) AS total FROM \` ${ config . DB _TABLE } \` WHERE status = ? ` ,
[ 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 ) {
} catch ( err ) {
console . error ( err . message ) ;
console . error ( err . message ) ;
res . status ( 500 ) . json ( { error : "Erreur lors de la requête." } ) ;
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
// 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/" ) ;