feat: return workshop metadata with mod details

Include the Workshop title and preview image in the items response so the mod header survives a page refresh, not just navigation from search. Resolve both from the same Steam API call.
This commit is contained in:
2026-08-14 10:59:14 +08:00
parent 7163451d1f
commit 30ef76632f
7 changed files with 85 additions and 40 deletions
+38 -5
View File
@@ -1,5 +1,5 @@
import { deserialize } from 'bson';
import type { TTSMod } from '@tts/shared';
import type { ModDetails, TTSMod } from '@tts/shared';
import { ItemNotFoundError, NoFileError, SteamApiError } from './errors.js';
const STEAM_API_URL =
@@ -36,6 +36,30 @@ export async function fetchMod(id: string, apiKey: string): Promise<TTSMod> {
return fetchModFromUrl(fileUrl);
}
/**
* Fetch a save plus its Workshop metadata (title, preview image) in one call.
* The metadata comes from the same Steam API request that resolves the save
* URL, so it costs no extra round-trip.
*
* @param id Workshop item ID (digits only).
* @param apiKey Steam Web API key.
*/
export async function fetchModDetails(
id: string,
apiKey: string,
): Promise<ModDetails> {
const details = await getPublishedFileDetails(id, apiKey);
if (!details.file_url) {
throw new NoFileError(id);
}
const mod = await fetchModFromUrl(details.file_url);
return {
mod,
title: details.title,
previewImageUrl: details.preview_url,
};
}
/**
* Download a TTS save from a direct URL and BSON-deserialize it.
*
@@ -94,6 +118,18 @@ async function downloadSave(fileUrl: string): Promise<ArrayBuffer> {
/** Resolve the `file_url` for a Workshop item via the Steam API. */
async function getFileUrl(id: string, apiKey: string): Promise<string> {
const details = await getPublishedFileDetails(id, apiKey);
if (!details.file_url) {
throw new NoFileError(id);
}
return details.file_url;
}
/** Fetch the Steam published-file details for a Workshop item. */
async function getPublishedFileDetails(
id: string,
apiKey: string,
): Promise<SteamPublishedFileDetails> {
const params = new URLSearchParams();
params.append('key', apiKey);
params.append('itemcount', '1');
@@ -112,10 +148,7 @@ async function getFileUrl(id: string, apiKey: string): Promise<string> {
if (!details) {
throw new ItemNotFoundError(id);
}
if (!details.file_url) {
throw new NoFileError(id);
}
return details.file_url;
return details;
}
export * from './errors.js';