1
0

Ajout des fonctionnalités de sélection multiple et unification de l'authentification

## Nouvelles fonctionnalités

### Sélection multiple et actions en lot
- Ajout d'une colonne de checkboxes avec case "Tout sélectionner"
- Panneau d'actions en lot (édition et suppression de plusieurs éléments)
- Modals dédiées pour l'édition et suppression en lot
- Gestion intelligente de la sélection (état indéterminé)
- Routes serveur `/bulk-edit` et `/bulk-delete` avec validation sécurisée

### Amélioration des modals de confirmation
- Modal de confirmation pour le renvoi (remplace le confirm() basique)
- Interface cohérente avec les autres modals
- Gestion clavier (Escape/Enter) pour toutes les modals

### Unification du système d'authentification
- Fusion des deux systèmes de login (DB + config) en une seule route
- Priorité à la base de données avec fallback sur le fichier config
- Logs détaillés avec émojis pour faciliter le débogage
- Robustesse améliorée (admin de secours si DB en panne)

## Améliorations configuration et posteur
- Configuration API pour le renvoi vers le site principal (config.js)
- Correction du calcul de taille pour les liens symboliques (posteur.sh)
- Support amélioré du mode symlink avec option -L pour du
- Ajout .gitignore pour exclure le dossier .specstory

## Améliorations techniques
- Interface utilisateur moderne avec compteur de sélection
- Mise à jour visuelle en temps réel
- Validation côté serveur avec gestion d'erreurs
- Conservation de toutes les fonctionnalités existantes
This commit is contained in:
unfr 2025-09-27 15:06:16 +02:00
parent 2847ed5514
commit 3de9e74331
6 changed files with 623 additions and 14 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.specstory

View File

@ -36,5 +36,9 @@ module.exports = {
auth: { auth: {
username: 'user', username: 'user',
password: 'pass' password: 'pass'
} },
// Configuration pour le renvoi vers le site principal
apiUrl: 'A NOUS DEMANDER SUR DISCORD',
apiKey: 'A RETROUVER DANS VOTRE PROFIL'
}; };

View File

@ -78,11 +78,25 @@ while true; do
echo -e "${JAUNE}VERIF DU NZB${NORMAL}" echo -e "${JAUNE}VERIF DU NZB${NORMAL}"
nzbsizebit=$(bash ${ANALYZER} "${DOSSIER_NZB_ATTENTE}${FILESANSEXT}.nzb" | jq '.Taillebit') nzbsizebit=$(bash ${ANALYZER} "${DOSSIER_NZB_ATTENTE}${FILESANSEXT}.nzb" | jq '.Taillebit')
echo -e "NZB_SIZE : ${nzbsizebit}" echo -e "NZB_SIZE : ${nzbsizebit}"
# Détermine si on est en mode "symlink" selon la conf
symlink_mode=0
case "${MOVE_CMD:-}" in
"cp -rs"|"ln -s") symlink_mode=1 ;;
esac
if [[ "${name}" =~ \.(iso)$ ]]; then if [[ "${name}" =~ \.(iso)$ ]]; then
jsonsizebit=$(du -b -c "${name}" | grep total | awk '{ print $1 }') if (( symlink_mode )); then
# Suivre le lien symbolique pour obtenir la taille réelle
jsonsizebit=$(du -Lb -c -- "${name}" | awk '/total/ {print $1}')
else
# Comportement inchangé
jsonsizebit=$(du -b -c -- "${name}" | awk '/total/ {print $1}')
fi
else else
# Comportement inchangé
jsonsizebit=$(jq -r '.media.track[] | select(."@type" == "General") | .FileSize' "${DOSSIER_NFO}${FILESANSEXT}.json") jsonsizebit=$(jq -r '.media.track[] | select(."@type" == "General") | .FileSize' "${DOSSIER_NFO}${FILESANSEXT}.json")
fi fi
echo -e "MEDIAINFO_SIZE : ${jsonsizebit}" echo -e "MEDIAINFO_SIZE : ${jsonsizebit}"
if [[ ${nzbsizebit} -le ${jsonsizebit} ]] || [[ ${nzbsizebit} = "NAN" ]]; then if [[ ${nzbsizebit} -le ${jsonsizebit} ]] || [[ ${nzbsizebit} = "NAN" ]]; then

