From 68414ac25710eac5a27d65deb7573a5fea126966 Mon Sep 17 00:00:00 2001 From: Bot Date: Sun, 1 Mar 2026 21:27:33 +0100 Subject: [PATCH] =?UTF-8?q?Perf:=20Vollst=C3=A4ndiger=20Norm-Cache-Sync=20?= =?UTF-8?q?beim=20Start=20+=20Auto-Cache=20bei=20Upload/Import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- server/src/index.ts | 87 ++++++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 35e701e..b77b970 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -181,7 +181,7 @@ function getNormCachePath(filePath: string): string | null { function normalizeToCache(filePath: string): Promise { const cacheFile = path.join(NORM_CACHE_DIR, normCacheKey(filePath)); 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}`, '-f', 's16le', '-ar', '48000', '-ac', '2', cacheFile]; const ff = child_process.spawn('ffmpeg', ffArgs); @@ -193,6 +193,54 @@ function normalizeToCache(filePath: string): Promise { }); } +/** + * 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 { + if (!NORMALIZE_ENABLE) return; + const t0 = Date.now(); + const allSounds = listAllSounds(); + + // Set aller erwarteten Cache-Keys + const expectedKeys = new Set(); + 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 --- await sodium.ready; // 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}`); const arrayBuffer = await res.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)}`); } } 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' }); const buf = Buffer.from(await r.arrayBuffer()); 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 { await playFilePath(guildId, channelId, dest, volume, path.basename(dest)); } catch { @@ -1352,33 +1408,8 @@ if (fs.existsSync(webDistPath)) { app.listen(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 - if (NORMALIZE_ENABLE) { - (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); - } - })(); - } + // Vollständige Cache-Synchronisation beim Start (Hintergrund) + syncNormCache(); });