feat(audio): ffmpeg loudnorm Laufzeit-Normalisierung; Nerdinfos: Top 3 Most Played als Tab; Playszählung
This commit is contained in:
parent
3d943f3178
commit
7f4b60c0bd
1 changed files with 51 additions and 7 deletions
|
|
@ -22,6 +22,7 @@ import {
|
|||
import sodium from 'libsodium-wrappers';
|
||||
import nacl from 'tweetnacl';
|
||||
// Streaming externer Plattformen entfernt – nur MP3-URLs werden noch unterstützt
|
||||
import child_process from 'node:child_process';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
|
@ -44,7 +45,7 @@ if (!DISCORD_TOKEN) {
|
|||
fs.mkdirSync(SOUNDS_DIR, { recursive: true });
|
||||
|
||||
// Persistente Lautstärke pro Guild speichern
|
||||
type PersistedState = { volumes: Record<string, number> };
|
||||
type PersistedState = { volumes: Record<string, number>; plays: Record<string, number> };
|
||||
const STATE_FILE = path.join(path.resolve(SOUNDS_DIR, '..'), 'state.json');
|
||||
|
||||
function readPersistedState(): PersistedState {
|
||||
|
|
@ -52,10 +53,10 @@ function readPersistedState(): PersistedState {
|
|||
if (fs.existsSync(STATE_FILE)) {
|
||||
const raw = fs.readFileSync(STATE_FILE, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
return { volumes: parsed.volumes ?? {} } as PersistedState;
|
||||
return { volumes: parsed.volumes ?? {}, plays: parsed.plays ?? {} } as PersistedState;
|
||||
}
|
||||
} catch {}
|
||||
return { volumes: {} };
|
||||
return { volumes: {}, plays: {} };
|
||||
}
|
||||
|
||||
function writePersistedState(state: PersistedState): void {
|
||||
|
|
@ -72,6 +73,19 @@ const getPersistedVolume = (guildId: string): number => {
|
|||
const v = persistedState.volumes[guildId];
|
||||
return typeof v === 'number' && Number.isFinite(v) ? Math.max(0, Math.min(1, v)) : 1;
|
||||
};
|
||||
function incrementPlaysFor(relativePath: string) {
|
||||
try {
|
||||
const key = relativePath.replace(/\\/g, '/');
|
||||
persistedState.plays[key] = (persistedState.plays[key] ?? 0) + 1;
|
||||
writePersistedState(persistedState);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// Normalisierung (ffmpeg loudnorm) Konfiguration
|
||||
const NORMALIZE_ENABLE = String(process.env.NORMALIZE_ENABLE ?? 'true').toLowerCase() !== 'false';
|
||||
const NORMALIZE_I = String(process.env.NORMALIZE_I ?? '-16');
|
||||
const NORMALIZE_LRA = String(process.env.NORMALIZE_LRA ?? '11');
|
||||
const NORMALIZE_TP = String(process.env.NORMALIZE_TP ?? '-1.5');
|
||||
|
||||
// --- Voice Abhängigkeiten prüfen ---
|
||||
await sodium.ready;
|
||||
|
|
@ -98,7 +112,7 @@ type GuildAudioState = {
|
|||
};
|
||||
const guildAudioState = new Map<string, GuildAudioState>();
|
||||
|
||||
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number): Promise<void> {
|
||||
async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise<void> {
|
||||
const guild = client.guilds.cache.get(guildId);
|
||||
if (!guild) throw new Error('Guild nicht gefunden');
|
||||
let state = guildAudioState.get(guildId);
|
||||
|
|
@ -120,12 +134,22 @@ async function playFilePath(guildId: string, channelId: string, filePath: string
|
|||
const useVolume = typeof volume === 'number' && Number.isFinite(volume)
|
||||
? Math.max(0, Math.min(1, volume))
|
||||
: (state.currentVolume ?? 1);
|
||||
const resource = createAudioResource(filePath, { inlineVolume: true });
|
||||
let resource: AudioResource;
|
||||
if (NORMALIZE_ENABLE) {
|
||||
const ffArgs = ['-hide_banner', '-loglevel', 'error', '-i', filePath,
|
||||
'-af', `loudnorm=I=${NORMALIZE_I}:LRA=${NORMALIZE_LRA}:TP=${NORMALIZE_TP}`,
|
||||
'-f', 's16le', '-ar', '48000', '-ac', '2', 'pipe:1'];
|
||||
const ff = child_process.spawn('ffmpeg', ffArgs);
|
||||
resource = createAudioResource(ff.stdout as any, { inlineVolume: true, inputType: StreamType.Raw });
|
||||
} else {
|
||||
resource = createAudioResource(filePath, { inlineVolume: true });
|
||||
}
|
||||
if (resource.volume) resource.volume.setVolume(useVolume);
|
||||
state.player.stop();
|
||||
state.player.play(resource);
|
||||
state.currentResource = resource;
|
||||
state.currentVolume = useVolume;
|
||||
if (relativeKey) incrementPlaysFor(relativeKey);
|
||||
}
|
||||
|
||||
async function handleCommand(message: Message, content: string) {
|
||||
|
|
@ -404,13 +428,31 @@ app.get('/api/sounds', (req: Request, res: Response) => {
|
|||
|
||||
const total = allItems.length;
|
||||
const recentCount = Math.min(10, total);
|
||||
// Nerdinfos: Top 3 meistgespielte
|
||||
const playsEntries = Object.entries(persistedState.plays || {});
|
||||
const top3 = playsEntries
|
||||
.sort((a, b) => (b[1] as number) - (a[1] as number))
|
||||
.slice(0, 3)
|
||||
.map(([rel, count]) => {
|
||||
const it = allItems.find(i => (i.relativePath === rel || i.fileName === rel));
|
||||
return it ? { key: `__top__:${rel}`, name: `${it.name} (${count})`, count: 1 } : null;
|
||||
})
|
||||
.filter(Boolean) as Array<{ key: string; name: string; count: number }>;
|
||||
|
||||
const foldersOut = [
|
||||
{ key: '__all__', name: 'Alle', count: total },
|
||||
{ key: '__recent__', name: 'Neu', count: recentCount },
|
||||
...(top3.length ? [{ key: '__top3__', name: 'Most Played (3)', count: top3.length }] : []),
|
||||
...folders
|
||||
];
|
||||
// isRecent-Flag für UI (Top 5 der neuesten)
|
||||
const withRecentFlag = filteredItems.map((it) => ({
|
||||
let result = filteredItems;
|
||||
if (folderFilter === '__top3__') {
|
||||
const keys = new Set(top3.map(t => t.key.split(':')[1]));
|
||||
result = allItems.filter(i => keys.has(i.relativePath ?? i.fileName));
|
||||
}
|
||||
|
||||
const withRecentFlag = result.map((it) => ({
|
||||
...it,
|
||||
isRecent: recentTop5Set.has(it.relativePath ?? it.fileName)
|
||||
}));
|
||||
|
|
@ -595,6 +637,8 @@ app.post('/api/play', async (req: Request, res: Response) => {
|
|||
persistedState.volumes[guildId] = volumeToUse;
|
||||
writePersistedState(persistedState);
|
||||
console.log(`${new Date().toISOString()} | player.play() called for ${soundName}`);
|
||||
// Plays zählen (relativer Key verfügbar?)
|
||||
if (relativePath) incrementPlaysFor(relativePath);
|
||||
return res.json({ ok: true });
|
||||
} catch (err: any) {
|
||||
console.error('Play-Fehler:', err);
|
||||
|
|
@ -684,7 +728,7 @@ app.post('/api/play-url', async (req: Request, res: Response) => {
|
|||
const buf = Buffer.from(await r.arrayBuffer());
|
||||
fs.writeFileSync(dest, buf);
|
||||
try {
|
||||
await playFilePath(guildId, channelId, dest, volume);
|
||||
await playFilePath(guildId, channelId, dest, volume, path.basename(dest));
|
||||
} catch {
|
||||
return res.status(500).json({ error: 'Abspielen fehlgeschlagen' });
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue