diff --git a/app/routes/api.py b/app/routes/api.py index 5dd68d2..8327d1f 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -24,8 +24,8 @@ AUTO_STATE_PATH = os.path.join(Config.LOG_DIR, 'auto_random_state.json') # Sound-Cache: path -> pygame.mixer.Sound loaded_sounds = {} random_history = defaultdict(deque) # sound_id -> deque[timestamps] -MAX_PER_HOUR = 2 -WINDOW_SECONDS = 3600 +DEFAULT_MAX_PER_HOUR = 2 +DEFAULT_WINDOW_SECONDS = 3600 def _ensure_mixer(): from config import Config @@ -103,8 +103,9 @@ def _pick_random_sound(): 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: + max_per_hour, window_seconds = _get_limits(sound) + dq = _prune_history(sid, now, window_seconds) + if len(dq) < max_per_hour: candidates.append(sound) if not candidates: @@ -117,6 +118,30 @@ def _pick_random_sound(): return sound_entry, None +def _get_limits(sound_entry=None): + """ + Ermittelt Grenzen für Random-Wiedergabe. + Priorität: pro Sound (max_per_hour, window_minutes) -> aktuelles Soundboard -> Defaults. + """ + max_per_hour = DEFAULT_MAX_PER_HOUR + window_seconds = DEFAULT_WINDOW_SECONDS + + if state.current_soundboard: + sb = state.current_soundboard + max_per_hour = int(sb.get('random_limit_per_hour', max_per_hour)) + window_minutes = sb.get('random_window_minutes') + if window_minutes: + window_seconds = int(window_minutes * 60) + + if sound_entry: + if 'max_per_hour' in sound_entry: + max_per_hour = int(sound_entry['max_per_hour']) + if 'window_minutes' in sound_entry: + window_seconds = int(sound_entry['window_minutes'] * 60) + + return max_per_hour, window_seconds + + def _auto_random_tick(): global auto_random_timer with auto_random_lock: @@ -165,9 +190,9 @@ def _persist_auto_state(clear=False): logger.error(f"Persist auto-random state fehlgeschlagen: {e}") -def _prune_history(sound_id, now): +def _prune_history(sound_id, now, window_seconds=DEFAULT_WINDOW_SECONDS): dq = random_history[sound_id] - while dq and now - dq[0] > WINDOW_SECONDS: + while dq and now - dq[0] > window_seconds: dq.popleft() return dq @@ -451,6 +476,8 @@ def api_soundboard_auto_start(): imax = float(data.get('interval_max', 10)) dmin = float(data.get('delay_min', 3)) dmax = float(data.get('delay_max', 12)) + limit_ph = int(data.get('limit_per_hour', DEFAULT_MAX_PER_HOUR)) + window_min = float(data.get('window_minutes', DEFAULT_WINDOW_SECONDS/60)) if imax < imin: imax = imin if dmax < dmin: dmax = dmin @@ -465,6 +492,8 @@ def api_soundboard_auto_start(): auto_random_params['dmax'] = dmax auto_random_params['next_ts'] = time.time() + delay auto_random_params['theme'] = state.current_soundboard.get('filename') + auto_random_params['limit_per_hour'] = limit_ph + auto_random_params['window_seconds'] = window_min * 60 global auto_random_timer auto_random_timer = threading.Timer(delay, _auto_random_tick) auto_random_timer.daemon = True @@ -500,6 +529,8 @@ def api_soundboard_status(): dmin = auto_random_params.get('dmin') dmax = auto_random_params.get('dmax') current_theme = state.current_soundboard.get('filename') if state.current_soundboard else None + limit_ph = auto_random_params.get('limit_per_hour', DEFAULT_MAX_PER_HOUR) + window_sec = auto_random_params.get('window_seconds', DEFAULT_WINDOW_SECONDS) next_seconds = max(0, next_ts - time.time()) if next_ts else None return jsonify({ "active": active, @@ -508,7 +539,9 @@ def api_soundboard_status(): "interval_max": imax, "delay_min": dmin, "delay_max": dmax, - "current_theme": current_theme + "current_theme": current_theme, + "limit_per_hour": limit_ph, + "window_minutes": window_sec/60 if window_sec else None }) @@ -557,7 +590,9 @@ def _restore_auto_state(): 'dmin': data.get('dmin', 3), 'dmax': data.get('dmax', 12), 'next_ts': next_ts, - 'theme': theme + 'theme': theme, + 'limit_per_hour': data.get('limit_per_hour', DEFAULT_MAX_PER_HOUR), + 'window_seconds': data.get('window_seconds', DEFAULT_WINDOW_SECONDS) }) delay = max(1, next_ts - time.time()) global auto_random_timer diff --git a/templates/soundboard.html b/templates/soundboard.html index 1bac977..237efb1 100644 --- a/templates/soundboard.html +++ b/templates/soundboard.html @@ -144,6 +144,14 @@ +
+ + +
+
+ + +
@@ -161,11 +169,17 @@ 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 limitPh = Math.max(1, parseInt(document.getElementById('auto-limit-ph').value, 10) || 2); + const windowMin = Math.max(1, parseInt(document.getElementById('auto-window-min').value, 10) || 60); 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 }) + body: JSON.stringify({ + interval_min: imin, interval_max: imax, + delay_min: dmin, delay_max: dmax, + limit_per_hour: limitPh, window_minutes: windowMin + }) }); const data = await res.json(); if (data.success) { @@ -212,6 +226,8 @@ if (data.interval_max) document.getElementById('auto-interval-max').value = data.interval_max; if (data.delay_min !== undefined && data.delay_min !== null) document.getElementById('auto-delay-min').value = data.delay_min; if (data.delay_max !== undefined && data.delay_max !== null) document.getElementById('auto-delay-max').value = data.delay_max; + if (data.limit_per_hour !== undefined && data.limit_per_hour !== null) document.getElementById('auto-limit-ph').value = data.limit_per_hour; + if (data.window_minutes !== undefined && data.window_minutes !== null) document.getElementById('auto-window-min').value = data.window_minutes; } else { status.textContent = 'Aus'; start.disabled = false;