Perf: Vollständiger Norm-Cache-Sync beim Start + Auto-Cache bei Upload/Import

- syncNormCache() beim Serverstart: normalisiert ALLE Sounds (nicht nur Top 50)
  und räumt verwaiste Cache-Dateien automatisch auf
- DM-Upload: neue Datei wird sofort im Hintergrund normalisiert
- URL-Import: Datei wird vor dem Abspielen normalisiert → direkt aus Cache
- Detailliertes Logging: neu/vorhanden/fehlgeschlagen/verwaist + Dauer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bot 2026-03-01 21:27:33 +01:00
parent 83b8f1acac
commit 68414ac257

View file

@ -181,7 +181,7 @@ function getNormCachePath(filePath: string): string | null {
function normalizeToCache(filePath: string): Promise<string> { function normalizeToCache(filePath: string): Promise<string> {
const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath)); const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath));
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const ffArgs = ['-hide_banner', '-loglevel', 'error', '-i', filePath, const ffArgs = ['-hide_banner', '-loglevel', 'error', '-y', '-i', filePath,
'-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`, '-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`,
'-f', 's16le', '-ar', '48000', '-ac', '2', cacheFile]; '-f', 's16le', '-ar', '48000', '-ac', '2', cacheFile];
const ff = child_process.spawn('ffmpeg', ffArgs); const ff = child_process.spawn('ffmpeg', ffArgs);
@ -193,6 +193,54 @@ function normalizeToCache(filePath: string): Promise<string> {
}); });
} }
/**
* Vollständige Cache-Synchronisation:
* 1. Alle Sound-Dateien normalisieren, die noch nicht im Cache sind
* 2. Verwaiste Cache-Dateien löschen (Sound wurde gelöscht/umbenannt)
* Läuft im Hintergrund, blockiert nicht den Server.
*/
async function syncNormCache(): Promise<void> {
if (!NORMALIZE_ENABLE) return;
const t0 = Date.now();
const allSounds = listAllSounds();
// Set aller erwarteten Cache-Keys
const expectedKeys = new Set<string>();
let created = 0;
let skipped = 0;
let failed = 0;
for (const s of allSounds) {
const fp = path.join(SOUNDS_DIR, s.relativePath);
const key = normCacheKey(fp);
expectedKeys.add(key);
if (!fs.existsSync(fp)) continue;
if (getNormCachePath(fp)) { skipped++; continue; } // bereits gecacht & gültig
try {
await normalizeToCache(fp);
created++;
} catch (e) {
failed++;
console.warn(`Norm-cache failed: ${s.relativePath}`, e);
}
}
// Verwaiste Cache-Dateien aufräumen
let cleaned = 0;
try {
for (const f of fs.readdirSync(NORM_CACHE_DIR)) {
if (!expectedKeys.has(f)) {
try { fs.unlinkSync(path.join(NORM_CACHE_DIR, f)); cleaned++; } catch {}
}
}
} catch {}
const dt = ((Date.now() - t0) / 1000).toFixed(1);
console.log(
`Norm-Cache sync (${dt}s): ${created} neu, ${skipped} vorhanden, ${failed} fehlgeschlagen, ${cleaned} verwaist entfernt (${allSounds.length} Sounds gesamt)`
);
}
// --- Voice Abhängigkeiten prüfen --- // --- Voice Abhängigkeiten prüfen ---
await sodium.ready; await sodium.ready;
// init nacl to ensure it loads // init nacl to ensure it loads
@ -622,6 +670,10 @@ client.on(Events.MessageCreate, async (message: Message) => {
if (!res.ok) throw new Error(`Download fehlgeschlagen: ${attachment.url}`); if (!res.ok) throw new Error(`Download fehlgeschlagen: ${attachment.url}`);
const arrayBuffer = await res.arrayBuffer(); const arrayBuffer = await res.arrayBuffer();
fs.writeFileSync(targetPath, Buffer.from(arrayBuffer)); fs.writeFileSync(targetPath, Buffer.from(arrayBuffer));
// Sofort normalisieren für instant Play
if (NORMALIZE_ENABLE) {
normalizeToCache(targetPath).catch((e) => console.warn('Norm after upload failed:', e));
}
await message.author.send?.(`Sound gespeichert: ${path.basename(targetPath)}`); await message.author.send?.(`Sound gespeichert: ${path.basename(targetPath)}`);
} }
} catch (err) { } catch (err) {
@ -1328,6 +1380,10 @@ 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);
// Vor dem Abspielen normalisieren → sofort aus Cache
if (NORMALIZE_ENABLE) {
try { await normalizeToCache(dest); } catch (e) { console.warn('Norm after URL import failed:', e); }
}
try { try {
await playFilePath(guildId, channelId, dest, volume, path.basename(dest)); await playFilePath(guildId, channelId, dest, volume, path.basename(dest));
} catch { } catch {
@ -1352,33 +1408,8 @@ if (fs.existsSync(webDistPath)) {
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server läuft auf http://0.0.0.0:${PORT}`); console.log(`Server läuft auf http://0.0.0.0:${PORT}`);
// Hintergrund-Warmup: häufigste Sounds vorab normalisieren, damit der erste Play sofort schnell ist // Vollständige Cache-Synchronisation beim Start (Hintergrund)
if (NORMALIZE_ENABLE) { syncNormCache();
(async () => {
try {
const allSounds = listAllSounds();
// Sortiere nach Play-Count (häufigste zuerst), maximal 50 vorab cachen
const plays = persistedState.plays ?? {};
const sorted = [...allSounds].sort((a, b) => ((plays[b.relativePath] ?? 0) as number) - ((plays[a.relativePath] ?? 0) as number));
const toWarm = sorted.slice(0, 50);
let cached = 0;
for (const s of toWarm) {
const fp = path.join(SOUNDS_DIR, s.relativePath);
if (!fs.existsSync(fp)) continue;
if (getNormCachePath(fp)) continue; // schon gecacht
try {
await normalizeToCache(fp);
cached++;
} catch (e) {
console.warn(`Warmup failed for ${s.relativePath}:`, e);
}
}
if (cached > 0) console.log(`Loudnorm-Warmup: ${cached} Sounds vorab normalisiert`);
} catch (e) {
console.warn('Loudnorm-Warmup error:', e);
}
})();
}
}); });