feat: Added way to update the local games from romm when IDs change based on IGDB or Retro Achievement ID

Fixes #2
This commit is contained in:
Simeon Radivoev 2026-04-10 02:00:11 +03:00
parent 7948bd24fa
commit 4806f3487a
Signed by: simeonradivoev
GPG key ID: 7611A451D2A5D37A
9 changed files with 143 additions and 52 deletions

View file

@ -8,7 +8,7 @@ import { GameListFilterSchema, SERVER_URL } from "@shared/constants";
import { InstallJob } from "../jobs/install-job";
import path from "node:path";
import { convertLocalToFrontend, convertStoreToFrontend, getLocalGameMatch, getSourceGameDetailed } from "./services/utils";
import buildStatusResponse, { getValidLaunchCommandsForGame } from "./services/statusService";
import buildStatusResponse, { fixSource, getValidLaunchCommandsForGame, validateGameSource } from "./services/statusService";
import { errorToResponse } from "elysia/adapter/bun/handler";
import { getEmulatorsForSystem, getRomFilePaths, launchCommand } from "./services/launchGameService";
import { getErrorMessage, SeededRandom } from "@/bun/utils";
@ -412,6 +412,15 @@ export default new Elysia()
params: z.object({ id: z.string(), source: z.string() }),
response: z.any()
})
.get('/game/:source/:id/validate', async ({ params: { id, source } }) =>
{
const valid = await validateGameSource(source, id);
return { valid: valid.valid, reason: valid.reason };
})
.post('/game/:source/:id/fix_source', async ({ params: { id, source } }) =>
{
return fixSource(source, id);
})
.post('/game/:source/:id/play', async ({ params: { id, source }, body, set }) =>
{
const validCommands = await getValidLaunchCommandsForGame(source, id);

View file

@ -44,26 +44,53 @@ export async function getLocalGame (source: string, id: string)
return localGame;
}
export async function validateGameSource (source: string, id: string): Promise<{ valid: boolean, reason?: string; }>
export async function fixSource (source: string, id: string)
{
const valid = await validateGameSource(source, id);
if (!valid.valid)
{
if (!valid.localGame) throw new Error("No Local Game");
if (!valid.localGame.source) throw new Error("No Valid Source");
const foundGame = await plugins.hooks.games.searchGame.promise({
igdb_id: valid.localGame.igdb_id ?? undefined,
ra_id: valid.localGame.ra_id ?? undefined,
source: valid.localGame.source
});
if (foundGame)
{
await db.update(appSchema.games).set({ source: foundGame.id.source, source_id: foundGame.id.id }).where(eq(appSchema.games.id, valid.localGame.id));
return true;
} else
{
throw new Error("Could not find Source Game");
}
} else
{
throw new Error("Game Source Already Valid");
}
}
export async function validateGameSource (source: string, id: string): Promise<{
valid: boolean,
localGame?: { id: number; igdb_id: number | null; ra_id: number | null; source: string | null; },
reason?: string;
}>
{
const localGame = await getLocalGame(source, id);
if (!localGame) throw new Error("Could not find local game");
if (!localGame) return { valid: true };
if (localGame.source && localGame.source_id)
{
const sourceGame = await plugins.hooks.games.fetchGame.promise({ source: localGame.source, id: localGame.source_id });
if (!sourceGame) return { valid: false, reason: "Source Missing" };
if (sourceGame.imdb_id !== (localGame.igdb_id ?? undefined))
if (!sourceGame) return { valid: false, reason: "Source Missing", localGame };
if (sourceGame.imdb_id !== (localGame.igdb_id ?? undefined) && sourceGame.ra_id !== (localGame.ra_id ?? undefined))
{
return { valid: false, reason: "IGDB Miss Match" };
}
if (sourceGame.ra_id !== (localGame.ra_id ?? undefined))
{
return { valid: false, reason: "RA Miss Match" };
return { valid: false, reason: "Metadata Missmatch", localGame };
}
}
return { valid: true };
return { valid: true, localGame };
}
export async function updateLocalLastPlayed (id: number)
@ -174,7 +201,7 @@ export default function buildStatusResponse ()
},
async open (ws)
{
sendLatests();
sendLatests().catch(e => ws.send({ status: 'error', error: JSON.stringify(e) }));
const installJobId = InstallJob.query({ source: ws.data.params.source, id: ws.data.params.id });
async function sendLatests ()

View file

@ -43,6 +43,11 @@ export class GameHooks
localGame?: FrontEndGameTypeDetailed;
id: string;
}], FrontEndGameTypeDetailed | undefined>(['ctx']);
searchGame = new AsyncSeriesBailHook<[ctx: {
source: string;
igdb_id?: number;
ra_id?: number;
}], FrontEndGameTypeDetailed | undefined>(['ctx']);
/** Get download file URLs
* @param ctx.checksum Check if file already exists using checksums
*/

View file

@ -2,7 +2,7 @@
import { PluginContextType, PluginType } from "@/bun/types/typesc.schema";
import desc from './package.json';
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomContentApiRomsIdContentFileNameGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
import { DetailedRomSchema, getCollectionApiCollectionsIdGet, getCollectionsApiCollectionsGet, getCurrentUserApiUsersMeGet, getPlatformApiPlatformsIdGet, getPlatformFirmwareApiFirmwareGet, getPlatformsApiPlatformsGet, getRomApiRomsIdGet, getRomByMetadataProviderApiRomsByMetadataProviderGet, getRomContentApiRomsIdContentFileNameGet, getRomsApiRomsGet, getSavesSummaryApiSavesSummaryGet, SimpleRomSchema, updateRomUserApiRomsIdPropsPut } from "@/clients/romm";
import { config, events } from "@/bun/api/app";
import path from 'node:path';
import fs from 'node:fs/promises';
@ -557,5 +557,14 @@ export default class RommIntegration implements PluginType
const platforms = await this.getAllRommPlatforms();
return platforms.find(p => p.id === Number(id));
});
ctx.hooks.games.searchGame.tapPromise(desc.name, async ({ source, igdb_id, ra_id }) =>
{
if (source !== 'romm') return;
const roms = await getRomByMetadataProviderApiRomsByMetadataProviderGet({ query: { igdb_id, ra_id } });
if (roms.error) throw roms.error;
if (!roms.data) return;
return this.convertRomToFrontendDetailed(roms.data);
});
}
}