From 51e6d9d69318026aa4f7696ff183a07a10959e86 Mon Sep 17 00:00:00 2001 From: Michael Date: Mon, 10 Aug 2026 22:14:47 +0200 Subject: [PATCH] fix(epg): stop swallowing EPG failures, cap gateway concurrency getShortEpg ended in `catch (e) { return []; }`, so a 401, a timeout, a rate-limited panel and a genuinely empty guide all rendered the same "No Info" label. That made the current problem undiagnosable from the UI. Failures are now logged with the HTTP status, the Dio error type and the response shape. Two concrete causes are handled while we are here: - Some Xtream panels answer player_api.php with a textual Content-Type. Dio then hands back a raw String, and `response.data['epg_listings']` threw, landing in the silent catch. The body is now parsed explicitly when it arrives as a String. - The channel grid mounts one EPG provider per visible tile, firing ~32 concurrent requests at player_api.php. Many panels rate-limit well below that and fail the whole batch. A 3-slot semaphore serialises them in small groups. This does not by itself prove why the guide is empty in production; it makes the reason visible in the browser console. Co-Authored-By: Claude Opus 5 --- .../iptv/services/xtream_service.dart | 85 +++++++++++++++++-- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 16eb1c8..d3aa05e 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:html' as html; import 'package:dio/dio.dart'; @@ -7,10 +8,43 @@ import '../../../core/models/playlist_config.dart'; import '../../../core/models/iptv_models.dart'; import '../models/xtream_models.dart' as xm; +/// Limiteur de concurrence minimal (pas de dépendance supplémentaire). +class _Semaphore { + _Semaphore(this.max); + + final int max; + int _current = 0; + final List> _waiters = []; + + Future acquire() { + if (_current < max) { + _current++; + return Future.value(); + } + final completer = Completer(); + _waiters.add(completer); + return completer.future; + } + + void release() { + if (_waiters.isNotEmpty) { + _waiters.removeAt(0).complete(); + } else { + _current--; + } + } +} + /// Xtream Codes API Service /// /// Handles all communication with Xtream API servers class XtreamService { + /// Une grille de chaînes déclenche un appel EPG par tuile visible — une + /// trentaine de requêtes simultanées vers `player_api.php`. Beaucoup de + /// panneaux Xtream limitent le débit et répondent en erreur au-delà de + /// quelques connexions parallèles, ce qui vide l'EPG de toute la grille. + /// On sérialise donc par petits paquets. + static final _Semaphore _epgGate = _Semaphore(3); late final Dio _dio; late final CacheOptions _cacheOptions; @@ -635,6 +669,7 @@ class XtreamService { await Future.delayed(_throttleDelay); } + await _epgGate.acquire(); try { final response = await _apiGet( { @@ -650,18 +685,58 @@ class XtreamService { ), ); - if (response.data == null || response.data['epg_listings'] == null) { + var data = response.data; + + // Certains panneaux renvoient du JSON avec un Content-Type textuel : + // Dio livre alors une String brute et l'accès par clé échouait + // silencieusement, aboutissant au même « No Info » qu'une vraie panne. + if (data is String) { + if (data.trim().isEmpty) { + debugPrint('[XtreamService] EPG $streamId : réponse vide'); + return []; + } + try { + data = json.decode(data); + } catch (_) { + debugPrint( + '[XtreamService] EPG $streamId : réponse non-JSON ' + '(${response.headers.value('content-type')}) — ' + '${data.length > 120 ? '${data.substring(0, 120)}…' : data}', + ); + return []; + } + } + + if (data == null) { + debugPrint('[XtreamService] EPG $streamId : corps nul'); return []; } - final List epgData = - response.data['epg_listings'] as List; + if (data is! Map || data['epg_listings'] == null) { + debugPrint( + '[XtreamService] EPG $streamId : pas de champ epg_listings ' + '(clés: ${data is Map ? data.keys.toList() : data.runtimeType})', + ); + return []; + } + + final List epgData = data['epg_listings'] as List; return epgData .map((entry) => EpgEntry.fromJson(entry as Map)) .toList(); - } catch (e) { - // EPG is optional, don't throw on failure + } on DioException catch (e) { + // L'EPG reste optionnel — mais avaler l'erreur en silence rendait toute + // panne indiscernable d'une grille réellement vide. + debugPrint( + '[XtreamService] EPG $streamId en échec : ' + 'HTTP ${e.response?.statusCode} ${e.type} — ${e.message}', + ); return []; + } catch (e) { + debugPrint('[XtreamService] EPG $streamId en échec : $e'); + return []; + } finally { + _epgGate.release(); } }