/** * FavMasToKey - Script JavaScript principal (version améliorée) */ // 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 step1 = document.getElementById('step1'); const step2 = document.getElementById('step2'); const step3 = document.getElementById('step3'); const fileSummary = document.getElementById('file-summary'); const backToStep1 = document.getElementById('back-to-step1'); 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 currentProgress = document.getElementById('current-progress'); const operationLog = document.getElementById('operation-log'); const slowModeCheckbox = document.getElementById('slow-mode'); const slowModeWarning = document.getElementById('slow-mode-warning'); const slowModeOptions = document.getElementById('slow-mode-options'); const slowModeDelay = document.getElementById('slow-mode-delay'); const delayValue = document.getElementById('delay-value'); // Éléments pour le mode tortue const tortoiseCheckbox = document.getElementById('tortoise-mode'); const tortoiseWarning = document.getElementById('tortoise-mode-warning'); const tortoiseOptions = document.getElementById('tortoise-mode-options'); const tortoiseDelay = document.getElementById('tortoise-mode-delay'); const tortoiseDelayValue = document.getElementById('tortoise-delay-value'); const autoPauseEnabled = document.getElementById('auto-pause-enabled'); // 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; let consecutiveRateLimitErrors = 0; let adaptiveDelayIncreases = 0; let autoRestartTimer = null; let rateLimitPauseActive = false; let migration = { status: 'not_started', // not_started, in_progress, paused, completed, error, auto_paused startTime: null, lastUpdateTime: null, progress: { current: 0, total: 0, percentage: 0 }, stats: { success: 0, error: 0, skipped: 0, warning: 0 }, options: { slowMode: false, delaySeconds: 30, tortoiseMode: false, tortoiseDelaySeconds: 120, autoPauseEnabled: true } }; // AMÉLIORATION 1: File d'attente pour les favoris en échec de rate-limit let rateLimitQueue = []; // AMÉLIORATION 3: Cache pour les résultats de recherche fédérée const federatedCache = { // Structure: { "mastodon_url": { id: "misskey_id", timestamp: Date.now() } } cache: {}, // Ajouter une entrée au cache add: function(mastodonUrl, misskeyId) { this.cache[mastodonUrl] = { id: misskeyId, timestamp: Date.now() }; this.saveToStorage(); }, // Récupérer une entrée du cache get: function(mastodonUrl) { if (this.cache[mastodonUrl]) { return this.cache[mastodonUrl].id; } return null; }, // Vérifier si une URL est dans le cache has: function(mastodonUrl) { return this.cache.hasOwnProperty(mastodonUrl); }, // Sauvegarder le cache dans localStorage saveToStorage: function() { try { localStorage.setItem('favmastokey_federated_cache', JSON.stringify(this.cache)); } catch (e) { // En cas d'erreur (ex: quota dépassé), nettoyer le cache ancien this.cleanup(180); // Nettoyer les entrées plus anciennes que 3 heures try { localStorage.setItem('favmastokey_federated_cache', JSON.stringify(this.cache)); } catch (e) { console.error("Impossible de sauvegarder le cache même après nettoyage", e); } } }, // Charger le cache depuis localStorage loadFromStorage: function() { const savedCache = localStorage.getItem('favmastokey_federated_cache'); if (savedCache) { try { this.cache = JSON.parse(savedCache); // Nettoyer les entrées trop anciennes au chargement this.cleanup(1440); // 24 heures } catch (e) { console.error("Erreur lors du chargement du cache", e); this.cache = {}; } } }, // Nettoyer les entrées trop anciennes (en minutes) cleanup: function(maxAgeMinutes) { const now = Date.now(); const maxAge = maxAgeMinutes * 60 * 1000; Object.keys(this.cache).forEach(url => { if (now - this.cache[url].timestamp > maxAge) { delete this.cache[url]; } }); } }; // AMÉLIORATION 4: Suivi des performances API const apiPerformance = { // Statistiques par domaine domainStats: {}, // Ajouter une mesure de temps de réponse addResponseTime: function(domain, responseTimeMs, success) { if (!this.domainStats[domain]) { this.domainStats[domain] = { requestCount: 0, successCount: 0, failureCount: 0, rateLimitCount: 0, totalResponseTime: 0, avgResponseTime: 0, lastRateLimitTime: null, recommendedDelay: null }; } const stats = this.domainStats[domain]; stats.requestCount++; stats.totalResponseTime += responseTimeMs; stats.avgResponseTime = stats.totalResponseTime / stats.requestCount; if (success === true) { stats.successCount++; } else if (success === false) { stats.failureCount++; } else if (success === 'rate_limit') { stats.rateLimitCount++; stats.lastRateLimitTime = Date.now(); // Calculer un délai recommandé basé sur les échecs de rate-limit // Plus de rate-limits = délai plus long const baseDelay = 30; // Délai de base en secondes stats.recommendedDelay = baseDelay + (Math.min(5, stats.rateLimitCount) * 30); } this.saveToStorage(); return stats; }, // Obtenir les statistiques pour un domaine getDomainStats: function(domain) { return this.domainStats[domain] || null; }, // Obtenir le délai recommandé pour un domaine getRecommendedDelay: function(domain) { const stats = this.getDomainStats(domain); if (stats && stats.recommendedDelay) { return stats.recommendedDelay; } return null; }, // Sauvegarder dans localStorage saveToStorage: function() { try { localStorage.setItem('favmastokey_api_performance', JSON.stringify(this.domainStats)); } catch (e) { console.error("Erreur lors de la sauvegarde des performances API", e); } }, // Charger depuis localStorage loadFromStorage: function() { const saved = localStorage.getItem('favmastokey_api_performance'); if (saved) { try { this.domainStats = JSON.parse(saved); } catch (e) { console.error("Erreur lors du chargement des performances API", e); this.domainStats = {}; } } } }; // AMÉLIORATION 5: Points de sauvegarde intermédiaires const saveCheckpoints = { // Intervalle entre les sauvegardes en millisecondes saveInterval: 5 * 60 * 1000, // 5 minutes par défaut lastSaveTime: null, timer: null, // Démarrer les sauvegardes automatiques start: function() { this.lastSaveTime = Date.now(); // Nettoyer l'ancien timer si existant if (this.timer) { clearInterval(this.timer); } // Configurer un nouveau timer this.timer = setInterval(() => { if (isProcessing && !isPaused) { this.createCheckpoint(); } }, this.saveInterval); }, // Arrêter les sauvegardes automatiques stop: function() { if (this.timer) { clearInterval(this.timer); this.timer = null; } }, // Créer un point de sauvegarde createCheckpoint: function() { if (!isProcessing) return; this.lastSaveTime = Date.now(); // Sauvegarder l'état actuel de la migration updateMigrationData(migration.status); // Sauvegarder également le cache de fédération et les performances API federatedCache.saveToStorage(); apiPerformance.saveToStorage(); // Sauvegarder la file d'attente rate-limit si non vide if (rateLimitQueue.length > 0) { try { localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue)); } catch (e) { console.error("Erreur lors de la sauvegarde de la file d'attente rate-limit", e); } } addLogEntry("Point de sauvegarde créé automatiquement", "info"); }, // Restaurer depuis un point de sauvegarde restore: function() { // La restauration principale est déjà gérée dans le code de démarrage // Restaurer la file d'attente rate-limit si existante const savedQueue = localStorage.getItem('favmastokey_ratelimit_queue'); if (savedQueue) { try { rateLimitQueue = JSON.parse(savedQueue); if (rateLimitQueue.length > 0) { addLogEntry(`Restauration de ${rateLimitQueue.length} favoris en attente après rate-limit`, "info"); } } catch (e) { console.error("Erreur lors de la restauration de la file d'attente rate-limit", e); rateLimitQueue = []; } } } }; console.log('Éléments DOM initialisés:', { tortoiseCheckbox: !!tortoiseCheckbox, tortoiseWarning: !!tortoiseWarning, tortoiseOptions: !!tortoiseOptions, slowModeCheckbox: !!slowModeCheckbox }); // Initialisation du cache et des statistiques au chargement federatedCache.loadFromStorage(); apiPerformance.loadFromStorage(); // Gérer l'affichage du message d'avertissement pour le mode lent if (slowModeCheckbox) { slowModeCheckbox.addEventListener('change', function() { if (this.checked) { console.log('Mode lent activé'); slowModeWarning.classList.remove('d-none'); if (slowModeOptions) { slowModeOptions.classList.remove('d-none'); } // Désactiver le mode tortue s'il est activé if (tortoiseCheckbox && tortoiseCheckbox.checked) { console.log('Désactivation du mode tortue (conflit)'); tortoiseCheckbox.checked = false; if (tortoiseWarning) { tortoiseWarning.classList.add('d-none'); } if (tortoiseOptions) { tortoiseOptions.classList.add('d-none'); } } } else { console.log('Mode lent désactivé'); slowModeWarning.classList.add('d-none'); if (slowModeOptions) { slowModeOptions.classList.add('d-none'); } } // Mettre à jour les options de migration migration.options.slowMode = this.checked; // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); savedMigration.options.slowMode = this.checked; localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } }); } // Gérer le curseur de délai pour le mode lent if (slowModeDelay && delayValue) { slowModeDelay.addEventListener('input', function() { delayValue.textContent = this.value; // Mettre à jour les options de migration migration.options.delaySeconds = parseInt(this.value); // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); if (savedMigration.options) { savedMigration.options.delaySeconds = parseInt(this.value); localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } } }); } // Gérer l'affichage du message d'avertissement pour le mode tortue if (tortoiseCheckbox) { tortoiseCheckbox.addEventListener('change', function() { console.log('Changement du mode tortue:', this.checked); if (this.checked) { // Désactiver le mode lent s'il est activé if (slowModeCheckbox && slowModeCheckbox.checked) { console.log('Désactivation du mode lent (conflit)'); slowModeCheckbox.checked = false; if (slowModeWarning) { slowModeWarning.classList.add('d-none'); } if (slowModeOptions) { slowModeOptions.classList.add('d-none'); } } // Activer le mode tortue if (tortoiseWarning) { tortoiseWarning.classList.remove('d-none'); } if (tortoiseOptions) { tortoiseOptions.classList.remove('d-none'); } console.log('Mode tortue activé avec succès'); } else { // Désactiver le mode tortue if (tortoiseWarning) { tortoiseWarning.classList.add('d-none'); } if (tortoiseOptions) { tortoiseOptions.classList.add('d-none'); } console.log('Mode tortue désactivé avec succès'); } // Mettre à jour les options de migration migration.options.tortoiseMode = this.checked; // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); savedMigration.options.tortoiseMode = this.checked; localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } }); } // Gérer le curseur de délai du mode tortue if (tortoiseDelay && tortoiseDelayValue) { tortoiseDelay.addEventListener('input', function() { tortoiseDelayValue.textContent = this.value; // Mettre à jour les options de migration migration.options.tortoiseDelaySeconds = parseInt(this.value); // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); if (savedMigration.options) { savedMigration.options.tortoiseDelaySeconds = parseInt(this.value); localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } } }); } // Gérer l'option de pause automatique if (autoPauseEnabled) { autoPauseEnabled.addEventListener('change', function() { // Mettre à jour les options de migration migration.options.autoPauseEnabled = this.checked; // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); if (savedMigration.options) { savedMigration.options.autoPauseEnabled = this.checked; localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } } }); } // 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.innerHTML = ` <strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon. `; // Passer à l'étape 2 step1.classList.add('d-none'); step2.classList.remove('d-none'); // 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: { slowMode: slowModeCheckbox ? slowModeCheckbox.checked : false, delaySeconds: slowModeDelay ? parseInt(slowModeDelay.value) : 30, tortoiseMode: tortoiseCheckbox ? tortoiseCheckbox.checked : false, tortoiseDelaySeconds: tortoiseDelay ? parseInt(tortoiseDelay.value) : 120, autoPauseEnabled: autoPauseEnabled ? autoPauseEnabled.checked : true } }; // Sauvegarder les données de migration localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); } 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); }); } // Gestion du retour à l'étape 1 if (backToStep1) { backToStep1.addEventListener('click', function() { step2.classList.add('d-none'); step1.classList.remove('d-none'); }); } // Vérifier si nous sommes à l'étape 3 (basé sur l'ancre dans l'URL) if (window.location.hash === '#step3' && document.getElementById('step3')) { // Récupérer les favoris du localStorage if (localStorage.getItem('favmastokey_favorites')) { favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); totalItems = favoritesList.length; // Montrer l'étape 3 step1.classList.add('d-none'); step2.classList.add('d-none'); step3.classList.remove('d-none'); // Récupérer les données de migration du localStorage if (localStorage.getItem('favmastokey_migration')) { migration = JSON.parse(localStorage.getItem('favmastokey_migration')); currentIndex = migration.progress.current; updateProgress(migration.progress.percentage); // Restaurer les statistiques successCount = migration.stats.success || 0; errorCount = migration.stats.error || 0; skippedCount = migration.stats.skipped || 0; warningCount = migration.stats.warning || 0; // Restaurer l'état du mode lent si disponible if (migration.options) { // Restaurer l'état du mode lent if (migration.options.slowMode !== undefined && slowModeCheckbox) { slowModeCheckbox.checked = migration.options.slowMode; // Mettre à jour l'affichage de l'avertissement if (slowModeWarning) { if (migration.options.slowMode) { slowModeWarning.classList.remove('d-none'); if (slowModeOptions) { slowModeOptions.classList.remove('d-none'); } } else { slowModeWarning.classList.add('d-none'); if (slowModeOptions) { slowModeOptions.classList.add('d-none'); } } } } // Restaurer la valeur du délai if (migration.options.delaySeconds && slowModeDelay && delayValue) { slowModeDelay.value = migration.options.delaySeconds; delayValue.textContent = migration.options.delaySeconds; } // Restaurer l'état du mode tortue if (migration.options.tortoiseMode !== undefined && tortoiseCheckbox) { tortoiseCheckbox.checked = migration.options.tortoiseMode; console.log('État du mode tortue restauré:', migration.options.tortoiseMode); // Mettre à jour l'affichage de l'avertissement if (tortoiseWarning) { if (migration.options.tortoiseMode) { tortoiseWarning.classList.remove('d-none'); if (tortoiseOptions) { tortoiseOptions.classList.remove('d-none'); } } else { tortoiseWarning.classList.add('d-none'); if (tortoiseOptions) { tortoiseOptions.classList.add('d-none'); } } } } // Restaurer la valeur du délai tortue if (migration.options.tortoiseDelaySeconds && tortoiseDelay && tortoiseDelayValue) { tortoiseDelay.value = migration.options.tortoiseDelaySeconds; tortoiseDelayValue.textContent = migration.options.tortoiseDelaySeconds; } // Restaurer l'état de pause automatique if (migration.options.autoPauseEnabled !== undefined && autoPauseEnabled) { autoPauseEnabled.checked = migration.options.autoPauseEnabled; } } // AMÉLIORATION 5: Restaurer depuis les points de sauvegarde saveCheckpoints.restore(); } } } /** * Met à jour les données de migration dans localStorage */ function updateMigrationData(status, progress = null) { migration.status = status; migration.lastUpdateTime = Date.now(); if (progress) { migration.progress = progress; } else { migration.progress = { current: currentIndex, total: totalItems, percentage: (currentIndex / totalItems) * 100 }; } migration.stats = { success: successCount, error: errorCount, skipped: skippedCount, warning: warningCount }; migration.options = { slowMode: slowModeCheckbox ? slowModeCheckbox.checked : false, delaySeconds: slowModeDelay ? parseInt(slowModeDelay.value) : 30, tortoiseMode: tortoiseCheckbox ? tortoiseCheckbox.checked : false, tortoiseDelaySeconds: tortoiseDelay ? parseInt(tortoiseDelay.value) : 120, autoPauseEnabled: autoPauseEnabled ? autoPauseEnabled.checked : true }; // Sauvegarder dans localStorage localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); } /** * Réinitialise les données de migration */ function resetMigration() { currentIndex = 0; successCount = 0; errorCount = 0; skippedCount = 0; warningCount = 0; consecutiveRateLimitErrors = 0; adaptiveDelayIncreases = 0; // Réinitialiser la file d'attente rate-limit rateLimitQueue = []; localStorage.removeItem('favmastokey_ratelimit_queue'); 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: { slowMode: slowModeCheckbox ? slowModeCheckbox.checked : false, delaySeconds: slowModeDelay ? parseInt(slowModeDelay.value) : 30, tortoiseMode: tortoiseCheckbox ? tortoiseCheckbox.checked : false, tortoiseDelaySeconds: tortoiseDelay ? parseInt(tortoiseDelay.value) : 120, autoPauseEnabled: autoPauseEnabled ? autoPauseEnabled.checked : true } }; localStorage.setItem('favmastokey_migration', JSON.stringify(migration)); updateProgress(0); } /** * Augmente le délai en cas de détection de rate limit */ function increaseAdaptiveDelay() { if (adaptiveDelayIncreases < 3) { if (tortoiseCheckbox && tortoiseCheckbox.checked && tortoiseDelay && tortoiseDelayValue) { // Augmenter le délai du mode tortue const currentDelay = parseInt(tortoiseDelay.value); const newDelay = Math.min(300, currentDelay + 30); // Augmenter de 30 secondes jusqu'à 5 min max tortoiseDelay.value = newDelay; tortoiseDelayValue.textContent = newDelay; // Mettre à jour les options de migration migration.options.tortoiseDelaySeconds = newDelay; // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); savedMigration.options.tortoiseDelaySeconds = newDelay; localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } adaptiveDelayIncreases++; addLogEntry(`Délai du mode tortue automatiquement augmenté à ${newDelay} secondes pour éviter les limitations d'API.`, 'warning'); return true; } else if (slowModeCheckbox && slowModeCheckbox.checked && slowModeDelay && delayValue) { // Augmenter le délai du mode lent const currentDelay = parseInt(slowModeDelay.value); const newDelay = Math.min(300, currentDelay + 30); // Augmenter de 30 secondes jusqu'à 5 min max slowModeDelay.value = newDelay; delayValue.textContent = newDelay; // Mettre à jour les options de migration migration.options.delaySeconds = newDelay; // Sauvegarder dans localStorage if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); savedMigration.options.delaySeconds = newDelay; localStorage.setItem('favmastokey_migration', JSON.stringify(savedMigration)); } adaptiveDelayIncreases++; addLogEntry(`Délai du mode lent automatiquement augmenté à ${newDelay} secondes pour éviter les limitations d'API.`, 'warning'); return true; } } return false; } /** * Déclenche une pause automatique due aux limites d'API */ function triggerRateLimitPause() { if (rateLimitPauseActive) return; // Éviter les pauses multiples // Vérifier si la pause automatique est activée if (autoPauseEnabled && !autoPauseEnabled.checked) { addLogEntry("Pause automatique désactivée. Considérez activer cette option ou augmenter le délai.", 'warning'); return; } rateLimitPauseActive = true; isPaused = true; // Durée de pause en minutes (15 minutes) const pauseDurationMinutes = 15; const pauseDurationMs = pauseDurationMinutes * 60 * 1000; addLogEntry(`Trop de limitations d'API détectées! Pause automatique de ${pauseDurationMinutes} minutes pour permettre la réinitialisation des limites.`, 'error'); if (pauseMigration) { pauseMigration.textContent = 'Reprise automatique en attente...'; pauseMigration.disabled = true; } // Mise à jour du statut updateMigrationData('auto_paused'); // Programmer la reprise automatique autoRestartTimer = setTimeout(() => { if (isProcessing && isPaused) { isPaused = false; rateLimitPauseActive = false; if (pauseMigration) { pauseMigration.textContent = 'Pause'; pauseMigration.disabled = false; } addLogEntry(`Reprise automatique après pause de ${pauseDurationMinutes} minutes.`, 'info'); updateMigrationData('in_progress'); // Redémarrer avec un délai réduit pour tester si les limites sont réinitialisées if (tortoiseCheckbox && tortoiseCheckbox.checked && tortoiseDelay && tortoiseDelayValue) { const currentDelay = parseInt(tortoiseDelay.value); // Réduire le délai mais pas en dessous de 60 secondes const newDelay = Math.max(60, currentDelay - 60); tortoiseDelay.value = newDelay; tortoiseDelayValue.textContent = newDelay; migration.options.tortoiseDelaySeconds = newDelay; } else if (slowModeCheckbox && slowModeCheckbox.checked && slowModeDelay && delayValue) { const currentDelay = parseInt(slowModeDelay.value); // Réduire le délai mais pas en dessous de 30 secondes const newDelay = Math.max(30, currentDelay - 10); slowModeDelay.value = newDelay; delayValue.textContent = newDelay; migration.options.delaySeconds = newDelay; } processBatch(); } }, pauseDurationMs); // Afficher un compte à rebours let remainingMinutes = pauseDurationMinutes; const countdownInterval = setInterval(() => { remainingMinutes--; if (remainingMinutes <= 0) { clearInterval(countdownInterval); return; } if (pauseMigration) { pauseMigration.textContent = `Reprise dans ${remainingMinutes}m...`; } }, 60000); // Mise à jour toutes les minutes } // Gérer le processus de migration if (startMigration) { startMigration.addEventListener('click', function() { if (isProcessing) return; // Récupérer les favoris et les données de migration depuis localStorage si nécessaire if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) { favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); totalItems = favoritesList.length; } // Vérifier s'il y a une migration en cours à reprendre if (localStorage.getItem('favmastokey_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_migration')); // Si la migration était en cours ou en pause, proposer de la reprendre if (savedMigration.status === 'in_progress' || savedMigration.status === 'paused' || savedMigration.status === 'auto_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; // Mettre à jour l'interface avec les données sauvegardées updateProgress(migration.progress.percentage); // Restaurer les statistiques successCount = migration.stats.success || 0; errorCount = migration.stats.error || 0; skippedCount = migration.stats.skipped || 0; warningCount = migration.stats.warning || 0; // Restaurer les options if (migration.options) { // Restaurer l'état du mode lent if (migration.options.slowMode !== undefined && slowModeCheckbox) { slowModeCheckbox.checked = migration.options.slowMode; // Mettre à jour l'affichage de l'avertissement if (slowModeWarning) { if (migration.options.slowMode) { slowModeWarning.classList.remove('d-none'); if (slowModeOptions) { slowModeOptions.classList.remove('d-none'); } } else { slowModeWarning.classList.add('d-none'); if (slowModeOptions) { slowModeOptions.classList.add('d-none'); } } } } // Restaurer la valeur du délai if (migration.options.delaySeconds && slowModeDelay && delayValue) { slowModeDelay.value = migration.options.delaySeconds; delayValue.textContent = migration.options.delaySeconds; } // Restaurer l'état du mode tortue if (migration.options.tortoiseMode !== undefined && tortoiseCheckbox) { tortoiseCheckbox.checked = migration.options.tortoiseMode; console.log('État du mode tortue restauré:', migration.options.tortoiseMode); // Mettre à jour l'affichage de l'avertissement if (tortoiseWarning) { if (migration.options.tortoiseMode) { tortoiseWarning.classList.remove('d-none'); if (tortoiseOptions) { tortoiseOptions.classList.remove('d-none'); } } else { tortoiseWarning.classList.add('d-none'); if (tortoiseOptions) { tortoiseOptions.classList.add('d-none'); } } } } // Restaurer la valeur du délai tortue if (migration.options.tortoiseDelaySeconds && tortoiseDelay && tortoiseDelayValue) { tortoiseDelay.value = migration.options.tortoiseDelaySeconds; tortoiseDelayValue.textContent = migration.options.tortoiseDelaySeconds; } // Restaurer l'état de pause automatique if (migration.options.autoPauseEnabled !== undefined && autoPauseEnabled) { autoPauseEnabled.checked = migration.options.autoPauseEnabled; } } // AMÉLIORATION 5: Restaurer depuis les points de sauvegarde saveCheckpoints.restore(); // Afficher un résumé addLogEntry(`Reprise de la migration: ${successCount} réussis, ${errorCount} échecs, ${skippedCount} ignorés, ${warningCount} avertissements.`, 'info'); } else { // Réinitialiser la migration resetMigration(); } } else { // Réinitialiser la migration resetMigration(); } } else { // Initialiser une nouvelle migration resetMigration(); } if (favoritesList.length === 0) { addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error'); return; } // Démarrer la migration isProcessing = true; isPaused = false; rateLimitPauseActive = false; // Annuler le timer de reprise automatique si en cours if (autoRestartTimer) { clearTimeout(autoRestartTimer); autoRestartTimer = null; } startMigration.classList.add('d-none'); pauseMigration.classList.remove('d-none'); pauseMigration.textContent = 'Pause'; pauseMigration.disabled = false; // Initialiser le temps de démarrage si c'est une nouvelle migration if (migration.status === 'not_started' || migration.startTime === null) { migration.startTime = Date.now(); } // Démarrer les sauvegardes automatiques saveCheckpoints.start(); // Mettre à jour le statut de la migration updateMigrationData('in_progress'); // Mettre à jour les options en fonction des dernières valeurs des cases à cocher let isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked; let isSlowMode = slowModeCheckbox && slowModeCheckbox.checked; // S'assurer que les deux modes ne sont pas activés en même temps if (isTortoiseMode && isSlowMode) { console.log("CONFLIT DE MODES: les deux modes sont activés, désactivation du mode lent"); if (slowModeCheckbox) { slowModeCheckbox.checked = false; isSlowMode = false; if (slowModeWarning) { slowModeWarning.classList.add('d-none'); } if (slowModeOptions) { slowModeOptions.classList.add('d-none'); } } } // Ajouter une entrée pour le mode utilisé let modeName = 'Mode normal'; let delayValue = 3; if (isTortoiseMode) { delayValue = tortoiseDelay ? parseInt(tortoiseDelay.value) : 120; modeName = `Mode tortue activé (${delayValue}s de délai)`; console.log('Mode tortue détecté pour la migration'); } else if (isSlowMode) { delayValue = slowModeDelay ? parseInt(slowModeDelay.value) : 30; modeName = `Mode ultra-lent activé (${delayValue}s de délai)`; console.log('Mode lent détecté pour la migration'); } else { console.log('Mode normal détecté pour la migration'); } addLogEntry(`Démarrage de la migration... (${modeName})`, 'info'); // Lancer le processus de migration processNextBatch(); }); } // Gérer la pause de la migration if (pauseMigration) { pauseMigration.addEventListener('click', function() { if (!isProcessing || pauseMigration.disabled) return; isPaused = !isPaused; if (isPaused) { pauseMigration.textContent = 'Reprendre'; addLogEntry('Migration en pause.', 'warning'); currentProgress.classList.remove('active'); // Mettre à jour le statut de la migration updateMigrationData('paused'); // Arrêter les sauvegardes automatiques saveCheckpoints.stop(); } else { pauseMigration.textContent = 'Pause'; addLogEntry('Reprise de la migration...', 'info'); currentProgress.classList.add('active'); // Mettre à jour le statut de la migration updateMigrationData('in_progress'); // Redémarrer les sauvegardes automatiques saveCheckpoints.start(); // Reprendre le traitement processNextBatch(); } }); } // Gérer l'annulation de la migration if (cancelMigration) { cancelMigration.addEventListener('click', function() { if (!isProcessing && currentIndex === 0) { // Retour à l'étape 1 si rien n'a commencé step3.classList.add('d-none'); step1.classList.remove('d-none'); return; } const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?'); if (confirmCancel) { isProcessing = false; isPaused = false; rateLimitPauseActive = false; // Arrêter les sauvegardes automatiques saveCheckpoints.stop(); // Annuler le timer de reprise automatique si en cours if (autoRestartTimer) { clearTimeout(autoRestartTimer); autoRestartTimer = null; } addLogEntry('Migration annulée.', 'error'); // Réinitialiser l'interface startMigration.classList.remove('d-none'); pauseMigration.classList.add('d-none'); pauseMigration.textContent = 'Pause'; pauseMigration.disabled = false; currentProgress.classList.remove('active'); // Réinitialiser les données de migration resetMigration(); } }); } /** * Point d'entrée principal du processus de traitement des favoris * Gère à la fois les favoris normaux et ceux en file d'attente après rate-limit */ function processNextBatch() { if (!isProcessing || isPaused) { return; } // AMÉLIORATION 1: Vérifier d'abord la file d'attente des favoris en rate-limit if (rateLimitQueue.length > 0) { addLogEntry(`Traitement des favoris en attente après rate-limit (${rateLimitQueue.length} restants)`, 'info'); const urlToRetry = rateLimitQueue.shift(); // Enregistrer l'état de la file d'attente dans localStorage if (rateLimitQueue.length > 0) { localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue)); } else { localStorage.removeItem('favmastokey_ratelimit_queue'); } // Traiter cette URL spécifique processBatch([urlToRetry], true); return; } // Vérifier si nous avons terminé if (currentIndex >= totalItems) { // Migration terminée finishMigration(); return; } // Vérification des modes et débogage let isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked; let isSlowMode = slowModeCheckbox && slowModeCheckbox.checked; // Nombre d'éléments à traiter dans ce lot const batchSize = (isTortoiseMode || isSlowMode) ? 1 : 2; const endIndex = Math.min(currentIndex + batchSize, totalItems); // Préparer les éléments du lot const batch = favoritesList.slice(currentIndex, endIndex); // Traiter le lot normal processBatch(batch, false); } /** * Traite un lot de favoris * @param {Array} batch - URLs à traiter * @param {boolean} isRetry - Indique si c'est une tentative après rate-limit */ function processBatch(batch, isRetry = false) { if (!isProcessing || isPaused) { return; } // Mettre à jour la progression actuelle currentProgress.classList.add('active'); updateProgress(); // Ajouter une entrée dans le journal si ce n'est pas une tentative après rate-limit if (!isRetry) { const endIndex = Math.min(currentIndex + batch.length, totalItems); addLogEntry(`Traitement du lot ${currentIndex + 1} à ${endIndex}...`, 'info'); } // Compteur de tentatives pour cette requête let retryAttempt = 0; const maxRetries = 3; // Variables pour mesurer les performances const requestStartTime = Date.now(); // Fonction pour envoyer la requête avec retry automatique function sendRequest() { // Déterminer le délai à utiliser - Assurez-vous que les valeurs sont à jour let isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked; let isSlowMode = slowModeCheckbox && slowModeCheckbox.checked; let delaySeconds = 3; if (isTortoiseMode && tortoiseDelay) { delaySeconds = parseInt(tortoiseDelay.value); } else if (isSlowMode && slowModeDelay) { delaySeconds = parseInt(slowModeDelay.value); } // AMÉLIORATION 3: Vérifier le cache pour les URLs du lot // Préparer les données avec le cache quand disponible const batchData = batch.map(url => { if (federatedCache.has(url)) { return { url: url, cached: true, cachedId: federatedCache.get(url) }; } return { url: url, cached: false }; }); // Journaliser les URLs mises en cache const cachedCount = batchData.filter(item => item.cached).length; if (cachedCount > 0) { console.log(`${cachedCount}/${batchData.length} URLs trouvées dans le cache de fédération`, batchData); // Ajouter une entrée dans le journal si toutes les URLs sont cachées if (cachedCount === batchData.length) { addLogEntry(`Traitement accéléré: tous les IDs déjà connus pour ce lot`, 'info'); } } console.log('Envoi de la requête avec paramètres:', { tortoiseMode: isTortoiseMode, slowMode: isSlowMode, delaySeconds: delaySeconds, batchWithCache: batchData }); fetch('process.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ batch: batch, batchData: batchData, // Inclure les données du cache currentIndex: currentIndex, totalItems: totalItems, slowMode: isSlowMode, tortoiseMode: isTortoiseMode, delaySeconds: delaySeconds, useCachedIds: true // Indiquer au serveur d'utiliser les IDs en cache quand disponibles }), // Augmenter le timeout pour éviter les erreurs de limite de temps timeout: 60000 }) .then(response => { // Calculer le temps de réponse const responseTime = Date.now() - requestStartTime; // Vérifier si la réponse est au format JSON const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { // Si ce n'est pas du JSON, récupérer le texte pour déboguer return response.text().then(text => { throw new Error(`Réponse non-JSON reçue: ${text.substring(0, 100)}${text.length > 100 ? '...' : ''}`); }); } return response.json().then(data => ({ data, responseTime })); }) .then(({ data, responseTime }) => { if (data.success) { // Traiter les résultats let hasRateLimitError = false; let allAlreadyFavorited = true; // Pour AMÉLIORATION 2 if (data.results && data.results.length) { data.results.forEach((result, index) => { const url = batch[index]; // Construction du message avec détails si disponibles let message = result.message; if (result.details) { message += ` (${result.details})`; } addLogEntry(message, result.status); // AMÉLIORATION 3: Mettre à jour le cache avec les résultats réussis if (result.status === 'success' && result.misskey_id) { federatedCache.add(url, result.misskey_id); } // AMÉLIORATION 4: Mettre à jour les statistiques de performance API const domain = extractDomain(url); let apiSuccess = result.status === 'success' ? true : (result.status === 'error' || result.status === 'warning') ? false : null; if (result.error_type === 'rate_limit') { apiSuccess = 'rate_limit'; } if (domain) { apiPerformance.addResponseTime(domain, responseTime, apiSuccess); } // AMÉLIORATION 2: Vérifier si c'est déjà favoris pour accélérer le traitement if (result.status !== 'info' || result.error_type !== 'already_favorited') { allAlreadyFavorited = false; } // Mettre à jour les compteurs if (result.status === 'success') { successCount++; consecutiveRateLimitErrors = 0; // Réinitialiser le compteur en cas de succès } else if (result.status === 'error') { errorCount++; } else if (result.status === 'info') { skippedCount++; } else if (result.status === 'warning') { warningCount++; // AMÉLIORATION 1: Gérer les erreurs de rate limit if (result.error_type === 'rate_limit') { hasRateLimitError = true; // Ajouter à la file d'attente pour réessayer plus tard if (!rateLimitQueue.includes(url)) { rateLimitQueue.push(url); addLogEntry(`URL ajoutée à la file d'attente pour réessai après rate-limit: ${url}`, 'info'); // Sauvegarder la file d'attente localStorage.setItem('favmastokey_ratelimit_queue', JSON.stringify(rateLimitQueue)); } } } }); } // Mettre à jour l'index si ce n'est pas un réessai après rate-limit if (!isRetry) { currentIndex = Math.min(currentIndex + batch.length, totalItems); } // Mettre à jour la progression updateProgress(data.progress.percentage); // Mettre à jour les données de migration updateMigrationData('in_progress', data.progress); // Si nous avons détecté une erreur de rate limit if (hasRateLimitError) { consecutiveRateLimitErrors++; if (consecutiveRateLimitErrors >= 3) { // Trop d'erreurs consécutives, déclencher une pause longue triggerRateLimitPause(); return; } else if (consecutiveRateLimitErrors >= 2) { // Si 2 erreurs consécutives, augmenter le délai automatiquement // AMÉLIORATION 4: Utiliser les statistiques pour ajuster le délai adjustDelayBasedOnPerformance(); // Traiter le lot suivant après un délai plus long setTimeout(processNextBatch, 30000); // 30 secondes de pause supplémentaire return; } } else { // Réduire le compteur de rate limit si la requête s'est bien passée consecutiveRateLimitErrors = Math.max(0, consecutiveRateLimitErrors - 1); } // Si le serveur suggère d'utiliser un mode plus lent if (data.suggestions) { if (data.suggestions.use_tortoise_mode && !isTortoiseMode) { addLogEntry("L'API semble limiter les requêtes. Il est recommandé d'activer le 'Mode tortue'.", 'warning'); } else if (data.suggestions.use_slow_mode && !isSlowMode && !isTortoiseMode) { addLogEntry("L'API semble limiter les requêtes. Il est recommandé d'activer le 'Mode ultra-lent'.", 'warning'); } else if (data.suggestions.increase_delay) { if (isTortoiseMode) { addLogEntry("Considérez augmenter le délai du mode tortue pour réduire les limitations d'API.", 'warning'); } else if (isSlowMode) { addLogEntry("Considérez augmenter le délai du mode ultra-lent pour réduire les limitations d'API.", 'warning'); } } } // AMÉLIORATION 2: Accélérer quand tous les favoris sont déjà traités if (allAlreadyFavorited) { // Pas besoin d'attendre le délai complet pour les notes déjà dans les favoris addLogEntry(`Tous les favoris de ce lot sont déjà présents, traitement immédiat du lot suivant`, 'info'); processNextBatch(); return; } // Traiter le lot suivant après un délai adapté au mode let nextBatchDelay = 3000; // Délai par défaut // Mettre à jour les états des modes isTortoiseMode = tortoiseCheckbox && tortoiseCheckbox.checked; isSlowMode = slowModeCheckbox && slowModeCheckbox.checked; if (isTortoiseMode) { // Utiliser le délai du mode tortue nextBatchDelay = delaySeconds * 1000; console.log('Utilisation du délai tortue:', nextBatchDelay); } else if (isSlowMode) { // Utiliser le délai du mode ultra-lent nextBatchDelay = delaySeconds * 1000; console.log('Utilisation du délai lent:', nextBatchDelay); } else { console.log('Utilisation du délai normal:', nextBatchDelay); } setTimeout(processNextBatch, nextBatchDelay); } else { // Gérer l'erreur addLogEntry('Erreur: ' + data.message, 'error'); // Pause en cas d'erreur isPaused = true; pauseMigration.textContent = 'Reprendre'; currentProgress.classList.remove('active'); // Mettre à jour le statut de la migration updateMigrationData('error'); } }) .catch(error => { // Afficher l'erreur détaillée dans la console pour le débogage console.error('Erreur complète:', error); // Ajouter l'erreur au journal addLogEntry('Erreur de connexion: ' + error.message, 'error'); // Retenter si nous n'avons pas atteint le nombre maximum de tentatives if (retryAttempt < maxRetries) { retryAttempt++; const waitTime = Math.pow(2, retryAttempt) * 1000; // Backoff exponentiel addLogEntry(`Nouvelle tentative (${retryAttempt}/${maxRetries}) dans ${waitTime/1000} secondes...`, 'warning'); setTimeout(sendRequest, waitTime); } else { // Pause après plusieurs échecs isPaused = true; pauseMigration.textContent = 'Reprendre'; currentProgress.classList.remove('active'); // Mettre à jour le statut de la migration updateMigrationData('error'); addLogEntry(`Échec après ${maxRetries} tentatives. Veuillez reprendre manuellement.`, 'error'); } }); } // Lancer la requête sendRequest(); } /** * Terminer la migration avec succès */ function finishMigration() { isProcessing = false; // Arrêter les sauvegardes automatiques saveCheckpoints.stop(); const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} échecs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`; addLogEntry(summary, 'success'); // Si des URLs sont encore dans la file d'attente rate-limit, avertir l'utilisateur if (rateLimitQueue.length > 0) { addLogEntry(`ATTENTION: ${rateLimitQueue.length} favoris n'ont pas pu être traités à cause de limitations d'API. Vous pouvez redémarrer la migration plus tard pour réessayer.`, 'warning'); } startMigration.classList.remove('d-none'); startMigration.textContent = 'Terminer'; startMigration.addEventListener('click', function() { // Nettoyer localStorage et retourner à l'étape 1 localStorage.removeItem('favmastokey_favorites'); localStorage.removeItem('favmastokey_migration'); localStorage.removeItem('favmastokey_ratelimit_queue'); // Conserver le cache de fédération et les performances API pour accélérer les futures migrations federatedCache.cleanup(1440); // Nettoyer les entrées plus vieilles de 24h federatedCache.saveToStorage(); step3.classList.add('d-none'); step1.classList.remove('d-none'); }); pauseMigration.classList.add('d-none'); // Mettre à jour la progression à 100% updateProgress(100); // Mettre à jour le statut de la migration updateMigrationData('completed'); } /** * Ajuste le délai en fonction des performances observées * AMÉLIORATION 4: Utiliser les statistiques pour ajuster le délai de manière intelligente */ function adjustDelayBasedOnPerformance() { // Identifier les domaines principaux const domains = []; // Explorer les URLs récentes pour identifier les domaines const startIndex = Math.max(0, currentIndex - 10); const recentUrls = favoritesList.slice(startIndex, currentIndex); recentUrls.forEach(url => { const domain = extractDomain(url); if (domain && !domains.includes(domain)) { domains.push(domain); } }); // Trouver le domaine le plus problématique let worstDomain = null; let maxRateLimits = 0; domains.forEach(domain => { const stats = apiPerformance.getDomainStats(domain); if (stats && stats.rateLimitCount > maxRateLimits) { maxRateLimits = stats.rateLimitCount; worstDomain = domain; } }); if (worstDomain) { const stats = apiPerformance.getDomainStats(worstDomain); const recommendedDelay = stats.recommendedDelay; if (recommendedDelay) { if (tortoiseCheckbox && tortoiseCheckbox.checked && tortoiseDelay && tortoiseDelayValue) { // Vérifier si le délai recommandé est supérieur au délai actuel const currentDelay = parseInt(tortoiseDelay.value); if (recommendedDelay > currentDelay) { const newDelay = Math.min(300, recommendedDelay); tortoiseDelay.value = newDelay; tortoiseDelayValue.textContent = newDelay; migration.options.tortoiseDelaySeconds = newDelay; addLogEntry(`Délai du mode tortue ajusté à ${newDelay}s basé sur les performances avec ${worstDomain}`, 'warning'); } } else if (slowModeCheckbox && slowModeCheckbox.checked && slowModeDelay && delayValue) { // Vérifier si le délai recommandé est supérieur au délai actuel const currentDelay = parseInt(slowModeDelay.value); if (recommendedDelay > currentDelay) { const newDelay = Math.min(300, recommendedDelay); slowModeDelay.value = newDelay; delayValue.textContent = newDelay; migration.options.delaySeconds = newDelay; addLogEntry(`Délai du mode lent ajusté à ${newDelay}s basé sur les performances avec ${worstDomain}`, 'warning'); } } else { // Suggérer d'activer le mode lent avec le délai recommandé addLogEntry(`Activez le mode lent avec un délai d'au moins ${recommendedDelay}s pour éviter les limitations avec ${worstDomain}`, 'warning'); } } } else { // Méthode de secours si aucun domaine problématique identifié increaseAdaptiveDelay(); } } /** * Extrait le nom de domaine d'une URL */ function extractDomain(url) { try { const parsedUrl = new URL(url); return parsedUrl.hostname; } catch (e) { console.error("URL invalide:", url); return null; } } /** * Met à jour la barre de progression */ function updateProgress(forcedValue = null) { const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100; globalProgress.style.width = progress + '%'; globalProgress.textContent = Math.round(progress) + '%'; globalProgress.setAttribute('aria-valuenow', progress); if (forcedValue === null) { // Mettre à jour la progression actuelle const batchProgress = ((currentIndex % 5) / 5) * 100; currentProgress.style.width = batchProgress + '%'; currentProgress.setAttribute('aria-valuenow', batchProgress); } else { currentProgress.style.width = '100%'; currentProgress.setAttribute('aria-valuenow', 100); } } /** * Ajoute une entrée dans le journal des opérations */ function addLogEntry(message, status = 'info') { 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'); logContainer.scrollTop = logContainer.scrollHeight; } });