mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
Extend plugin config validation
Cache validation results Show field-level errors in UI Enable manual revalidation Move PluginConfigService and PluginManagementService into PluginService
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<component name="ProjectRunConfigurationManager">
|
<component name="ProjectRunConfigurationManager">
|
||||||
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080">
|
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080">
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
</component>
|
</component>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {Button, Card, Chip, Skeleton, Tooltip, useDisclosure} from "@heroui/react";
|
import {Button, Card, Chip, Tooltip, useDisclosure} from "@heroui/react";
|
||||||
import {
|
import {
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
IconContext,
|
IconContext,
|
||||||
@@ -16,31 +16,24 @@ import {
|
|||||||
XCircle
|
XCircle
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import PluginState from "Frontend/generated/org/pf4j/PluginState";
|
import PluginState from "Frontend/generated/org/pf4j/PluginState";
|
||||||
import React, {ReactNode, useEffect, useState} from "react";
|
import React, {ReactNode} from "react";
|
||||||
import PluginDetailsModal from "Frontend/components/general/modals/PluginDetailsModal";
|
import PluginDetailsModal from "Frontend/components/general/modals/PluginDetailsModal";
|
||||||
import PluginLogo from "Frontend/components/general/PluginLogo";
|
import PluginLogo from "Frontend/components/general/PluginLogo";
|
||||||
import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel";
|
import PluginTrustLevel from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginTrustLevel";
|
||||||
import PluginConfigValidationResult
|
|
||||||
from "Frontend/generated/de/grimsi/gameyfin/core/plugins/config/PluginConfigValidationResult";
|
|
||||||
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
|
||||||
|
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResult";
|
||||||
|
import PluginConfigValidationResultType
|
||||||
|
from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigValidationResultType";
|
||||||
|
|
||||||
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
||||||
const pluginDetailsModal = useDisclosure();
|
const pluginDetailsModal = useDisclosure();
|
||||||
const [configValidationResult, setConfigValidationResult] = useState<PluginConfigValidationResult | undefined>(undefined);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
PluginEndpoint.validatePluginConfig(plugin.id).then((response: PluginConfigValidationResult) => {
|
|
||||||
setConfigValidationResult(response);
|
|
||||||
});
|
|
||||||
}, [pluginDetailsModal.isOpen, plugin.state]);
|
|
||||||
|
|
||||||
function borderColor(state: PluginState | undefined, trustLevel: PluginTrustLevel | undefined): "success" | "warning" | "danger" | "default" {
|
function borderColor(state: PluginState | undefined, trustLevel: PluginTrustLevel | undefined): "success" | "warning" | "danger" | "default" {
|
||||||
if (trustLevel === PluginTrustLevel.UNTRUSTED) return "danger";
|
if (trustLevel === PluginTrustLevel.UNTRUSTED) return "danger";
|
||||||
|
|
||||||
if (isDisabled(state)) return "warning";
|
if (isDisabled(state)) return "warning";
|
||||||
if (configValidationResult === undefined) return "default";
|
|
||||||
if (!configValidationResult) return "danger";
|
|
||||||
return stateToColor(state);
|
return stateToColor(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,14 +69,14 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configValidationResultToChip(validationResult: PluginConfigValidationResult | undefined): ReactNode {
|
function configValidationResultToChip(validationResult: PluginConfigValidationResult | undefined): ReactNode {
|
||||||
switch (validationResult) {
|
switch (validationResult?.result) {
|
||||||
case PluginConfigValidationResult.VALID:
|
case PluginConfigValidationResultType.VALID:
|
||||||
return <Tooltip content="Config valid" placement="bottom" color="foreground">
|
return <Tooltip content="Config valid" placement="bottom" color="foreground">
|
||||||
<Chip size="sm" radius="sm" className="text-xs" color="success">
|
<Chip size="sm" radius="sm" className="text-xs" color="success">
|
||||||
<CheckCircle/>
|
<CheckCircle/>
|
||||||
</Chip>
|
</Chip>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
case PluginConfigValidationResult.INVALID:
|
case PluginConfigValidationResultType.INVALID:
|
||||||
return <Tooltip content="Config invalid" placement="bottom" color="foreground">
|
return <Tooltip content="Config invalid" placement="bottom" color="foreground">
|
||||||
<Chip size="sm" radius="sm" className="text-xs" color="danger">
|
<Chip size="sm" radius="sm" className="text-xs" color="danger">
|
||||||
<WarningCircle/>
|
<WarningCircle/>
|
||||||
@@ -173,10 +166,7 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
|
|||||||
{stateToIcon(plugin.state)}
|
{stateToIcon(plugin.state)}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Chip>
|
</Chip>
|
||||||
{configValidationResult === undefined ?
|
{configValidationResultToChip(plugin.configValidation)}
|
||||||
<Skeleton className="rounded-md h-6 w-9"/> :
|
|
||||||
configValidationResultToChip(configValidationResult)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
|
|||||||
import {XCircle} from "@phosphor-icons/react";
|
import {XCircle} from "@phosphor-icons/react";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const Input = ({label, ...props}) => {
|
const Input = ({label, showErrorUntouched = false, ...props}) => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const [field, meta] = useField(props);
|
const [field, meta] = useField(props);
|
||||||
|
|
||||||
@@ -15,10 +15,10 @@ const Input = ({label, ...props}) => {
|
|||||||
{...field}
|
{...field}
|
||||||
id={label}
|
id={label}
|
||||||
label={label}
|
label={label}
|
||||||
isInvalid={meta.touched && !!meta.error}
|
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
|
||||||
/>
|
/>
|
||||||
<div className="min-h-6 text-danger">
|
<div className="min-h-6 text-danger">
|
||||||
{meta.touched && meta.error && meta.error.trim().length > 0 && (
|
{(meta.touched || showErrorUntouched) && meta.error && meta.error.trim().length > 0 && (
|
||||||
<SmallInfoField icon={XCircle} message={meta.error}/>
|
<SmallInfoField icon={XCircle} message={meta.error}/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React, {useState} from "react";
|
||||||
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} 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 PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement";
|
||||||
import Input from "Frontend/components/general/input/Input";
|
import Input from "Frontend/components/general/input/Input";
|
||||||
@@ -8,6 +8,7 @@ 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";
|
||||||
|
|
||||||
interface PluginDetailsModalProps {
|
interface PluginDetailsModalProps {
|
||||||
plugin: PluginDto;
|
plugin: PluginDto;
|
||||||
@@ -16,6 +17,8 @@ interface PluginDetailsModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
|
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
|
||||||
|
const [configValidated, setConfigValidated] = useState<boolean>(false);
|
||||||
|
|
||||||
async function saveConfig(values: Record<string, string>) {
|
async function saveConfig(values: Record<string, string>) {
|
||||||
await PluginEndpoint.updateConfig(plugin.id, values);
|
await PluginEndpoint.updateConfig(plugin.id, values);
|
||||||
addToast({
|
addToast({
|
||||||
@@ -30,13 +33,14 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
|||||||
<ModalContent>
|
<ModalContent>
|
||||||
{(onClose) => (
|
{(onClose) => (
|
||||||
<Formik initialValues={plugin.config}
|
<Formik initialValues={plugin.config}
|
||||||
|
initialErrors={plugin.configValidation?.errors}
|
||||||
enableReinitialize={true}
|
enableReinitialize={true}
|
||||||
onSubmit={async (values: any) => {
|
onSubmit={async (values: any) => {
|
||||||
await saveConfig(values);
|
await saveConfig(values);
|
||||||
onClose();
|
onClose();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(formik: { isSubmitting: any; }) => (
|
{(formik: any) => (
|
||||||
<Form>
|
<Form>
|
||||||
<ModalHeader className="flex flex-col gap-1">
|
<ModalHeader className="flex flex-col gap-1">
|
||||||
Plugin configuration for {plugin.name}
|
Plugin configuration for {plugin.name}
|
||||||
@@ -88,10 +92,33 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
|
|||||||
>{plugin.description}</Markdown>
|
>{plugin.description}</Markdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="text-l font-bold mt-4">Configuration</h4>
|
<div className="flex flex-row items-center mt-4 gap-2">
|
||||||
|
<h4 className="text-l font-bold">Configuration</h4>
|
||||||
|
<div className="flex-1"/>
|
||||||
|
{(plugin.configMetadata && plugin.configMetadata.length > 0) && <>
|
||||||
|
{configValidated &&
|
||||||
|
<p className="text-small text-success">Validation successful</p>}
|
||||||
|
<Tooltip content="Re-validate configuration" placement="bottom"
|
||||||
|
color="foreground">
|
||||||
|
<Button isIconOnly variant="light" size="sm"
|
||||||
|
onPress={async () => {
|
||||||
|
setConfigValidated(false);
|
||||||
|
let result = await PluginEndpoint.validateNewConfig(plugin.id, formik.values)
|
||||||
|
if (result.errors) formik.setErrors(result.errors);
|
||||||
|
else {
|
||||||
|
setConfigValidated(true);
|
||||||
|
setTimeout(() => setConfigValidated(false), 5000);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<ArrowClockwise/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
</>}
|
||||||
|
</div>
|
||||||
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
{(plugin.configMetadata && plugin.configMetadata.length > 0) ?
|
||||||
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
plugin.configMetadata.map((entry: PluginConfigElement) => (
|
||||||
<Input key={entry.key} name={entry.key} label={entry.name}
|
<Input key={entry.key} name={entry.key} label={entry.name}
|
||||||
|
showErrorUntouched={true}
|
||||||
type={entry.secret ? "password" : "text"}/>
|
type={entry.secret ? "password" : "text"}/>
|
||||||
)) : "This plugin has no configuration options."
|
)) : "This plugin has no configuration options."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import de.grimsi.gameyfin.config.dto.ConfigEntryDto
|
|||||||
import de.grimsi.gameyfin.config.dto.ConfigUpdateDto
|
import de.grimsi.gameyfin.config.dto.ConfigUpdateDto
|
||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.users.util.isAdmin
|
import de.grimsi.gameyfin.users.util.isAdmin
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import jakarta.annotation.security.PermitAll
|
import jakarta.annotation.security.PermitAll
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
import org.springframework.security.core.context.SecurityContextHolder
|
import org.springframework.security.core.context.SecurityContextHolder
|
||||||
@@ -16,6 +17,9 @@ import reactor.core.publisher.Flux
|
|||||||
class ConfigEndpoint(
|
class ConfigEndpoint(
|
||||||
private val configService: ConfigService
|
private val configService: ConfigService
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
val log = KotlinLogging.logger { }
|
||||||
|
}
|
||||||
|
|
||||||
/** CRUD endpoints for admins **/
|
/** CRUD endpoints for admins **/
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,19 @@ import java.io.Serializable
|
|||||||
class ConfigService(
|
class ConfigService(
|
||||||
private val appConfigRepository: ConfigRepository
|
private val appConfigRepository: ConfigRepository
|
||||||
) {
|
) {
|
||||||
private val log = KotlinLogging.logger {}
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
}
|
||||||
|
|
||||||
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
|
||||||
|
|
||||||
fun subscribe(): Flux<ConfigUpdateDto> {
|
fun subscribe(): Flux<ConfigUpdateDto> {
|
||||||
|
log.debug { "New subscription for configUpdates (#${configUpdates.currentSubscriberCount()})" }
|
||||||
return configUpdates.asFlux()
|
return configUpdates.asFlux()
|
||||||
|
.doOnSubscribe { log.debug { "Subscriber added to configUpdates [${configUpdates.currentSubscriberCount()}]" } }
|
||||||
|
.doFinally {
|
||||||
|
log.debug { "Subscriber removed from configUpdates with signal type $it [${configUpdates.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ 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.config.PluginConfigValidationResult
|
|
||||||
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
import de.grimsi.gameyfin.core.plugins.dto.PluginUpdateDto
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
|
import de.grimsi.gameyfin.core.plugins.management.PluginService
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.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
|
||||||
@@ -15,28 +15,31 @@ import reactor.core.publisher.Flux
|
|||||||
@Endpoint
|
@Endpoint
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
class PluginEndpoint(
|
class PluginEndpoint(
|
||||||
private val pluginManagementService: PluginManagementService
|
private val pluginService: PluginService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
fun subscribe(): Flux<PluginUpdateDto> {
|
fun subscribe(): Flux<PluginUpdateDto> {
|
||||||
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
|
||||||
return if (user.isAdmin()) pluginManagementService.subscribe()
|
return if (user.isAdmin()) pluginService.subscribe()
|
||||||
else Flux.empty()
|
else Flux.empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll() = pluginManagementService.getAll()
|
fun getAll() = pluginService.getAll()
|
||||||
|
|
||||||
fun enablePlugin(pluginId: String) = pluginManagementService.enablePlugin(pluginId)
|
fun enablePlugin(pluginId: String) = pluginService.enablePlugin(pluginId)
|
||||||
|
|
||||||
fun disablePlugin(pluginId: String) = pluginManagementService.disablePlugin(pluginId)
|
fun disablePlugin(pluginId: String) = pluginService.disablePlugin(pluginId)
|
||||||
|
|
||||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
fun setPluginPriorities(pluginPriorities: Map<String, Int>) =
|
||||||
pluginManagementService.setPluginPriorities(pluginPriorities)
|
pluginService.setPluginPriorities(pluginPriorities)
|
||||||
|
|
||||||
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult =
|
||||||
pluginManagementService.validatePluginConfig(pluginId)
|
pluginService.validatePluginConfig(pluginId, true)
|
||||||
|
|
||||||
|
fun validateNewConfig(pluginId: String, config: Map<String, String>): PluginConfigValidationResult =
|
||||||
|
pluginService.validatePluginConfig(pluginId, config)
|
||||||
|
|
||||||
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) =
|
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) =
|
||||||
pluginManagementService.updateConfig(pluginId, updatedConfig)
|
pluginService.updateConfig(pluginId, updatedConfig)
|
||||||
}
|
}
|
||||||
+2
-20
@@ -1,10 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.config
|
package de.grimsi.gameyfin.core.plugins.config
|
||||||
|
|
||||||
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
import de.grimsi.gameyfin.core.plugins.management.GameyfinPluginManager
|
||||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.pf4j.PluginWrapper
|
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -13,24 +10,9 @@ class PluginConfigService(
|
|||||||
private val pluginManager: GameyfinPluginManager
|
private val pluginManager: GameyfinPluginManager
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val log = KotlinLogging.logger {}
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
|
||||||
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
|
||||||
val plugin = pluginWrapper.plugin
|
|
||||||
if (plugin !is Configurable) return emptyList()
|
|
||||||
return plugin.configMetadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
|
||||||
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
|
||||||
return pluginConfigRepository.findAllById_PluginId(pluginWrapper.pluginId).associate { it.id.key to it.value }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfig(pluginId: String, config: Map<String, String>) {
|
|
||||||
log.debug { "Setting config entries for plugin $pluginId" }
|
|
||||||
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
|
|
||||||
pluginConfigRepository.saveAll(entries)
|
|
||||||
pluginManager.restart(pluginId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.config
|
|
||||||
|
|
||||||
enum class PluginConfigValidationResult {
|
|
||||||
VALID,
|
|
||||||
INVALID,
|
|
||||||
UNKNWOWN,
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,7 @@ 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.PluginConfigElement
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||||
import org.pf4j.PluginState
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
data class PluginDto(
|
data class PluginDto(
|
||||||
@@ -18,6 +19,7 @@ data class PluginDto(
|
|||||||
val state: PluginState,
|
val state: PluginState,
|
||||||
val configMetadata: List<PluginConfigElement>? = null,
|
val configMetadata: List<PluginConfigElement>? = null,
|
||||||
val config: Map<String, String?>? = null,
|
val config: Map<String, String?>? = null,
|
||||||
|
val configValidation: PluginConfigValidationResult? = null,
|
||||||
val priority: Int,
|
val priority: Int,
|
||||||
val trustLevel: PluginTrustLevel,
|
val trustLevel: PluginTrustLevel,
|
||||||
)
|
)
|
||||||
@@ -1,6 +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 org.pf4j.PluginState
|
import org.pf4j.PluginState
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
@@ -8,5 +9,6 @@ data class PluginUpdateDto(
|
|||||||
val id: String,
|
val id: String,
|
||||||
val state: PluginState? = null,
|
val state: PluginState? = null,
|
||||||
val config: Map<String, String?>? = null,
|
val config: Map<String, String?>? = null,
|
||||||
|
val configValidation: PluginConfigValidationResult? = null,
|
||||||
val priority: Int? = null
|
val priority: Int? = null
|
||||||
)
|
)
|
||||||
|
|||||||
+23
-8
@@ -1,8 +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.core.plugins.config.PluginConfigValidationResult
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
import de.grimsi.gameyfin.pluginapi.core.Configurable
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.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
|
||||||
@@ -34,9 +35,9 @@ class GameyfinPluginManager(
|
|||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
private val publicKey: PublicKey = loadPluginSignaturePublicKey()
|
private val publicKey: PublicKey = loadPluginSignaturePublicKey()
|
||||||
|
|
||||||
// This took me way too long to figure out...
|
|
||||||
// But I learned a lot about Kotlin and Java interoperability in the process
|
|
||||||
init {
|
init {
|
||||||
|
// This took me way too long to figure out...
|
||||||
|
// But I learned a lot about Kotlin and Java interoperability in the process
|
||||||
pluginStatusProvider = dbPluginStatusProvider
|
pluginStatusProvider = dbPluginStatusProvider
|
||||||
|
|
||||||
pluginStateListeners.add { event ->
|
pluginStateListeners.add { event ->
|
||||||
@@ -143,7 +144,7 @@ class GameyfinPluginManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate config before starting the plugin
|
// Validate config before starting the plugin
|
||||||
if (validatePluginConfig(pluginId) == PluginConfigValidationResult.INVALID) {
|
if (validatePluginConfig(pluginId).result == PluginConfigValidationResultType.INVALID) {
|
||||||
log.warn { "Plugin $pluginId has invalid configuration" }
|
log.warn { "Plugin $pluginId has invalid configuration" }
|
||||||
|
|
||||||
val pluginWrapper = getPlugin(pluginId)
|
val pluginWrapper = getPlugin(pluginId)
|
||||||
@@ -171,14 +172,28 @@ class GameyfinPluginManager(
|
|||||||
val plugin = try {
|
val plugin = try {
|
||||||
getPlugin(pluginId)?.plugin
|
getPlugin(pluginId)?.plugin
|
||||||
} catch (_: NoClassDefFoundError) {
|
} catch (_: NoClassDefFoundError) {
|
||||||
return PluginConfigValidationResult.UNKNWOWN
|
return PluginConfigValidationResult(PluginConfigValidationResultType.UNKNWOWN)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (plugin !is Configurable || plugin.validateConfig()) {
|
if (plugin !is Configurable) {
|
||||||
return PluginConfigValidationResult.VALID
|
return PluginConfigValidationResult(PluginConfigValidationResultType.VALID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PluginConfigValidationResult.INVALID
|
return plugin.validateConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validatePluginConfig(pluginId: String, configToValidate: Map<String, String>): PluginConfigValidationResult {
|
||||||
|
val plugin = try {
|
||||||
|
getPlugin(pluginId)?.plugin
|
||||||
|
} catch (_: NoClassDefFoundError) {
|
||||||
|
return PluginConfigValidationResult(PluginConfigValidationResultType.UNKNWOWN)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin !is Configurable) {
|
||||||
|
return PluginConfigValidationResult(PluginConfigValidationResultType.VALID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugin.validateConfig(configToValidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getExtensionTypeClasses(pluginId: String): List<Class<ExtensionPoint>> {
|
fun getExtensionTypeClasses(pluginId: String): List<Class<ExtensionPoint>> {
|
||||||
|
|||||||
+70
-16
@@ -1,10 +1,16 @@
|
|||||||
package de.grimsi.gameyfin.core.plugins.management
|
package de.grimsi.gameyfin.core.plugins.management
|
||||||
|
|
||||||
|
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.config.PluginConfigService
|
import de.grimsi.gameyfin.core.plugins.config.PluginConfigService
|
||||||
import de.grimsi.gameyfin.core.plugins.config.PluginConfigValidationResult
|
|
||||||
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.pluginapi.core.Configurable
|
||||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import org.pf4j.ExtensionPoint
|
import org.pf4j.ExtensionPoint
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
@@ -13,21 +19,32 @@ import reactor.core.publisher.Flux
|
|||||||
import reactor.core.publisher.Sinks
|
import reactor.core.publisher.Sinks
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
class PluginManagementService(
|
class PluginService(
|
||||||
private val pluginManager: GameyfinPluginManager,
|
private val pluginManager: GameyfinPluginManager,
|
||||||
private val pluginConfigService: PluginConfigService,
|
private val pluginConfigService: PluginConfigService,
|
||||||
private val pluginManagementRepository: PluginManagementRepository,
|
private val pluginManagementRepository: PluginManagementRepository,
|
||||||
|
private val pluginConfigRepository: PluginConfigRepository
|
||||||
) {
|
) {
|
||||||
|
companion object {
|
||||||
|
private val log = KotlinLogging.logger {}
|
||||||
|
}
|
||||||
|
|
||||||
private val pluginUpdates = Sinks.many().multicast().onBackpressureBuffer<PluginUpdateDto>()
|
private val pluginUpdates = Sinks.many().multicast().onBackpressureBuffer<PluginUpdateDto>()
|
||||||
|
private val pluginConfigValidationCache = mutableMapOf<String, PluginConfigValidationResult>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
pluginManager.addPluginStateListener {
|
pluginManager.addPluginStateListener { event ->
|
||||||
pluginUpdates.tryEmitNext(PluginUpdateDto(id = it.plugin.pluginId, state = it.pluginState))
|
pluginUpdates.tryEmitNext(PluginUpdateDto(id = event.plugin.pluginId, state = event.pluginState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribe(): Flux<PluginUpdateDto> {
|
fun subscribe(): Flux<PluginUpdateDto> {
|
||||||
|
log.debug { "New subscription for pluginUpdates" }
|
||||||
return pluginUpdates.asFlux()
|
return pluginUpdates.asFlux()
|
||||||
|
.doOnSubscribe { log.debug { "Subscriber added to pluginUpdates [${pluginUpdates.currentSubscriberCount()}]" } }
|
||||||
|
.doFinally {
|
||||||
|
log.debug { "Subscriber removed from pluginUpdates with signal type $it [${pluginUpdates.currentSubscriberCount()}]" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSupportedPluginTypes(): List<String> {
|
fun getSupportedPluginTypes(): List<String> {
|
||||||
@@ -59,16 +76,6 @@ class PluginManagementService(
|
|||||||
pluginManager.disablePlugin(pluginId)
|
pluginManager.disablePlugin(pluginId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun validatePluginConfig(pluginId: String): PluginConfigValidationResult {
|
|
||||||
return pluginManager.validatePluginConfig(pluginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateConfig(pluginId: String, updatedConfig: Map<String, String>) {
|
|
||||||
pluginConfigService.updateConfig(pluginId, updatedConfig)
|
|
||||||
val update = PluginUpdateDto(pluginId, config = updatedConfig)
|
|
||||||
pluginUpdates.tryEmitNext(update)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setPluginPriorities(pluginPriorities: Map<String, Int>) {
|
fun setPluginPriorities(pluginPriorities: Map<String, Int>) {
|
||||||
pluginPriorities.forEach { (pluginId, priority) ->
|
pluginPriorities.forEach { (pluginId, priority) ->
|
||||||
val pluginManagementEntry = getPluginManagementEntry(pluginId)
|
val pluginManagementEntry = getPluginManagementEntry(pluginId)
|
||||||
@@ -82,6 +89,52 @@ class PluginManagementService(
|
|||||||
return plugin.getLogo()
|
return plugin.getLogo()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getConfigMetadata(pluginWrapper: PluginWrapper): List<PluginConfigElement> {
|
||||||
|
log.debug { "Getting config metadata for plugin ${pluginWrapper.pluginId}" }
|
||||||
|
val plugin = pluginWrapper.plugin
|
||||||
|
if (plugin !is Configurable) return emptyList()
|
||||||
|
return plugin.configMetadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getConfig(pluginWrapper: PluginWrapper): Map<String, String?> {
|
||||||
|
log.debug { "Getting config for plugin ${pluginWrapper.pluginId}" }
|
||||||
|
return pluginConfigRepository.findAllById_PluginId(pluginWrapper.pluginId).associate { it.id.key to it.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateConfig(pluginId: String, config: Map<String, String>) {
|
||||||
|
log.debug { "Setting config entries for plugin $pluginId" }
|
||||||
|
val entries = config.map { PluginConfigEntry(PluginConfigEntryKey(pluginId, it.key), it.value) }
|
||||||
|
|
||||||
|
// Persist new config
|
||||||
|
pluginConfigRepository.saveAll(entries)
|
||||||
|
|
||||||
|
// Restart plugin to apply new config
|
||||||
|
pluginManager.restart(pluginId)
|
||||||
|
|
||||||
|
// Validate new config
|
||||||
|
val result = validatePluginConfig(pluginId, true)
|
||||||
|
|
||||||
|
// Emit update event
|
||||||
|
val update = PluginUpdateDto(pluginId, config = config, configValidation = result)
|
||||||
|
pluginUpdates.tryEmitNext(update)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validatePluginConfig(pluginId: String, forceRevalidation: Boolean = false): PluginConfigValidationResult {
|
||||||
|
if (forceRevalidation || !pluginConfigValidationCache.containsKey(pluginId)) {
|
||||||
|
log.debug { "Validating config for plugin $pluginId" }
|
||||||
|
val result = pluginManager.validatePluginConfig(pluginId)
|
||||||
|
pluginConfigValidationCache[pluginId] = result
|
||||||
|
return result
|
||||||
|
} else {
|
||||||
|
log.debug { "Using cached validation result for plugin $pluginId" }
|
||||||
|
return pluginConfigValidationCache[pluginId]!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun validatePluginConfig(pluginId: String, configToValidate: Map<String, String>): PluginConfigValidationResult {
|
||||||
|
return pluginManager.validatePluginConfig(pluginId, configToValidate)
|
||||||
|
}
|
||||||
|
|
||||||
private fun toDto(pluginWrapper: PluginWrapper): PluginDto {
|
private fun toDto(pluginWrapper: PluginWrapper): PluginDto {
|
||||||
val pluginManagementEntry = getPluginManagementEntry(pluginWrapper.pluginId)
|
val pluginManagementEntry = getPluginManagementEntry(pluginWrapper.pluginId)
|
||||||
|
|
||||||
@@ -108,8 +161,9 @@ class PluginManagementService(
|
|||||||
url = descriptor.pluginUrl,
|
url = descriptor.pluginUrl,
|
||||||
hasLogo = hasLogo,
|
hasLogo = hasLogo,
|
||||||
state = pluginWrapper.pluginState,
|
state = pluginWrapper.pluginState,
|
||||||
configMetadata = pluginConfigService.getConfigMetadata(pluginWrapper),
|
configMetadata = getConfigMetadata(pluginWrapper),
|
||||||
config = pluginConfigService.getConfig(pluginWrapper),
|
config = getConfig(pluginWrapper),
|
||||||
|
configValidation = validatePluginConfig(descriptor.pluginId),
|
||||||
priority = pluginManagementEntry.priority,
|
priority = pluginManagementEntry.priority,
|
||||||
trustLevel = pluginManagementEntry.trustLevel
|
trustLevel = pluginManagementEntry.trustLevel
|
||||||
)
|
)
|
||||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.games
|
|||||||
import de.grimsi.gameyfin.core.alphaNumeric
|
import de.grimsi.gameyfin.core.alphaNumeric
|
||||||
import de.grimsi.gameyfin.core.filterValuesNotNull
|
import de.grimsi.gameyfin.core.filterValuesNotNull
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
import de.grimsi.gameyfin.core.plugins.management.PluginManagementEntry
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
|
import de.grimsi.gameyfin.core.plugins.management.PluginService
|
||||||
import de.grimsi.gameyfin.core.replaceRomanNumerals
|
import de.grimsi.gameyfin.core.replaceRomanNumerals
|
||||||
import de.grimsi.gameyfin.games.dto.GameDto
|
import de.grimsi.gameyfin.games.dto.GameDto
|
||||||
import de.grimsi.gameyfin.games.dto.GameMetadataDto
|
import de.grimsi.gameyfin.games.dto.GameMetadataDto
|
||||||
@@ -28,7 +28,7 @@ import java.nio.file.Path
|
|||||||
@Service
|
@Service
|
||||||
class GameService(
|
class GameService(
|
||||||
private val pluginManager: PluginManager,
|
private val pluginManager: PluginManager,
|
||||||
private val pluginManagementService: PluginManagementService,
|
private val pluginService: PluginService,
|
||||||
private val gameRepository: GameRepository,
|
private val gameRepository: GameRepository,
|
||||||
private val companyService: CompanyService
|
private val companyService: CompanyService
|
||||||
) {
|
) {
|
||||||
@@ -179,11 +179,11 @@ class GameService(
|
|||||||
|
|
||||||
// Cache the plugin management entries for each provider
|
// Cache the plugin management entries for each provider
|
||||||
val providerToManagementEntry =
|
val providerToManagementEntry =
|
||||||
results.entries.associate { it.key to pluginManagementService.getPluginManagementEntry(it.key.javaClass) }
|
results.entries.associate { it.key to pluginService.getPluginManagementEntry(it.key.javaClass) }
|
||||||
|
|
||||||
// Sort results by plugin priority
|
// Sort results by plugin priority
|
||||||
val sortedResults = results.entries.sortedByDescending {
|
val sortedResults = results.entries.sortedByDescending {
|
||||||
pluginManagementService.getPluginManagementEntry(it.key.javaClass).priority
|
pluginService.getPluginManagementEntry(it.key.javaClass).priority
|
||||||
}
|
}
|
||||||
|
|
||||||
sortedResults.forEach { (provider, metadata) ->
|
sortedResults.forEach { (provider, metadata) ->
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.media
|
|||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
import de.grimsi.gameyfin.core.Utils
|
import de.grimsi.gameyfin.core.Utils
|
||||||
import de.grimsi.gameyfin.core.annotations.DynamicPublicAccess
|
import de.grimsi.gameyfin.core.annotations.DynamicPublicAccess
|
||||||
import de.grimsi.gameyfin.core.plugins.management.PluginManagementService
|
import de.grimsi.gameyfin.core.plugins.management.PluginService
|
||||||
import de.grimsi.gameyfin.games.entities.Image
|
import de.grimsi.gameyfin.games.entities.Image
|
||||||
import de.grimsi.gameyfin.games.entities.ImageType
|
import de.grimsi.gameyfin.games.entities.ImageType
|
||||||
import de.grimsi.gameyfin.users.UserService
|
import de.grimsi.gameyfin.users.UserService
|
||||||
@@ -24,7 +24,7 @@ import org.springframework.web.multipart.MultipartFile
|
|||||||
class ImageEndpoint(
|
class ImageEndpoint(
|
||||||
private val imageService: ImageService,
|
private val imageService: ImageService,
|
||||||
private val userService: UserService,
|
private val userService: UserService,
|
||||||
private val pluginManagementService: PluginManagementService
|
private val pluginService: PluginService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@GetMapping("/screenshot/{id}")
|
@GetMapping("/screenshot/{id}")
|
||||||
@@ -39,7 +39,7 @@ class ImageEndpoint(
|
|||||||
|
|
||||||
@GetMapping("/plugins/{id}/logo")
|
@GetMapping("/plugins/{id}/logo")
|
||||||
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<ByteArrayResource>? {
|
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<ByteArrayResource>? {
|
||||||
val logo = pluginManagementService.getLogo(pluginId)
|
val logo = pluginService.getLogo(pluginId)
|
||||||
return Utils.inputStreamToResponseEntity(logo)
|
return Utils.inputStreamToResponseEntity(logo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ interface Configurable {
|
|||||||
val configMetadata: List<PluginConfigElement>
|
val configMetadata: List<PluginConfigElement>
|
||||||
var config: Map<String, String?>
|
var config: Map<String, String?>
|
||||||
|
|
||||||
fun validateConfig(): Boolean = validateConfig(config)
|
fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
|
||||||
fun validateConfig(config: Map<String, String?>): Boolean
|
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
|
||||||
}
|
}
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
package de.grimsi.gameyfin.pluginapi.core
|
||||||
|
|
||||||
|
data class PluginConfigValidationResult(
|
||||||
|
val result: PluginConfigValidationResultType,
|
||||||
|
val errors: Map<String, String>? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val VALID = PluginConfigValidationResult(PluginConfigValidationResultType.VALID)
|
||||||
|
val UNKNOWN = PluginConfigValidationResult(PluginConfigValidationResultType.UNKNWOWN)
|
||||||
|
fun INVALID(errors: Map<String, String>): PluginConfigValidationResult {
|
||||||
|
return PluginConfigValidationResult(PluginConfigValidationResultType.INVALID, errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PluginConfigValidationResultType {
|
||||||
|
VALID,
|
||||||
|
INVALID,
|
||||||
|
UNKNWOWN,
|
||||||
|
}
|
||||||
+15
-9
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.plugins.directdownload
|
|||||||
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||||
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
|
||||||
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
|
||||||
@@ -26,21 +27,26 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
|
|||||||
override val configMetadata: List<PluginConfigElement> = listOf(
|
override val configMetadata: List<PluginConfigElement> = listOf(
|
||||||
PluginConfigElement(
|
PluginConfigElement(
|
||||||
key = "compressionMode",
|
key = "compressionMode",
|
||||||
name = "Compression mode for generated ZIP files (\"none\", \"fast\", \"best\")",
|
name = "Compression mode for generated ZIP files (\"none\" = default, \"fast\", \"best\")",
|
||||||
description = "Higher compression uses more CPU but saves bandwidth",
|
description = "Higher compression uses more CPU but saves bandwidth",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
return config["compressionMode"]?.let {
|
val compressionMode = config["compressionMode"]
|
||||||
try {
|
|
||||||
CompressionMode.valueOf(it.uppercase())
|
if (compressionMode != null) {
|
||||||
true
|
return try {
|
||||||
|
CompressionMode.valueOf(compressionMode.uppercase())
|
||||||
|
PluginConfigValidationResult.VALID
|
||||||
} catch (_: IllegalArgumentException) {
|
} catch (_: IllegalArgumentException) {
|
||||||
log.error("Invalid compression mode: $it")
|
PluginConfigValidationResult.INVALID(
|
||||||
false
|
mapOf("compressionMode" to "Invalid compression mode: $compressionMode (must be \"none\", \"fast\", or \"best\")")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} ?: true
|
}
|
||||||
|
|
||||||
|
return PluginConfigValidationResult.VALID
|
||||||
}
|
}
|
||||||
|
|
||||||
@Extension
|
@Extension
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Plugin-Version: 1.0.0-alpha1
|
Plugin-Version: 1.0.0-alpha2
|
||||||
Plugin-Class: de.grimsi.gameyfin.plugins.directdownload.DirectDownloadPlugin
|
Plugin-Class: de.grimsi.gameyfin.plugins.directdownload.DirectDownloadPlugin
|
||||||
Plugin-Id: de.grimsi.gameyfin.directdownload
|
Plugin-Id: de.grimsi.gameyfin.directdownload
|
||||||
Plugin-Name: Direct Download
|
Plugin-Name: Direct Download
|
||||||
|
|||||||
@@ -5,10 +5,7 @@ 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.Configurable
|
import de.grimsi.gameyfin.pluginapi.core.*
|
||||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
|
||||||
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
|
||||||
@@ -36,19 +33,24 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
|
|||||||
)
|
)
|
||||||
override var config: Map<String, String?> = emptyMap()
|
override var config: Map<String, String?> = emptyMap()
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
try {
|
try {
|
||||||
authenticate()
|
authenticate(config["clientId"], config["clientSecret"])
|
||||||
return true
|
return PluginConfigValidationResult.VALID
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
return false
|
return PluginConfigValidationResult.INVALID(
|
||||||
|
mapOf(
|
||||||
|
"clientId" to "Invalid client ID and/or client secret",
|
||||||
|
"clientSecret" to "Invalid client ID and/or client secret"
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
try {
|
try {
|
||||||
authenticate()
|
authenticate(config["clientId"], config["clientSecret"])
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
}
|
}
|
||||||
@@ -58,11 +60,11 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
|
|||||||
log.debug("IgdbPlugin.stop()")
|
log.debug("IgdbPlugin.stop()")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun authenticate() {
|
private fun authenticate(clientId: String? = null, clientSecret: String? = null) {
|
||||||
log.debug("Authenticating on Twitch API...")
|
log.debug("Authenticating on Twitch API...")
|
||||||
|
|
||||||
val clientId: String = config["clientId"] ?: throw PluginConfigError("Twitch Client ID not set")
|
val clientId: String = clientId ?: throw PluginConfigError("Twitch Client ID not set")
|
||||||
val clientSecret: String = config["clientSecret"] ?: throw PluginConfigError("Twitch Client Secret not set")
|
val clientSecret: String = clientSecret ?: throw PluginConfigError("Twitch Client Secret not set")
|
||||||
|
|
||||||
val token = TwitchAuthenticator.requestTwitchToken(clientId, clientSecret)
|
val token = TwitchAuthenticator.requestTwitchToken(clientId, clientSecret)
|
||||||
?: throw PluginConfigError("Failed to authenticate on Twitch API with provided credentials")
|
?: throw PluginConfigError("Failed to authenticate on Twitch API with provided credentials")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Plugin-Version: 1.0.0-alpha6
|
Plugin-Version: 1.0.0-alpha7
|
||||||
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
|
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
|
||||||
Plugin-Id: de.grimsi.gameyfin.igdb
|
Plugin-Id: de.grimsi.gameyfin.igdb
|
||||||
Plugin-Name: IGDB Metadata
|
Plugin-Name: IGDB Metadata
|
||||||
|
|||||||
+10
-7
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.plugins.steamgriddb
|
|||||||
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
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
|
||||||
@@ -28,28 +29,30 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun validateConfig(config: Map<String, String?>): Boolean {
|
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
|
||||||
try {
|
try {
|
||||||
runBlocking { authenticate() }
|
runBlocking { authenticate(config["apiKey"]) }
|
||||||
return true
|
return PluginConfigValidationResult.VALID
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
return false
|
return PluginConfigValidationResult.INVALID(
|
||||||
|
mapOf("apiKey" to "Invalid API key")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun start() {
|
override fun start() {
|
||||||
try {
|
try {
|
||||||
runBlocking { authenticate() }
|
runBlocking { authenticate(config["apiKey"]) }
|
||||||
} catch (e: PluginConfigError) {
|
} catch (e: PluginConfigError) {
|
||||||
log.error(e.message)
|
log.error(e.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun authenticate() {
|
private suspend fun authenticate(apiKey: String? = null) {
|
||||||
log.debug("Authenticating on SteamGridDB API...")
|
log.debug("Authenticating on SteamGridDB API...")
|
||||||
|
|
||||||
val apiKey: String = config["apiKey"] ?: throw PluginConfigError("SteamGridDB API key not set")
|
val apiKey: String = apiKey ?: throw PluginConfigError("SteamGridDB API key not set")
|
||||||
val client = SteamGridDbApiClient(apiKey)
|
val client = SteamGridDbApiClient(apiKey)
|
||||||
|
|
||||||
if (!client.isApiKeyValid()) {
|
if (!client.isApiKeyValid()) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
Plugin-Version: 1.0.0-alpha3
|
Plugin-Version: 1.0.0-alpha4
|
||||||
Plugin-Class: de.grimsi.gameyfin.plugins.steamgriddb.SteamGridDbPlugin
|
Plugin-Class: de.grimsi.gameyfin.plugins.steamgriddb.SteamGridDbPlugin
|
||||||
Plugin-Id: de.grimsi.gameyfin.steamgriddb
|
Plugin-Id: de.grimsi.gameyfin.steamgriddb
|
||||||
Plugin-Name: SteamGridDB Covers
|
Plugin-Name: SteamGridDB Covers
|
||||||
|
|||||||
Reference in New Issue
Block a user