format everything, make now playying actually good
This commit is contained in:
parent
4a3b3aabcf
commit
fca2614603
5 changed files with 313 additions and 173 deletions
98
.env.example
98
.env.example
|
|
@ -1,4 +1,100 @@
|
||||||
WEBHOOK_URL=https://discord.com/api/webhooks/no/no
|
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"
|
DISCORD_TOKEN="no.no"
|
||||||
LASTFM_API_KEY=no
|
LASTFM_API_KEY=no
|
||||||
|
|
||||||
|
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,5 +2,5 @@
|
||||||
deezer_cache
|
deezer_cache
|
||||||
orpheus_links.txt
|
orpheus_links.txt
|
||||||
discord_send.txt
|
discord_send.txt
|
||||||
|
state.json
|
||||||
node_modules
|
node_modules
|
||||||
|
|
|
||||||
|
|
@ -1,112 +1,164 @@
|
||||||
|
import { Client, GatewayIntentBits } from "discord.js";
|
||||||
|
import crypto from "crypto";
|
||||||
import { existsSync, readFileSync, writeFileSync } from "fs";
|
import { existsSync, readFileSync, writeFileSync } from "fs";
|
||||||
import crypto from "crypto"
|
|
||||||
|
|
||||||
const salt = crypto.randomBytes(6).toString("hex");
|
const REFRESH_MS = Number(process.env.REFRESH_MS ?? 15_000);
|
||||||
const token = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(process.env.NAVIDROME_PASS + salt)
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
|
|
||||||
const NOW_PLAYING_URL = `${process.env.NAVIDROME_URL}/rest/getNowPlaying?u=${process.env.NAVIDROME_USER}&t=${token}&s=${salt}&f=json&v=0.0.1&c=now-playing`;
|
|
||||||
const COVER_FETCH_URL = (id) =>
|
|
||||||
`${process.env.NAVIDROME_URL}/rest/getCoverArt?u=${process.env.NAVIDROME_USER}&t=${token}&s=${salt}&f=json&v=0.0.1&c=now-playing&id=${id}&size=80`;
|
|
||||||
|
|
||||||
const WEBHOOK_URL = process.env.WEBHOOK_URL;
|
|
||||||
const REFRESH_MS = 15_000;
|
|
||||||
const STATE_FILE = "./state.json";
|
const STATE_FILE = "./state.json";
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.Guilds],
|
||||||
|
});
|
||||||
|
|
||||||
let messageId = null;
|
let messageId = null;
|
||||||
|
let lastSignature = null;
|
||||||
|
|
||||||
if (existsSync(STATE_FILE)) {
|
if (existsSync(STATE_FILE)) {
|
||||||
try {
|
try {
|
||||||
const state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
|
const state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
|
||||||
messageId = state.messageId ?? null;
|
messageId = state.messageId ?? null;
|
||||||
} catch {
|
lastSignature = state.lastSignature ?? null;
|
||||||
// will be made later
|
} catch {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveState() {
|
function saveState() {
|
||||||
writeFileSync(STATE_FILE, JSON.stringify({ messageId }, null, 2));
|
writeFileSync(
|
||||||
|
STATE_FILE,
|
||||||
|
JSON.stringify({ messageId, lastSignature }, null, 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function colorFromString(str) {
|
function colorFromString(str) {
|
||||||
let hash = 0;
|
let hash = 0;
|
||||||
for (let i = 0; i < str.length; i++) {
|
for (const c of str) hash = (hash << 5) - hash + c.charCodeAt(0);
|
||||||
hash = (hash << 5) - hash + str.charCodeAt(i);
|
|
||||||
hash |= 0;
|
|
||||||
}
|
|
||||||
return Math.abs(hash) % 0xffffff;
|
return Math.abs(hash) % 0xffffff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signature(entries) {
|
||||||
|
return entries
|
||||||
|
.map((e) => `${e.username}:${e.id}:${e.minutesAgo ?? 0}`)
|
||||||
|
.join("|");
|
||||||
|
}
|
||||||
|
|
||||||
async function getNowPlaying() {
|
async function getNowPlaying() {
|
||||||
const res = await fetch(NOW_PLAYING_URL);
|
const res = await fetch(nowPlayingUrl());
|
||||||
if (!res.ok) throw new Error("Failed to fetch now playing");
|
if (!res.ok) throw new Error("Failed to fetch now playing");
|
||||||
|
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
return json["subsonic-response"].nowPlaying?.entry ?? [];
|
return json["subsonic-response"].nowPlaying?.entry ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildPayload(entries) {
|
async function buildMessage(entries) {
|
||||||
if (entries.length === 0) {
|
if (!entries.length) {
|
||||||
return { embeds: [{ title: "Nothing playing", description: "No active listeners", color: 0x808080 }], files: [] };
|
return {
|
||||||
|
embeds: [
|
||||||
|
{
|
||||||
|
title: "Nothing playing",
|
||||||
|
description: "No active listeners",
|
||||||
|
color: 0x808080,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
files: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const embeds = [];
|
const embeds = [];
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|
||||||
for (const e of entries) {
|
for (let i = 0; i < entries.length; i++) {
|
||||||
const filename = `cover_${e.id}.jpg`;
|
const e = entries[i];
|
||||||
const imgRes = await fetch(COVER_FETCH_URL(e.coverArt));
|
const filename = `cover_${i}.jpg`;
|
||||||
|
|
||||||
|
const imgRes = await fetch(coverFetchUrl(e.coverArt));
|
||||||
if (imgRes.ok) {
|
if (imgRes.ok) {
|
||||||
const buf = await imgRes.arrayBuffer();
|
const buf = Buffer.from(await imgRes.arrayBuffer());
|
||||||
files.push({ name: filename, data: new Blob([buf]) });
|
files.push({ attachment: buf, name: filename });
|
||||||
}
|
}
|
||||||
|
|
||||||
embeds.push({
|
embeds.push({
|
||||||
title: e.title,
|
title: e.title,
|
||||||
description: `**${e.artist}** — *${e.album}*`,
|
description: `**${e.artist}** — *${e.album}*`,
|
||||||
color: colorFromString(e.username),
|
color: colorFromString(e.username),
|
||||||
thumbnail: { url: `attachment://${filename}` },
|
thumbnail: files.length ? { url: `attachment://${filename}` } : undefined,
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "User", value: e.username, inline: true },
|
{ name: "User", value: e.username, inline: true },
|
||||||
{ name: "Player", value: e.playerName, inline: true },
|
{ name: "Player", value: e.playerName, inline: true },
|
||||||
{ name: "Duration", value: `${Math.floor(e.duration / 60)}:${String(e.duration % 60).padStart(2, "0")}`, inline: false },
|
{
|
||||||
|
name: "Duration",
|
||||||
|
value: `${Math.floor(e.duration / 60)}:${String(
|
||||||
|
e.duration % 60,
|
||||||
|
).padStart(2, "0")}`,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
footer: { text: "now playing!" },
|
footer: { text: "now playing" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { embeds, files };
|
return { embeds, files };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendOrResend(payload) {
|
async function updateMessage() {
|
||||||
// delete previous message if exists
|
const entries = await getNowPlaying();
|
||||||
if (messageId) {
|
const sig = signature(entries);
|
||||||
await fetch(`${WEBHOOK_URL}/messages/${messageId}`, { method: "DELETE" }).catch(() => {});
|
|
||||||
messageId = null;
|
if (sig === lastSignature) return;
|
||||||
|
|
||||||
|
const payload = await buildMessage(entries);
|
||||||
|
const channel = await client.channels.fetch(process.env.DISCORD_CHANNEL_ID);
|
||||||
|
|
||||||
|
if (!messageId) {
|
||||||
|
const msg = await channel.send(payload);
|
||||||
|
messageId = msg.id;
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
const msg = await channel.messages.fetch(messageId);
|
||||||
|
await msg.edit(payload);
|
||||||
|
} catch {
|
||||||
|
const msg = await channel.send(payload);
|
||||||
|
messageId = msg.id;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const form = new FormData();
|
lastSignature = sig;
|
||||||
payload.files.forEach((f, i) => form.append(`files[${i}]`, f.data, f.name));
|
|
||||||
form.append("payload_json", JSON.stringify({ embeds: payload.embeds }));
|
|
||||||
|
|
||||||
const res = await fetch(`${WEBHOOK_URL}?wait=true`, { method: "POST", body: form });
|
|
||||||
if (!res.ok) throw new Error("Webhook send failed");
|
|
||||||
const data = await res.json();
|
|
||||||
messageId = data.id;
|
|
||||||
saveState();
|
saveState();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tick() {
|
client.once("ready", async () => {
|
||||||
try {
|
console.log(`Logged in as ${client.user.tag}`);
|
||||||
const entries = await getNowPlaying();
|
await updateMessage();
|
||||||
const payload = await buildPayload(entries);
|
setInterval(() => {
|
||||||
await sendOrResend(payload);
|
updateMessage().catch((err) =>
|
||||||
} catch (err) {
|
console.error("Update failed:", err.message),
|
||||||
console.error("Update failed:", err.message);
|
);
|
||||||
}
|
}, REFRESH_MS);
|
||||||
}
|
});
|
||||||
|
|
||||||
await tick();
|
client.login(process.env.DISCORD_BOT_TOKEN);
|
||||||
setInterval(tick, REFRESH_MS);
|
|
||||||
|
|
|
||||||
137
src/pillow.js
137
src/pillow.js
|
|
@ -1,12 +1,10 @@
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import { createWriteStream } from "fs";
|
import { createWriteStream } from "fs";
|
||||||
|
|
||||||
// ======== COLOR UTILS ==========
|
info: (msg) => console.log("\x1b[36m[i]\x1b[0m " + msg),
|
||||||
const C = {
|
ok: (msg) => console.log("\x1b[32m[✓]\x1b[0m " + msg),
|
||||||
info: msg => console.log("\x1b[36m[i]\x1b[0m " + msg),
|
warn: (msg) => console.log("\x1b[33m[!]\x1b[0m " + msg),
|
||||||
ok: msg => console.log("\x1b[32m[✓]\x1b[0m " + msg),
|
err: (msg) => console.log("\x1b[31m[✗]\x1b[0m " + msg),
|
||||||
warn: msg => console.log("\x1b[33m[!]\x1b[0m " + msg),
|
|
||||||
err: msg => console.log("\x1b[31m[✗]\x1b[0m " + msg),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// ======== DOWNLOAD WITH PROGRESS ==========
|
// ======== DOWNLOAD WITH PROGRESS ==========
|
||||||
|
|
@ -15,69 +13,78 @@ function download(url, filename, referer = url) {
|
||||||
let file = null; // will open later
|
let file = null; // will open later
|
||||||
|
|
||||||
function doReq(link, ref) {
|
function doReq(link, ref) {
|
||||||
C.info(`Requesting: ${link}`);
|
https
|
||||||
|
.get(
|
||||||
|
link,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"User-Agent": "Mozilla/5.0",
|
||||||
|
Referer: ref,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
// --- Redirect handler ---
|
||||||
|
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
||||||
|
const loc = res.headers.location;
|
||||||
|
if (!loc)
|
||||||
|
return reject(new Error("Redirect without Location header"));
|
||||||
|
const nextURL = loc.startsWith("http")
|
||||||
|
? loc
|
||||||
|
: new URL(loc, link).href;
|
||||||
|
C.warn(`↪ Redirect (${res.statusCode}): ${link} → ${nextURL}`);
|
||||||
|
return doReq(nextURL, link);
|
||||||
|
}
|
||||||
|
|
||||||
https.get(link, {
|
// --- Error handler ---
|
||||||
headers: {
|
if (res.statusCode !== 200) {
|
||||||
"User-Agent": "Mozilla/5.0",
|
return reject(new Error(`HTTP ${res.statusCode}`));
|
||||||
"Referer": ref
|
}
|
||||||
}
|
|
||||||
}, res => {
|
|
||||||
|
|
||||||
// --- Redirect handler ---
|
// ======= OPEN FILE NOW (final target reached) =======
|
||||||
if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
|
if (!file) file = createWriteStream(filename);
|
||||||
const loc = res.headers.location;
|
|
||||||
if (!loc) return reject(new Error("Redirect without Location header"));
|
|
||||||
const nextURL = loc.startsWith("http") ? loc : new URL(loc, link).href;
|
|
||||||
C.warn(`↪ Redirect (${res.statusCode}): ${link} → ${nextURL}`);
|
|
||||||
return doReq(nextURL, link);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Error handler ---
|
// ======= Progress =======
|
||||||
if (res.statusCode !== 200) {
|
const total = parseInt(res.headers["content-length"] || "0", 10);
|
||||||
return reject(new Error(`HTTP ${res.statusCode}`));
|
let downloaded = 0;
|
||||||
}
|
const start = Date.now();
|
||||||
|
|
||||||
// ======= OPEN FILE NOW (final target reached) =======
|
res.on("data", (chunk) => {
|
||||||
if (!file) file = createWriteStream(filename);
|
downloaded += chunk.length;
|
||||||
|
|
||||||
// ======= Progress =======
|
if (total) {
|
||||||
const total = parseInt(res.headers["content-length"] || "0", 10);
|
const percent = ((downloaded / total) * 100).toFixed(1);
|
||||||
let downloaded = 0;
|
const speed = (
|
||||||
const start = Date.now();
|
downloaded /
|
||||||
|
1024 /
|
||||||
|
((Date.now() - start) / 1000)
|
||||||
|
).toFixed(1);
|
||||||
|
const barSize = 30;
|
||||||
|
const filled = Math.round((percent / 100) * barSize);
|
||||||
|
const bar =
|
||||||
|
"[" + "#".repeat(filled) + "-".repeat(barSize - filled) + "]";
|
||||||
|
process.stdout.write(`\r${bar} ${percent}% ${speed}KB/s`);
|
||||||
|
} else {
|
||||||
|
process.stdout.write(
|
||||||
|
`\rDownloaded ${Math.round(downloaded / 1024)}KB`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
res.on("data", chunk => {
|
res.pipe(file);
|
||||||
downloaded += chunk.length;
|
|
||||||
|
|
||||||
if (total) {
|
file.on("finish", () => {
|
||||||
const percent = ((downloaded / total) * 100).toFixed(1);
|
process.stdout.write("\n");
|
||||||
const speed = (downloaded / 1024 / ((Date.now() - start) / 1000)).toFixed(1);
|
C.ok("Download complete!");
|
||||||
const barSize = 30;
|
resolve();
|
||||||
const filled = Math.round(percent / 100 * barSize);
|
});
|
||||||
const bar = "[" + "#".repeat(filled) + "-".repeat(barSize - filled) + "]";
|
},
|
||||||
process.stdout.write(`\r${bar} ${percent}% ${speed}KB/s`);
|
)
|
||||||
} else {
|
.on("error", (err) => reject(err));
|
||||||
process.stdout.write(`\rDownloaded ${Math.round(downloaded / 1024)}KB`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
res.pipe(file);
|
|
||||||
|
|
||||||
file.on("finish", () => {
|
|
||||||
process.stdout.write("\n");
|
|
||||||
C.ok("Download complete!");
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
|
|
||||||
}).on("error", err => reject(err));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doReq(url, referer);
|
doReq(url, referer);
|
||||||
});
|
});
|
||||||
}
|
// ======== MAIN FUNCTION ==========
|
||||||
|
|
||||||
|
|
||||||
// ======== MAIN FUNCTION ==========
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const target = process.argv[2];
|
const target = process.argv[2];
|
||||||
|
|
||||||
|
|
@ -88,9 +95,9 @@ function download(url, filename, referer = url) {
|
||||||
|
|
||||||
C.info(`Fetching webpage: ${target}`);
|
C.info(`Fetching webpage: ${target}`);
|
||||||
|
|
||||||
let html;
|
html = await fetch(target, {
|
||||||
try {
|
headers: { "User-Agent": "Mozilla/5.0" },
|
||||||
html = await fetch(target, { headers: { "User-Agent": "Mozilla/5.0" } }).then(r => r.text());
|
}).then((r) => r.text());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
C.err("Failed to fetch the page");
|
C.err("Failed to fetch the page");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
@ -105,16 +112,12 @@ function download(url, filename, referer = url) {
|
||||||
C.ok("Found data[] block");
|
C.ok("Found data[] block");
|
||||||
|
|
||||||
// Safer eval (only our captured data)
|
// Safer eval (only our captured data)
|
||||||
let arr;
|
arr = eval("[" + match[1] + "]");
|
||||||
try {
|
|
||||||
arr = eval("["+match[1]+"]");
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
C.err("❌ Failed to parse data[] as JavaScript");
|
C.err("❌ Failed to parse data[] as JavaScript");
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
const item = arr.find((x) => x?.type === "data" && x?.data?.filename);
|
||||||
|
|
||||||
const item = arr.find(x => x?.type === "data" && x?.data?.filename);
|
|
||||||
if (!item) {
|
if (!item) {
|
||||||
C.err("❌ Could not find downloadable file info in data[]");
|
C.err("❌ Could not find downloadable file info in data[]");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
||||||
|
|
@ -12,25 +12,23 @@ const client = new Client({
|
||||||
});
|
});
|
||||||
|
|
||||||
const command = new SlashCommandBuilder()
|
const command = new SlashCommandBuilder()
|
||||||
.setName("request")
|
.addStringOption((opt) =>
|
||||||
.setDescription("Request a new artist or submit a SendGB link")
|
|
||||||
.addStringOption(opt =>
|
|
||||||
opt
|
opt
|
||||||
.setName("artist")
|
.setName("artist")
|
||||||
.setDescription("Artist name (Last.fm)")
|
.setDescription("Artist name (Last.fm)")
|
||||||
.setRequired(false)
|
.setRequired(false),
|
||||||
)
|
)
|
||||||
.addStringOption(opt =>
|
.addStringOption((opt) =>
|
||||||
opt
|
opt
|
||||||
.setName("url")
|
.setName("url")
|
||||||
.setDescription("SendGB URL (https://sendgb.com/...)")
|
.setDescription("SendGB URL (https://sendgb.com/...)")
|
||||||
.setRequired(false)
|
.setRequired(false),
|
||||||
)
|
)
|
||||||
.addStringOption(opt =>
|
.addStringOption((opt) =>
|
||||||
opt
|
opt
|
||||||
.setName("description")
|
.setName("description")
|
||||||
.setDescription("Description for the SendGB link")
|
.setDescription("Description for the SendGB link")
|
||||||
.setRequired(false)
|
.setRequired(false),
|
||||||
);
|
);
|
||||||
|
|
||||||
function isValidSendGbUrl(url) {
|
function isValidSendGbUrl(url) {
|
||||||
|
|
@ -43,14 +41,11 @@ function isValidDeezerUrl(url) {
|
||||||
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
|
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
|
||||||
|
|
||||||
await rest.put(
|
await rest.put(
|
||||||
Routes.applicationGuildCommands(
|
process.env.GUILD_ID,
|
||||||
(await rest.get(Routes.oauth2CurrentApplication())).id,
|
|
||||||
process.env.GUILD_ID
|
|
||||||
),
|
),
|
||||||
{ body: [command.toJSON()] }
|
{ body: [command.toJSON()] },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
async function getDeezerAlbum(url) {
|
async function getDeezerAlbum(url) {
|
||||||
const match = url.match(/album\/(\d+)/);
|
const match = url.match(/album\/(\d+)/);
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
|
|
@ -65,9 +60,8 @@ async function getDeezerAlbum(url) {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
artist: data.artist.name,
|
artist: data.artist.name,
|
||||||
link: data.link,
|
link: data.link,
|
||||||
cover: data.cover_medium,
|
tracks:
|
||||||
releaseDate: data.release_date,
|
data.tracks?.data?.map((t) => t.title).join(", ") || "No tracks info",
|
||||||
tracks: data.tracks?.data?.map(t => t.title).join(", ") || "No tracks info",
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,10 +85,7 @@ async function getLastFmArtist(artist) {
|
||||||
console.error("Last.fm response was not JSON:");
|
console.error("Last.fm response was not JSON:");
|
||||||
console.error(text.slice(0, 500));
|
console.error(text.slice(0, 500));
|
||||||
return null;
|
return null;
|
||||||
}
|
if (!data.artist) return null;
|
||||||
|
|
||||||
|
|
||||||
if (!data.artist) return null;
|
|
||||||
return data.artist;
|
return data.artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,9 +103,7 @@ async function navidromeHasArtist(artist) {
|
||||||
t: token,
|
t: token,
|
||||||
s: salt,
|
s: salt,
|
||||||
v: "1.16.1",
|
v: "1.16.1",
|
||||||
c: "discord-bot",
|
f: "json",
|
||||||
query: artist,
|
|
||||||
f: "json"
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|
@ -127,18 +116,12 @@ async function navidromeHasArtist(artist) {
|
||||||
console.error("Navidrome response was not JSON:");
|
console.error("Navidrome response was not JSON:");
|
||||||
console.error(text.slice(0, 500));
|
console.error(text.slice(0, 500));
|
||||||
return false;
|
return false;
|
||||||
}
|
const artists = json["subsonic-response"]?.searchResult3?.artist ?? [];
|
||||||
|
|
||||||
const artists =
|
return artists.some((a) => a.name.toLowerCase() === artist.toLowerCase());
|
||||||
json["subsonic-response"]?.searchResult3?.artist ?? [];
|
|
||||||
|
|
||||||
return artists.some(
|
|
||||||
a => a.name.toLowerCase() === artist.toLowerCase()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
client.on("interactionCreate", async (interaction) => {
|
||||||
client.on("interactionCreate", async interaction => {
|
|
||||||
if (!interaction.isChatInputCommand()) return;
|
if (!interaction.isChatInputCommand()) return;
|
||||||
if (interaction.commandName !== "request") return;
|
if (interaction.commandName !== "request") return;
|
||||||
|
|
||||||
|
|
@ -175,9 +158,10 @@ client.on("interactionCreate", async interaction => {
|
||||||
embeds: [
|
embeds: [
|
||||||
{
|
{
|
||||||
title: "📦 External Upload",
|
title: "📦 External Upload",
|
||||||
description,
|
{
|
||||||
fields: [
|
name: "Download",
|
||||||
{ name: "Download", value: url+"#"+encodeURIComponent(description) },
|
value: url + "#" + encodeURIComponent(description),
|
||||||
|
},
|
||||||
{ name: "Requested by", value: `<@${interaction.user.id}>` },
|
{ name: "Requested by", value: `<@${interaction.user.id}>` },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
@ -208,9 +192,9 @@ client.on("interactionCreate", async interaction => {
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
return interaction.editReply({
|
||||||
|
content: "✅ Deezer album info submitted.",
|
||||||
});
|
});
|
||||||
|
|
||||||
return interaction.editReply({ content: "✅ Deezer album info submitted." });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return interaction.editReply({
|
return interaction.editReply({
|
||||||
|
|
@ -225,9 +209,9 @@ client.on("interactionCreate", async interaction => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const artist = await getLastFmArtist(artistName);
|
return interaction.editReply({
|
||||||
if (!artist) {
|
content: "❌ Artist not found on Last.fm.",
|
||||||
return interaction.editReply({ content: "❌ Artist not found on Last.fm." });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await channel.send({
|
await channel.send({
|
||||||
|
|
@ -235,17 +219,22 @@ client.on("interactionCreate", async interaction => {
|
||||||
{
|
{
|
||||||
title: artist.name,
|
title: artist.name,
|
||||||
url: artist.url,
|
url: artist.url,
|
||||||
description: artist.bio?.summary?.replace(/<[^>]*>/g, "") ?? "No description available.",
|
description:
|
||||||
|
artist.bio?.summary?.replace(/<[^>]*>/g, "") ??
|
||||||
|
"No description available.",
|
||||||
thumbnail: {
|
thumbnail: {
|
||||||
url: artist.image?.find(i => i.size === "extralarge")?.["#text"] ?? null,
|
url:
|
||||||
|
artist.image?.find((i) => i.size === "extralarge")?.["#text"] ??
|
||||||
|
null,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{ name: "Listeners", value: artist.stats.listeners, inline: true },
|
{ name: "Listeners", value: artist.stats.listeners, inline: true },
|
||||||
{ name: "Playcount", value: artist.stats.playcount, inline: true },
|
{ name: "Playcount", value: artist.stats.playcount, inline: true },
|
||||||
{ name: "Tags", value: artist.tags?.tag?.map(t => t.name).join(", ") || "None" },
|
{
|
||||||
{ name: "Requested by", value: `<@${interaction.user.id}>` },
|
name: "Tags",
|
||||||
],
|
value: artist.tags?.tag?.map((t) => t.name).join(", ") || "None",
|
||||||
},
|
},
|
||||||
|
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue