Compare commits
2 Commits
Author | SHA1 | Date | |
---|---|---|---|
3d5675c7a1 | |||
e39e76fd06 |
@ -173,10 +173,26 @@ $process_status = test_url($process_url);
|
||||
if (isset($_SESSION['misskey_token'])) {
|
||||
$token = $_SESSION['misskey_token'];
|
||||
$masked_token = substr($token, 0, 4) . '...' . substr($token, -4);
|
||||
echo '<code>' . htmlspecialchars($masked_token) . '</code>';
|
||||
echo '<code>' . htmlspecialchars($masked_token) . '</code> (principal)';
|
||||
} else {
|
||||
echo '<span class="text-muted">Non défini</span>';
|
||||
}
|
||||
|
||||
// Ajouter les tokens supplémentaires du mode filou
|
||||
if (isset($_SESSION['additional_tokens']) && !empty($_SESSION['additional_tokens'])) {
|
||||
echo '<br><strong>Tokens supplémentaires (mode filou):</strong><ul>';
|
||||
|
||||
foreach ($_SESSION['additional_tokens'] as $token_id => $token_data) {
|
||||
$token = $token_data['token'];
|
||||
$name = isset($token_data['name']) ? $token_data['name'] : 'Token sans nom';
|
||||
$masked_token = substr($token, 0, 4) . '...' . substr($token, -4);
|
||||
|
||||
echo '<li><strong>' . htmlspecialchars($name) . ':</strong> <code>' .
|
||||
htmlspecialchars($masked_token) . '</code></li>';
|
||||
}
|
||||
|
||||
echo '</ul>';
|
||||
}
|
||||
?>
|
||||
</dd>
|
||||
|
||||
@ -278,52 +294,6 @@ $process_status = test_url($process_url);
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Afficher les informations de localStorage
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const localStorageInfo = document.getElementById('local-storage-info');
|
||||
const clearLocalStorageBtn = document.getElementById('clear-localstorage');
|
||||
|
||||
function updateLocalStorageInfo() {
|
||||
let html = '<ul class="list-unstyled mb-0">';
|
||||
|
||||
if (localStorage.getItem('favmastokey_favorites')) {
|
||||
const favorites = JSON.parse(localStorage.getItem('favmastokey_favorites'));
|
||||
html += '<li><strong>Favoris stockés:</strong> ' + favorites.length + ' URLs</li>';
|
||||
} else {
|
||||
html += '<li><strong>Favoris stockés:</strong> <span class="text-muted">Aucun</span></li>';
|
||||
}
|
||||
|
||||
if (localStorage.getItem('favmastokey_migration')) {
|
||||
const migration = JSON.parse(localStorage.getItem('favmastokey_migration'));
|
||||
html += '<li><strong>État de la migration:</strong> ' + migration.status + '</li>';
|
||||
html += '<li><strong>Progression:</strong> ' + migration.progress.current + '/' + migration.progress.total + ' (' + migration.progress.percentage.toFixed(1) + '%)</li>';
|
||||
|
||||
if (migration.lastUpdateTime) {
|
||||
const date = new Date(migration.lastUpdateTime);
|
||||
html += '<li><strong>Dernière mise à jour:</strong> ' + date.toLocaleString() + '</li>';
|
||||
}
|
||||
} else {
|
||||
html += '<li><strong>État de la migration:</strong> <span class="text-muted">Aucune migration en cours</span></li>';
|
||||
}
|
||||
|
||||
html += '</ul>';
|
||||
localStorageInfo.innerHTML = html;
|
||||
}
|
||||
|
||||
// Mettre à jour au chargement
|
||||
updateLocalStorageInfo();
|
||||
|
||||
// Gérer le bouton d'effacement
|
||||
clearLocalStorageBtn.addEventListener('click', function() {
|
||||
if (confirm('Êtes-vous sûr de vouloir effacer toutes les données de migration stockées localement ?')) {
|
||||
localStorage.removeItem('favmastokey_favorites');
|
||||
localStorage.removeItem('favmastokey_migration');
|
||||
updateLocalStorageInfo();
|
||||
alert('Données localStorage effacées avec succès.');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script src="js/diagnostic.js"></script>
|
||||
</body>
|
||||
</html>
|
@ -29,7 +29,7 @@ $config = [
|
||||
// Informations de l'application
|
||||
'app_name' => 'FavMasToKey',
|
||||
'app_description' => 'Outil de transfert des favoris de Mastodon vers Misskey',
|
||||
'app_version' => '0.5.1', // Mise à jour de la version pour les améliorations
|
||||
'app_version' => '0.6.1', // Mise à jour de la version pour les améliorations
|
||||
|
||||
// URL de base - Utilisée pour les liens dans l'application
|
||||
'app_url' => 'https://concepts.esenjin.xyz/favmastokey', // Remplacez par l'URL exacte de votre application
|
||||
|
@ -117,7 +117,7 @@ if (isset($_SESSION['messages'])) {
|
||||
<header class="text-center mb-5">
|
||||
<h1>FavMasToKey</h1>
|
||||
<p class="lead">Transférez vos favoris Mastodon vers Misskey en quelques clics</p>
|
||||
<p><a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a></p>
|
||||
<p><a href="diagnostic.php" class="btn btn-sm btn-outline-primary">Diagnostic</a> <a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a> <a href="multitokens.php" class="btn btn-sm btn-outline-primary">Mode Filou</a></p>
|
||||
</header>
|
||||
|
||||
<!-- Messages d'alerte -->
|
||||
|
109
js/diagnostic.js
Normal file
109
js/diagnostic.js
Normal file
@ -0,0 +1,109 @@
|
||||
// Gestion du localStorage pour la page de diagnostic
|
||||
function initLocalStorageDiagnostic() {
|
||||
const localStorageInfo = document.getElementById('local-storage-info');
|
||||
const clearLocalStorageBtn = document.getElementById('clear-localstorage');
|
||||
|
||||
function updateLocalStorageInfo() {
|
||||
let html = '<ul class="list-unstyled mb-0">';
|
||||
|
||||
// Vérifier les favoris
|
||||
try {
|
||||
const favoritesData = localStorage.getItem('favmastokey_favorites');
|
||||
if (favoritesData) {
|
||||
const favorites = JSON.parse(favoritesData);
|
||||
html += '<li><strong>Favoris stockés:</strong> ' + (Array.isArray(favorites) ? favorites.length : '?') + ' URLs</li>';
|
||||
} else {
|
||||
html += '<li><strong>Favoris stockés:</strong> <span class="text-muted">Aucun</span></li>';
|
||||
}
|
||||
} catch (e) {
|
||||
html += '<li><strong>Favoris stockés:</strong> <span class="text-danger">Erreur</span></li>';
|
||||
}
|
||||
|
||||
// Vérifier la migration
|
||||
try {
|
||||
const migrationData = localStorage.getItem('favmastokey_migration');
|
||||
if (migrationData) {
|
||||
const migration = JSON.parse(migrationData);
|
||||
html += '<li><strong>État de la migration:</strong> ' + (migration.status || 'Non défini') + '</li>';
|
||||
|
||||
if (migration.progress) {
|
||||
html += '<li><strong>Progression:</strong> ' +
|
||||
migration.progress.current + '/' +
|
||||
migration.progress.total + ' (' +
|
||||
(migration.progress.percentage || 0).toFixed(1) + '%)</li>';
|
||||
}
|
||||
|
||||
if (migration.lastUpdateTime) {
|
||||
const date = new Date(migration.lastUpdateTime);
|
||||
html += '<li><strong>Dernière mise à jour:</strong> ' + date.toLocaleString() + '</li>';
|
||||
}
|
||||
} else {
|
||||
html += '<li><strong>État de la migration:</strong> <span class="text-muted">Aucune migration en cours</span></li>';
|
||||
}
|
||||
} catch (e) {
|
||||
html += '<li><strong>État de la migration:</strong> <span class="text-danger">Erreur</span></li>';
|
||||
}
|
||||
|
||||
// Autres données
|
||||
const additionalKeys = [
|
||||
{key: 'favmastokey_federated_cache', label: 'Cache fédéré'},
|
||||
{key: 'favmastokey_ratelimit_queue', label: 'File d\'attente rate-limit'},
|
||||
{key: 'favmastokey_api_performance', label: 'Statistiques API'},
|
||||
{key: 'favmastokey_multitoken_migration', label: 'Migration mode filou'}
|
||||
];
|
||||
|
||||
for (const item of additionalKeys) {
|
||||
try {
|
||||
const dataStr = localStorage.getItem(item.key);
|
||||
if (dataStr) {
|
||||
const data = JSON.parse(dataStr);
|
||||
const size = typeof data === 'object' ?
|
||||
(Array.isArray(data) ? data.length : Object.keys(data).length) : 1;
|
||||
html += `<li><strong>${item.label}:</strong> ${size} entrées</li>`;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignorer les erreurs
|
||||
}
|
||||
}
|
||||
|
||||
html += '</ul>';
|
||||
localStorageInfo.innerHTML = html;
|
||||
}
|
||||
|
||||
// Mettre à jour au chargement
|
||||
updateLocalStorageInfo();
|
||||
|
||||
// Gérer le bouton d'effacement
|
||||
if (clearLocalStorageBtn) {
|
||||
clearLocalStorageBtn.addEventListener('click', function() {
|
||||
if (confirm('Êtes-vous sûr de vouloir effacer toutes les données de migration stockées localement ?')) {
|
||||
// Liste des clés connues
|
||||
const keysToRemove = [
|
||||
'favmastokey_favorites',
|
||||
'favmastokey_migration',
|
||||
'favmastokey_federated_cache',
|
||||
'favmastokey_ratelimit_queue',
|
||||
'favmastokey_api_performance',
|
||||
'favmastokey_multitoken_migration'
|
||||
];
|
||||
|
||||
// Supprimer toutes les clés connues
|
||||
keysToRemove.forEach(key => localStorage.removeItem(key));
|
||||
|
||||
// Rechercher d'autres clés potentielles
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const key = localStorage.key(i);
|
||||
if (key && key.startsWith('favmastokey_')) {
|
||||
localStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalStorageInfo();
|
||||
alert('Données localStorage effacées avec succès.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Exécuter après chargement du DOM
|
||||
document.addEventListener('DOMContentLoaded', initLocalStorageDiagnostic);
|
995
js/multitokens.js
Normal file
995
js/multitokens.js
Normal file
@ -0,0 +1,995 @@
|
||||
/**
|
||||
* 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();
|
||||
});
|
570
multitokens.php
Normal file
570
multitokens.php
Normal file
@ -0,0 +1,570 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Mode Filou (multiple tokens)
|
||||
*/
|
||||
|
||||
// Définir la constante pour inclure les fichiers
|
||||
define('FAVMASTOKEY', true);
|
||||
|
||||
// Inclure les fichiers requis
|
||||
require_once 'includes/config.php';
|
||||
require_once 'includes/functions.php';
|
||||
|
||||
// Vérifier si l'utilisateur est authentifié (au moins avec un token)
|
||||
$has_primary_token = isset($_SESSION['misskey_token']) && !empty($_SESSION['misskey_token']);
|
||||
$instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
|
||||
|
||||
// Traiter le formulaire de connexion Misskey (pour le token principal)
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'connect_token') {
|
||||
if (isset($_POST['misskey_instance']) && isset($_POST['misskey_token'])) {
|
||||
$instance = trim($_POST['misskey_instance']);
|
||||
$token = trim($_POST['misskey_token']);
|
||||
|
||||
// Vérifier que l'instance et le token sont valides
|
||||
if (empty($instance) || empty($token)) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Veuillez renseigner à la fois l\'instance Misskey et le jeton d\'accès.'
|
||||
];
|
||||
} else {
|
||||
// Valider le format de l'instance
|
||||
$instance = preg_replace('/^https?:\/\//', '', $instance);
|
||||
$instance = rtrim($instance, '/');
|
||||
|
||||
if (!preg_match('/^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/', $instance)) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'L\'URL de l\'instance Misskey semble invalide.'
|
||||
];
|
||||
} else {
|
||||
// Vérifier la validité du token en effectuant une requête test
|
||||
$validate_result = validate_misskey_token($instance, $token);
|
||||
|
||||
if ($validate_result['success']) {
|
||||
// Stocker le token et l'instance dans la session
|
||||
$_SESSION['misskey_token'] = $token;
|
||||
$_SESSION['misskey_instance'] = $instance;
|
||||
|
||||
// Initialiser le tableau des tokens supplémentaires s'il n'existe pas
|
||||
if (!isset($_SESSION['additional_tokens'])) {
|
||||
$_SESSION['additional_tokens'] = [];
|
||||
}
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'success',
|
||||
'text' => 'Connecté avec succès à ' . $instance . ' (token principal).'
|
||||
];
|
||||
|
||||
// Rediriger vers la section des tokens multiples
|
||||
header('Location: multitokens.php#tokens');
|
||||
exit;
|
||||
} else {
|
||||
// Formater le message d'erreur correctement
|
||||
$errorDetails = isset($validate_result['message']) ? $validate_result['message'] : 'Erreur inconnue';
|
||||
// Si le message est un tableau, le convertir en chaîne JSON
|
||||
if (is_array($errorDetails)) {
|
||||
$errorDetails = json_encode($errorDetails, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Le jeton d\'accès semble invalide ou a expiré.',
|
||||
'details' => $errorDetails
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Données manquantes.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter l'ajout d'un token supplémentaire
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'add_token') {
|
||||
if (isset($_POST['token_name']) && isset($_POST['token_value'])) {
|
||||
$name = trim($_POST['token_name']);
|
||||
$token = trim($_POST['token_value']);
|
||||
|
||||
// Vérifier que le nom et le token sont valides
|
||||
if (empty($name) || empty($token)) {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Veuillez renseigner à la fois un nom et un jeton d\'accès.'
|
||||
];
|
||||
} else {
|
||||
// Vérifier la validité du token en effectuant une requête test
|
||||
$validate_result = validate_misskey_token($instance, $token);
|
||||
|
||||
if ($validate_result['success']) {
|
||||
// Ajouter le token au tableau des tokens supplémentaires
|
||||
if (!isset($_SESSION['additional_tokens'])) {
|
||||
$_SESSION['additional_tokens'] = [];
|
||||
}
|
||||
|
||||
// Générer un ID unique pour ce token
|
||||
$token_id = uniqid('token_');
|
||||
|
||||
$_SESSION['additional_tokens'][$token_id] = [
|
||||
'name' => $name,
|
||||
'token' => $token,
|
||||
'added_at' => time(),
|
||||
'last_used' => null,
|
||||
'usage_count' => 0
|
||||
];
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'success',
|
||||
'text' => 'Token supplémentaire "' . $name . '" ajouté avec succès.'
|
||||
];
|
||||
|
||||
// Rediriger pour éviter la soumission multiple
|
||||
header('Location: multitokens.php#tokens');
|
||||
exit;
|
||||
} else {
|
||||
$errorDetails = isset($validate_result['message']) ? $validate_result['message'] : 'Erreur inconnue';
|
||||
if (is_array($errorDetails)) {
|
||||
$errorDetails = json_encode($errorDetails, JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Le jeton d\'accès supplémentaire "' . $name . '" semble invalide ou a expiré.',
|
||||
'details' => $errorDetails
|
||||
];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'danger',
|
||||
'text' => 'Données manquantes pour l\'ajout du token.'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Traiter la suppression d'un token
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'remove_token') {
|
||||
if (isset($_POST['token_id']) && isset($_SESSION['additional_tokens'][$_POST['token_id']])) {
|
||||
$token_id = $_POST['token_id'];
|
||||
$token_name = $_SESSION['additional_tokens'][$token_id]['name'];
|
||||
|
||||
// Supprimer le token
|
||||
unset($_SESSION['additional_tokens'][$token_id]);
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'info',
|
||||
'text' => 'Token "' . $token_name . '" supprimé avec succès.'
|
||||
];
|
||||
|
||||
// Rediriger pour éviter la soumission multiple
|
||||
header('Location: multitokens.php#tokens');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// Traitement de la déconnexion complète
|
||||
if (isset($_GET['action']) && $_GET['action'] === 'logout') {
|
||||
// Supprimer les informations d'authentification
|
||||
unset($_SESSION['misskey_token']);
|
||||
unset($_SESSION['misskey_instance']);
|
||||
unset($_SESSION['additional_tokens']);
|
||||
|
||||
$_SESSION['messages'][] = [
|
||||
'type' => 'info',
|
||||
'text' => 'Vous avez été déconnecté (tous les tokens ont été supprimés).'
|
||||
];
|
||||
|
||||
// Rediriger vers la page d'accueil
|
||||
header('Location: multitokens.php');
|
||||
exit;
|
||||
}
|
||||
|
||||
// Initialiser les messages
|
||||
$messages = [];
|
||||
if (isset($_SESSION['messages'])) {
|
||||
$messages = $_SESSION['messages'];
|
||||
unset($_SESSION['messages']);
|
||||
}
|
||||
|
||||
// Récupérer le nombre de tokens supplémentaires
|
||||
$additional_tokens_count = isset($_SESSION['additional_tokens']) ? count($_SESSION['additional_tokens']) : 0;
|
||||
$total_tokens_count = $has_primary_token ? 1 + $additional_tokens_count : $additional_tokens_count;
|
||||
|
||||
// Récupérer les favoris du localStorage
|
||||
$has_favorites = false;
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr" data-bs-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="theme-color" content="#7e57c2">
|
||||
<link rel="icon" href="images/favicon.svg" type="image/svg+xml">
|
||||
<meta name="description" content="FavMasToKey Mode Filou - Accélérez le transfert de vos favoris de Mastodon vers Misskey avec plusieurs jetons">
|
||||
<title>Mode Filou - FavMasToKey</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
|
||||
<link rel="stylesheet" href="css/styles.css">
|
||||
<style>
|
||||
.token-card {
|
||||
transition: all 0.3s ease;
|
||||
border-left: 4px solid #7e57c2;
|
||||
}
|
||||
.token-card.active {
|
||||
border-left-color: #4caf50;
|
||||
background-color: rgba(76, 175, 80, 0.1);
|
||||
}
|
||||
.token-card.cooldown {
|
||||
border-left-color: #ffb74d;
|
||||
background-color: rgba(255, 183, 77, 0.1);
|
||||
}
|
||||
.token-card.error {
|
||||
border-left-color: #f44336;
|
||||
background-color: rgba(244, 67, 54, 0.1);
|
||||
}
|
||||
.token-status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.token-status.ready {
|
||||
background-color: #4caf50;
|
||||
}
|
||||
.token-status.cooldown {
|
||||
background-color: #ffb74d;
|
||||
}
|
||||
.token-status.error {
|
||||
background-color: #f44336;
|
||||
}
|
||||
.token-status.idle {
|
||||
background-color: #9e9e9e;
|
||||
}
|
||||
#tokens-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container py-5">
|
||||
<header class="text-center mb-5">
|
||||
<h1>FavMasToKey - Mode Filou</h1>
|
||||
<p class="lead">Accélérez le transfert de vos favoris avec plusieurs jetons d'accès</p>
|
||||
<div class="mt-3">
|
||||
<a href="index.php" class="btn btn-sm btn-outline-primary">Mode Standard</a>
|
||||
<a href="doc.php" class="btn btn-sm btn-outline-primary">Documentation</a>
|
||||
<a href="diagnostic.php" class="btn btn-sm btn-outline-primary">Diagnostic</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Messages d'alerte -->
|
||||
<?php if (!empty($messages)): ?>
|
||||
<?php foreach ($messages as $message): ?>
|
||||
<div class="alert alert-<?php echo $message['type']; ?> alert-dismissible fade show" role="alert">
|
||||
<?php echo $message['text']; ?>
|
||||
<?php if (isset($message['details']) && ENVIRONMENT === 'development'): ?>
|
||||
<hr>
|
||||
<details>
|
||||
<summary>Détails techniques (mode développement)</summary>
|
||||
<pre class="mt-2 p-2 bg-dark text-light"><?php echo htmlspecialchars($message['details']); ?></pre>
|
||||
</details>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-10">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Comment ça marche ?</h2>
|
||||
<p>Le <strong>Mode Filou</strong> permet d'accélérer le transfert de vos favoris en utilisant plusieurs jetons d'accès en rotation. Cela permet de contourner les limites d'API de certaines instances Misskey très strictes.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<h5><i class="bi bi-info-circle"></i> Principe de fonctionnement</h5>
|
||||
<p>Au lieu d'utiliser un seul jeton avec un long délai entre chaque requête, le mode filou va :</p>
|
||||
<ol>
|
||||
<li>Utiliser plusieurs jetons en rotation</li>
|
||||
<li>Distribuer les requêtes entre ces jetons</li>
|
||||
<li>Respecter un délai suffisant avant de réutiliser un même jeton</li>
|
||||
</ol>
|
||||
<p class="mb-0">Par exemple, avec 10 jetons et un délai de 15 secondes entre chaque requête, vous pourrez traiter un favori toutes les 15 secondes tout en n'utilisant chaque jeton qu'une fois toutes les 150 secondes.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<h5><i class="bi bi-exclamation-triangle"></i> Utilisation responsable</h5>
|
||||
<p>Bien que cette méthode soit efficace, veuillez l'utiliser de manière responsable :</p>
|
||||
<ul>
|
||||
<li>Utilisez uniquement vos propres jetons (n'empruntez pas les jetons d'autres personnes)</li>
|
||||
<li>Respectez un délai raisonnable entre les requêtes pour ne pas surcharger l'instance</li>
|
||||
<li>Limitez le nombre de jetons à ce qui est nécessaire pour votre migration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Étape 1: Téléchargement du fichier JSON -->
|
||||
<div class="card shadow-sm mb-4" id="step1">
|
||||
<div class="card-header text-white">
|
||||
<h3 class="card-title h5 mb-0">1. Importer vos favoris Mastodon</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Téléchargez d'abord votre fichier d'export de favoris depuis Mastodon.</p>
|
||||
|
||||
<div class="card bg-light mb-3">
|
||||
<div class="card-body">
|
||||
<h5>Comment obtenir mon fichier de favoris ?</h5>
|
||||
<ol>
|
||||
<li>Connectez-vous à votre compte Mastodon</li>
|
||||
<li>Allez dans <strong>Préférences</strong> > <strong>Exporter et importer</strong></li>
|
||||
<li>Dans la section <strong>Exporter</strong>, cliquez sur <strong>Demander vos favoris</strong></li>
|
||||
<li>Une fois le fichier prêt, téléchargez-le</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="upload-form" class="mt-4">
|
||||
<div class="mb-3">
|
||||
<label for="json-file" class="form-label">Fichier JSON des favoris</label>
|
||||
<input type="file" class="form-control" id="json-file" name="json_file" accept=".json" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Analyser le fichier</button>
|
||||
</form>
|
||||
|
||||
<div id="file-summary" class="alert alert-info mt-3 d-none"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Étape 2: Gestion des tokens -->
|
||||
<div class="card shadow-sm mb-4" id="tokens">
|
||||
<div class="card-header text-white">
|
||||
<h3 class="card-title h5 mb-0">2. Configurer vos jetons d'accès</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if (!$has_primary_token): ?>
|
||||
<!-- Formulaire de connexion pour le token principal -->
|
||||
<div class="card bg-light mb-4">
|
||||
<div class="card-body">
|
||||
<h4>Token principal</h4>
|
||||
<p>Commencez par configurer votre jeton d'accès principal en vous connectant à votre instance Misskey.</p>
|
||||
|
||||
<form id="misskey-form" method="post" class="mt-3">
|
||||
<input type="hidden" name="action" value="connect_token">
|
||||
<div class="mb-3">
|
||||
<label for="misskey-instance" class="form-label">Instance Misskey</label>
|
||||
<input type="text" class="form-control" id="misskey-instance" name="misskey_instance"
|
||||
placeholder="misskey.io" required>
|
||||
<div class="form-text">Entrez le nom de domaine de votre instance Misskey (ex: misskey.io)</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="misskey-token" class="form-label">Jeton d'accès</label>
|
||||
<input type="password" class="form-control" id="misskey-token" name="misskey_token"
|
||||
placeholder="Votre jeton d'accès Misskey" required>
|
||||
<div class="form-text">Collez le jeton d'accès généré dans les paramètres de votre compte Misskey</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Se connecter à Misskey</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<!-- Informations sur l'instance connectée -->
|
||||
<div class="alert alert-success mb-4">
|
||||
<strong>Connecté à <?php echo htmlspecialchars($instance); ?></strong>
|
||||
<p class="mb-0">Vous êtes authentifié avec votre token principal.</p>
|
||||
<div class="mt-2">
|
||||
<a href="multitokens.php?action=logout" class="btn btn-sm btn-outline-dark">Déconnexion complète</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gestion des tokens supplémentaires -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="h5 mb-0">Tokens supplémentaires</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>Pour accélérer le transfert, ajoutez des jetons d'accès supplémentaires de votre compte Misskey.</p>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<strong>Conseil :</strong> Pour créer plusieurs jetons, connectez-vous à votre instance Misskey, allez dans <strong>Paramètres > API</strong> et générez plusieurs jetons d'accès avec les mêmes permissions que votre token principal.
|
||||
</div>
|
||||
|
||||
<form method="post" class="mb-4">
|
||||
<input type="hidden" name="action" value="add_token">
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label for="token-name" class="form-label">Nom du jeton</label>
|
||||
<input type="text" class="form-control" id="token-name" name="token_name" placeholder="Token 1" required>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label for="token-value" class="form-label">Valeur du jeton</label>
|
||||
<input type="password" class="form-control" id="token-value" name="token_value" placeholder="Jeton d'accès supplémentaire" required>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-primary w-100">Ajouter</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<h5>Tokens configurés (<?php echo $total_tokens_count; ?>)</h5>
|
||||
<div id="tokens-container">
|
||||
<!-- Token principal -->
|
||||
<div class="card mb-2 token-card">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="token-status ready"></span>
|
||||
<strong>Token principal</strong>
|
||||
<span class="badge bg-primary ms-2">Principal</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-muted small">Prêt</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tokens supplémentaires -->
|
||||
<?php if (isset($_SESSION['additional_tokens']) && !empty($_SESSION['additional_tokens'])): ?>
|
||||
<?php foreach ($_SESSION['additional_tokens'] as $token_id => $token_data): ?>
|
||||
<div class="card mb-2 token-card" data-token-id="<?php echo $token_id; ?>">
|
||||
<div class="card-body py-2">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<span class="token-status ready"></span>
|
||||
<strong><?php echo htmlspecialchars($token_data['name']); ?></strong>
|
||||
<?php if (isset($token_data['usage_count']) && $token_data['usage_count'] > 0): ?>
|
||||
<span class="badge bg-secondary ms-2"><?php echo $token_data['usage_count']; ?> utilisations</span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="text-muted small me-3">Prêt</span>
|
||||
<form method="post" onsubmit="return confirm('Êtes-vous sûr de vouloir supprimer ce jeton ?');">
|
||||
<input type="hidden" name="action" value="remove_token">
|
||||
<input type="hidden" name="token_id" value="<?php echo $token_id; ?>">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger">Supprimer</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">
|
||||
<p class="mb-0">Aucun token supplémentaire configuré. Ajoutez-en pour accélérer le processus.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Étape 3: Migration avec tokens multiples -->
|
||||
<div class="card shadow-sm mb-4" id="step3">
|
||||
<div class="card-header text-white">
|
||||
<h3 class="card-title h5 mb-0">3. Migration avec rotation des jetons</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<?php if ($has_primary_token && $total_tokens_count > 0): ?>
|
||||
<!-- Configuration de la migration -->
|
||||
<div class="mb-4">
|
||||
<h4>Configuration</h4>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="delay-between-tokens" class="form-label">Délai entre deux jetons (secondes)</label>
|
||||
<input type="range" class="form-range" id="delay-between-tokens" min="5" max="60" value="15" step="5">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="small">5s</span>
|
||||
<span id="delay-value">15s</span>
|
||||
<span class="small">60s</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="token-cooldown" class="form-label">Délai avant réutilisation d'un jeton (secondes)</label>
|
||||
<input type="range" class="form-range" id="token-cooldown" min="60" max="300" value="150" step="30">
|
||||
<div class="d-flex justify-content-between">
|
||||
<span class="small">60s</span>
|
||||
<span id="cooldown-value">150s</span>
|
||||
<span class="small">5min</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="auto-adjust" checked>
|
||||
<label class="form-check-label" for="auto-adjust">Ajustement automatique des délais en cas d'erreur</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Barre de progression -->
|
||||
<div class="mb-4">
|
||||
<h4>Progression</h4>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Progression globale</label>
|
||||
<div class="progress" style="height: 20px;">
|
||||
<div id="global-progress" class="progress-bar" role="progressbar"
|
||||
style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100">0%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label d-flex justify-content-between">
|
||||
<span>Statistiques</span>
|
||||
<span id="stats-display">-</span>
|
||||
</label>
|
||||
<div class="progress" style="height: 10px;">
|
||||
<div id="success-progress" class="progress-bar bg-success" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div id="skip-progress" class="progress-bar bg-info" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div id="warn-progress" class="progress-bar bg-warning" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
<div id="error-progress" class="progress-bar bg-danger" role="progressbar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Journal des opérations</label>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-log">Effacer</button>
|
||||
</div>
|
||||
<div id="log-container" class="border p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
|
||||
<div id="operation-log"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Contrôles de la migration -->
|
||||
<div class="d-flex justify-content-between">
|
||||
<button type="button" class="btn btn-primary" id="start-migration">Démarrer la migration</button>
|
||||
<div>
|
||||
<button type="button" class="btn btn-warning d-none" id="pause-migration">Pause</button>
|
||||
<button type="button" class="btn btn-danger" id="cancel-migration">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning">
|
||||
<h5>Configuration incomplète</h5>
|
||||
<p>Pour démarrer la migration, vous devez :</p>
|
||||
<ol>
|
||||
<li>Télécharger votre fichier d'export de favoris</li>
|
||||
<li>Configurer au moins un jeton d'accès</li>
|
||||
</ol>
|
||||
<p class="mb-0">Complétez les étapes précédentes avant de continuer.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="js/multitokens.js"></script>
|
||||
</body>
|
||||
</html>
|
308
process_multitoken.php
Normal file
308
process_multitoken.php
Normal file
@ -0,0 +1,308 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Traitement des favoris (version multi-tokens)
|
||||
*/
|
||||
|
||||
// Définir la constante pour inclure les fichiers
|
||||
define('FAVMASTOKEY', true);
|
||||
|
||||
// Forcer les en-têtes JSON dès le début pour éviter tout conflit
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Activer la capture d'erreurs
|
||||
set_error_handler(function($errno, $errstr, $errfile, $errline) {
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => "Erreur PHP: $errstr (ligne $errline dans $errfile)",
|
||||
]);
|
||||
exit;
|
||||
});
|
||||
|
||||
try {
|
||||
// Inclure les fichiers requis
|
||||
require_once 'includes/config.php';
|
||||
require_once 'includes/functions.php';
|
||||
|
||||
// Vérifier que la requête est en POST
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
http_response_code(405);
|
||||
echo json_encode(['success' => false, 'message' => 'Méthode non autorisée']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer les données envoyées
|
||||
$input_data = file_get_contents('php://input');
|
||||
$input = json_decode($input_data, true);
|
||||
|
||||
if (!$input || !isset($input['url'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Données invalides',
|
||||
'debug' => [
|
||||
'received' => $input_data ? substr($input_data, 0, 200) . '...' : 'Aucune donnée reçue'
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'instance Misskey (devrait être la même pour tous les tokens)
|
||||
$misskey_instance = isset($_SESSION['misskey_instance']) ? $_SESSION['misskey_instance'] : '';
|
||||
if (empty($misskey_instance)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Instance Misskey non définie']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Récupérer l'URL à traiter
|
||||
$url = $input['url'];
|
||||
|
||||
// Récupérer l'ID du token à utiliser
|
||||
$token_id = isset($input['token_id']) ? $input['token_id'] : 'primary';
|
||||
|
||||
// Récupérer le token d'accès selon l'ID fourni
|
||||
if ($token_id === 'primary') {
|
||||
// Utiliser le token principal
|
||||
if (!isset($_SESSION['misskey_token']) || empty($_SESSION['misskey_token'])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Token principal non disponible']);
|
||||
exit;
|
||||
}
|
||||
$token = $_SESSION['misskey_token'];
|
||||
} else {
|
||||
// Utiliser un token supplémentaire
|
||||
if (!isset($_SESSION['additional_tokens']) || !isset($_SESSION['additional_tokens'][$token_id])) {
|
||||
http_response_code(401);
|
||||
echo json_encode(['success' => false, 'message' => 'Token supplémentaire non trouvé: ' . $token_id]);
|
||||
exit;
|
||||
}
|
||||
$token = $_SESSION['additional_tokens'][$token_id]['token'];
|
||||
|
||||
// Mettre à jour les statistiques d'utilisation du token
|
||||
$_SESSION['additional_tokens'][$token_id]['last_used'] = time();
|
||||
$_SESSION['additional_tokens'][$token_id]['usage_count'] =
|
||||
(isset($_SESSION['additional_tokens'][$token_id]['usage_count']) ?
|
||||
$_SESSION['additional_tokens'][$token_id]['usage_count'] : 0) + 1;
|
||||
}
|
||||
|
||||
// Journaliser le début du traitement
|
||||
error_log("Traitement de l'URL: " . $url . " avec token ID: " . $token_id);
|
||||
|
||||
// Traitement de l'URL
|
||||
// 1. Vérifier si nous avons un ID en cache pour cette URL
|
||||
$cachedMisskeyId = null;
|
||||
$federatedCache = get_federated_cache();
|
||||
|
||||
if (isset($federatedCache[$url])) {
|
||||
$cachedMisskeyId = $federatedCache[$url]['id'];
|
||||
error_log("ID trouvé en cache pour $url: $cachedMisskeyId");
|
||||
}
|
||||
|
||||
// 2. Recherche de la note sur le réseau fédéré
|
||||
$noteId = null;
|
||||
$searchNeeded = true;
|
||||
|
||||
if ($cachedMisskeyId) {
|
||||
$noteId = $cachedMisskeyId;
|
||||
$searchNeeded = false;
|
||||
error_log("Utilisation de l'ID en cache, recherche fédérée évitée pour: $url");
|
||||
}
|
||||
|
||||
if ($searchNeeded) {
|
||||
$searchResult = search_federated_note($misskey_instance, $url, $token);
|
||||
|
||||
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
|
||||
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
|
||||
$noteId = $searchResult['data']['id'];
|
||||
|
||||
// Sauvegarder dans le cache
|
||||
update_federated_cache($url, $noteId);
|
||||
|
||||
error_log("Note trouvée et mise en cache: $url -> $noteId");
|
||||
} else {
|
||||
// L'ID est manquant dans la réponse
|
||||
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'error',
|
||||
'message' => "Note trouvée mais sans ID valide: $url",
|
||||
'error_type' => 'invalid_response',
|
||||
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
// Note non trouvée ou erreur de recherche
|
||||
$errorMessage = isset($searchResult['message']) ? $searchResult['message'] : "Publication introuvable";
|
||||
$errorCode = isset($searchResult['error_code']) ? $searchResult['error_code'] : '';
|
||||
$httpCode = isset($searchResult['http_code']) ? $searchResult['http_code'] : '';
|
||||
|
||||
// Déterminer le type d'erreur
|
||||
$errorType = 'not_found';
|
||||
if (strpos($errorMessage, 'rate limit') !== false || $httpCode == 429 ||
|
||||
(isset($searchResult['data']['error']) &&
|
||||
isset($searchResult['data']['error']['code']) && $searchResult['data']['error']['code'] == 'RATE_LIMIT_EXCEEDED')) {
|
||||
$errorType = 'rate_limit';
|
||||
$errorMessage = "Limite d'API atteinte. Ce jeton doit se reposer.";
|
||||
} elseif ($httpCode >= 500) {
|
||||
$errorType = 'server_error';
|
||||
$errorMessage = "L'instance Misskey rencontre des problèmes. Statut HTTP: $httpCode";
|
||||
} elseif ($httpCode == 401 || $httpCode == 403) {
|
||||
$errorType = 'auth_error';
|
||||
$errorMessage = "Problème d'authentification ou de permission avec ce jeton.";
|
||||
} elseif ($httpCode == 0) {
|
||||
$errorType = 'network_error';
|
||||
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => $errorType == 'rate_limit' ? 'warning' : 'error',
|
||||
'message' => "Publication non trouvée sur le réseau fédéré: $url ($errorMessage)",
|
||||
'error_type' => $errorType,
|
||||
'details' => $httpCode ? "HTTP: $httpCode" : ""
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Vérifier si la note est déjà favorite
|
||||
$favoriteCheckResult = check_if_favorited($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteCheckResult['success'] && $favoriteCheckResult['is_favorited']) {
|
||||
// La note est déjà dans les favoris
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'info',
|
||||
'message' => "Déjà dans vos favoris: $url",
|
||||
'error_type' => 'already_favorited',
|
||||
'misskey_id' => $noteId
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// 4. Ajouter la note aux favoris
|
||||
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteResult['success']) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url",
|
||||
'misskey_id' => $noteId
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
// Déterminer le type d'erreur
|
||||
$errorMessage = isset($favoriteResult['data']['error']['message'])
|
||||
? $favoriteResult['data']['error']['message']
|
||||
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
|
||||
|
||||
// Extraire le code d'erreur si disponible
|
||||
$errorCode = isset($favoriteResult['data']['error']['code'])
|
||||
? $favoriteResult['data']['error']['code']
|
||||
: '';
|
||||
|
||||
// Déterminer le type d'erreur
|
||||
$errorType = 'api_error';
|
||||
if (strpos($errorMessage, 'already') !== false) {
|
||||
$errorType = 'already_favorited';
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'info',
|
||||
'message' => "Déjà dans vos favoris: $url",
|
||||
'error_type' => $errorType,
|
||||
'misskey_id' => $noteId
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
|
||||
$errorType = 'rate_limit';
|
||||
$message = "Limite d'API atteinte pour: $url. Ce jeton doit se reposer.";
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'warning',
|
||||
'message' => $message,
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorMessage,
|
||||
'misskey_id' => $noteId
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
|
||||
$errorType = 'permission_denied';
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => [
|
||||
'status' => 'error',
|
||||
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorCode ? "Code: $errorCode" : "",
|
||||
'misskey_id' => $noteId
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Capturer toutes les exceptions et renvoyer un message d'erreur formaté en JSON
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Exception: ' . $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère le cache de fédération depuis la session
|
||||
*
|
||||
* @return array Cache de fédération
|
||||
*/
|
||||
function get_federated_cache() {
|
||||
if (!isset($_SESSION['federated_cache'])) {
|
||||
$_SESSION['federated_cache'] = [];
|
||||
}
|
||||
|
||||
return $_SESSION['federated_cache'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Met à jour le cache de fédération dans la session
|
||||
*
|
||||
* @param string $url URL de la publication Mastodon
|
||||
* @param string $misskey_id ID de la publication sur Misskey
|
||||
*/
|
||||
function update_federated_cache($url, $misskey_id) {
|
||||
if (!isset($_SESSION['federated_cache'])) {
|
||||
$_SESSION['federated_cache'] = [];
|
||||
}
|
||||
|
||||
$_SESSION['federated_cache'][$url] = [
|
||||
'id' => $misskey_id,
|
||||
'timestamp' => time()
|
||||
];
|
||||
|
||||
// Nettoyer le cache si nécessaire (plus de 1000 entrées)
|
||||
if (count($_SESSION['federated_cache']) > 1000) {
|
||||
// Trier par timestamp et ne garder que les 500 plus récentes
|
||||
uasort($_SESSION['federated_cache'], function($a, $b) {
|
||||
return $b['timestamp'] - $a['timestamp'];
|
||||
});
|
||||
|
||||
$_SESSION['federated_cache'] = array_slice($_SESSION['federated_cache'], 0, 500, true);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user