Make downloaded game assets persistent and offline-friendly

This commit is contained in:
Pandipipas
2026-03-04 16:10:08 +01:00
parent 774bd373d3
commit c1e9133970
2 changed files with 63 additions and 5 deletions
@@ -70,8 +70,13 @@ export const useGameAssetsStore = defineStore('game-assets', () => {
}; };
const refreshInstalledGames = async () => { const refreshInstalledGames = async () => {
const availableResponse = await sendNodecgMessage<RemoteGame[]>('scoreko-assets:listRemoteGames'); try {
availableGames.value = Array.isArray(availableResponse) ? availableResponse : []; const availableResponse = await sendNodecgMessage<RemoteGame[]>('scoreko-assets:listRemoteGames');
availableGames.value = Array.isArray(availableResponse) ? availableResponse : [];
} catch {
availableGames.value = [];
}
const response = await sendNodecgMessage<string[]>('scoreko-assets:listInstalled'); const response = await sendNodecgMessage<string[]>('scoreko-assets:listInstalled');
installedGames.value = Array.isArray(response) ? response : []; installedGames.value = Array.isArray(response) ? response : [];
const configResponse = await sendNodecgMessage<{ assetsBaseUrl?: string }>('scoreko-assets:getAssetsBaseUrl'); const configResponse = await sendNodecgMessage<{ assetsBaseUrl?: string }>('scoreko-assets:getAssetsBaseUrl');
+56 -3
View File
@@ -1,4 +1,4 @@
import { mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises'; import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { nodecg } from './util/nodecg.js'; import { nodecg } from './util/nodecg.js';
@@ -35,7 +35,36 @@ type HttpGameTitlesFile = Record<string, unknown> | HttpGameTitleEntry[];
const extensionDir = path.dirname(fileURLToPath(import.meta.url)); const extensionDir = path.dirname(fileURLToPath(import.meta.url));
const bundleRoot = path.resolve(extensionDir, '..'); const bundleRoot = path.resolve(extensionDir, '..');
const assetsRoot = path.join(bundleRoot, 'game-assets'); const legacyAssetsRoot = path.join(bundleRoot, 'game-assets');
const nodecgRoot = path.resolve(bundleRoot, '..', '..');
const assetsRoot = path.join(nodecgRoot, 'db', `${nodecg.bundleName}-game-assets`);
let assetsStorageReady = false;
const ensureAssetsStorageReady = async () => {
if (assetsStorageReady) {
return;
}
await mkdir(path.dirname(assetsRoot), { recursive: true });
const [currentStats, legacyStats] = await Promise.all([
stat(assetsRoot).catch(() => null),
stat(legacyAssetsRoot).catch(() => null),
]);
if (!currentStats && legacyStats?.isDirectory()) {
await rename(legacyAssetsRoot, assetsRoot).catch(async () => {
await mkdir(assetsRoot, { recursive: true });
});
} else {
await mkdir(assetsRoot, { recursive: true });
}
assetsStorageReady = true;
};
void ensureAssetsStorageReady();
const assetsRouter = nodecg.Router(); const assetsRouter = nodecg.Router();
@@ -240,10 +269,21 @@ const listHttpFiles = async (game: RemoteGame): Promise<AssetFileEntry[]> => {
}; };
const listInstalledGames = async () => { const listInstalledGames = async () => {
await ensureAssetsStorageReady();
const entries = await readdir(assetsRoot, { withFileTypes: true }).catch(() => []); const entries = await readdir(assetsRoot, { withFileTypes: true }).catch(() => []);
return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right)); return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
}; };
const listInstalledGamesAsRemote = async (): Promise<RemoteGame[]> => {
const installedGames = await listInstalledGames();
return installedGames.map((slug) => ({
slug,
repoFolder: slug,
title: titleFromSlug(slug),
logoFile: `${slug}.png`,
}));
};
const parseCharacterNames = (content: string, gameTitle: string) => { const parseCharacterNames = (content: string, gameTitle: string) => {
const parsed = JSON.parse(content) as unknown; const parsed = JSON.parse(content) as unknown;
const names = Array.isArray(parsed) const names = Array.isArray(parsed)
@@ -277,6 +317,7 @@ const listInstalledCharacterNamesByGame = async () => {
}; };
const downloadGameAssets = async (gameSlug: string) => { const downloadGameAssets = async (gameSlug: string) => {
await ensureAssetsStorageReady();
const customTitles = await fetchCustomGameTitles(); const customTitles = await fetchCustomGameTitles();
const game: RemoteGame = { const game: RemoteGame = {
slug: gameSlug, slug: gameSlug,
@@ -330,6 +371,7 @@ const downloadGameAssets = async (gameSlug: string) => {
}; };
const removeGameAssets = async (gameSlug: string) => { const removeGameAssets = async (gameSlug: string) => {
await ensureAssetsStorageReady();
const customTitles = await fetchCustomGameTitles(); const customTitles = await fetchCustomGameTitles();
const destinationFolder = path.join(assetsRoot, gameSlug); const destinationFolder = path.join(assetsRoot, gameSlug);
await rm(destinationFolder, { recursive: true, force: true }); await rm(destinationFolder, { recursive: true, force: true });
@@ -346,8 +388,19 @@ nodecg.listenFor('scoreko-assets:listRemoteGames', async (_payload: unknown, ack
} }
try { try {
ack(null, await listRemoteGames()); const remoteGames = await listRemoteGames();
ack(null, remoteGames);
} catch (error) { } catch (error) {
try {
const installedGames = await listInstalledGamesAsRemote();
if (installedGames.length > 0) {
ack(null, installedGames);
return;
}
} catch {
// noop
}
ack((error as Error).message); ack((error as Error).message);
} }
}); });