Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
3d5675c7a1 | |||
e39e76fd06 | |||
d657ad0557 | |||
87eeeafdde | |||
5a1d788b18 |
@ -102,6 +102,27 @@ p {
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
/* Range slider pour le délai */
|
||||
.form-range {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-thumb {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-range::-moz-range-thumb {
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.form-range::-webkit-slider-runnable-track {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.form-range::-moz-range-track {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn-primary {
|
||||
background-color: var(--primary-color);
|
||||
|
@ -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>
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Configuration
|
||||
* FavMasToKey - Configuration (version améliorée)
|
||||
*/
|
||||
|
||||
// Empêcher l'accès direct au fichier
|
||||
@ -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.4.0', // Mise à jour de la version pour les nouvelles fonctionnalités
|
||||
'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
|
||||
@ -41,8 +41,44 @@ $config = [
|
||||
'batch_size' => 2,
|
||||
'timeout' => 90,
|
||||
'max_retries' => 3,
|
||||
'delay_between_requests' => 3000, // Délai normal entre les requêtes (en millisecondes)
|
||||
'slow_mode_delay' => 7000 // Délai en mode lent entre les requêtes (en millisecondes)
|
||||
'delay_between_requests' => 3000, // Délai normal entre les requêtes (en millisecondes)
|
||||
|
||||
// Paramètres pour le mode lent
|
||||
'slow_mode_delay' => 30000, // Délai par défaut en mode lent (30 secondes)
|
||||
'slow_mode_min' => 10000, // Délai minimum pour le mode lent (10 secondes)
|
||||
'slow_mode_max' => 300000, // Délai maximum pour le mode lent (5 minutes)
|
||||
|
||||
// Paramètres pour le mode tortue
|
||||
'tortoise_mode_delay' => 120000, // Délai par défaut en mode tortue (2 minutes)
|
||||
'tortoise_mode_min' => 60000, // Délai minimum pour le mode tortue (1 minute)
|
||||
'tortoise_mode_max' => 300000, // Délai maximum pour le mode tortue (5 minutes)
|
||||
|
||||
// AMÉLIORATION 1: Paramètres pour la file d'attente des rate-limit
|
||||
'rate_limit_queue_retry_delay' => 60000, // Délai avant de réessayer un élément en file d'attente (60 secondes)
|
||||
'rate_limit_max_retries' => 5, // Nombre maximum de tentatives pour un même élément
|
||||
|
||||
// AMÉLIORATION 2: Paramètres pour l'optimisation des favoris déjà existants
|
||||
'skip_delay_for_already_favorited' => true, // Ne pas attendre le délai pour les favoris déjà existants
|
||||
|
||||
// AMÉLIORATION 3: Paramètres pour le cache de fédération
|
||||
'federation_cache_enabled' => true, // Activer le cache de fédération
|
||||
'federation_cache_ttl' => 1440, // Durée de vie du cache en minutes (24 heures)
|
||||
'federation_cache_cleanup_interval' => 60, // Intervalle de nettoyage du cache en minutes
|
||||
|
||||
// AMÉLIORATION 4: Paramètres pour l'adaptation des délais
|
||||
'adaptive_delay_enabled' => true, // Activer l'ajustement automatique des délais
|
||||
'adaptive_delay_step' => 5000, // Pas d'augmentation du délai en cas de rate limiting (5 secondes)
|
||||
'adaptive_delay_max_increases' => 3, // Nombre maximum d'augmentations automatiques du délai
|
||||
'adaptive_delay_decrease_rate' => 0.8, // Facteur de diminution du délai après succès (80%)
|
||||
|
||||
// AMÉLIORATION 5: Paramètres pour les points de sauvegarde
|
||||
'checkpoint_interval' => 300000, // Intervalle entre les points de sauvegarde (5 minutes)
|
||||
'checkpoint_max_stored' => 3, // Nombre maximum de points de sauvegarde à conserver
|
||||
|
||||
// Paramètres pour la pause automatique
|
||||
'auto_pause_enabled' => true, // Activer la pause automatique en cas de trop de rate-limits
|
||||
'auto_pause_duration' => 15, // Durée de pause automatique en minutes
|
||||
'auto_pause_threshold' => 3, // Nombre d'erreurs de rate-limit consécutives avant pause
|
||||
];
|
||||
|
||||
// Session
|
||||
@ -77,4 +113,26 @@ function debug($data, $title = '', $log_to_file = false) {
|
||||
echo '</pre>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Journalise les performances des requêtes API pour analyse
|
||||
*
|
||||
* @param string $domain Domaine de l'instance
|
||||
* @param string $action Type d'action (recherche, ajout favori, etc.)
|
||||
* @param int $response_time Temps de réponse en millisecondes
|
||||
* @param bool $success Succès de la requête
|
||||
* @param string $error_type Type d'erreur si échec
|
||||
*/
|
||||
function log_api_performance($domain, $action, $response_time, $success, $error_type = null) {
|
||||
if (ENVIRONMENT === 'development') {
|
||||
$status = $success ? 'success' : ($error_type === 'rate_limit' ? 'rate_limit' : 'error');
|
||||
$log_entry = date('Y-m-d H:i:s') . " | $domain | $action | {$response_time}ms | $status";
|
||||
|
||||
if ($error_type) {
|
||||
$log_entry .= " | $error_type";
|
||||
}
|
||||
|
||||
error_log("[API_PERF] " . $log_entry);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Fonctions utilitaires
|
||||
* FavMasToKey - Fonctions utilitaires (version améliorée)
|
||||
*/
|
||||
|
||||
// Empêcher l'accès direct au fichier
|
||||
@ -94,6 +94,17 @@ function extract_toot_ids($urls) {
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient l'URL de base d'une instance ActivityPub
|
||||
*
|
||||
* @param string $url URL complète
|
||||
* @return string Domaine de l'instance
|
||||
*/
|
||||
function get_instance_domain($url) {
|
||||
$parsed = parse_url($url);
|
||||
return isset($parsed['host']) ? $parsed['host'] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Effectue une requête cURL vers l'API Misskey
|
||||
*
|
||||
@ -101,11 +112,14 @@ function extract_toot_ids($urls) {
|
||||
* @param string $endpoint Point d'accès API (ex: /api/notes/favorites/create)
|
||||
* @param array $data Données à envoyer
|
||||
* @param string $token Token d'accès OAuth
|
||||
* @param bool $with_timing Indique si les informations de timing doivent être retournées
|
||||
* @return array Résultat de la requête
|
||||
*/
|
||||
function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
function misskey_api_request($instance, $endpoint, $data, $token, $with_timing = false) {
|
||||
global $config;
|
||||
|
||||
$start_time = microtime(true);
|
||||
|
||||
// Construire l'URL complète
|
||||
$url = "https://{$instance}{$endpoint}";
|
||||
|
||||
@ -134,17 +148,30 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
// Calcul du temps de réponse
|
||||
$end_time = microtime(true);
|
||||
$response_time = round(($end_time - $start_time) * 1000); // en millisecondes
|
||||
|
||||
// Fermer la session cURL
|
||||
curl_close($ch);
|
||||
|
||||
// Vérifier les erreurs
|
||||
if ($error) {
|
||||
return [
|
||||
$result = [
|
||||
'success' => false,
|
||||
'message' => 'Erreur cURL: ' . $error,
|
||||
'http_code' => $http_code,
|
||||
'error_code' => 'NETWORK_ERROR'
|
||||
];
|
||||
|
||||
if ($with_timing) {
|
||||
$result['timing'] = [
|
||||
'response_time_ms' => $response_time,
|
||||
'endpoint' => $endpoint
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Décoder la réponse
|
||||
@ -152,22 +179,40 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
|
||||
// Analyse spécifique des erreurs
|
||||
if ($http_code == 429) {
|
||||
return [
|
||||
$result = [
|
||||
'success' => false,
|
||||
'message' => 'Limite de taux (rate limit) dépassée. Réduisez la fréquence des requêtes ou activez le mode lent.',
|
||||
'http_code' => $http_code,
|
||||
'error_code' => 'RATE_LIMIT_EXCEEDED',
|
||||
'data' => $response_data
|
||||
];
|
||||
|
||||
if ($with_timing) {
|
||||
$result['timing'] = [
|
||||
'response_time_ms' => $response_time,
|
||||
'endpoint' => $endpoint
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Vérifier si la requête a réussi
|
||||
if ($http_code >= 200 && $http_code < 300) {
|
||||
return [
|
||||
$result = [
|
||||
'success' => true,
|
||||
'data' => $response_data,
|
||||
'http_code' => $http_code
|
||||
];
|
||||
|
||||
if ($with_timing) {
|
||||
$result['timing'] = [
|
||||
'response_time_ms' => $response_time,
|
||||
'endpoint' => $endpoint
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
} else {
|
||||
// Déterminer le type d'erreur
|
||||
$error_code = 'API_ERROR';
|
||||
@ -182,13 +227,22 @@ function misskey_api_request($instance, $endpoint, $data, $token) {
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
$result = [
|
||||
'success' => false,
|
||||
'message' => $error_message,
|
||||
'http_code' => $http_code,
|
||||
'error_code' => $error_code,
|
||||
'data' => $response_data
|
||||
];
|
||||
|
||||
if ($with_timing) {
|
||||
$result['timing'] = [
|
||||
'response_time_ms' => $response_time,
|
||||
'endpoint' => $endpoint
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
@ -243,6 +297,56 @@ function validate_misskey_token($instance, $token) {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une note est déjà dans les favoris
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @param string $note_id ID de la note à vérifier
|
||||
* @param string $token Token d'accès
|
||||
* @return array Résultat de la vérification
|
||||
*/
|
||||
function check_if_favorited($instance, $note_id, $token) {
|
||||
// Endpoint pour récupérer le statut de favori d'une note
|
||||
$endpoint = '/api/notes/show';
|
||||
|
||||
// Données pour la requête
|
||||
$data = [
|
||||
'noteId' => $note_id
|
||||
];
|
||||
|
||||
// Effectuer la requête avec timing pour les statistiques de performance
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
|
||||
|
||||
if ($result['success'] && isset($result['data'])) {
|
||||
// La note existe et nous avons une réponse
|
||||
// Vérifier si l'utilisateur a déjà mis cette note en favori
|
||||
if (isset($result['data']['isFavorited']) && $result['data']['isFavorited'] === true) {
|
||||
return [
|
||||
'success' => true,
|
||||
'is_favorited' => true,
|
||||
'message' => 'Cette note est déjà dans vos favoris',
|
||||
'timing' => isset($result['timing']) ? $result['timing'] : null
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => true,
|
||||
'is_favorited' => false,
|
||||
'message' => 'Cette note n\'est pas encore dans vos favoris',
|
||||
'timing' => isset($result['timing']) ? $result['timing'] : null
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// En cas d'erreur ou si la note n'existe pas
|
||||
return [
|
||||
'success' => false,
|
||||
'is_favorited' => false,
|
||||
'message' => isset($result['message']) ? $result['message'] : 'Impossible de vérifier le statut de favori',
|
||||
'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR',
|
||||
'timing' => isset($result['timing']) ? $result['timing'] : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche une note Mastodon sur le réseau fédéré de Misskey
|
||||
* Utilise l'endpoint ap/show qui s'est avéré le plus fiable avec différentes instances
|
||||
@ -265,8 +369,8 @@ function search_federated_note($instance, $url, $token) {
|
||||
'uri' => $cleanUrl
|
||||
];
|
||||
|
||||
// Effectuer la requête
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token);
|
||||
// Effectuer la requête avec timing pour les statistiques de performance
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
|
||||
|
||||
// Journal pour le format de la réponse
|
||||
if ($result['success'] && isset($result['data'])) {
|
||||
@ -313,7 +417,7 @@ function search_federated_note($instance, $url, $token) {
|
||||
}
|
||||
|
||||
// Méthode de secours 1: Essayer notes/search-by-url (parfois utilisé dans les anciennes versions)
|
||||
$fallback_result = misskey_api_request($instance, '/api/notes/search-by-url', ['url' => $cleanUrl], $token);
|
||||
$fallback_result = misskey_api_request($instance, '/api/notes/search-by-url', ['url' => $cleanUrl], $token, true);
|
||||
|
||||
if ($fallback_result['success'] && isset($fallback_result['data'])) {
|
||||
error_log("Format de réponse search-by-url: " . json_encode(array_keys($fallback_result['data'])));
|
||||
@ -354,7 +458,7 @@ function search_federated_note($instance, $url, $token) {
|
||||
$remoteId = "https://{$acctDomain}/users/{$username}/statuses/{$statusId}";
|
||||
|
||||
// Essayer d'abord avec /api/notes/show
|
||||
$remote_result = misskey_api_request($instance, '/api/notes/show', ['uri' => $remoteId], $token);
|
||||
$remote_result = misskey_api_request($instance, '/api/notes/show', ['uri' => $remoteId], $token, true);
|
||||
|
||||
if ($remote_result['success'] && isset($remote_result['data']['id'])) {
|
||||
return $remote_result;
|
||||
@ -369,7 +473,7 @@ function search_federated_note($instance, $url, $token) {
|
||||
$renote_result = misskey_api_request($instance, '/api/notes/search', [
|
||||
'query' => "@{$username}@{$acctDomain} {$statusId}",
|
||||
'limit' => 10
|
||||
], $token);
|
||||
], $token, true);
|
||||
|
||||
if ($renote_result['success'] && !empty($renote_result['data'])) {
|
||||
// Parcourir les résultats pour trouver une correspondance
|
||||
@ -394,7 +498,8 @@ function search_federated_note($instance, $url, $token) {
|
||||
'success' => false,
|
||||
'message' => "Impossible de trouver la publication sur le réseau fédéré après plusieurs tentatives",
|
||||
'error_code' => 'NOT_FOUND',
|
||||
'http_code' => isset($result['http_code']) ? $result['http_code'] : 404
|
||||
'http_code' => isset($result['http_code']) ? $result['http_code'] : 404,
|
||||
'timing' => isset($result['timing']) ? $result['timing'] : null
|
||||
];
|
||||
}
|
||||
|
||||
@ -417,8 +522,93 @@ function add_to_favorites($instance, $note_id, $token) {
|
||||
'noteId' => $note_id
|
||||
];
|
||||
|
||||
// Effectuer la requête avec timing pour les statistiques de performance
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token, true);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les favoris existants sur Misskey (pour une vérification en masse)
|
||||
*
|
||||
* @param string $instance Instance Misskey
|
||||
* @param string $token Token d'accès
|
||||
* @param int $limit Nombre de favoris à récupérer par page
|
||||
* @param string $untilId ID pour la pagination
|
||||
* @return array Liste des favoris et statut de la requête
|
||||
*/
|
||||
function get_existing_favorites($instance, $token, $limit = 100, $untilId = null) {
|
||||
// Endpoint pour récupérer les favoris
|
||||
$endpoint = '/api/i/favorites';
|
||||
|
||||
// Données pour la requête
|
||||
$data = [
|
||||
'limit' => $limit
|
||||
];
|
||||
|
||||
// Ajouter l'ID pour la pagination si fourni
|
||||
if ($untilId) {
|
||||
$data['untilId'] = $untilId;
|
||||
}
|
||||
|
||||
// Effectuer la requête
|
||||
$result = misskey_api_request($instance, $endpoint, $data, $token);
|
||||
|
||||
return $result;
|
||||
if ($result['success'] && isset($result['data']) && is_array($result['data'])) {
|
||||
// Extraire les IDs
|
||||
$favoriteIds = [];
|
||||
foreach ($result['data'] as $favorite) {
|
||||
if (isset($favorite['id'])) {
|
||||
$favoriteIds[] = $favorite['id'];
|
||||
}
|
||||
}
|
||||
|
||||
// Déterminer s'il y a plus de résultats (pour la pagination)
|
||||
$hasMore = count($result['data']) >= $limit;
|
||||
$lastId = $hasMore && !empty($result['data']) ? end($result['data'])['id'] : null;
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'favorites' => $favoriteIds,
|
||||
'has_more' => $hasMore,
|
||||
'last_id' => $lastId,
|
||||
'count' => count($favoriteIds)
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => isset($result['message']) ? $result['message'] : 'Impossible de récupérer les favoris',
|
||||
'error_code' => isset($result['error_code']) ? $result['error_code'] : 'UNKNOWN_ERROR'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Récupère les performances de l'API pour un domaine spécifique
|
||||
* et recommande un délai adapté
|
||||
*
|
||||
* @param string $domain Domaine de l'instance
|
||||
* @param array $stats Statistiques de performance
|
||||
* @return int Délai recommandé en secondes
|
||||
*/
|
||||
function get_recommended_delay_for_domain($domain, $stats) {
|
||||
// Valeurs par défaut
|
||||
$baseDelay = 3; // secondes
|
||||
$increment = 5; // secondes par échec de rate-limit
|
||||
|
||||
if (!$stats || !isset($stats['rateLimitCount'])) {
|
||||
return $baseDelay;
|
||||
}
|
||||
|
||||
// Calculer un délai basé sur le nombre d'erreurs de rate-limit
|
||||
$rateLimitCount = (int)$stats['rateLimitCount'];
|
||||
|
||||
if ($rateLimitCount > 0) {
|
||||
// Formule: délai de base + (nombre d'erreurs de rate-limit * incrément)
|
||||
// Limité à un maximum de 300 secondes (5 minutes)
|
||||
$recommendedDelay = min(300, $baseDelay + ($rateLimitCount * $increment));
|
||||
return $recommendedDelay;
|
||||
}
|
||||
|
||||
return $baseDelay;
|
||||
}
|
42
index.php
42
index.php
@ -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 -->
|
||||
@ -235,16 +235,52 @@ if (isset($_SESSION['messages'])) {
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Mode lent -->
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="slow-mode">
|
||||
<label class="form-check-label" for="slow-mode">Mode lent</label>
|
||||
<div class="form-text">Activez cette option pour les instances Misskey avec des limitations d'API strictes</div>
|
||||
</div>
|
||||
|
||||
<div id="slow-mode-options" class="mb-4 d-none">
|
||||
<label for="slow-mode-delay" class="form-label">Délai entre les requêtes : <span id="delay-value">30</span> secondes</label>
|
||||
<input type="range" class="form-range" id="slow-mode-delay" min="10" max="60" step="5" value="30">
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>10s (plus rapide)</span>
|
||||
<span>60s (plus lent)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="slow-mode-warning" class="alert alert-warning mb-4 d-none">
|
||||
<strong>Mode lent activé</strong>
|
||||
<p>Le mode lent ajoute des délais supplémentaires entre les requêtes et réduit le nombre de favoris traités simultanément. Cela rend l'importation beaucoup plus lente mais réduit considérablement le risque de blocage par l'API.</p>
|
||||
<p class="mb-0">Recommandé si vous rencontrez des erreurs de limite de taux (rate limit) ou si votre instance Misskey est très stricte sur le nombre de requêtes autorisées.</p>
|
||||
<p>Ce mode ajoute des délais importants entre les requêtes (30 secondes par défaut) et ne traite qu'un seul favori à la fois. Cela rend l'importation plus lente mais permet d'éviter (dans une certaine mesure) le risque de blocage par l'API.</p>
|
||||
<p class="mb-0">Recommandé si vous rencontrez systématiquement des erreurs de limite de taux (rate limit) ou si votre instance Misskey est extrêmement stricte sur le nombre de requêtes autorisées.</p>
|
||||
</div>
|
||||
|
||||
<!-- Mode tortue -->
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" id="tortoise-mode">
|
||||
<label class="form-check-label" for="tortoise-mode">Mode tortue</label>
|
||||
<div class="form-text">Activez cette option pour les instances Misskey avec des limitations d'API extrêmement strictes</div>
|
||||
</div>
|
||||
|
||||
<div id="tortoise-mode-options" class="mb-4 d-none">
|
||||
<label for="tortoise-mode-delay" class="form-label">Délai entre les requêtes : <span id="tortoise-delay-value">120</span> secondes</label>
|
||||
<input type="range" class="form-range" id="tortoise-mode-delay" min="60" max="300" step="30" value="120">
|
||||
<div class="d-flex justify-content-between small text-muted">
|
||||
<span>60s (plus rapide)</span>
|
||||
<span>300s (5 min)</span>
|
||||
</div>
|
||||
<div class="form-check mt-2">
|
||||
<input class="form-check-input" type="checkbox" id="auto-pause-enabled" checked>
|
||||
<label class="form-check-label" for="auto-pause-enabled">Pauses automatiques après détection de rate limit</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="tortoise-mode-warning" class="alert alert-warning mb-4 d-none">
|
||||
<strong>Mode tortue activé</strong>
|
||||
<p>Ce mode utilise des délais extrêmement longs entre les requêtes (2-5 minutes par défaut) et effectue des pauses automatiques de 15 minutes lorsque des limitations d'API sont détectées.</p>
|
||||
<p class="mb-0">Utilisez ce mode pour les instances Misskey avec des limites d'API très strictes. La migration sera <strong>extrêmement lente</strong> mais devrait pouvoir terminer sans interventions manuelles fréquentes.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
|
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>
|
389
process.php
389
process.php
@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* FavMasToKey - Traitement des favoris
|
||||
* FavMasToKey - Traitement des favoris (version améliorée)
|
||||
*/
|
||||
|
||||
// Définir la constante pour inclure les fichiers
|
||||
@ -64,14 +64,44 @@ try {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Journaliser les paramètres reçus
|
||||
$receivedParams = [
|
||||
'tortoiseMode' => isset($input['tortoiseMode']) ? $input['tortoiseMode'] : 'non défini',
|
||||
'slowMode' => isset($input['slowMode']) ? $input['slowMode'] : 'non défini',
|
||||
'delaySeconds' => isset($input['delaySeconds']) ? $input['delaySeconds'] : 'non défini',
|
||||
'useCachedIds' => isset($input['useCachedIds']) ? $input['useCachedIds'] : false,
|
||||
'hasBatchData' => isset($input['batchData']) ? true : false
|
||||
];
|
||||
error_log("Paramètres reçus dans process.php: " . json_encode($receivedParams));
|
||||
|
||||
// Récupérer le lot à traiter
|
||||
$batch = $input['batch'];
|
||||
$currentIndex = isset($input['currentIndex']) ? (int)$input['currentIndex'] : 0;
|
||||
$totalItems = isset($input['totalItems']) ? (int)$input['totalItems'] : count($batch);
|
||||
$slowMode = isset($input['slowMode']) ? (bool)$input['slowMode'] : false;
|
||||
$tortoiseMode = isset($input['tortoiseMode']) ? (bool)$input['tortoiseMode'] : false;
|
||||
$delaySeconds = isset($input['delaySeconds']) ? (int)$input['delaySeconds'] : 30;
|
||||
$useCachedIds = isset($input['useCachedIds']) ? (bool)$input['useCachedIds'] : false;
|
||||
|
||||
// AMÉLIORATION 3: Récupérer les données du cache si fournies
|
||||
$batchData = isset($input['batchData']) ? $input['batchData'] : null;
|
||||
|
||||
// Ajuster le délai entre les requêtes selon le mode
|
||||
$delayMs = $slowMode ? $config['slow_mode_delay'] : $config['delay_between_requests'];
|
||||
error_log("Paramètres après conversion: slowMode=" . ($slowMode ? 'true' : 'false') .
|
||||
", tortoiseMode=" . ($tortoiseMode ? 'true' : 'false') .
|
||||
", delaySeconds=" . $delaySeconds .
|
||||
", useCachedIds=" . ($useCachedIds ? 'true' : 'false'));
|
||||
|
||||
// Ajuster le délai entre les requêtes selon le mode et la valeur personnalisée
|
||||
if ($tortoiseMode) {
|
||||
$delayMs = $delaySeconds * 1000; // Convertir en millisecondes (délai tortue - 60-300 secondes)
|
||||
error_log("Utilisation du mode tortue avec délai: " . $delayMs . "ms");
|
||||
} else if ($slowMode) {
|
||||
$delayMs = $delaySeconds * 1000; // Convertir en millisecondes (délai lent - 10-60 secondes)
|
||||
error_log("Utilisation du mode lent avec délai: " . $delayMs . "ms");
|
||||
} else {
|
||||
$delayMs = $config['delay_between_requests']; // Délai normal (3000ms par défaut)
|
||||
error_log("Utilisation du mode normal avec délai: " . $delayMs . "ms");
|
||||
}
|
||||
|
||||
// Résultats du traitement
|
||||
$results = [];
|
||||
@ -83,7 +113,7 @@ try {
|
||||
|
||||
// Vérifier que l'URL est valide
|
||||
if (!$urlParts || !isset($urlParts['host']) || !isset($urlParts['path'])) {
|
||||
$results[] = [
|
||||
$results[$index] = [
|
||||
'status' => 'error',
|
||||
'message' => "URL invalide: $url",
|
||||
'error_type' => 'invalid_url'
|
||||
@ -92,123 +122,242 @@ try {
|
||||
}
|
||||
|
||||
// Ajouter un log pour déboguer
|
||||
error_log("Recherche de l'URL: " . $url . " sur l'instance: " . $misskey_instance);
|
||||
error_log("Traitement de l'URL: " . $url . " sur l'instance: " . $misskey_instance);
|
||||
|
||||
// Rechercher la note sur Misskey à partir de l'URL
|
||||
$searchResult = search_federated_note($misskey_instance, $url, $token);
|
||||
|
||||
// Vérifier si la recherche a réussi ET si les données contiennent un ID valide
|
||||
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
|
||||
// Vérifier et extraire l'ID de manière sécurisée
|
||||
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
|
||||
$noteId = $searchResult['data']['id'];
|
||||
|
||||
// Tenter d'ajouter la note aux favoris
|
||||
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteResult['success']) {
|
||||
$results[] = [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url"
|
||||
];
|
||||
} else {
|
||||
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
|
||||
$errorMessage = isset($favoriteResult['data']['error']['message'])
|
||||
? $favoriteResult['data']['error']['message']
|
||||
: (isset($favoriteResult['message']) ? $favoriteResult['message'] : 'Erreur inconnue');
|
||||
// AMÉLIORATION 3: Vérifier si nous avons un ID en cache pour cette URL
|
||||
$cachedMisskeyId = null;
|
||||
if ($useCachedIds && $batchData) {
|
||||
// Vérifier si batchData est un tableau indexé ou associatif
|
||||
if (isset($batchData[$index])) {
|
||||
// Format indexé
|
||||
if ($batchData[$index]['url'] === $url &&
|
||||
$batchData[$index]['cached'] === true) {
|
||||
|
||||
// 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';
|
||||
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
|
||||
$errorType = 'rate_limit';
|
||||
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
|
||||
$errorType = 'permission_denied';
|
||||
}
|
||||
|
||||
if ($errorType === 'already_favorited') {
|
||||
$results[] = [
|
||||
'status' => 'info',
|
||||
'message' => "Déjà dans vos favoris: $url",
|
||||
'error_type' => $errorType
|
||||
];
|
||||
} else if ($errorType === 'rate_limit') {
|
||||
$results[] = [
|
||||
'status' => 'warning',
|
||||
'message' => "Limite d'API atteinte, veuillez activer le mode lent: $url",
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorMessage
|
||||
];
|
||||
} else {
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorCode ? "Code: $errorCode" : ""
|
||||
];
|
||||
}
|
||||
$cachedMisskeyId = $batchData[$index]['cachedId'];
|
||||
error_log("ID trouvé en cache pour $url: $cachedMisskeyId");
|
||||
}
|
||||
} else {
|
||||
// L'ID est manquant ou vide dans la réponse
|
||||
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
|
||||
$results[] = [
|
||||
'status' => 'error',
|
||||
'message' => "Note trouvée mais sans ID valide: $url",
|
||||
'error_type' => 'invalid_response',
|
||||
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
|
||||
];
|
||||
|
||||
// Log pour déboguer
|
||||
error_log("Structure de données reçue: " . json_encode($searchResult['data']));
|
||||
}
|
||||
} 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. Essayez d'activer le mode lent ou d'attendre quelques minutes.";
|
||||
} 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. Vérifiez votre token.";
|
||||
} elseif ($httpCode == 0) {
|
||||
$errorType = 'network_error';
|
||||
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'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" : ""
|
||||
];
|
||||
|
||||
// Ajouter des infos de debug
|
||||
if (isset($searchResult['data']) && is_array($searchResult['data'])) {
|
||||
error_log("Données reçues pour URL $url: " . json_encode($searchResult['data']));
|
||||
// Format non-indexé, parcourir et chercher l'URL
|
||||
foreach ($batchData as $item) {
|
||||
if (isset($item['url']) && $item['url'] === $url &&
|
||||
isset($item['cached']) && $item['cached'] === true &&
|
||||
isset($item['cachedId'])) {
|
||||
|
||||
$cachedMisskeyId = $item['cachedId'];
|
||||
error_log("ID trouvé en cache pour $url: $cachedMisskeyId (recherche)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pause pour éviter le rate limiting - utilise le délai approprié selon le mode
|
||||
usleep($delayMs * 1000);
|
||||
// AMÉLIORATION 2: Si nous avons un ID en cache, nous pouvons sauter directement à la vérification du statut de favori
|
||||
$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");
|
||||
}
|
||||
|
||||
// Rechercher la note sur Misskey à partir de l'URL si nécessaire
|
||||
if ($searchNeeded) {
|
||||
$searchResult = search_federated_note($misskey_instance, $url, $token);
|
||||
|
||||
// Vérifier si la recherche a réussi ET si les données contiennent un ID valide
|
||||
if ($searchResult['success'] && isset($searchResult['data']) && !empty($searchResult['data'])) {
|
||||
// Vérifier et extraire l'ID de manière sécurisée
|
||||
if (isset($searchResult['data']['id']) && !empty($searchResult['data']['id'])) {
|
||||
$noteId = $searchResult['data']['id'];
|
||||
|
||||
// AMÉLIORATION 3: Sauvegarder le résultat dans la réponse pour mise à jour du cache côté client
|
||||
$results[$index]['misskey_id'] = $noteId;
|
||||
} else {
|
||||
// L'ID est manquant ou vide dans la réponse
|
||||
$dataKeys = is_array($searchResult['data']) ? array_keys($searchResult['data']) : ['non_array'];
|
||||
$results[$index] = [
|
||||
'status' => 'error',
|
||||
'message' => "Note trouvée mais sans ID valide: $url",
|
||||
'error_type' => 'invalid_response',
|
||||
'details' => "Clés disponibles: " . implode(', ', $dataKeys)
|
||||
];
|
||||
|
||||
// Log pour déboguer
|
||||
error_log("Structure de données reçue: " . json_encode($searchResult['data']));
|
||||
continue;
|
||||
}
|
||||
} 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';
|
||||
if ($tortoiseMode) {
|
||||
$errorMessage = "Limite d'API atteinte malgré le mode tortue. Essayez d'augmenter le délai ou attendez quelques minutes.";
|
||||
} else if ($slowMode) {
|
||||
$errorMessage = "Limite d'API atteinte. Activez le mode tortue ou attendez quelques minutes.";
|
||||
} else {
|
||||
$errorMessage = "Limite d'API atteinte. Activez le mode ultra-lent ou le mode tortue.";
|
||||
}
|
||||
} 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. Vérifiez votre token.";
|
||||
} elseif ($httpCode == 0) {
|
||||
$errorType = 'network_error';
|
||||
$errorMessage = "Problème de connexion réseau. Vérifiez votre connectivité.";
|
||||
}
|
||||
|
||||
$results[$index] = [
|
||||
'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" : ""
|
||||
];
|
||||
|
||||
// Ajouter des infos de debug
|
||||
if (isset($searchResult['data']) && is_array($searchResult['data'])) {
|
||||
error_log("Données reçues pour URL $url: " . json_encode($searchResult['data']));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// AMÉLIORATION 2: Vérifier d'abord si la note est déjà favorite pour économiser une requête API
|
||||
$favoriteCheckResult = check_if_favorited($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteCheckResult['success'] && $favoriteCheckResult['is_favorited']) {
|
||||
// La note est déjà dans les favoris
|
||||
$results[$index] = [
|
||||
'status' => 'info',
|
||||
'message' => "Déjà dans vos favoris: $url",
|
||||
'error_type' => 'already_favorited',
|
||||
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID même si déjà favori
|
||||
];
|
||||
|
||||
// AMÉLIORATION 2: Ne pas attendre le délai complet pour les notes déjà favorites
|
||||
error_log("Note déjà favorite, aucun délai nécessaire: $url");
|
||||
continue;
|
||||
} else {
|
||||
// La note n'est pas encore dans les favoris ou la vérification a échoué
|
||||
// Dans le cas d'échec, nous essayons quand même d'ajouter la note
|
||||
$favoriteResult = add_to_favorites($misskey_instance, $noteId, $token);
|
||||
|
||||
if ($favoriteResult['success']) {
|
||||
$results[$index] = [
|
||||
'status' => 'success',
|
||||
'message' => "Ajouté aux favoris: $url",
|
||||
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
|
||||
];
|
||||
} else {
|
||||
// Vérifier si c'est une erreur de "déjà ajouté aux favoris"
|
||||
$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';
|
||||
} elseif (strpos($errorMessage, 'rate limit') !== false || strpos($errorMessage, 'limit exceeded') !== false || $errorCode == 'RATE_LIMIT_EXCEEDED') {
|
||||
$errorType = 'rate_limit';
|
||||
} elseif (strpos($errorMessage, 'permission') !== false || $errorCode == 'NO_PERMISSION') {
|
||||
$errorType = 'permission_denied';
|
||||
}
|
||||
|
||||
if ($errorType === 'already_favorited') {
|
||||
$results[$index] = [
|
||||
'status' => 'info',
|
||||
'message' => "Déjà dans vos favoris: $url",
|
||||
'error_type' => $errorType,
|
||||
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID même si déjà favori
|
||||
];
|
||||
} else if ($errorType === 'rate_limit') {
|
||||
$message = "Limite d'API atteinte pour: $url. ";
|
||||
if ($tortoiseMode) {
|
||||
$message .= "Essayez d'augmenter le délai ou attendez quelques minutes.";
|
||||
} else if ($slowMode) {
|
||||
$message .= "Activez le mode tortue.";
|
||||
} else {
|
||||
$message .= "Activez le mode ultra-lent ou le mode tortue.";
|
||||
}
|
||||
|
||||
$results[$index] = [
|
||||
'status' => 'warning',
|
||||
'message' => $message,
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorMessage,
|
||||
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
|
||||
];
|
||||
} else {
|
||||
$results[$index] = [
|
||||
'status' => 'error',
|
||||
'message' => "Erreur lors de l'ajout aux favoris: $errorMessage",
|
||||
'error_type' => $errorType,
|
||||
'details' => $errorCode ? "Code: $errorCode" : "",
|
||||
'misskey_id' => $noteId // AMÉLIORATION 3: Inclure l'ID pour mise à jour du cache
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AMÉLIORATION 2: Pas besoin de pause pour les notes déjà dans les favoris ou si nous avons utilisé l'ID en cache
|
||||
// Nous vérifions si l'opération actuelle nécessite un délai
|
||||
$requiresDelay = true;
|
||||
|
||||
// Pas de délai nécessaire si c'est déjà en favoris
|
||||
if ($results[$index]['status'] === 'info' && $results[$index]['error_type'] === 'already_favorited') {
|
||||
$requiresDelay = false;
|
||||
}
|
||||
|
||||
// Pas de délai nécessaire si nous avons utilisé un ID en cache et l'opération a réussi
|
||||
if ($cachedMisskeyId && ($results[$index]['status'] === 'success' ||
|
||||
($results[$index]['status'] === 'info' && $results[$index]['error_type'] === 'already_favorited'))) {
|
||||
$requiresDelay = false;
|
||||
}
|
||||
|
||||
// Pour les notes qui requièrent vraiment un délai (opérations lourdes d'API)
|
||||
if ($requiresDelay) {
|
||||
// En mode tortue ou lent, appliquer un délai réduit pour les notes où nous avons fait moins de requêtes API
|
||||
if (($tortoiseMode || $slowMode) && !$searchNeeded) {
|
||||
// Si nous avons sauté la recherche fédérée, utiliser seulement 10% du délai complet
|
||||
$reducedDelay = floor($delayMs * 0.1);
|
||||
error_log("Pause réduite de {$reducedDelay}ms pour note avec ID connu: $url");
|
||||
usleep($reducedDelay * 1000);
|
||||
} else {
|
||||
// Délai complet pour les requêtes complètes qui nécessitent une recherche fédérée
|
||||
error_log("Pause complète de {$delayMs}ms après traitement complet de: $url");
|
||||
usleep($delayMs * 1000);
|
||||
}
|
||||
} else {
|
||||
error_log("Aucune pause nécessaire pour la note: $url");
|
||||
}
|
||||
}
|
||||
|
||||
// Si au moins une erreur de rate limit a été détectée, suggérer le mode lent
|
||||
// AMÉLIORATION 2: Vérifier si toutes les notes sont déjà favorites
|
||||
$allAlreadyFavorited = true;
|
||||
foreach ($results as $result) {
|
||||
if (!($result['status'] === 'info' && $result['error_type'] === 'already_favorited')) {
|
||||
$allAlreadyFavorited = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Si au moins une erreur de rate limit a été détectée, suggérer le mode approprié
|
||||
$hasRateLimitErrors = false;
|
||||
foreach ($results as $result) {
|
||||
if (isset($result['error_type']) && $result['error_type'] === 'rate_limit') {
|
||||
@ -217,6 +366,14 @@ try {
|
||||
}
|
||||
}
|
||||
|
||||
// Déterminer les suggestions à faire à l'utilisateur
|
||||
$suggestions = [
|
||||
'use_slow_mode' => $hasRateLimitErrors && !$slowMode && !$tortoiseMode,
|
||||
'use_tortoise_mode' => $hasRateLimitErrors && $slowMode && !$tortoiseMode,
|
||||
'increase_delay' => $hasRateLimitErrors && ($slowMode || $tortoiseMode) && $delaySeconds < ($tortoiseMode ? 300 : 60),
|
||||
'all_already_favorited' => $allAlreadyFavorited // AMÉLIORATION 2: Indiquer si toutes les notes sont déjà favorites
|
||||
];
|
||||
|
||||
// Renvoyer les résultats
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
@ -226,8 +383,12 @@ try {
|
||||
'total' => $totalItems,
|
||||
'percentage' => round((($currentIndex + count($batch)) / $totalItems) * 100, 2)
|
||||
],
|
||||
'suggestions' => [
|
||||
'use_slow_mode' => $hasRateLimitErrors && !$slowMode
|
||||
'suggestions' => $suggestions,
|
||||
'config' => [
|
||||
'slow_mode' => $slowMode,
|
||||
'tortoise_mode' => $tortoiseMode,
|
||||
'delay_seconds' => $delaySeconds,
|
||||
'all_already_favorited' => $allAlreadyFavorited // AMÉLIORATION 2: Indiquer dans la config si tout est déjà traité
|
||||
]
|
||||
]);
|
||||
|
||||
|
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