From 3f43d229da7ab2a6a7e64d66efa734a2225a8359 Mon Sep 17 00:00:00 2001 From: scayac Date: Sun, 12 Apr 2026 21:59:31 +0200 Subject: [PATCH] =?UTF-8?q?Gestion=20du=20nombre=20de=20r=C3=A9p=C3=A9titi?= =?UTF-8?q?ons=20des=20triggers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/README_BACKEND.md | 8 +- backend/app/audio_player.py | 6 + backend/app/main.py | 14 ++- backend/app/models.py | 2 + frontend/README_FRONTEND.md | 12 ++ frontend/app/backend_client.py | 7 +- frontend/app/routes.py | 27 ++++- frontend/app/templates/dashboard.html | 156 +++++++++++++++++++------- 8 files changed, 184 insertions(+), 48 deletions(-) diff --git a/backend/README_BACKEND.md b/backend/README_BACKEND.md index 2c5188b..1d452ed 100644 --- a/backend/README_BACKEND.md +++ b/backend/README_BACKEND.md @@ -79,10 +79,15 @@ Exemple d'entree trigger: "volume": 80, "fade_in_seconds": 0.3, "fade_out_seconds": 0.4, + "repeat_count": 0, "normalize_audio": true } ``` +Champ supplementaire: + +- `repeat_count` (entier >= 0) : nombre de repetitions automatiques apres la premiere lecture (`0` = pas de repetition) + ## Lancement ```bash @@ -121,7 +126,8 @@ curl -k -u admin:change-me \ "type": "GPIO23", "music_file": "bell.mp3", "start_seconds": 0, - "end_seconds": null + "end_seconds": null, + "repeat_count": 0 }' ``` diff --git a/backend/app/audio_player.py b/backend/app/audio_player.py index 7ebac82..46df0d8 100644 --- a/backend/app/audio_player.py +++ b/backend/app/audio_player.py @@ -67,6 +67,7 @@ class AudioPlayer: volume: int = 80, fade_in_seconds: float = 0.0, fade_out_seconds: float = 0.0, + repeat_count: int = 0, normalize_audio: bool = False, ) -> None: with self._lock: @@ -75,6 +76,8 @@ class AudioPlayer: if start_seconds < 0: raise ValueError("start_seconds must be greater than or equal to 0") + if repeat_count < 0: + raise ValueError("repeat_count must be greater than or equal to 0") duration: Optional[float] = None if end_seconds is not None: @@ -127,6 +130,9 @@ class AudioPlayer: ) effects += ["fade", "t", str(fade_in)] + if repeat_count > 0: + effects += ["repeat", str(int(repeat_count))] + # If sox lacks mp3 handlers, decode with ffmpeg and stream wav to sox. if music_path.suffix.lower() == ".mp3": decoder_cmd = ["ffmpeg", "-v", "error", "-i", str(music_path), "-f", "wav", "-"] diff --git a/backend/app/main.py b/backend/app/main.py index bb621eb..1ec3ce8 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -57,9 +57,15 @@ def _resolve_trigger(config: Dict[str, object], trigger_id: str) -> tuple[str, D raise KeyError(trigger_id) -def _play_from_trigger(trigger_id: str) -> Dict[str, object]: +def _play_from_trigger(trigger_id: str, repeat_count_override: int | None = None) -> Dict[str, object]: config = config_store.get_copy() _resolved_key, trigger = _resolve_trigger(config, trigger_id) + effective_repeat_count = int(trigger.get("repeat_count", 0) or 0) + if repeat_count_override is not None: + effective_repeat_count = int(repeat_count_override) + if effective_repeat_count < 0: + raise ValueError("repeat_count must be greater than or equal to 0") + audio_player.play( music_file=trigger["music_file"], start_seconds=float(trigger.get("start_seconds", 0.0) or 0.0), @@ -67,6 +73,7 @@ def _play_from_trigger(trigger_id: str) -> Dict[str, object]: volume=int(trigger.get("volume", 80)), fade_in_seconds=float(trigger.get("fade_in_seconds", 0.0) or 0.0), fade_out_seconds=float(trigger.get("fade_out_seconds", 0.0) or 0.0), + repeat_count=effective_repeat_count, normalize_audio=bool(trigger.get("normalize_audio", False)), ) return trigger @@ -125,6 +132,7 @@ def _serial_callback(raw_message: str) -> TriggerLogInfo | None: volume=int(trigger.get("volume", 80)), fade_in_seconds=float(trigger.get("fade_in_seconds", 0.0) or 0.0), fade_out_seconds=float(trigger.get("fade_out_seconds", 0.0) or 0.0), + repeat_count=int(trigger.get("repeat_count", 0) or 0), normalize_audio=bool(trigger.get("normalize_audio", False)), ) @@ -204,9 +212,9 @@ def delete_trigger(trigger_id: str) -> Dict[str, str]: @app.get("/api/play/{trigger_id}", dependencies=[Depends(auth_required)]) -def force_play(trigger_id: str) -> Dict[str, str]: +def force_play(trigger_id: str, repeat_count: int | None = None) -> Dict[str, str]: try: - _play_from_trigger(trigger_id) + _play_from_trigger(trigger_id, repeat_count_override=repeat_count) except KeyError: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Unknown trigger") except (FileNotFoundError, ValueError, RuntimeError) as exc: diff --git a/backend/app/models.py b/backend/app/models.py index 5f6767f..3d7b3e7 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -14,6 +14,7 @@ class TriggerConfig(BaseModel): volume: int = Field(default=80, ge=0, le=100) fade_in_seconds: float = Field(default=0.0, ge=0) fade_out_seconds: float = Field(default=0.0, ge=0) + repeat_count: int = Field(default=0, ge=0) normalize_audio: bool = Field(default=False) @field_validator("end_seconds") @@ -34,6 +35,7 @@ class TriggerPatch(BaseModel): volume: Optional[int] = Field(default=None, ge=0, le=100) fade_in_seconds: Optional[float] = Field(default=None, ge=0) fade_out_seconds: Optional[float] = Field(default=None, ge=0) + repeat_count: Optional[int] = Field(default=None, ge=0) normalize_audio: Optional[bool] = Field(default=None) diff --git a/frontend/README_FRONTEND.md b/frontend/README_FRONTEND.md index 01cfa38..f7356f6 100644 --- a/frontend/README_FRONTEND.md +++ b/frontend/README_FRONTEND.md @@ -6,11 +6,23 @@ Frontend web responsive en Flask pour piloter le backend pySonnerie déjà en pl - Page de connexion (URL backend + identifiants Basic Auth) - Tableau de bord de gestion des triggers (creation/modification/suppression) +- Parametrage du nombre de repetitions automatiques par trigger (champ `Nombre de repetitions`) - Lancement manuel d'un trigger (`/api/play/{trigger_id}`) - Arrêt audio (`/api/stop`) - Gestion du stockage audio dans `backend/data/musiques` (televersement, telechargement, suppression) - Import audio depuis un lien YouTube (extraction via `yt-dlp` puis conversion en MP3) +## Parametres trigger (dashboard) + +Le formulaire trigger permet de regler: + +- Fenetre de lecture (`Debut`, `Fin`) +- Volume et fondus (`Fade in`, `Fade out`) +- Normalisation audio +- `Nombre de repetitions` : +- `0` = lecture unique +- `N` (> 0) = `N` repetitions automatiques apres la premiere lecture + ## Prerequis - Python 3.11+ diff --git a/frontend/app/backend_client.py b/frontend/app/backend_client.py index 2b5e426..8436993 100644 --- a/frontend/app/backend_client.py +++ b/frontend/app/backend_client.py @@ -97,8 +97,11 @@ class BackendClient: data = self._request("DELETE", f"/api/triggers/{trigger_id}") return data if isinstance(data, dict) else {} - def play_trigger(self, trigger_id: str) -> dict[str, Any]: - data = self._request("GET", f"/api/play/{trigger_id}") + def play_trigger(self, trigger_id: str, repeat_count: int | None = None) -> dict[str, Any]: + path = f"/api/play/{trigger_id}" + if repeat_count is not None: + path += f"?repeat_count={repeat_count}" + data = self._request("GET", path) return data if isinstance(data, dict) else {} def stop_audio(self) -> dict[str, Any]: diff --git a/frontend/app/routes.py b/frontend/app/routes.py index 0323e6e..cd3b520 100644 --- a/frontend/app/routes.py +++ b/frontend/app/routes.py @@ -393,6 +393,7 @@ def save_trigger() -> Response: volume_raw = request.form.get("volume", "80") fade_in_raw = request.form.get("fade_in_seconds", "0") fade_out_raw = request.form.get("fade_out_seconds", "0") + repeat_count_raw = request.form.get("repeat_count", "0") normalize_audio = request.form.get("normalize_audio") in {"on", "true", "1"} def _err(msg: str) -> Response: @@ -413,8 +414,9 @@ def save_trigger() -> Response: volume = int(volume_raw) fade_in_seconds = float(fade_in_raw) fade_out_seconds = float(fade_out_raw) + repeat_count = int(repeat_count_raw) except ValueError: - return _err("Les temps de debut/fin, fondus et le volume doivent être numériques.") + return _err("Les temps de debut/fin, fondus, repetitions et le volume doivent être numériques.") if start_seconds < 0 or (end_seconds is not None and end_seconds <= start_seconds): return _err("Fenêtre temporelle invalide.") @@ -425,6 +427,9 @@ def save_trigger() -> Response: if fade_in_seconds < 0 or fade_out_seconds < 0: return _err("Les durées de fondu doivent être supérieures ou égales à 0.") + if repeat_count < 0: + return _err("Le nombre de répétitions doit être supérieur ou égal à 0.") + payload = { "name": name, "type": trigger_type, @@ -434,6 +439,7 @@ def save_trigger() -> Response: "volume": volume, "fade_in_seconds": fade_in_seconds, "fade_out_seconds": fade_out_seconds, + "repeat_count": repeat_count, "normalize_audio": normalize_audio, } @@ -511,14 +517,31 @@ def play_trigger() -> Response: client = client_or_redirect trigger_id = request.form.get("trigger_id", "").strip() + repeat_count_raw = request.form.get("repeat_count", "").strip() if not trigger_id: if request.headers.get("X-Requested-With") == "fetch": return jsonify({"ok": False, "error": "Identifiant du trigger manquant."}), 400 flash("Identifiant du trigger manquant.", "error") return redirect(url_for("ui.dashboard")) + repeat_count: int | None = None + if repeat_count_raw: + try: + parsed_repeat_count = int(repeat_count_raw) + except ValueError: + if request.headers.get("X-Requested-With") == "fetch": + return jsonify({"ok": False, "error": "Nombre de répétitions invalide."}), 400 + flash("Nombre de répétitions invalide.", "error") + return redirect(url_for("ui.dashboard")) + if parsed_repeat_count < 0: + if request.headers.get("X-Requested-With") == "fetch": + return jsonify({"ok": False, "error": "Le nombre de répétitions doit être >= 0."}), 400 + flash("Le nombre de répétitions doit être >= 0.", "error") + return redirect(url_for("ui.dashboard")) + repeat_count = parsed_repeat_count + try: - client.play_trigger(trigger_id) + client.play_trigger(trigger_id, repeat_count=repeat_count) if request.headers.get("X-Requested-With") == "fetch": return jsonify({"ok": True, "message": "Trigger démarré."}) flash("Trigger démarré.", "success") diff --git a/frontend/app/templates/dashboard.html b/frontend/app/templates/dashboard.html index 394b0db..06a13c9 100644 --- a/frontend/app/templates/dashboard.html +++ b/frontend/app/templates/dashboard.html @@ -66,6 +66,10 @@ +