first commit

This commit is contained in:
Soph :3 2025-12-20 12:02:36 +02:00
commit 010598a8be
15 changed files with 1128 additions and 0 deletions

132
src/deezify.js Normal file
View file

@ -0,0 +1,132 @@
import fs from "fs";
import path from "path";
import crypto from "crypto"
const CACHE_DIR = "./deezer_cache";
fs.mkdirSync(CACHE_DIR, { recursive: true });
const artists = [
];
const BAD_KEYWORDS = [
" remix",
"remix ",
"remastered",
"extended mix",
"extended version",
" edit",
"live",
"acoustic",
"instrumental",
"anniversary",
"(remixes)",
"(remix)",
];
function normalize(str) {
return str.toLowerCase().trim();
}
function hasBadKeyword(title) {
const t = normalize(title);
return BAD_KEYWORDS.some(k => t.includes(k));
}
function cachePath(url) {
const hash = crypto.createHash("sha1").update(url).digest("hex");
return path.join(CACHE_DIR, `${hash}.json`);
}
async function cachedFetch(url) {
const file = cachePath(url);
if (fs.existsSync(file)) {
return JSON.parse(fs.readFileSync(file, "utf8"));
}
const res = await fetch(url);
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const json = await res.json();
fs.writeFileSync(file, JSON.stringify(json, null, 2));
return json;
}
async function getArtistId(name) {
const q = encodeURIComponent(name);
const data = await cachedFetch(
`https://api.deezer.com/search/artist?q=${q}`
);
return data?.data?.[0]?.id ?? null;
}
async function getAllAlbums(artistId) {
let url = `https://api.deezer.com/artist/${artistId}/albums?limit=200`;
let out = [];
while (url) {
const data = await cachedFetch(url);
out.push(...(data.data ?? []));
url = data.next ?? null;
}
return out;
}
function selectBestAlbumVersions(albums) {
const byTitle = new Map();
for (const a of albums) {
if (hasBadKeyword(a.title)) continue;
const key = normalize(a.title.replace(/\s*\(.*?\)\s*/g, ""));
if (!byTitle.has(key)) {
byTitle.set(key, a);
continue;
}
const existing = byTitle.get(key);
// Prefer explicit
if (!existing.explicit_lyrics && a.explicit_lyrics) {
byTitle.set(key, a);
}
}
return [...byTitle.values()];
}
(async () => {
const sex = [];
const sex_squared = []
for (const artist of artists) {
console.log(`\n=== ${artist} ===`);
const artistId = await getArtistId(artist);
if (!artistId) {
console.log("Artist not found");
continue;
}
const albums = await getAllAlbums(artistId);
const cleanAlbums = selectBestAlbumVersions(albums);
for (const a of cleanAlbums) {
console.log(
`${a.title} | ${a.explicit_lyrics ? "EXPLICIT" : "CLEAN"} | https://www.deezer.com/en/album/${a.id}`
);
sex_squared.push(`+ ${artist} - ${a.title} ${a.explicit_lyrics ? "[E]" : ""}`)
sex.push(`https://www.deezer.com/en/album/${a.id}`)
}
}
fs.writeFileSync("orpheus_links.txt", sex.join(" "))
fs.writeFileSync("discord_send.txt", sex_squared.join(" \n").match(/.{1,2000}$/gms).map(z => z.trim()).join("\n\n"+'-'.repeat(15)+"\n\n"))
})().catch(err => {
console.error(err);
process.exit(1);
});

112
src/now-playing.js Normal file
View file

