mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-18 20:59:59 +02:00
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:
1 parent
bde32adda9
commit
9843999809
13 files changed
+722
-87
No files matched your search
@@ -0,0 +1,20 @@
|
||||
/// URL du logo d'une chaîne, servie par le backend (`/api/logo`).
|
||||
///
|
||||
/// Le backend tente d'abord [streamIcon] (l'URL fournie par le panneau), puis
|
||||
/// cherche un logo par [name] quand l'hébergeur de picons du revendeur ne
|
||||
/// répond pas. Le nom est donc transmis même sans `stream_icon` : c'est
|
||||
/// souvent le seul moyen d'obtenir un logo.
|
||||
///
|
||||
/// Une chaîne sans logo trouvé reçoit un statut d'erreur : l'`errorBuilder`
|
||||
/// de l'`Image.network` appelante affiche alors son propre repli.
|
||||
String channelLogoUrl({required String streamIcon, required String name}) {
|
||||
final icon = streamIcon.trim();
|
||||
return Uri(
|
||||
path: '/api/logo',
|
||||
queryParameters: {
|
||||
if (icon.startsWith('http://') || icon.startsWith('https://'))
|
||||
'src': icon,
|
||||
'name': name,
|
||||
},
|
||||
).toString();
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -5,6 +5,7 @@ import '../../../../features/iptv/providers/xtream_provider.dart';
|
||||
import '../../../../features/iptv/providers/settings_provider.dart';
|
||||
import '../../../../features/iptv/providers/favorites_provider.dart';
|
||||
import '../screens/mobile_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';
|
||||
@@ -349,10 +350,10 @@ class _MobileChannelTile extends ConsumerWidget {
|
||||
// Sans ce bouton, aucun moyen d'ajouter un favori : le filtre « Favoris »
|
||||
// de l'en-tête affichait toujours une liste vide.
|
||||
final isFav = ref.watch(favoritesProvider).contains(channel.streamId);
|
||||
final iconUrl =
|
||||
channel.streamIcon.isNotEmpty && channel.streamIcon.startsWith('http')
|
||||
? '/api/xtream/${channel.streamIcon}'
|
||||
: null;
|
||||
final iconUrl = channelLogoUrl(
|
||||
streamIcon: channel.streamIcon,
|
||||
name: channel.name,
|
||||
);
|
||||
|
||||
return GlassContainer.glass(
|
||||
borderRadius: 12,
|
||||
@@ -372,16 +373,14 @@ class _MobileChannelTile extends ConsumerWidget {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: iconUrl != null
|
||||
? Image.network(
|
||||
iconUrl,
|
||||
fit: BoxFit.contain,
|
||||
cacheWidth: 120,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.tv, color: AppColors.onSurface24),
|
||||
)
|
||||
: const Icon(Icons.tv, color: AppColors.onSurface24),
|
||||
child: Image.network(
|
||||
iconUrl,
|
||||
fit: BoxFit.contain,
|
||||
cacheWidth: 120,
|
||||
filterQuality: FilterQuality.low,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.tv, color: AppColors.onSurface24),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
|
||||
Reference in new issue
Block a user