mirror of
https://github.com/aydenjahola/discord-multipurpose-bot.git
synced 2025-09-21 06:41:35 +01:00
Some checks are pending
Docker / build (push) Waiting to run
* 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
140 lines
3.9 KiB
JavaScript
140 lines
3.9 KiB
JavaScript
// Tiny provider that tries LRCLIB first (synced LRC), then falls back to unsynced lines.
|
|
// Docs: https://lrclib.net (no API key required)
|
|
//
|
|
// Returned format: [{ t: Number(seconds), text: String }, ...] sorted by t
|
|
|
|
function parseLRC(lrcText) {
|
|
// Supports tags like [ti:], [ar:], [length:], and timestamp lines [mm:ss.xx]
|
|
const lines = lrcText.split(/\r?\n/);
|
|
const out = [];
|
|
const timeRe = /\[(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?]/g;
|
|
|
|
for (const line of lines) {
|
|
let m;
|
|
let lastIndex = 0;
|
|
// extract all timestamps from this line
|
|
const stamps = [];
|
|
while ((m = timeRe.exec(line)) !== null) {
|
|
const mm = Number(m[1]);
|
|
const ss = Number(m[2]);
|
|
const ms = Number(m[3] || 0);
|
|
const t = mm * 60 + ss + ms / 1000;
|
|
stamps.push({ t, idx: m.index });
|
|
lastIndex = timeRe.lastIndex;
|
|
}
|
|
if (!stamps.length) continue;
|
|
// text is after last timestamp tag
|
|
const text = line.slice(lastIndex).trim();
|
|
if (!text) continue;
|
|
for (const s of stamps) out.push({ t: s.t, text });
|
|
}
|
|
|
|
// remove duplicates, sort
|
|
out.sort((a, b) => a.t - b.t);
|
|
const dedup = [];
|
|
let prev = "";
|
|
for (const l of out) {
|
|
const key = `${l.t.toFixed(2)}|${l.text}`;
|
|
if (key !== prev) dedup.push(l);
|
|
prev = key;
|
|
}
|
|
return dedup;
|
|
}
|
|
|
|
function splitUnsyncedLyrics(text) {
|
|
// Fallback for plain lyrics (no timestamps): just emit a line every ~2s
|
|
const lines = text
|
|
.split(/\r?\n/)
|
|
.map((l) => l.trim())
|
|
.filter(Boolean)
|
|
.slice(0, 500); // keep it sane
|
|
const out = [];
|
|
let t = 0;
|
|
for (const l of lines) {
|
|
out.push({ t, text: l });
|
|
t += 2;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function pickArtistAndTitle(song) {
|
|
// Try to infer artist/title for better matches
|
|
const name = song?.name || "";
|
|
const byUploader = song?.uploader?.name || "";
|
|
let title = name;
|
|
let artist = "";
|
|
|
|
// If the title looks like "Artist - Title"
|
|
if (name.includes(" - ")) {
|
|
const [a, b] = name.split(" - ");
|
|
if (a && b) {
|
|
artist = a.trim();
|
|
title = b.trim();
|
|
}
|
|
}
|
|
if (!artist) {
|
|
artist = byUploader || song?.author || "";
|
|
}
|
|
return { artist, title };
|
|
}
|
|
|
|
async function fetchLRCLIBLyrics(song) {
|
|
const { artist, title } = pickArtistAndTitle(song);
|
|
// Build a simple query, we also try with raw name as a fallback
|
|
const candidates = [];
|
|
|
|
if (title) {
|
|
candidates.push({ track_name: title, artist_name: artist || "" });
|
|
}
|
|
if (song?.name) {
|
|
candidates.push({ track_name: song.name, artist_name: artist || "" });
|
|
}
|
|
|
|
for (const c of candidates) {
|
|
try {
|
|
const url = new URL("https://lrclib.net/api/get");
|
|
if (c.track_name) url.searchParams.set("track_name", c.track_name);
|
|
if (c.artist_name) url.searchParams.set("artist_name", c.artist_name);
|
|
// lrclib also accepts album_name + duration if you have them
|
|
|
|
const res = await fetch(url.toString(), {
|
|
headers: {
|
|
"user-agent": "CircuitrixBot/1.0 (+https://github.com/aydenjahola)",
|
|
},
|
|
});
|
|
|
|
if (!res.ok) continue;
|
|
const data = await res.json();
|
|
|
|
// Prefer synced lyrics
|
|
if (data?.syncedLyrics) {
|
|
const parsed = parseLRC(data.syncedLyrics);
|
|
if (parsed.length) return parsed;
|
|
}
|
|
|
|
// Fallback to unsynced lyrics
|
|
if (data?.plainLyrics) {
|
|
const parsed = splitUnsyncedLyrics(data.plainLyrics);
|
|
if (parsed.length) return parsed;
|
|
}
|
|
} catch (e) {
|
|
// Keep trying next candidate
|
|
// console.warn("LRCLIB fetch failed:", e?.message);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return an array of { t, text } (seconds) for synced display, or null if none.
|
|
* This is the only function the rest of the bot uses.
|
|
*/
|
|
async function getSyncedLyrics(song) {
|
|
// LRCLIB (synced LRC, free)
|
|
const lrclib = await fetchLRCLIBLyrics(song);
|
|
if (lrclib && lrclib.length) return lrclib;
|
|
|
|
return null;
|
|
}
|
|
|
|
module.exports = { getSyncedLyrics, parseLRC, splitUnsyncedLyrics };
|