139 lines
3.8 KiB
JavaScript
139 lines
3.8 KiB
JavaScript
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);
|
|
}
|
|
})();
|