feat: live-Volume während Wiedergabe (inlineVolume state) + Anzeige Gesamtanzahl Sounds

This commit is contained in:
vibe-bot 2025-08-08 01:40:49 +02:00
parent 826b07e994
commit 9a97a9d7bb
5 changed files with 28 additions and 8 deletions

View file

@ -12,6 +12,7 @@ import {
NoSubscriberBehavior, NoSubscriberBehavior,
getVoiceConnection, getVoiceConnection,
type VoiceConnection, type VoiceConnection,
type AudioResource,
generateDependencyReport, generateDependencyReport,
entersState, entersState,
VoiceConnectionStatus VoiceConnectionStatus
@ -58,6 +59,8 @@ type GuildAudioState = {
player: ReturnType<typeof createAudioPlayer>; player: ReturnType<typeof createAudioPlayer>;
guildId: string; guildId: string;
channelId: string; channelId: string;
currentResource?: AudioResource;
currentVolume: number; // 0..1
}; };
const guildAudioState = new Map<string, GuildAudioState>(); const guildAudioState = new Map<string, GuildAudioState>();
@ -197,7 +200,7 @@ app.get('/api/sounds', (req: Request, res: Response) => {
.map((file) => ({ fileName: file, name: path.parse(file).name })) .map((file) => ({ fileName: file, name: path.parse(file).name }))
.filter((s) => (q ? s.name.toLowerCase().includes(q) : true)); .filter((s) => (q ? s.name.toLowerCase().includes(q) : true));
res.json(items); res.json({ items, total: files.length });
}); });
app.get('/api/channels', (_req: Request, res: Response) => { app.get('/api/channels', (_req: Request, res: Response) => {
@ -227,7 +230,7 @@ app.post('/api/play', async (req: Request, res: Response) => {
volume?: number; // 0..1 volume?: number; // 0..1
}; };
if (!soundName || !guildId || !channelId) return res.status(400).json({ error: 'soundName, guildId, channelId erforderlich' }); if (!soundName || !guildId || !channelId) return res.status(400).json({ error: 'soundName, guildId, channelId erforderlich' });
const safeVolume = typeof volume === 'number' && Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : 1; const safeVolume = typeof volume === 'number' && Number.isFinite(volume) ? Math.max(0, Math.min(1, volume)) : state?.currentVolume ?? 1;
const filePath = path.join(SOUNDS_DIR, `${soundName}.mp3`); const filePath = path.join(SOUNDS_DIR, `${soundName}.mp3`);
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Sound nicht gefunden' }); if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'Sound nicht gefunden' });
@ -250,7 +253,7 @@ app.post('/api/play', async (req: Request, res: Response) => {
}); });
const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } });
connection.subscribe(player); connection.subscribe(player);
state = { connection, player, guildId, channelId }; state = { connection, player, guildId, channelId, currentVolume: 1 };
guildAudioState.set(guildId, state); guildAudioState.set(guildId, state);
// Connection State Logs // Connection State Logs
@ -297,7 +300,7 @@ app.post('/api/play', async (req: Request, res: Response) => {
}); });
const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } }); const player = createAudioPlayer({ behaviors: { noSubscriber: NoSubscriberBehavior.Play } });
connection.subscribe(player); connection.subscribe(player);
state = { connection, player, guildId, channelId }; state = { connection, player, guildId, channelId, currentVolume: 1 };
guildAudioState.set(guildId, state); guildAudioState.set(guildId, state);
state.connection = await ensureConnectionReady(connection, channelId, guildId, guild); state.connection = await ensureConnectionReady(connection, channelId, guildId, guild);
@ -323,6 +326,8 @@ app.post('/api/play', async (req: Request, res: Response) => {
} }
state.player.stop(); state.player.stop();
state.player.play(resource); state.player.play(resource);
state.currentResource = resource;
state.currentVolume = safeVolume;
console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`); console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`);
return res.json({ ok: true }); return res.json({ ok: true });
} catch (err: any) { } catch (err: any) {

View file

@ -4,6 +4,7 @@ import type { VoiceChannelInfo, Sound } from './types';
export default function App() { export default function App() {
const [sounds, setSounds] = useState<Sound[]>([]); const [sounds, setSounds] = useState<Sound[]>([]);
const [total, setTotal] = useState<number>(0);
const [channels, setChannels] = useState<VoiceChannelInfo[]>([]); const [channels, setChannels] = useState<VoiceChannelInfo[]>([]);
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [selected, setSelected] = useState<string>(''); const [selected, setSelected] = useState<string>('');
@ -15,7 +16,8 @@ export default function App() {
(async () => { (async () => {
try { try {
const [s, c] = await Promise.all([fetchSounds(), fetchChannels()]); const [s, c] = await Promise.all([fetchSounds(), fetchChannels()]);
setSounds(s); setSounds(s.items);
setTotal(s.total);
setChannels(c); setChannels(c);
if (c[0]) setSelected(`${c[0].guildId}:${c[0].channelId}`); if (c[0]) setSelected(`${c[0].guildId}:${c[0].channelId}`);
} catch (e: any) { } catch (e: any) {
@ -26,7 +28,8 @@ export default function App() {
const interval = setInterval(async () => { const interval = setInterval(async () => {
try { try {
const s = await fetchSounds(query); const s = await fetchSounds(query);
setSounds(s); setSounds(s.items);
setTotal(s.total);
} catch {} } catch {}
}, 10000); }, 10000);
return () => clearInterval(interval); return () => clearInterval(interval);
@ -101,6 +104,7 @@ export default function App() {
))} ))}
{filtered.length === 0 && <div className="hint">Keine Sounds gefunden.</div>} {filtered.length === 0 && <div className="hint">Keine Sounds gefunden.</div>}
</section> </section>
<div className="footer-info">Geladene Sounds: {total}</div>
</div> </div>
); );
} }

View file

@ -1,8 +1,8 @@
import type { Sound, VoiceChannelInfo } from './types'; import type { Sound, SoundsResponse, VoiceChannelInfo } from './types';
const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api'; const API_BASE = import.meta.env.VITE_API_BASE_URL || '/api';
export async function fetchSounds(q?: string): Promise<Sound[]> { export async function fetchSounds(q?: string): Promise<SoundsResponse> {
const url = new URL(`${API_BASE}/sounds`, window.location.origin); const url = new URL(`${API_BASE}/sounds`, window.location.origin);
if (q) url.searchParams.set('q', q); if (q) url.searchParams.set('q', q);
const res = await fetch(url.toString()); const res = await fetch(url.toString());

View file

@ -49,6 +49,12 @@ header p { opacity: .8; }
.hint { opacity: .7; padding: 24px 0; } .hint { opacity: .7; padding: 24px 0; }
.footer-info {
margin-top: 14px;
opacity: .8;
font-size: 14px;
}

View file

@ -3,6 +3,11 @@ export type Sound = {
name: string; name: string;
}; };
export type SoundsResponse = {
items: Sound[];
total: number;
};
export type VoiceChannelInfo = { export type VoiceChannelInfo = {
guildId: string; guildId: string;
guildName: string; guildName: string;