From 9bb402edd3f4b812049e3e712c50faf488e28785 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sat, 9 Aug 2025 22:43:13 +0200 Subject: [PATCH] Nightly: Partymode serverseitig Start/Stop Endpunkte + Panic stoppt global; Frontend triggert Party-Start/Stop --- server/src/index.ts | 85 +++++++++++++++++++++++++++++++++++++++++++++ web/src/App.tsx | 8 +++-- web/src/api.ts | 16 +++++++++ 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 62ce229..f792dd2 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -149,6 +149,9 @@ type GuildAudioState = { currentVolume: number; // 0..1 }; const guildAudioState = new Map(); +// Partymode: serverseitige Steuerung (global pro Guild) +const partyTimers = new Map(); +const partyActive = new Set(); async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise { const guild = client.guilds.cache.get(guildId); @@ -862,12 +865,94 @@ app.post('/api/stop', (req: Request, res: Response) => { const state = guildAudioState.get(guildId); if (!state) return res.status(404).json({ error: 'Kein aktiver Player' }); state.player.stop(true); + // Partymode für diese Guild ebenfalls stoppen + try { + const t = partyTimers.get(guildId); + if (t) clearTimeout(t); + partyTimers.delete(guildId); + partyActive.delete(guildId); + } catch {} return res.json({ ok: true }); } catch (e: any) { return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' }); } }); +// --- Partymode (serverseitig) --- +function schedulePartyPlayback(guildId: string, channelId: string) { + const MIN_DELAY = 30_000; // 30s + const MAX_EXTRA = 60_000; // +0..60s => 30..90s + + const doPlay = async () => { + try { + // Dateien ermitteln (mp3/wav, inkl. Subfolder) + const rootEntries = fs.readdirSync(SOUNDS_DIR, { withFileTypes: true }); + const pick: string[] = []; + for (const d of rootEntries) { + if (d.isFile()) { + const l = d.name.toLowerCase(); if (l.endsWith('.mp3') || l.endsWith('.wav')) pick.push(path.join(SOUNDS_DIR, d.name)); + } else if (d.isDirectory()) { + const folderPath = path.join(SOUNDS_DIR, d.name); + const entries = fs.readdirSync(folderPath, { withFileTypes: true }); + for (const e of entries) { + if (!e.isFile()) continue; + const n = e.name.toLowerCase(); + if (n.endsWith('.mp3') || n.endsWith('.wav')) pick.push(path.join(folderPath, e.name)); + } + } + } + if (pick.length === 0) return; + const filePath = pick[Math.floor(Math.random() * pick.length)]; + await playFilePath(guildId, channelId, filePath); + } catch (err) { + console.error('Partymode play error:', err); + } + }; + + const loop = async () => { + if (!partyActive.has(guildId)) return; + await doPlay(); + if (!partyActive.has(guildId)) return; + const delay = MIN_DELAY + Math.floor(Math.random() * MAX_EXTRA); + const t = setTimeout(loop, delay); + partyTimers.set(guildId, t); + }; + + // Start: sofort spielen und nächste planen + partyActive.add(guildId); + void loop(); +} + +app.post('/api/party/start', async (req: Request, res: Response) => { + try { + const { guildId, channelId } = req.body as { guildId?: string; channelId?: string }; + if (!guildId || !channelId) return res.status(400).json({ error: 'guildId und channelId erforderlich' }); + // vorhandenen Timer stoppen + const old = partyTimers.get(guildId); if (old) clearTimeout(old); + partyTimers.delete(guildId); + schedulePartyPlayback(guildId, channelId); + return res.json({ ok: true }); + } catch (e: any) { + console.error('party/start error', e); + return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' }); + } +}); + +app.post('/api/party/stop', (req: Request, res: Response) => { + try { + const { guildId } = req.body as { guildId?: string }; + const id = String(guildId ?? ''); + if (!id) return res.status(400).json({ error: 'guildId erforderlich' }); + const t = partyTimers.get(id); if (t) clearTimeout(t); + partyTimers.delete(id); + partyActive.delete(id); + return res.json({ ok: true }); + } catch (e: any) { + console.error('party/stop error', e); + return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' }); + } +}); + // 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 589573e..b8d60cc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, assignBadges, clearBadges, updateCategory, deleteCategory } from './api'; +import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, assignBadges, clearBadges, updateCategory, deleteCategory, partyStart, partyStop } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; @@ -243,9 +243,13 @@ export default function App() { if (chaosMode) { setChaosMode(false); await stopChaosMode(); + // serverseitig stoppen + if (selected) { const [guildId] = selected.split(':'); try { await partyStop(guildId); } catch {} } } else { setChaosMode(true); await startChaosMode(); + // serverseitig starten + if (selected) { const [guildId, channelId] = selected.split(':'); try { await partyStart(guildId, channelId); } catch {} } } }; @@ -310,7 +314,7 @@ export default function App() { > Partymode - + diff --git a/web/src/api.ts b/web/src/api.ts index df6b535..f43bc7a 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -90,6 +90,22 @@ export async function playSound(soundName: string, guildId: string, channelId: s } } +export async function partyStart(guildId: string, channelId: string) { + const res = await fetch(`${API_BASE}/party/start`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId, channelId }) + }); + if (!res.ok) throw new Error('Partymode Start fehlgeschlagen'); +} + +export async function partyStop(guildId: string) { + const res = await fetch(`${API_BASE}/party/stop`, { + method: 'POST', headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ guildId }) + }); + if (!res.ok) throw new Error('Partymode Stop fehlgeschlagen'); +} + export async function setVolumeLive(guildId: string, volume: number): Promise { const res = await fetch(`${API_BASE}/volume`, { method: 'POST',