mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-17 20:29:30 +02:00
Fiabilise le système d'enregistrement de bout en bout
Fuseaux horaires : - POST /api/recordings rejette en 400 les dates sans fuseau et normalise tout en UTC à l'écriture (l'interprétation des dates naïves dans le TZ du conteneur décalait les enregistrements de 1-2 h) - Helper frontend unique postRecording() : les 2 points de création (modal, guide EPG) envoient la même convention UTC Contrôle d'accès : - stop/delete/logs d'un enregistrement et delete d'un season pass vérifient la propriété (userId ou admin), comme playlists_handler - Suppression des replis 'dev_user_id' et 'admin' (401 sans session) SQLite : - PRAGMA foreign_keys/WAL/busy_timeout (les ON DELETE CASCADE déclarés ne s'appliquaient pas : sessions et playlists orphelines) - Migrations de schéma versionnées (schema_version) + index user_id, start_time, season_passes(user_id) Gestion disque : - Refus explicite de capture sous MIN_FREE_DISK_MB (défaut 500 Mo) - Rotation par quota d'octets (RECORDINGS_QUOTA_GB, opt-in) qui ne touche jamais un enregistrement actif et supprime fichiers + ligne ensemble ; l'ancienne rotation « 50 fichiers » pouvait effacer une capture en cours - DELETE /api/recordings/<id> supprime aussi .mkv/.log/parties (SafePath) Scheduler : - Arrêt gracieux orchestré par server.dart : clôture des enregistrements (fusion des parties, statut) avant killAll des sessions de streaming ; l'ancien handler SIGTERM de FfmpegSessionManager faisait exit(0) direct - Noms de fichiers uniques par fragment d'id (deux enregistrements du même programme s'écrasaient mutuellement avec -y) - Requête filtrée scheduled/recording au lieu de toute la table / 10 s - Statut cancelled pour un scheduled arrêté (completed sans fichier cassait la lecture) Season passes : - Playlist du propriétaire du pass résolue à chaque scan (l'injection figée du 1er utilisateur rendait les passes muets après ajout de playlist, et mélangeait les credentials en multi-utilisateurs) - Correspondance exacte par défaut (match_mode, migration en 'contains' pour l'existant), plafond de créations par scan, réalignement des horaires déplacés dans l'EPG, déduplication tolérante ±2 min - Redaction des erreurs de scan (ClientException contient l'URL amont) API de suivi (polling conservé) : - GET /api/recordings enrichi : progress_pct, file_size_bytes, retry_count, is_active ; barre de progression + taille dans la liste Validé : dart analyze (0 issue) et dart test (48/48) sur bin/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015oEu9QayWsw7hCKhenxgVa
This commit is contained in:
10 files changed
+740
-187
No files matched your search
+25
-19
@@ -42,31 +42,37 @@ void main(List<String> args) async {
|
||||
await db.seedAdmin();
|
||||
|
||||
// Initialize and start Recording Scheduler
|
||||
// (les Season Passes résolvent la playlist de leur propriétaire à chaque
|
||||
// scan : plus d'injection figée du premier utilisateur au démarrage)
|
||||
final recordingScheduler = RecordingScheduler(db);
|
||||
recordingScheduler.start();
|
||||
|
||||
// Injecter la config playlist dans le scheduler pour les Season Passes
|
||||
// (on prend la playlist du premier utilisateur disponible)
|
||||
Future<void> injectPlaylistToScheduler() async {
|
||||
final users = db.getAllUsers();
|
||||
if (users.isNotEmpty) {
|
||||
final playlists = db.getPlaylists(users[0].id);
|
||||
if (playlists.isNotEmpty) {
|
||||
final p = playlists.first;
|
||||
recordingScheduler.playlistDns = p.serverUrl;
|
||||
recordingScheduler.playlistUsername = p.username;
|
||||
recordingScheduler.playlistPassword = p.password;
|
||||
print('[Server] Playlist injectée dans le scheduler: ${p.name}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Injecter après 5s pour attendre l'initialisation complète
|
||||
Future.delayed(const Duration(seconds: 5), injectPlaylistToScheduler);
|
||||
|
||||
// Initialize Streaming Subsystem
|
||||
await initStreaming();
|
||||
|
||||
// Arrêt gracieux unique (docker stop / Ctrl+C) : clôturer d'abord les
|
||||
// enregistrements (kill FFmpeg, fusion des parties, statut en base), puis
|
||||
// les sessions de streaming, puis sortir. Sans cela les enregistrements
|
||||
// restaient au statut « recording » et la reprise d'orphelins devait
|
||||
// systématiquement rattraper au redémarrage.
|
||||
var shuttingDown = false;
|
||||
Future<void> shutdownServer(String signal) async {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
print('[Server] $signal reçu, arrêt en cours…');
|
||||
try {
|
||||
await recordingScheduler.shutdown();
|
||||
} catch (e) {
|
||||
print('[Server] Erreur à l\'arrêt du scheduler: $e');
|
||||
}
|
||||
sessionManager.killAll();
|
||||
db.close();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
ProcessSignal.sigterm.watch().listen((_) => shutdownServer('SIGTERM'));
|
||||
ProcessSignal.sigint.watch().listen((_) => shutdownServer('SIGINT'));
|
||||
|
||||
// Helper to get playlist from request
|
||||
Future<PlaylistConfig?> getPlaylist(Request request) async {
|
||||
Playlist? playlist;
|
||||
|
||||
Reference in new issue
Block a user