Nightly: Partymode-Status global per SSE /api/events Broadcast; Panic/Stop/Start senden Status an alle Clients
This commit is contained in:
parent
23b90b5923
commit
21b4e9bd0c
3 changed files with 51 additions and 1 deletions
|
|
@ -152,6 +152,14 @@ const guildAudioState = new Map<string, GuildAudioState>();
|
||||||
// Partymode: serverseitige Steuerung (global pro Guild)
|
// Partymode: serverseitige Steuerung (global pro Guild)
|
||||||
const partyTimers = new Map<string, NodeJS.Timeout>();
|
const partyTimers = new Map<string, NodeJS.Timeout>();
|
||||||
const partyActive = new Set<string>();
|
const partyActive = new Set<string>();
|
||||||
|
// SSE-Klienten für Broadcasts (z.B. Partymode Status)
|
||||||
|
const sseClients = new Set<Response>();
|
||||||
|
function sseBroadcast(payload: any) {
|
||||||
|
const data = `data: ${JSON.stringify(payload)}\n\n`;
|
||||||
|
for (const res of sseClients) {
|
||||||
|
try { res.write(data); } catch {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
||||||
const guild = client.guilds.cache.get(guildId);
|
const guild = client.guilds.cache.get(guildId);
|
||||||
|
|
@ -871,6 +879,7 @@ app.post('/api/stop', (req: Request, res: Response) => {
|
||||||
if (t) clearTimeout(t);
|
if (t) clearTimeout(t);
|
||||||
partyTimers.delete(guildId);
|
partyTimers.delete(guildId);
|
||||||
partyActive.delete(guildId);
|
partyActive.delete(guildId);
|
||||||
|
sseBroadcast({ type: 'party', guildId, active: false });
|
||||||
} catch {}
|
} catch {}
|
||||||
return res.json({ ok: true });
|
return res.json({ ok: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
@ -921,6 +930,8 @@ function schedulePartyPlayback(guildId: string, channelId: string) {
|
||||||
// Start: sofort spielen und nächste planen
|
// Start: sofort spielen und nächste planen
|
||||||
partyActive.add(guildId);
|
partyActive.add(guildId);
|
||||||
void loop();
|
void loop();
|
||||||
|
// Broadcast Status
|
||||||
|
sseBroadcast({ type: 'party', guildId, active: true, channelId });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.post('/api/party/start', async (req: Request, res: Response) => {
|
app.post('/api/party/start', async (req: Request, res: Response) => {
|
||||||
|
|
@ -946,6 +957,7 @@ app.post('/api/party/stop', (req: Request, res: Response) => {
|
||||||
const t = partyTimers.get(id); if (t) clearTimeout(t);
|
const t = partyTimers.get(id); if (t) clearTimeout(t);
|
||||||
partyTimers.delete(id);
|
partyTimers.delete(id);
|
||||||
partyActive.delete(id);
|
partyActive.delete(id);
|
||||||
|
sseBroadcast({ type: 'party', guildId: id, active: false });
|
||||||
return res.json({ ok: true });
|
return res.json({ ok: true });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('party/stop error', e);
|
console.error('party/stop error', e);
|
||||||
|
|
@ -953,6 +965,23 @@ app.post('/api/party/stop', (req: Request, res: Response) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Server-Sent Events Endpoint
|
||||||
|
app.get('/api/events', (req: Request, res: Response) => {
|
||||||
|
res.setHeader('Content-Type', 'text/event-stream');
|
||||||
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
|
res.setHeader('Connection', 'keep-alive');
|
||||||
|
res.flushHeaders?.();
|
||||||
|
|
||||||
|
// Snapshot senden
|
||||||
|
try { res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive) })}\n\n`); } catch {}
|
||||||
|
|
||||||
|
sseClients.add(res);
|
||||||
|
req.on('close', () => {
|
||||||
|
sseClients.delete(res);
|
||||||
|
try { res.end(); } catch {}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Static Frontend ausliefern (Vite build)
|
// Static Frontend ausliefern (Vite build)
|
||||||
const webDistPath = path.resolve(__dirname, '../../web/dist');
|
const webDistPath = path.resolve(__dirname, '../../web/dist');
|
||||||
if (fs.existsSync(webDistPath)) {
|
if (fs.existsSync(webDistPath)) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop } from './api';
|
import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents } from './api';
|
||||||
import type { VoiceChannelInfo, Sound, Category } from './types';
|
import type { VoiceChannelInfo, Sound, Category } from './types';
|
||||||
import { getCookie, setCookie } from './cookies';
|
import { getCookie, setCookie } from './cookies';
|
||||||
|
|
||||||
|
|
@ -74,6 +74,19 @@ export default function App() {
|
||||||
const h = await fetch('/api/health').then(r => r.json()).catch(() => null);
|
const h = await fetch('/api/health').then(r => r.json()).catch(() => null);
|
||||||
if (h && typeof h.totalPlays === 'number') setTotalPlays(h.totalPlays);
|
if (h && typeof h.totalPlays === 'number') setTotalPlays(h.totalPlays);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
// SSE: Partymode Status global synchronisieren
|
||||||
|
const unsub = subscribeEvents((msg)=>{
|
||||||
|
if (msg?.type === 'party') {
|
||||||
|
const [gid] = (selected||'').split(':');
|
||||||
|
if (gid && msg.guildId === gid) {
|
||||||
|
setChaosMode(!!msg.active);
|
||||||
|
}
|
||||||
|
} else if (msg?.type === 'snapshot') {
|
||||||
|
const [gid] = (selected||'').split(':');
|
||||||
|
if (gid) setChaosMode(msg.party?.includes(gid));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => { try { unsub(); } catch {} };
|
||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,14 @@ export async function partyStop(guildId: string) {
|
||||||
if (!res.ok) throw new Error('Partymode Stop fehlgeschlagen');
|
if (!res.ok) throw new Error('Partymode Stop fehlgeschlagen');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function subscribeEvents(onMessage: (data: any)=>void) {
|
||||||
|
const ev = new EventSource(`${API_BASE}/events`);
|
||||||
|
ev.onmessage = (e) => {
|
||||||
|
try { const data = JSON.parse(e.data); onMessage(data); } catch {}
|
||||||
|
};
|
||||||
|
return () => ev.close();
|
||||||
|
}
|
||||||
|
|
||||||
export async function setVolumeLive(guildId: string, volume: number): Promise<void> {
|
export async function setVolumeLive(guildId: string, volume: number): Promise<void> {
|
||||||
const res = await fetch(`${API_BASE}/volume`, {
|
const res = await fetch(`${API_BASE}/volume`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue