From 4c2e3ce60076fadaabcb28d091f5c9f062e03f2c Mon Sep 17 00:00:00 2001 From: Michael Date: Sun, 8 Mar 2026 23:53:32 +0100 Subject: [PATCH] feat: add stop recording button and POST /api/recordings/stop/ endpoint to kill FFmpeg process --- bin/api/recordings_api.dart | 25 +++++++++++++++++ bin/server.dart | 1 + bin/services/recording_scheduler.dart | 17 ++++++++++-- lib/features/iptv/widgets/recordings_tab.dart | 27 +++++++++++++++++++ 4 files changed, 68 insertions(+), 2 deletions(-) diff --git a/bin/api/recordings_api.dart b/bin/api/recordings_api.dart index 129d52e..baabbb0 100644 --- a/bin/api/recordings_api.dart +++ b/bin/api/recordings_api.dart @@ -101,4 +101,29 @@ class RecordingsApi { headers: {'Content-Type': 'application/json'}, ); } + + /// POST /api/recordings/stop/ — Arrête un enregistrement FFmpeg en cours + Future handleStop(Request request, String id) async { + final recording = _db.getRecordingById(id); + if (recording == null) { + return Response.notFound( + json.encode({'error': 'Enregistrement non trouvé'}), + headers: {'Content-Type': 'application/json'}, + ); + } + final stopped = await _scheduler.stopRecording(id); + if (stopped) { + return Response.ok( + json.encode({'message': 'Enregistrement arrêté'}), + headers: {'Content-Type': 'application/json'}, + ); + } else { + // Pas de processus FFmpeg actif pour cet ID → marquer comme complété quand même + _db.updateRecordingStatus(id, 'completed'); + return Response.ok( + json.encode({'message': 'Enregistrement marqué comme terminé'}), + headers: {'Content-Type': 'application/json'}, + ); + } + } } diff --git a/bin/server.dart b/bin/server.dart index 34d9704..5dfafab 100644 --- a/bin/server.dart +++ b/bin/server.dart @@ -122,6 +122,7 @@ void main(List args) async { ..get('/api/recordings', recordingsApi.handleGetAll) ..post('/api/recordings', recordingsApi.handlePost) ..delete('/api/recordings/', recordingsApi.handleDelete) + ..post('/api/recordings/stop/', recordingsApi.handleStop) ..get('/api/recordings/logs/', recordingsApi.getLogHandler); // NOTE: /api/xtream is handled by proxyHandler in the Cascade below // Do NOT mount here as it would intercept and block the actual proxy diff --git a/bin/services/recording_scheduler.dart b/bin/services/recording_scheduler.dart index 762c56b..9bf71f0 100644 --- a/bin/services/recording_scheduler.dart +++ b/bin/services/recording_scheduler.dart @@ -186,10 +186,23 @@ class RecordingScheduler { } } + /// Arrêter un enregistrement en cours (appelé depuis l'API) + Future stopRecording(String id) async { + if (_ffmpegProcess != null && _currentRecording?.id == id) { + print('[RecordingScheduler] Arrêt demandé pour: ${_currentRecording!.title}'); + _ffmpegProcess!.kill(ProcessSignal.sigterm); + _db.updateRecordingStatus(id, 'completed'); + _ffmpegProcess = null; + _currentRecording = null; + return true; + } + return false; // Pas d'enregistrement actif avec cet ID + } + Future _stopCurrentRecording({String? reason}) async { if (_ffmpegProcess != null && _currentRecording != null) { - print('[RecordingScheduler] Arrêt forcé du processus FFmpeg. Raison: ${reason ?? "Fin programmée"}'); - _ffmpegProcess!.kill(ProcessSignal.sigterm); // Envoyer un signal d'arrêt propre + print('[RecordingScheduler] Arrêt auto: ${reason ?? "Fin programmée"}'); + _ffmpegProcess!.kill(ProcessSignal.sigterm); _db.updateRecordingStatus(_currentRecording!.id, 'completed'); _ffmpegProcess = null; _currentRecording = null; diff --git a/lib/features/iptv/widgets/recordings_tab.dart b/lib/features/iptv/widgets/recordings_tab.dart index 3bd6544..d8eb0ed 100644 --- a/lib/features/iptv/widgets/recordings_tab.dart +++ b/lib/features/iptv/widgets/recordings_tab.dart @@ -71,6 +71,26 @@ class _RecordingsTabState extends State { } } + Future _stopRecording(String id, String title) async { + try { + final response = await http.post(Uri.parse('/api/recordings/stop/$id')); + if (response.statusCode == 200) { + _fetchRecordings(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('⏹ Enregistrement "$title" arrêté')), + ); + } + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Erreur lors de l\'arrêt de l\'enregistrement: $e'), backgroundColor: Colors.red), + ); + } + } + } + Future _showLogs(String id, String title) async { try { final response = await http.get(Uri.parse('/api/recordings/logs/$id')); @@ -268,6 +288,13 @@ class _RecordingsTabState extends State { ), ), const SizedBox(width: 8), + // Bouton Stop (uniquement pour les enregistrements en cours) + if (rec['status'] == 'recording') + IconButton( + icon: const Icon(Icons.stop_circle, color: Colors.redAccent), + tooltip: 'Arrêter l\'enregistrement', + onPressed: () => _stopRecording(rec['id'], rec['title'] ?? 'Inconnu'), + ), IconButton( icon: const Icon(Icons.description_outlined, color: Colors.blueAccent), tooltip: 'Voir les logs',