mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
feat: game metadata rating support
This commit is contained in:
@ -207,8 +207,7 @@ export class GiantBombProvider implements MetadataProvider {
|
||||
|
||||
tags: [],
|
||||
|
||||
reviewCount: 0,
|
||||
reviewRating: 0,
|
||||
reviews: [],
|
||||
|
||||
publishers,
|
||||
developers,
|
||||
|
||||
@ -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: [],
|
||||
|
||||
@ -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<GameRating, "gameId" | "created" | "id">;
|
||||
}[] = [];
|
||||
|
||||
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),
|
||||
},
|
||||
|
||||
@ -34,8 +34,7 @@ export class ManualMetadataProvider implements MetadataProvider {
|
||||
publishers: [],
|
||||
developers: [],
|
||||
tags: [],
|
||||
reviewCount: 0,
|
||||
reviewRating: 0,
|
||||
reviews: [],
|
||||
|
||||
icon: iconId,
|
||||
coverId: iconId,
|
||||
|
||||
@ -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<T> = {
|
||||
[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<PCGamingWikiParsedPage> {
|
||||
/**
|
||||
* 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,
|
||||
|
||||
|
||||
14
server/internal/metadata/types.d.ts
vendored
14
server/internal/metadata/types.d.ts
vendored
@ -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;
|
||||
|
||||
Reference in New Issue
Block a user