From fdf98f9659f24c69dec6857e18b2efe07dee53b7 Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 10 Oct 2026 12:09:11 +0200 Subject: [PATCH] Films : demander le fichier sous sa vraie extension MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le serveur réclamait toujours .mkv au panneau. Un film stocké en .mp4 était refusé (HTTP 551 constaté en prod). Le client transmet désormais container_extension (?ext=), validé côté serveur ; mkv reste le défaut pour un client qui ne l'envoie pas. Même approche que xtremobile, qui lit ces films sans souci. Co-Authored-By: Claude Opus 5.5 --- bin/api/streaming_handler.dart | 28 +++++++++++++++---- bin/test/vod_extension_test.dart | 23 +++++++++++++++ .../iptv/services/xtream_service.dart | 14 ++++++++-- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 bin/test/vod_extension_test.dart diff --git a/bin/api/streaming_handler.dart b/bin/api/streaming_handler.dart index 691e0ff..cdc5cd7 100644 --- a/bin/api/streaming_handler.dart +++ b/bin/api/streaming_handler.dart @@ -99,6 +99,16 @@ String _sanitizeQuality(String? quality) { bool _isValidStreamId(String id) => RegExp(r'^[a-zA-Z0-9_-]+$').hasMatch(id); +/// Extension de conteneur d'un film/épisode, validée (elle finit dans l'URL +/// du panneau). `mkv` par défaut : c'était l'unique valeur jusqu'ici, les +/// anciens clients qui n'envoient pas `ext` gardent leur comportement. +String vodContainerExtension(String? ext) { + final value = ext?.toLowerCase(); + return value != null && RegExp(r'^[a-z0-9]{2,5}$').hasMatch(value) + ? value + : 'mkv'; +} + /// Video encoding args for the requested quality (live profile). List _liveVideoArgs(String quality, bool gpu) { switch (quality) { @@ -680,8 +690,11 @@ Handler createVodStreamHandler( final contentType = request.url.queryParameters['type'] ?? 'movie'; final basePath = contentType == 'series' ? 'series' : 'movie'; + // Extension réelle du fichier (`container_extension` du catalogue) : le + // panneau refuse un `.mkv` imposé à un film stocké en `.mp4`. final targetUrl = - '${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/$streamId.mkv'; + '${playlist.dns}/$basePath/${playlist.username}/${playlist.password}/' + '$streamId.${vodContainerExtension(request.url.queryParameters['ext'])}'; final useNvidiaGpu = isGpuEnabled?.call() ?? _isNvidiaGpuEnabled(); final sessionId = 'vod_${streamId}_$quality'; @@ -751,10 +764,15 @@ Handler createVodStreamHandler( (Request request, String streamId) async { final quality = _sanitizeQuality(request.url.queryParameters['quality']); - final query = request.url.queryParameters['type'] != null - ? '?type=${request.url.queryParameters['type']}' - : ''; - return Response.found('/api/vod/$streamId/$quality/playlist.m3u8$query'); + final params = request.url.queryParameters; + final query = Uri(queryParameters: { + if (params['type'] != null) 'type': params['type']!, + if (params['ext'] != null) 'ext': params['ext']!, + }).query; + return Response.found( + '/api/vod/$streamId/$quality/playlist.m3u8' + '${query.isEmpty ? '' : '?$query'}', + ); }); // Route: /api/vod/{streamId}/{quality}/{segment} diff --git a/bin/test/vod_extension_test.dart b/bin/test/vod_extension_test.dart new file mode 100644 index 0000000..8e70f46 --- /dev/null +++ b/bin/test/vod_extension_test.dart @@ -0,0 +1,23 @@ +import 'package:test/test.dart'; +import '../api/streaming_handler.dart'; + +void main() { + group('vodContainerExtension', () { + test('garde l\'extension réelle du catalogue', () { + expect(vodContainerExtension('mp4'), 'mp4'); + expect(vodContainerExtension('MKV'), 'mkv'); + expect(vodContainerExtension('avi'), 'avi'); + }); + + test('ancien client sans ext : mkv, comme avant', () { + expect(vodContainerExtension(null), 'mkv'); + expect(vodContainerExtension(''), 'mkv'); + }); + + test('refuse tout ce qui pourrait sortir du nom de fichier', () { + expect(vodContainerExtension('mp4/../x'), 'mkv'); + expect(vodContainerExtension('mp4?a=b'), 'mkv'); + expect(vodContainerExtension('toolongext'), 'mkv'); + }); + }); +} diff --git a/lib/features/iptv/services/xtream_service.dart b/lib/features/iptv/services/xtream_service.dart index 91623b8..654a6ac 100644 --- a/lib/features/iptv/services/xtream_service.dart +++ b/lib/features/iptv/services/xtream_service.dart @@ -191,7 +191,16 @@ class XtreamService { String quality = 'high', }) { if (_currentPlaylist == null) throw Exception('No playlist configured'); - return '$_backendBaseUrl/api/vod/$streamId/$quality/playlist.m3u8'; + // `ext` : le serveur demande le fichier au panneau sous son extension + // réelle — un `.mp4` réclamé en `.mkv` est refusé (HTTP 551). + return '$_backendBaseUrl/api/vod/$streamId/$quality/playlist.m3u8' + '${_extQuery(containerExtension, first: true)}'; + } + + String _extQuery(String ext, {required bool first}) { + final value = ext.trim().toLowerCase(); + if (value.isEmpty) return ''; + return '${first ? '?' : '&'}ext=${Uri.encodeQueryComponent(value)}'; } /// Generate stream URL for series episodes @@ -201,7 +210,8 @@ class XtreamService { String quality = 'high', }) { if (_currentPlaylist == null) throw Exception('No playlist configured'); - return '$_backendBaseUrl/api/vod/$streamId/$quality/playlist.m3u8?type=series'; + return '$_backendBaseUrl/api/vod/$streamId/$quality/playlist.m3u8?type=series' + '${_extQuery(containerExtension, first: false)}'; } /// Authenticate and get server info