@ -0,0 +1,112 @@
import { existsSync, readFileSync, writeFileSync } from "fs";
const salt = crypto.randomBytes(6).toString("hex");
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";
let messageId = null;
if (existsSync(STATE_FILE)) {
try {
const state = JSON.parse(readFileSync(STATE_FILE, "utf8"));
messageId = state.messageId ?? null;
} catch {
// will be made later
}
}
function saveState() {
writeFileSync(STATE_FILE, JSON.stringify({ messageId }, null, 2));
}
function colorFromString(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = (hash << 5) - hash + str.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) % 0xffffff;
}
async function getNowPlaying() {
const res = await fetch(NOW_PLAYING_URL);
if (!res.ok) throw new Error("Failed to fetch now playing");
const json = await res.json();
return json["subsonic-response"].nowPlaying?.entry ?? [];
}
async function buildPayload(entries) {
if (entries.length === 0) {
return { embeds: [{ title: "Nothing playing", description: "No active listeners", color: 0x808080 }], files: [] };
}
const embeds = [];
const files = [];
for (const e of entries) {
const filename = `cover_${e.id}.jpg`;
const imgRes = await fetch(COVER_FETCH_URL(e.coverArt));
if (imgRes.ok) {
const buf = await imgRes.arrayBuffer();
files.push({ name: filename, data: new Blob([buf]) });
}
embeds.push({
title: e.title,
description: `**${e.artist}** — *${e.album}*`,
color: colorFromString(e.username),
thumbnail: { url: `attachment://${filename}` },
fields: [
{ name: "User", value: e.username, 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 },
],
footer: { text: "Navidrome Now Playing" },
timestamp: new Date(e.played).toISOString(),
});
}
return { embeds, files };
}
async function sendOrResend(payload) {
// delete previous message if exists
if (messageId) {
await fetch(`${WEBHOOK_URL}/messages/${messageId}`, { method: "DELETE" }).catch(() => {});
messageId = null;
}
const form = new FormData();
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();
}
async function tick() {
try {
const entries = await getNowPlaying();
const payload = await buildPayload(entries);
await sendOrResend(payload);
} catch (err) {
console.error("Update failed:", err.message);
}
}
await tick();
setInterval(tick, REFRESH_MS);

139
src/pillow.js Normal file
View file

@ -0,0 +1,139 @@
import https from "https";
import { createWriteStream } from "fs";
// ======== COLOR UTILS ==========
const C = {
info: msg => console.log("\x1b[36m[i]\x1b[0m " + msg),
ok: msg => console.log("\x1b[32m[✓]\x1b[0m " + msg),
warn: msg => console.log("\x1b[33m[!]\x1b[0m " + msg),
err: msg => console.log("\x1b[31m[✗]\x1b[0m " + msg),
};
// ======== DOWNLOAD WITH PROGRESS ==========
function download(url, filename, referer = url) {
return new Promise((resolve, reject) => {
let file = null; // will open later
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);
}
// --- Error handler ---
if (res.statusCode !== 200) {
return reject(new Error(`HTTP ${res.statusCode}`));
}
// ======= OPEN FILE NOW (final target reached) =======
if (!file) file = createWriteStream(filename);
// ======= Progress =======
const total = parseInt(res.headers["content-length"] || "0", 10);
let downloaded = 0;
const start = Date.now();
res.on("data", chunk => {
downloaded += chunk.length;
if (total) {
const percent = ((downloaded / total) * 100).toFixed(1);
const speed = (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.pipe(file);
file.on("finish", () => {
process.stdout.write("\n");
C.ok("Download complete!");
resolve();
});
}).on("error", err => reject(err));
}
doReq(url, referer);
});
}
// ======== MAIN FUNCTION ==========
(async () => {
const target = process.argv[2];
if (!target) {
C.err("Usage: node pillows-dl.js https://pillows.su/f/ID");
process.exit(1);
}
C.info(`Fetching webpage: ${target}`);
let html;
try {
html = await fetch(target, { headers: { "User-Agent": "Mozilla/5.0" } }).then(r => r.text());
} catch (e) {
C.err("Failed to fetch the page");
console.error(e);
process.exit(1);
}
const match = html.match(/data:\s*\[(.*)\],/);
if (!match) {
C.err("❌ Failed to extract data[] from webpage!");
process.exit(1);
}
C.ok("Found data[] block");
// Safer eval (only our captured data)
let arr;
try {
arr = eval("["+match[1]+"]");
} catch (e) {
C.err("❌ Failed to parse data[] as JavaScript");
console.error(e);
process.exit(1);
}
const item = arr.find(x => x?.type === "data" && x?.data?.filename);
if (!item) {
C.err("❌ Could not find downloadable file info in data[]");
process.exit(1);
}
const data = item.data;
C.ok(`File: ${data.filename}`);
C.info(`ID: ${data.id}`);
C.info(`Views: ${data.views}`);
C.info(`Bitrate: ${data.bitrate || "??"} kbps`);
const url = `https://api.pillows.su/api/download/${data.id}.mp3`;
try {
await download(url, data.filename);
C.ok("Done!");
} catch (e) {
C.err("❌ Download failed");
console.error(e);
process.exit(1);
}
})();

242
src/request-bot.js Normal file
View file

