Remove tracked PNG game logos from repository

This commit is contained in:
Pandipipas
2026-03-03 15:44:47 +01:00
parent 1735e38edd
commit 057b5a29c3
10 changed files with 853 additions and 25 deletions
+245
View File
@@ -0,0 +1,245 @@
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { nodecg } from './util/nodecg.js';
const GITHUB_OWNER = 'Pandipipas';
const GITHUB_REPO = 'scoreko-assets';
const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/contents`;
let cachedDefaultBranch: string | null = null;
const gameCatalog = [
{ title: 'Street Fighter 6', slug: 'street-fighter-6', repoFolder: 'street-fighter-6' },
{ title: 'TEKKEN 8', slug: 'tekken-8', repoFolder: 'tekken-8' },
{ title: '2XKO', slug: '2xko', repoFolder: '2xko' },
{ title: 'Guilty Gear -Strive-', slug: 'guilty-gear-strive', repoFolder: 'guilty-gear-strive' },
{ title: 'Mortal Kombat 1', slug: 'mortal-kombat-1', repoFolder: 'mortal-kombat-1' },
{ title: 'THE KING OF FIGHTERS XV', slug: 'the-king-of-fighters-xv', repoFolder: 'the-king-of-fighters-xv' },
] as const;
type GitHubContentEntry = {
type: 'file' | 'dir';
path: string;
size: number;
download_url: string | null;
};
const extensionDir = path.dirname(fileURLToPath(import.meta.url));
const bundleRoot = path.resolve(extensionDir, '..');
const assetsRoot = path.join(bundleRoot, 'game-assets');
const assetsRouter = nodecg.Router();
assetsRouter.get('/*', async (req, res) => {
const wildcardParam = (req.params as Record<string, unknown>)['0']
?? (req.params as Record<string, unknown>)[''];
const requestedPath = Array.isArray(wildcardParam)
? String(wildcardParam[0] ?? '')
: typeof wildcardParam === 'string'
? wildcardParam
: '';
const normalizedPath = path.normalize(requestedPath).replace(/^(\.\.(?:[\\/]|$))+/, '');
const filePath = path.resolve(assetsRoot, normalizedPath);
if (!filePath.startsWith(assetsRoot)) {
res.status(400).send('Invalid asset path.');
return;
}
try {
const fileStats = await stat(filePath);
if (!fileStats.isFile()) {
res.status(404).send('Asset not found.');
return;
}
res.type(path.extname(filePath));
res.send(await readFile(filePath));
} catch {
res.status(404).send('Asset not found.');
}
});
nodecg.mount(`/bundles/${nodecg.bundleName}/game-assets`, assetsRouter);
const githubHeaders = {
Accept: 'application/vnd.github+json',
'User-Agent': 'scoreko-dev-nodecg-bundle',
};
const emitProgress = (title: string, progress: number, status: 'downloading' | 'completed' | 'error') => {
nodecg.sendMessage('scoreko-assets:downloadProgress', {
title,
progress: Math.max(0, Math.min(100, progress)),
status,
});
};
const fetchJson = async <T>(url: string): Promise<T> => {
const response = await fetch(url, { headers: githubHeaders });
if (!response.ok) {
throw new Error(`GitHub API error (${response.status}) while requesting ${url}`);
}
return response.json() as Promise<T>;
};
const getDefaultBranch = async () => {
if (cachedDefaultBranch) {
return cachedDefaultBranch;
}
const repoMetadata = await fetchJson<{ default_branch?: string }>(`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`);
cachedDefaultBranch = repoMetadata.default_branch || 'main';
return cachedDefaultBranch;
};
const listRepoFilesRecursively = async (repoPath: string): Promise<GitHubContentEntry[]> => {
const githubRef = await getDefaultBranch();
const url = `${GITHUB_API_BASE}/${repoPath}?ref=${encodeURIComponent(githubRef)}`;
const entries = await fetchJson<GitHubContentEntry[]>(url);
const files: GitHubContentEntry[] = [];
for (const entry of entries) {
if (entry.type === 'file') {
files.push(entry);
continue;
}
if (entry.type === 'dir') {
const nested = await listRepoFilesRecursively(entry.path);
files.push(...nested);
}
}
return files;
};
const listInstalledGames = async () => {
const entries = await readdir(assetsRoot, { withFileTypes: true }).catch(() => []);
const installedSlugs = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
return gameCatalog.filter((game) => installedSlugs.includes(game.slug)).map((game) => game.title);
};
const downloadGameAssets = async (gameTitle: string) => {
const game = gameCatalog.find((entry) => entry.title === gameTitle);
if (!game) {
throw new Error('Juego no soportado en el catálogo.');
}
const repoFolderPath = `games/${game.repoFolder}`;
emitProgress(game.title, 0, 'downloading');
const files = await listRepoFilesRecursively(repoFolderPath);
if (!files.length) {
throw new Error(`No se encontraron archivos en ${repoFolderPath} dentro de scoreko-assets.`);
}
const totalBytes = files.reduce((acc, file) => acc + (file.size || 0), 0);
let downloadedBytes = 0;
const destinationFolder = path.join(assetsRoot, game.slug);
await rm(destinationFolder, { recursive: true, force: true });
for (const file of files) {
if (!file.download_url) {
continue;
}
const relativePath = file.path.replace(`${repoFolderPath}/`, '');
const targetPath = path.join(destinationFolder, relativePath);
await mkdir(path.dirname(targetPath), { recursive: true });
const response = await fetch(file.download_url, { headers: githubHeaders });
if (!response.ok) {
throw new Error(`No se pudo descargar ${file.path} (status ${response.status}).`);
}
const arrayBuffer = await response.arrayBuffer();
await writeFile(targetPath, Buffer.from(arrayBuffer));
downloadedBytes += file.size || 0;
const progress = totalBytes > 0 ? Math.round((downloadedBytes / totalBytes) * 100) : 100;
emitProgress(game.title, progress, 'downloading');
}
emitProgress(game.title, 100, 'completed');
return {
title: game.title,
slug: game.slug,
};
};
const removeGameAssets = async (gameTitle: string) => {
const game = gameCatalog.find((entry) => entry.title === gameTitle);
if (!game) {
throw new Error('Juego no soportado en el catálogo.');
}
const destinationFolder = path.join(assetsRoot, game.slug);
await rm(destinationFolder, { recursive: true, force: true });
return {
title: game.title,
slug: game.slug,
};
};
nodecg.listenFor('scoreko-assets:listInstalled', async (_payload: unknown, ack) => {
if (typeof ack !== 'function') {
return;
}
try {
ack(null, await listInstalledGames());
} catch (error) {
ack((error as Error).message);
}
});
nodecg.listenFor('scoreko-assets:downloadGame', async (payload: unknown, ack) => {
if (typeof ack !== 'function') {
return;
}
try {
const title = typeof payload === 'object' && payload !== null ? (payload as { title?: unknown }).title : undefined;
if (typeof title !== 'string') {
throw new Error('Título de juego inválido.');
}
const downloaded = await downloadGameAssets(title);
ack(null, {
downloaded,
installedGames: await listInstalledGames(),
});
} catch (error) {
if (typeof payload === 'object' && payload !== null && typeof (payload as { title?: unknown }).title === 'string') {
emitProgress((payload as { title: string }).title, 0, 'error');
}
ack((error as Error).message);
}
});
nodecg.listenFor('scoreko-assets:removeGame', async (payload: unknown, ack) => {
if (typeof ack !== 'function') {
return;
}
try {
const title = typeof payload === 'object' && payload !== null ? (payload as { title?: unknown }).title : undefined;
if (typeof title !== 'string') {
throw new Error('Título de juego inválido.');
}
const removed = await removeGameAssets(title);
ack(null, {
removed,
installedGames: await listInstalledGames(),
});
} catch (error) {
ack((error as Error).message);
}
});