mirror of
https://github.com/R0m1k3/xtremflow.git
synced 2026-10-23 23:18:06 +02:00
Serveur réécrit en Rust
Le serveur Dart (bin/) est remplacé par un serveur Rust (server/) : tokio + axum, reqwest/rustls, SQLite embarqué via rusqlite. Même API, mêmes routes et mêmes réponses : le front Flutter ne change pas. La base de production est reprise telle quelle : schéma, migrations et formats de dates (DateTime.toIso8601String de Dart) sont reproduits à l'identique, les sessions et hachages existants restent valides. Comportement conservé : cascade de routage, anti-SSRF, logs masqués, rate limits, transcodage HLS/turbo.ts avec repli GPU, quotas fournisseur, enregistrements (reprise, relances, fusion), season passes, EPG, logos de repli, arrêt gracieux sur SIGTERM. Tests Dart portés (132 tests Rust) ; la CI exécute fmt, test et clippy -D warnings, l'image Docker compile le serveur dans un étage rust:1.98.1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f82c70ae05
commit
3ea6fa0ae3
109 files changed
+14008
-11047
No files matched your search
@@ -1,126 +0,0 @@
|
||||
/// Pure decision logic for the recording scheduler, extracted for testability.
|
||||
|
||||
enum RecordingAction {
|
||||
/// Start the recording now.
|
||||
start,
|
||||
|
||||
/// Capacity reached but the window is still open: retry on a later tick.
|
||||
wait,
|
||||
|
||||
/// The end time passed before the recording could start.
|
||||
fail,
|
||||
|
||||
/// Not yet due — nothing to do.
|
||||
none,
|
||||
}
|
||||
|
||||
/// Decides what to do with a scheduled recording on a scheduler tick.
|
||||
RecordingAction decideRecordingAction({
|
||||
required DateTime now,
|
||||
required DateTime startTime,
|
||||
required DateTime endTime,
|
||||
required int activeCount,
|
||||
required int maxConcurrent,
|
||||
}) {
|
||||
final nowUtc = now.toUtc();
|
||||
final startUtc = startTime.toUtc();
|
||||
final endUtc = endTime.toUtc();
|
||||
|
||||
if (nowUtc.isAfter(endUtc)) return RecordingAction.fail;
|
||||
if (!nowUtc.isAfter(startUtc)) return RecordingAction.none;
|
||||
if (activeCount >= maxConcurrent) return RecordingAction.wait;
|
||||
return RecordingAction.start;
|
||||
}
|
||||
|
||||
/// Ce que le planificateur doit faire d'un enregistrement resté au statut
|
||||
/// « recording » en base alors qu'aucun processus FFmpeg ne lui correspond
|
||||
/// (redémarrage du conteneur, crash de l'isolate…).
|
||||
enum OrphanAction {
|
||||
/// La fenêtre est encore ouverte : relancer la capture.
|
||||
resume,
|
||||
|
||||
/// La fenêtre est passée mais un fichier partiel existe : le conserver.
|
||||
finish,
|
||||
|
||||
/// Rien d'exploitable n'a été capturé.
|
||||
fail,
|
||||
}
|
||||
|
||||
/// Décide du sort d'un enregistrement orphelin détecté au démarrage/à un tick.
|
||||
OrphanAction decideOrphanAction({
|
||||
required DateTime now,
|
||||
required DateTime endTime,
|
||||
required bool hasFile,
|
||||
Duration minRemaining = const Duration(seconds: 60),
|
||||
}) {
|
||||
final remaining = endTime.toUtc().difference(now.toUtc());
|
||||
if (remaining > minRemaining) return OrphanAction.resume;
|
||||
return hasFile ? OrphanAction.finish : OrphanAction.fail;
|
||||
}
|
||||
|
||||
/// Ce que le planificateur doit faire quand un processus FFmpeg se termine.
|
||||
enum PostExitAction {
|
||||
/// La capture est allée jusqu'au bout (ou assez loin) : marquer terminé.
|
||||
complete,
|
||||
|
||||
/// FFmpeg s'est arrêté trop tôt : relancer la capture sur la fin de fenêtre.
|
||||
retry,
|
||||
|
||||
/// Arrêt prématuré sans rien d'exploitable.
|
||||
fail,
|
||||
}
|
||||
|
||||
/// Nombre d'échecs FFmpeg consécutifs tolérés avant d'abandonner un
|
||||
/// enregistrement dont la fenêtre est encore ouverte.
|
||||
///
|
||||
/// Le compteur est remis à zéro dès qu'un lancement a duré assez longtemps
|
||||
/// pour être considéré sain : une coupure toutes les dix minutes ne doit pas
|
||||
/// finir par épuiser le quota.
|
||||
const int maxFfmpegAttempts = 30;
|
||||
|
||||
/// Décide de la suite à donner à la sortie d'un processus FFmpeg.
|
||||
///
|
||||
/// FFmpeg rend la main dès que la source se tarit : sur un flux IPTV, une
|
||||
/// coupure amont à mi-parcours ne doit pas condamner l'heure restante.
|
||||
PostExitAction decidePostExitAction({
|
||||
required DateTime now,
|
||||
required DateTime endTime,
|
||||
required int exitCode,
|
||||
required int consecutiveFailures,
|
||||
required bool hasFile,
|
||||
int maxAttempts = maxFfmpegAttempts,
|
||||
Duration minRemaining = const Duration(seconds: 60),
|
||||
}) {
|
||||
final remaining = endTime.toUtc().difference(now.toUtc());
|
||||
if (remaining > minRemaining && consecutiveFailures + 1 < maxAttempts) {
|
||||
return PostExitAction.retry;
|
||||
}
|
||||
// `255` est le code renvoyé par FFmpeg quand on l'interrompt proprement.
|
||||
final cleanExit = exitCode == 0 || exitCode == 255;
|
||||
return (cleanExit || hasFile) ? PostExitAction.complete : PostExitAction.fail;
|
||||
}
|
||||
|
||||
/// Durée de capture à demander à FFmpeg (`-t`).
|
||||
///
|
||||
/// Se base sur le temps qu'il reste jusqu'à la fin programmée, et non sur la
|
||||
/// durée théorique du programme : un démarrage tardif (ou une relance après
|
||||
/// coupure) ne doit pas décaler la fin de l'enregistrement.
|
||||
Duration captureDuration({
|
||||
required DateTime now,
|
||||
required DateTime endTime,
|
||||
Duration minimum = const Duration(seconds: 30),
|
||||
}) {
|
||||
final remaining = endTime.toUtc().difference(now.toUtc());
|
||||
return remaining < minimum ? minimum : remaining;
|
||||
}
|
||||
|
||||
/// Délai avant relance de FFmpeg, croissant avec les échecs consécutifs.
|
||||
///
|
||||
/// Rouvrir la source dans la seconde se fait refuser sur les comptes limités
|
||||
/// en connexions simultanées ; s'acharner à cette cadence pendant une panne
|
||||
/// amont ne ferait qu'épuiser le quota de relances.
|
||||
Duration ffmpegRetryDelay(int consecutiveFailures) {
|
||||
final steps = (consecutiveFailures - 1).clamp(0, 4);
|
||||
final seconds = 3 * (1 << steps);
|
||||
return Duration(seconds: seconds > 30 ? 30 : seconds);
|
||||
}
|
||||
Reference in new issue
Block a user