mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-14 02:37:20 +02:00
Logos absents : l'hébergeur des picons du fournisseur répond 503 sur tout, y compris sa racine (serveur en maintenance). Le proxy relayait ce 503 au navigateur, qui redemandait chaque logo à chaque affichage de la grille — des centaines de requêtes sortantes mortes par seconde. - Un statut >= 400 sur une image rend désormais un pixel transparent avec `Cache-Control: max-age=600`, au lieu de propager l'erreur - Après trois échecs consécutifs, l'hôte est marqué hors service cinq minutes : plus aucun appel sortant, réponse servie localement - Le corps d'erreur amont est drainé, sinon la connexion restait ouverte jusqu'au timeout Guide TV très lent (4 à 8 s par chaîne dans les logs) : `player_api` demande un appel par chaîne, et le panneau met ~4 s à répondre — une grille de trente chaînes dépassait la minute. - Le dump `xmltv.php` du panneau passe en premier : une requête couvre toutes les chaînes, réutilisée trois heures - L'interrogation chaîne par chaîne reste le repli, puis la source XMLTV externe - Une source XMLTV en échec n'est plus retéléchargée à chaque consultation : quinze minutes de recul - Index borné à 48 h en avant : un dump national couvre sept jours pour un millier de chaînes, soit des centaines de Mo pour un guide qui n'affiche que le programme courant Lecture saccadée : le relais FFmpeg -> navigateur de `/turbo.ts` ne transmettait pas la pause du client à la source. Un lecteur plus lent que le flux laissait FFmpeg produire à pleine vitesse et le serveur empilait les paquets en mémoire, d'où la dérive derrière le direct. La contre-pression est maintenant propagée (`pipeWithBackpressure`). Le serveur tourne sur un seul isolate : le flot de requêtes mortes et les attentes EPG de plusieurs secondes partageaient la boucle d'événements avec les paquets vidéo. Tests ajoutés : cache d'échecs par hôte, contre-pression du relais, priorité des sources EPG, recul après échec XMLTV, horizon d'index. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
168 lines
5.2 KiB
Dart
168 lines
5.2 KiB
Dart
import 'package:http/http.dart' as http;
|
||
import 'package:http/testing.dart';
|
||
import 'package:test/test.dart';
|
||
import '../services/xmltv_epg_service.dart';
|
||
|
||
/// Extrait représentatif d'un dump XMLTV public : décalage horaire explicite,
|
||
/// identifiant ponctué côté source là où le panneau annonce `France2.fr`, et
|
||
/// un `display-name` distinct de l'identifiant.
|
||
String _fixture(DateTime start, DateTime stop) {
|
||
String stamp(DateTime d) {
|
||
final u = d.toUtc();
|
||
String two(int v) => v.toString().padLeft(2, '0');
|
||
return '${u.year}${two(u.month)}${two(u.day)}'
|
||
'${two(u.hour)}${two(u.minute)}${two(u.second)} +0000';
|
||
}
|
||
|
||
return '''
|
||
<?xml version="1.0" encoding="utf-8" ?>
|
||
<tv>
|
||
<channel id="France.2.fr"><display-name>FR - FRANCE 2</display-name></channel>
|
||
<programme start="${stamp(start)}" stop="${stamp(stop)}" channel="France.2.fr">
|
||
<title lang="fr">Journal de 20h</title>
|
||
<desc lang="fr">L'info du soir</desc>
|
||
</programme>
|
||
</tv>
|
||
''';
|
||
}
|
||
|
||
void main() {
|
||
final service = XmltvEpgService(sourceUrls: const []);
|
||
|
||
group('normalizeKey', () {
|
||
test('rapproche les identifiants ponctués différemment', () {
|
||
// Le cœur du repli : sans cette normalisation, `France2.fr` du panneau
|
||
// et `France.2.fr` du dump ne se rencontrent jamais.
|
||
expect(
|
||
XmltvEpgService.normalizeKey('France2.fr'),
|
||
XmltvEpgService.normalizeKey('France.2.fr'),
|
||
);
|
||
});
|
||
|
||
test('replie la casse et les accents', () {
|
||
expect(XmltvEpgService.normalizeKey('Chérie 25'), 'cherie25');
|
||
});
|
||
|
||
test('rend une clé vide sur une entrée nulle ou sans caractère utile', () {
|
||
expect(XmltvEpgService.normalizeKey(null), '');
|
||
expect(XmltvEpgService.normalizeKey('...'), '');
|
||
});
|
||
});
|
||
|
||
group('parseXmltvDate', () {
|
||
test('applique le décalage horaire annoncé', () {
|
||
expect(
|
||
XmltvEpgService.parseXmltvDate('20260811200000 +0200'),
|
||
DateTime.utc(2026, 8, 11, 18),
|
||
);
|
||
});
|
||
|
||
test('traite une date sans décalage comme de l UTC', () {
|
||
expect(
|
||
XmltvEpgService.parseXmltvDate('20260811200000'),
|
||
DateTime.utc(2026, 8, 11, 20),
|
||
);
|
||
});
|
||
|
||
test('rejette une valeur tronquée', () {
|
||
expect(XmltvEpgService.parseXmltvDate('202608'), isNull);
|
||
});
|
||
});
|
||
|
||
group('parse', () {
|
||
test('indexe un programme en cours et décode son titre', () {
|
||
final now = DateTime.now().toUtc();
|
||
final index = service.parseForTest(
|
||
_fixture(
|
||
now.subtract(const Duration(minutes: 10)),
|
||
now.add(const Duration(minutes: 20)),
|
||
),
|
||
);
|
||
|
||
final programmes = index[XmltvEpgService.normalizeKey('France2.fr')];
|
||
expect(programmes, isNotNull);
|
||
expect(programmes!.single.title, 'Journal de 20h');
|
||
expect(programmes.single.description, "L'info du soir");
|
||
});
|
||
|
||
test('rend la chaîne atteignable par son nom affiché', () {
|
||
final now = DateTime.now().toUtc();
|
||
final index = service.parseForTest(
|
||
_fixture(now, now.add(const Duration(minutes: 20))),
|
||
);
|
||
|
||
expect(index[XmltvEpgService.normalizeKey('FR - FRANCE 2')], isNotNull);
|
||
});
|
||
|
||
test('écarte les programmes terminés hors fenêtre de rétention', () {
|
||
final old = DateTime.now().toUtc().subtract(const Duration(days: 2));
|
||
final index = service.parseForTest(
|
||
_fixture(old, old.add(const Duration(minutes: 30))),
|
||
);
|
||
|
||
expect(index, isEmpty);
|
||
});
|
||
});
|
||
|
||
group('ensureFresh', () {
|
||
test('ne retélécharge pas tant que l’index est frais', () async {
|
||
final now = DateTime.now().toUtc();
|
||
var calls = 0;
|
||
final service = XmltvEpgService(
|
||
sourceUrls: const ['http://dump.example/epg.xml'],
|
||
client: MockClient((_) async {
|
||
calls++;
|
||
return http.Response(
|
||
_fixture(now, now.add(const Duration(minutes: 30))),
|
||
200,
|
||
);
|
||
}),
|
||
);
|
||
|
||
await service.ensureFresh();
|
||
await service.ensureFresh();
|
||
|
||
expect(calls, 1);
|
||
expect(service.hasData, isTrue);
|
||
});
|
||
|
||
test('espace les tentatives après un échec', () async {
|
||
// Sans ce recul, une source morte était retéléchargée à chaque
|
||
// consultation du guide : une requête sortante par chaîne affichée.
|
||
var calls = 0;
|
||
final service = XmltvEpgService(
|
||
sourceUrls: const ['http://dump.example/epg.xml'],
|
||
retryBackoff: const Duration(minutes: 15),
|
||
client: MockClient((_) async {
|
||
calls++;
|
||
return http.Response('nope', 500);
|
||
}),
|
||
);
|
||
|
||
await service.ensureFresh();
|
||
await service.ensureFresh();
|
||
await service.ensureFresh();
|
||
|
||
expect(calls, 1);
|
||
expect(service.hasData, isFalse);
|
||
});
|
||
});
|
||
|
||
group('horizon', () {
|
||
test('écarte les programmes au-delà de l’horizon d’indexation', () {
|
||
// Un dump national couvre sept jours : tout garder ferait grossir
|
||
// l'index pour un guide qui n'affiche que le programme courant.
|
||
final service = XmltvEpgService(
|
||
sourceUrls: const [],
|
||
horizon: const Duration(hours: 48),
|
||
);
|
||
final far = DateTime.now().toUtc().add(const Duration(days: 5));
|
||
|
||
expect(
|
||
service.parseForTest(_fixture(far, far.add(const Duration(hours: 1)))),
|
||
isEmpty,
|
||
);
|
||
});
|
||
});
|
||
}
|