Logos des chaînes : repli par nom quand l'hébergeur de picons tombe

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>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-09-19 13:39:01 +02:00
1 parent bde32adda9
commit 9843999809
13 files changed
+722 -87

No files matched your search

+21 -27
View File
@@ -5,6 +5,7 @@ import '../providers/xtream_provider.dart';
import '../providers/settings_provider.dart';
import '../providers/favorites_provider.dart';
import '../screens/player_screen.dart';
import '../../../core/api/channel_logo.dart';
import '../../../core/models/iptv_models.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart';
@@ -412,25 +413,26 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
),
padding: const EdgeInsets.all(16),
child: Center(
child: channel.streamIcon.isNotEmpty
? Image.network(
_getProxiedIconUrl(channel.streamIcon)!,
cacheWidth: 160,
filterQuality: FilterQuality.low,
errorBuilder: (_, __, ___) => Icon(
Icons.tv,
color: AppColors.onSurface.withOpacity(0.3),
size: 40,
),
)
: Text(
channel.name.characters.first.toUpperCase(),
style: GoogleFonts.fraunces(
fontSize: 40,
fontWeight: FontWeight.bold,
color: AppColors.onSurface24,
),
),
// Toujours demandé au backend, même sans
// stream_icon : il cherche alors un logo par nom.
child: Image.network(
channelLogoUrl(
streamIcon: channel.streamIcon,
name: channel.name,
),
cacheWidth: 160,
filterQuality: FilterQuality.low,
errorBuilder: (_, __, ___) => Text(
channel.name.isEmpty
? '?'
: channel.name.characters.first.toUpperCase(),
style: GoogleFonts.fraunces(
fontSize: 40,
fontWeight: FontWeight.bold,
color: AppColors.onSurface24,
),
),
),
),
),
),
@@ -599,14 +601,6 @@ class _LiveTVTabState extends ConsumerState<LiveTVTab>
);
}
String? _getProxiedIconUrl(String originalUrl) {
if (originalUrl.isEmpty) return null;
if (originalUrl.startsWith('http://')) {
return '/api/xtream/$originalUrl';
}
return originalUrl;
}
void _playChannel(Channel channel, List<Channel> contextList) {
Navigator.of(context).push(
MaterialPageRoute(
+20 -19
View File
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import '../../../core/api/authed_http.dart';
import '../../../core/api/recording_requests.dart';
import '../../../core/api/channel_logo.dart';
import '../../../core/models/iptv_models.dart';
import '../../../core/models/playlist_config.dart';
import '../../../core/theme/app_colors.dart';
@@ -368,25 +369,25 @@ class _EpgGuideViewState extends ConsumerState<_EpgGuideView>
),
child: Row(
children: [
ch.streamIcon.isNotEmpty
? Image.network(
ch.streamIcon,
width: 24,
height: 16,
cacheWidth: 64,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) =>
const Icon(
Icons.tv,
color: AppColors.onSurface24,
size: 16,
),
)
: const Icon(
Icons.tv,
color: AppColors.onSurface24,
size: 16,
),
// URL panneau brute auparavant : en
// http depuis une page https, le
// navigateur la bloquait.
Image.network(
channelLogoUrl(
streamIcon: ch.streamIcon,
name: ch.name,
),
width: 24,
height: 16,
cacheWidth: 64,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) =>
const Icon(
Icons.tv,
color: AppColors.onSurface24,
size: 16,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(