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

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 });