added themes to soundboard
This commit is contained in:
+115
-37
@@ -3,19 +3,24 @@ import os
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict, deque
|
||||
|
||||
import pygame
|
||||
from flask import Blueprint, jsonify, request, current_app
|
||||
|
||||
import app.state as state
|
||||
from app.bluetooth.manager import MouldKing, advertiser, tracer
|
||||
from app.utils.helpers import load_default_sounds
|
||||
from app.utils.helpers import load_default_sounds, load_soundboard_configs, load_soundboard_config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
audio_lock = threading.Lock()
|
||||
|
||||
# Sound-Cache: path -> pygame.mixer.Sound
|
||||
loaded_sounds = {}
|
||||
random_history = defaultdict(deque) # sound_id -> deque[timestamps]
|
||||
MAX_PER_HOUR = 2
|
||||
WINDOW_SECONDS = 3600
|
||||
|
||||
def _ensure_mixer():
|
||||
from config import Config
|
||||
@@ -34,6 +39,55 @@ def _load_sound(file_path):
|
||||
loaded_sounds[file_path] = snd
|
||||
return snd
|
||||
|
||||
|
||||
def _play_sound_entry(sound_entry, channel_req=None, loop_req=0):
|
||||
sounds_dir = current_app.config['SOUNDS_DIR']
|
||||
base_path = sound_entry.get('base_path') or sounds_dir
|
||||
file_path = os.path.join(base_path, sound_entry['file'])
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Sound-Datei nicht gefunden: {file_path}")
|
||||
return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404
|
||||
|
||||
# Abspielen (serialisiert, aber mehrere Channels erlaubt)
|
||||
with audio_lock:
|
||||
_ensure_mixer()
|
||||
|
||||
# Sound laden (Cache)
|
||||
snd = _load_sound(file_path)
|
||||
|
||||
# Ziel-Channel bestimmen
|
||||
ch = None
|
||||
if channel_req not in [None, ""]:
|
||||
try:
|
||||
ch_id = int(channel_req)
|
||||
ch = pygame.mixer.Channel(ch_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ungültiger channel-Wert '{channel_req}': {e}")
|
||||
return jsonify({"success": False, "message": "Ungültiger Channel"}), 400
|
||||
else:
|
||||
ch = pygame.mixer.find_channel(True) # zwingend freien nehmen
|
||||
|
||||
if ch is None:
|
||||
return jsonify({"success": False, "message": "Kein freier Audio-Channel verfügbar"}), 503
|
||||
|
||||
# Loop-Wert interpretieren
|
||||
loops = -1 if loop_req in [True, "true", "True", -1, "loop", "1", 1] else 0
|
||||
|
||||
try:
|
||||
ch.play(snd, loops=loops)
|
||||
except Exception as e:
|
||||
logger.exception(f"Fehler beim Abspielen von {file_path}")
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
return None # Erfolg
|
||||
|
||||
|
||||
def _prune_history(sound_id, now):
|
||||
dq = random_history[sound_id]
|
||||
while dq and now - dq[0] > WINDOW_SECONDS:
|
||||
dq.popleft()
|
||||
return dq
|
||||
|
||||
api_bp = Blueprint('api', __name__)
|
||||
|
||||
@api_bp.route('/connect', methods=['POST'])
|
||||
@@ -237,42 +291,9 @@ def api_play_sound():
|
||||
if not sound_entry:
|
||||
return jsonify({"success": False, "message": f"Sound mit ID '{sound_id}' nicht gefunden"}), 404
|
||||
|
||||
sounds_dir = current_app.config['SOUNDS_DIR']
|
||||
file_path = os.path.join(sounds_dir, sound_entry['file'])
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Sound-Datei nicht gefunden: {file_path}")
|
||||
return jsonify({"success": False, "message": "Sound-Datei nicht gefunden"}), 404
|
||||
|
||||
# Abspielen (serialisiert, aber mehrere Channels erlaubt)
|
||||
with audio_lock:
|
||||
_ensure_mixer()
|
||||
|
||||
# Sound laden (Cache)
|
||||
snd = _load_sound(file_path)
|
||||
|
||||
# Ziel-Channel bestimmen
|
||||
ch = None
|
||||
if channel_req not in [None, ""]:
|
||||
try:
|
||||
ch_id = int(channel_req)
|
||||
ch = pygame.mixer.Channel(ch_id)
|
||||
except Exception as e:
|
||||
logger.error(f"Ungültiger channel-Wert '{channel_req}': {e}")
|
||||
return jsonify({"success": False, "message": "Ungültiger Channel"}), 400
|
||||
else:
|
||||
ch = pygame.mixer.find_channel(True) # zwingend freien nehmen
|
||||
|
||||
if ch is None:
|
||||
return jsonify({"success": False, "message": "Kein freier Audio-Channel verfügbar"}), 503
|
||||
|
||||
# Loop-Wert interpretieren
|
||||
loops = -1 if loop_req in [True, "true", "True", -1, "loop", "1", 1] else 0
|
||||
|
||||
try:
|
||||
ch.play(snd, loops=loops)
|
||||
except Exception as e:
|
||||
logger.exception(f"Fehler beim Abspielen von {file_path}")
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
res = _play_sound_entry(sound_entry, channel_req, loop_req)
|
||||
if res is not None:
|
||||
return res
|
||||
|
||||
logger.info(f"Spiele Sound: {sound_entry['name']} ({sound_id})")
|
||||
return jsonify({"success": True, "message": f"Spiele: {sound_entry['name']}"})
|
||||
@@ -283,6 +304,63 @@ def api_play_sound():
|
||||
pass
|
||||
|
||||
|
||||
# ---------- Soundboard (themenbezogen, hub-unabhängig) ----------
|
||||
|
||||
@api_bp.route('/soundboard/configs', methods=['GET'])
|
||||
def api_soundboard_configs():
|
||||
configs = load_soundboard_configs(current_app.config['SOUNDBOARD_CONFIG_DIR'])
|
||||
return jsonify(configs)
|
||||
|
||||
|
||||
@api_bp.route('/soundboard/load', methods=['POST'])
|
||||
def api_soundboard_load():
|
||||
data = request.get_json()
|
||||
filename = data.get('filename')
|
||||
if not filename:
|
||||
return jsonify({"success": False, "message": "filename fehlt"}), 400
|
||||
try:
|
||||
sb = load_soundboard_config(filename,
|
||||
current_app.config['SOUNDBOARD_CONFIG_DIR'],
|
||||
current_app.config['SOUNDS_DIR'])
|
||||
state.current_soundboard = sb
|
||||
logger.info(f"Soundboard geladen: {filename}")
|
||||
return jsonify({"success": True, "soundboard": sb})
|
||||
except Exception as e:
|
||||
logger.exception("Soundboard laden fehlgeschlagen")
|
||||
return jsonify({"success": False, "message": str(e)}), 500
|
||||
|
||||
|
||||
@api_bp.route('/soundboard/play_random', methods=['POST'])
|
||||
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')
|
||||
|
||||
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)}"})
|
||||
|
||||
|
||||
@api_bp.route('/stop_sound', methods=['POST'])
|
||||
def api_stop_sound():
|
||||
try:
|
||||
|
||||
+4
-9
@@ -4,7 +4,7 @@ import logging
|
||||
import json
|
||||
|
||||
from flask import Blueprint, render_template, redirect, url_for, request, jsonify, send_from_directory, current_app
|
||||
from app.utils.helpers import load_configs, load_default_sounds
|
||||
from app.utils.helpers import load_configs, load_default_sounds, load_soundboard_configs
|
||||
import app.state as state
|
||||
|
||||
from config import Config
|
||||
@@ -78,13 +78,8 @@ def control_page():
|
||||
|
||||
@main_bp.route('/soundboard')
|
||||
def soundboard():
|
||||
if state.current_config is None:
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
sounds_local = state.current_config.get('sounds', [])
|
||||
sounds_global = load_default_sounds(current_app.config['CONFIG_DIR'])
|
||||
# Soundboard ist bewusst unabhängig vom Hub; es werden Sound-Themen geladen
|
||||
sb_configs = load_soundboard_configs(current_app.config['SOUNDBOARD_CONFIG_DIR'])
|
||||
return render_template('soundboard.html',
|
||||
sounds_local=sounds_local,
|
||||
sounds_global=sounds_global,
|
||||
config=state.current_config)
|
||||
soundboard_configs=sb_configs)
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user