View File

@ -58,6 +58,10 @@ function updateTable(rows) {
? ' | <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>'
: ''; : '';
var resendLink = (parseInt(row.status) === 1)
? ' | <a href="#" class="resend-link text-green-400 hover:underline" data-id="' + row.id + '" data-filename="' + esc(row.nom) + '">Renvoyer</a>'
: '';
var tr = var tr =
'<tr id="row-' + row.id + '" data-status="' + row.status + '" class="odd:bg-gray-800 even:bg-gray-700">' + '<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">' + esc(row.nom) + '</td>' + '<td class="px-4 py-2 border border-gray-700 whitespace-nowrap">' + esc(row.nom) + '</td>' +
@ -66,7 +70,7 @@ function updateTable(rows) {
'<td class="px-4 py-2 border border-gray-700">' + '<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="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>' + '<a href="#" class="delete-link text-blue-400 hover:underline" data-id="' + row.id + '">Supprimer</a>' +
logLink + mediainfoLink + dlLink + logLink + mediainfoLink + dlLink + resendLink +
'</td>' + '</td>' +
'</tr>'; '</tr>';
@ -261,6 +265,25 @@ $(document).ready(function() {
pendingDeleteId = null; pendingDeleteId = null;
} }
// Fonctions pour la modal de renvoi
var pendingResendId = null;
var pendingResendFilename = null;
function openConfirmResendModal(id, filename) {
pendingResendId = id;
pendingResendFilename = filename;
$('#confirmResendItemName').text(filename || ('ID ' + id));
$('#confirmResendModal').removeClass('hidden').fadeIn(120);
}
function closeConfirmResendModal() {
$('#confirmResendModal').fadeOut(120, function() {
$(this).addClass('hidden');
});
pendingResendId = null;
pendingResendFilename = null;
}
function showToast(msg) { function showToast(msg) {
$('#toastMsg').text(msg || 'Opération effectuée'); $('#toastMsg').text(msg || 'Opération effectuée');
$('#toast').removeClass('hidden').fadeIn(120); $('#toast').removeClass('hidden').fadeIn(120);
@ -282,17 +305,33 @@ $(document).ready(function() {
closeConfirmModal(); closeConfirmModal();
}); });
// Clic sur loverlay pour fermer // Clic sur l'overlay pour fermer
$('#confirmDeleteModal').on('click', function(e) { $('#confirmDeleteModal').on('click', function(e) {
if (e.target === this) { closeConfirmModal(); } if (e.target === this) { closeConfirmModal(); }
}); });
// Gestionnaires pour la modal de renvoi
$('#cancelResendBtn, #confirmResendClose').on('click', function() {
closeConfirmResendModal();
});
// Clic sur l'overlay pour fermer la modal de renvoi
$('#confirmResendModal').on('click', function(e) {
if (e.target === this) { closeConfirmResendModal(); }
});
// Accessibilité clavier: Esc = fermer, Enter = confirmer // Accessibilité clavier: Esc = fermer, Enter = confirmer
$(document).on('keydown', function(e) { $(document).on('keydown', function(e) {
var modalVisible = !$('#confirmDeleteModal').hasClass('hidden'); var deleteModalVisible = !$('#confirmDeleteModal').hasClass('hidden');
if (!modalVisible) return; var resendModalVisible = !$('#confirmResendModal').hasClass('hidden');
if (e.key === 'Escape') { closeConfirmModal(); }
if (e.key === 'Enter') { $('#confirmDeleteBtn').click(); } if (deleteModalVisible) {
if (e.key === 'Escape') { closeConfirmModal(); }
if (e.key === 'Enter') { $('#confirmDeleteBtn').click(); }
} else if (resendModalVisible) {
if (e.key === 'Escape') { closeConfirmResendModal(); }
if (e.key === 'Enter') { $('#confirmResendBtn').click(); }
}
}); });
// Confirmer et supprimer via AJAX // Confirmer et supprimer via AJAX
@ -318,6 +357,38 @@ $(document).ready(function() {
}); });
}); });
// Confirmer et renvoyer via AJAX
$('#confirmResendBtn').on('click', function() {
if (!pendingResendId) return;
var releaseId = pendingResendId;
var filename = pendingResendFilename;
var $button = $(this);
var originalText = $button.text();
$button.text('Envoi...');
$.ajax({
url: '/autopost/resend/' + releaseId,
type: 'POST',
success: function(data) {
showToast(data.message || 'Renvoi effectué avec succès');
console.log('Renvoi réussi pour', filename);
},
error: function(xhr) {
var errorMsg = 'Erreur lors du renvoi';
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMsg = xhr.responseJSON.error;
}
alert(errorMsg);
console.error('Erreur renvoi:', xhr.responseJSON);
},
complete: function() {
$button.text(originalText);
closeConfirmResendModal();
}
});
});
// Affichage log // Affichage log
$(document).on('click', '.log-link', function(e) { $(document).on('click', '.log-link', function(e) {
e.preventDefault(); e.preventDefault();
@ -419,4 +490,219 @@ $(document).ready(function() {
alert('Erreur lors de la copie : ' + err); alert('Erreur lors de la copie : ' + err);
}); });
}); });
// Renvoi vers le site principal
$(document).on('click', '.resend-link', function(e) {
e.preventDefault();
var releaseId = $(this).data('id');
var filename = $(this).data('filename');
openConfirmResendModal(releaseId, filename);
});
// ==================== GESTION SÉLECTION MULTIPLE ====================
function updateBulkActions() {
const selectedCheckboxes = $('.row-checkbox:checked');
const count = selectedCheckboxes.length;
if (count > 0) {
$('#bulkActions').removeClass('hidden');
$('#selectedCount').text(count + ' élément' + (count > 1 ? 's' : '') + ' sélectionné' + (count > 1 ? 's' : ''));
} else {
$('#bulkActions').addClass('hidden');
}
// Mettre à jour la checkbox "Tout sélectionner"
const totalCheckboxes = $('.row-checkbox').length;
const allChecked = count === totalCheckboxes && totalCheckboxes > 0;
const someChecked = count > 0 && count < totalCheckboxes;
$('#selectAll').prop('checked', allChecked);
$('#selectAll').prop('indeterminate', someChecked);
}
// Checkbox "Tout sélectionner"
$(document).on('click', '#selectAll', function() {
const isChecked = $(this).is(':checked');
$('.row-checkbox').prop('checked', isChecked);
updateBulkActions();
});
// Checkboxes individuelles
$(document).on('click', '.row-checkbox', function() {
updateBulkActions();
});
// Bouton "Éditer la sélection"
$(document).on('click', '#bulkEditBtn', function() {
const selectedCheckboxes = $('.row-checkbox:checked');
const count = selectedCheckboxes.length;
if (count === 0) return;
$('#bulkEditCount').text(count);
$('#bulkEditModal').removeClass('hidden').fadeIn(120);
});
// Bouton "Supprimer la sélection"
$(document).on('click', '#bulkDeleteBtn', function() {
const selectedCheckboxes = $('.row-checkbox:checked');
const count = selectedCheckboxes.length;
if (count === 0) return;
// Construire la liste des éléments à supprimer
let listHTML = '';
selectedCheckboxes.each(function() {
const name = $(this).data('name');
const id = $(this).data('id');
listHTML += `<div class="text-sm text-gray-300 mb-1">• ${name} (ID: ${id})</div>`;
});
$('#bulkDeleteCount').text(count);
$('#bulkDeleteList').html(listHTML);
$('#bulkDeleteModal').removeClass('hidden').fadeIn(120);
});
// Fermeture des modals en lot
$('#bulkEditClose, #bulkDeleteClose, #cancelBulkDeleteBtn').on('click', function() {
$('#bulkEditModal, #bulkDeleteModal').fadeOut(120, function() {
$(this).addClass('hidden');
});
});
// Clic sur l'overlay pour fermer les modals en lot
$('#bulkEditModal, #bulkDeleteModal').on('click', function(e) {
if (e.target === this) {
$(this).fadeOut(120, function() {
$(this).addClass('hidden');
});
}
});
// Soumission du formulaire d'édition en lot
$('#bulkEditForm').on('submit', function(e) {
e.preventDefault();
const selectedCheckboxes = $('.row-checkbox:checked');
const newStatus = $('#bulkStatusSelect').val();
const ids = [];
selectedCheckboxes.each(function() {
ids.push($(this).data('id'));
});
if (ids.length === 0) return;
const $submitBtn = $('#bulkEditForm button[type="submit"]');
const originalText = $submitBtn.text();
$submitBtn.text('Mise à jour...').prop('disabled', true);
$.ajax({
url: '/autopost/bulk-edit',
type: 'POST',
data: {
ids: ids,
status: newStatus,
_csrf: window.__BOOTSTRAP__.csrf
},
success: function(data) {
// Mettre à jour l'interface pour chaque ligne modifiée
ids.forEach(function(id) {
updateRowStatus(id, newStatus);
});
showToast(`${ids.length} élément(s) mis à jour`);
// Fermer la modal et désélectionner
$('#bulkEditModal').fadeOut(120, function() {
$(this).addClass('hidden');
});
$('.row-checkbox').prop('checked', false);
updateBulkActions();
},
error: function(xhr) {
let errorMsg = 'Erreur lors de la mise à jour en lot';
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMsg = xhr.responseJSON.error;
}
alert(errorMsg);
},
complete: function() {
$submitBtn.text(originalText).prop('disabled', false);
}
});
});
// Confirmation de suppression en lot
$('#confirmBulkDeleteBtn').on('click', function() {
const selectedCheckboxes = $('.row-checkbox:checked');
const ids = [];
selectedCheckboxes.each(function() {
ids.push($(this).data('id'));
});
if (ids.length === 0) return;
const $button = $(this);
const originalText = $button.text();
$button.text('Suppression...').prop('disabled', true);
$.ajax({
url: '/autopost/bulk-delete',
type: 'POST',
data: {
ids: ids,
_csrf: window.__BOOTSTRAP__.csrf
},
success: function(data) {
// Supprimer visuellement les lignes
ids.forEach(function(id) {
$('#row-' + id)
.css('outline', '2px solid rgba(239,68,68,0.6)')
.fadeOut('300', function(){ $(this).remove(); });
});
showToast(`${ids.length} élément(s) supprimé(s)`);
// Fermer la modal
$('#bulkDeleteModal').fadeOut(120, function() {
$(this).addClass('hidden');
});
updateBulkActions();
},
error: function(xhr) {
let errorMsg = 'Erreur lors de la suppression en lot';
if (xhr.responseJSON && xhr.responseJSON.error) {
errorMsg = xhr.responseJSON.error;
}
alert(errorMsg);
},
complete: function() {
$button.text(originalText).prop('disabled', false);
}
});
});
// Fonction utilitaire pour mettre à jour le statut d'une ligne
function updateRowStatus(id, newStatus) {
const $row = $('#row-' + id);
const $statusCell = $row.find('.status-text');
let statusText = '';
let statusClass = '';
switch (parseInt(newStatus)) {
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';
}
$statusCell.removeClass().addClass('px-4 py-2 border border-gray-700 status-text whitespace-nowrap ' + statusClass);
$statusCell.text(statusText);
$row.attr('data-status', newStatus);
}
}); });

