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; +}