feat: unified company metadata store

still need to migrate users from old developer and publisher tables
This commit is contained in:
Huskydog9988
2025-05-08 20:35:15 -04:00
committed by DecDuck
parent 14f0833d17
commit afaaaf2eb5
9 changed files with 159 additions and 65 deletions

View File

@ -7,9 +7,8 @@ import type {
_FetchGameMetadataParams,
GameMetadata,
_FetchPublisherMetadataParams,
PublisherMetadata,
CompanyMetadata,
_FetchDeveloperMetadataParams,
DeveloperMetadata,
} from "./types";
import type { AxiosRequestConfig } from "axios";
import axios from "axios";
@ -125,9 +124,6 @@ export class GiantBombProvider implements MetadataProvider {
return response;
}
id() {
return "giantbomb";
}
name() {
return "GiantBomb";
}
@ -229,7 +225,7 @@ export class GiantBombProvider implements MetadataProvider {
async fetchPublisher({
query,
createObject,
}: _FetchPublisherMetadataParams): Promise<PublisherMetadata> {
}: _FetchPublisherMetadataParams): Promise<CompanyMetadata> {
const results = await this.request<Array<CompanySearchResult>>(
"search",
"",
@ -246,7 +242,7 @@ export class GiantBombProvider implements MetadataProvider {
? this.turndown.turndown(company.description)
: company.deck;
const metadata: PublisherMetadata = {
const metadata: CompanyMetadata = {
id: company.guid,
name: company.name,
shortDescription: company.deck ?? "",
@ -261,7 +257,7 @@ export class GiantBombProvider implements MetadataProvider {
}
async fetchDeveloper(
params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> {
): Promise<CompanyMetadata> {
return await this.fetchPublisher(params);
}
}

View File

@ -7,9 +7,8 @@ import type {
_FetchGameMetadataParams,
GameMetadata,
_FetchPublisherMetadataParams,
PublisherMetadata,
CompanyMetadata,
_FetchDeveloperMetadataParams,
DeveloperMetadata,
} from "./types";
import type { AxiosRequestConfig } from "axios";
import axios from "axios";
@ -265,9 +264,6 @@ export class IGDBProvider implements MetadataProvider {
return msg.length > len ? msg.substring(0, 280) + "..." : msg;
}
id() {
return "igdb";
}
name() {
return "IGDB";
}
@ -375,7 +371,7 @@ export class IGDBProvider implements MetadataProvider {
async fetchPublisher({
query,
createObject,
}: _FetchPublisherMetadataParams): Promise<PublisherMetadata> {
}: _FetchPublisherMetadataParams): Promise<CompanyMetadata> {
const response = await this.request<IGDBCompany>(
"companies",
`where name = "${query}"; fields *; limit 1;`,
@ -395,7 +391,7 @@ export class IGDBProvider implements MetadataProvider {
if (company_url.length <= 0) company_url = site.url;
}
}
const metadata: PublisherMetadata = {
const metadata: CompanyMetadata = {
id: "" + company.id,
name: company.name,
shortDescription: this.trimMessage(company.description, 280),
@ -413,7 +409,7 @@ export class IGDBProvider implements MetadataProvider {
}
async fetchDeveloper(
params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> {
): Promise<CompanyMetadata> {
return await this.fetchPublisher(params);
}
}

View File

@ -5,11 +5,10 @@ import type {
_FetchDeveloperMetadataParams,
_FetchGameMetadataParams,
_FetchPublisherMetadataParams,
DeveloperMetadata,
GameMetadata,
GameMetadataSearchResult,
InternalGameMetadataResult,
PublisherMetadata,
CompanyMetadata,
} from "./types";
import { ObjectTransactionalHandler } from "../objects/transactional";
import { PriorityListIndexed } from "../utils/prioritylist";
@ -31,7 +30,6 @@ export class MissingMetadataProviderConfig extends Error {
export const DropUserAgent = "Drop/0.2";
export abstract class MetadataProvider {
abstract id(): string;
abstract name(): string;
abstract source(): MetadataSource;
@ -39,16 +37,16 @@ export abstract class MetadataProvider {
abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>;
abstract fetchPublisher(
params: _FetchPublisherMetadataParams,
): Promise<PublisherMetadata | undefined>;
): Promise<CompanyMetadata | undefined>;
abstract fetchDeveloper(
params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata | undefined>;
): Promise<CompanyMetadata | undefined>;
}
export class MetadataHandler {
// Ordered by priority
private providers: PriorityListIndexed<MetadataProvider> =
new PriorityListIndexed("id");
new PriorityListIndexed("source");
private objectHandler: ObjectTransactionalHandler =
new ObjectTransactionalHandler();
@ -63,8 +61,8 @@ export class MetadataHandler {
fetchProviderIdsInOrder() {
return this.providers
.values()
.map((e) => e.id())
.filter((e) => e !== "manual");
.map((e) => e.source())
.filter((e) => e !== "Manual");
}
async search(query: string) {
@ -80,7 +78,7 @@ export class MetadataHandler {
const mappedResults: InternalGameMetadataResult[] = results.map(
(result) =>
Object.assign({}, result, {
sourceId: provider.id(),
sourceId: provider.source(),
sourceName: provider.name(),
}),
);
@ -129,7 +127,7 @@ export class MetadataHandler {
where: {
metadataKey: {
metadataSource: provider.source(),
metadataId: provider.id(),
metadataId: result.id,
},
},
});
@ -163,12 +161,6 @@ export class MetadataHandler {
mName: metadata.name,
mShortDescription: metadata.shortDescription,
mDescription: metadata.description,
mDevelopers: {
connect: metadata.developers,
},
mPublishers: {
connect: metadata.publishers,
},
mReviewCount: metadata.reviewCount,
mReviewRating: metadata.reviewRating,
@ -182,6 +174,44 @@ export class MetadataHandler {
libraryBasePath,
},
});
// relate companies to game
for (const pub of metadata.publishers) {
await prisma.companyGameRelation.upsert({
where: {
companyGame: {
gameId: game.id,
companyId: pub.id,
},
},
create: {
gameId: game.id,
companyId: pub.id,
publisher: true,
},
update: {
publisher: true,
},
});
}
for (const dev of metadata.developers) {
await prisma.companyGameRelation.upsert({
where: {
companyGame: {
gameId: game.id,
companyId: dev.id,
},
},
create: {
gameId: game.id,
companyId: dev.id,
developer: true,
},
update: {
developer: true,
},
});
}
await pullObjects();
return game;
@ -208,10 +238,10 @@ export class MetadataHandler {
private async fetchDeveloperPublisher(
query: string,
functionName: "fetchDeveloper" | "fetchPublisher",
databaseName: "developer" | "publisher",
type: "developer" | "publisher",
) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const existing = await (prisma as any)[databaseName].findFirst({
const existing = await (prisma as any)[type].findFirst({
where: {
metadataOriginalQuery: query,
},
@ -226,12 +256,12 @@ export class MetadataHandler {
{},
["internal:read"],
);
let result: PublisherMetadata | undefined;
let result: CompanyMetadata | undefined;
try {
result = await provider[functionName]({ query, createObject });
if (result === undefined) {
throw new Error(
`${provider.source()} failed to find a ${databaseName} for "${query}`,
`${provider.source()} failed to find a ${type} for "${query}`,
);
}
} catch (e) {
@ -243,18 +273,32 @@ export class MetadataHandler {
// If we're successful
await pullObjects();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const object = await (prisma as any)[databaseName].create({
data: {
// TODO: dedupe metadata in event that a company with same source and id appears
const object = await prisma.company.upsert({
where: {
metadataKey: {
metadataId: result.id,
metadataSource: provider.source(),
},
},
create: {
metadataSource: provider.source(),
metadataId: provider.id(),
metadataId: result.id,
metadataOriginalQuery: query,
mName: result.name,
mShortDescription: result.shortDescription,
mDescription: result.description,
mLogo: result.logo,
mBanner: result.banner,
mLogoObjectId: result.logo,
mBannerObjectId: result.banner,
mWebsite: result.website,
},
update: {
mName: result.name,
mShortDescription: result.shortDescription,
mDescription: result.description,
mLogoObjectId: result.logo,
mBannerObjectId: result.banner,
mWebsite: result.website,
},
});

View File

@ -4,16 +4,12 @@ import type {
_FetchGameMetadataParams,
GameMetadata,
_FetchPublisherMetadataParams,
PublisherMetadata,
CompanyMetadata,
_FetchDeveloperMetadataParams,
DeveloperMetadata,
} from "./types";
import * as jdenticon from "jdenticon";
export class ManualMetadataProvider implements MetadataProvider {
id() {
return "manual";
}
name() {
return "Manual";
}
@ -49,12 +45,12 @@ export class ManualMetadataProvider implements MetadataProvider {
}
async fetchPublisher(
_params: _FetchPublisherMetadataParams,
): Promise<PublisherMetadata> {
): Promise<CompanyMetadata> {
throw new Error("Method not implemented.");
}
async fetchDeveloper(
_params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> {
): Promise<CompanyMetadata> {
throw new Error("Method not implemented.");
}
}

View File

@ -6,9 +6,8 @@ import type {
_FetchGameMetadataParams,
GameMetadata,
_FetchPublisherMetadataParams,
PublisherMetadata,
CompanyMetadata,
_FetchDeveloperMetadataParams,
DeveloperMetadata,
} from "./types";
import type { AxiosRequestConfig } from "axios";
import axios from "axios";
@ -60,9 +59,6 @@ interface PCGamingWikiCargoResult<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 {
id() {
return "pcgamingwiki";
}
name() {
return "PCGamingWiki";
}
@ -219,12 +215,12 @@ export class PCGamingWikiProvider implements MetadataProvider {
async fetchPublisher({
query,
createObject,
}: _FetchPublisherMetadataParams): Promise<PublisherMetadata> {
}: _FetchPublisherMetadataParams): Promise<CompanyMetadata> {
const searchParams = new URLSearchParams({
action: "cargoquery",
tables: "Company",
fields:
"Company.Parent,Company.Founded,Company.Defunct,Company.Website,Company._pageName=PageName,Company._pageID=pageID",
"Company.Parent,Company.Founded,Company.Defunct,Company.Website,Company._pageName=PageName,Company._pageID=PageID",
where: `Company._pageName="Company:${query}"`,
format: "json",
});
@ -240,7 +236,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
const fixedCompanyName =
company.PageName.split("Company:").at(1) ?? company.PageName;
const metadata: PublisherMetadata = {
const metadata: CompanyMetadata = {
id: company.PageID,
name: fixedCompanyName,
shortDescription: "",
@ -258,7 +254,7 @@ export class PCGamingWikiProvider implements MetadataProvider {
async fetchDeveloper(
params: _FetchDeveloperMetadataParams,
): Promise<DeveloperMetadata> {
): Promise<CompanyMetadata> {
return await this.fetchPublisher(params);
}
}

View File

@ -40,7 +40,7 @@ export interface GameMetadata {
images: ObjectReference[];
}
export interface PublisherMetadata {
export interface CompanyMetadata {
id: string;
name: string;
shortDescription: string;
@ -51,8 +51,6 @@ export interface PublisherMetadata {
website: string;
}
export type DeveloperMetadata = PublisherMetadata;
export interface _FetchGameMetadataParams {
id: string;
name: string;