View File

@ -84,16 +84,22 @@ function renderRow(row) {
const dlLink = (parseInt(row.status) === 1) const 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="/autopost/dl?name=' + encodeURIComponent(row.nom) + '" class="dl-link text-blue-400 hover:underline">DL</a>'
: ''; : '';
const resendLink = (parseInt(row.status) === 1)
? ' | <a href="#" class="resend-link text-green-400 hover:underline" data-id="' + row.id + '" data-filename="' + esc(row.nom) + '">Renvoyer</a>'
: '';
return ` return `
<tr id="row-${row.id}" data-status="${row.status}" class="odd:bg-gray-800 even:bg-gray-700"> <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 text-center">
<input type="checkbox" class="row-checkbox rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500" data-id="${row.id}" data-name="${esc(row.nom)}">
</td>
<td class="px-4 py-2 border border-gray-700">${esc(row.nom)}</td> <td class="px-4 py-2 border border-gray-700">${esc(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 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">${row.id}</td>
<td class="px-4 py-2 border border-gray-700 whitespace-nowrap"> <td class="px-4 py-2 border border-gray-700 whitespace-nowrap">
<a href="#" class="edit-link text-blue-400 hover:underline" data-id="${row.id}" data-status="${row.status}">Editer</a> | <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> <a href="#" class="delete-link text-blue-400 hover:underline" data-id="${row.id}">Supprimer</a>
${logLink}${mediainfoLink}${dlLink} ${logLink}${mediainfoLink}${dlLink}${resendLink}
</td> </td>
</tr>`; </tr>`;
} }
@ -207,14 +213,66 @@ autopostRouter.get('/login', (req, res) => {
`); `);
}); });
autopostRouter.post('/login', (req, res) => { autopostRouter.post('/login', async (req, res) => {
const { username, password } = req.body; const { username, password } = req.body;
if (username === config.auth.username && password === config.auth.password) {
console.log(`[LOGIN] Tentative de connexion pour "${username}"`);
console.log(`[LOGIN] Password reçu: longueur ${password ? password.length : 0} caractères`);
// 1. Essayer d'abord l'authentification via la base de données
try {
console.log(`[LOGIN] Tentative d'authentification DB pour "${username}"`);
const [rows] = await db.query(
'SELECT * FROM core_members WHERE name = ? AND member_group_id IN (1,2) LIMIT 1',
[username]
);
console.log(`[LOGIN] Nombre de résultats DB: ${rows.length}`);
if (rows.length > 0) {
const member = rows[0];
console.log(`[LOGIN] Utilisateur trouvé en DB:`, {
member_id: member.member_id,
name: member.name,
group: member.member_group_id
});
// Vérification du hash Argon2id
console.log(`[LOGIN] Vérification du mot de passe avec Argon2id`);
const valid = await argon2.verify(member.members_pass_hash, password);
console.log(`[LOGIN] Résultat vérification Argon2id: ${valid}`);
if (valid) {
console.log(`[LOGIN] ✅ Authentification DB réussie pour "${member.name}"`);
req.session.authenticated = true; req.session.authenticated = true;
res.redirect('/autopost'); req.session.user_id = member.member_id;
req.session.user_name = member.name;
return res.redirect('/autopost');
} else {
console.warn(`[LOGIN] ❌ Mot de passe DB incorrect pour "${member.name}"`);
}
} else { } else {
res.send('Identifiants invalides. <a href="/autopost/login">Réessayer</a>'); console.log(`[LOGIN] Aucun utilisateur trouvé en DB pour "${username}"`);
} }
} catch (err) {
console.error(`[LOGIN] Erreur DB:`, err);
// Continue vers le fallback config
}
// 2. Fallback : authentification via fichier config
console.log(`[LOGIN] Tentative d'authentification config pour "${username}"`);
if (username === config.auth.username && password === config.auth.password) {
console.log(`[LOGIN] ✅ Authentification config réussie pour "${username}"`);
req.session.authenticated = true;
req.session.user_name = username;
return res.redirect('/autopost');
} else {
console.log(`[LOGIN] ❌ Authentification config échouée pour "${username}"`);
}
// 3. Échec des deux méthodes
console.warn(`[LOGIN] 🚫 Échec total d'authentification pour "${username}"`);
res.redirect('login?e=1');
}); });
autopostRouter.get('/logout', (req, res) => { autopostRouter.get('/logout', (req, res) => {
@ -607,6 +665,184 @@ autopostRouter.post('/delete/:id', async (req, res) => {
} }
}); });
// --------------------------- Opérations en lot -----------------------------
// Édition en lot
autopostRouter.post('/bulk-edit', async (req, res) => {
const { ids, status } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'Liste d\'IDs invalide' });
}
const statusInt = parseInt(status, 10);
if (![0, 1, 2, 3, 4].includes(statusInt)) {
return res.status(400).json({ error: 'Statut invalide' });
}
// Valider que tous les IDs sont des entiers positifs
const validIds = ids.filter(id => parseInt(id, 10) > 0).map(id => parseInt(id, 10));
if (validIds.length === 0) {
return res.status(400).json({ error: 'Aucun ID valide' });
}
try {
const placeholders = validIds.map(() => '?').join(',');
const query = `UPDATE \`${config.DB_TABLE}\` SET status = ? WHERE id IN (${placeholders})`;
const params = [statusInt, ...validIds];
const [result] = await db.query(query, params);
res.json({
message: `${result.affectedRows} élément(s) mis à jour`,
updated: result.affectedRows
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: 'Erreur DB lors de la mise à jour en lot' });
}
});
// Suppression en lot
autopostRouter.post('/bulk-delete', async (req, res) => {
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({ error: 'Liste d\'IDs invalide' });
}
// Valider que tous les IDs sont des entiers positifs
const validIds = ids.filter(id => parseInt(id, 10) > 0).map(id => parseInt(id, 10));
if (validIds.length === 0) {
return res.status(400).json({ error: 'Aucun ID valide' });
}
try {
const placeholders = validIds.map(() => '?').join(',');
const query = `DELETE FROM \`${config.DB_TABLE}\` WHERE id IN (${placeholders})`;
const [result] = await db.query(query, validIds);
res.json({
message: `${result.affectedRows} élément(s) supprimé(s)`,
deleted: result.affectedRows
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: 'Erreur DB lors de la suppression en lot' });
}
});
// --------------------------- Renvoi -----------------------------
autopostRouter.post('/resend/:id', async (req, res) => {
const id = req.params.id;
try {
// Récupérer les informations de l'enregistrement
const [rows] = await db.query(`SELECT nom, status FROM \`${config.DB_TABLE}\` WHERE id = ?`, [id]);
if (rows.length === 0) {
return res.status(404).json({ error: "Enregistrement non trouvé." });
}
const row = rows[0];
const fileName = row.nom;
const status = parseInt(row.status);
// Vérifier que le statut est "ENVOI TERMINÉ" (status = 1)
if (status !== 1) {
return res.status(400).json({ error: "Seuls les enregistrements avec statut 'ENVOI TERMINÉ' peuvent être renvoyés." });
}
const base = safeBaseName(fileName);
if (!base) {
return res.status(400).json({ error: "Nom de fichier invalide." });
}
// Chemins des fichiers nécessaires
const subfolder = base.charAt(0).toUpperCase();
const nzbArchivePath = path.join(config.finishdirectory, subfolder, base + '.7z');
const jsonPath = path.join(config.infodirectory, base + '.json');
const bdinfoPath = path.join(config.infodirectory, base + '.bdinfo.txt');
const quicksummaryPath = path.join(config.infodirectory, base + '.quicksummary.txt');
// Vérifier que l'archive NZB existe
if (!fs.existsSync(nzbArchivePath)) {
return res.status(404).json({ error: "Archive NZB non trouvée." });
}
// Extraire le NZB temporairement
const tmpDir = path.join(__dirname, 'tmp');
fs.mkdirSync(tmpDir, { recursive: true });
const tmpNzbPath = path.join(tmpDir, base + '.nzb');
const extractCommand = `7z e "${nzbArchivePath}" -o"${tmpDir}" -y`;
exec(extractCommand, (extractError) => {
if (extractError) {
console.error(`Erreur lors de l'extraction: ${extractError.message}`);
return res.status(500).json({ error: "Erreur lors de l'extraction du NZB." });
}
// Vérifier si c'est un ISO (BDInfo) ou autre (JSON)
const isIso = fileName.toLowerCase().endsWith('.iso');
let curlCommand;
if (isIso) {
// Pour les ISO : bdinfo_full + bdinfo_mini + nzb
if (!fs.existsSync(bdinfoPath) || !fs.existsSync(quicksummaryPath)) {
fs.unlinkSync(tmpNzbPath);
return res.status(404).json({ error: "Fichiers BDInfo non trouvés." });
}
curlCommand = `curl -s -k -L -m 60 \\
-F rlsname=${base} \\
-F bdinfo_full=@${bdinfoPath} \\
-F bdinfo_mini=@${quicksummaryPath} \\
-F nzb=@${tmpNzbPath} \\
-F upload=upload "${config.apiUrl}${config.apiKey}"`;
} else {
// Pour les autres : generated_nfo_json + nzb
if (!fs.existsSync(jsonPath)) {
fs.unlinkSync(tmpNzbPath);
return res.status(404).json({ error: "Fichier JSON non trouvé." });
}
curlCommand = `curl -s -k -L -m 60 \\
-F rlsname=${base} \\
-F generated_nfo_json=@${jsonPath} \\
-F nzb=@${tmpNzbPath} \\
-F upload=upload "${config.apiUrl}${config.apiKey}"`;
}
// Exécuter la commande curl
exec(curlCommand, (curlError, stdout, stderr) => {
// Nettoyer le fichier temporaire
fs.unlinkSync(tmpNzbPath);
// Log des sorties curl pour debug
console.log(`=== CURL OUTPUT pour ${fileName} ===`);
console.log(`Command: ${curlCommand}`);
if (stdout) console.log(`STDOUT: ${stdout}`);
if (stderr) console.log(`STDERR: ${stderr}`);
console.log(`================================`);
if (curlError) {
console.error(`Erreur lors du renvoi: ${curlError.message}`);
return res.status(500).json({ error: "Erreur lors du renvoi vers le site principal." });
}
console.log(`Renvoi réussi pour ${fileName}`);
res.json({ success: true, message: "Renvoi effectué avec succès." });
});
});
} catch (err) {
console.error(err.message);
res.status(500).json({ error: "Erreur lors du renvoi." });
}
});
// --------------------------- Filtrage ----------------------------- // --------------------------- Filtrage -----------------------------
autopostRouter.get('/filter', async (req, res) => { autopostRouter.get('/filter', async (req, res) => {
const status = Number.isFinite(parseInt(req.query.status, 10)) const status = Number.isFinite(parseInt(req.query.status, 10))

View File

@ -114,10 +114,28 @@
{{PAGINATION_HTML}} {{PAGINATION_HTML}}
</nav> </nav>
<!-- Boutons d'actions en lot -->
<div id="bulkActions" class="hidden mb-4 p-4 bg-gray-800 rounded-lg border border-gray-600">
<div class="flex items-center justify-between flex-wrap gap-3">
<span id="selectedCount" class="text-gray-300 font-medium">0 élément sélectionné</span>
<div class="flex gap-2">
<button id="bulkEditBtn" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-semibold transition">
✏️ Éditer la sélection
</button>
<button id="bulkDeleteBtn" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg font-semibold transition">
🗑️ Supprimer la sélection
</button>
</div>
</div>
</div>
<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">
<tr> <tr>
<th class="px-4 py-2">
<input type="checkbox" id="selectAll" class="rounded border-gray-600 bg-gray-700 text-blue-600 focus:ring-blue-500">
</th>
<th class="px-4 py-2">Name</th> <th class="px-4 py-2">Name</th>
<th class="px-4 py-2">Status</th> <th class="px-4 py-2">Status</th>
<th class="px-4 py-2">ID</th> <th class="px-4 py-2">ID</th>
@ -187,6 +205,56 @@
</div> </div>
</div> </div>
<!-- Modal de confirmation de renvoi -->
<div id="confirmResendModal" 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-3xl md:max-w-4xl relative">
<button type="button" class="absolute top-3 right-3 text-gray-400 hover:text-white text-xl font-bold" id="confirmResendClose" aria-label="Fermer">&times;</button>
<h3 class="text-xl font-bold text-white mb-2">Renvoyer cet enregistrement ?</h3>
<p class="text-gray-300 text-sm mb-6">Vous êtes sur le point de renvoyer <span id="confirmResendItemName" class="text-white font-semibold"></span> vers le site principal.</p>
<div class="flex justify-end gap-3">
<button type="button" id="cancelResendBtn" class="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white">Annuler</button>
<button type="button" id="confirmResendBtn" class="px-4 py-2 rounded-lg bg-green-600 hover:bg-green-700 text-white font-semibold">Renvoyer</button>
</div>
</div>
</div>
<!-- Modal d'édition en lot -->
<div id="bulkEditModal" 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 relative">
<button type="button" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl font-bold" id="bulkEditClose">&times;</button>
<h2 class="text-2xl font-bold mb-6 text-center text-white">✏️ Éditer la sélection</h2>
<p class="text-gray-300 text-sm mb-4">Modifier le statut de <span id="bulkEditCount" class="text-white font-semibold">0</span> élément(s) sélectionné(s).</p>
<form id="bulkEditForm">
<label for="bulkStatusSelect" class="block mb-2 text-gray-300 font-medium">Nouveau statut :</label>
<select id="bulkStatusSelect" name="status"
class="w-full p-3 mb-6 rounded-lg border border-gray-700 bg-gray-800 text-white focus:ring-2 focus:ring-blue-500 focus:outline-none">
<option value="0" class="text-cyan-400">EN ATTENTE</option>
<option value="1" class="text-green-400">ENVOI TERMINÉ</option>
<option value="2" class="text-red-400">ERREUR</option>
<option value="3" class="text-pink-400">DEJA DISPONIBLE</option>
<option value="4" class="text-yellow-400">EN COURS</option>
</select>
<button type="submit" class="w-full p-3 bg-gradient-to-r from-blue-600 to-blue-500 text-white font-semibold rounded-lg shadow hover:from-blue-500 hover:to-blue-400 transform hover:-translate-y-0.5 transition">💾 Mettre à jour la sélection</button>
</form>
</div>
</div>
<!-- Modal de confirmation de suppression en lot -->
<div id="bulkDeleteModal" 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-3xl md:max-w-4xl relative">
<button type="button" class="absolute top-3 right-3 text-gray-400 hover:text-white text-xl font-bold" id="bulkDeleteClose" aria-label="Fermer">&times;</button>
<h3 class="text-xl font-bold text-white mb-2">Supprimer ces enregistrements ?</h3>
<p class="text-gray-300 text-sm mb-4">Vous êtes sur le point de supprimer <span id="bulkDeleteCount" class="text-white font-semibold">0</span> élément(s) sélectionné(s).</p>
<div id="bulkDeleteList" class="max-h-40 overflow-y-auto mb-6 p-3 bg-gray-800 rounded-lg border border-gray-700">
<!-- Liste des éléments à supprimer -->
</div>
<div class="flex justify-end gap-3">
<button type="button" id="cancelBulkDeleteBtn" class="px-4 py-2 rounded-lg bg-gray-700 hover:bg-gray-600 text-white">Annuler</button>
<button type="button" id="confirmBulkDeleteBtn" class="px-4 py-2 rounded-lg bg-red-600 hover:bg-red-700 text-white font-semibold">Supprimer tout</button>
</div>
</div>
</div>
<!-- Toast --> <!-- Toast -->
<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>