feat: games now have tag support

This commit is contained in:
Huskydog9988
2025-05-11 12:52:00 -04:00
parent a8ee27eea9
commit dad2161754
8 changed files with 125 additions and 23 deletions

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "_GameToTag" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL,
CONSTRAINT "_GameToTag_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "_GameToTag_B_index" ON "_GameToTag"("B");
-- AddForeignKey
ALTER TABLE "_GameToTag" ADD CONSTRAINT "_GameToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_GameToTag" ADD CONSTRAINT "_GameToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -3,18 +3,18 @@ import prisma from "~/server/internal/db/database";
import objectHandler from "~/server/internal/objects";
import { type } from "arktype";
const ModifyGameImage = type({
const DeleteGameImage = type({
gameId: "string",
imageId: "string",
});
export default defineEventHandler<{
body: typeof ModifyGameImage.infer;
body: typeof DeleteGameImage.infer;
}>(async (h3) => {
const allowed = await aclManager.allowSystemACL(h3, ["game:image:delete"]);
if (!allowed) throw createError({ statusCode: 403 });
const body = ModifyGameImage(await readBody(h3));
const body = DeleteGameImage(await readBody(h3));
if (body instanceof type.errors) {
// hover out.summary to see validation errors
console.error(body.summary);

View File

@ -205,6 +205,8 @@ export class GiantBombProvider implements MetadataProvider {
description: longDescription,
released: releaseDate,
tags: [],
reviewCount: 0,
reviewRating: 0,

View File

@ -358,6 +358,9 @@ export class IGDBProvider implements MetadataProvider {
publishers: [],
developers: [],
// TODO: support tags
tags: [],
icon,
bannerId: banner,
coverId: icon,

View File

@ -110,6 +110,30 @@ export class MetadataHandler {
);
}
private parseTags(tags: string[]) {
const results: {
where: {
name: string;
};
create: {
name: string;
};
}[] = [];
tags.forEach((t) =>
results.push({
where: {
name: t,
},
create: {
name: t,
},
}),
);
return results;
}
async createGame(
result: InternalGameMetadataResult,
libraryBasePath: string,
@ -173,6 +197,10 @@ export class MetadataHandler {
connect: metadata.developers,
},
tags: {
connectOrCreate: this.parseTags(metadata.tags),
},
libraryBasePath,
},
});

View File

@ -33,6 +33,7 @@ export class ManualMetadataProvider implements MetadataProvider {
released: new Date(),
publishers: [],
developers: [],
tags: [],
reviewCount: 0,
reviewRating: 0,

View File

@ -48,12 +48,19 @@ interface PCGamingWikiSearchStub extends PCGamingWikiPage {
}
interface PCGamingWikiGame extends PCGamingWikiSearchStub {
Developers: string | null;
Genres: string | null;
Publishers: string | null;
Themes: string | null;
Developers: string | string[] | null;
Publishers: string | string[] | null;
// TODO: save this somewhere, maybe a tag?
Series: string | null;
Modes: string | null;
// tags
Perspectives: string | string[] | null; // ie: First-person
Genres: string | string[] | null; // ie: Action, FPS
"Art styles": string | string[] | null; // ie: Stylized
Themes: string | string[] | null; // ie: Post-apocalyptic, Sci-fi, Space
Modes: string | string[] | null; // ie: Singleplayer, Multiplayer
Pacing: string | string[] | null; // ie: Real-time
}
interface PCGamingWikiCompany extends PCGamingWikiPage {
@ -78,6 +85,10 @@ interface PCGamingWikiCargoResult<T> {
};
}
type StringArrayKeys<T> = {
[K in keyof T]: T[K] extends string | string[] | null ? K : never;
}[keyof T];
// Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API
// Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery
export class PCGamingWikiProvider implements MetadataProvider {
@ -135,6 +146,8 @@ export class PCGamingWikiProvider implements MetadataProvider {
const $ = cheerio.load(res.data.parse.text["*"]);
// get intro based on 'introduction' class
const introductionEle = $(".introduction").first();
// remove citations from intro
introductionEle.find("sup").remove();
return {
shortIntro: introductionEle.find("p").first().text(),
@ -175,20 +188,33 @@ export class PCGamingWikiProvider implements MetadataProvider {
}
/**
* Parses the specific format that the wiki returns when specifying a company
* @param companyStr
* Parses the specific format that the wiki returns when specifying an array
* @param input string or array
* @returns
*/
private parseCompanyStr(companyStr: string): string[] {
const results: string[] = [];
// provides the string as a list of companies
// ie: "Company:Digerati Distribution,Company:Greylock Studio"
const items = companyStr.split(",");
private parseWikiStringArray(input: string | string[]): string[] {
const cleanStr = (str: string): string => {
// remove any dumb prefixes we don't care about
return str.replace("Company:", "").trim();
};
items.forEach((item) => {
// remove the `Company:` and trim and whitespace
results.push(item.replace("Company:", "").trim());
});
// input can provides the string as a list
// ie: "Company:Digerati Distribution,Company:Greylock Studio"
// or as an array, sometimes the array has empty values
const results: string[] = [];
if (Array.isArray(input)) {
input.forEach((c) => {
const clean = cleanStr(c);
if (clean !== "") results.push(clean);
});
} else {
const items = input.split(",");
items.forEach((item) => {
const clean = cleanStr(item);
if (clean !== "") results.push(clean);
});
}
return results;
}
@ -209,6 +235,28 @@ export class PCGamingWikiProvider implements MetadataProvider {
return websiteStr.replaceAll(/\[|]/g, "").split(" ")[0] ?? "";
}
private compileTags(game: PCGamingWikiGame): string[] {
const results: string[] = [];
const properties: StringArrayKeys<PCGamingWikiGame>[] = [
"Art styles",
"Genres",
"Modes",
"Pacing",
"Perspectives",
"Themes",
];
// loop through all above keys, get the tags they contain
properties.forEach((p) => {
if (game[p] === null) return;
results.push(...this.parseWikiStringArray(game[p]));
});
return results;
}
async fetchGame({
id,
name,
@ -220,7 +268,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
action: "cargoquery",
tables: "Infobox_game",
fields:
"Infobox_game._pageID=PageID,Infobox_game._pageName=PageName,Infobox_game.Cover_URL,Infobox_game.Developers,Infobox_game.Released,Infobox_game.Genres,Infobox_game.Publishers,Infobox_game.Themes,Infobox_game.Series,Infobox_game.Modes",
"Infobox_game._pageID=PageID,Infobox_game._pageName=PageName,Infobox_game.Cover_URL,Infobox_game.Developers,Infobox_game.Released,Infobox_game.Genres,Infobox_game.Publishers,Infobox_game.Themes,Infobox_game.Series,Infobox_game.Modes,Infobox_game.Perspectives,Infobox_game.Art_styles,Infobox_game.Pacing",
where: `Infobox_game._pageID="${id}"`,
format: "json",
});
@ -236,7 +284,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
const publishers: Company[] = [];
if (game.Publishers !== null) {
const pubListClean = this.parseCompanyStr(game.Publishers);
const pubListClean = this.parseWikiStringArray(game.Publishers);
for (const pub of pubListClean) {
const res = await publisher(pub);
if (res === undefined) continue;
@ -246,7 +294,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
const developers: Company[] = [];
if (game.Developers !== null) {
const devListClean = this.parseCompanyStr(game.Developers);
const devListClean = this.parseWikiStringArray(game.Developers);
for (const dev of devListClean) {
const res = await developer(dev);
if (res === undefined) continue;
@ -268,6 +316,8 @@ export class PCGamingWikiProvider implements MetadataProvider {
? DateTime.fromISO(game.Released.split(";")[0]).toJSDate()
: new Date(),
tags: this.compileTags(game),
reviewCount: 0,
reviewRating: 0,
@ -305,7 +355,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
const company = res.data.cargoquery[i].title;
const fixedCompanyName =
this.parseCompanyStr(company.PageName)[0] ?? company.PageName;
this.parseWikiStringArray(company.PageName)[0] ?? company.PageName;
const metadata: CompanyMetadata = {
id: company.PageID,

View File

@ -30,6 +30,8 @@ export interface GameMetadata {
publishers: Company[];
developers: Company[];
tags: string[];
reviewCount: number;
reviewRating: number;