Feat: Drag & Drop MP3/WAV Upload mit Progress-Tracking

Backend:
- multer reaktiviert (war auskommentiert) mit diskStorage + Collision-Handling
- /api/upload (POST, admin-protected): bis zu 20 Dateien gleichzeitig
- MP3/WAV-Filter (50MB Limit), sofortige Hintergrund-Normalisierung nach Upload

Frontend:
- Globale window dragenter/dragleave/drop Listener mit Counter gegen false-positives
- Drag-Overlay: Vollbild-Blur + animierter Drop-Zone (pulsierender Accent-Border, bouncing Icon)
- Upload-Queue: floating Card bottom-right mit Per-Datei Progressbar + Status-Icons
  (sync-Animation beim Hochladen, check_circle grün, error rot)
- Auto-Refresh der Soundliste + Analytics nach Upload
- Auto-Dismiss der Queue nach 3.5s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bot 2026-03-01 22:15:07 +01:00
parent a61663166f
commit 52c86240af
4 changed files with 426 additions and 3 deletions

View file

@ -2,7 +2,7 @@ import path from 'node:path';
import fs from 'node:fs';
import { fileURLToPath } from 'node:url';
import express, { Request, Response } from 'express';
// import multer from 'multer';
import multer from 'multer';
import cors from 'cors';
import crypto from 'node:crypto';
import { Client, GatewayIntentBits, Partials, ChannelType, Events, type Message, VoiceState } from 'discord.js';
@ -1029,6 +1029,47 @@ app.post('/api/admin/sounds/rename', requireAdmin, (req: Request, res: Response)
}
});
// --- Datei-Upload (Drag & Drop) ---
const uploadStorage = multer.diskStorage({
destination: (_req, _file, cb) => cb(null, SOUNDS_DIR),
filename: (_req, file, cb) => {
const safe = file.originalname.replace(/[<>:"/\\|?*\x00-\x1f]/g, '_');
const { name, ext } = path.parse(safe);
let finalName = safe;
let i = 2;
while (fs.existsSync(path.join(SOUNDS_DIR, finalName))) {
finalName = `${name}-${i}${ext}`;
i++;
}
cb(null, finalName);
},
});
const uploadMulter = multer({
storage: uploadStorage,
fileFilter: (_req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
cb(null, ext === '.mp3' || ext === '.wav');
},
limits: { fileSize: 50 * 1024 * 1024, files: 20 },
});
app.post('/api/upload', requireAdmin, (req: Request, res: Response) => {
uploadMulter.array('files', 20)(req, res, async (err) => {
if (err) return res.status(400).json({ error: err.message ?? 'Upload fehlgeschlagen' });
const files = (req as any).files as Express.Multer.File[] | undefined;
if (!files?.length) return res.status(400).json({ error: 'Keine gültigen Dateien (nur MP3/WAV)' });
const saved = files.map(f => ({ name: f.filename, size: f.size }));
// Normalisierung im Hintergrund starten
if (NORMALIZE_ENABLE) {
for (const f of files) {
normalizeToCache(f.path).catch(e => console.warn(`Norm after upload failed (${f.filename}):`, e));
}
}
console.log(`${new Date().toISOString()} | Upload: ${files.map(f => f.filename).join(', ')}`);
res.json({ ok: true, files: saved });
});
});
// --- Kategorien API ---
app.get('/api/categories', (_req: Request, res: Response) => {
res.json({ categories: persistedState.categories ?? [] });