@ -0,0 +1,242 @@
import {
Client,
GatewayIntentBits,
SlashCommandBuilder,
REST,
Routes,
} from "discord.js";
import crypto from "crypto";
const GUILD_ID = "1449756600733798501";
const REQUEST_CHANNEL_ID = "1449767378882920568";
const client = new Client({
intents: [GatewayIntentBits.Guilds],
});
const command = new SlashCommandBuilder()
.setName("request")
.setDescription("Request a new artist or submit a SendGB link")
.addStringOption(opt =>
opt
.setName("artist")
.setDescription("Artist name (Last.fm)")
.setRequired(false)
)
.addStringOption(opt =>
opt
.setName("url")
.setDescription("SendGB URL (https://sendgb.com/...)")
.setRequired(false)
)
.addStringOption(opt =>
opt
.setName("description")
.setDescription("Description for the SendGB link")
.setRequired(false)
);
function isValidSendGbUrl(url) {
return /^https:\/\/sendgb\.com\/[A-Za-z0-9]+$/.test(url);
}
const rest = new REST({ version: "10" }).setToken(process.env.DISCORD_TOKEN);
await rest.put(
Routes.applicationGuildCommands(
(await rest.get(Routes.oauth2CurrentApplication())).id,
GUILD_ID
),
{ body: [command.toJSON()] }
);
async function getLastFmArtist(artist) {
const url =
"https://ws.audioscrobbler.com/2.0/?" +
new URLSearchParams({
method: "artist.getinfo",
artist,
api_key: process.env.LASTFM_API_KEY,
format: "json",
});
const res = await fetch(url);
const text = await res.text();
let data;
try {
data = JSON.parse(text);
} catch {
console.error("Last.fm response was not JSON:");
console.error(text.slice(0, 500));
return null;
}
if (!data.artist) return null;
return data.artist;
}
async function navidromeHasArtist(artist) {
const salt = crypto.randomBytes(6).toString("hex");
const token = crypto
.createHash("md5")
.update(process.env.NAVIDROME_PASS + salt)
.digest("hex");
const url =
`${process.env.NAVIDROME_URL}/rest/search3.view?` +
new URLSearchParams({
u: process.env.NAVIDROME_USER,
t: token,
s: salt,
v: "1.16.1",
c: "discord-bot",
query: artist,
f: "json"
});
const res = await fetch(url);
const text = await res.text();
let json;
try {
json = JSON.parse(text);
} catch {
console.error("Navidrome response was not JSON:");
console.error(text.slice(0, 500));
return false;
}
const artists =
json["subsonic-response"]?.searchResult3?.artist ?? [];
return artists.some(
a => a.name.toLowerCase() === artist.toLowerCase()
);
}
client.on("interactionCreate", async interaction => {
if (!interaction.isChatInputCommand()) return;
if (interaction.commandName !== "request") return;
if (interaction.guildId !== GUILD_ID) {
return interaction.reply({
content: "❌ This command can only be used in the main server.",
flags: {
}
});
}
const artistName = interaction.options.getString("artist");
const url = interaction.options.getString("url");
const description = interaction.options.getString("description");
await interaction.deferReply({ ephemeral: true });
if (!artistName && !url) {
return interaction.editReply({
content: "❌ You must provide either an artist name or a SendGB URL.",
});
}
if (url) {
if (!isValidSendGbUrl(url)) {
return interaction.editReply({
content: "❌ Invalid SendGB URL format.",
});
}
if (!description) {
return interaction.editReply({
content: "❌ A description is required when submitting a URL.",
});
}
}
if (artistName) {
if (await navidromeHasArtist(artistName)) {
return interaction.editReply({
content: "❌ This artist already exists in Navidrome.",
});
}
const artist = await getLastFmArtist(artistName);
if (!artist) {
return interaction.editReply({
content: "❌ Artist not found on Last.fm.",
});
}
const channel = await client.channels.fetch(REQUEST_CHANNEL_ID);
await channel.send({
embeds: [
{
title: artist.name,
url: artist.url,
description:
artist.bio?.summary?.replace(/<[^>]*>/g, "") ??
"No description available.",
thumbnail: {
url:
artist.image?.find(i => i.size === "extralarge")?.["#text"] ??
null,
},
fields: [
{ name: "Listeners", value: artist.stats.listeners, 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}>`,
},
],
},
],
});
return interaction.editReply({
content: "✅ Artist request submitted.",
});
}
const channel = await client.channels.fetch(REQUEST_CHANNEL_ID);
await channel.send({
embeds: [
{
title: "📦 External Upload",
description,
fields: [
{
name: "Download",
value: url,
},
{
name: "Requested by",
value: `<@${interaction.user.id}>`,
},
],
},
],
});
await interaction.editReply({
content: "✅ Link submitted.",
});
});
/* --------------------------------- Ready -------------------------------- */
client.once("ready", () => {
console.log(`Logged in as ${client.user.tag}`);
});
client.login(process.env.DISCORD_TOKEN);