mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-10 04:22:09 +10:00
Compare commits
3 Commits
v0.3.1
...
image-impo
| Author | SHA1 | Date | |
|---|---|---|---|
| a287138650 | |||
| 545a6b154a | |||
| 442f940cc4 |
@ -16,7 +16,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/content/types";
|
||||
|
||||
const { game } = defineProps<{
|
||||
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
||||
|
||||
@ -171,7 +171,7 @@ import {
|
||||
ListboxOption,
|
||||
ListboxOptions,
|
||||
} from "@headlessui/vue";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/content/types";
|
||||
import { FetchError } from "ofetch";
|
||||
import type { SerializeObject } from "nitropack";
|
||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||
|
||||
@ -67,38 +67,6 @@
|
||||
"signout": "Signout",
|
||||
"username": "Username"
|
||||
},
|
||||
"setup": {
|
||||
"welcome": "Hey there.",
|
||||
"welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works.",
|
||||
"finish": "Let's go {arrow}",
|
||||
"noPage": "no page",
|
||||
"auth": {
|
||||
"title": "Authentication",
|
||||
"description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.",
|
||||
"docs": "Documentation {arrow}",
|
||||
"enabled": "Enabled?",
|
||||
"simple": {
|
||||
"title": "Simple authentication",
|
||||
"description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.",
|
||||
"register": "Register as admin {arrow}"
|
||||
},
|
||||
"openid": {
|
||||
"title": "OpenID Connect",
|
||||
"description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.",
|
||||
"skip": "I have a user with OIDC"
|
||||
}
|
||||
},
|
||||
"stages": {
|
||||
"account": {
|
||||
"name": "Setup your admin account.",
|
||||
"description": "You need at least one account to start using Drop."
|
||||
},
|
||||
"library": {
|
||||
"name": "Create a library.",
|
||||
"description": "Add at least one library source to use Drop."
|
||||
}
|
||||
}
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"chars": {
|
||||
"arrow": "→",
|
||||
@ -107,6 +75,7 @@
|
||||
"srComma": ", {0}"
|
||||
},
|
||||
"common": {
|
||||
"add": "Add",
|
||||
"cannotUndo": "This action cannot be undone.",
|
||||
"close": "Close",
|
||||
"create": "Create",
|
||||
@ -126,8 +95,7 @@
|
||||
"servers": "Servers",
|
||||
"srLoading": "Loading...",
|
||||
"tags": "Tags",
|
||||
"today": "Today",
|
||||
"add": "Add"
|
||||
"today": "Today"
|
||||
},
|
||||
"delete": "Delete",
|
||||
"drop": {
|
||||
@ -284,9 +252,7 @@
|
||||
"addToLib": "Add to Library",
|
||||
"admin": {
|
||||
"detectedGame": "Drop has detected you have new games to import.",
|
||||
"detectedVersion": "Drop has detected you have new verions of this game to import.",
|
||||
"offlineTitle": "Game offline",
|
||||
"offline": "Drop couldn't access this game.",
|
||||
"detectedVersion": "Drop has detected you have new versions of this game to import.",
|
||||
"game": {
|
||||
"addCarouselNoImages": "No images to add.",
|
||||
"addDescriptionNoImages": "No images to add.",
|
||||
@ -307,6 +273,8 @@
|
||||
},
|
||||
"gameLibrary": "Game Library",
|
||||
"import": {
|
||||
"bulkImportDescription": "When on, this page won't redirect you to the import task, so you can import multiple games in succession.",
|
||||
"bulkImportTitle": "Bulk import mode",
|
||||
"import": "Import",
|
||||
"link": "Import {arrow}",
|
||||
"loading": "Loading game results...",
|
||||
@ -317,8 +285,6 @@
|
||||
"selectGamePlaceholder": "Please select a game...",
|
||||
"selectGameSearch": "Select game",
|
||||
"selectPlatform": "Please select a platform...",
|
||||
"bulkImportTitle": "Bulk import mode",
|
||||
"bulkImportDescription": "When on, this page won't redirect you to the import task, so you can import multiple games in succession.",
|
||||
"version": {
|
||||
"advancedOptions": "Advanced options",
|
||||
"import": "Import version",
|
||||
@ -344,16 +310,64 @@
|
||||
},
|
||||
"withoutMetadata": "Import without metadata"
|
||||
},
|
||||
"metadata": {
|
||||
"companies": {
|
||||
"action": "Manage {arrow}",
|
||||
"addGame": {
|
||||
"description": "Pick a game to add to the company, and whether it should be listed as a developer, publisher, or both.",
|
||||
"developer": "Developer?",
|
||||
"noGames": "No games to add",
|
||||
"publisher": "Publisher?",
|
||||
"title": "Connect game to this company"
|
||||
},
|
||||
"description": "Companies organize games by who they were developed or published by.",
|
||||
"editor": {
|
||||
"action": "Add Game {plus}",
|
||||
"developed": "Developed",
|
||||
"libraryDescription": "Add, remove, or customise what this company has developed and/or published.",
|
||||
"libraryTitle": "Game Library",
|
||||
"noDescription": "(no description)",
|
||||
"published": "Published",
|
||||
"uploadBanner": "Upload banner",
|
||||
"uploadIcon": "Upload icon"
|
||||
},
|
||||
"modals": {
|
||||
"nameDescription": "Edit the company's name. Used to match to new game imports.",
|
||||
"nameTitle": "Edit company name",
|
||||
"shortDeckDescription": "Edit the company's description. Doesn't affect long (markdown) description.",
|
||||
"shortDeckTitle": "Edit company description",
|
||||
"websiteDescription": "Edit the company's website. Note: this will be a link, and won't have redirect protection.",
|
||||
"websiteTitle": "Edit company website"
|
||||
},
|
||||
"noCompanies": "No companies",
|
||||
"noGames": "No games",
|
||||
"search": "Search companies...",
|
||||
"searchGames": "Search company games...",
|
||||
"title": "Companies"
|
||||
},
|
||||
"tags": {
|
||||
"action": "Manage {arrow}",
|
||||
"create": "Create",
|
||||
"description": "Tags are automatically created from imported genres. You can add custom tags to add categorisation to your game library.",
|
||||
"modal": {
|
||||
"description": "Create a tag to organize your library.",
|
||||
"title": "Create Tag"
|
||||
},
|
||||
"title": "Tags"
|
||||
}
|
||||
},
|
||||
"metadataProvider": "Metadata provider",
|
||||
"noGames": "No games imported",
|
||||
"offline": "Drop couldn't access this game.",
|
||||
"offlineTitle": "Game offline",
|
||||
"openEditor": "Open in Editor {arrow}",
|
||||
"openStore": "Open in Store",
|
||||
"shortDesc": "Short Description",
|
||||
"sources": {
|
||||
"create": "Create source",
|
||||
"edit": "Edit source",
|
||||
"createDesc": "Drop will use this source to access your game library, and make them available.",
|
||||
"desc": "Configure your library sources, where Drop will look for new games and versions to import.",
|
||||
"edit": "Edit source",
|
||||
"fsDesc": "Imports games from a path on disk. Requires version-based folder structure, and supports archived games.",
|
||||
"fsFlatDesc": "Imports games from a path on disk, but without a separate version subfolder. Useful when migrating an existing library to Drop.",
|
||||
"fsPath": "Path",
|
||||
@ -373,53 +387,7 @@
|
||||
"noVersions": "You have no versions of this game available.",
|
||||
"noVersionsAdded": "no versions added"
|
||||
},
|
||||
"versionPriority": "Version priority",
|
||||
"metadata": {
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"description": "Tags are automatically created from imported genres. You can add custom tags to add categorisation to your game library.",
|
||||
"action": "Manage {arrow}",
|
||||
"create": "Create",
|
||||
"modal": {
|
||||
"title": "Create Tag",
|
||||
"description": "Create a tag to organize your library."
|
||||
}
|
||||
},
|
||||
"companies": {
|
||||
"title": "Companies",
|
||||
"description": "Companies organize games by who they were developed or published by.",
|
||||
"action": "Manage {arrow}",
|
||||
"search": "Search companies...",
|
||||
"searchGames": "Search company games...",
|
||||
"noCompanies": "No companies",
|
||||
"noGames": "No games",
|
||||
"editor": {
|
||||
"libraryTitle": "Game Library",
|
||||
"libraryDescription": "Add, remove, or customise what this company has developed and/or published.",
|
||||
"action": "Add Game {plus}",
|
||||
"published": "Published",
|
||||
"developed": "Developed",
|
||||
"uploadIcon": "Upload icon",
|
||||
"uploadBanner": "Upload banner",
|
||||
"noDescription": "(no description)"
|
||||
},
|
||||
"addGame": {
|
||||
"title": "Connect game to this company",
|
||||
"description": "Pick a game to add to the company, and whether it should be listed as a developer, publisher, or both.",
|
||||
"publisher": "Publisher?",
|
||||
"developer": "Developer?",
|
||||
"noGames": "No games to add"
|
||||
},
|
||||
"modals": {
|
||||
"nameTitle": "Edit company name",
|
||||
"nameDescription": "Edit the company's name. Used to match to new game imports.",
|
||||
"shortDeckTitle": "Edit company description",
|
||||
"shortDeckDescription": "Edit the company's description. Doesn't affect long (markdown) description.",
|
||||
"websiteTitle": "Edit company website",
|
||||
"websiteDescription": "Edit the company's website. Note: this will be a link, and won't have redirect protection."
|
||||
}
|
||||
}
|
||||
}
|
||||
"versionPriority": "Version priority"
|
||||
},
|
||||
"back": "Back to Library",
|
||||
"collection": {
|
||||
@ -490,6 +458,38 @@
|
||||
"title": "Settings"
|
||||
}
|
||||
},
|
||||
"setup": {
|
||||
"auth": {
|
||||
"description": "Authentication in Drop happens through multiple configured 'providers'. Each one can allow users to sign-in through their method. To get started, have at least one authentication provider enabled, and create an account through it.",
|
||||
"docs": "Documentation {arrow}",
|
||||
"enabled": "Enabled?",
|
||||
"openid": {
|
||||
"description": "OpenID Connect (OIDC) is an OAuth2 extension commonly supported. Drop requires OIDC configuration to be done via environment variables.",
|
||||
"skip": "I have a user with OIDC",
|
||||
"title": "OpenID Connect"
|
||||
},
|
||||
"simple": {
|
||||
"description": "Simple authentication uses username/password to authentication users. It is enabled by default if no other authentication provider is enabled.",
|
||||
"register": "Register as admin {arrow}",
|
||||
"title": "Simple authentication"
|
||||
},
|
||||
"title": "Authentication"
|
||||
},
|
||||
"finish": "Let's go {arrow}",
|
||||
"noPage": "no page",
|
||||
"stages": {
|
||||
"account": {
|
||||
"description": "You need at least one account to start using Drop.",
|
||||
"name": "Setup your admin account."
|
||||
},
|
||||
"library": {
|
||||
"description": "Add at least one library source to use Drop.",
|
||||
"name": "Create a library."
|
||||
}
|
||||
},
|
||||
"welcome": "Hey there.",
|
||||
"welcomeDescription": "Welcome to Drop setup wizard. It will walk you through configuring Drop for the first time, and how it works."
|
||||
},
|
||||
"store": {
|
||||
"about": "About",
|
||||
"commingSoon": "coming soon",
|
||||
@ -568,6 +568,7 @@
|
||||
"admin": {
|
||||
"adminHeader": "Admin?",
|
||||
"adminUserLabel": "Admin user",
|
||||
"authLink": "Authentication {arrow}",
|
||||
"authentication": {
|
||||
"configure": "Configure",
|
||||
"description": "Drop supports a variety of \"authentication mechanisms\". As you enable or disable them, they are shown on the sign in screen for users to select from. Click the dot menu to configure the authentication mechanism.",
|
||||
@ -579,7 +580,6 @@
|
||||
"srOpenOptions": "Open options",
|
||||
"title": "Authentication"
|
||||
},
|
||||
"authLink": "Authentication {arrow}",
|
||||
"authoptionsHeader": "Auth Options",
|
||||
"delete": "Delete",
|
||||
"deleteUser": "Delete user {0}",
|
||||
|
||||
@ -4,6 +4,9 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"engines": {
|
||||
"node": ">=22.16.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "nuxt build",
|
||||
"dev": "nuxt dev",
|
||||
|
||||
@ -309,7 +309,7 @@ import {
|
||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/content/types";
|
||||
|
||||
definePageMeta({
|
||||
layout: "admin",
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
-- AlterEnum
|
||||
ALTER TYPE "MetadataSource" ADD VALUE 'SteamGridDB';
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "GameTag_name_idx";
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "GameTag_name_idx" ON "GameTag" USING GIST ("name" gist_trgm_ops(siglen=32));
|
||||
@ -5,6 +5,7 @@ enum MetadataSource {
|
||||
IGDB
|
||||
Metacritic
|
||||
OpenCritic
|
||||
SteamGridDB
|
||||
}
|
||||
|
||||
model Game {
|
||||
|
||||
11
server/api/v1/admin/game/image/import/index.get.ts
Normal file
11
server/api/v1/admin/game/image/import/index.get.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import imageHandler from "~/server/internal/metadata/image";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["game:image:import"]);
|
||||
if (!allowed) throw createError({ statusCode: 403 });
|
||||
|
||||
const images = await imageHandler.searchImages("space engineers");
|
||||
|
||||
return images;
|
||||
});
|
||||
@ -2,7 +2,7 @@ import { type } from "arktype";
|
||||
import { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import libraryManager from "~/server/internal/library";
|
||||
import metadataHandler from "~/server/internal/metadata";
|
||||
import metadataHandler from "~/server/internal/metadata/content";
|
||||
|
||||
const ImportGameBody = type({
|
||||
library: "string",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import aclManager from "~/server/internal/acls";
|
||||
import metadataHandler from "~/server/internal/metadata";
|
||||
import metadataHandler from "~/server/internal/metadata/content";
|
||||
|
||||
export default defineEventHandler(async (h3) => {
|
||||
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
|
||||
|
||||
@ -71,6 +71,7 @@ export const systemACLDescriptions: ObjectFromList<typeof systemACLs> = {
|
||||
"game:version:update": "Update the version order on a game.",
|
||||
"game:version:delete": "Delete a version for a game.",
|
||||
"game:image:new": "Upload an image for a game.",
|
||||
"game:image:import": "Import images from image providers for a game.",
|
||||
"game:image:delete": "Delete an image for a game.",
|
||||
|
||||
"company:read": "Fetch companies.",
|
||||
|
||||
@ -65,6 +65,7 @@ export const systemACLs = [
|
||||
"game:version:update",
|
||||
"game:version:delete",
|
||||
"game:image:new",
|
||||
"game:image:import",
|
||||
"game:image:delete",
|
||||
|
||||
"company:read",
|
||||
|
||||
7
server/internal/metadata/README.md
Normal file
7
server/internal/metadata/README.md
Normal file
@ -0,0 +1,7 @@
|
||||
# Metadata structure
|
||||
|
||||
## Content providers (`MetadataProvider`)
|
||||
Types & implementation is under `./content`. Providers under `./providers`
|
||||
|
||||
## Image providers (`ImageProvider`)
|
||||
Types & implementation is under `./image`. Providers under `./providers`
|
||||
@ -1,6 +1,11 @@
|
||||
/**
|
||||
* Metadata providers search and download game metadata
|
||||
* Basically an overkill auto-fill
|
||||
*/
|
||||
|
||||
import type { Prisma } from "~/prisma/client/client";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import prisma from "../db/database";
|
||||
import prisma from "../../db/database";
|
||||
import type {
|
||||
_FetchGameMetadataParams,
|
||||
_FetchCompanyMetadataParams,
|
||||
@ -10,15 +15,15 @@ import type {
|
||||
CompanyMetadata,
|
||||
GameMetadataRating,
|
||||
} from "./types";
|
||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
||||
import { PriorityListIndexed } from "../utils/prioritylist";
|
||||
import { systemConfig } from "../config/sys-conf";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import taskHandler, { wrapTaskContext } from "../tasks";
|
||||
import { ObjectTransactionalHandler } from "../../objects/transactional";
|
||||
import { PriorityListIndexed } from "../../utils/prioritylist";
|
||||
import { systemConfig } from "../../config/sys-conf";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
import taskHandler, { wrapTaskContext } from "../../tasks";
|
||||
import { randomUUID } from "crypto";
|
||||
import { fuzzy } from "fast-fuzzy";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import libraryManager from "../library";
|
||||
import libraryManager from "../../library";
|
||||
import type { GameTagModel } from "~/prisma/client/models";
|
||||
|
||||
export class MissingMetadataProviderConfig extends Error {
|
||||
@ -1,6 +1,6 @@
|
||||
import type { Company, GameRating } from "~/prisma/client";
|
||||
import type { TransactionDataType } from "../objects/transactional";
|
||||
import type { ObjectReference } from "../objects/objectHandler";
|
||||
import type { TransactionDataType } from "../../objects/transactional";
|
||||
import type { ObjectReference } from "../../objects/objectHandler";
|
||||
|
||||
export interface GameMetadataSearchResult {
|
||||
id: string;
|
||||
81
server/internal/metadata/image/index.ts
Normal file
81
server/internal/metadata/image/index.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { ImageSearchResult } from "./types";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
import { ObjectTransactionalHandler } from "../../objects/transactional";
|
||||
import { PriorityListIndexed } from "../../utils/prioritylist";
|
||||
import { logger } from "../../logging";
|
||||
|
||||
export abstract class ImageProvider {
|
||||
abstract name(): string;
|
||||
abstract source(): MetadataSource;
|
||||
|
||||
abstract searchImages(query: string): Promise<ImageSearchResult[]>;
|
||||
abstract importImages(
|
||||
images: ImageSearchResult[],
|
||||
taskRunContext?: TaskRunContext,
|
||||
): Promise<string[]>; // List of object IDs
|
||||
}
|
||||
|
||||
/**
|
||||
* Confusingly, does videos too.
|
||||
*/
|
||||
export class ImageHandler {
|
||||
private providers: PriorityListIndexed<ImageProvider> =
|
||||
new PriorityListIndexed("source");
|
||||
private objectHandler: ObjectTransactionalHandler =
|
||||
new ObjectTransactionalHandler();
|
||||
|
||||
addProvider(provider: ImageProvider, priority: number = 0) {
|
||||
this.providers.push(provider, priority);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns provider IDs, used to save to applicationConfig
|
||||
* @returns The provider IDs in order, missing manual
|
||||
*/
|
||||
fetchProviderIdsInOrder() {
|
||||
return this.providers
|
||||
.values()
|
||||
.map((e) => e.source())
|
||||
.filter((e) => e !== "Manual");
|
||||
}
|
||||
|
||||
async searchImages(query: string) {
|
||||
const providers = this.providers.values();
|
||||
const promises = [];
|
||||
for (const provider of providers) {
|
||||
const localFetch = async () => {
|
||||
try {
|
||||
const providerResults = await provider.searchImages(query);
|
||||
return providerResults.map((result) => ({ ...result, provider: provider.source() }));
|
||||
} catch (e) {
|
||||
throw `${provider.name()}: ${e}`;
|
||||
}
|
||||
};
|
||||
promises.push(localFetch());
|
||||
}
|
||||
|
||||
const result = await Promise.allSettled(promises);
|
||||
|
||||
const fails = result.filter((e) => e.status === "rejected");
|
||||
if (fails.length > 0) {
|
||||
const failText = fails
|
||||
.map((e) => e.reason)
|
||||
.map((e) => "" + e)
|
||||
.join("\n");
|
||||
|
||||
logger.warn(
|
||||
`Failed to fetch some images from providers. Errors:\n${failText}`,
|
||||
);
|
||||
}
|
||||
|
||||
const successes = result
|
||||
.filter((e) => e.status === "fulfilled")
|
||||
.map((e) => e.value)
|
||||
.flat();
|
||||
return successes;
|
||||
}
|
||||
}
|
||||
|
||||
export const imageHandler = new ImageHandler();
|
||||
export default imageHandler;
|
||||
10
server/internal/metadata/image/types.d.ts
vendored
Normal file
10
server/internal/metadata/image/types.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
export type ImageSearchResultType = "image" | "video";
|
||||
|
||||
|
||||
export interface ImageSearchResult {
|
||||
id: string; // internal identifier for the result
|
||||
url: string;
|
||||
type: ImageSearchResultType;
|
||||
name?: string;
|
||||
size?: number;
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import { MissingMetadataProviderConfig } from ".";
|
||||
import type { MetadataProvider } from "../content";
|
||||
import { MissingMetadataProviderConfig } from "../content";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
_FetchGameMetadataParams,
|
||||
@ -9,11 +9,11 @@ import type {
|
||||
_FetchCompanyMetadataParams,
|
||||
CompanyMetadata,
|
||||
GameMetadataRating,
|
||||
} from "./types";
|
||||
} from "../content/types";
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import TurndownService from "turndown";
|
||||
import { DateTime } from "luxon";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
|
||||
interface GiantBombResponseType<T> {
|
||||
error: "OK" | string;
|
||||
@ -277,6 +277,7 @@ export class GiantBombProvider implements MetadataProvider {
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
async fetchCompany({
|
||||
query,
|
||||
createObject,
|
||||
@ -1,19 +1,19 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import { MissingMetadataProviderConfig } from ".";
|
||||
import type { MetadataProvider } from "../content";
|
||||
import { MissingMetadataProviderConfig } from "../content";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
_FetchGameMetadataParams,
|
||||
GameMetadata,
|
||||
_FetchCompanyMetadataParams,
|
||||
CompanyMetadata,
|
||||
} from "./types";
|
||||
} from "../content/types";
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
import axios from "axios";
|
||||
import { DateTime } from "luxon";
|
||||
import * as jdenticon from "jdenticon";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
|
||||
type IGDBID = number;
|
||||
@ -1,11 +1,11 @@
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type { MetadataProvider } from "../content";
|
||||
import type {
|
||||
_FetchGameMetadataParams,
|
||||
GameMetadata,
|
||||
_FetchCompanyMetadataParams,
|
||||
CompanyMetadata,
|
||||
} from "./types";
|
||||
} from "../content/types";
|
||||
import * as jdenticon from "jdenticon";
|
||||
|
||||
export class ManualMetadataProvider implements MetadataProvider {
|
||||
@ -1,6 +1,6 @@
|
||||
import type { CompanyModel } from "~/prisma/client/models";
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { MetadataProvider } from ".";
|
||||
import type { MetadataProvider } from "../content";
|
||||
import type {
|
||||
GameMetadataSearchResult,
|
||||
_FetchGameMetadataParams,
|
||||
@ -8,14 +8,14 @@ import type {
|
||||
_FetchCompanyMetadataParams,
|
||||
CompanyMetadata,
|
||||
GameMetadataRating,
|
||||
} from "./types";
|
||||
} from "../content/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";
|
||||
import type { TaskRunContext } from "../tasks";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
|
||||
interface PCGamingWikiParseRawPage {
|
||||
112
server/internal/metadata/providers/steamgriddb.ts
Normal file
112
server/internal/metadata/providers/steamgriddb.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { MetadataSource } from "~/prisma/client/enums";
|
||||
import type { TaskRunContext } from "../../tasks";
|
||||
import type { ImageProvider } from "../image";
|
||||
import { ImageSearchResultType, type ImageSearchResult } from "../image/types";
|
||||
import { MissingMetadataProviderConfig } from "../content";
|
||||
import type { NitroFetchOptions } from "nitropack";
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
name: string;
|
||||
release_date: number;
|
||||
}
|
||||
|
||||
interface SteamGridResponse<T> {
|
||||
success: boolean;
|
||||
data: Array<T>;
|
||||
}
|
||||
|
||||
interface Grid {
|
||||
id: number;
|
||||
nsfw: boolean;
|
||||
humor: boolean;
|
||||
url: string;
|
||||
thumb: string;
|
||||
}
|
||||
|
||||
export class SteamGridDB implements ImageProvider {
|
||||
private apikey: string;
|
||||
|
||||
constructor() {
|
||||
const apikey = process.env.STEAMGRID_API_KEY;
|
||||
if (!apikey)
|
||||
throw new MissingMetadataProviderConfig("STEAMGRID_API_KEY", this.name());
|
||||
|
||||
this.apikey = apikey;
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
path: string,
|
||||
opts?: NitroFetchOptions<
|
||||
string,
|
||||
| "get"
|
||||
| "head"
|
||||
| "patch"
|
||||
| "post"
|
||||
| "put"
|
||||
| "delete"
|
||||
| "connect"
|
||||
| "options"
|
||||
| "trace"
|
||||
>,
|
||||
) {
|
||||
const fullPath = `https://www.steamgriddb.com/api/v2${path}`;
|
||||
|
||||
const response = await $fetch<T>(fullPath, {
|
||||
...opts,
|
||||
headers: { ...opts?.headers, Authorization: `Bearer ${this.apikey}` },
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
name(): string {
|
||||
return "SteamGridDB";
|
||||
}
|
||||
source(): MetadataSource {
|
||||
return MetadataSource.SteamGridDB;
|
||||
}
|
||||
async searchImages(query: string): Promise<ImageSearchResult[]> {
|
||||
const games = await this.request<SteamGridResponse<SearchResult>>(
|
||||
`/search/autocomplete/${encodeURIComponent(query)}`,
|
||||
);
|
||||
if (!games.success)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "[SteamGridDB] Search indicated failed response.",
|
||||
});
|
||||
|
||||
const firstGame = games.data.at(0);
|
||||
if (!firstGame)
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: "No results found.",
|
||||
});
|
||||
|
||||
const grids = await this.request<SteamGridResponse<Grid>>(
|
||||
`/grids/game/${firstGame.id}&nsfw=false&humor=false`,
|
||||
);
|
||||
if (!grids.success)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
statusMessage: "Failed to fetch grids for result.",
|
||||
});
|
||||
|
||||
const results = grids.data.map(
|
||||
(e) =>
|
||||
({
|
||||
id: e.id.toString(),
|
||||
url: e.thumb,
|
||||
type: "image",
|
||||
}) satisfies ImageSearchResult,
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
importImages(
|
||||
images: ImageSearchResult[],
|
||||
taskRunContext?: TaskRunContext,
|
||||
): Promise<string[]> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@ -1,20 +1,23 @@
|
||||
import { applicationSettings } from "../internal/config/application-configuration";
|
||||
import type { MetadataProvider } from "../internal/metadata";
|
||||
import metadataHandler from "../internal/metadata";
|
||||
import { GiantBombProvider } from "../internal/metadata/giantbomb";
|
||||
import { IGDBProvider } from "../internal/metadata/igdb";
|
||||
import { ManualMetadataProvider } from "../internal/metadata/manual";
|
||||
import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki";
|
||||
import type { MetadataProvider } from "../internal/metadata/content";
|
||||
import metadataHandler from "../internal/metadata/content";
|
||||
import type { ImageProvider } from "../internal/metadata/image";
|
||||
import imageHandler from "../internal/metadata/image";
|
||||
import { GiantBombProvider } from "../internal/metadata/providers/giantbomb";
|
||||
import { IGDBProvider } from "../internal/metadata/providers/igdb";
|
||||
import { ManualMetadataProvider } from "../internal/metadata/providers/manual";
|
||||
import { PCGamingWikiProvider } from "../internal/metadata/providers/pcgamingwiki";
|
||||
import { logger } from "~/server/internal/logging";
|
||||
import { SteamGridDB } from "../internal/metadata/providers/steamgriddb";
|
||||
|
||||
export default defineNitroPlugin(async (_nitro) => {
|
||||
const metadataProviders = [
|
||||
GiantBombProvider,
|
||||
PCGamingWikiProvider,
|
||||
IGDBProvider,
|
||||
SteamGridDB,
|
||||
];
|
||||
|
||||
const providers = new Map<string, MetadataProvider>();
|
||||
const providers = new Map<string, MetadataProvider | ImageProvider>();
|
||||
|
||||
for (const provider of metadataProviders) {
|
||||
try {
|
||||
@ -22,37 +25,25 @@ export default defineNitroPlugin(async (_nitro) => {
|
||||
const id = prov.source();
|
||||
providers.set(id, prov);
|
||||
|
||||
logger.info(`enabled metadata provider: ${prov.name()}`);
|
||||
logger.info(`created metadata/image provider: ${prov.name()}`);
|
||||
} catch (e) {
|
||||
logger.warn(`skipping metadata provider setup: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Add providers based on their position in the application settings
|
||||
const configuredProviderList =
|
||||
await applicationSettings.get("metadataProviders");
|
||||
const max = configuredProviderList.length;
|
||||
for (const [index, providerId] of configuredProviderList.entries()) {
|
||||
const max = metadataProviders.length;
|
||||
for (const [index, provider] of providers.entries().map((e, i) => [i, e[1]] as const)) {
|
||||
const priority = max * 2 - index; // Offset by the length --- (max - index) + max
|
||||
const provider = providers.get(providerId);
|
||||
if (!provider) {
|
||||
logger.warn(`failed to add existing metadata provider: ${providerId}`);
|
||||
continue;
|
||||
}
|
||||
metadataHandler.addProvider(provider, priority);
|
||||
providers.delete(providerId);
|
||||
}
|
||||
|
||||
// Add the rest with no position
|
||||
for (const [, provider] of providers.entries()) {
|
||||
metadataHandler.addProvider(provider);
|
||||
if ((provider as MetadataProvider)["search"]) {
|
||||
logger.info(`added ${provider.name()} as metadata provider`);
|
||||
metadataHandler.addProvider(provider as MetadataProvider, priority);
|
||||
}
|
||||
if((provider as ImageProvider)["searchImages"]){
|
||||
logger.info(`added ${provider.name()} as image provider`);
|
||||
imageHandler.addProvider(provider as ImageProvider, priority);
|
||||
}
|
||||
}
|
||||
|
||||
metadataHandler.addProvider(new ManualMetadataProvider(), -1000);
|
||||
|
||||
// Update the applicatonConfig
|
||||
await applicationSettings.set(
|
||||
"metadataProviders",
|
||||
metadataHandler.fetchProviderIdsInOrder(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -2898,7 +2898,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.18.tgz#529f24a88d3ed678d50fd5c07455841fbe8ac95e"
|
||||
integrity sha512-cZy8Dq+uuIXbxCZpuLd2GJdeSO/lIzIspC2WtkqIpje5QyFbvLaI5wZtdUjLHjGZrlVX6GilejatWwVYYRc8tA==
|
||||
|
||||
"@vueuse/core@13.6.0", "@vueuse/core@^13.6.0":
|
||||
"@vueuse/core@13.6.0":
|
||||
version "13.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-13.6.0.tgz#4137f63dc4cef2ff8ae74ee146d6b6070d707878"
|
||||
integrity sha512-DJbD5fV86muVmBgS9QQPddVX7d9hWYswzlf4bIyUD2dj8GC46R1uNClZhVAmsdVts4xb2jwp1PbpuiA50Qee1A==
|
||||
|
||||
Reference in New Issue
Block a user