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