added themes to soundboard
This commit is contained in:
parent
5f9920f096
commit
21c0b13b8d
@ -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,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
|
||||
|
||||
@ -4,6 +4,7 @@ current_config = None
|
||||
current_hub = None
|
||||
current_module = None
|
||||
current_device = None
|
||||
current_soundboard = None # aktives Soundboard-Thema
|
||||
|
||||
# Optional: Funktionen zum Setzen/Resetten (für Klarheit)
|
||||
def reset_state():
|
||||
@ -11,4 +12,4 @@ def reset_state():
|
||||
current_config = None
|
||||
current_hub = None
|
||||
current_module = None
|
||||
current_device = None
|
||||
current_device = None
|
||||
|
||||
@ -87,3 +87,60 @@ def load_default_sounds(config_dir=None):
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden default_sounds.json: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# ---------- Soundboard-Themen ----------
|
||||
|
||||
def load_soundboard_configs(config_dir=None):
|
||||
"""
|
||||
Lädt alle Soundboard-Themen aus einem separaten Ordner (z. B. soundboards/).
|
||||
Unterstützt Unterordner. Jede *.json gilt als Thema (z. B. soundboards/station/theme.json).
|
||||
Erwartet Schema mit keys wie backgrounds / random_pool / sounds.
|
||||
"""
|
||||
config_dir = config_dir or _resolve_config_dir(None)
|
||||
configs = []
|
||||
if not os.path.exists(config_dir):
|
||||
logger.warning(f"Soundboard-Verzeichnis nicht gefunden: {config_dir}")
|
||||
return configs
|
||||
|
||||
for root, _, files in os.walk(config_dir):
|
||||
for filename in files:
|
||||
if not filename.lower().endswith('.json'):
|
||||
continue
|
||||
rel_path = os.path.relpath(os.path.join(root, filename), config_dir)
|
||||
try:
|
||||
with open(os.path.join(config_dir, rel_path), 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
data['filename'] = rel_path # relativer Pfad ab soundboards/
|
||||
data.setdefault('name', filename.replace('.json', '').replace('_', ' ').title())
|
||||
configs.append(data)
|
||||
except Exception as e:
|
||||
logger.error(f"Fehler beim Laden Soundboard {rel_path}: {e}")
|
||||
return sorted(configs, key=lambda x: x.get('name', ''))
|
||||
|
||||
|
||||
def load_soundboard_config(filename, config_dir, sounds_dir=None):
|
||||
"""
|
||||
Lädt ein Soundboard-Theme. Falls die Theme-Datei in Unterordnern liegt,
|
||||
wird ein basis Sound-Pfad auf denselben relativen Unterordner unterhalb von sounds/ gesetzt.
|
||||
Alle Einträge (backgrounds/sounds/random_pool) erhalten ein 'base_path'.
|
||||
"""
|
||||
sounds_dir = sounds_dir or 'sounds'
|
||||
path = os.path.normpath(os.path.join(config_dir, filename))
|
||||
if not path.startswith(os.path.abspath(config_dir)):
|
||||
raise ValueError("Ungültiger Pfad")
|
||||
|
||||
rel_dir = os.path.dirname(filename) # z.B. "station" oder "urban/night"
|
||||
base_sound_dir = os.path.normpath(os.path.join(sounds_dir, rel_dir)) if rel_dir else sounds_dir
|
||||
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
data['filename'] = filename # relative Angabe beibehalten
|
||||
data.setdefault('name', filename.replace('.json', '').replace('_', ' ').title())
|
||||
data['base_sound_dir'] = base_sound_dir
|
||||
|
||||
for key in ('backgrounds', 'sounds', 'random_pool'):
|
||||
if key in data and isinstance(data[key], list):
|
||||
for item in data[key]:
|
||||
item.setdefault('base_path', base_sound_dir)
|
||||
return data
|
||||
|
||||
@ -12,6 +12,7 @@ class Config:
|
||||
CONFIG_DIR = os.path.join(BASE_DIR, 'configs')
|
||||
LOG_DIR = os.path.join(BASE_DIR, 'logs')
|
||||
SOUNDS_DIR = os.path.join(BASE_DIR, 'sounds')
|
||||
SOUNDBOARD_CONFIG_DIR = os.path.join(BASE_DIR, 'soundboards')
|
||||
|
||||
# Audio-Parameter (für pygame.mixer)
|
||||
AUDIO_FREQ = 44100
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 31 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 93 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 80 KiB |
13
soundboards/wilder-westen/theme.json
Normal file
13
soundboards/wilder-westen/theme.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "Wilder Westen",
|
||||
"backgrounds": [
|
||||
{"id": "wind", "file": "freesound_community-wind-western-64661.mp3", "loop": true, "channel": 0}
|
||||
],
|
||||
"sounds": [
|
||||
{"id": "Pferdegalopp", "file": "dragon-studio-horse-galloping-sfx-339732.mp3", "channel": 2}
|
||||
],
|
||||
"random_pool": [
|
||||
{"id": "Gunshot1", "file": "magiaz-revolver_shots-407325.mp3"},
|
||||
{"id": "Horse2", "file": "dragon-studio-horse-whinny-sound-effect-339727.mp3"}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
sounds/wilder-westen/magiaz-revolver_shots-407325.mp3
Normal file
BIN
sounds/wilder-westen/magiaz-revolver_shots-407325.mp3
Normal file
Binary file not shown.
@ -1,117 +1,150 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Soundboard – {{ config.name }}{% endblock %}
|
||||
{% block title %}Soundboard – Themen{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-5">
|
||||
<h1>Soundboard – {{ config.name }}</h1>
|
||||
<p class="lead mb-4">Wähle einen Sound aus – wird direkt über den Raspberry Pi ausgegeben.</p>
|
||||
<h1>Soundboard</h1>
|
||||
<p class="lead mb-4">Themenbezogene Soundsets, unabhängig vom Hub.</p>
|
||||
|
||||
{% set has_local = sounds_local and sounds_local|length > 0 %}
|
||||
{% set has_global = sounds_global and sounds_global|length > 0 %}
|
||||
|
||||
{% if has_local %}
|
||||
<h4 class="mt-3 mb-3">Lok-spezifische Sounds</h4>
|
||||
<div class="row g-3">
|
||||
{% for sound in sounds_local %}
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">{{ sound.name }}</h5>
|
||||
{% if sound.description %}
|
||||
<p class="card-text text-muted small">{{ sound.description }}</p>
|
||||
{% endif %}
|
||||
<button class="btn btn-primary mt-3 play-sound-btn"
|
||||
data-sound-id="{{ sound.id }}"
|
||||
data-channel="{{ sound.channel | default('') }}"
|
||||
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||
<i class="bi bi-play-fill me-2"></i> Abspielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="row mb-4">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label fw-bold">Thema wählen</label>
|
||||
<select id="soundboard-select" class="form-select">
|
||||
{% for sb in soundboard_configs %}
|
||||
<option value="{{ sb.filename }}">{{ sb.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if has_global %}
|
||||
<h4 class="mt-5 mb-3">Standard-Sounds</h4>
|
||||
<div class="row g-3">
|
||||
{% for sound in sounds_global %}
|
||||
<div class="col-md-4 col-lg-3">
|
||||
<div class="card h-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<h5 class="card-title">{{ sound.name }}</h5>
|
||||
{% if sound.description %}
|
||||
<p class="card-text text-muted small">{{ sound.description }}</p>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-secondary mt-3 play-sound-btn"
|
||||
data-sound-id="{{ sound.id }}"
|
||||
data-channel="{{ sound.channel | default('') }}"
|
||||
data-loop="{{ 1 if sound.loop else 0 }}">
|
||||
<i class="bi bi-play-fill me-2"></i> Abspielen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="col-md-6 d-flex align-items-end">
|
||||
<div class="btn-group">
|
||||
<button id="btn-load-sb" class="btn btn-primary">Laden</button>
|
||||
<button id="btn-play-random" class="btn btn-outline-secondary" disabled>Zufällig (max 2/h pro Datei)</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if not has_local and not has_global %}
|
||||
<div class="alert alert-info">
|
||||
In dieser Konfiguration sind keine Sounds definiert und keine Default-Sounds vorhanden.
|
||||
</div>
|
||||
{% endif %}
|
||||
<div id="sb-content" style="display:none">
|
||||
<div id="sb-backgrounds" class="mb-4"></div>
|
||||
<div id="sb-sounds" class="mb-4"></div>
|
||||
<div id="sb-random" class="mb-4"></div>
|
||||
</div>
|
||||
|
||||
<div class="mt-5 text-center">
|
||||
<a href="{{ url_for('main.control_page') }}" class="btn btn-secondary">Zurück zur Steuerung</a>
|
||||
<div class="mt-4">
|
||||
<a href="{{ url_for('main.index') }}" class="btn btn-secondary">Zurück</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.querySelectorAll('.play-sound-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const soundId = btn.dataset.soundId;
|
||||
const channel = btn.dataset.channel || null;
|
||||
const loopFlag = btn.dataset.loop === '1' || btn.dataset.loop === 'true';
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span> Spielt...';
|
||||
const selectEl = document.getElementById('soundboard-select');
|
||||
const loadBtn = document.getElementById('btn-load-sb');
|
||||
const randBtn = document.getElementById('btn-play-random');
|
||||
const content = document.getElementById('sb-content');
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/play_sound', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ sound_id: soundId, channel, loop: loopFlag })
|
||||
});
|
||||
let currentSB = null;
|
||||
|
||||
const data = await res.json();
|
||||
async function loadSoundboard(filename) {
|
||||
loadBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch('/api/soundboard/load', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) throw new Error(data.message || 'Load failed');
|
||||
currentSB = data.soundboard;
|
||||
renderSoundboard(currentSB);
|
||||
content.style.display = 'block';
|
||||
randBtn.disabled = false;
|
||||
} catch (e) {
|
||||
alert('Konnte Soundboard nicht laden: ' + e.message);
|
||||
} finally {
|
||||
loadBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
btn.classList.remove('btn-primary');
|
||||
btn.classList.add('btn-success');
|
||||
btn.innerHTML = '<i class="bi bi-check-lg me-2"></i> Gespielt';
|
||||
setTimeout(() => {
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-primary');
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
btn.disabled = false;
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('Fehler: ' + (data.message || 'Unbekannt'));
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Sound-Play-Fehler:', err);
|
||||
alert('Netzwerkfehler beim Abspielen');
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="bi bi-play-fill me-2"></i> Abspielen';
|
||||
}
|
||||
function renderSoundboard(sb) {
|
||||
renderBackgrounds(sb.backgrounds || []);
|
||||
renderSounds(sb.sounds || []);
|
||||
renderRandom(sb.random_pool || []);
|
||||
}
|
||||
|
||||
function renderBackgrounds(list) {
|
||||
const container = document.getElementById('sb-backgrounds');
|
||||
if (!list.length) { container.innerHTML=''; return; }
|
||||
container.innerHTML = '<h4>Hintergrund (Loop)</h4><div class="d-flex flex-wrap gap-2"></div>';
|
||||
const wrap = container.querySelector('div');
|
||||
list.forEach(s => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-outline-secondary';
|
||||
btn.textContent = s.name || s.id || s.file;
|
||||
btn.onclick = () => playSound(s);
|
||||
const stop = document.createElement('button');
|
||||
stop.className = 'btn btn-outline-danger';
|
||||
stop.textContent = 'Stop';
|
||||
stop.onclick = () => stopSound(s.channel);
|
||||
wrap.append(btn, stop);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderSounds(list) {
|
||||
const container = document.getElementById('sb-sounds');
|
||||
if (!list.length) { container.innerHTML=''; return; }
|
||||
container.innerHTML = '<h4>Einzelsounds</h4><div class="d-flex flex-wrap gap-2"></div>';
|
||||
const wrap = container.querySelector('div');
|
||||
list.forEach(s => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = 'btn btn-primary';
|
||||
btn.textContent = s.name || s.id || s.file;
|
||||
btn.onclick = () => playSound(s);
|
||||
wrap.append(btn);
|
||||
});
|
||||
}
|
||||
|
||||
function renderRandom(list) {
|
||||
const container = document.getElementById('sb-random');
|
||||
if (!list.length) { container.innerHTML=''; randBtn.disabled=true; return; }
|
||||
container.innerHTML = '<p class="text-muted">Zufallspool: ' + list.length + ' Dateien, max 2x pro Stunde je Datei.</p>';
|
||||
}
|
||||
|
||||
async function playSound(sound) {
|
||||
const payload = {
|
||||
sound_id: sound.id || sound.file,
|
||||
channel: sound.channel ?? null,
|
||||
loop: !!sound.loop
|
||||
};
|
||||
const res = await fetch('/api/play_sound', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) alert(data.message || 'Fehler beim Abspielen');
|
||||
}
|
||||
|
||||
async function stopSound(channel) {
|
||||
const res = await fetch('/api/stop_sound', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ channel })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!data.success) alert(data.message || 'Fehler beim Stoppen');
|
||||
}
|
||||
|
||||
loadBtn.onclick = () => loadSoundboard(selectEl.value);
|
||||
randBtn.onclick = async () => {
|
||||
const res = await fetch('/api/soundboard/play_random', { method: 'POST' });
|
||||
const data = await res.json();
|
||||
if (!data.success) alert(data.message || 'Random fehlgeschlagen');
|
||||
};
|
||||
|
||||
// Autoload erstes Thema (falls vorhanden)
|
||||
if (selectEl && selectEl.value) {
|
||||
loadSoundboard(selectEl.value);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user