permet de faire tourner différents jetons d'accès pour outrepasser les limitations vraiment beaucoup trop stricts de certaines api
995 lines
38 KiB
JavaScript
995 lines
38 KiB
JavaScript
/**
|
|
* FavMasToKey - Mode Filou (script de gestion des tokens multiples)
|
|
*/
|
|
|
|
// Attendre que le DOM soit chargé
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Éléments DOM
|
|
const uploadForm = document.getElementById('upload-form');
|
|
const jsonFileInput = document.getElementById('json-file');
|
|
const fileSummary = document.getElementById('file-summary');
|
|
const startMigration = document.getElementById('start-migration');
|
|
const pauseMigration = document.getElementById('pause-migration');
|
|
const cancelMigration = document.getElementById('cancel-migration');
|
|
const globalProgress = document.getElementById('global-progress');
|
|
const successProgress = document.getElementById('success-progress');
|
|
const skipProgress = document.getElementById('skip-progress');
|
|
const warnProgress = document.getElementById('warn-progress');
|
|
const errorProgress = document.getElementById('error-progress');
|
|
const operationLog = document.getElementById('operation-log');
|
|
const clearLog = document.getElementById('clear-log');
|
|
const statsDisplay = document.getElementById('stats-display');
|
|
|
|
// Sliders de configuration
|
|
const delayBetweenTokens = document.getElementById('delay-between-tokens');
|
|
const tokenCooldown = document.getElementById('token-cooldown');
|
|
const delayValue = document.getElementById('delay-value');
|
|
const cooldownValue = document.getElementById('cooldown-value');
|
|
const autoAdjust = document.getElementById('auto-adjust');
|
|
|
|
// Variables globales
|
|
let favoritesList = [];
|
|
let currentIndex = 0;
|
|
let totalItems = 0;
|
|
let isProcessing = false;
|
|
let isPaused = false;
|
|
let successCount = 0;
|
|
let errorCount = 0;
|
|
let skippedCount = 0;
|
|
let warningCount = 0;
|
|
|
|
// Variables pour la gestion des tokens
|
|
let tokens = [];
|
|
let tokensQueue = [];
|
|
let currentTokenIndex = 0;
|
|
let tokenTimers = {};
|
|
let activeRequests = 0;
|
|
let maxConcurrentRequests = 1; // Pour éviter les problèmes de race condition
|
|
|
|
// Options de migration
|
|
let migration = {
|
|
status: 'not_started',
|
|
startTime: null,
|
|
lastUpdateTime: null,
|
|
progress: {
|
|
current: 0,
|
|
total: 0,
|
|
percentage: 0
|
|
},
|
|
stats: {
|
|
success: 0,
|
|
error: 0,
|
|
skipped: 0,
|
|
warning: 0
|
|
},
|
|
options: {
|
|
delayBetweenTokens: 15, // secondes
|
|
tokenCooldown: 150, // secondes
|
|
autoAdjust: true
|
|
}
|
|
};
|
|
|
|
// ---- Gestionnaire d'événements ----
|
|
|
|
// Gérer les changements sur les sliders
|
|
if (delayBetweenTokens && delayValue) {
|
|
delayBetweenTokens.addEventListener('input', function() {
|
|
delayValue.textContent = this.value + 's';
|
|
migration.options.delayBetweenTokens = parseInt(this.value);
|
|
updateLocalStorage();
|
|
});
|
|
}
|
|
|
|
if (tokenCooldown && cooldownValue) {
|
|
tokenCooldown.addEventListener('input', function() {
|
|
cooldownValue.textContent = this.value + 's';
|
|
migration.options.tokenCooldown = parseInt(this.value);
|
|
updateLocalStorage();
|
|
});
|
|
}
|
|
|
|
if (autoAdjust) {
|
|
autoAdjust.addEventListener('change', function() {
|
|
migration.options.autoAdjust = this.checked;
|
|
updateLocalStorage();
|
|
});
|
|
}
|
|
|
|
// Gérer le téléchargement et l'analyse du fichier JSON
|
|
if (uploadForm) {
|
|
uploadForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const file = jsonFileInput.files[0];
|
|
if (!file) {
|
|
alert('Veuillez sélectionner un fichier JSON.');
|
|
return;
|
|
}
|
|
|
|
// Vérifier l'extension du fichier
|
|
if (!file.name.endsWith('.json')) {
|
|
alert('Le fichier doit être au format JSON.');
|
|
return;
|
|
}
|
|
|
|
// Lire le fichier
|
|
const reader = new FileReader();
|
|
reader.onload = function(event) {
|
|
try {
|
|
const json = JSON.parse(event.target.result);
|
|
|
|
// Vérifier la structure du fichier
|
|
if (!json['@context'] || !json.type || !json.orderedItems) {
|
|
alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.');
|
|
return;
|
|
}
|
|
|
|
favoritesList = json.orderedItems;
|
|
totalItems = favoritesList.length;
|
|
|
|
// Afficher un résumé
|
|
fileSummary.classList.remove('d-none');
|
|
fileSummary.innerHTML = `
|
|
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
|
|
Le transfert sera effectué avec les jetons configurés.
|
|
`;
|
|
|
|
// Stocker les données dans localStorage pour les conserver
|
|
localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList));
|
|
|
|
// Initialiser les données de migration
|
|
migration = {
|
|
status: 'not_started',
|
|
startTime: null,
|
|
lastUpdateTime: null,
|
|
progress: {
|
|
current: 0,
|
|
total: totalItems,
|
|
percentage: 0
|
|
},
|
|
stats: {
|
|
success: 0,
|
|
error: 0,
|
|
skipped: 0,
|
|
warning: 0
|
|
},
|
|
options: {
|
|
delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15),
|
|
tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150),
|
|
autoAdjust: autoAdjust ? autoAdjust.checked : true
|
|
}
|
|
};
|
|
|
|
// Sauvegarder les données de migration
|
|
updateLocalStorage();
|
|
|
|
// Scroll jusqu'à la section de migration
|
|
document.getElementById('step3').scrollIntoView({ behavior: 'smooth' });
|
|
|
|
} catch (error) {
|
|
alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message);
|
|
}
|
|
};
|
|
|
|
reader.onerror = function() {
|
|
alert('Erreur lors de la lecture du fichier.');
|
|
};
|
|
|
|
reader.readAsText(file);
|
|
});
|
|
}
|
|
|
|
// Effacer le journal des opérations
|
|
if (clearLog) {
|
|
clearLog.addEventListener('click', function() {
|
|
operationLog.innerHTML = '';
|
|
});
|
|
}
|
|
|
|
// Démarrer la migration
|
|
if (startMigration) {
|
|
startMigration.addEventListener('click', function() {
|
|
if (isProcessing) return;
|
|
|
|
// Récupérer les favoris du localStorage si nécessaire
|
|
if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) {
|
|
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
|
|
totalItems = favoritesList.length;
|
|
}
|
|
|
|
if (favoritesList.length === 0) {
|
|
addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Vérifier s'il y a une migration en cours à reprendre
|
|
if (localStorage.getItem('favmastokey_multitoken_migration')) {
|
|
const savedMigration = JSON.parse(localStorage.getItem('favmastokey_multitoken_migration'));
|
|
|
|
// Si la migration était en cours ou en pause, proposer de la reprendre
|
|
if (savedMigration.status === 'in_progress' || savedMigration.status === 'paused') {
|
|
const resumeConfirm = confirm(`Une migration précédente a été trouvée (${savedMigration.progress.percentage.toFixed(1)}% terminée). Voulez-vous la reprendre?`);
|
|
|
|
if (resumeConfirm) {
|
|
// Restaurer l'état de la migration
|
|
migration = savedMigration;
|
|
currentIndex = migration.progress.current;
|
|
|
|
// Restaurer les statistiques
|
|
successCount = migration.stats.success || 0;
|
|
errorCount = migration.stats.error || 0;
|
|
skippedCount = migration.stats.skipped || 0;
|
|
warningCount = migration.stats.warning || 0;
|
|
|
|
// Mettre à jour l'interface
|
|
updateProgress(migration.progress.percentage);
|
|
updateStatistics();
|
|
} else {
|
|
// Réinitialiser la migration
|
|
resetMigration();
|
|
}
|
|
} else {
|
|
// Réinitialiser la migration
|
|
resetMigration();
|
|
}
|
|
} else {
|
|
// Initialiser une nouvelle migration
|
|
resetMigration();
|
|
}
|
|
|
|
// Initialiser les tokens
|
|
initializeTokens();
|
|
|
|
if (tokens.length === 0) {
|
|
addLogEntry('Aucun jeton d\'accès disponible. Veuillez configurer au moins un jeton.', 'error');
|
|
return;
|
|
}
|
|
|
|
// Démarrer la migration
|
|
isProcessing = true;
|
|
isPaused = false;
|
|
|
|
startMigration.classList.add('d-none');
|
|
pauseMigration.classList.remove('d-none');
|
|
|
|
// Initialiser le temps de démarrage si c'est une nouvelle migration
|
|
if (migration.status === 'not_started' || migration.startTime === null) {
|
|
migration.startTime = Date.now();
|
|
}
|
|
|
|
// Mettre à jour le statut de la migration
|
|
migration.status = 'in_progress';
|
|
updateLocalStorage();
|
|
|
|
// Afficher un message de démarrage
|
|
const tokenCountMsg = tokens.length === 1 ? '1 jeton' : `${tokens.length} jetons`;
|
|
addLogEntry(`Démarrage de la migration avec ${tokenCountMsg}. Délai entre jetons: ${migration.options.delayBetweenTokens}s, Délai de récupération: ${migration.options.tokenCooldown}s`, 'info');
|
|
|
|
// Lancer le processus de migration
|
|
startTokenRotation();
|
|
});
|
|
}
|
|
|
|
// Gérer la pause de la migration
|
|
if (pauseMigration) {
|
|
pauseMigration.addEventListener('click', function() {
|
|
if (!isProcessing) return;
|
|
|
|
isPaused = !isPaused;
|
|
|
|
if (isPaused) {
|
|
pauseMigration.textContent = 'Reprendre';
|
|
addLogEntry('Migration en pause.', 'warning');
|
|
|
|
// Mettre à jour le statut de la migration
|
|
migration.status = 'paused';
|
|
updateLocalStorage();
|
|
} else {
|
|
pauseMigration.textContent = 'Pause';
|
|
addLogEntry('Reprise de la migration...', 'info');
|
|
|
|
// Mettre à jour le statut de la migration
|
|
migration.status = 'in_progress';
|
|
updateLocalStorage();
|
|
|
|
// Reprendre le traitement
|
|
startTokenRotation();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Gérer l'annulation de la migration
|
|
if (cancelMigration) {
|
|
cancelMigration.addEventListener('click', function() {
|
|
if (!isProcessing && currentIndex === 0) return;
|
|
|
|
const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?');
|
|
if (confirmCancel) {
|
|
isProcessing = false;
|
|
isPaused = false;
|
|
|
|
addLogEntry('Migration annulée.', 'error');
|
|
|
|
// Nettoyer les timers
|
|
clearAllTimers();
|
|
|
|
// Réinitialiser l'interface
|
|
startMigration.classList.remove('d-none');
|
|
pauseMigration.classList.add('d-none');
|
|
pauseMigration.textContent = 'Pause';
|
|
|
|
// Réinitialiser les tokens
|
|
resetTokensStatus();
|
|
|
|
// Réinitialiser les données de migration
|
|
resetMigration();
|
|
}
|
|
});
|
|
}
|
|
|
|
// Vérifier si nous avons des données à restaurer au chargement
|
|
function initOnLoad() {
|
|
// Récupérer les favoris du localStorage
|
|
if (localStorage.getItem('favmastokey_favorites')) {
|
|
favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites'));
|
|
totalItems = favoritesList.length;
|
|
|
|
if (fileSummary) {
|
|
fileSummary.classList.remove('d-none');
|
|
fileSummary.innerHTML = `
|
|
<strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon.
|
|
Le transfert sera effectué avec les jetons configurés.
|
|
`;
|
|
}
|
|
}
|
|
|
|
// Récupérer les données de migration
|
|
if (localStorage.getItem('favmastokey_multitoken_migration')) {
|
|
migration = JSON.parse(localStorage.getItem('favmastokey_multitoken_migration'));
|
|
currentIndex = migration.progress.current;
|
|
|
|
successCount = migration.stats.success || 0;
|
|
errorCount = migration.stats.error || 0;
|
|
skippedCount = migration.stats.skipped || 0;
|
|
warningCount = migration.stats.warning || 0;
|
|
|
|
// Mettre à jour l'interface
|
|
updateProgress(migration.progress.percentage);
|
|
updateStatistics();
|
|
|
|
// Restaurer les options
|
|
if (migration.options) {
|
|
if (delayBetweenTokens && migration.options.delayBetweenTokens) {
|
|
delayBetweenTokens.value = migration.options.delayBetweenTokens;
|
|
if (delayValue) {
|
|
delayValue.textContent = migration.options.delayBetweenTokens + 's';
|
|
}
|
|
}
|
|
|
|
if (tokenCooldown && migration.options.tokenCooldown) {
|
|
tokenCooldown.value = migration.options.tokenCooldown;
|
|
if (cooldownValue) {
|
|
cooldownValue.textContent = migration.options.tokenCooldown + 's';
|
|
}
|
|
}
|
|
|
|
if (autoAdjust && migration.options.autoAdjust !== undefined) {
|
|
autoAdjust.checked = migration.options.autoAdjust;
|
|
}
|
|
}
|
|
|
|
// Si la migration était en cours, proposer de la reprendre
|
|
if ((migration.status === 'in_progress' || migration.status === 'paused') &&
|
|
migration.progress.current < migration.progress.total) {
|
|
|
|
const timeAgo = getTimeAgoString(new Date(migration.lastUpdateTime));
|
|
|
|
// Ajouter une entrée au journal
|
|
addLogEntry(`Une migration précédente a été trouvée (${migration.progress.percentage.toFixed(1)}% terminée, ${timeAgo}). Utilisez le bouton "Démarrer la migration" pour la reprendre.`, 'info');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initialiser les tokens à partir des éléments du DOM
|
|
function initializeTokens() {
|
|
tokens = [];
|
|
tokensQueue = [];
|
|
|
|
// Ajouter le token principal
|
|
tokens.push({
|
|
id: 'primary',
|
|
name: 'Token principal',
|
|
status: 'ready',
|
|
lastUsed: null,
|
|
cooldownUntil: null,
|
|
errorCount: 0,
|
|
element: document.querySelector('.token-card:first-child')
|
|
});
|
|
|
|
// Ajouter les tokens supplémentaires
|
|
const tokenCards = document.querySelectorAll('.token-card[data-token-id]');
|
|
tokenCards.forEach(card => {
|
|
const tokenId = card.getAttribute('data-token-id');
|
|
tokens.push({
|
|
id: tokenId,
|
|
name: card.querySelector('strong').textContent,
|
|
status: 'ready',
|
|
lastUsed: null,
|
|
cooldownUntil: null,
|
|
errorCount: 0,
|
|
element: card
|
|
});
|
|
});
|
|
|
|
// Initialiser la file d'attente avec tous les tokens
|
|
resetTokensQueue();
|
|
}
|
|
|
|
// Réinitialiser la file d'attente des tokens
|
|
function resetTokensQueue() {
|
|
tokensQueue = [...tokens]; // Copie de tous les tokens
|
|
|
|
// Mélanger pour éviter d'utiliser toujours le même ordre
|
|
shuffleArray(tokensQueue);
|
|
}
|
|
|
|
// Mélanger un tableau (algorithme de Fisher-Yates)
|
|
function shuffleArray(array) {
|
|
for (let i = array.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[array[i], array[j]] = [array[j], array[i]];
|
|
}
|
|
return array;
|
|
}
|
|
|
|
// Démarrer la rotation des tokens
|
|
function startTokenRotation() {
|
|
if (!isProcessing || isPaused) {
|
|
return;
|
|
}
|
|
|
|
// Vérifier si nous avons terminé
|
|
if (currentIndex >= totalItems) {
|
|
finishMigration();
|
|
return;
|
|
}
|
|
|
|
// Si la file d'attente est vide, la réinitialiser
|
|
if (tokensQueue.length === 0) {
|
|
resetTokensQueue();
|
|
}
|
|
|
|
// Vérifier si on a un token disponible
|
|
const availableToken = findAvailableToken();
|
|
|
|
if (availableToken) {
|
|
// Traiter le prochain favori avec ce token
|
|
processWithToken(availableToken);
|
|
} else {
|
|
// Aucun token disponible actuellement, attendre et réessayer
|
|
addLogEntry('Tous les jetons sont en cours d\'utilisation ou en récupération. Attente...', 'info');
|
|
setTimeout(startTokenRotation, 5000);
|
|
}
|
|
}
|
|
|
|
// Trouver un token disponible
|
|
function findAvailableToken() {
|
|
const now = Date.now();
|
|
|
|
// Vérifier d'abord la file d'attente
|
|
for (let i = 0; i < tokensQueue.length; i++) {
|
|
const token = tokensQueue[i];
|
|
|
|
// Vérifier si le token est prêt
|
|
if (token.status === 'ready' &&
|
|
(!token.cooldownUntil || now >= token.cooldownUntil)) {
|
|
|
|
// Retirer ce token de la file d'attente
|
|
tokensQueue.splice(i, 1);
|
|
return token;
|
|
}
|
|
}
|
|
|
|
// Si aucun token n'est disponible dans la file d'attente,
|
|
// vérifier tous les tokens (au cas où un token aurait fini son cooldown)
|
|
for (const token of tokens) {
|
|
if (token.status === 'ready' &&
|
|
(!token.cooldownUntil || now >= token.cooldownUntil)) {
|
|
|
|
// Retirer ce token de la file d'attente s'il y est encore
|
|
const queueIndex = tokensQueue.findIndex(t => t.id === token.id);
|
|
if (queueIndex !== -1) {
|
|
tokensQueue.splice(queueIndex, 1);
|
|
}
|
|
|
|
return token;
|
|
}
|
|
}
|
|
|
|
// Aucun token disponible
|
|
return null;
|
|
}
|
|
|
|
// Traiter un favori avec un token spécifique
|
|
function processWithToken(token) {
|
|
if (!isProcessing || isPaused) {
|
|
return;
|
|
}
|
|
|
|
// Mettre à jour le statut du token
|
|
updateTokenStatus(token, 'active');
|
|
|
|
// URL à traiter
|
|
const url = favoritesList[currentIndex];
|
|
|
|
// Ajouter une entrée dans le journal
|
|
addLogEntry(`[${token.name}] Traitement de: ${getTruncatedUrl(url)}`, 'info');
|
|
|
|
// Envoyer la requête au serveur
|
|
fetch('process_multitoken.php', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
url: url,
|
|
token_id: token.id,
|
|
current_index: currentIndex,
|
|
total_items: totalItems
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Mettre à jour les statistiques en fonction du résultat
|
|
handleProcessResult(data, token, url);
|
|
|
|
// Mettre à jour l'index
|
|
currentIndex++;
|
|
|
|
// Mettre à jour la progression
|
|
updateProgress();
|
|
|
|
// Mettre le token en cooldown
|
|
const cooldownPeriod = migration.options.tokenCooldown * 1000; // Convertir en millisecondes
|
|
const cooldownUntil = Date.now() + cooldownPeriod;
|
|
|
|
token.lastUsed = Date.now();
|
|
token.cooldownUntil = cooldownUntil;
|
|
|
|
updateTokenStatus(token, 'cooldown');
|
|
|
|
// Programmer la réactivation du token après le cooldown
|
|
tokenTimers[token.id] = setTimeout(() => {
|
|
// Remettre le token en état "prêt"
|
|
updateTokenStatus(token, 'ready');
|
|
|
|
// Ajouter le token à la fin de la file d'attente
|
|
tokensQueue.push(token);
|
|
|
|
}, cooldownPeriod);
|
|
|
|
// Démarrer le traitement du prochain favori après un délai
|
|
setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000);
|
|
})
|
|
.catch(error => {
|
|
console.error('Erreur lors du traitement:', error);
|
|
|
|
// Ajouter l'erreur au journal
|
|
addLogEntry(`[${token.name}] Erreur lors du traitement: ${error.message}`, 'error');
|
|
|
|
// Incrémenter le compteur d'erreurs du token
|
|
token.errorCount = (token.errorCount || 0) + 1;
|
|
|
|
// Si trop d'erreurs avec ce token, le mettre en erreur
|
|
if (token.errorCount >= 3) {
|
|
updateTokenStatus(token, 'error');
|
|
addLogEntry(`[${token.name}] Ce jeton rencontre des problèmes répétés et a été désactivé.`, 'error');
|
|
} else {
|
|
// Sinon, mettre le token en cooldown plus court
|
|
const cooldownPeriod = 30 * 1000; // 30 secondes
|
|
const cooldownUntil = Date.now() + cooldownPeriod;
|
|
|
|
token.lastUsed = Date.now();
|
|
token.cooldownUntil = cooldownUntil;
|
|
|
|
updateTokenStatus(token, 'cooldown');
|
|
|
|
// Programmer la réactivation du token après le cooldown
|
|
tokenTimers[token.id] = setTimeout(() => {
|
|
updateTokenStatus(token, 'ready');
|
|
tokensQueue.push(token);
|
|
}, cooldownPeriod);
|
|
}
|
|
|
|
// Augmenter le compteur d'erreurs
|
|
errorCount++;
|
|
updateStatistics();
|
|
|
|
// Continuer avec le prochain token après un délai
|
|
setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000);
|
|
});
|
|
}
|
|
|
|
// Gérer le résultat du traitement
|
|
function handleProcessResult(data, token, url) {
|
|
if (data.success) {
|
|
// Résultat du traitement
|
|
const result = data.result;
|
|
|
|
// Construction du message avec détails si disponibles
|
|
let message = result.message;
|
|
if (result.details) {
|
|
message += ` (${result.details})`;
|
|
}
|
|
|
|
// Ajouter l'entrée au journal avec le nom du token
|
|
addLogEntry(`[${token.name}] ${message}`, result.status);
|
|
|
|
// Mettre à jour les compteurs
|
|
if (result.status === 'success') {
|
|
successCount++;
|
|
token.errorCount = 0; // Réinitialiser le compteur d'erreurs
|
|
} else if (result.status === 'error') {
|
|
errorCount++;
|
|
|
|
// Incrémenter le compteur d'erreurs du token
|
|
token.errorCount = (token.errorCount || 0) + 1;
|
|
|
|
// Si autoAdjust est activé et qu'il y a beaucoup d'erreurs, augmenter les délais
|
|
if (migration.options.autoAdjust && token.errorCount >= 2) {
|
|
if (result.error_type === 'rate_limit') {
|
|
// Augmenter le délai entre les tokens
|
|
if (migration.options.delayBetweenTokens < 60) {
|
|
migration.options.delayBetweenTokens += 5;
|
|
if (delayBetweenTokens) delayBetweenTokens.value = migration.options.delayBetweenTokens;
|
|
if (delayValue) delayValue.textContent = migration.options.delayBetweenTokens + 's';
|
|
|
|
addLogEntry(`Délai entre jetons automatiquement augmenté à ${migration.options.delayBetweenTokens}s en raison de limitations d'API.`, 'warning');
|
|
}
|
|
|
|
// Augmenter le délai de cooldown
|
|
if (migration.options.tokenCooldown < 300) {
|
|
migration.options.tokenCooldown += 30;
|
|
if (tokenCooldown) tokenCooldown.value = migration.options.tokenCooldown;
|
|
if (cooldownValue) cooldownValue.textContent = migration.options.tokenCooldown + 's';
|
|
|
|
addLogEntry(`Délai de récupération automatiquement augmenté à ${migration.options.tokenCooldown}s.`, 'warning');
|
|
}
|
|
|
|
// Mettre à jour le localStorage
|
|
updateLocalStorage();
|
|
}
|
|
}
|
|
} else if (result.status === 'info') {
|
|
skippedCount++;
|
|
token.errorCount = 0; // Réinitialiser le compteur d'erreurs
|
|
} else if (result.status === 'warning') {
|
|
warningCount++;
|
|
|
|
// Si c'est une erreur de rate limit, incrémenter le compteur d'erreurs
|
|
if (result.error_type === 'rate_limit') {
|
|
token.errorCount = (token.errorCount || 0) + 1;
|
|
}
|
|
}
|
|
|
|
// Mettre à jour les statistiques
|
|
updateStatistics();
|
|
} else {
|
|
// Erreur lors du traitement
|
|
addLogEntry(`[${token.name}] Erreur: ${data.message}`, 'error');
|
|
errorCount++;
|
|
updateStatistics();
|
|
|
|
// Incrémenter le compteur d'erreurs du token
|
|
token.errorCount = (token.errorCount || 0) + 1;
|
|
}
|
|
}
|
|
|
|
// Mettre à jour le statut d'un token
|
|
function updateTokenStatus(token, status) {
|
|
token.status = status;
|
|
|
|
if (token.element) {
|
|
// Mettre à jour la classe CSS
|
|
token.element.classList.remove('active', 'cooldown', 'error');
|
|
|
|
// Mettre à jour l'indicateur de statut
|
|
const statusIndicator = token.element.querySelector('.token-status');
|
|
const statusText = token.element.querySelector('.text-muted.small');
|
|
|
|
if (statusIndicator) {
|
|
statusIndicator.classList.remove('ready', 'cooldown', 'error', 'idle');
|
|
}
|
|
|
|
switch (status) {
|
|
case 'active':
|
|
token.element.classList.add('active');
|
|
if (statusIndicator) statusIndicator.classList.add('ready');
|
|
if (statusText) statusText.textContent = 'En cours d\'utilisation';
|
|
break;
|
|
|
|
case 'cooldown':
|
|
token.element.classList.add('cooldown');
|
|
if (statusIndicator) statusIndicator.classList.add('cooldown');
|
|
|
|
if (statusText && token.cooldownUntil) {
|
|
const cooldownSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000);
|
|
statusText.textContent = `Récupération: ${cooldownSeconds}s`;
|
|
|
|
// Mettre à jour le compteur de cooldown toutes les secondes
|
|
const updateInterval = setInterval(() => {
|
|
const remainingSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000);
|
|
if (remainingSeconds <= 0) {
|
|
clearInterval(updateInterval);
|
|
if (statusText) statusText.textContent = 'Prêt';
|
|
} else {
|
|
if (statusText) statusText.textContent = `Récupération: ${remainingSeconds}s`;
|
|
}
|
|
}, 1000);
|
|
}
|
|
break;
|
|
|
|
case 'error':
|
|
token.element.classList.add('error');
|
|
if (statusIndicator) statusIndicator.classList.add('error');
|
|
if (statusText) statusText.textContent = 'Erreur';
|
|
break;
|
|
|
|
case 'ready':
|
|
default:
|
|
if (statusIndicator) statusIndicator.classList.add('ready');
|
|
if (statusText) statusText.textContent = 'Prêt';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Réinitialiser le statut de tous les tokens
|
|
function resetTokensStatus() {
|
|
tokens.forEach(token => {
|
|
updateTokenStatus(token, 'ready');
|
|
token.lastUsed = null;
|
|
token.cooldownUntil = null;
|
|
token.errorCount = 0;
|
|
});
|
|
}
|
|
|
|
// Nettoyer tous les timers
|
|
function clearAllTimers() {
|
|
Object.keys(tokenTimers).forEach(tokenId => {
|
|
clearTimeout(tokenTimers[tokenId]);
|
|
delete tokenTimers[tokenId];
|
|
});
|
|
}
|
|
|
|
// Obtenir une version tronquée d'une URL pour l'affichage
|
|
function getTruncatedUrl(url) {
|
|
if (url.length <= 60) return url;
|
|
|
|
const urlObj = new URL(url);
|
|
const domain = urlObj.hostname;
|
|
const path = urlObj.pathname;
|
|
|
|
const lastSegment = path.split('/').pop();
|
|
return `${domain}/.../${lastSegment}`;
|
|
}
|
|
|
|
// Générer une chaîne "il y a X minutes/heures" à partir d'une date
|
|
function getTimeAgoString(date) {
|
|
const now = new Date();
|
|
const diffMs = now - date;
|
|
const diffSeconds = Math.floor(diffMs / 1000);
|
|
|
|
if (diffSeconds < 60) {
|
|
return `il y a ${diffSeconds} seconde${diffSeconds > 1 ? 's' : ''}`;
|
|
}
|
|
|
|
const diffMinutes = Math.floor(diffSeconds / 60);
|
|
if (diffMinutes < 60) {
|
|
return `il y a ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`;
|
|
}
|
|
|
|
const diffHours = Math.floor(diffMinutes / 60);
|
|
if (diffHours < 24) {
|
|
return `il y a ${diffHours} heure${diffHours > 1 ? 's' : ''}`;
|
|
}
|
|
|
|
const diffDays = Math.floor(diffHours / 24);
|
|
return `il y a ${diffDays} jour${diffDays > 1 ? 's' : ''}`;
|
|
}
|
|
|
|
/**
|
|
* Réinitialise les données de migration
|
|
*/
|
|
function resetMigration() {
|
|
currentIndex = 0;
|
|
successCount = 0;
|
|
errorCount = 0;
|
|
skippedCount = 0;
|
|
warningCount = 0;
|
|
|
|
migration = {
|
|
status: 'not_started',
|
|
startTime: null,
|
|
lastUpdateTime: null,
|
|
progress: {
|
|
current: 0,
|
|
total: totalItems,
|
|
percentage: 0
|
|
},
|
|
stats: {
|
|
success: 0,
|
|
error: 0,
|
|
skipped: 0,
|
|
warning: 0
|
|
},
|
|
options: {
|
|
delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15),
|
|
tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150),
|
|
autoAdjust: autoAdjust ? autoAdjust.checked : true
|
|
}
|
|
};
|
|
|
|
// Mettre à jour le localStorage
|
|
updateLocalStorage();
|
|
|
|
// Mettre à jour l'interface
|
|
updateProgress(0);
|
|
updateStatistics();
|
|
}
|
|
|
|
/**
|
|
* Met à jour les données de migration dans localStorage
|
|
*/
|
|
function updateLocalStorage() {
|
|
migration.lastUpdateTime = Date.now();
|
|
|
|
migration.progress = {
|
|
current: currentIndex,
|
|
total: totalItems,
|
|
percentage: (currentIndex / totalItems) * 100
|
|
};
|
|
|
|
migration.stats = {
|
|
success: successCount,
|
|
error: errorCount,
|
|
skipped: skippedCount,
|
|
warning: warningCount
|
|
};
|
|
|
|
if (delayBetweenTokens && tokenCooldown && autoAdjust) {
|
|
migration.options = {
|
|
delayBetweenTokens: parseInt(delayBetweenTokens.value),
|
|
tokenCooldown: parseInt(tokenCooldown.value),
|
|
autoAdjust: autoAdjust.checked
|
|
};
|
|
}
|
|
|
|
// Sauvegarder dans localStorage
|
|
localStorage.setItem('favmastokey_multitoken_migration', JSON.stringify(migration));
|
|
}
|
|
|
|
/**
|
|
* Terminer la migration avec succès
|
|
*/
|
|
function finishMigration() {
|
|
isProcessing = false;
|
|
|
|
// Nettoyer les timers
|
|
clearAllTimers();
|
|
|
|
const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} échecs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`;
|
|
addLogEntry(summary, 'success');
|
|
|
|
startMigration.classList.remove('d-none');
|
|
startMigration.textContent = 'Terminer';
|
|
|
|
startMigration.addEventListener('click', function onceFinished() {
|
|
// Nettoyer localStorage
|
|
localStorage.removeItem('favmastokey_multitoken_migration');
|
|
|
|
// Ne pas supprimer les favoris pour permettre une nouvelle migration si nécessaire
|
|
// localStorage.removeItem('favmastokey_favorites');
|
|
|
|
// Réinitialiser l'interface
|
|
resetTokensStatus();
|
|
startMigration.textContent = 'Démarrer la migration';
|
|
|
|
// Retirer cet écouteur d'événement pour éviter les doublons
|
|
startMigration.removeEventListener('click', onceFinished);
|
|
|
|
// Scroll vers le haut
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
|
|
pauseMigration.classList.add('d-none');
|
|
|
|
// Mettre à jour la progression à 100%
|
|
updateProgress(100);
|
|
|
|
// Mettre à jour le statut de la migration
|
|
migration.status = 'completed';
|
|
updateLocalStorage();
|
|
}
|
|
|
|
/**
|
|
* Met à jour la barre de progression
|
|
*/
|
|
function updateProgress(forcedValue = null) {
|
|
const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100;
|
|
|
|
if (globalProgress) {
|
|
globalProgress.style.width = progress + '%';
|
|
globalProgress.textContent = Math.round(progress) + '%';
|
|
globalProgress.setAttribute('aria-valuenow', progress);
|
|
}
|
|
|
|
// Mettre à jour le localStorage
|
|
updateLocalStorage();
|
|
}
|
|
|
|
/**
|
|
* Met à jour les statistiques de progression
|
|
*/
|
|
function updateStatistics() {
|
|
// Calculer les pourcentages pour les barres de progression
|
|
const total = successCount + errorCount + skippedCount + warningCount;
|
|
const successPct = total > 0 ? (successCount / total) * 100 : 0;
|
|
const skipPct = total > 0 ? (skippedCount / total) * 100 : 0;
|
|
const warnPct = total > 0 ? (warningCount / total) * 100 : 0;
|
|
const errorPct = total > 0 ? (errorCount / total) * 100 : 0;
|
|
|
|
// Mettre à jour les barres
|
|
if (successProgress) {
|
|
successProgress.style.width = successPct + '%';
|
|
successProgress.setAttribute('aria-valuenow', successPct);
|
|
}
|
|
|
|
if (skipProgress) {
|
|
skipProgress.style.width = skipPct + '%';
|
|
skipProgress.setAttribute('aria-valuenow', skipPct);
|
|
}
|
|
|
|
if (warnProgress) {
|
|
warnProgress.style.width = warnPct + '%';
|
|
warnProgress.setAttribute('aria-valuenow', warnPct);
|
|
}
|
|
|
|
if (errorProgress) {
|
|
errorProgress.style.width = errorPct + '%';
|
|
errorProgress.setAttribute('aria-valuenow', errorPct);
|
|
}
|
|
|
|
// Mettre à jour le texte des statistiques
|
|
if (statsDisplay) {
|
|
statsDisplay.textContent = `${successCount} succès, ${skippedCount} ignorés, ${warningCount} avertissements, ${errorCount} erreurs`;
|
|
}
|
|
|
|
// Mettre à jour le localStorage
|
|
updateLocalStorage();
|
|
}
|
|
|
|
/**
|
|
* Ajoute une entrée dans le journal des opérations
|
|
*/
|
|
function addLogEntry(message, status = 'info') {
|
|
if (!operationLog) return;
|
|
|
|
const entry = document.createElement('div');
|
|
entry.className = `log-entry ${status}`;
|
|
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
entry.textContent = `[${timestamp}] ${message}`;
|
|
|
|
operationLog.appendChild(entry);
|
|
|
|
// Scroll vers le bas pour voir la dernière entrée
|
|
const logContainer = document.getElementById('log-container');
|
|
if (logContainer) {
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Initialiser la page au chargement
|
|
initOnLoad();
|
|
}); |