mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-14 08:41:15 +10:00
feat: database redist support
This commit is contained in:
@ -0,0 +1,35 @@
|
|||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "GameTag_name_idx";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RedistVersion" (
|
||||||
|
"redistId" TEXT NOT NULL,
|
||||||
|
"versionName" TEXT NOT NULL,
|
||||||
|
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"platform" "Platform" NOT NULL,
|
||||||
|
"dropletManifest" JSONB NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RedistVersion_pkey" PRIMARY KEY ("redistId","versionName")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Redist" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"created" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT NOT NULL,
|
||||||
|
"iconObjectId" TEXT NOT NULL,
|
||||||
|
"libraryId" TEXT NOT NULL,
|
||||||
|
"libraryPath" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "Redist_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RedistVersion" ADD CONSTRAINT "RedistVersion_redistId_fkey" FOREIGN KEY ("redistId") REFERENCES "Redist"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Redist" ADD CONSTRAINT "Redist_libraryId_fkey" FOREIGN KEY ("libraryId") REFERENCES "Library"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- A unique constraint covering the columns `[libraryId,libraryPath]` on the table `Redist` will be added. If there are existing duplicate values, this will fail.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "GameTag_name_idx";
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Redist_libraryId_libraryPath_key" ON "Redist"("libraryId", "libraryPath");
|
||||||
@ -29,4 +29,5 @@ model Library {
|
|||||||
options Json
|
options Json
|
||||||
|
|
||||||
games Game[]
|
games Game[]
|
||||||
|
redists Redist[]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +1,3 @@
|
|||||||
enum MetadataSource {
|
|
||||||
Manual
|
|
||||||
GiantBomb
|
|
||||||
PCGamingWiki
|
|
||||||
IGDB
|
|
||||||
Metacritic
|
|
||||||
OpenCritic
|
|
||||||
}
|
|
||||||
|
|
||||||
model Game {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
|
|
||||||
metadataSource MetadataSource
|
|
||||||
metadataId String
|
|
||||||
created DateTime @default(now())
|
|
||||||
|
|
||||||
// Any field prefixed with m is filled in from metadata
|
|
||||||
// Acts as a cache so we can search and filter it
|
|
||||||
mName String // Name of game
|
|
||||||
mShortDescription String // Short description
|
|
||||||
mDescription String // Supports markdown
|
|
||||||
mReleased DateTime // When the game was released
|
|
||||||
|
|
||||||
ratings GameRating[]
|
|
||||||
|
|
||||||
featured Boolean @default(false)
|
|
||||||
|
|
||||||
mIconObjectId String // linked to objects in s3
|
|
||||||
mBannerObjectId String // linked to objects in s3
|
|
||||||
mCoverObjectId String
|
|
||||||
mImageCarouselObjectIds String[] // linked to below array
|
|
||||||
mImageLibraryObjectIds String[] // linked to objects in s3
|
|
||||||
|
|
||||||
versions GameVersion[]
|
|
||||||
|
|
||||||
// These fields will not be optional in the next version
|
|
||||||
// Any game without a library ID will be assigned one at startup, based on the defaults
|
|
||||||
libraryId String?
|
|
||||||
library Library? @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
|
||||||
libraryPath String
|
|
||||||
|
|
||||||
collections CollectionEntry[]
|
|
||||||
saves SaveSlot[]
|
|
||||||
screenshots Screenshot[]
|
|
||||||
tags GameTag[]
|
|
||||||
playtime Playtime[]
|
|
||||||
|
|
||||||
developers Company[] @relation(name: "developers")
|
|
||||||
publishers Company[] @relation(name: "publishers")
|
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
|
||||||
@@unique([libraryId, libraryPath], name: "libraryKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
model GameTag {
|
|
||||||
id String @id @default(uuid())
|
|
||||||
name String @unique
|
|
||||||
|
|
||||||
games Game[]
|
|
||||||
|
|
||||||
@@index([name(ops: raw("gist_trgm_ops(siglen=32)"))], type: Gist)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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
|
// A particular set of files that relate to the version
|
||||||
model GameVersion {
|
model GameVersion {
|
||||||
gameId String
|
gameId String
|
||||||
@ -106,6 +24,20 @@ model GameVersion {
|
|||||||
@@id([gameId, versionName])
|
@@id([gameId, versionName])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model RedistVersion {
|
||||||
|
redistId String
|
||||||
|
redist Redist @relation(fields: [redistId], references: [id], onDelete: Cascade)
|
||||||
|
versionName String
|
||||||
|
|
||||||
|
created DateTime @default(now())
|
||||||
|
|
||||||
|
platform Platform
|
||||||
|
|
||||||
|
dropletManifest Json // Results from droplet
|
||||||
|
|
||||||
|
@@id([redistId, versionName])
|
||||||
|
}
|
||||||
|
|
||||||
// A save slot for a game
|
// A save slot for a game
|
||||||
model SaveSlot {
|
model SaveSlot {
|
||||||
gameId String
|
gameId String
|
||||||
@ -158,26 +90,6 @@ model Playtime {
|
|||||||
@@index([userId])
|
@@index([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
|
|
||||||
|
|
||||||
developed Game[] @relation(name: "developers")
|
|
||||||
published Game[] @relation(name: "publishers")
|
|
||||||
|
|
||||||
@@unique([metadataSource, metadataId], name: "metadataKey")
|
|
||||||
}
|
|
||||||
|
|
||||||
model ObjectHash {
|
model ObjectHash {
|
||||||
id String @id
|
id String @id
|
||||||
hash String
|
hash String
|
||||||
|
|||||||
117
prisma/models/metadata.prisma
Normal file
117
prisma/models/metadata.prisma
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
enum MetadataSource {
|
||||||
|
Manual
|
||||||
|
GiantBomb
|
||||||
|
PCGamingWiki
|
||||||
|
IGDB
|
||||||
|
Metacritic
|
||||||
|
OpenCritic
|
||||||
|
}
|
||||||
|
|
||||||
|
model Game {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
|
||||||
|
metadataSource MetadataSource
|
||||||
|
metadataId String
|
||||||
|
created DateTime @default(now())
|
||||||
|
|
||||||
|
// Any field prefixed with m is filled in from metadata
|
||||||
|
// Acts as a cache so we can search and filter it
|
||||||
|
mName String // Name of game
|
||||||
|
mShortDescription String // Short description
|
||||||
|
mDescription String // Supports markdown
|
||||||
|
mReleased DateTime // When the game was released
|
||||||
|
|
||||||
|
ratings GameRating[]
|
||||||
|
|
||||||
|
featured Boolean @default(false)
|
||||||
|
|
||||||
|
mIconObjectId String // linked to objects in s3
|
||||||
|
mBannerObjectId String // linked to objects in s3
|
||||||
|
mCoverObjectId String
|
||||||
|
mImageCarouselObjectIds String[] // linked to below array
|
||||||
|
mImageLibraryObjectIds String[] // linked to objects in s3
|
||||||
|
|
||||||
|
versions GameVersion[]
|
||||||
|
|
||||||
|
// These fields will not be optional in the next version
|
||||||
|
// Any game without a library ID will be assigned one at startup, based on the defaults
|
||||||
|
libraryId String?
|
||||||
|
library Library? @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
libraryPath String
|
||||||
|
|
||||||
|
collections CollectionEntry[]
|
||||||
|
saves SaveSlot[]
|
||||||
|
screenshots Screenshot[]
|
||||||
|
tags GameTag[]
|
||||||
|
playtime Playtime[]
|
||||||
|
|
||||||
|
developers Company[] @relation(name: "developers")
|
||||||
|
publishers Company[] @relation(name: "publishers")
|
||||||
|
|
||||||
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
|
@@unique([libraryId, libraryPath], name: "libraryKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Redist {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
created DateTime @default(now())
|
||||||
|
|
||||||
|
name String
|
||||||
|
description String
|
||||||
|
iconObjectId String
|
||||||
|
|
||||||
|
libraryId String
|
||||||
|
library Library @relation(fields: [libraryId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
|
libraryPath String
|
||||||
|
|
||||||
|
versions RedistVersion[]
|
||||||
|
|
||||||
|
@@unique([libraryId, libraryPath], name: "libraryKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
model GameTag {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String @unique
|
||||||
|
|
||||||
|
games Game[]
|
||||||
|
|
||||||
|
@@index([name(ops: raw("gist_trgm_ops(siglen=32)"))], type: Gist)
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
developed Game[] @relation(name: "developers")
|
||||||
|
published Game[] @relation(name: "publishers")
|
||||||
|
|
||||||
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
|
}
|
||||||
3
server/internal/library/REDIST-README.md
Normal file
3
server/internal/library/REDIST-README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Redistributables
|
||||||
|
|
||||||
|
They are called 'redist' in the codebase/database models for brevity. Additionally, because they intentionally are drop-ins for games, they are added to all the functions that deal with games, without changing the names.
|
||||||
@ -13,7 +13,6 @@ import { parsePlatform } from "../utils/parseplatform";
|
|||||||
import notificationSystem from "../notifications";
|
import notificationSystem from "../notifications";
|
||||||
import { GameNotFoundError, type LibraryProvider } from "./provider";
|
import { GameNotFoundError, type LibraryProvider } from "./provider";
|
||||||
import { logger } from "../logging";
|
import { logger } from "../logging";
|
||||||
import type { GameModel } from "~/prisma/client/models";
|
|
||||||
import { createHash } from "node:crypto";
|
import { createHash } from "node:crypto";
|
||||||
|
|
||||||
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
|
export function createGameImportTaskId(libraryId: string, libraryPath: string) {
|
||||||
@ -49,14 +48,15 @@ class LibraryManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchGamesByLibrary() {
|
async fetchGamesByLibrary() {
|
||||||
const results: { [key: string]: { [key: string]: GameModel } } = {};
|
const results: { [key: string]: { [key: string]: boolean } } = {};
|
||||||
const games = await prisma.game.findMany({});
|
const games = await prisma.game.findMany({});
|
||||||
for (const game of games) {
|
const redist = await prisma.redist.findMany({});
|
||||||
const libraryId = game.libraryId!;
|
for (const item of [...games, ...redist]) {
|
||||||
const libraryPath = game.libraryPath!;
|
const libraryId = item.libraryId!;
|
||||||
|
const libraryPath = item.libraryPath!;
|
||||||
|
|
||||||
results[libraryId] ??= {};
|
results[libraryId] ??= {};
|
||||||
results[libraryId][libraryPath] = game;
|
results[libraryId][libraryPath] = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@ -82,7 +82,8 @@ class LibraryManager {
|
|||||||
async fetchUnimportedGameVersions(libraryId: string, libraryPath: string) {
|
async fetchUnimportedGameVersions(libraryId: string, libraryPath: string) {
|
||||||
const provider = this.libraries.get(libraryId);
|
const provider = this.libraries.get(libraryId);
|
||||||
if (!provider) return undefined;
|
if (!provider) return undefined;
|
||||||
const game = await prisma.game.findUnique({
|
const game =
|
||||||
|
(await prisma.game.findUnique({
|
||||||
where: {
|
where: {
|
||||||
libraryKey: {
|
libraryKey: {
|
||||||
libraryId,
|
libraryId,
|
||||||
@ -93,7 +94,19 @@ class LibraryManager {
|
|||||||
id: true,
|
id: true,
|
||||||
versions: true,
|
versions: true,
|
||||||
},
|
},
|
||||||
});
|
})) ??
|
||||||
|
(await prisma.redist.findUnique({
|
||||||
|
where: {
|
||||||
|
libraryKey: {
|
||||||
|
libraryId,
|
||||||
|
libraryPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
versions: true,
|
||||||
|
},
|
||||||
|
}));
|
||||||
if (!game) return undefined;
|
if (!game) return undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -217,6 +230,10 @@ class LibraryManager {
|
|||||||
})) > 0;
|
})) > 0;
|
||||||
if (hasGame) return false;
|
if (hasGame) return false;
|
||||||
|
|
||||||
|
const hasRedist =
|
||||||
|
(await prisma.redist.count({ where: { libraryId, libraryPath } })) > 0;
|
||||||
|
if (hasRedist) return false;
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -286,7 +303,6 @@ class LibraryManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Then, create the database object
|
// Then, create the database object
|
||||||
if (metadata.onlySetup) {
|
|
||||||
await prisma.gameVersion.create({
|
await prisma.gameVersion.create({
|
||||||
data: {
|
data: {
|
||||||
gameId: gameId,
|
gameId: gameId,
|
||||||
@ -297,30 +313,13 @@ class LibraryManager {
|
|||||||
umuIdOverride: metadata.umuId,
|
umuIdOverride: metadata.umuId,
|
||||||
platform: platform,
|
platform: platform,
|
||||||
|
|
||||||
onlySetup: true,
|
onlySetup: metadata.onlySetup,
|
||||||
setupCommand: metadata.setup,
|
|
||||||
setupArgs: metadata.setupArgs.split(" "),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await prisma.gameVersion.create({
|
|
||||||
data: {
|
|
||||||
gameId: gameId,
|
|
||||||
versionName: versionName,
|
|
||||||
dropletManifest: manifest,
|
|
||||||
versionIndex: currentIndex,
|
|
||||||
delta: metadata.delta,
|
|
||||||
umuIdOverride: metadata.umuId,
|
|
||||||
platform: platform,
|
|
||||||
|
|
||||||
onlySetup: false,
|
|
||||||
setupCommand: metadata.setup,
|
setupCommand: metadata.setup,
|
||||||
setupArgs: metadata.setupArgs.split(" "),
|
setupArgs: metadata.setupArgs.split(" "),
|
||||||
launchCommand: metadata.launch,
|
launchCommand: metadata.launch,
|
||||||
launchArgs: metadata.launchArgs.split(" "),
|
launchArgs: metadata.launchArgs.split(" "),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
logger.info("Successfully created version!");
|
logger.info("Successfully created version!");
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ import taskHandler, { wrapTaskContext } from "../tasks";
|
|||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { fuzzy } from "fast-fuzzy";
|
import { fuzzy } from "fast-fuzzy";
|
||||||
import { logger } from "~/server/internal/logging";
|
import { logger } from "~/server/internal/logging";
|
||||||
import { createGameImportTaskId } from "../library";
|
import libraryManager, { createGameImportTaskId } from "../library";
|
||||||
import type { GameTagModel } from "~/prisma/client/models";
|
import type { GameTagModel } from "~/prisma/client/models";
|
||||||
|
|
||||||
export class MissingMetadataProviderConfig extends Error {
|
export class MissingMetadataProviderConfig extends Error {
|
||||||
@ -175,15 +175,8 @@ export class MetadataHandler {
|
|||||||
if (!provider)
|
if (!provider)
|
||||||
throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`);
|
throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`);
|
||||||
|
|
||||||
const existing = await prisma.game.findUnique({
|
const okay = await libraryManager.checkUnimportedGamePath(libraryId, libraryPath);
|
||||||
where: {
|
if (!okay) return undefined;
|
||||||
metadataKey: {
|
|
||||||
metadataSource: provider.source(),
|
|
||||||
metadataId: result.id,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
if (existing) return undefined;
|
|
||||||
|
|
||||||
const gameId = randomUUID();
|
const gameId = randomUUID();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user