You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
153 lines
4.1 KiB
153 lines
4.1 KiB
import renderLottie from "puppeteer-lottie";
|
|
import * as fs from "fs/promises";
|
|
import { spawn } from "node:child_process";
|
|
|
|
const API_URL = "https://discord.com/api/v9/sticker-packs";
|
|
const STICKER_SIZE = 160;
|
|
|
|
const LOTTIE_BASE_URL = "https://discord.com/stickers/{id}.json";
|
|
const PNG_BASE_URL = "https://media.discordapp.net/stickers/{id}.png";
|
|
|
|
interface StickerPacks {
|
|
sticker_packs: StickerPack[];
|
|
}
|
|
|
|
interface StickerPack {
|
|
id: string;
|
|
stickers: Sticker[];
|
|
name: string;
|
|
sku_id: string;
|
|
cover_sticker_id: string;
|
|
description: string;
|
|
banner_asset_id?: string;
|
|
}
|
|
|
|
interface Sticker {
|
|
id: string;
|
|
name: string;
|
|
tags: string;
|
|
type: number;
|
|
format_type: StickerFormat;
|
|
description: string;
|
|
asset: string;
|
|
pack_id: string;
|
|
sort_value: number;
|
|
}
|
|
|
|
enum StickerFormat {
|
|
STATIC = 1,
|
|
APNG = 2,
|
|
LOTTIE = 3,
|
|
}
|
|
|
|
async function findFilesWithPrefix(prefix: string, path: string): Promise<string[]> {
|
|
const files = await fs.readdir(path);
|
|
const filteredFiles = files.filter((file) => file.startsWith(prefix));
|
|
return filteredFiles;
|
|
}
|
|
|
|
const stickerPacks: StickerPacks = await (await fetch(API_URL)).json();
|
|
|
|
await fs.mkdir("output");
|
|
await fs.mkdir("temp");
|
|
|
|
for (const pack of stickerPacks.sticker_packs) {
|
|
for (const sticker of pack.stickers) {
|
|
switch (sticker.format_type) {
|
|
case StickerFormat.STATIC:
|
|
{
|
|
const stickerFile = await (
|
|
await fetch(PNG_BASE_URL.replace("{id}", sticker.id))
|
|
).blob();
|
|
await fs.writeFile(
|
|
`output/${sticker.id}.png`,
|
|
Buffer.from(await stickerFile.arrayBuffer())
|
|
);
|
|
}
|
|
break;
|
|
|
|
case StickerFormat.APNG:
|
|
{
|
|
const stickerFile = await (
|
|
await fetch(PNG_BASE_URL.replace("{id}", sticker.id))
|
|
).blob();
|
|
await fs.writeFile(
|
|
`temp/${sticker.id}.apng`,
|
|
Buffer.from(await stickerFile.arrayBuffer())
|
|
);
|
|
|
|
const ffmpeg = spawn("ffmpeg", [
|
|
"-v",
|
|
"error",
|
|
"-i",
|
|
`temp/${sticker.id}.apng`,
|
|
`temp/${sticker.id}-%8d.png`,
|
|
]);
|
|
ffmpeg.stdout.on("data", (data) => {
|
|
console.log(`stdout: ${data}`);
|
|
});
|
|
ffmpeg.stderr.on("data", (data) => {
|
|
console.error(`stderr: ${data}`);
|
|
});
|
|
await new Promise((resolve) => ffmpeg.on("exit", resolve));
|
|
|
|
let framerate = "";
|
|
|
|
const ffprobe = spawn("ffprobe", [
|
|
"-v",
|
|
"error",
|
|
"-select_streams",
|
|
"v",
|
|
"-of",
|
|
"default=noprint_wrappers=1:nokey=1",
|
|
"-show_entries",
|
|
"stream=r_frame_rate",
|
|
`temp/${sticker.id}.apng`
|
|
]);
|
|
ffprobe.stdout.on("data", (data) => {
|
|
console.log(`stdout: ${data}`);
|
|
framerate += data;
|
|
});
|
|
ffprobe.stderr.on("data", (data) => {
|
|
console.error(`stderr: ${data}`);
|
|
});
|
|
await new Promise((resolve) => ffprobe.on("exit", resolve));
|
|
|
|
framerate = eval(framerate);
|
|
|
|
const gifski = spawn("gifski", [
|
|
"-o",
|
|
`output/${sticker.id}.gif`,
|
|
"--fps",
|
|
framerate,
|
|
...(await findFilesWithPrefix(`${sticker.id}-`, "temp")).map(e => "temp/" + e)
|
|
]);
|
|
gifski.stdout.on("data", (data) => {
|
|
console.log(`stdout: ${data}`);
|
|
});
|
|
gifski.stderr.on("data", (data) => {
|
|
console.error(`stderr: ${data}`);
|
|
});
|
|
await new Promise((resolve) => gifski.on("exit", resolve));
|
|
}
|
|
break;
|
|
|
|
case StickerFormat.LOTTIE:
|
|
{
|
|
const stickerFile = await (
|
|
await fetch(LOTTIE_BASE_URL.replace("{id}", sticker.id))
|
|
).json();
|
|
await renderLottie({
|
|
animationData: stickerFile,
|
|
output: `output/${sticker.id}.gif`,
|
|
width: STICKER_SIZE,
|
|
height: STICKER_SIZE,
|
|
});
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
await fs.rm("temp", { recursive: true });
|