added new functions for random sounds

This commit is contained in:
oberon 2026-02-19 09:48:07 +01:00
parent 4a4f55fbe9
commit 63e62670d5
2 changed files with 60 additions and 9 deletions

View File

@ -24,8 +24,8 @@ AUTO_STATE_PATH = os.path.join(Config.LOG_DIR, 'auto_random_state.json')
# Sound-Cache: path -> pygame.mixer.Sound # Sound-Cache: path -> pygame.mixer.Sound
loaded_sounds = {} loaded_sounds = {}
random_history = defaultdict(deque) # sound_id -> deque[timestamps] random_history = defaultdict(deque) # sound_id -> deque[timestamps]
MAX_PER_HOUR = 2 DEFAULT_MAX_PER_HOUR = 2
WINDOW_SECONDS = 3600 DEFAULT_WINDOW_SECONDS = 3600
def _ensure_mixer(): def _ensure_mixer():
from config import Config from config import Config
@ -103,8 +103,9 @@ def _pick_random_sound():
candidates = [] candidates = []
for sound in rnd_list: for sound in rnd_list:
sid = sound.get('id') or sound.get('file') sid = sound.get('id') or sound.get('file')
dq = _prune_history(sid, now) max_per_hour, window_seconds = _get_limits(sound)
if len(dq) < MAX_PER_HOUR: dq = _prune_history(sid, now, window_seconds)
if len(dq) < max_per_hour:
candidates.append(sound) candidates.append(sound)
if not candidates: if not candidates:
@ -117,6 +118,30 @@ def _pick_random_sound():
return sound_entry, None 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(): def _auto_random_tick():
global auto_random_timer global auto_random_timer
with auto_random_lock: with auto_random_lock:
@ -165,9 +190,9 @@ def _persist_auto_state(clear=False):
logger.error(f"Persist auto-random state fehlgeschlagen: {e}") 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] dq = random_history[sound_id]
while dq and now - dq[0] > WINDOW_SECONDS: while dq and now - dq[0] > window_seconds:
dq.popleft() dq.popleft()
return dq return dq
@ -451,6 +476,8 @@ def api_soundboard_auto_start():
imax = float(data.get('interval_max', 10)) imax = float(data.get('interval_max', 10))
dmin = float(data.get('delay_min', 3)) dmin = float(data.get('delay_min', 3))
dmax = float(data.get('delay_max', 12)) 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 imax < imin: imax = imin
if dmax < dmin: dmax = dmin if dmax < dmin: dmax = dmin
@ -465,6 +492,8 @@ def api_soundboard_auto_start():
auto_random_params['dmax'] = dmax auto_random_params['dmax'] = dmax
auto_random_params['next_ts'] = time.time() + delay auto_random_params['next_ts'] = time.time() + delay
auto_random_params['theme'] = state.current_soundboard.get('filename') 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 global auto_random_timer
auto_random_timer = threading.Timer(delay, _auto_random_tick) auto_random_timer = threading.Timer(delay, _auto_random_tick)
auto_random_timer.daemon = True auto_random_timer.daemon = True
@ -500,6 +529,8 @@ def api_soundboard_status():
dmin = auto_random_params.get('dmin') dmin = auto_random_params.get('dmin')
dmax = auto_random_params.get('dmax') dmax = auto_random_params.get('dmax')
current_theme = state.current_soundboard.get('filename') if state.current_soundboard else None 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 next_seconds = max(0, next_ts - time.time()) if next_ts else None
return jsonify({ return jsonify({
"active": active, "active": active,
@ -508,7 +539,9 @@ def api_soundboard_status():
"interval_max": imax, "interval_max": imax,
"delay_min": dmin, "delay_min": dmin,
"delay_max": dmax, "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), 'dmin': data.get('dmin', 3),
'dmax': data.get('dmax', 12), 'dmax': data.get('dmax', 12),
'next_ts': next_ts, '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()) delay = max(1, next_ts - time.time())
global auto_random_timer global auto_random_timer

View File

@ -144,6 +144,14 @@
<label class="form-label">Startverzögerung max (Minuten)</label> <label class="form-label">Startverzögerung max (Minuten)</label>
<input id="auto-delay-max" type="number" class="form-control" value="12" min="0" max="120"> <input id="auto-delay-max" type="number" class="form-control" value="12" min="0" max="120">
</div> </div>
<div class="col-md-3">
<label class="form-label">Max Wiederholungen pro Stunde</label>
<input id="auto-limit-ph" type="number" class="form-control" value="2" min="1" max="50">
</div>
<div class="col-md-3">
<label class="form-label">Limit-Fenster (Minuten)</label>
<input id="auto-window-min" type="number" class="form-control" value="60" min="1" max="240">
</div>
</div> </div>
<div class="mt-3 d-flex gap-2 align-items-center"> <div class="mt-3 d-flex gap-2 align-items-center">
<button id="auto-start" class="btn btn-success" ${!randBtn.disabled ? '' : 'disabled'}>Auto-Start</button> <button id="auto-start" class="btn btn-success" ${!randBtn.disabled ? '' : 'disabled'}>Auto-Start</button>
@ -161,11 +169,17 @@
const imax = Math.max(imin, parseInt(document.getElementById('auto-interval-max').value, 10) || 10); 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 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 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', { const res = await fetch('/api/soundboard/auto_start', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, 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(); const data = await res.json();
if (data.success) { if (data.success) {
@ -212,6 +226,8 @@
if (data.interval_max) document.getElementById('auto-interval-max').value = data.interval_max; 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_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.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 { } else {
status.textContent = 'Aus'; status.textContent = 'Aus';
start.disabled = false; start.disabled = false;