diff --git a/pages/store/[id]/index.vue b/pages/store/[id]/index.vue
index cc6943f..48f2ec7 100644
--- a/pages/store/[id]/index.vue
+++ b/pages/store/[id]/index.vue
@@ -103,9 +103,7 @@
'w-4 h-4',
]"
/>
- ({{ game.mReviewCount }} reviews)
+ ({{ 0 }} reviews)
@@ -220,7 +218,8 @@ const platforms = game.versions
.flat()
.filter((e, i, u) => u.indexOf(e) === i);
-const rating = Math.round(game.mReviewRating * 5);
+// const rating = Math.round(game.mReviewRating * 5);
+const rating = Math.round(0 * 5);
const ratingArray = Array(5)
.fill(null)
.map((_, i) => i + 1 <= rating);
diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma
index ee3b267..4631b06 100644
--- a/prisma/models/content.prisma
+++ b/prisma/models/content.prisma
@@ -3,6 +3,8 @@ enum MetadataSource {
GiantBomb
PCGamingWiki
IGDB
+ Metacritic
+ OpenCritic
}
model Game {
@@ -19,8 +21,7 @@ model Game {
mDescription String // Supports markdown
mReleased DateTime // When the game was released
- mReviewCount Int
- mReviewRating Float // 0 to 1
+ ratings GameRating[]
mIconObjectId String // linked to objects in s3
mBannerObjectId String // linked to objects in s3
@@ -42,6 +43,24 @@ model Game {
@@unique([metadataSource, metadataId], name: "metadataKey")
}
+model GameRating {
+ id String @id @default(uuid())
+
+ metadataSource MetadataSource
+ metadataId String
+ created DateTime @default(now())
+
+ mReviewCount Int
+ mReviewRating Float // 0 to 1
+
+ mReviewHref String?
+
+ Game Game? @relation(fields: [gameId], references: [id], onDelete: Cascade)
+ gameId String?
+
+ @@unique([metadataSource, metadataId], name: "metadataKey")
+}
+
// A particular set of files that relate to the version
model GameVersion {
gameId String
diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts
index 2441aa4..312fab5 100644
--- a/server/internal/metadata/giantbomb.ts
+++ b/server/internal/metadata/giantbomb.ts
@@ -207,8 +207,7 @@ export class GiantBombProvider implements MetadataProvider {
tags: [],
- reviewCount: 0,
- reviewRating: 0,
+ reviews: [],
publishers,
developers,
diff --git a/server/internal/metadata/igdb.ts b/server/internal/metadata/igdb.ts
index 075ad38..57656d7 100644
--- a/server/internal/metadata/igdb.ts
+++ b/server/internal/metadata/igdb.ts
@@ -390,8 +390,15 @@ export class IGDBProvider implements MetadataProvider {
? DateTime.now().toJSDate()
: DateTime.fromSeconds(firstReleaseDate).toJSDate(),
- reviewCount: response[i]?.total_rating_count ?? 0,
- reviewRating: (response[i]?.total_rating ?? 0) / 100,
+ reviews: [
+ {
+ metadataId: "" + response[i].id,
+ metadataSource: MetadataSource.IGDB,
+ mReviewCount: response[i]?.total_rating_count ?? 0,
+ mReviewRating: (response[i]?.total_rating ?? 0) / 100,
+ mReviewHref: response[i].url,
+ },
+ ],
publishers: [],
developers: [],
diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts
index 8721459..b62137c 100644
--- a/server/internal/metadata/index.ts
+++ b/server/internal/metadata/index.ts
@@ -1,4 +1,4 @@
-import { MetadataSource } from "~/prisma/client";
+import { MetadataSource, type GameRating } from "~/prisma/client";
import prisma from "../db/database";
import type {
_FetchGameMetadataParams,
@@ -7,6 +7,7 @@ import type {
GameMetadataSearchResult,
InternalGameMetadataResult,
CompanyMetadata,
+ GameMetadataRating,
} from "./types";
import { ObjectTransactionalHandler } from "../objects/transactional";
import { PriorityListIndexed } from "../utils/prioritylist";
@@ -135,6 +136,34 @@ export class MetadataHandler {
return results;
}
+ private parseRatings(ratings: GameMetadataRating[]) {
+ const results: {
+ where: {
+ metadataKey: {
+ metadataId: string;
+ metadataSource: MetadataSource;
+ };
+ };
+ create: Omit;
+ }[] = [];
+
+ ratings.forEach((r) => {
+ results.push({
+ where: {
+ metadataKey: {
+ metadataId: r.metadataId,
+ metadataSource: r.metadataSource,
+ },
+ },
+ create: {
+ ...r,
+ },
+ });
+ });
+
+ return results;
+ }
+
async createGame(
result: InternalGameMetadataResult,
libraryBasePath: string,
@@ -181,9 +210,6 @@ export class MetadataHandler {
mName: metadata.name,
mShortDescription: metadata.shortDescription,
mDescription: metadata.description,
-
- mReviewCount: metadata.reviewCount,
- mReviewRating: metadata.reviewRating,
mReleased: metadata.released,
mIconObjectId: metadata.icon,
@@ -198,6 +224,9 @@ export class MetadataHandler {
connect: metadata.developers,
},
+ ratings: {
+ connectOrCreate: this.parseRatings(metadata.reviews),
+ },
tags: {
connectOrCreate: this.parseTags(metadata.tags),
},
diff --git a/server/internal/metadata/manual.ts b/server/internal/metadata/manual.ts
index d7b00dc..551faf3 100644
--- a/server/internal/metadata/manual.ts
+++ b/server/internal/metadata/manual.ts
@@ -34,8 +34,7 @@ export class ManualMetadataProvider implements MetadataProvider {
publishers: [],
developers: [],
tags: [],
- reviewCount: 0,
- reviewRating: 0,
+ reviews: [],
icon: iconId,
coverId: iconId,
diff --git a/server/internal/metadata/pcgamingwiki.ts b/server/internal/metadata/pcgamingwiki.ts
index 489e41a..251b0fe 100644
--- a/server/internal/metadata/pcgamingwiki.ts
+++ b/server/internal/metadata/pcgamingwiki.ts
@@ -7,12 +7,14 @@ import type {
GameMetadata,
_FetchCompanyMetadataParams,
CompanyMetadata,
+ GameMetadataRating,
} from "./types";
import type { AxiosRequestConfig } from "axios";
import axios from "axios";
import * as jdenticon from "jdenticon";
import { DateTime } from "luxon";
import * as cheerio from "cheerio";
+import { type } from "arktype";
interface PCGamingWikiParseRawPage {
parse: {
@@ -31,11 +33,6 @@ interface PCGamingWikiParseRawPage {
};
}
-interface PCGamingWikiParsedPage {
- shortIntro: string;
- introduction: string;
-}
-
interface PCGamingWikiPage {
PageID: string;
PageName: string;
@@ -89,6 +86,10 @@ type StringArrayKeys = {
[K in keyof T]: T[K] extends string | string[] | null ? K : never;
}[keyof T];
+const ratingProviderReview = type({
+ rating: "string.integer.parse",
+});
+
// 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 {
@@ -115,7 +116,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
if (response.status !== 200)
throw new Error(
- `Error in pcgamingwiki \nStatus Code: ${response.status}`,
+ `Error in pcgamingwiki \nStatus Code: ${response.status}\n${response.data}`,
);
return response;
@@ -134,9 +135,13 @@ export class PCGamingWikiProvider implements MetadataProvider {
return response;
}
- private async getPageContent(
- pageID: string,
- ): Promise {
+ /**
+ * Gets the raw wiki page for parsing,
+ * requested values are to be considered unstable as compared to cargo queries
+ * @param pageID
+ * @returns
+ */
+ private async getPageContent(pageID: string) {
const searchParams = new URLSearchParams({
action: "parse",
format: "json",
@@ -149,9 +154,116 @@ export class PCGamingWikiProvider implements MetadataProvider {
// remove citations from intro
introductionEle.find("sup").remove();
+ const infoBoxEle = $(".template-infobox").first();
+ const receptionEle = infoBoxEle
+ .find(".template-infobox-header")
+ .filter((_, el) => $(el).text().trim() === "Reception");
+
+ const receptionResults: (GameMetadataRating | undefined)[] = [];
+ if (receptionEle.length > 0) {
+ // we have a match!
+
+ const ratingElements = infoBoxEle.find(".template-infobox-type");
+
+ // TODO: cleanup this ratnest
+ const parseIdFromHref = (href: string): string | undefined => {
+ const url = new URL(href);
+ const opencriticRegex = /^\/game\/(\d+)\/.+$/;
+ switch (url.hostname.toLocaleLowerCase()) {
+ case "www.metacritic.com": {
+ // https://www.metacritic.com/game/elden-ring/critic-reviews/?platform=pc
+ return url.pathname
+ .replace("/game/", "")
+ .replace("/critic-reviews", "")
+ .replace(/\/$/, "");
+ }
+ case "opencritic.com": {
+ // https://opencritic.com/game/12090/elden-ring
+ let id = "unknown";
+ let matches;
+ if ((matches = opencriticRegex.exec(url.pathname)) !== null) {
+ matches.forEach((match, _groupIndex) => {
+ // console.log(`Found match, group ${_groupIndex}: ${match}`);
+ id = match;
+ });
+ }
+
+ if (id === "unknown") {
+ return undefined;
+ }
+ return id;
+ }
+ case "www.igdb.com": {
+ // https://www.igdb.com/games/elden-ring
+ return url.pathname.replace("/games/", "").replace(/\/$/, "");
+ }
+ default: {
+ console.warn("Pcgamingwiki, unknown host", url.hostname);
+ return undefined;
+ }
+ }
+ };
+ const getRating = (
+ source: MetadataSource,
+ ): GameMetadataRating | undefined => {
+ const providerEle = ratingElements.filter(
+ (_, el) =>
+ $(el).text().trim().toLocaleLowerCase() ===
+ source.toLocaleLowerCase(),
+ );
+ if (providerEle.length > 0) {
+ // get info associated with provider
+ const reviewEle = providerEle
+ .first()
+ .parent()
+ .find(".template-infobox-info")
+ .find("a")
+ .first();
+
+ const href = reviewEle.attr("href");
+ if (!href) {
+ console.log(
+ `pcgamingwiki: failed to properly get review href for ${source}`,
+ );
+ return undefined;
+ }
+ const ratingObj = ratingProviderReview({
+ rating: reviewEle.text().trim(),
+ });
+ if (ratingObj instanceof type.errors) {
+ console.log(
+ "pcgamingwiki: failed to properly get review rating",
+ ratingObj.summary,
+ );
+ return undefined;
+ }
+
+ const id = parseIdFromHref(href);
+ if (!id) return undefined;
+
+ return {
+ mReviewHref: href,
+ metadataId: id,
+ metadataSource: source,
+ mReviewCount: 0,
+ // make float within 0 to 1
+ mReviewRating: ratingObj.rating / 100,
+ };
+ }
+
+ return undefined;
+ };
+ receptionResults.push(getRating(MetadataSource.Metacritic));
+ receptionResults.push(getRating(MetadataSource.IGDB));
+ receptionResults.push(getRating(MetadataSource.OpenCritic));
+ }
+
+ console.log(res.data.parse.title, receptionResults);
+
return {
- shortIntro: introductionEle.find("p").first().text(),
- introduction: introductionEle.text(),
+ shortIntro: introductionEle.find("p").first().text().trim(),
+ introduction: introductionEle.text().trim(),
+ reception: receptionResults,
};
}
@@ -318,9 +430,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
tags: this.compileTags(game),
- reviewCount: 0,
- reviewRating: 0,
-
+ reviews: pageContent.reception.filter((v) => typeof v !== "undefined"),
publishers,
developers,
diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts
index b627041..b5286a7 100644
--- a/server/internal/metadata/types.d.ts
+++ b/server/internal/metadata/types.d.ts
@@ -1,4 +1,4 @@
-import type { Company } from "~/prisma/client";
+import type { Company, GameRating } from "~/prisma/client";
import type { TransactionDataType } from "../objects/transactional";
import type { ObjectReference } from "../objects/objectHandler";
@@ -18,6 +18,15 @@ export interface GameMetadataSource {
export type InternalGameMetadataResult = GameMetadataSearchResult &
GameMetadataSource;
+export type GameMetadataRating = Pick<
+ GameRating,
+ | "metadataSource"
+ | "metadataId"
+ | "mReviewCount"
+ | "mReviewHref"
+ | "mReviewRating"
+>;
+
export interface GameMetadata {
id: string;
name: string;
@@ -32,8 +41,7 @@ export interface GameMetadata {
tags: string[];
- reviewCount: number;
- reviewRating: number;
+ reviews: GameMetadataRating[];
// Created with another utility function
icon: ObjectReference;