mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-17 04:09:03 +02:00
Guide TV : plus jamais de 500, et bien plus de chaînes couvertes
Constaté en prod : chaque /api/epg répondait 500 en 4 s. Un appel player_api qui lève (connexion coupée, délai) n'était pas rattrapé, et le dump XMLTV externe, qui couvre pourtant TF1, France 2, M6…, n'était jamais consulté. Et 59 % des chaînes françaises n'ont pas d'epg_channel_id : leur nom brut (« FR - TF1 FHD ◉ ») ne correspondait à aucune entrée XMLTV. - échec player_api rattrapé : la chaîne passe à la source suivante - dump externe (déjà en mémoire) avant l'appel lent chaîne par chaîne - correspondance par nom nettoyé : préfixe pays, FHD/HD/UHD/4K/HEVC…, symboles retirés, forme « nom.pays » tentée ; « +1 » conservé - tous les display-name XMLTV indexés, plus seulement le premier - table des chaînes : un seul téléchargement partagé, décodé hors de la boucle principale, et un échec n'est plus gardé 6 h (2 min) - guide vide gardé 5 min au lieu de 30 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8d79472ca8
commit
abe7d522c1
4 files changed
+336
-33
No files matched your search
@@ -217,5 +217,111 @@ void main() {
|
||||
expect(response.headers['X-Epg-Source'], 'xtream');
|
||||
expect(actions, contains('get_simple_data_table'));
|
||||
});
|
||||
|
||||
// Dump externe couvrant TF1, et panneau dont le xmltv.php est vide.
|
||||
String externalDump() {
|
||||
final now = DateTime.now().toUtc();
|
||||
return xmltvDump(now, now.add(const Duration(hours: 1)))
|
||||
.replaceAll('France2.fr', 'TF1.fr')
|
||||
.replaceAll('FRANCE 2', 'TF1');
|
||||
}
|
||||
|
||||
EpgApi apiWith(http.Client client) => EpgApi(
|
||||
(_) async => playlist,
|
||||
httpClient: client,
|
||||
xmltv: XmltvEpgService(
|
||||
sourceUrls: const ['http://ext.example/epg.xml'],
|
||||
client: client,
|
||||
),
|
||||
panelXmltvBuilder: (config) => XmltvEpgService(
|
||||
sourceUrls: ['${config.dns}/xmltv.php'],
|
||||
client: client,
|
||||
),
|
||||
);
|
||||
|
||||
test('un panneau qui plante ne prive plus la chaîne du dump externe',
|
||||
() async {
|
||||
// Constaté en prod : chaque /api/epg répondait 500 en 4 s, le guide
|
||||
// externe (qui couvrait pourtant la chaîne) n'étant jamais consulté.
|
||||
final client = MockClient((request) async {
|
||||
final url = request.url;
|
||||
if (url.host == 'ext.example') return http.Response(externalDump(), 200);
|
||||
if (url.path.endsWith('/xmltv.php')) return http.Response('<tv></tv>', 200);
|
||||
final action = url.queryParameters['action'] ?? '';
|
||||
if (action == 'get_live_streams') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'stream_id': 1, 'name': 'FR - TF1 FHD ◉', 'epg_channel_id': ''},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
throw http.ClientException('Connection closed', url);
|
||||
});
|
||||
|
||||
final response = await apiWith(client).handleGetEpg(requestFor('1'), '1');
|
||||
final body = jsonDecode(await response.readAsString()) as Map;
|
||||
|
||||
expect(response.statusCode, 200);
|
||||
expect(response.headers['X-Epg-Source'], 'xmltv');
|
||||
expect((body['programmes'] as List).single['title'], 'Journal de 20h');
|
||||
});
|
||||
|
||||
test('le dump externe passe avant l\'appel lent chaîne par chaîne',
|
||||
() async {
|
||||
final actions = <String>[];
|
||||
final client = MockClient((request) async {
|
||||
final url = request.url;
|
||||
if (url.host == 'ext.example') return http.Response(externalDump(), 200);
|
||||
if (url.path.endsWith('/xmltv.php')) return http.Response('<tv></tv>', 200);
|
||||
final action = url.queryParameters['action'] ?? '';
|
||||
actions.add(action);
|
||||
if (action == 'get_live_streams') {
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
{'stream_id': 1, 'name': 'TF1', 'epg_channel_id': 'TF1.fr'},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('{}', 200);
|
||||
});
|
||||
|
||||
final response = await apiWith(client).handleGetEpg(requestFor('1'), '1');
|
||||
expect(response.headers['X-Epg-Source'], 'xmltv');
|
||||
expect(actions, isNot(contains('get_simple_data_table')));
|
||||
});
|
||||
|
||||
test('la grille entière partage un seul téléchargement de la table',
|
||||
() async {
|
||||
// 30 tuiles demandent leur guide d'un coup : sans mutualisation, autant
|
||||
// de `get_live_streams` (7 Mo en prod) partaient en parallèle.
|
||||
var liveStreamsCalls = 0;
|
||||
final client = MockClient((request) async {
|
||||
final url = request.url;
|
||||
if (url.host == 'ext.example') return http.Response(externalDump(), 200);
|
||||
if (url.path.endsWith('/xmltv.php')) return http.Response('<tv></tv>', 200);
|
||||
final action = url.queryParameters['action'] ?? '';
|
||||
if (action == 'get_live_streams') {
|
||||
liveStreamsCalls++;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||||
return http.Response(
|
||||
jsonEncode([
|
||||
for (var i = 1; i <= 5; i++)
|
||||
{'stream_id': i, 'name': 'TF1', 'epg_channel_id': 'TF1.fr'},
|
||||
]),
|
||||
200,
|
||||
);
|
||||
}
|
||||
return http.Response('{}', 200);
|
||||
});
|
||||
|
||||
final api = apiWith(client);
|
||||
final responses = await Future.wait([
|
||||
for (var i = 1; i <= 5; i++) api.handleGetEpg(requestFor('$i'), '$i'),
|
||||
]);
|
||||
expect(responses.every((r) => r.statusCode == 200), isTrue);
|
||||
expect(liveStreamsCalls, 1);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -28,6 +28,13 @@ 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';
|
||||
}
|
||||
|
||||
void main() {
|
||||
final service = XmltvEpgService(sourceUrls: const []);
|
||||
|
||||
@@ -51,6 +58,71 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('nameKeys', () {
|
||||
// Noms réels du panneau (prod) : 59 % des chaînes françaises n'ont pas
|
||||
// d'epg_channel_id, seul leur nom permet de retrouver le guide.
|
||||
test('retire le préfixe pays et les marqueurs de qualité', () {
|
||||
expect(XmltvEpgService.nameKeys('FR - TF1 FHD ◉'), contains('tf1'));
|
||||
expect(XmltvEpgService.nameKeys('FR - FRANCE 4 FHD'), contains('france4'));
|
||||
expect(XmltvEpgService.nameKeys('|FR| M6 HD'), contains('m6'));
|
||||
expect(XmltvEpgService.nameKeys('[FR] Arte UHD 4K'), contains('arte'));
|
||||
expect(XmltvEpgService.nameKeys('FR: RMC Story HEVC'), contains('rmcstory'));
|
||||
});
|
||||
|
||||
test('propose aussi la forme identifiant « nom.pays »', () {
|
||||
// Les dumps publics identifient souvent la chaîne par `TF1.fr`.
|
||||
expect(XmltvEpgService.nameKeys('FR - TF1 FHD'), contains('tf1fr'));
|
||||
});
|
||||
|
||||
test('garde la variante décalée distincte de la chaîne principale', () {
|
||||
expect(XmltvEpgService.nameKeys('FR - TF1 +1 FHD'), isNot(contains('tf1')));
|
||||
});
|
||||
|
||||
test('ne réduit pas un nom court à rien', () {
|
||||
expect(XmltvEpgService.nameKeys('M6'), contains('m6'));
|
||||
expect(XmltvEpgService.nameKeys('HD'), isNot(contains('')));
|
||||
});
|
||||
});
|
||||
|
||||
group('programmesFor par nom', () {
|
||||
test('trouve le guide d\'une chaîne sans epg_channel_id par son nom nettoyé',
|
||||
() async {
|
||||
final now = DateTime.now().toUtc();
|
||||
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';
|
||||
}
|
||||
|
||||
final dump = '''
|
||||
<tv>
|
||||
<channel id="TF1.fr"><display-name>TF1 HD</display-name><display-name>TF1</display-name></channel>
|
||||
<programme start="${stamp(now)}" stop="${stamp(now.add(const Duration(hours: 1)))}" channel="TF1.fr">
|
||||
<title>Le 13h</title>
|
||||
</programme>
|
||||
</tv>
|
||||
''';
|
||||
final svc = XmltvEpgService(
|
||||
sourceUrls: const ['http://ext/epg.xml'],
|
||||
client: MockClient((_) async => http.Response(dump, 200)),
|
||||
);
|
||||
final hits = await svc.programmesFor(null, displayName: 'FR - TF1 FHD ◉');
|
||||
expect(hits.single.title, 'Le 13h');
|
||||
});
|
||||
|
||||
test('indexe chaque display-name, pas seulement le premier', () {
|
||||
final now = DateTime.now().toUtc();
|
||||
final index = service.parseForTest('''
|
||||
<tv>
|
||||
<channel id="x.fr"><display-name>Premier Nom</display-name><display-name>Second Nom</display-name></channel>
|
||||
<programme start="${_stamp(now)}" stop="${_stamp(now.add(const Duration(hours: 1)))}" channel="x.fr"><title>T</title></programme>
|
||||
</tv>
|
||||
''');
|
||||
expect(index[XmltvEpgService.normalizeKey('Second Nom')], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseXmltvDate', () {
|
||||
test('applique le décalage horaire annoncé', () {
|
||||
expect(
|
||||
|
||||
Reference in new issue
Block a user