Files
buzz/desktop/scripts/sync-goose-avatars.mjs
2026-06-22 15:43:27 +01:00

176 lines
4.4 KiB
JavaScript

#!/usr/bin/env node
import { spawn } from "node:child_process";
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";
const ARTIFACTORY_BASE =
"https://global.block-artifacts.com/artifactory/goose-internal/avatars";
const LATEST_URL = `${ARTIFACTORY_BASE}/latest.json`;
const scriptDir = dirname(fileURLToPath(import.meta.url));
const desktopRoot = dirname(scriptDir);
const outputRoot = join(desktopRoot, "src/shared/assets/goose-avatars");
const catalogPath = join(outputRoot, "catalog.json");
const FORMATS = ["webm", "hevc"];
function variantOutputPath(asset, format) {
const extension = format === "hevc" ? "mp4" : "webm";
return join(
outputRoot,
format,
asset.collectionId,
`${asset.id}.${extension}`,
);
}
function posterOutputPath(asset) {
return join(outputRoot, "posters", asset.collectionId, `${asset.id}.png`);
}
async function fetchJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status}`);
}
return response.json();
}
async function fileExistsWithSize(path, byteSize) {
try {
const info = await stat(path);
return info.size === byteSize;
} catch {
return false;
}
}
async function downloadFile(url, path, byteSize) {
if (await fileExistsWithSize(path, byteSize)) {
return "skipped";
}
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to download ${url}: ${response.status}`);
}
const bytes = new Uint8Array(await response.arrayBuffer());
if (bytes.byteLength !== byteSize) {
throw new Error(
`Downloaded ${url} with ${bytes.byteLength} bytes, expected ${byteSize}.`,
);
}
await mkdir(dirname(path), { recursive: true });
await writeFile(path, bytes);
return "downloaded";
}
async function runFfmpeg(args) {
await new Promise((resolve, reject) => {
const child = spawn("ffmpeg", args, {
stdio: ["ignore", "ignore", "pipe"],
});
let stderr = "";
child.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
child.on("error", reject);
child.on("close", (code) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`ffmpeg exited with ${code}: ${stderr.trim() || "no stderr"}`,
),
);
}
});
});
}
async function ensurePoster(asset) {
const posterPath = posterOutputPath(asset);
try {
await stat(posterPath);
return "skipped";
} catch {
// Generate it below.
}
await mkdir(dirname(posterPath), { recursive: true });
await runFfmpeg([
"-hide_banner",
"-loglevel",
"error",
"-y",
"-i",
variantOutputPath(asset, "webm"),
"-frames:v",
"1",
posterPath,
]);
return "generated";
}
async function main() {
const latest = await fetchJson(LATEST_URL);
const manifest = await fetchJson(
`${ARTIFACTORY_BASE}/${latest.manifestPath}`,
);
const versionRoot = `${ARTIFACTORY_BASE}/${manifest.catalogVersion}`;
await mkdir(outputRoot, { recursive: true });
await writeFile(catalogPath, `${JSON.stringify(manifest, null, 2)}\n`);
const totals = {
downloaded: 0,
skipped: 0,
postersGenerated: 0,
postersSkipped: 0,
};
for (const [index, asset] of manifest.assets.entries()) {
for (const format of FORMATS) {
const variant = asset.variants[format];
const sourceUrl = `${versionRoot}/${variant.path}`;
const result = await downloadFile(
sourceUrl,
variantOutputPath(asset, format),
variant.byteSize,
);
totals[result] += 1;
}
const posterResult = await ensurePoster(asset);
if (posterResult === "generated") {
totals.postersGenerated += 1;
} else {
totals.postersSkipped += 1;
}
const completed = index + 1;
if (completed % 5 === 0 || completed === manifest.assets.length) {
console.log(
`Synced ${completed}/${manifest.assets.length} Goose avatars...`,
);
}
}
const catalogBytes = await readFile(catalogPath, "utf8");
JSON.parse(catalogBytes);
console.log(
`Done. Downloaded ${totals.downloaded}, skipped ${totals.skipped}, generated ${totals.postersGenerated} posters, skipped ${totals.postersSkipped} posters.`,
);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});