3 Commits

Author SHA1 Message Date
a287138650 feat: image provider + importer partial backend 2025-08-01 18:33:06 +10:00
545a6b154a Fix #119 (#153) 2025-08-01 16:26:27 +10:00
442f940cc4 Translated using Weblate (English) (#151)
Currently translated at 100.0% (456 of 456 strings)

Translation: Drop/Drop
Translate-URL: http://translate.droposs.org/projects/drop/drop/en/
2025-08-01 14:31:50 +10:00
24 changed files with 379 additions and 147 deletions

View File

@ -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 };

View File

@ -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";

View File

@ -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}",

View File

@ -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",

View File

@ -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",

View File

@ -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));

View File

@ -5,6 +5,7 @@ enum MetadataSource {
IGDB
Metacritic
OpenCritic
SteamGridDB
}
model Game {

View 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;
});

View File

@ -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",

View File

@ -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"]);

View File

@ -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.",

View File

@ -65,6 +65,7 @@ export const systemACLs = [
"game:version:update",
"game:version:delete",
"game:image:new",
"game:image:import",
"game:image:delete",
"company:read",

View 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`

View File

@ -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 {

View File

@ -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;

View 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;

View 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;
}

View File

@ -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,

View File

@ -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;

View File

@ -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 {

View File

@ -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 {

View 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.");
}
}

View File

@ -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(),
);
});

View File

@ -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==