feat(mp3): Erfolg-/Fehlerstatus beim Download; Panik-Button (Stop-Endpoint) und UI-Badge; interne playFilePath-Hilfe
This commit is contained in:
parent
c2bd7b4503
commit
b70703d51b
2 changed files with 63 additions and 7 deletions
|
|
@ -98,6 +98,36 @@ type GuildAudioState = {
|
||||||
};
|
};
|
||||||
const guildAudioState = new Map<string, GuildAudioState>();
|
const guildAudioState = new Map<string, GuildAudioState>();
|
||||||
|
|
||||||
|
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number): Promise<void> {
|
||||||
|
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<VoiceConnection> {
|
async function ensureConnectionReady(connection: VoiceConnection, channelId: string, guildId: string, guild: any): Promise<VoiceConnection> {
|
||||||
try {
|
try {
|
||||||
await entersState(connection, VoiceConnectionStatus.Ready, 15_000);
|
await entersState(connection, VoiceConnectionStatus.Ready, 15_000);
|
||||||
|
|
@ -568,6 +598,20 @@ app.get('/api/volume', (req: Request, res: Response) => {
|
||||||
return res.json({ volume: v });
|
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)
|
// 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)) {
|
||||||
|
|
@ -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' });
|
if (!r.ok) return res.status(400).json({ error: 'Download fehlgeschlagen' });
|
||||||
const buf = Buffer.from(await r.arrayBuffer());
|
const buf = Buffer.from(await r.arrayBuffer());
|
||||||
fs.writeFileSync(dest, buf);
|
fs.writeFileSync(dest, buf);
|
||||||
// sofort abspielen
|
try {
|
||||||
req.body = { soundName: path.parse(fileName).name, guildId, channelId, volume, relativePath: fileName } as any;
|
await playFilePath(guildId, channelId, dest, volume);
|
||||||
return (app._router as any).handle({ ...req, method: 'POST', url: '/api/play' }, res, () => {});
|
} 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.' });
|
return res.status(400).json({ error: 'Nur MP3-Links werden unterstützt.' });
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export default function App() {
|
||||||
const [selected, setSelected] = useState<string>('');
|
const [selected, setSelected] = useState<string>('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [info, setInfo] = useState<string | null>(null);
|
||||||
const [volume, setVolume] = useState<number>(1);
|
const [volume, setVolume] = useState<number>(1);
|
||||||
const [favs, setFavs] = useState<Record<string, boolean>>({});
|
const [favs, setFavs] = useState<Record<string, boolean>>({});
|
||||||
const [theme, setTheme] = useState<string>(() => localStorage.getItem('theme') || 'dark');
|
const [theme, setTheme] = useState<string>(() => localStorage.getItem('theme') || 'dark');
|
||||||
|
|
@ -128,7 +129,14 @@ export default function App() {
|
||||||
<h1>Einmal mit Soundboard -Profis</h1>
|
<h1>Einmal mit Soundboard -Profis</h1>
|
||||||
<div className="clock">{clock}</div>
|
<div className="clock">{clock}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="badge">Geladene Sounds: {total}</div>
|
<div style={{ display:'flex', gap:12, alignItems:'center' }}>
|
||||||
|
<div className="badge">Geladene Sounds: {total}</div>
|
||||||
|
<button type="button" className="tab" style={{ background:'#b91c1c', borderColor:'transparent', color:'#fff' }} onClick={async () => {
|
||||||
|
if (!selected) return;
|
||||||
|
const [guildId] = selected.split(':');
|
||||||
|
try { await fetch(`/api/stop?guildId=${encodeURIComponent(guildId)}`, { method:'POST' }); } catch {}
|
||||||
|
}}>Panik</button>
|
||||||
|
</div>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<div className="badge">Admin-Modus</div>
|
<div className="badge">Admin-Modus</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -192,10 +200,10 @@ export default function App() {
|
||||||
placeholder="MP3 URL..."
|
placeholder="MP3 URL..."
|
||||||
/>
|
/>
|
||||||
<button type="button" className="tab" onClick={async () => {
|
<button type="button" className="tab" onClick={async () => {
|
||||||
if (!selected) { setError('Bitte Voice-Channel wählen'); return; }
|
if (!selected) { setError('Bitte Voice-Channel wählen'); setInfo(null); return; }
|
||||||
const [guildId, channelId] = selected.split(':');
|
const [guildId, channelId] = selected.split(':');
|
||||||
try { await playUrl(mediaUrl, guildId, channelId, volume); }
|
try { await playUrl(mediaUrl, guildId, channelId, volume); setError(null); setInfo('MP3 heruntergeladen und abgespielt.'); }
|
||||||
catch (e: any) { setError(e?.message || 'Play-URL fehlgeschlagen'); }
|
catch (e: any) { setInfo(null); setError(e?.message || 'Download fehlgeschlagen'); }
|
||||||
}}>⬇ Download</button>
|
}}>⬇ Download</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -279,6 +287,7 @@ export default function App() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && <div className="error">{error}</div>}
|
{error && <div className="error">{error}</div>}
|
||||||
|
{info && <div className="badge" style={{ background:'rgba(34,197,94,.18)', borderColor:'rgba(34,197,94,.35)' }}>{info}</div>}
|
||||||
|
|
||||||
<section className="grid">
|
<section className="grid">
|
||||||
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
|
{(activeFolder === '__favs__' ? filtered.filter((s) => !!favs[s.relativePath ?? s.fileName]) : filtered).map((s) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue