feat(soundboard): download modal with filename input + fix yt-dlp binary
- Add download modal: filename input, progress phases (input/downloading/done/error) - Refactor backend: shared handleUrlDownload() with optional custom filename + rename - Fix Dockerfile: use yt-dlp_linux standalone binary (no Python dependency) - Modal shows URL type badge (YouTube/Instagram/MP3), spinner, retry on error Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0b2ba0ef86
commit
9ff8a38547
4 changed files with 332 additions and 76 deletions
|
|
@ -958,42 +958,66 @@ const soundboardPlugin: Plugin = {
|
|||
} catch (e: any) { console.error(`${SB} /play error: ${e?.message ?? e}`); res.status(500).json({ error: e?.message ?? 'Fehler' }); }
|
||||
});
|
||||
|
||||
/** Shared download logic for play-url and download-url */
|
||||
async function handleUrlDownload(url: string, customFilename?: string): Promise<{ savedFile: string; savedPath: string }> {
|
||||
let savedFile: string;
|
||||
let savedPath: string;
|
||||
|
||||
if (isYtDlpUrl(url)) {
|
||||
console.log(`${SB} [url-dl] → yt-dlp...`);
|
||||
const result = await downloadWithYtDlp(url);
|
||||
savedFile = result.filename;
|
||||
savedPath = result.filepath;
|
||||
} else {
|
||||
const parsed = new URL(url);
|
||||
savedFile = path.basename(parsed.pathname);
|
||||
savedPath = path.join(SOUNDS_DIR, savedFile);
|
||||
console.log(`${SB} [url-dl] → direct MP3: ${savedFile}`);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`Download fehlgeschlagen (HTTP ${r.status})`);
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
fs.writeFileSync(savedPath, buf);
|
||||
console.log(`${SB} [url-dl] saved ${(buf.length / 1024).toFixed(0)} KB → ${savedFile}`);
|
||||
}
|
||||
|
||||
// Rename if custom filename provided
|
||||
if (customFilename) {
|
||||
const safeName = customFilename.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_').trim();
|
||||
if (safeName) {
|
||||
const ext = path.extname(savedFile).toLowerCase() || '.mp3';
|
||||
const newName = safeName.endsWith(ext) ? safeName : safeName + ext;
|
||||
const newPath = path.join(SOUNDS_DIR, newName);
|
||||
if (newPath !== savedPath && !fs.existsSync(newPath)) {
|
||||
fs.renameSync(savedPath, newPath);
|
||||
console.log(`${SB} [url-dl] renamed: ${savedFile} → ${newName}`);
|
||||
savedFile = newName;
|
||||
savedPath = newPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (NORMALIZE_ENABLE) {
|
||||
try { await normalizeToCache(savedPath); console.log(`${SB} [url-dl] normalized`); }
|
||||
catch (e: any) { console.error(`${SB} [url-dl] normalize failed: ${e?.message}`); }
|
||||
}
|
||||
|
||||
return { savedFile, savedPath };
|
||||
}
|
||||
|
||||
app.post('/api/soundboard/play-url', async (req, res) => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const { url, guildId, channelId, volume } = req.body ?? {};
|
||||
const { url, guildId, channelId, volume, filename } = req.body ?? {};
|
||||
const urlType = isYtDlpUrl(url || '') ? 'yt-dlp' : isDirectMp3Url(url || '') ? 'mp3' : 'unknown';
|
||||
console.log(`${SB} [play-url] ▶ type=${urlType} url=${url} guild=${guildId} channel=${channelId}`);
|
||||
console.log(`${SB} [play-url] ▶ type=${urlType} url=${url} filename=${filename || '(auto)'} guild=${guildId}`);
|
||||
|
||||
if (!url || !guildId || !channelId) { console.log(`${SB} [play-url] ✗ missing params`); res.status(400).json({ error: 'url, guildId, channelId erforderlich' }); return; }
|
||||
try { new URL(url); } catch { console.log(`${SB} [play-url] ✗ invalid URL`); res.status(400).json({ error: 'Ungültige URL' }); return; }
|
||||
if (!isSupportedUrl(url)) { console.log(`${SB} [play-url] ✗ unsupported URL type`); res.status(400).json({ error: 'Nur YouTube, Instagram oder direkte MP3-Links' }); return; }
|
||||
if (!url || !guildId || !channelId) { res.status(400).json({ error: 'url, guildId, channelId erforderlich' }); return; }
|
||||
try { new URL(url); } catch { res.status(400).json({ error: 'Ungültige URL' }); return; }
|
||||
if (!isSupportedUrl(url)) { res.status(400).json({ error: 'Nur YouTube, Instagram oder direkte MP3-Links' }); return; }
|
||||
|
||||
let savedFile: string;
|
||||
let savedPath: string;
|
||||
const { savedFile, savedPath } = await handleUrlDownload(url, filename);
|
||||
|
||||
if (isYtDlpUrl(url)) {
|
||||
console.log(`${SB} [play-url] → delegating to yt-dlp...`);
|
||||
const result = await downloadWithYtDlp(url);
|
||||
savedFile = result.filename;
|
||||
savedPath = result.filepath;
|
||||
} else {
|
||||
const parsed = new URL(url);
|
||||
savedFile = path.basename(parsed.pathname);
|
||||
savedPath = path.join(SOUNDS_DIR, savedFile);
|
||||
console.log(`${SB} [play-url] → direct MP3 download: ${savedFile}`);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
console.error(`${SB} [play-url] ✗ HTTP ${r.status} ${r.statusText}`);
|
||||
res.status(400).json({ error: `Download fehlgeschlagen (HTTP ${r.status})` }); return;
|
||||
}
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
fs.writeFileSync(savedPath, buf);
|
||||
console.log(`${SB} [play-url] saved ${(buf.length / 1024).toFixed(0)} KB → ${savedFile}`);
|
||||
}
|
||||
|
||||
if (NORMALIZE_ENABLE) { try { await normalizeToCache(savedPath); console.log(`${SB} [play-url] normalized`); } catch (e: any) { console.error(`${SB} [play-url] normalize failed: ${e?.message}`); } }
|
||||
try { await playFilePath(guildId, channelId, savedPath, volume, savedFile); console.log(`${SB} [play-url] playing in channel`); }
|
||||
try { await playFilePath(guildId, channelId, savedPath, volume, savedFile); console.log(`${SB} [play-url] playing`); }
|
||||
catch (e: any) { console.error(`${SB} [play-url] play failed (file saved): ${e?.message}`); }
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
|
|
@ -1006,42 +1030,18 @@ const soundboardPlugin: Plugin = {
|
|||
}
|
||||
});
|
||||
|
||||
// Download-only route (save without auto-play)
|
||||
app.post('/api/soundboard/download-url', requireAdmin, async (req, res) => {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const { url } = req.body ?? {};
|
||||
const { url, filename } = req.body ?? {};
|
||||
const urlType = isYtDlpUrl(url || '') ? 'yt-dlp' : isDirectMp3Url(url || '') ? 'mp3' : 'unknown';
|
||||
console.log(`${SB} [download-url] ▶ type=${urlType} url=${url}`);
|
||||
console.log(`${SB} [download-url] ▶ type=${urlType} url=${url} filename=${filename || '(auto)'}`);
|
||||
|
||||
if (!url) { console.log(`${SB} [download-url] ✗ no URL`); res.status(400).json({ error: 'URL erforderlich' }); return; }
|
||||
try { new URL(url); } catch { console.log(`${SB} [download-url] ✗ invalid URL`); res.status(400).json({ error: 'Ungültige URL' }); return; }
|
||||
if (!isSupportedUrl(url)) { console.log(`${SB} [download-url] ✗ unsupported URL type`); res.status(400).json({ error: 'Nur YouTube, Instagram oder direkte MP3-Links' }); return; }
|
||||
if (!url) { res.status(400).json({ error: 'URL erforderlich' }); return; }
|
||||
try { new URL(url); } catch { res.status(400).json({ error: 'Ungültige URL' }); return; }
|
||||
if (!isSupportedUrl(url)) { res.status(400).json({ error: 'Nur YouTube, Instagram oder direkte MP3-Links' }); return; }
|
||||
|
||||
let savedFile: string;
|
||||
let savedPath: string;
|
||||
|
||||
if (isYtDlpUrl(url)) {
|
||||
console.log(`${SB} [download-url] → delegating to yt-dlp...`);
|
||||
const result = await downloadWithYtDlp(url);
|
||||
savedFile = result.filename;
|
||||
savedPath = result.filepath;
|
||||
} else {
|
||||
const parsed = new URL(url);
|
||||
savedFile = path.basename(parsed.pathname);
|
||||
savedPath = path.join(SOUNDS_DIR, savedFile);
|
||||
console.log(`${SB} [download-url] → direct MP3 download: ${savedFile}`);
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) {
|
||||
console.error(`${SB} [download-url] ✗ HTTP ${r.status} ${r.statusText}`);
|
||||
res.status(400).json({ error: `Download fehlgeschlagen (HTTP ${r.status})` }); return;
|
||||
}
|
||||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
fs.writeFileSync(savedPath, buf);
|
||||
console.log(`${SB} [download-url] saved ${(buf.length / 1024).toFixed(0)} KB → ${savedFile}`);
|
||||
}
|
||||
|
||||
if (NORMALIZE_ENABLE) { try { await normalizeToCache(savedPath); console.log(`${SB} [download-url] normalized`); } catch (e: any) { console.error(`${SB} [download-url] normalize failed: ${e?.message}`); } }
|
||||
const { savedFile } = await handleUrlDownload(url, filename);
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
console.log(`${SB} [download-url] ✓ DONE in ${elapsed}s → ${savedFile}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue