mirror of
https://github.com/Drop-OSS/drop.git
synced 2025-11-09 20:12:10 +10:00
feat: image provider + importer partial backend
This commit is contained in:
@ -16,7 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { GameMetadataSearchResult } from "~/server/internal/metadata/types";
|
import type { GameMetadataSearchResult } from "~/server/internal/metadata/content/types";
|
||||||
|
|
||||||
const { game } = defineProps<{
|
const { game } = defineProps<{
|
||||||
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
game: Omit<GameMetadataSearchResult, "year"> & { sourceName?: string };
|
||||||
|
|||||||
@ -171,7 +171,7 @@ import {
|
|||||||
ListboxOption,
|
ListboxOption,
|
||||||
ListboxOptions,
|
ListboxOptions,
|
||||||
} from "@headlessui/vue";
|
} 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 { FetchError } from "ofetch";
|
||||||
import type { SerializeObject } from "nitropack";
|
import type { SerializeObject } from "nitropack";
|
||||||
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
import { XCircleIcon } from "@heroicons/vue/24/solid";
|
||||||
|
|||||||
@ -309,7 +309,7 @@ import {
|
|||||||
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
import { XCircleIcon } from "@heroicons/vue/16/solid";
|
||||||
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/vue/20/solid";
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/vue/24/outline";
|
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({
|
definePageMeta({
|
||||||
layout: "admin",
|
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
|
IGDB
|
||||||
Metacritic
|
Metacritic
|
||||||
OpenCritic
|
OpenCritic
|
||||||
|
SteamGridDB
|
||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
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 { readDropValidatedBody, throwingArktype } from "~/server/arktype";
|
||||||
import aclManager from "~/server/internal/acls";
|
import aclManager from "~/server/internal/acls";
|
||||||
import libraryManager from "~/server/internal/library";
|
import libraryManager from "~/server/internal/library";
|
||||||
import metadataHandler from "~/server/internal/metadata";
|
import metadataHandler from "~/server/internal/metadata/content";
|
||||||
|
|
||||||
const ImportGameBody = type({
|
const ImportGameBody = type({
|
||||||
library: "string",
|
library: "string",
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import aclManager from "~/server/internal/acls";
|
import aclManager from "~/server/internal/acls";
|
||||||
import metadataHandler from "~/server/internal/metadata";
|
import metadataHandler from "~/server/internal/metadata/content";
|
||||||
|
|
||||||
export default defineEventHandler(async (h3) => {
|
export default defineEventHandler(async (h3) => {
|
||||||
const allowed = await aclManager.allowSystemACL(h3, ["import:game:read"]);
|
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:update": "Update the version order on a game.",
|
||||||
"game:version:delete": "Delete a version for a game.",
|
"game:version:delete": "Delete a version for a game.",
|
||||||
"game:image:new": "Upload an image 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.",
|
"game:image:delete": "Delete an image for a game.",
|
||||||
|
|
||||||
"company:read": "Fetch companies.",
|
"company:read": "Fetch companies.",
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export const systemACLs = [
|
|||||||
"game:version:update",
|
"game:version:update",
|
||||||
"game:version:delete",
|
"game:version:delete",
|
||||||
"game:image:new",
|
"game:image:new",
|
||||||
|
"game:image:import",
|
||||||
"game:image:delete",
|
"game:image:delete",
|
||||||
|
|
||||||
"company:read",
|
"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 type { Prisma } from "~/prisma/client/client";
|
||||||
import { MetadataSource } from "~/prisma/client/enums";
|
import { MetadataSource } from "~/prisma/client/enums";
|
||||||
import prisma from "../db/database";
|
import prisma from "../../db/database";
|
||||||
import type {
|
import type {
|
||||||
_FetchGameMetadataParams,
|
_FetchGameMetadataParams,
|
||||||
_FetchCompanyMetadataParams,
|
_FetchCompanyMetadataParams,
|
||||||
@ -10,15 +15,15 @@ import type {
|
|||||||
CompanyMetadata,
|
CompanyMetadata,
|
||||||
GameMetadataRating,
|
GameMetadataRating,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { ObjectTransactionalHandler } from "../objects/transactional";
|
import { ObjectTransactionalHandler } from "../../objects/transactional";
|
||||||
import { PriorityListIndexed } from "../utils/prioritylist";
|
import { PriorityListIndexed } from "../../utils/prioritylist";
|
||||||
import { systemConfig } from "../config/sys-conf";
|
import { systemConfig } from "../../config/sys-conf";
|
||||||
import type { TaskRunContext } from "../tasks";
|
import type { TaskRunContext } from "../../tasks";
|
||||||
import taskHandler, { wrapTaskContext } from "../tasks";
|
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 libraryManager from "../library";
|
import libraryManager 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 {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { Company, GameRating } from "~/prisma/client";
|
import type { Company, GameRating } from "~/prisma/client";
|
||||||
import type { TransactionDataType } from "../objects/transactional";
|
import type { TransactionDataType } from "../../objects/transactional";
|
||||||
import type { ObjectReference } from "../objects/objectHandler";
|
import type { ObjectReference } from "../../objects/objectHandler";
|
||||||
|
|
||||||
export interface GameMetadataSearchResult {
|
export interface GameMetadataSearchResult {
|
||||||
id: string;
|
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 type { CompanyModel } from "~/prisma/client/models";
|
||||||
import { MetadataSource } from "~/prisma/client/enums";
|
import { MetadataSource } from "~/prisma/client/enums";
|
||||||
import type { MetadataProvider } from ".";
|
import type { MetadataProvider } from "../content";
|
||||||
import { MissingMetadataProviderConfig } from ".";
|
import { MissingMetadataProviderConfig } from "../content";
|
||||||
import type {
|
import type {
|
||||||
GameMetadataSearchResult,
|
GameMetadataSearchResult,
|
||||||
_FetchGameMetadataParams,
|
_FetchGameMetadataParams,
|
||||||
@ -9,11 +9,11 @@ import type {
|
|||||||
_FetchCompanyMetadataParams,
|
_FetchCompanyMetadataParams,
|
||||||
CompanyMetadata,
|
CompanyMetadata,
|
||||||
GameMetadataRating,
|
GameMetadataRating,
|
||||||
} from "./types";
|
} from "../content/types";
|
||||||
import axios, { type AxiosRequestConfig } from "axios";
|
import axios, { type AxiosRequestConfig } from "axios";
|
||||||
import TurndownService from "turndown";
|
import TurndownService from "turndown";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import type { TaskRunContext } from "../tasks";
|
import type { TaskRunContext } from "../../tasks";
|
||||||
|
|
||||||
interface GiantBombResponseType<T> {
|
interface GiantBombResponseType<T> {
|
||||||
error: "OK" | string;
|
error: "OK" | string;
|
||||||
@ -277,6 +277,7 @@ export class GiantBombProvider implements MetadataProvider {
|
|||||||
|
|
||||||
return metadata;
|
return metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchCompany({
|
async fetchCompany({
|
||||||
query,
|
query,
|
||||||
createObject,
|
createObject,
|
||||||
@ -1,19 +1,19 @@
|
|||||||
import type { CompanyModel } from "~/prisma/client/models";
|
import type { CompanyModel } from "~/prisma/client/models";
|
||||||
import { MetadataSource } from "~/prisma/client/enums";
|
import { MetadataSource } from "~/prisma/client/enums";
|
||||||
import type { MetadataProvider } from ".";
|
import type { MetadataProvider } from "../content";
|
||||||
import { MissingMetadataProviderConfig } from ".";
|
import { MissingMetadataProviderConfig } from "../content";
|
||||||
import type {
|
import type {
|
||||||
GameMetadataSearchResult,
|
GameMetadataSearchResult,
|
||||||
_FetchGameMetadataParams,
|
_FetchGameMetadataParams,
|
||||||
GameMetadata,
|
GameMetadata,
|
||||||
_FetchCompanyMetadataParams,
|
_FetchCompanyMetadataParams,
|
||||||
CompanyMetadata,
|
CompanyMetadata,
|
||||||
} from "./types";
|
} from "../content/types";
|
||||||
import type { AxiosRequestConfig } from "axios";
|
import type { AxiosRequestConfig } from "axios";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import * as jdenticon from "jdenticon";
|
import * as jdenticon from "jdenticon";
|
||||||
import type { TaskRunContext } from "../tasks";
|
import type { TaskRunContext } from "../../tasks";
|
||||||
import { logger } from "~/server/internal/logging";
|
import { logger } from "~/server/internal/logging";
|
||||||
|
|
||||||
type IGDBID = number;
|
type IGDBID = number;
|
||||||
@ -1,11 +1,11 @@
|
|||||||
import { MetadataSource } from "~/prisma/client/enums";
|
import { MetadataSource } from "~/prisma/client/enums";
|
||||||
import type { MetadataProvider } from ".";
|
import type { MetadataProvider } from "../content";
|
||||||
import type {
|
import type {
|
||||||
_FetchGameMetadataParams,
|
_FetchGameMetadataParams,
|
||||||
GameMetadata,
|
GameMetadata,
|
||||||
_FetchCompanyMetadataParams,
|
_FetchCompanyMetadataParams,
|
||||||
CompanyMetadata,
|
CompanyMetadata,
|
||||||
} from "./types";
|
} from "../content/types";
|
||||||
import * as jdenticon from "jdenticon";
|
import * as jdenticon from "jdenticon";
|
||||||
|
|
||||||
export class ManualMetadataProvider implements MetadataProvider {
|
export class ManualMetadataProvider implements MetadataProvider {
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import type { CompanyModel } from "~/prisma/client/models";
|
import type { CompanyModel } from "~/prisma/client/models";
|
||||||
import { MetadataSource } from "~/prisma/client/enums";
|
import { MetadataSource } from "~/prisma/client/enums";
|
||||||
import type { MetadataProvider } from ".";
|
import type { MetadataProvider } from "../content";
|
||||||
import type {
|
import type {
|
||||||
GameMetadataSearchResult,
|
GameMetadataSearchResult,
|
||||||
_FetchGameMetadataParams,
|
_FetchGameMetadataParams,
|
||||||
@ -8,14 +8,14 @@ import type {
|
|||||||
_FetchCompanyMetadataParams,
|
_FetchCompanyMetadataParams,
|
||||||
CompanyMetadata,
|
CompanyMetadata,
|
||||||
GameMetadataRating,
|
GameMetadataRating,
|
||||||
} from "./types";
|
} from "../content/types";
|
||||||
import type { AxiosRequestConfig } from "axios";
|
import type { AxiosRequestConfig } from "axios";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import * as jdenticon from "jdenticon";
|
import * as jdenticon from "jdenticon";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import * as cheerio from "cheerio";
|
import * as cheerio from "cheerio";
|
||||||
import { type } from "arktype";
|
import { type } from "arktype";
|
||||||
import type { TaskRunContext } from "../tasks";
|
import type { TaskRunContext } from "../../tasks";
|
||||||
import { logger } from "~/server/internal/logging";
|
import { logger } from "~/server/internal/logging";
|
||||||
|
|
||||||
interface PCGamingWikiParseRawPage {
|
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/content";
|
||||||
import type { MetadataProvider } from "../internal/metadata";
|
import metadataHandler from "../internal/metadata/content";
|
||||||
import metadataHandler from "../internal/metadata";
|
import type { ImageProvider } from "../internal/metadata/image";
|
||||||
import { GiantBombProvider } from "../internal/metadata/giantbomb";
|
import imageHandler from "../internal/metadata/image";
|
||||||
import { IGDBProvider } from "../internal/metadata/igdb";
|
import { GiantBombProvider } from "../internal/metadata/providers/giantbomb";
|
||||||
import { ManualMetadataProvider } from "../internal/metadata/manual";
|
import { IGDBProvider } from "../internal/metadata/providers/igdb";
|
||||||
import { PCGamingWikiProvider } from "../internal/metadata/pcgamingwiki";
|
import { ManualMetadataProvider } from "../internal/metadata/providers/manual";
|
||||||
|
import { PCGamingWikiProvider } from "../internal/metadata/providers/pcgamingwiki";
|
||||||
import { logger } from "~/server/internal/logging";
|
import { logger } from "~/server/internal/logging";
|
||||||
|
import { SteamGridDB } from "../internal/metadata/providers/steamgriddb";
|
||||||
|
|
||||||
export default defineNitroPlugin(async (_nitro) => {
|
export default defineNitroPlugin(async (_nitro) => {
|
||||||
const metadataProviders = [
|
const metadataProviders = [
|
||||||
GiantBombProvider,
|
GiantBombProvider,
|
||||||
PCGamingWikiProvider,
|
PCGamingWikiProvider,
|
||||||
IGDBProvider,
|
IGDBProvider,
|
||||||
|
SteamGridDB,
|
||||||
];
|
];
|
||||||
|
|
||||||
const providers = new Map<string, MetadataProvider>();
|
const providers = new Map<string, MetadataProvider | ImageProvider>();
|
||||||
|
|
||||||
for (const provider of metadataProviders) {
|
for (const provider of metadataProviders) {
|
||||||
try {
|
try {
|
||||||
@ -22,37 +25,25 @@ export default defineNitroPlugin(async (_nitro) => {
|
|||||||
const id = prov.source();
|
const id = prov.source();
|
||||||
providers.set(id, prov);
|
providers.set(id, prov);
|
||||||
|
|
||||||
logger.info(`enabled metadata provider: ${prov.name()}`);
|
logger.info(`created metadata/image provider: ${prov.name()}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`skipping metadata provider setup: ${e}`);
|
logger.warn(`skipping metadata provider setup: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add providers based on their position in the application settings
|
const max = metadataProviders.length;
|
||||||
const configuredProviderList =
|
for (const [index, provider] of providers.entries().map((e, i) => [i, e[1]] as const)) {
|
||||||
await applicationSettings.get("metadataProviders");
|
|
||||||
const max = configuredProviderList.length;
|
|
||||||
for (const [index, providerId] of configuredProviderList.entries()) {
|
|
||||||
const priority = max * 2 - index; // Offset by the length --- (max - index) + max
|
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
|
if ((provider as MetadataProvider)["search"]) {
|
||||||
for (const [, provider] of providers.entries()) {
|
logger.info(`added ${provider.name()} as metadata provider`);
|
||||||
metadataHandler.addProvider(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);
|
metadataHandler.addProvider(new ManualMetadataProvider(), -1000);
|
||||||
|
|
||||||
// Update the applicatonConfig
|
|
||||||
await applicationSettings.set(
|
|
||||||
"metadataProviders",
|
|
||||||
metadataHandler.fetchProviderIdsInOrder(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user