diff --git a/gameyfin/build.gradle.kts b/gameyfin/build.gradle.kts index f271060..92b302c 100644 --- a/gameyfin/build.gradle.kts +++ b/gameyfin/build.gradle.kts @@ -35,7 +35,7 @@ dependencies { implementation("jakarta.validation:jakarta.validation-api:3.1.0") // Kotlin extensions - implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation(kotlin("reflect")) // Reactive implementation("org.springframework.boot:spring-boot-starter-webflux") diff --git a/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx b/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx index c58855c..62ed3d5 100644 --- a/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/LibraryManagement.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from "react"; +import React from "react"; import ConfigFormField from "Frontend/components/administration/ConfigFormField"; import withConfigPage from "Frontend/components/administration/withConfigPage"; import Section from "Frontend/components/general/Section"; @@ -11,16 +11,12 @@ import LibraryCreationModal from "Frontend/components/general/modals/LibraryCrea import LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto"; import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto"; import {useSnapshot} from "valtio/react"; -import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState"; +import {libraryState} from "Frontend/state/LibraryState"; function LibraryManagementLayout({getConfig, formik}: any) { const libraryCreationModal = useDisclosure(); const state = useSnapshot(libraryState); - useEffect(() => { - initializeLibraryState(); - }, []); - async function updateLibrary(library: LibraryUpdateDto) { await LibraryEndpoint.updateLibrary(library); addToast({ diff --git a/gameyfin/src/main/frontend/components/administration/SsoManagement.tsx b/gameyfin/src/main/frontend/components/administration/SsoManagement.tsx index 0a29bad..3d4cab7 100644 --- a/gameyfin/src/main/frontend/components/administration/SsoManagement.tsx +++ b/gameyfin/src/main/frontend/components/administration/SsoManagement.tsx @@ -52,9 +52,11 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
- +
+ +
diff --git a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx index dbdafa2..301a086 100644 --- a/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx +++ b/gameyfin/src/main/frontend/components/general/cards/PluginManagementCard.tsx @@ -23,9 +23,9 @@ import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins import {PluginEndpoint} from "Frontend/generated/endpoints"; import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto"; import PluginConfigValidationResult - from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult"; + from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult"; import PluginConfigValidationResultType - from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResultType"; + from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResultType"; export function PluginManagementCard({plugin}: { plugin: PluginDto }) { const pluginDetailsModal = useDisclosure(); diff --git a/gameyfin/src/main/frontend/components/general/input/PluginConfigFormField.tsx b/gameyfin/src/main/frontend/components/general/input/PluginConfigFormField.tsx new file mode 100644 index 0000000..9deec2a --- /dev/null +++ b/gameyfin/src/main/frontend/components/general/input/PluginConfigFormField.tsx @@ -0,0 +1,58 @@ +import SelectInput from "Frontend/components/general/input/SelectInput"; +import CheckboxInput from "Frontend/components/general/input/CheckboxInput"; +import Input from "Frontend/components/general/input/Input"; +import React from "react"; +import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto"; + +export default function PluginConfigFormField({pluginConfigMetadata, ...props}: any) { + function inputElement(metadata: PluginConfigMetadataDto) { + + if (metadata.allowedValues != null && metadata.allowedValues.length > 0) { + return ( + + ); + } + + switch (metadata.type) { + case "Boolean": + return ( + + ); + case "String": + return ( + + ); + case "Float": + return ( + + ); + case "Int": + return ( + + ); + default: + return
Unsupported type: {metadata.type} for key {metadata.key}
; + } + } + + return inputElement(pluginConfigMetadata!); +} \ No newline at end of file diff --git a/gameyfin/src/main/frontend/components/general/input/SelectInput.tsx b/gameyfin/src/main/frontend/components/general/input/SelectInput.tsx index 85165d4..01d0824 100644 --- a/gameyfin/src/main/frontend/components/general/input/SelectInput.tsx +++ b/gameyfin/src/main/frontend/components/general/input/SelectInput.tsx @@ -6,23 +6,19 @@ const SelectInput = ({label, values, ...props}) => { // @ts-ignore const [field] = useField(props); + const items = values.map((v: string) => ({key: v, label: v})); + return ( -
- -
+ ); } diff --git a/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx b/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx index 981c6e3..f99993b 100644 --- a/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx +++ b/gameyfin/src/main/frontend/components/general/modals/PluginDetailsModal.tsx @@ -1,14 +1,14 @@ import React, {useState} from "react"; import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Tooltip} from "@heroui/react"; import {Form, Formik} from "formik"; -import PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement"; -import Input from "Frontend/components/general/input/Input"; import PluginLogo from "Frontend/components/general/plugin/PluginLogo"; import Markdown from "react-markdown"; import remarkBreaks from "remark-breaks"; import {PluginEndpoint} from "Frontend/generated/endpoints"; import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto"; import {ArrowClockwise} from "@phosphor-icons/react"; +import PluginConfigMetadataDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto"; +import PluginConfigFormField from "Frontend/components/general/input/PluginConfigFormField"; interface PluginDetailsModalProps { plugin: PluginDto; @@ -35,11 +35,28 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi }); } + function getEffectiveConfig(): Record { + const effectiveConfig: Record = {}; + if (!plugin.configMetadata) return effectiveConfig; + + for (const meta of plugin.configMetadata) { + const key = meta.key; + let value = plugin.config?.[key]?.toString(); + if (value == null && meta.default != null) { + value = meta.default.toString(); + } + if (value) { + effectiveConfig[key] = value; + } + } + return effectiveConfig; + } + return ( {(onClose) => ( - { @@ -138,10 +155,11 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi } {(plugin.configMetadata && plugin.configMetadata.length > 0) ? - plugin.configMetadata.map((entry: PluginConfigElement) => ( - + plugin.configMetadata.map((entry: PluginConfigMetadataDto) => ( + )) : "This plugin has no configuration options." } diff --git a/gameyfin/src/main/frontend/index.tsx b/gameyfin/src/main/frontend/index.tsx index 9f6ea4a..0c4daa5 100644 --- a/gameyfin/src/main/frontend/index.tsx +++ b/gameyfin/src/main/frontend/index.tsx @@ -1,7 +1,7 @@ import {createRoot} from 'react-dom/client'; import {StrictMode} from "react"; import {RouterProvider} from "react-router"; -import router from "./routes"; +import {router} from './routes'; const container = document.getElementById('outlet')!; const root = createRoot(container); diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginEndpoint.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginEndpoint.kt index 75a7988..171f4c8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginEndpoint.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginEndpoint.kt @@ -3,7 +3,7 @@ package de.grimsi.gameyfin.core.plugins import com.vaadin.hilla.Endpoint import de.grimsi.gameyfin.core.Role import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult import de.grimsi.gameyfin.users.util.isAdmin import jakarta.annotation.security.PermitAll import jakarta.annotation.security.RolesAllowed diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginService.kt index d9749ee..db87df1 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/PluginService.kt @@ -3,16 +3,16 @@ package de.grimsi.gameyfin.core.plugins import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntry import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntryKey import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository +import de.grimsi.gameyfin.core.plugins.dto.PluginConfigMetadataDto import de.grimsi.gameyfin.core.plugins.dto.PluginDto import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginDescriptor import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry import de.grimsi.gameyfin.core.plugins.management.PluginManagementRepository -import de.grimsi.gameyfin.pluginapi.core.Configurable -import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin -import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.Configurable +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.wrapper.GameyfinPlugin import io.github.oshai.kotlinlogging.KotlinLogging import org.pf4j.ExtensionPoint import org.pf4j.PluginWrapper @@ -96,11 +96,24 @@ class PluginService( return plugin.getLogo() } - fun getConfigMetadata(pluginWrapper: PluginWrapper): List { + fun getConfigMetadata(pluginWrapper: PluginWrapper): List? { log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" } val plugin = pluginWrapper.plugin - if (plugin !is Configurable) return emptyList() - return plugin.configMetadata + + if (plugin !is Configurable) return null + + return plugin.configMetadata.map { meta -> + PluginConfigMetadataDto( + key = meta.key, + type = meta.type.simpleName ?: "Unknown", + label = meta.label, + description = meta.description, + default = meta.default, + isSecret = meta.isSecret, + isRequired = meta.isRequired, + allowedValues = meta.allowedValues?.map { it.toString() } + ) + } } fun getConfig(pluginWrapper: PluginWrapper): Map { diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto.kt new file mode 100644 index 0000000..d98f9ad --- /dev/null +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginConfigMetadataDto.kt @@ -0,0 +1,16 @@ +package de.grimsi.gameyfin.core.plugins.dto + +import com.fasterxml.jackson.annotation.JsonInclude +import java.io.Serializable + +@JsonInclude(JsonInclude.Include.ALWAYS) +class PluginConfigMetadataDto( + val key: String, + val type: String, + val label: String, + val description: String, + val default: Serializable?, + val isSecret: Boolean, + val isRequired: Boolean, + val allowedValues: List? +) \ No newline at end of file diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginDto.kt index 1b569ad..bbf092f 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginDto.kt @@ -1,8 +1,7 @@ package de.grimsi.gameyfin.core.plugins.dto import de.grimsi.gameyfin.core.plugins.management.PluginTrustLevel -import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult import org.pf4j.PluginState data class PluginDto( @@ -17,7 +16,7 @@ data class PluginDto( val url: String? = null, val hasLogo: Boolean, val state: PluginState, - val configMetadata: List? = null, + val configMetadata: List? = null, val config: Map? = null, val configValidation: PluginConfigValidationResult? = null, val priority: Int, diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto.kt index e673bc7..6a84ba8 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/dto/PluginUpdateDto.kt @@ -1,7 +1,7 @@ package de.grimsi.gameyfin.core.plugins.dto import com.fasterxml.jackson.annotation.JsonInclude -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult import org.pf4j.PluginState @JsonInclude(JsonInclude.Include.NON_NULL) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt index 19fc3e3..70800df 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/plugins/management/GameyfinPluginManager.kt @@ -1,9 +1,9 @@ package de.grimsi.gameyfin.core.plugins.management import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository -import de.grimsi.gameyfin.pluginapi.core.Configurable -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResultType +import de.grimsi.gameyfin.pluginapi.core.config.Configurable +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResultType import io.github.oshai.kotlinlogging.KotlinLogging import org.pf4j.* import org.springframework.data.repository.findByIdOrNull @@ -164,7 +164,7 @@ class GameyfinPluginManager( fun restart(pluginId: String) { val plugin = getPlugin(pluginId)?.plugin ?: return stopPlugin(pluginId) - if (plugin is Configurable) plugin.config = getConfig(pluginId) + if (plugin is Configurable) plugin.loadConfig(getConfig(pluginId)) startPlugin(pluginId) } @@ -213,7 +213,7 @@ class GameyfinPluginManager( val plugin = pluginWrapper.plugin if (plugin is Configurable) { val config = getConfig(pluginWrapper.pluginId) - plugin.config = config + plugin.loadConfig(config) } } diff --git a/gameyfin/vite.generated.ts b/gameyfin/vite.generated.ts index 862286c..7667173 100644 --- a/gameyfin/vite.generated.ts +++ b/gameyfin/vite.generated.ts @@ -16,20 +16,19 @@ import settings from './build/vaadin-dev-server-settings.json'; import { AssetInfo, ChunkInfo, - build, defineConfig, mergeConfig, OutputOptions, PluginOption, - InlineConfig, UserConfigFn } from 'vite'; -import { getManifest, type ManifestTransform } from 'workbox-build'; import * as rollup from 'rollup'; import brotli from 'rollup-plugin-brotli'; import checker from 'vite-plugin-checker'; import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js'; +import vaadinI18n from './build/plugins/rollup-plugin-vaadin-i18n/rollup-plugin-vaadin-i18n.js'; +import serviceWorkerPlugin from './build/plugins/vite-plugin-service-worker'; import { createRequire } from 'module'; @@ -41,8 +40,6 @@ import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js // Make `require` compatible with ES modules const require = createRequire(import.meta.url); -const appShellUrl = '.'; - const frontendFolder = path.resolve(__dirname, settings.frontendFolder); const themeFolder = path.resolve(frontendFolder, settings.themeFolder); const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput); @@ -56,6 +53,7 @@ const buildOutputFolder = devBundle ? devBundleFolder : frontendBundleFolder; const statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput); const statsFile = path.resolve(statsFolder, 'stats.json'); const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html'); +const i18nFolder = path.resolve(__dirname, settings.i18nOutput); const nodeModulesFolder = path.resolve(__dirname, 'node_modules'); const webComponentTags = ''; @@ -91,94 +89,6 @@ const target = ['safari15', 'es2022']; console.trace = () => {}; console.debug = () => {}; -function injectManifestToSWPlugin(): rollup.Plugin { - const rewriteManifestIndexHtmlUrl: ManifestTransform = (manifest) => { - const indexEntry = manifest.find((entry) => entry.url === 'index.html'); - if (indexEntry) { - indexEntry.url = appShellUrl; - } - - return { manifest, warnings: [] }; - }; - - return { - name: 'vaadin:inject-manifest-to-sw', - async transform(code, id) { - if (/sw\.(ts|js)$/.test(id)) { - const { manifestEntries } = await getManifest({ - globDirectory: buildOutputFolder, - globPatterns: ['**/*'], - globIgnores: ['**/*.br', 'pwa-icons/**'], - manifestTransforms: [rewriteManifestIndexHtmlUrl], - maximumFileSizeToCacheInBytes: 100 * 1024 * 1024 // 100mb, - }); - - return code.replace('self.__WB_MANIFEST', JSON.stringify(manifestEntries)); - } - } - }; -} - -function buildSWPlugin(opts: { devMode: boolean }): PluginOption { - let buildConfig: InlineConfig; - let buildOutput: rollup.RollupOutput; - const devMode = opts.devMode; - - return { - name: 'vaadin:build-sw', - enforce: 'post', - async configResolved(viteConfig) { - buildConfig = { - base: viteConfig.base, - root: viteConfig.root, - mode: viteConfig.mode, - resolve: viteConfig.resolve, - define: { - ...viteConfig.define, - 'process.env.NODE_ENV': JSON.stringify(viteConfig.mode), - }, - build: { - write: !devMode, - minify: viteConfig.build.minify, - outDir: viteConfig.build.outDir, - target, - sourcemap: viteConfig.command === 'serve' || viteConfig.build.sourcemap, - emptyOutDir: false, - modulePreload: false, - rollupOptions: { - input: { - sw: settings.clientServiceWorkerSource - }, - output: { - exports: 'none', - entryFileNames: 'sw.js', - inlineDynamicImports: true, - }, - }, - }, - }; - }, - async buildStart() { - if (devMode) { - buildOutput = await build(buildConfig) as rollup.RollupOutput; - } - }, - async load(id) { - if (id.endsWith('sw.js')) { - return buildOutput.output[0].code; - } - }, - async closeBundle() { - if (!devMode) { - await build({ - ...buildConfig, - plugins: [injectManifestToSWPlugin(), brotli()] - }); - } - }, - }; -} - function statsExtracterPlugin(): PluginOption { function collectThemeJsonsInFrontend(themeJsonContents: Record, themeName: string) { const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json'); @@ -276,7 +186,7 @@ function statsExtracterPlugin(): PluginOption { const frontendFiles: Record = {}; frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex'); - const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map', '.']; + const projectFileExtensions = ['.js', '.js.map', '.ts', '.ts.map', '.tsx', '.tsx.map', '.css', '.css.map']; const isThemeComponentsResource = (id: string) => id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/')) @@ -753,7 +663,9 @@ export const vaadinConfig: UserConfigFn = (env) => { productionMode && brotli(), devMode && vaadinBundlesPlugin(), devMode && showRecompileReason(), - settings.offlineEnabled && buildSWPlugin({ devMode }), + settings.offlineEnabled && serviceWorkerPlugin({ + srcPath: settings.clientServiceWorkerSource, + }), !devMode && statsExtracterPlugin(), !productionMode && preserveUsageStats(), themePlugin({ devMode }), @@ -795,6 +707,14 @@ export const vaadinConfig: UserConfigFn = (env) => { ].filter(Boolean) } }), + productionMode && vaadinI18n({ + cwd: __dirname, + meta: { + output: { + dir: i18nFolder, + }, + }, + }), { name: 'vaadin:force-remove-html-middleware', configureServer(server) { diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts index 8269de3..f7b212d 100644 --- a/plugin-api/build.gradle.kts +++ b/plugin-api/build.gradle.kts @@ -20,13 +20,4 @@ publishing { dependencies { // PF4J (shared) api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}") - - implementation(kotlin("stdlib")) - - // Test dependencies - testImplementation(kotlin("test")) -} - -tasks.test { - useJUnitPlatform() } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/Configurable.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/Configurable.kt deleted file mode 100644 index af07549..0000000 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/Configurable.kt +++ /dev/null @@ -1,9 +0,0 @@ -package de.grimsi.gameyfin.pluginapi.core - -interface Configurable { - val configMetadata: List - var config: Map - - fun validateConfig(): PluginConfigValidationResult = validateConfig(config) - fun validateConfig(config: Map): PluginConfigValidationResult -} \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/ConfigurableGameyfinPlugin.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/ConfigurableGameyfinPlugin.kt deleted file mode 100644 index fc54003..0000000 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/ConfigurableGameyfinPlugin.kt +++ /dev/null @@ -1,17 +0,0 @@ -package de.grimsi.gameyfin.pluginapi.core - -import org.pf4j.PluginWrapper - -abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable { - - companion object { - lateinit var plugin: ConfigurableGameyfinPlugin - private set - } - - init { - plugin = this - } - - override var config: Map = emptyMap() -} \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement.kt deleted file mode 100644 index 293c8a7..0000000 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.grimsi.gameyfin.pluginapi.core - -data class PluginConfigElement( - val key: String, - val name: String, - val description: String, - val isSecret: Boolean = false -) \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/ConfigMetadata.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/ConfigMetadata.kt new file mode 100644 index 0000000..f92e869 --- /dev/null +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/ConfigMetadata.kt @@ -0,0 +1,21 @@ +package de.grimsi.gameyfin.pluginapi.core.config + +import java.io.Serializable + +typealias PluginConfigMetadata = List> + +data class ConfigMetadata( + val key: String, + val type: Class, + val label: String, + val description: String, + val default: T? = null, + val isSecret: Boolean = false, + val isRequired: Boolean = true, +) { + var allowedValues: List? = null + + init { + allowedValues = type.enumConstants?.toList() + } +} \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/Configurable.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/Configurable.kt new file mode 100644 index 0000000..ba173e8 --- /dev/null +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/Configurable.kt @@ -0,0 +1,15 @@ +package de.grimsi.gameyfin.pluginapi.core.config + +import java.io.Serializable + +interface Configurable { + val configMetadata: PluginConfigMetadata + + fun loadConfig(config: Map) + + fun validateConfig(): PluginConfigValidationResult + fun validateConfig(config: Map): PluginConfigValidationResult + + fun config(key: String): T + fun optionalConfig(key: String): T? +} \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigError.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigError.kt similarity index 58% rename from plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigError.kt rename to plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigError.kt index c461e7c..4b8719c 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigError.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigError.kt @@ -1,3 +1,3 @@ -package de.grimsi.gameyfin.pluginapi.core +package de.grimsi.gameyfin.pluginapi.core.config class PluginConfigError(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt similarity index 92% rename from plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult.kt rename to plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt index 338b5d3..2495116 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt @@ -1,4 +1,4 @@ -package de.grimsi.gameyfin.pluginapi.core +package de.grimsi.gameyfin.pluginapi.core.config data class PluginConfigValidationResult( val result: PluginConfigValidationResultType, diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt new file mode 100644 index 0000000..f7e5632 --- /dev/null +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt @@ -0,0 +1,92 @@ +package de.grimsi.gameyfin.pluginapi.core.wrapper + +import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata +import de.grimsi.gameyfin.pluginapi.core.config.Configurable +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigError +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult +import org.pf4j.PluginWrapper +import java.io.Serializable + +@Suppress("UNCHECKED_CAST") +abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable { + + private var config: Map = emptyMap() + + override fun loadConfig(config: Map) { + this.config = config + } + + override fun validateConfig(): PluginConfigValidationResult = validateConfig(config) + + override fun validateConfig(config: Map): PluginConfigValidationResult { + val errors = mutableMapOf() + + for (meta in configMetadata) { + val value = resolveValue(meta.key, config) + if (meta.isRequired && value == null) { + errors[meta.key] = "${meta.label} is required" + continue + } + if (value != null) { + try { + castConfigValue(meta, value) + } catch (e: PluginConfigError) { + errors[meta.key] = e.message ?: "Invalid value" + } + } + } + + return if (errors.isEmpty()) { + PluginConfigValidationResult.VALID + } else { + PluginConfigValidationResult.INVALID(errors) + } + } + + override fun optionalConfig(key: String): T? { + val meta = resolveMetadata(key) + val value = resolveValue(key) + if (value == null) return null + + return try { + castConfigValue(meta, value) as T + } catch (e: Exception) { + throw PluginConfigError("Failed to cast value for key '$key' to type ${meta.type.simpleName}: ${e.message}") + } + } + + private fun castConfigValue(meta: ConfigMetadata<*>, value: Any): Any? { + val expectedType = meta.type + return if (expectedType.isEnum) { + try { + java.lang.Enum.valueOf(expectedType as Class>, value.toString()) + } catch (_: IllegalArgumentException) { + throw PluginConfigError("Invalid value '${value}', must be one of ${meta.allowedValues!!.joinToString(", ")}") + } + } else { + if (!expectedType.isInstance(value)) { + throw PluginConfigError("Value for key '${meta.key}' is not of type ${expectedType.simpleName}") + } + value + } + } + + override fun config(key: String): T { + val value = optionalConfig(key) + if (value == null) { + throw PluginConfigError("Required configuration key '$key' is missing or has no value") + } + return value + } + + private fun resolveMetadata(key: String): ConfigMetadata<*> { + return configMetadata.find { it.key == key } + ?: throw PluginConfigError("Unknown configuration key: $key") + } + + private fun resolveValue(key: String, configOverride: Map? = null): Serializable? { + val meta = resolveMetadata(key) + val conf = configOverride ?: config + return conf[key] ?: meta.default + } +} \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt similarity index 93% rename from plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt rename to plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt index ed15165..46e1f49 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/GameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt @@ -1,8 +1,9 @@ -package de.grimsi.gameyfin.pluginapi.core +package de.grimsi.gameyfin.pluginapi.core.wrapper import org.pf4j.Plugin import org.pf4j.PluginWrapper +@Suppress("DEPRECATION") abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { companion object { diff --git a/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/CompressionMode.kt b/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/CompressionMode.kt new file mode 100644 index 0000000..c571604 --- /dev/null +++ b/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/CompressionMode.kt @@ -0,0 +1,18 @@ +package de.grimsi.gameyfin.plugins.directdownload + +import de.grimsi.gameyfin.plugins.directdownload.CompressionMode.* +import java.util.zip.Deflater + +enum class CompressionMode { + NONE, + FAST, + BEST; +} + +fun CompressionMode.deflaterLevel(): Int { + return when (this) { + NONE -> Deflater.NO_COMPRESSION + FAST -> Deflater.BEST_SPEED + BEST -> Deflater.BEST_COMPRESSION + } +} \ No newline at end of file diff --git a/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/DirectDownloadPlugin.kt b/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/DirectDownloadPlugin.kt index 470caef..81f0fac 100644 --- a/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/DirectDownloadPlugin.kt +++ b/plugins/directdownload/src/main/kotlin/de/grimsi/gameyfin/plugins/directdownload/DirectDownloadPlugin.kt @@ -1,8 +1,8 @@ package de.grimsi.gameyfin.plugins.directdownload -import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin -import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata +import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigMetadata +import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin import de.grimsi.gameyfin.pluginapi.download.Download import de.grimsi.gameyfin.pluginapi.download.DownloadProvider import de.grimsi.gameyfin.pluginapi.download.FileDownload @@ -14,7 +14,6 @@ import java.io.PipedInputStream import java.io.PipedOutputStream import java.nio.file.* import java.nio.file.attribute.BasicFileAttributes -import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.io.path.exists @@ -24,31 +23,25 @@ import kotlin.io.path.isDirectory class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) { - override val configMetadata: List = listOf( - PluginConfigElement( + companion object { + lateinit var plugin: DirectDownloadPlugin + private set + } + + init { + plugin = this + } + + override val configMetadata: PluginConfigMetadata = listOf( + ConfigMetadata( key = "compressionMode", - name = "Compression mode (\"none\" = default, \"fast\", \"best\")", + type = CompressionMode::class.java, + label = "Compression mode", description = "Higher compression modes are more resource intensive, but save bandwidth", + default = CompressionMode.NONE ) ) - override fun validateConfig(config: Map): PluginConfigValidationResult { - val compressionMode = config["compressionMode"] - - if (compressionMode != null) { - return try { - CompressionMode.valueOf(compressionMode.uppercase()) - PluginConfigValidationResult.VALID - } catch (_: IllegalArgumentException) { - PluginConfigValidationResult.INVALID( - mapOf("compressionMode" to "Invalid compression mode: $compressionMode (must be \"none\", \"fast\", or \"best\")") - ) - } - } - - return PluginConfigValidationResult.VALID - } - @Extension class DirectDownloadProvider : DownloadProvider { override fun download(path: Path): Download { @@ -95,9 +88,8 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin( try { ZipOutputStream(pipeOut).use { zos -> - zos.setLevel(CompressionMode.toDeflaterLevel(plugin.config["compressionMode"]?.let { - CompressionMode.valueOf(it.uppercase()) - } ?: CompressionMode.NONE)) + val compressionMode = plugin.config("compressionMode") + zos.setLevel(compressionMode.deflaterLevel()) Files.walkFileTree(path, object : SimpleFileVisitor() { override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult { @@ -124,21 +116,4 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin( return pipeIn } } -} - - -enum class CompressionMode { - NONE, - FAST, - BEST; - - companion object { - fun toDeflaterLevel(mode: CompressionMode): Int { - return when (mode) { - NONE -> Deflater.NO_COMPRESSION - FAST -> Deflater.BEST_SPEED - BEST -> Deflater.BEST_COMPRESSION - } - } - } } \ No newline at end of file diff --git a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt index bd587ee..eab29fb 100644 --- a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt +++ b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfin/plugins/igdb/IgdbPlugin.kt @@ -5,7 +5,8 @@ import com.api.igdb.exceptions.RequestException import com.api.igdb.request.IGDBWrapper import com.api.igdb.request.TwitchAuthenticator import com.api.igdb.request.games -import de.grimsi.gameyfin.pluginapi.core.* +import de.grimsi.gameyfin.pluginapi.core.config.* +import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import me.xdrop.fuzzywuzzy.FuzzySearch @@ -16,26 +17,35 @@ import proto.Game import java.time.Instant import java.util.concurrent.TimeUnit -class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable { +class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) { - override val configMetadata = listOf( - PluginConfigElement( + override val configMetadata: PluginConfigMetadata = listOf( + ConfigMetadata( key = "clientId", - name = "Twitch client ID", + type = String::class.java, + label = "Twitch client ID", description = "Your Twitch Client ID" ), - PluginConfigElement( + ConfigMetadata( key = "clientSecret", - name = "Twitch client secret", + type = String::class.java, + label = "Twitch client secret", description = "Your Twitch Client Secret", isSecret = true ) ) - override var config: Map = emptyMap() override fun validateConfig(config: Map): PluginConfigValidationResult { + val pluginConfigValidationResult = super.validateConfig(config) + + if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) { + return pluginConfigValidationResult + } + try { - authenticate(config["clientId"], config["clientSecret"]) + val clientIdToValidate = config["clientId"] + val clientSecretToValidate = config["clientSecret"] + authenticate(clientIdToValidate, clientSecretToValidate) return PluginConfigValidationResult.VALID } catch (e: PluginConfigError) { log.error(e.message) @@ -50,7 +60,7 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable override fun start() { try { - authenticate(config["clientId"], config["clientSecret"]) + authenticate(config("clientId"), config("clientSecret")) } catch (e: PluginConfigError) { log.error(e.message) } diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt index 68c6e3c..ad3dd89 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfin/plugins/steam/SteamPlugin.kt @@ -1,6 +1,6 @@ package de.grimsi.gameyfin.plugins.steam -import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin +import de.grimsi.gameyfin.pluginapi.core.wrapper.GameyfinPlugin import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import de.grimsi.gameyfin.plugins.steam.dto.SteamDetailsResultWrapper diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt index 5c8ebf7..0b4b4be 100644 --- a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt +++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt @@ -1,9 +1,7 @@ package de.grimsi.gameyfin.plugins.steamgriddb -import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin -import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement -import de.grimsi.gameyfin.pluginapi.core.PluginConfigError -import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult +import de.grimsi.gameyfin.pluginapi.core.config.* +import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider import de.grimsi.gameyfin.plugins.steamgriddb.api.SteamGridDbApiClient @@ -20,18 +18,26 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra private var client: SteamGridDbApiClient? = null } - override val configMetadata: List = listOf( - PluginConfigElement( + override val configMetadata: PluginConfigMetadata = listOf( + ConfigMetadata( key = "apiKey", - name = "SteamGridDB API key", - description = "Your SteamGridDB API key", + type = String::class.java, + label = "SteamGridDB API key", + description = "The API key can be obtained from your SteamGridDB account preferences", isSecret = true ) ) override fun validateConfig(config: Map): PluginConfigValidationResult { + val pluginConfigValidationResult = super.validateConfig(config) + + if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) { + return pluginConfigValidationResult + } + try { - runBlocking { authenticate(config["apiKey"]) } + val apiKeyToValidate = config["apiKey"] + runBlocking { authenticate(apiKeyToValidate) } return PluginConfigValidationResult.VALID } catch (e: PluginConfigError) { log.error(e.message) @@ -43,7 +49,7 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra override fun start() { try { - runBlocking { authenticate(config["apiKey"]) } + runBlocking { authenticate(config("apiKey")) } } catch (e: PluginConfigError) { log.error(e.message) }