discord-multipurpose-bot/utils/liveLyricsManager.js
Ayden cb5a906850
Some checks are pending
Docker / build (push) Waiting to run
Feat/Add Music Commands (#1)
* add simple music functionality

* update workflow

* update Dockerfile

* update Dockerfile

* update Dockerfile

* update Dockerfile

* add few more music commands

* add lyrics command

* update lyrics command

* add loop, and add categories to all commands

* change discord status

* seperate distube and change startup console theme

* Update README

* UPDATE LICENSE file

* fix docker compose image, add better error handling for distube and update tagging workflow

* switch to node-alpine image for docker

* switch to node-alpine image for docker

* update ascii

* music commands imporvements, implement live lyrics, some guards and bot leaving on empty

* use ffmpeg package rather than ffmpeg-static
2025-09-21 01:26:18 +01:00

138 lines
4 KiB
JavaScript

const { getSyncedLyrics } = require("./lyricsProvider");
const states = new Map(); // guildId -> { thread, parent, timers, startedAtMs, pausedAtMs, songId, lastSentAtMs, lyrics }
function clearTimers(state) {
if (!state?.timers) return;
for (const t of state.timers) clearTimeout(t);
state.timers = new Set();
}
/** coerce number seconds */
function sec(x) {
return Math.max(0, Math.floor(Number(x || 0)));
}
/** schedule lyric lines starting from startTimeSec */
function scheduleLines(state, queue, lyrics, startTimeSec) {
clearTimers(state);
state.timers = new Set();
state.startedAtMs = Date.now() - startTimeSec * 1000;
const parentToSend = state.thread || state.parent;
const MIN_GAP_MS = 400; // rate-limit safety
for (const line of lyrics) {
if (!line || typeof line.t !== "number") continue;
const delayMs = Math.max(0, Math.round((line.t - startTimeSec) * 1000));
const timer = setTimeout(async () => {
try {
const now = Date.now();
if (state.lastSentAtMs && now - state.lastSentAtMs < MIN_GAP_MS) {
await new Promise((r) =>
setTimeout(r, MIN_GAP_MS - (now - state.lastSentAtMs))
);
}
state.lastSentAtMs = Date.now();
await parentToSend.send(line.text || "");
} catch (e) {
console.error("live lyrics send failed:", e?.message || e);
}
}, delayMs);
state.timers.add(timer);
}
}
async function createThreadOrFallback(queue, song) {
const parent = queue.textChannel;
let thread = null;
if (parent?.threads?.create) {
try {
thread = await parent.threads.create({
name: `${(song?.name || "Now Playing").slice(0, 80)} • Live`,
autoArchiveDuration: 60,
reason: "Live lyrics",
});
} catch (e) {
console.warn("Thread create failed, falling back to parent:", e?.message);
}
}
return { thread, parent };
}
async function start(queue, song) {
try {
const guildId = queue.id;
await stop(guildId, { deleteThread: true });
const lyrics = await getSyncedLyrics(song);
if (!lyrics || lyrics.length === 0) {
queue.textChannel?.send("🎤 No synced lyrics available for this track.");
return;
}
const { thread, parent } = await createThreadOrFallback(queue, song);
const state = {
thread,
parent,
timers: new Set(),
startedAtMs: Date.now(),
pausedAtMs: null,
songId: song?.id || song?.url || song?.name,
lastSentAtMs: 0,
lyrics,
};
states.set(guildId, state);
const header = `**Live lyrics for:** ${song?.name || "Unknown title"}`;
if (thread) await thread.send(header);
else await parent.send(`${header} *(thread unavailable, posting here)*`);
const current = sec(queue.currentTime);
scheduleLines(state, queue, lyrics, current);
} catch (e) {
console.error("liveLyrics.start failed:", e?.message || e);
}
}
async function pause(guildId) {
const state = states.get(guildId);
if (!state || state.pausedAtMs) return;
state.pausedAtMs = Date.now();
clearTimers(state);
}
async function resume(queue) {
const state = states.get(queue.id);
if (!state || !state.pausedAtMs) return;
state.pausedAtMs = null;
const current = sec(queue.currentTime);
scheduleLines(state, queue, state.lyrics, current);
}
async function seek(queue, timeSecOptional) {
const state = states.get(queue.id);
if (!state) return;
const current = sec(timeSecOptional ?? queue.currentTime ?? 0);
scheduleLines(state, queue, state.lyrics, current);
}
async function stop(guildId, { deleteThread = false } = {}) {
const state = states.get(guildId);
if (!state) return;
clearTimers(state);
try {
if (deleteThread && state.thread?.delete) {
await state.thread.delete("Song ended — removing live lyrics thread.");
}
} catch (e) {
console.warn("liveLyrics thread delete failed:", e?.message || e);
}
states.delete(guildId);
}
module.exports = { start, pause, resume, seek, stop };