diff --git a/server/src/index.ts b/server/src/index.ts index 7033628..2911839 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -39,6 +39,36 @@ if (!DISCORD_TOKEN) { fs.mkdirSync(SOUNDS_DIR, { recursive: true }); +// Persistente Lautstärke pro Guild speichern +type PersistedState = { volumes: Record }; +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)) { diff --git a/web/src/App.tsx b/web/src/App.tsx index c22e185..149e964 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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(() => { diff --git a/web/src/api.ts b/web/src/api.ts index 0a425cc..52c72b9 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -41,6 +41,15 @@ export async function setVolumeLive(guildId: string, volume: number): Promise { + 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; +} +