feat(volume): serverseitig pro Guild persistieren (state.json), API GET/POST /api/volume; Frontend lädt gespeicherte Lautstärke

This commit is contained in:
vibe-bot 2025-08-08 13:46:27 +02:00
parent fee1feaca4
commit beeffb7605
3 changed files with 71 additions and 5 deletions

View file

@ -39,6 +39,36 @@ if (!DISCORD_TOKEN) {
fs.mkdirSync(SOUNDS_DIR, { recursive: true });
// Persistente Lautstärke pro Guild speichern
type PersistedState = { volumes: Record<string, number> };
const STATE_FILE = path.join(path.resolve(SOUNDS_DIR, '..'), 'state.json');
function readPersistedState(): PersistedState {
try {
if (fs.existsSync(STATE_FILE)) {
const raw = fs.readFileSync(STATE_FILE, 'utf8');
const parsed = JSON.parse(raw);
return { volumes: parsed.volumes ?? {} } as PersistedState;
}
} catch {}
return { volumes: {} };
}
function writePersistedState(state: PersistedState): void {
try {
fs.mkdirSync(path.dirname(STATE_FILE), { recursive: true });
fs.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
} catch (e) {
console.warn('Persisted state konnte nicht geschrieben werden:', e);
}
}
const persistedState: PersistedState = readPersistedState();
const getPersistedVolume = (guildId: string): number => {
const v = persistedState.volumes[guildId];
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
};
// --- Voice Abhängigkeiten prüfen ---
await sodium.ready;
// init nacl to ensure it loads
@ -284,7 +314,7 @@ app.post('/api/play', async (req: Request, res: Response) => {
});
const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } });
connection.subscribe(player);
state = { connection, player, guildId, channelId, currentVolume: 1 };
state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) };
guildAudioState.set(guildId, state);
// Connection State Logs
@ -331,7 +361,7 @@ app.post('/api/play', async (req: Request, res: Response) => {
});
const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } });
connection.subscribe(player);
state = { connection, player, guildId, channelId, currentVolume: 1 };
state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) };
guildAudioState.set(guildId, state);
state.connection = await ensureConnectionReady(connection, channelId, guildId, guild);
@ -363,6 +393,9 @@ app.post('/api/play', async (req: Request, res: Response) => {
state.player.play(resource);
state.currentResource = resource;
state.currentVolume = volumeToUse;
// Persistieren
persistedState.volumes[guildId] = volumeToUse;
writePersistedState(persistedState);
console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`);
return res.json({ ok: true });
} catch (err: any) {
@ -381,13 +414,18 @@ app.post('/api/volume', (req: Request, res: Response) => {
const safeVolume = Math.max(0, Math.min(1, volume));
const state = guildAudioState.get(guildId);
if (!state) {
return res.status(404).json({ error: 'Kein Voice-State für diese Guild' });
// Kein aktiver Player: nur persistieren für nächste Wiedergabe
persistedState.volumes[guildId] = safeVolume;
writePersistedState(persistedState);
return res.json({ ok: true, volume: safeVolume, persistedOnly: true });
}
state.currentVolume = safeVolume;
if (state.currentResource?.volume) {
state.currentResource.volume.setVolume(safeVolume);
console.log(`${new Date().toISOString()} | live setVolume(${safeVolume}) guild=${guildId}`);
}
persistedState.volumes[guildId] = safeVolume;
writePersistedState(persistedState);
return res.json({ ok: true, volume: safeVolume });
} catch (e: any) {
console.error('Volume-Fehler:', e);
@ -395,6 +433,15 @@ app.post('/api/volume', (req: Request, res: Response) => {
}
});
// Aktuelle/gespeicherte Lautstärke abrufen
app.get('/api/volume', (req: Request, res: Response) => {
const guildId = String(req.query.guildId ?? '');
if (!guildId) return res.status(400).json({ error: 'guildId erforderlich' });
const state = guildAudioState.get(guildId);
const v = state?.currentVolume ?? getPersistedVolume(guildId);
return res.json({ volume: v });
});
// Static Frontend ausliefern (Vite build)
const webDistPath = path.resolve(__dirname, '../../web/dist');
if (fs.existsSync(webDistPath)) {

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { fetchChannels, fetchSounds, playSound, setVolumeLive } from './api';
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume } from './api';
import type { VoiceChannelInfo, Sound } from './types';
import { getCookie, setCookie } from './cookies';
@ -68,7 +68,17 @@ export default function App() {
}, [theme]);
useEffect(() => {
if (selected) localStorage.setItem('selectedChannel', selected);
(async () => {
if (selected) {
localStorage.setItem('selectedChannel', selected);
// gespeicherte Lautstärke vom Server laden
try {
const [guildId] = selected.split(':');
const v = await getVolume(guildId);
setVolume(v);
} catch {}
}
})();
}, [selected]);
const filtered = useMemo(() => {

View file

@ -41,6 +41,15 @@ export async function setVolumeLive(guildId: string, volume: number): Promise<vo
}
}
export async function getVolume(guildId: string): Promise<number> {
const url = new URL(`${API_BASE}/volume`, window.location.origin);
url.searchParams.set('guildId', guildId);
const res = await fetch(url.toString());
if (!res.ok) throw new Error('Fehler beim Laden der Lautstärke');
const data = await res.json();
return typeof data?.volume === 'number' ? data.volume : 1;
}