diff --git a/app/routes/api.py b/app/routes/api.py index 9076753..2436513 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -15,6 +15,9 @@ from app.utils.helpers import load_default_sounds, load_soundboard_configs, load logger = logging.getLogger(__name__) audio_lock = threading.Lock() +auto_random_lock = threading.Lock() +auto_random_timer = None +auto_random_params = {} # Sound-Cache: path -> pygame.mixer.Sound loaded_sounds = {} @@ -82,6 +85,55 @@ def _play_sound_entry(sound_entry, channel_req=None, loop_req=0): return None # Erfolg +def _pick_random_sound(): + if state.current_soundboard is None: + return None, "Kein Soundboard geladen" + rnd_list = state.current_soundboard.get('random_pool') or state.current_soundboard.get('sounds', []) + if not rnd_list: + return None, "Keine Random-Sounds definiert" + + now = time.time() + candidates = [] + for sound in rnd_list: + sid = sound.get('id') or sound.get('file') + dq = _prune_history(sid, now) + if len(dq) < MAX_PER_HOUR: + candidates.append(sound) + + if not candidates: + return None, "Limit erreicht (2x pro Stunde)" + + sound_entry = random.choice(candidates) + sid = sound_entry.get('id') or sound_entry.get('file') + random_history[sid].append(now) + sound_entry.setdefault('id', sid) + return sound_entry, None + + +def _auto_random_tick(): + global auto_random_timer + with auto_random_lock: + if not state.auto_random_active: + return + imin = auto_random_params.get('imin', 5) + imax = auto_random_params.get('imax', 10) + # Play one random sound + sound_entry, err = _pick_random_sound() + if sound_entry: + _play_sound_entry(sound_entry, sound_entry.get('channel'), sound_entry.get('loop')) + # schedule next + delay = rand_between(imin, imax) * 60 + with auto_random_lock: + if state.auto_random_active: + auto_random_timer = threading.Timer(delay, _auto_random_tick) + auto_random_timer.daemon = True + auto_random_timer.start() + + +def rand_between(a, b): + return a + random.random() * (b - a) + + def _prune_history(sound_id, now): dq = random_history[sound_id] while dq and now - dq[0] > WINDOW_SECONDS: @@ -331,6 +383,8 @@ def api_soundboard_load(): current_app.config['SOUNDBOARD_CONFIG_DIR'], current_app.config['SOUNDS_DIR']) state.current_soundboard = sb + # Stoppe ggf. laufende Auto-Randoms beim Laden neuer Themen + _stop_auto_random_internal() logger.info(f"Soundboard geladen: {filename}") return jsonify({"success": True, "soundboard": sb}) except Exception as e: @@ -343,30 +397,69 @@ def api_soundboard_play_random(): if state.current_soundboard is None: return jsonify({"success": False, "message": "Kein Soundboard geladen"}), 400 - rnd_list = state.current_soundboard.get('random_pool') or state.current_soundboard.get('sounds', []) - if not rnd_list: - return jsonify({"success": False, "message": "Keine Random-Sounds definiert"}), 400 - - now = time.time() - candidates = [] - for sound in rnd_list: - sid = sound.get('id') or sound.get('file') - dq = _prune_history(sid, now) - if len(dq) < MAX_PER_HOUR: - candidates.append(sound) - - if not candidates: - return jsonify({"success": False, "message": "Limit erreicht (2x pro Stunde)"}), 429 - - sound_entry = random.choice(candidates) - sid = sound_entry.get('id') or sound_entry.get('file') + sound_entry, err = _pick_random_sound() + if err: + return jsonify({"success": False, "message": err}), 429 if "Limit" in err else 400 res = _play_sound_entry(sound_entry) if res is not None: return res # already Response - random_history[sid].append(now) - return jsonify({"success": True, "message": f"Random: {sound_entry.get('name', sid)}"}) + return jsonify({"success": True, "message": f"Random: {sound_entry.get('name', sound_entry.get('id'))}"}) + + +@api_bp.route('/soundboard/auto_start', methods=['POST']) +def api_soundboard_auto_start(): + if state.current_soundboard is None: + return jsonify({"success": False, "message": "Kein Soundboard geladen"}), 400 + data = request.get_json() or {} + imin = float(data.get('interval_min', 5)) + imax = float(data.get('interval_max', 10)) + dmin = float(data.get('delay_min', 3)) + dmax = float(data.get('delay_max', 12)) + if imax < imin: imax = imin + if dmax < dmin: dmax = dmin + + delay = rand_between(dmin, dmax) * 60 + with auto_random_lock: + _stop_auto_random_internal() + state.auto_random_active = True + auto_random_params['imin'] = imin + auto_random_params['imax'] = imax + auto_random_params['dmin'] = dmin + auto_random_params['dmax'] = dmax + global auto_random_timer + auto_random_timer = threading.Timer(delay, _auto_random_tick) + auto_random_timer.daemon = True + auto_random_timer.start() + logger.info(f"Auto-Random gestartet (delay {delay/60:.1f} min, intervall {imin}-{imax} min)") + return jsonify({"success": True, "message": "Auto-Random gestartet", + "next_seconds": delay}) + + +@api_bp.route('/soundboard/auto_stop', methods=['POST']) +def api_soundboard_auto_stop(): + stopped = _stop_auto_random_internal() + return jsonify({"success": True, "message": "Auto-Random gestoppt", "was_running": stopped}) + + +@api_bp.route('/soundboard/status', methods=['GET']) +def api_soundboard_status(): + with auto_random_lock: + active = state.auto_random_active + return jsonify({"active": active}) + + +def _stop_auto_random_internal(): + global auto_random_timer + stopped = False + with auto_random_lock: + if auto_random_timer: + auto_random_timer.cancel() + auto_random_timer = None + stopped = True + state.auto_random_active = False + return stopped @api_bp.route('/stop_sound', methods=['POST']) diff --git a/app/state.py b/app/state.py index ae556e9..d0735e8 100644 --- a/app/state.py +++ b/app/state.py @@ -5,6 +5,7 @@ current_hub = None current_module = None current_device = None current_soundboard = None # aktives Soundboard-Thema +auto_random_active = False # Optional: Funktionen zum Setzen/Resetten (für Klarheit) def reset_state(): diff --git a/templates/soundboard.html b/templates/soundboard.html index 7a19831..88b3d43 100644 --- a/templates/soundboard.html +++ b/templates/soundboard.html @@ -156,44 +156,36 @@ const stop = document.getElementById('auto-stop'); const status = document.getElementById('auto-status'); - start.onclick = () => { + start.onclick = async () => { const imin = Math.max(1, parseInt(document.getElementById('auto-interval-min').value, 10) || 5); const imax = Math.max(imin, parseInt(document.getElementById('auto-interval-max').value, 10) || 10); const dmin = Math.max(0, parseInt(document.getElementById('auto-delay-min').value, 10) || 3); const dmax = Math.max(dmin, parseInt(document.getElementById('auto-delay-max').value, 10) || 12); - const delayMs = randBetween(dmin, dmax) * 60 * 1000; - scheduleAuto(imin, imax, delayMs, status, start, stop); + const res = await fetch('/api/soundboard/auto_start', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ interval_min: imin, interval_max: imax, delay_min: dmin, delay_max: dmax }) + }); + const data = await res.json(); + if (data.success) { + status.textContent = `Aktiv (nächster in ${(data.next_seconds/60).toFixed(1)} min)`; + start.disabled = true; + stop.disabled = false; + } else { + statusBox(data.message || 'Auto-Start fehlgeschlagen', 'warn'); + } }; - stop.onclick = () => stopAuto(status, start, stop); - } - - function randBetween(min, max) { - return min + Math.random() * (max - min); - } - - function scheduleAuto(imin, imax, delayMs, status, startBtn, stopBtn) { - stopAuto(status, startBtn, stopBtn); - status.textContent = `Geplant in ${(delayMs/60000).toFixed(1)} min`; - startBtn.disabled = true; - stopBtn.disabled = false; - autoTimer = setTimeout(async function tick() { - await playRandom(); - const nextMs = randBetween(imin, imax) * 60 * 1000; - status.textContent = `Nächster in ${(nextMs/60000).toFixed(1)} min`; - autoTimer = setTimeout(tick, nextMs); - }, delayMs); - } - - function stopAuto(status, startBtn, stopBtn) { - if (autoTimer) { - clearTimeout(autoTimer); - autoTimer = null; - } - if (status) status.textContent = 'Aus'; - if (startBtn) startBtn.disabled = false; - if (stopBtn) stopBtn.disabled = true; + stop.onclick = async () => { + const res = await fetch('/api/soundboard/auto_stop', { method: 'POST' }); + const data = await res.json(); + if (data.success) { + status.textContent = 'Aus'; + start.disabled = false; + stop.disabled = true; + } + }; } async function playSound(sound) {