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:
grimsi
2025-05-19 12:21:42 +02:00
parent 08c41265c8
commit d9fef0f30c
23 changed files with 240 additions and 130 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<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" />
</configuration>
</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 {
CheckCircle,
IconContext,
@@ -16,31 +16,24 @@ import {
XCircle
} from "@phosphor-icons/react";
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 PluginLogo from "Frontend/components/general/PluginLogo";
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 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 }) {
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" {
if (trustLevel === PluginTrustLevel.UNTRUSTED) return "danger";
if (isDisabled(state)) return "warning";
if (configValidationResult === undefined) return "default";
if (!configValidationResult) return "danger";
return stateToColor(state);
}
@@ -76,14 +69,14 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
}
function configValidationResultToChip(validationResult: PluginConfigValidationResult | undefined): ReactNode {
switch (validationResult) {
case PluginConfigValidationResult.VALID:
switch (validationResult?.result) {
case PluginConfigValidationResultType.VALID:
return <Tooltip content="Config valid" placement="bottom" color="foreground">
<Chip size="sm" radius="sm" className="text-xs" color="success">
<CheckCircle/>
</Chip>
</Tooltip>
case PluginConfigValidationResult.INVALID:
case PluginConfigValidationResultType.INVALID:
return <Tooltip content="Config invalid" placement="bottom" color="foreground">
<Chip size="sm" radius="sm" className="text-xs" color="danger">
<WarningCircle/>
@@ -173,10 +166,7 @@ export function PluginManagementCard({plugin}: { plugin: PluginDto }) {
{stateToIcon(plugin.state)}
</Tooltip>
</Chip>
{configValidationResult === undefined ?
<Skeleton className="rounded-md h-6 w-9"/> :
configValidationResultToChip(configValidationResult)
}
{configValidationResultToChip(plugin.configValidation)}
</div>
</div>
</Card>
@@ -4,7 +4,7 @@ import {SmallInfoField} from "Frontend/components/general/SmallInfoField";
import {XCircle} from "@phosphor-icons/react";
// @ts-ignore
const Input = ({label, ...props}) => {
const Input = ({label, showErrorUntouched = false, ...props}) => {
// @ts-ignore
const [field, meta] = useField(props);
@@ -15,10 +15,10 @@ const Input = ({label, ...props}) => {
{...field}
id={label}
label={label}
isInvalid={meta.touched && !!meta.error}
isInvalid={(meta.touched || showErrorUntouched) && !!meta.error}
/>
<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}/>
)}
</div>
@@ -1,5 +1,5 @@
import React from "react";
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
import React, {useState} from "react";
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, Tooltip} from "@heroui/react";
import {Form, Formik} from "formik";
import PluginConfigElement from "Frontend/generated/de/grimsi/gameyfin/pluginapi/core/PluginConfigElement";
import Input from "Frontend/components/general/input/Input";
@@ -8,6 +8,7 @@ import Markdown from "react-markdown";
import remarkBreaks from "remark-breaks";
import {PluginEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/dto/PluginDto";
import {ArrowClockwise} from "@phosphor-icons/react";
interface PluginDetailsModalProps {
plugin: PluginDto;
@@ -16,6 +17,8 @@ interface PluginDetailsModalProps {
}
export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: PluginDetailsModalProps) {
const [configValidated, setConfigValidated] = useState<boolean>(false);
async function saveConfig(values: Record<string, string>) {
await PluginEndpoint.updateConfig(plugin.id, values);
addToast({
@@ -30,13 +33,14 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
<ModalContent>
{(onClose) => (
<Formik initialValues={plugin.config}
initialErrors={plugin.configValidation?.errors}
enableReinitialize={true}
onSubmit={async (values: any) => {
await saveConfig(values);
onClose();
}}
>
{(formik: { isSubmitting: any; }) => (
{(formik: any) => (
<Form>
<ModalHeader className="flex flex-col gap-1">
Plugin configuration for {plugin.name}
@@ -88,10 +92,33 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange}: Plugi
>{plugin.description}</Markdown>
</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.map((entry: PluginConfigElement) => (
<Input key={entry.key} name={entry.key} label={entry.name}
showErrorUntouched={true}
type={entry.secret ? "password" : "text"}/>
)) : "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.core.Role
import de.grimsi.gameyfin.users.util.isAdmin
import io.github.oshai.kotlinlogging.KotlinLogging
import jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed
import org.springframework.security.core.context.SecurityContextHolder
@@ -16,6 +17,9 @@ import reactor.core.publisher.Flux
class ConfigEndpoint(
private val configService: ConfigService
) {
companion object {
val log = KotlinLogging.logger { }
}
/** CRUD endpoints for admins **/
@@ -15,12 +15,19 @@ import java.io.Serializable
class ConfigService(
private val appConfigRepository: ConfigRepository
) {
private val log = KotlinLogging.logger {}
companion object {
private val log = KotlinLogging.logger {}
}
private val configUpdates = Sinks.many().multicast().onBackpressureBuffer<ConfigUpdateDto>()
fun subscribe(): Flux<ConfigUpdateDto> {
log.debug { "New subscription for configUpdates (#${configUpdates.currentSubscriberCount()})" }
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 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.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 jakarta.annotation.security.PermitAll
import jakarta.annotation.security.RolesAllowed
@@ -15,28 +15,31 @@ import reactor.core.publisher.Flux
@Endpoint
@RolesAllowed(Role.Names.ADMIN)
class PluginEndpoint(
private val pluginManagementService: PluginManagementService
private val pluginService: PluginService
) {
@PermitAll
fun subscribe(): Flux<PluginUpdateDto> {
val user = SecurityContextHolder.getContext().authentication.principal as UserDetails
return if (user.isAdmin()) pluginManagementService.subscribe()
return if (user.isAdmin()) pluginService.subscribe()
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>) =
pluginManagementService.setPluginPriorities(pluginPriorities)
pluginService.setPluginPriorities(pluginPriorities)
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>) =
pluginManagementService.updateConfig(pluginId, updatedConfig)
pluginService.updateConfig(pluginId, updatedConfig)
}
@@ -1,10 +1,7 @@
package de.grimsi.gameyfin.core.plugins.config
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 org.pf4j.PluginWrapper
import org.springframework.stereotype.Service
@Service
@@ -13,24 +10,9 @@ class PluginConfigService(
private val pluginManager: GameyfinPluginManager
) {
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
companion object {
private val log = KotlinLogging.logger {}
}
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)
}
}
@@ -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.pluginapi.core.PluginConfigElement
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
import org.pf4j.PluginState
data class PluginDto(
@@ -18,6 +19,7 @@ data class PluginDto(
val state: PluginState,
val configMetadata: List<PluginConfigElement>? = null,
val config: Map<String, String?>? = null,
val configValidation: PluginConfigValidationResult? = null,
val priority: Int,
val trustLevel: PluginTrustLevel,
)
@@ -1,6 +1,7 @@
package de.grimsi.gameyfin.core.plugins.dto
import com.fasterxml.jackson.annotation.JsonInclude
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
import org.pf4j.PluginState
@JsonInclude(JsonInclude.Include.NON_NULL)
@@ -8,5 +9,6 @@ data class PluginUpdateDto(
val id: String,
val state: PluginState? = null,
val config: Map<String, String?>? = null,
val configValidation: PluginConfigValidationResult? = null,
val priority: Int? = null
)
@@ -1,8 +1,9 @@
package de.grimsi.gameyfin.core.plugins.management
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.PluginConfigValidationResult
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResultType
import io.github.oshai.kotlinlogging.KotlinLogging
import org.pf4j.*
import org.springframework.data.repository.findByIdOrNull
@@ -34,9 +35,9 @@ class GameyfinPluginManager(
private val log = KotlinLogging.logger {}
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 {
// This took me way too long to figure out...
// But I learned a lot about Kotlin and Java interoperability in the process
pluginStatusProvider = dbPluginStatusProvider
pluginStateListeners.add { event ->
@@ -143,7 +144,7 @@ class GameyfinPluginManager(
}
// Validate config before starting the plugin
if (validatePluginConfig(pluginId) == PluginConfigValidationResult.INVALID) {
if (validatePluginConfig(pluginId).result == PluginConfigValidationResultType.INVALID) {
log.warn { "Plugin $pluginId has invalid configuration" }
val pluginWrapper = getPlugin(pluginId)
@@ -171,14 +172,28 @@ class GameyfinPluginManager(
val plugin = try {
getPlugin(pluginId)?.plugin
} catch (_: NoClassDefFoundError) {
return PluginConfigValidationResult.UNKNWOWN
return PluginConfigValidationResult(PluginConfigValidationResultType.UNKNWOWN)
}
if (plugin !is Configurable || plugin.validateConfig()) {
return PluginConfigValidationResult.VALID
if (plugin !is Configurable) {
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>> {
@@ -1,10 +1,16 @@
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.PluginConfigValidationResult
import de.grimsi.gameyfin.core.plugins.dto.PluginDto
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.PluginConfigElement
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
import io.github.oshai.kotlinlogging.KotlinLogging
import org.pf4j.ExtensionPoint
import org.pf4j.PluginWrapper
import org.springframework.data.repository.findByIdOrNull
@@ -13,21 +19,32 @@ import reactor.core.publisher.Flux
import reactor.core.publisher.Sinks
@Service
class PluginManagementService(
class PluginService(
private val pluginManager: GameyfinPluginManager,
private val pluginConfigService: PluginConfigService,
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 pluginConfigValidationCache = mutableMapOf<String, PluginConfigValidationResult>()
init {
pluginManager.addPluginStateListener {
pluginUpdates.tryEmitNext(PluginUpdateDto(id = it.plugin.pluginId, state = it.pluginState))
pluginManager.addPluginStateListener { event ->
pluginUpdates.tryEmitNext(PluginUpdateDto(id = event.plugin.pluginId, state = event.pluginState))
}
}
fun subscribe(): Flux<PluginUpdateDto> {
log.debug { "New subscription for pluginUpdates" }
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> {
@@ -59,16 +76,6 @@ class PluginManagementService(
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>) {
pluginPriorities.forEach { (pluginId, priority) ->
val pluginManagementEntry = getPluginManagementEntry(pluginId)
@@ -82,6 +89,52 @@ class PluginManagementService(
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 {
val pluginManagementEntry = getPluginManagementEntry(pluginWrapper.pluginId)
@@ -108,8 +161,9 @@ class PluginManagementService(
url = descriptor.pluginUrl,
hasLogo = hasLogo,
state = pluginWrapper.pluginState,
configMetadata = pluginConfigService.getConfigMetadata(pluginWrapper),
config = pluginConfigService.getConfig(pluginWrapper),
configMetadata = getConfigMetadata(pluginWrapper),
config = getConfig(pluginWrapper),
configValidation = validatePluginConfig(descriptor.pluginId),
priority = pluginManagementEntry.priority,
trustLevel = pluginManagementEntry.trustLevel
)
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.games
import de.grimsi.gameyfin.core.alphaNumeric
import de.grimsi.gameyfin.core.filterValuesNotNull
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.games.dto.GameDto
import de.grimsi.gameyfin.games.dto.GameMetadataDto
@@ -28,7 +28,7 @@ import java.nio.file.Path
@Service
class GameService(
private val pluginManager: PluginManager,
private val pluginManagementService: PluginManagementService,
private val pluginService: PluginService,
private val gameRepository: GameRepository,
private val companyService: CompanyService
) {
@@ -179,11 +179,11 @@ class GameService(
// Cache the plugin management entries for each provider
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
val sortedResults = results.entries.sortedByDescending {
pluginManagementService.getPluginManagementEntry(it.key.javaClass).priority
pluginService.getPluginManagementEntry(it.key.javaClass).priority
}
sortedResults.forEach { (provider, metadata) ->
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.media
import de.grimsi.gameyfin.core.Role
import de.grimsi.gameyfin.core.Utils
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.ImageType
import de.grimsi.gameyfin.users.UserService
@@ -24,7 +24,7 @@ import org.springframework.web.multipart.MultipartFile
class ImageEndpoint(
private val imageService: ImageService,
private val userService: UserService,
private val pluginManagementService: PluginManagementService
private val pluginService: PluginService
) {
@GetMapping("/screenshot/{id}")
@@ -39,7 +39,7 @@ class ImageEndpoint(
@GetMapping("/plugins/{id}/logo")
fun getPluginLogo(@PathVariable("id") pluginId: String): ResponseEntity<ByteArrayResource>? {
val logo = pluginManagementService.getLogo(pluginId)
val logo = pluginService.getLogo(pluginId)
return Utils.inputStreamToResponseEntity(logo)
}
@@ -4,6 +4,6 @@ interface Configurable {
val configMetadata: List<PluginConfigElement>
var config: Map<String, String?>
fun validateConfig(): Boolean = validateConfig(config)
fun validateConfig(config: Map<String, String?>): Boolean
fun validateConfig(): PluginConfigValidationResult = validateConfig(config)
fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult
}
@@ -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,
}
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.plugins.directdownload
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
import de.grimsi.gameyfin.pluginapi.download.Download
import de.grimsi.gameyfin.pluginapi.download.DownloadProvider
import de.grimsi.gameyfin.pluginapi.download.FileDownload
@@ -26,21 +27,26 @@ class DirectDownloadPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(
override val configMetadata: List<PluginConfigElement> = listOf(
PluginConfigElement(
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",
)
)
override fun validateConfig(config: Map<String, String?>): Boolean {
return config["compressionMode"]?.let {
try {
CompressionMode.valueOf(it.uppercase())
true
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) {
log.error("Invalid compression mode: $it")
false
PluginConfigValidationResult.INVALID(
mapOf("compressionMode" to "Invalid compression mode: $compressionMode (must be \"none\", \"fast\", or \"best\")")
)
}
} ?: true
}
return PluginConfigValidationResult.VALID
}
@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-Id: de.grimsi.gameyfin.directdownload
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.TwitchAuthenticator
import com.api.igdb.request.games
import de.grimsi.gameyfin.pluginapi.core.Configurable
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
import de.grimsi.gameyfin.pluginapi.core.*
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
import me.xdrop.fuzzywuzzy.FuzzySearch
@@ -36,19 +33,24 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
)
override var config: Map<String, String?> = emptyMap()
override fun validateConfig(config: Map<String, String?>): Boolean {
override fun validateConfig(config: Map<String, String?>): PluginConfigValidationResult {
try {
authenticate()
return true
authenticate(config["clientId"], config["clientSecret"])
return PluginConfigValidationResult.VALID
} catch (e: PluginConfigError) {
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() {
try {
authenticate()
authenticate(config["clientId"], config["clientSecret"])
} catch (e: PluginConfigError) {
log.error(e.message)
}
@@ -58,11 +60,11 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable
log.debug("IgdbPlugin.stop()")
}
private fun authenticate() {
private fun authenticate(clientId: String? = null, clientSecret: String? = null) {
log.debug("Authenticating on Twitch API...")
val clientId: String = config["clientId"] ?: throw PluginConfigError("Twitch Client ID not set")
val clientSecret: String = config["clientSecret"] ?: throw PluginConfigError("Twitch Client Secret not set")
val clientId: String = clientId ?: throw PluginConfigError("Twitch Client ID not set")
val clientSecret: String = clientSecret ?: throw PluginConfigError("Twitch Client Secret not set")
val token = TwitchAuthenticator.requestTwitchToken(clientId, clientSecret)
?: throw PluginConfigError("Failed to authenticate on Twitch API with provided credentials")
+1 -1
View File
@@ -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-Id: de.grimsi.gameyfin.igdb
Plugin-Name: IGDB Metadata
@@ -3,6 +3,7 @@ package de.grimsi.gameyfin.plugins.steamgriddb
import de.grimsi.gameyfin.pluginapi.core.ConfigurableGameyfinPlugin
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
import de.grimsi.gameyfin.pluginapi.core.PluginConfigValidationResult
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
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 {
runBlocking { authenticate() }
return true
runBlocking { authenticate(config["apiKey"]) }
return PluginConfigValidationResult.VALID
} catch (e: PluginConfigError) {
log.error(e.message)
return false
return PluginConfigValidationResult.INVALID(
mapOf("apiKey" to "Invalid API key")
)
}
}
override fun start() {
try {
runBlocking { authenticate() }
runBlocking { authenticate(config["apiKey"]) }
} catch (e: PluginConfigError) {
log.error(e.message)
}
}
private suspend fun authenticate() {
private suspend fun authenticate(apiKey: String? = null) {
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)
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-Id: de.grimsi.gameyfin.steamgriddb
Plugin-Name: SteamGridDB Covers