fix(epg): cache client du guide et plafond de débit relevé

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 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-11 11:17:55 +02:00
1 parent de49e8b34d
commit 5ab7203c8d
2 files changed
+32 -1

No files matched your search

+5 -1
View File
@@ -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 = <String, List<DateTime>>{};
// Cleanup timer to remove old entries and prevent memory leaks
@@ -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<String, _EpgCacheEntry> _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<String, dynamic>.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<Map<String, dynamic>> programmes;
final DateTime expiresAt;
}