Files
MichaelandClaude Opus 5 9843999809 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>
2026-09-19 13:39:01 +02:00

73 lines
2.3 KiB
Dart

import 'package:shelf/shelf.dart';
import '../services/logo_catalog.dart';
import 'asset_failure_cache.dart';
/// GET /api/logo?src=<stream_icon>&name=<nom de chaîne>
///
/// Logo d'une chaîne, avec repli. L'URL fournie par le panneau est tentée
/// d'abord, via le proxy Xtream (mêmes protections anti-SSRF, même mémoire
/// des hôtes morts). En cas d'échec, le logo est cherché par nom dans le
/// dépôt public `tv-logos`.
///
/// POURQUOI : quand l'hébergeur de picons du revendeur tombe, toute la grille
/// perd ses logos d'un coup, alors que les autres applications IPTV les
/// affichent encore grâce à leurs propres sources de repli.
class LogoApi {
LogoApi(this._proxy, this._catalog);
/// Proxy Xtream (`ProxyHandler.handler`), qui attend `/api/xtream/<url>`.
final Handler _proxy;
final LogoCatalog _catalog;
/// Un logo trouvé change rarement : un jour de cache navigateur.
static const _found = 'public, max-age=86400';
Future<Response> handle(Request request) async {
final src = request.url.queryParameters['src'] ?? '';
final name = request.url.queryParameters['name'] ?? '';
// Une URL panneau malformée ne doit pas priver la chaîne du repli.
final proxied = src.startsWith('http://') || src.startsWith('https://')
? Uri.tryParse('${request.requestedUri.origin}/api/xtream/$src')
: null;
if (proxied != null) {
final upstream = await _proxy(
Request(
'GET',
proxied,
headers: {
for (final header in const ['cookie', 'authorization'])
if (request.headers[header] != null)
header: request.headers[header]!,
},
),
);
final type = upstream.headers['content-type'] ?? '';
if (upstream.statusCode == 200 && type.startsWith('image/')) {
return Response.ok(
upstream.read(),
headers: {'content-type': type, 'cache-control': _found},
);
}
// Corps d'erreur consommé : sinon la connexion amont reste ouverte.
await upstream.read().drain<void>();
}
if (name.isNotEmpty) {
final bytes = await _catalog.logoFor(name);
if (bytes != null) {
return Response.ok(
bytes,
headers: {'content-type': 'image/png', 'cache-control': _found},
);
}
}
return missingImageResponse();
}
}