mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
feat: unified company metadata store
still need to migrate users from old developer and publisher tables
This commit is contained in:
@ -0,0 +1,35 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Company" (
|
||||
"id" TEXT NOT NULL,
|
||||
"metadataSource" "MetadataSource" NOT NULL,
|
||||
"metadataId" TEXT NOT NULL,
|
||||
"metadataOriginalQuery" TEXT NOT NULL,
|
||||
"mName" TEXT NOT NULL,
|
||||
"mShortDescription" TEXT NOT NULL,
|
||||
"mDescription" TEXT NOT NULL,
|
||||
"mLogoObjectId" TEXT NOT NULL,
|
||||
"mBannerObjectId" TEXT NOT NULL,
|
||||
"mWebsite" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "Company_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "CompanyGameRelation" (
|
||||
"companyId" TEXT NOT NULL,
|
||||
"gameId" TEXT NOT NULL,
|
||||
"developer" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publisher" BOOLEAN NOT NULL DEFAULT false
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Company_metadataSource_metadataId_key" ON "Company"("metadataSource", "metadataId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "CompanyGameRelation_companyId_gameId_key" ON "CompanyGameRelation"("companyId", "gameId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@ -33,9 +33,10 @@ model Game {
|
||||
versions GameVersion[]
|
||||
libraryBasePath String @unique // Base dir for all the game versions
|
||||
|
||||
collections CollectionEntry[]
|
||||
saves SaveSlot[]
|
||||
screenshots Screenshot[]
|
||||
collections CollectionEntry[]
|
||||
saves SaveSlot[]
|
||||
screenshots Screenshot[]
|
||||
companyRelations CompanyGameRelation[]
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
}
|
||||
@ -102,6 +103,38 @@ model Screenshot {
|
||||
@@index([gameId, userId])
|
||||
}
|
||||
|
||||
model Company {
|
||||
id String @id @default(uuid())
|
||||
|
||||
metadataSource MetadataSource
|
||||
metadataId String
|
||||
metadataOriginalQuery String
|
||||
|
||||
mName String
|
||||
mShortDescription String
|
||||
mDescription String
|
||||
mLogoObjectId String
|
||||
mBannerObjectId String
|
||||
mWebsite String
|
||||
|
||||
gameRelations CompanyGameRelation[]
|
||||
|
||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||
}
|
||||
|
||||
model CompanyGameRelation {
|
||||
companyId String
|
||||
company Company @relation(fields: [companyId], references: [id], onDelete: Cascade)
|
||||
gameId String
|
||||
game Game @relation(fields: [gameId], references: [id], onDelete: Cascade)
|
||||
|
||||
// what the company did for the game
|
||||
developer Boolean @default(false)
|
||||
publisher Boolean @default(false)
|
||||
|
||||
@@unique([companyId, gameId], name: "companyGame")
|
||||
}
|
||||
|
||||
model Developer {
|
||||
id String @id @default(uuid())
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
4
server/internal/metadata/types.d.ts
vendored
4
server/internal/metadata/types.d.ts
vendored
@ -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;
|
||||
|
||||
@ -18,7 +18,7 @@ export default defineNitroPlugin(async (_nitro) => {
|
||||
for (const provider of metadataProviders) {
|
||||
try {
|
||||
const prov = new provider();
|
||||
const id = prov.id();
|
||||
const id = prov.source();
|
||||
providers.set(id, prov);
|
||||
|
||||
console.log(`enabled metadata provider: ${prov.name()}`);
|
||||
|
||||
Reference in New Issue
Block a user