From 5ab7203c8d780ed08c9a9359a383924c7462aca8 Mon Sep 17 00:00:00 2001 From: R0m1k3 Date: Tue, 11 Aug 2026 11:17:55 +0200 Subject: [PATCH] =?UTF-8?q?fix(epg):=20cache=20client=20du=20guide=20et=20?= =?UTF-8?q?plafond=20de=20d=C3=A9bit=20relev=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les journaux du serveur montrent la grille Live TV se faire couper en 429 : "Rate limit exceeded", une trentaine de requêtes refusées d'affilée après quelques écrans de défilement. Deux causes cumulées. D'une part le passage du guide de Dio vers AuthedHttp a fait perdre le cache de réponses qui existait auparavant : chaque reconstruction de tuile repartait en requête. Un cache mémoire de dix minutes le rétablit — le backend garde déjà ces réponses trente minutes, il n'y a aucune raison d'aller les rechercher. D'autre part le plafond de deux cents requêtes par minute et par adresse était trop bas pour l'usage réel : une grille demande un logo et un guide par tuile, et tous les clients partagent l'adresse du reverse proxy. Il passe à six cents. Co-Authored-By: Claude Opus 5 --- bin/middleware/security_middleware.dart | 6 ++++- .../iptv/services/xtream_service.dart | 27 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/bin/middleware/security_middleware.dart b/bin/middleware/security_middleware.dart index 259d9e4..d896d80 100644 --- a/bin/middleware/security_middleware.dart +++ b/bin/middleware/security_middleware.dart @@ -89,7 +89,11 @@ Middleware honeypotMiddleware() { /// 3. Rate Limit Middleware (In-Memory) /// Limits requests per IP address. /// Default: 200 requests per minute per IP. -Middleware rateLimitMiddleware({int requestsPerMinute = 200}) { +/// 600 plutôt que 200 : une grille de chaînes déclenche une requête de +/// logo et une de guide par tuile, et tous les clients partagent l'IP du +/// reverse proxy. Le plafond précédent coupait la grille en 429 au bout de +/// quelques écrans de défilement. +Middleware rateLimitMiddleware({int requestsPerMinute = 600}) { final clientRequests = >{}; // Cleanup timer to remove old entries and prevent memory leaks diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 358153e..e15db9a 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -47,6 +47,16 @@ class XtreamService { /// On sérialise donc par petits paquets. static final _Semaphore _epgGate = _Semaphore(3); + /// Cache mémoire du guide, partagé par toutes les instances. + /// + /// Une grille de chaînes reconstruit ses tuiles à chaque défilement et + /// redemandait le guide à chaque fois. Sur une playlist de 500 chaînes, le + /// limiteur de débit du backend (200 requêtes par minute et par IP) coupait + /// la grille en 429 au bout de quelques écrans. Le backend garde déjà ces + /// réponses 30 minutes ; ce cache évite d'aller les chercher. + static final Map _epgCache = {}; + static const _epgCacheTtl = Duration(minutes: 10); + late final Dio _dio; late final CacheOptions _cacheOptions; @@ -726,6 +736,11 @@ class XtreamService { await Future.delayed(_throttleDelay); } + final cached = _epgCache[streamId]; + if (cached != null && DateTime.now().isBefore(cached.expiresAt)) { + return cached.programmes; + } + await _epgGate.acquire(); try { final response = await AuthedHttp.get(Uri.parse('/api/epg/$streamId')); @@ -750,6 +765,11 @@ class XtreamService { result.add(Map.from(item)); if (result.length >= 8) break; } + + _epgCache[streamId] = _EpgCacheEntry( + programmes: result, + expiresAt: DateTime.now().add(_epgCacheTtl), + ); return result; } catch (e) { // L'EPG reste optionnel : une panne ne doit pas casser la grille. @@ -765,3 +785,10 @@ class XtreamService { _dio.close(); } } + +class _EpgCacheEntry { + const _EpgCacheEntry({required this.programmes, required this.expiresAt}); + + final List> programmes; + final DateTime expiresAt; +}