mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 08:15:44 +00:00
Implement type-safe config for plugins in BE and FE
This commit is contained in:
@@ -35,7 +35,7 @@ dependencies {
|
|||||||
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
implementation("jakarta.validation:jakarta.validation-api:3.1.0")
|
||||||
|
|
||||||
// Kotlin extensions
|
// Kotlin extensions
|
||||||
implementation("org.jetbrains.kotlin:kotlin-reflect")
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
// Reactive
|
// Reactive
|
||||||
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
implementation("org.springframework.boot:spring-boot-starter-webflux")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useEffect} from "react";
|
import React from "react";
|
||||||
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
import ConfigFormField from "Frontend/components/administration/ConfigFormField";
|
||||||
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
import withConfigPage from "Frontend/components/administration/withConfigPage";
|
||||||
import Section from "Frontend/components/general/Section";
|
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 LibraryUpdateDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryUpdateDto";
|
||||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||||
import {useSnapshot} from "valtio/react";
|
import {useSnapshot} from "valtio/react";
|
||||||
import {initializeLibraryState, libraryState} from "Frontend/state/LibraryState";
|
import {libraryState} from "Frontend/state/LibraryState";
|
||||||
|
|
||||||
function LibraryManagementLayout({getConfig, formik}: any) {
|
function LibraryManagementLayout({getConfig, formik}: any) {
|
||||||
const libraryCreationModal = useDisclosure();
|
const libraryCreationModal = useDisclosure();
|
||||||
const state = useSnapshot(libraryState);
|
const state = useSnapshot(libraryState);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
initializeLibraryState();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function updateLibrary(library: LibraryUpdateDto) {
|
async function updateLibrary(library: LibraryUpdateDto) {
|
||||||
await LibraryEndpoint.updateLibrary(library);
|
await LibraryEndpoint.updateLibrary(library);
|
||||||
addToast({
|
addToast({
|
||||||
|
|||||||
@@ -52,9 +52,11 @@ function SsoManagementLayout({getConfig, formik, setSaveMessage}: any) {
|
|||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")}
|
<ConfigFormField configElement={getConfig("sso.oidc.auto-register-new-users")}
|
||||||
isDisabled={!formik.values.sso.oidc.enabled}/>
|
isDisabled={!formik.values.sso.oidc.enabled}/>
|
||||||
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
|
<div className="flex flex-row flex-1 justify-center gap-2">
|
||||||
isDisabled={!formik.values.sso.oidc.enabled ||
|
<ConfigFormField configElement={getConfig("sso.oidc.match-existing-users-by")}
|
||||||
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
isDisabled={!formik.values.sso.oidc.enabled ||
|
||||||
|
!formik.values.sso.oidc["auto-register-new-users"]}/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Section title="SSO provider configuration"/>
|
<Section title="SSO provider configuration"/>
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins
|
|||||||
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
||||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
import PluginConfigValidationResult
|
import PluginConfigValidationResult
|
||||||
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult";
|
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/config/PluginConfigValidationResult";
|
||||||
import PluginConfigValidationResultType
|
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 }) {
|
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||||
const pluginDetailsModal = useDisclosure();
|
const pluginDetailsModal = useDisclosure();
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<SelectInput label={metadata.label}
|
||||||
|
name={metadata.key}
|
||||||
|
values={metadata.allowedValues}
|
||||||
|
{...props}/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (metadata.type) {
|
||||||
|
case "Boolean":
|
||||||
|
return (
|
||||||
|
<CheckboxInput label={metadata.label}
|
||||||
|
name={metadata.key}
|
||||||
|
{...props}/>
|
||||||
|
);
|
||||||
|
case "String":
|
||||||
|
return (
|
||||||
|
<Input label={metadata.label}
|
||||||
|
name={metadata.key}
|
||||||
|
type={metadata.secret ? "password" : "text"}
|
||||||
|
isRequired={metadata.required}
|
||||||
|
{...props}/>
|
||||||
|
);
|
||||||
|
case "Float":
|
||||||
|
return (
|
||||||
|
<Input label={metadata.label}
|
||||||
|
name={metadata.key}
|
||||||
|
type="number"
|
||||||
|
isRequired={metadata.required}
|
||||||
|
step="0.1"
|
||||||
|
{...props}/>
|
||||||
|
);
|
||||||
|
case "Int":
|
||||||
|
return (
|
||||||
|
<Input label={metadata.label}
|
||||||
|
name={metadata.key}
|
||||||
|
type="number"
|
||||||
|
isRequired={metadata.required}
|
||||||
|
step="1"
|
||||||
|
{...props}/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <pre>Unsupported type: {metadata.type} for key {metadata.key}</pre>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputElement(pluginConfigMetadata!);
|
||||||
|
}
|
||||||
@@ -6,23 +6,19 @@ const SelectInput = ({label, values, ...props}) => {
|
|||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const [field] = useField(props);
|
const [field] = useField(props);
|
||||||
|
|
||||||
|
const items = values.map((v: string) => ({key: v, label: v}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row flex-1 justify-center gap-2">
|
<Select
|
||||||
<Select
|
{...field}
|
||||||
{...field}
|
{...props}
|
||||||
{...props}
|
label={label}
|
||||||
id={field.name}
|
items={items}
|
||||||
label={label}
|
selectedKeys={[field.value]}
|
||||||
selectedKeys={[field.value]}
|
disallowEmptySelection
|
||||||
disallowEmptySelection
|
>
|
||||||
>
|
{(item: { key: string, label: string }) => <SelectItem>{item.label}</SelectItem>}
|
||||||
{values.map((value: string) => (
|
</Select>
|
||||||
<SelectItem key={value}>
|
|
||||||
{value}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React, {useState} from "react";
|
import React, {useState} from "react";
|
||||||
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Tooltip} from "@heroui/react";
|
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Tooltip} from "@heroui/react";
|
||||||
import {Form, Formik} from "formik";
|
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 PluginLogo from "Frontend/components/general/plugin/PluginLogo";
|
||||||
import Markdown from "react-markdown";
|
import Markdown from "react-markdown";
|
||||||
import remarkBreaks from "remark-breaks";
|
import remarkBreaks from "remark-breaks";
|
||||||
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
import {PluginEndpoint} from "Frontend/generated/endpoints";
|
||||||
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
|
||||||
import {ArrowClockwise} from "@phosphor-icons/react";
|
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 {
|
interface PluginDetailsModalProps {
|
||||||
plugin: PluginDto;
|
plugin: PluginDto;
|
||||||
@@ -35,11 +35,28 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getEffectiveConfig(): Record<string, string> {
|
||||||
|
const effectiveConfig: Record<string, string> = {};
|
||||||
|
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 (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="lg">
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<Formik initialValues={plugin.config}
|
<Formik initialValues={getEffectiveConfig()}
|
||||||
initialErrors={plugin.configValidation?.errors}
|
initialErrors={plugin.configValidation?.errors}
|
||||||
enableReinitialize={true}
|
enableReinitialize={true}
|
||||||
onSubmit={async (values: any) => {
|
onSubmit={async (values: any) => {
|
||||||
@@ -138,10 +155,11 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
|||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
||||||
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
plugin.configMetadata.map((entry: PluginConfigMetadataDto) => (
|
||||||
<Input key={entry.key} name={entry.key} label={entry.name}
|
<PluginConfigFormField
|
||||||
showErrorUntouched={true}
|
key={entry.key}
|
||||||
type={entry.secret ? "password" : "text"}/>
|
pluginConfigMetadata={entry}
|
||||||
|
showErrorUntouched={true}/>
|
||||||
)) : "This plugin has no configuration options."
|
)) : "This plugin has no configuration options."
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {createRoot} from 'react-dom/client';
|
import {createRoot} from 'react-dom/client';
|
||||||
import {StrictMode} from "react";
|
import {StrictMode} from "react";
|
||||||
import {RouterProvider} from "react-router";
|
import {RouterProvider} from "react-router";
|
||||||
import router from "./routes";
|
import {router} from './routes';
|
||||||
|
|
||||||
const container = document.getElementById('outlet')!;
|
const container = document.getElementById('outlet')!;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.core.plugins
|
|||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
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 de.grimsi.gameyfin.users.util.isAdmin
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
|||||||
@@ -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.PluginConfigEntry
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntryKey
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigEntryKey
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
|
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.PluginDto
|
||||||
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||||
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginDescriptor
|
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginDescriptor
|
||||||
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementRepository
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementRepository
|
||||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
import de.grimsi.gameyfin.pluginapi.core.config.Configurable
|
||||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.wrapper.GameyfinPlugin
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.pf4j.ExtensionPoint
|
import org.pf4j.ExtensionPoint
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
@@ -96,11 +96,24 @@ class PluginService(
|
|||||||
return plugin.getLogo()
|
return plugin.getLogo()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigMetadataDto>? {
|
||||||
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
||||||
val plugin = pluginWrapper.plugin
|
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<String, String?> {
|
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||||
|
|||||||
+16
@@ -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<String>?
|
||||||
|
)
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.dto
|
package de.grimsi.gameyfin.core.plugins.dto
|
||||||
|
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginTrustLevel
|
import de.grimsi.gameyfin.core.plugins.management.PluginTrustLevel
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
|
||||||
import org.pf4j.PluginState
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
data class PluginDto(
|
data class PluginDto(
|
||||||
@@ -17,7 +16,7 @@ data class PluginDto(
|
|||||||
val url: String? = null,
|
val url: String? = null,
|
||||||
val hasLogo: Boolean,
|
val hasLogo: Boolean,
|
||||||
val state: PluginState,
|
val state: PluginState,
|
||||||
val configMetadata: List<PluginConfigElement>? = null,
|
val configMetadata: List<PluginConfigMetadataDto>? = null,
|
||||||
val config: Map<String, String?>? = null,
|
val config: Map<String, String?>? = null,
|
||||||
val configValidation: PluginConfigValidationResult? = null,
|
val configValidation: PluginConfigValidationResult? = null,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.dto
|
package de.grimsi.gameyfin.core.plugins.dto
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude
|
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
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
|||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
package de.grimsi.gameyfin.core.plugins.management
|
||||||
|
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigRepository
|
||||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
import de.grimsi.gameyfin.pluginapi.core.config.Configurable
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResult
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResultType
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigValidationResultType
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.pf4j.*
|
import org.pf4j.*
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
@@ -164,7 +164,7 @@ class GameyfinPluginManager(
|
|||||||
fun restart(pluginId: String) {
|
fun restart(pluginId: String) {
|
||||||
val plugin = getPlugin(pluginId)?.plugin ?: return
|
val plugin = getPlugin(pluginId)?.plugin ?: return
|
||||||
stopPlugin(pluginId)
|
stopPlugin(pluginId)
|
||||||
if (plugin is Configurable) plugin.config = getConfig(pluginId)
|
if (plugin is Configurable) plugin.loadConfig(getConfig(pluginId))
|
||||||
startPlugin(pluginId)
|
startPlugin(pluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,7 +213,7 @@ class GameyfinPluginManager(
|
|||||||
val plugin = pluginWrapper.plugin
|
val plugin = pluginWrapper.plugin
|
||||||
if (plugin is Configurable) {
|
if (plugin is Configurable) {
|
||||||
val config = getConfig(pluginWrapper.pluginId)
|
val config = getConfig(pluginWrapper.pluginId)
|
||||||
plugin.config = config
|
plugin.loadConfig(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+15
-95
@@ -16,20 +16,19 @@ import settings from './build/vaadin-dev-server-settings.json';
|
|||||||
import {
|
import {
|
||||||
AssetInfo,
|
AssetInfo,
|
||||||
ChunkInfo,
|
ChunkInfo,
|
||||||
build,
|
|
||||||
defineConfig,
|
defineConfig,
|
||||||
mergeConfig,
|
mergeConfig,
|
||||||
OutputOptions,
|
OutputOptions,
|
||||||
PluginOption,
|
PluginOption,
|
||||||
InlineConfig,
|
|
||||||
UserConfigFn
|
UserConfigFn
|
||||||
} from 'vite';
|
} from 'vite';
|
||||||
import { getManifest, type ManifestTransform } from 'workbox-build';
|
|
||||||
|
|
||||||
import * as rollup from 'rollup';
|
import * as rollup from 'rollup';
|
||||||
import brotli from 'rollup-plugin-brotli';
|
import brotli from 'rollup-plugin-brotli';
|
||||||
import checker from 'vite-plugin-checker';
|
import checker from 'vite-plugin-checker';
|
||||||
import postcssLit from './build/plugins/rollup-plugin-postcss-lit-custom/rollup-plugin-postcss-lit.js';
|
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';
|
import { createRequire } from 'module';
|
||||||
|
|
||||||
@@ -41,8 +40,6 @@ import vitePluginFileSystemRouter from '@vaadin/hilla-file-router/vite-plugin.js
|
|||||||
// Make `require` compatible with ES modules
|
// Make `require` compatible with ES modules
|
||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
const appShellUrl = '.';
|
|
||||||
|
|
||||||
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
const frontendFolder = path.resolve(__dirname, settings.frontendFolder);
|
||||||
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
const themeFolder = path.resolve(frontendFolder, settings.themeFolder);
|
||||||
const frontendBundleFolder = path.resolve(__dirname, settings.frontendBundleOutput);
|
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 statsFolder = path.resolve(__dirname, devBundle ? settings.devBundleStatsOutput : settings.statsOutput);
|
||||||
const statsFile = path.resolve(statsFolder, 'stats.json');
|
const statsFile = path.resolve(statsFolder, 'stats.json');
|
||||||
const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html');
|
const bundleSizeFile = path.resolve(statsFolder, 'bundle-size.html');
|
||||||
|
const i18nFolder = path.resolve(__dirname, settings.i18nOutput);
|
||||||
const nodeModulesFolder = path.resolve(__dirname, 'node_modules');
|
const nodeModulesFolder = path.resolve(__dirname, 'node_modules');
|
||||||
const webComponentTags = '';
|
const webComponentTags = '';
|
||||||
|
|
||||||
@@ -91,94 +89,6 @@ const target = ['safari15', 'es2022'];
|
|||||||
console.trace = () => {};
|
console.trace = () => {};
|
||||||
console.debug = () => {};
|
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 statsExtracterPlugin(): PluginOption {
|
||||||
function collectThemeJsonsInFrontend(themeJsonContents: Record<string, string>, themeName: string) {
|
function collectThemeJsonsInFrontend(themeJsonContents: Record<string, string>, themeName: string) {
|
||||||
const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json');
|
const themeJson = path.resolve(frontendFolder, settings.themeFolder, themeName, 'theme.json');
|
||||||
@@ -276,7 +186,7 @@ function statsExtracterPlugin(): PluginOption {
|
|||||||
const frontendFiles: Record<string, string> = {};
|
const frontendFiles: Record<string, string> = {};
|
||||||
frontendFiles['index.html'] = createHash('sha256').update(customIndexData.replace(/\r\n/g, '\n'), 'utf8').digest('hex');
|
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) =>
|
const isThemeComponentsResource = (id: string) =>
|
||||||
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
id.startsWith(themeOptions.frontendGeneratedFolder.replace(/\\/g, '/'))
|
||||||
@@ -753,7 +663,9 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
|||||||
productionMode && brotli(),
|
productionMode && brotli(),
|
||||||
devMode && vaadinBundlesPlugin(),
|
devMode && vaadinBundlesPlugin(),
|
||||||
devMode && showRecompileReason(),
|
devMode && showRecompileReason(),
|
||||||
settings.offlineEnabled && buildSWPlugin({ devMode }),
|
settings.offlineEnabled && serviceWorkerPlugin({
|
||||||
|
srcPath: settings.clientServiceWorkerSource,
|
||||||
|
}),
|
||||||
!devMode && statsExtracterPlugin(),
|
!devMode && statsExtracterPlugin(),
|
||||||
!productionMode && preserveUsageStats(),
|
!productionMode && preserveUsageStats(),
|
||||||
themePlugin({ devMode }),
|
themePlugin({ devMode }),
|
||||||
@@ -795,6 +707,14 @@ export const vaadinConfig: UserConfigFn = (env) => {
|
|||||||
].filter(Boolean)
|
].filter(Boolean)
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
productionMode && vaadinI18n({
|
||||||
|
cwd: __dirname,
|
||||||
|
meta: {
|
||||||
|
output: {
|
||||||
|
dir: i18nFolder,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
name: 'vaadin:force-remove-html-middleware',
|
name: 'vaadin:force-remove-html-middleware',
|
||||||
configureServer(server) {
|
configureServer(server) {
|
||||||
|
|||||||
@@ -20,13 +20,4 @@ publishing {
|
|||||||
dependencies {
|
dependencies {
|
||||||
// PF4J (shared)
|
// PF4J (shared)
|
||||||
api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}")
|
api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}")
|
||||||
|
|
||||||
implementation(kotlin("stdlib"))
|
|
||||||
|
|
||||||
// Test dependencies
|
|
||||||
testImplementation(kotlin("test"))
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.pluginapi.core
|
|
||||||
|
|
||||||
interface Configurable {
|
|
||||||
val configMetadata: List<PluginConfigElement>
|
|
||||||
var config: Map<String, String?>
|
|
||||||
|
|
||||||
fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
|
|
||||||
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
|
|
||||||
}
|
|
||||||
-17
@@ -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<String, String?> = emptyMap()
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package de.grimsi.gameyfin.pluginapi.core.config
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
typealias PluginConfigMetadata = List<ConfigMetadata<*>>
|
||||||
|
|
||||||
|
data class ConfigMetadata<T : Serializable>(
|
||||||
|
val key: String,
|
||||||
|
val type: Class<T>,
|
||||||
|
val label: String,
|
||||||
|
val description: String,
|
||||||
|
val default: T? = null,
|
||||||
|
val isSecret: Boolean = false,
|
||||||
|
val isRequired: Boolean = true,
|
||||||
|
) {
|
||||||
|
var allowedValues: List<T>? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
allowedValues = type.enumConstants?.toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package de.grimsi.gameyfin.pluginapi.core.config
|
||||||
|
|
||||||
|
import java.io.Serializable
|
||||||
|
|
||||||
|
interface Configurable {
|
||||||
|
val configMetadata: PluginConfigMetadata
|
||||||
|
|
||||||
|
fun loadConfig(config: Map<String, String?>)
|
||||||
|
|
||||||
|
fun validateConfig(): PluginConfigValidationResult
|
||||||
|
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
|
||||||
|
|
||||||
|
fun <T : Serializable> config(key: String): T
|
||||||
|
fun <T : Serializable> optionalConfig(key: String): T?
|
||||||
|
}
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
package de.grimsi.gameyfin.pluginapi.core
|
package de.grimsi.gameyfin.pluginapi.core.config
|
||||||
|
|
||||||
class PluginConfigError(message: String) : RuntimeException(message)
|
class PluginConfigError(message: String) : RuntimeException(message)
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package de.grimsi.gameyfin.pluginapi.core
|
package de.grimsi.gameyfin.pluginapi.core.config
|
||||||
|
|
||||||
data class PluginConfigValidationResult(
|
data class PluginConfigValidationResult(
|
||||||
val result: PluginConfigValidationResultType,
|
val result: PluginConfigValidationResultType,
|
||||||
+92
@@ -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<String, String?> = emptyMap()
|
||||||
|
|
||||||
|
override fun loadConfig(config: Map<String, String?>) {
|
||||||
|
this.config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
|
||||||
|
|
||||||
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
|
val errors = mutableMapOf<String, String>()
|
||||||
|
|
||||||
|
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 <T : Serializable> 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<out Enum<*>>, 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 <T : Serializable> config(key: String): T {
|
||||||
|
val value = optionalConfig<T>(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<String, Serializable?>? = null): Serializable? {
|
||||||
|
val meta = resolveMetadata(key)
|
||||||
|
val conf = configOverride ?: config
|
||||||
|
return conf[key] ?: meta.default
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
package de.grimsi.gameyfin.pluginapi.core
|
package de.grimsi.gameyfin.pluginapi.core.wrapper
|
||||||
|
|
||||||
import org.pf4j.Plugin
|
import org.pf4j.Plugin
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
|
abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
+18
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
-44
@@ -1,8 +1,8 @@
|
|||||||
package de.grimsi.gameyfin.plugins.directdownload
|
package de.grimsi.gameyfin.plugins.directdownload
|
||||||
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.config.ConfigMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.config.PluginConfigMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||||
import de.grimsi.gameyfin.pluginapi.download.Download
|
import de.grimsi.gameyfin.pluginapi.download.Download
|
||||||
import de.grimsi.gameyfin.pluginapi.download.DownloadProvider
|
import de.grimsi.gameyfin.pluginapi.download.DownloadProvider
|
||||||
import de.grimsi.gameyfin.pluginapi.download.FileDownload
|
import de.grimsi.gameyfin.pluginapi.download.FileDownload
|
||||||
@@ -14,7 +14,6 @@ import java.io.PipedInputStream
|
|||||||
import java.io.PipedOutputStream
|
import java.io.PipedOutputStream
|
||||||
import java.nio.file.*
|
import java.nio.file.*
|
||||||
import java.nio.file.attribute.BasicFileAttributes
|
import java.nio.file.attribute.BasicFileAttributes
|
||||||
import java.util.zip.Deflater
|
|
||||||
import java.util.zip.ZipEntry
|
import java.util.zip.ZipEntry
|
||||||
import java.util.zip.ZipOutputStream
|
import java.util.zip.ZipOutputStream
|
||||||
import kotlin.io.path.exists
|
import kotlin.io.path.exists
|
||||||
@@ -24,31 +23,25 @@ import kotlin.io.path.isDirectory
|
|||||||
|
|
||||||
class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||||
|
|
||||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
companion object {
|
||||||
PluginConfigElement(
|
lateinit var plugin: DirectDownloadPlugin
|
||||||
|
private set
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
plugin = this
|
||||||
|
}
|
||||||
|
|
||||||
|
override val configMetadata: PluginConfigMetadata = listOf(
|
||||||
|
ConfigMetadata(
|
||||||
key = "compressionMode",
|
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",
|
description = "Higher compression modes are more resource intensive, but save bandwidth",
|
||||||
|
default = CompressionMode.NONE
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): 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
|
@Extension
|
||||||
class DirectDownloadProvider : DownloadProvider {
|
class DirectDownloadProvider : DownloadProvider {
|
||||||
override fun download(path: Path): Download {
|
override fun download(path: Path): Download {
|
||||||
@@ -95,9 +88,8 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
|
|||||||
try {
|
try {
|
||||||
ZipOutputStream(pipeOut).use { zos ->
|
ZipOutputStream(pipeOut).use { zos ->
|
||||||
|
|
||||||
zos.setLevel(CompressionMode.toDeflaterLevel(plugin.config["compressionMode"]?.let {
|
val compressionMode = plugin.config<CompressionMode>("compressionMode")
|
||||||
CompressionMode.valueOf(it.uppercase())
|
zos.setLevel(compressionMode.deflaterLevel())
|
||||||
} ?: CompressionMode.NONE))
|
|
||||||
|
|
||||||
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
|
Files.walkFileTree(path, object : SimpleFileVisitor<Path>() {
|
||||||
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
|
||||||
@@ -125,20 +117,3 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,8 @@ import com.api.igdb.exceptions.RequestException
|
|||||||
import com.api.igdb.request.IGDBWrapper
|
import com.api.igdb.request.IGDBWrapper
|
||||||
import com.api.igdb.request.TwitchAuthenticator
|
import com.api.igdb.request.TwitchAuthenticator
|
||||||
import com.api.igdb.request.games
|
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.GameMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||||
@@ -16,26 +17,35 @@ import proto.Game
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable {
|
class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
||||||
|
|
||||||
override val configMetadata = listOf(
|
override val configMetadata: PluginConfigMetadata = listOf(
|
||||||
PluginConfigElement(
|
ConfigMetadata(
|
||||||
key = "clientId",
|
key = "clientId",
|
||||||
name = "Twitch client ID",
|
type = String::class.java,
|
||||||
|
label = "Twitch client ID",
|
||||||
description = "Your Twitch Client ID"
|
description = "Your Twitch Client ID"
|
||||||
),
|
),
|
||||||
PluginConfigElement(
|
ConfigMetadata(
|
||||||
key = "clientSecret",
|
key = "clientSecret",
|
||||||
name = "Twitch client secret",
|
type = String::class.java,
|
||||||
|
label = "Twitch client secret",
|
||||||
description = "Your Twitch Client Secret",
|
description = "Your Twitch Client Secret",
|
||||||
isSecret = true
|
isSecret = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
override var config: Map<String, String?> = emptyMap()
|
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
|
val pluginConfigValidationResult = super.validateConfig(config)
|
||||||
|
|
||||||
|
if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) {
|
||||||
|
return pluginConfigValidationResult
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
authenticate(config["clientId"], config["clientSecret"])
|
val clientIdToValidate = config["clientId"]
|
||||||
|
val clientSecretToValidate = config["clientSecret"]
|
||||||
|
authenticate(clientIdToValidate, clientSecretToValidate)
|
||||||
return PluginConfigValidationResult.VALID
|
return PluginConfigValidationResult.VALID
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
@@ -50,7 +60,7 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
|
|||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
try {
|
try {
|
||||||
authenticate(config["clientId"], config["clientSecret"])
|
authenticate(config("clientId"), config("clientSecret"))
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package de.grimsi.gameyfin.plugins.steam
|
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.GameMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import de.grimsi.gameyfin.plugins.steam.dto.SteamDetailsResultWrapper
|
import de.grimsi.gameyfin.plugins.steam.dto.SteamDetailsResultWrapper
|
||||||
|
|||||||
+16
-10
@@ -1,9 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.plugins.steamgriddb
|
package de.grimsi.gameyfin.plugins.steamgriddb
|
||||||
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.config.*
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.wrapper.ConfigurableGameyfinPlugin
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import de.grimsi.gameyfin.plugins.steamgriddb.api.SteamGridDbApiClient
|
import de.grimsi.gameyfin.plugins.steamgriddb.api.SteamGridDbApiClient
|
||||||
@@ -20,18 +18,26 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
private var client: SteamGridDbApiClient? = null
|
private var client: SteamGridDbApiClient? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
override val configMetadata: PluginConfigMetadata = listOf(
|
||||||
PluginConfigElement(
|
ConfigMetadata(
|
||||||
key = "apiKey",
|
key = "apiKey",
|
||||||
name = "SteamGridDB API key",
|
type = String::class.java,
|
||||||
description = "Your SteamGridDB API key",
|
label = "SteamGridDB API key",
|
||||||
|
description = "The API key can be obtained from your SteamGridDB account preferences",
|
||||||
isSecret = true
|
isSecret = true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
|
val pluginConfigValidationResult = super.validateConfig(config)
|
||||||
|
|
||||||
|
if (pluginConfigValidationResult.result == PluginConfigValidationResultType.INVALID) {
|
||||||
|
return pluginConfigValidationResult
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
runBlocking { authenticate(config["apiKey"]) }
|
val apiKeyToValidate = config["apiKey"]
|
||||||
|
runBlocking { authenticate(apiKeyToValidate) }
|
||||||
return PluginConfigValidationResult.VALID
|
return PluginConfigValidationResult.VALID
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
@@ -43,7 +49,7 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
try {
|
try {
|
||||||
runBlocking { authenticate(config["apiKey"]) }
|
runBlocking { authenticate(config("apiKey")) }
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user