mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-12 01:36:20 +02:00
L'hébergeur des picons du fournisseur (51.158.145.100) répond toujours 503 sur toutes ses URL. Couper le flot de requêtes ne suffisait pas : la grille restait sans logo. Pire, le pixel transparent servi en 200 était pris pour une image valide par Flutter, qui n'affichait plus son icône de repli — d'où des tuiles entièrement vides. - Nouvelle route `/api/logo?src=…&name=…` : URL du panneau d'abord (via le proxy, mêmes protections anti-SSRF et même mémoire des hôtes morts), puis recherche par nom dans le dépôt public tv-logo/tv-logos - Appariement par nom : préfixe pays et suffixes de qualité ignorés, `+` → `plus`, variante plus longue (`bein-sports-1-french`), puis marque sans numéro pour les canaux événementiels. Aucune approximation sur un mot seul - Liste de logos en cache 24 h (API GitHub : 60 requêtes/h non authentifiées), 15 min de recul après échec, images en mémoire - Image indisponible : 410 mis en cache au lieu du pixel. 410 et non 404 : la Cascade du serveur rattrape les 404 et renvoyait celui du handler statique, sans cache-control - Client (grille bureau, mobile, enregistrements) : logo toujours demandé au backend, nom compris, même sans stream_icon. L'onglet Enregistrements chargeait l'URL http brute, bloquée en HTTPS Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.9 KiB
Dart
82 lines
2.9 KiB
Dart
import 'package:shelf/shelf.dart';
|
|
|
|
/// Réponse pour une image indisponible.
|
|
///
|
|
/// Statut 410 et non 404 : la `Cascade` du serveur retombe sur le handler
|
|
/// suivant pour tout 404 et renvoie finalement celui du handler statique, sans
|
|
/// notre `cache-control`. Un 410 s'arrête ici.
|
|
///
|
|
/// Un statut d'erreur plutôt qu'une image vide : le client affiche alors son
|
|
/// propre repli (icône, initiale) au lieu d'une tuile blanche. Le `max-age`
|
|
/// est essentiel : sans lui le navigateur redemande le logo à chaque
|
|
/// affichage de la grille et le flot de requêtes mortes reprend aussitôt.
|
|
Response missingImageResponse() => Response(
|
|
410,
|
|
headers: {
|
|
'cache-control': 'public, max-age=600',
|
|
'access-control-allow-origin': '*',
|
|
},
|
|
);
|
|
|
|
/// Mémoire courte des hôtes d'images injoignables.
|
|
///
|
|
/// POURQUOI : les URL de picons pointent chez l'hébergeur du revendeur, pas
|
|
/// sur le panneau. Quand cette machine tombe (maintenance, DNS, quota), elle
|
|
/// répond 5xx en quelques millisecondes, le navigateur réessaie à chaque
|
|
/// rendu de la grille et le proxy relaie des centaines de requêtes mortes par
|
|
/// seconde. Le serveur tournant sur un unique isolate Dart, ce flot passe
|
|
/// devant les paquets vidéo dans la boucle d'événements : l'image saccade
|
|
/// pendant qu'on s'acharne sur un hôte qu'on sait hors service.
|
|
///
|
|
/// On coupe donc court : au bout de [threshold] échecs consécutifs, l'hôte
|
|
/// est considéré mort pendant [ttl] et les requêtes suivantes sont servies
|
|
/// localement, sans appel sortant.
|
|
class AssetFailureCache {
|
|
AssetFailureCache({
|
|
this.threshold = 3,
|
|
this.ttl = const Duration(minutes: 5),
|
|
DateTime Function()? clock,
|
|
}) : _clock = clock ?? DateTime.now;
|
|
|
|
/// Nombre d'échecs consécutifs avant de couper un hôte. Un 5xx passager ne
|
|
/// doit pas priver l'utilisateur de ses logos.
|
|
final int threshold;
|
|
|
|
/// Durée de la coupure. Assez courte pour qu'un hôte réparé revienne seul.
|
|
final Duration ttl;
|
|
|
|
final DateTime Function() _clock;
|
|
|
|
final Map<String, int> _failures = {};
|
|
final Map<String, DateTime> _downUntil = {};
|
|
|
|
/// L'hôte est-il réputé hors service en ce moment ?
|
|
bool isDown(String host) {
|
|
final key = host.toLowerCase();
|
|
final until = _downUntil[key];
|
|
if (until == null) return false;
|
|
if (_clock().isBefore(until)) return true;
|
|
|
|
// Coupure expirée : on repart d'une ardoise vierge pour laisser une
|
|
// vraie chance au prochain appel.
|
|
_downUntil.remove(key);
|
|
_failures.remove(key);
|
|
return false;
|
|
}
|
|
|
|
void recordFailure(String host) {
|
|
final key = host.toLowerCase();
|
|
final count = (_failures[key] ?? 0) + 1;
|
|
_failures[key] = count;
|
|
if (count >= threshold) {
|
|
_downUntil[key] = _clock().add(ttl);
|
|
}
|
|
}
|
|
|
|
void recordSuccess(String host) {
|
|
final key = host.toLowerCase();
|
|
_failures.remove(key);
|
|
_downUntil.remove(key);
|
|
}
|
|
}
|