feat(media): URL-Player (YouTube/Instagram via ytdl/yt-dlp, MP3-Download und sofortiges Abspielen) + Frontend-URL-Feld

This commit is contained in:
vibe-bot 2025-08-08 15:22:15 +02:00
parent 018c36487d
commit 6d4dba3ad3
5 changed files with 81 additions and 3 deletions

View file

@ -19,7 +19,9 @@
"sodium-native": "^4.0.8",
"cors": "^2.8.5",
"multer": "^1.4.5-lts.1",
"ws": "^8.18.0"
"ws": "^8.18.0",
"ytdl-core": "^4.11.5",
"youtube-dl-exec": "^2.4.8"
},
"devDependencies": {
"@types/cors": "^2.8.17",

View file

@ -20,6 +20,9 @@ import {
} from '@discordjs/voice';
import sodium from 'libsodium-wrappers';
import nacl from 'tweetnacl';
import ytdl from 'ytdl-core';
import { createRequire } from 'node:module';
import child_process from 'node:child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -579,6 +582,58 @@ app.listen(PORT, () => {
console.log(`Server läuft auf http://0.0.0.0:${PORT}`);
});
// --- Medien-URL abspielen ---
// Unterstützt: YouTube (ytdl-core), generische URLs (yt-dlp), direkte mp3 (Download und Ablage)
app.post('/api/play-url', async (req: Request, res: Response) => {
try {
const { url, guildId, channelId, volume } = req.body as { url?: string; guildId?: string; channelId?: string; volume?: number };
if (!url || !guildId || !channelId) return res.status(400).json({ error: 'url, guildId, channelId erforderlich' });
// MP3 direkt?
const lower = url.toLowerCase();
if (lower.endsWith('.mp3')) {
const fileName = path.basename(new URL(url).pathname);
const dest = path.join(SOUNDS_DIR, fileName);
const r = await fetch(url);
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, () => {});
}
const guild = client.guilds.cache.get(guildId);
if (!guild) return res.status(404).json({ error: 'Guild nicht gefunden' });
let state = guildAudioState.get(guildId);
if (!state) return res.status(400).json({ error: 'Bitte zuerst einen Sound abspielen, um die Verbindung herzustellen' });
const useVolume = typeof volume === 'number' ? Math.max(0, Math.min(1, volume)) : state.currentVolume ?? 1;
// Audio-Stream besorgen
let stream: any;
if (ytdl.validateURL(url)) {
stream = ytdl(url, { filter: 'audioonly', quality: 'highestaudio', highWaterMark: 1 << 25 });
} else {
// Fallback via yt-dlp, benötigt binary im Image/Host
// wir nutzen stdout mit ffmpeg pipe
const yt = child_process.spawn('yt-dlp', ['-f', 'bestaudio', '-o', '-', url]);
stream = yt.stdout;
}
const resource = createAudioResource(stream as any, { inlineVolume: true });
if (resource.volume) resource.volume.setVolume(useVolume);
state.player.stop();
state.player.play(resource);
state.currentResource = resource;
state.currentVolume = useVolume;
return res.json({ ok: true });
} catch (e: any) {
console.error('play-url error:', e);
return res.status(500).json({ error: e?.message ?? 'Unbekannter Fehler' });
}
});