From b70703d51b18362a0c391d53a69afa157f966142 Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Fri, 8 Aug 2025 18:40:40 +0200 Subject: [PATCH] feat(mp3): Erfolg-/Fehlerstatus beim Download; Panik-Button (Stop-Endpoint) und UI-Badge; interne playFilePath-Hilfe --- server/src/index.ts | 53 ++++++++++++++++++++++++++++++++++++++++++--- web/src/App.tsx | 17 +++++++++++---- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index a000fd5..4dcd081 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -98,6 +98,36 @@ type GuildAudioState = { }; const guildAudioState = new Map(); +async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number): Promise { + const guild = client.guilds.cache.get(guildId); + if (!guild) throw new Error('Guild nicht gefunden'); + let state = guildAudioState.get(guildId); + if (!state) { + const connection = joinVoiceChannel({ + channelId, + guildId, + adapterCreator: guild.voiceAdapterCreator as any, + selfMute: false, + selfDeaf: false + }); + const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); + connection.subscribe(player); + state = { connection, player, guildId, channelId, currentVolume: getPersistedVolume(guildId) }; + guildAudioState.set(guildId, state); + state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); + attachVoiceLifecycle(state, guild); + } + const useVolume = typeof volume === 'number' && Number.isFinite(volume) + ? Math.max(0, Math.min(1, volume)) + : (state.currentVolume ?? 1); + const resource = createAudioResource(filePath, { inlineVolume: true }); + if (resource.volume) resource.volume.setVolume(useVolume); + state.player.stop(); + state.player.play(resource); + state.currentResource = resource; + state.currentVolume = useVolume; +} + async function ensureConnectionReady(connection: VoiceConnection, channelId: string, guildId: string, guild: any): Promise { try { await entersState(connection, VoiceConnectionStatus.Ready, 15_000); @@ -568,6 +598,20 @@ app.get('/api/volume', (req: Request, res: Response) => { return res.json({ volume: v }); }); +// Panik: Stoppe aktuelle Wiedergabe sofort +app.post('/api/stop', (req: Request, res: Response) => { + try { + const guildId = String((req.query.guildId || (req.body as any)?.guildId) ?? ''); + if (!guildId) return res.status(400).json({ error: 'guildId erforderlich' }); + const state = guildAudioState.get(guildId); + if (!state) return res.status(404).json({ error: 'Kein aktiver Player' }); + state.player.stop(true); + return res.json({ ok: true }); + } catch (e: any) { + 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)) { @@ -597,9 +641,12 @@ app.post('/api/play-url', async (req: Request, res: Response) => { if (!r.ok) return res.status(400).json({ error: 'Download fehlgeschlagen' }); const buf = Buffer.from(await r.arrayBuffer()); fs.writeFileSync(dest, buf); - // sofort abspielen - req.body = { soundName: path.parse(fileName).name, guildId, channelId, volume, relativePath: fileName } as any; - return (app._router as any).handle({ ...req, method: 'POST', url: '/api/play' }, res, () => {}); + try { + await playFilePath(guildId, channelId, dest, volume); + } catch { + return res.status(500).json({ error: 'Abspielen fehlgeschlagen' }); + } + return res.json({ ok: true, saved: path.basename(dest) }); } return res.status(400).json({ error: 'Nur MP3-Links werden unterstützt.' }); } catch (e: any) { diff --git a/web/src/App.tsx b/web/src/App.tsx index 31b9889..25ab1b2 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -14,6 +14,7 @@ export default function App() { const [selected, setSelected] = useState(''); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [info, setInfo] = useState(null); const [volume, setVolume] = useState(1); const [favs, setFavs] = useState>({}); const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'dark'); @@ -128,7 +129,14 @@ export default function App() {

Einmal mit Soundboard -Profis

{clock}
-
Geladene Sounds: {total}
+
+
Geladene Sounds: {total}
+ +
{isAdmin && (
Admin-Modus
)} @@ -192,10 +200,10 @@ export default function App() { placeholder="MP3 URL..." /> @@ -279,6 +287,7 @@ export default function App() { )} {error &&
{error}
} + {info &&
{info}
}
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {