import { Client, GatewayIntentBits } from "discord.js"; import crypto from "crypto"; import { existsSync, readFileSync, writeFileSync } from "fs"; /* ================= CONFIG ================= */ const REFRESH_MS = Number(process.env.REFRESH_MS ?? 15_000); const STATE_FILE = "./state.json"; /* ================= DISCORD ================= */ const client = new Client({ intents: [GatewayIntentBits.Guilds], }); /* ================= STATE ================= */ let messageId = null; let lastSignature = null; if (existsSync(STATE_FILE)) { try { const state = JSON.parse(readFileSync(STATE_FILE, "utf8")); messageId = state.messageId ?? null; lastSignature = state.lastSignature ?? null; } catch {} } function saveState() { writeFileSync( STATE_FILE, JSON.stringify({ messageId, lastSignature }, null, 2) ); } /* ================= NAVIDROME AUTH ================= */ function navidromeAuth() { const salt = crypto.randomBytes(6).toString("hex"); const token = crypto .createHash("md5") .update(process.env.NAVIDROME_PASS + salt) .digest("hex"); return { salt, token }; } function nowPlayingUrl() { const { salt, token } = navidromeAuth(); return `${process.env.NAVIDROME_URL}/rest/getNowPlaying` + `?u=${process.env.NAVIDROME_USER}` + `&t=${token}&s=${salt}&f=json&v=1.16.1&c=discord-bot`; } function coverFetchUrl(id) { const { salt, token } = navidromeAuth(); return `${process.env.NAVIDROME_URL}/rest/getCoverArt` + `?u=${process.env.NAVIDROME_USER}` + `&t=${token}&s=${salt}&v=1.16.1&c=discord-bot` + `&id=${id}&size=96`; } /* ================= UTIL ================= */ function colorFromString(str) { let hash = 0; for (const c of str) hash = (hash << 5) - hash + c.charCodeAt(0); return Math.abs(hash) % 0xffffff; } function signature(entries) { return entries .map(e => `${e.username}:${e.id}:${e.minutesAgo ?? 0}`) .join("|"); } /* ================= NAVIDROME ================= */ async function getNowPlaying() { const res = await fetch(nowPlayingUrl()); if (!res.ok) throw new Error("Failed to fetch now playing"); const json = await res.json(); return json["subsonic-response"].nowPlaying?.entry ?? []; } /* ================= MESSAGE BUILD ================= */ async function buildMessage(entries) { if (!entries.length) { return { embeds: [{ title: "Nothing playing", description: "No active listeners", color: 0x808080, }], files: [], }; } const embeds = []; const files = []; for (let i = 0; i < e DISCORD_TOKEN="no.no" LASTFM_API_KEY=no NAVIDROME_URL=https://no.no.no NAVIDROME_USER="no" NAVIDROME_PASS="no" SPOTIFY_CLIENT_ID="no" SPOTIFY_CLIENT_SECRET="no"