fixxes for background playbacks

This commit is contained in:
oberon 2026-02-18 22:35:10 +01:00
parent f580d67ccb
commit 36d1a36549
3 changed files with 136 additions and 50 deletions

View File

@ -15,6 +15,9 @@ from app.utils.helpers import load_default_sounds, load_soundboard_configs, load
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
audio_lock = threading.Lock() audio_lock = threading.Lock()
auto_random_lock = threading.Lock()
auto_random_timer = None
auto_random_params = {}
# Sound-Cache: path -> pygame.mixer.Sound # Sound-Cache: path -> pygame.mixer.Sound
loaded_sounds = {} loaded_sounds = {}
@ -82,6 +85,55 @@ def _play_sound_entry(sound_entry, channel_req=None, loop_req=0):
return None # Erfolg 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): def _prune_history(sound_id, now):
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:
@ -331,6 +383,8 @@ def api_soundboard_load():
current_app.config['SOUNDBOARD_CONFIG_DIR'], current_app.config['SOUNDBOARD_CONFIG_DIR'],
current_app.config['SOUNDS_DIR']) current_app.config['SOUNDS_DIR'])
state.current_soundboard = sb state.current_soundboard = sb
# Stoppe ggf. laufende Auto-Randoms beim Laden neuer Themen
_stop_auto_random_internal()
logger.info(f"Soundboard geladen: {filename}") logger.info(f"Soundboard geladen: {filename}")
return jsonify({"success": True, "soundboard": sb}) return jsonify({"success": True, "soundboard": sb})
except Exception as e: except Exception as e:
@ -343,30 +397,69 @@ def api_soundboard_play_random():
if state.current_soundboard is None: if state.current_soundboard is None:
return jsonify({"success": False, "message": "Kein Soundboard geladen"}), 400 return jsonify({"success": False, "message": "Kein Soundboard geladen"}), 400
rnd_list = state.current_soundboard.get('random_pool') or state.current_soundboard.get('sounds', []) sound_entry, err = _pick_random_sound()
if not rnd_list: if err:
return jsonify({"success": False, "message": "Keine Random-Sounds definiert"}), 400 return jsonify({"success": False, "message": err}), 429 if "Limit" in err else 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')
res = _play_sound_entry(sound_entry) res = _play_sound_entry(sound_entry)
if res is not None: if res is not None:
return res # already Response return res # already Response
random_history[sid].append(now) return jsonify({"success": True, "message": f"Random: {sound_entry.get('name', sound_entry.get('id'))}"})
return jsonify({"success": True, "message": f"Random: {sound_entry.get('name', sid)}"})
@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']) @api_bp.route('/stop_sound', methods=['POST'])

View File

@ -5,6 +5,7 @@ current_hub = None
current_module = None current_module = None
current_device = None current_device = None
current_soundboard = None # aktives Soundboard-Thema current_soundboard = None # aktives Soundboard-Thema
auto_random_active = False
# Optional: Funktionen zum Setzen/Resetten (für Klarheit) # Optional: Funktionen zum Setzen/Resetten (für Klarheit)
def reset_state(): def reset_state():

View File

@ -156,44 +156,36 @@
const stop = document.getElementById('auto-stop'); const stop = document.getElementById('auto-stop');
const status = document.getElementById('auto-status'); 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 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 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 delayMs = randBetween(dmin, dmax) * 60 * 1000; const res = await fetch('/api/soundboard/auto_start', {
scheduleAuto(imin, imax, delayMs, status, start, stop); 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); stop.onclick = async () => {
} const res = await fetch('/api/soundboard/auto_stop', { method: 'POST' });
const data = await res.json();
function randBetween(min, max) { if (data.success) {
return min + Math.random() * (max - min); status.textContent = 'Aus';
} start.disabled = false;
stop.disabled = true;
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;
} }
async function playSound(sound) { async function playSound(sound) {