Refactor and extend the plugin MANIFEST.MF parser

Redesign the PluginDetailsModal
This commit is contained in:
grimsi
2025-05-15 14:15:15 +02:00
parent 9f7233cb88
commit 4230bf31cc
13 changed files with 1412 additions and 46 deletions
+1233 -3
View File
File diff suppressed because it is too large Load Diff
+6 -2
View File
@@ -43,8 +43,10 @@
"react-aria-components": "^1.7.1",
"react-confetti-boom": "^1.0.0",
"react-dom": "18.3.1",
"react-markdown": "^10.1.0",
"react-player": "^2.16.0",
"react-router": "7.5.2",
"remark-breaks": "^4.0.0",
"swiper": "^11.2.6",
"yup": "^1.6.1"
},
@@ -132,7 +134,9 @@
"rand-seed": "$rand-seed",
"react-router": "$react-router",
"swiper": "$swiper",
"react-player": "$react-player"
"react-player": "$react-player",
"react-markdown": "$react-markdown",
"remark-breaks": "$remark-breaks"
},
"vaadin": {
"dependencies": {
@@ -193,6 +197,6 @@
"workbox-core": "7.3.0",
"workbox-precaching": "7.3.0"
},
"hash": "b7f2b9b343406ec77ce90fe42104642df3b7205f522d2cc60db71051097a32de"
"hash": "f66bddf01ef8dd09a431102050ddd561b4587fdd43bcbff127b93872febbee92"
}
}
@@ -1,11 +1,13 @@
import React, {useEffect, useState} from "react";
import {addToast, Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
import {addToast, Button, Link, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader} from "@heroui/react";
import {Form, Formik} from "formik";
import {PluginConfigEndpoint, PluginManagementEndpoint} from "Frontend/generated/endpoints";
import PluginDto from "Frontend/generated/de/grimsi/gameyfin/core/plugins/management/PluginDto";
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/PluginLogo";
import Markdown from "react-markdown";
import remarkBreaks from "remark-breaks";
interface PluginDetailsModalProps {
plugin: PluginDto;
@@ -58,18 +60,53 @@ export default function PluginDetailsModal({plugin, isOpen, onOpenChange, update
Plugin configuration for {plugin.name}
</ModalHeader>
<ModalBody>
<h4 className="text-l font-bold">Details</h4>
<div className="flex flex-row gap-8">
<PluginLogo plugin={plugin}/>
<div className="grid grid-cols-2">
<p>Author: {plugin.author}</p>
<p>Version: {plugin.version}</p>
<p>Plugin ID: {plugin.id}</p>
<p>Status: {plugin.state?.toLowerCase()}</p>
<div className="flex flex-col text-sm">
<div className="flex flex-row items-center gap-8 mb-4">
<PluginLogo plugin={plugin}/>
<table className="text-left table-auto">
<tbody>
{Object.entries({
"Author": plugin.author,
"Version": plugin.version,
"License": plugin.license,
"URL": <Link isExternal
showAnchorIcon
color="foreground"
size="sm"
href={plugin.url}>
{plugin.url}
</Link>,
}).map(([key, value]) => {
if (!value) return;
return (
<tr key={key}>
<td className="text-foreground/60 w-0 min-w-20">{key}</td>
<td className="flex flex-row gap-1">{value}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<p className="text-foreground/60">Description</p>
<Markdown
remarkPlugins={[remarkBreaks]}
components={{
a(props) {
return <Link isExternal
showAnchorIcon
color="foreground"
underline="always"
href={props.href}
size="sm">
{props.children}
</Link>
}
}}
>{plugin.description}</Markdown>
</div>
<h4 className="text-l font-bold mt-6">Configuration</h4>
<h4 className="text-l font-bold mt-4">Configuration</h4>
{(pluginConfigMeta && pluginConfigMeta.length > 0) ?
pluginConfigMeta.map((entry: PluginConfigElement) => (
<Input key={entry.key} name={entry.key} label={entry.name}
@@ -10,13 +10,11 @@ class DatabasePluginStatusProvider(
) : PluginStatusProvider {
override fun isPluginDisabled(pluginId: String): Boolean {
var pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId)
val pluginManagement = pluginManagementRepository.findByIdOrNull(pluginId)
if (pluginManagement == null) {
return true
}
if (pluginManagement == null) return true
return pluginManagement.enabled != true
return !pluginManagement.enabled
}
override fun disablePlugin(pluginId: String) {
@@ -0,0 +1,30 @@
package de.grimsi.gameyfin.core.plugins.management
import org.pf4j.ManifestPluginDescriptorFinder
import java.util.jar.Manifest
class GameyfinManifestPluginDescriptorFinder() : ManifestPluginDescriptorFinder() {
companion object {
const val PLUGIN_NAME: String = "Plugin-Name"
const val PLUGIN_AUTHOR: String = "Plugin-Author"
const val PLUGIN_URL: String = "Plugin-Url"
}
override fun createPluginDescriptor(manifest: Manifest?): GameyfinPluginDescriptor {
if (manifest == null) throw IllegalArgumentException("Manifest cannot be null")
val pluginDescriptor = super.createPluginDescriptor(manifest)
val attributes = manifest.mainAttributes
return GameyfinPluginDescriptor(
descriptor = pluginDescriptor,
name = attributes.getValue(PLUGIN_NAME)
?: throw IllegalStateException("Plugin-Name not found in manifest"),
author = attributes.getValue(PLUGIN_AUTHOR)
?: throw IllegalStateException("Plugin-Author not found in manifest"),
url = attributes.getValue(PLUGIN_URL) ?: "",
)
}
}
@@ -0,0 +1,39 @@
package de.grimsi.gameyfin.core.plugins.management
import org.pf4j.DefaultPluginDescriptor
import org.pf4j.PluginDescriptor
data class GameyfinPluginDescriptor(
var pluginUrl: String,
var pluginName: String,
var author: String
) : DefaultPluginDescriptor() {
constructor(
descriptor: PluginDescriptor,
url: String,
name: String,
author: String
) : this(
pluginUrl = url,
pluginName = name,
author = author
) {
this.pluginId = descriptor.pluginId
// The Manifest parser ignores newlines in the description
this.pluginDescription = descriptor.pluginDescription.replace("<br>", "\n")
this.pluginClass = descriptor.pluginClass
this.setPluginVersion(descriptor.version)
this.requires = descriptor.requires
this.license = descriptor.license
// Use reflection to access the private 'dependencies' field
// This is because the internal (List<PluginDependency>) and external (List<String>) representation of the field differ
this.javaClass.superclass.getDeclaredField("dependencies").let {
it.isAccessible = true
it.set(this, descriptor.dependencies)
}
}
override fun getProvider() = author
}
@@ -62,8 +62,17 @@ class GameyfinPluginManager(
return dbPluginStatusProvider
}
override fun createPluginDescriptorFinder(): PluginDescriptorFinder {
return GameyfinManifestPluginDescriptorFinder()
}
override fun loadPluginFromPath(pluginPath: Path?): PluginWrapper? {
val pluginWrapper = super.loadPluginFromPath(pluginPath)
val pluginWrapper = try {
super.loadPluginFromPath(pluginPath)
} catch (e: Exception) {
log.error { "Failed to load plugin $pluginPath: ${e.message}" }
null
}
if (pluginWrapper == null || pluginPath == null) return null
@@ -5,10 +5,13 @@ import org.pf4j.PluginState
data class PluginDto(
val id: String,
val name: String,
val description: String,
val version: String,
val author: String,
val license: String? = null,
val url: String? = null,
val hasLogo: Boolean,
val state: PluginState,
val priority: Int,
val trustLevel: PluginTrustLevel
val trustLevel: PluginTrustLevel,
)
@@ -87,15 +87,20 @@ class PluginManagementService(
false
}
val descriptor = pluginWrapper.descriptor as GameyfinPluginDescriptor
return PluginDto(
pluginWrapper.pluginId,
pluginWrapper.descriptor.pluginDescription,
pluginWrapper.descriptor.version,
pluginWrapper.descriptor.provider,
hasLogo,
pluginWrapper.pluginState,
pluginManagementEntry.priority,
pluginManagementEntry.trustLevel
id = descriptor.pluginId,
name = descriptor.pluginName,
description = descriptor.pluginDescription,
version = descriptor.version,
author = descriptor.author,
license = descriptor.license,
url = descriptor.pluginUrl,
hasLogo = hasLogo,
state = pluginWrapper.pluginState,
priority = pluginManagementEntry.priority,
trustLevel = pluginManagementEntry.trustLevel
)
}
}
@@ -3,7 +3,7 @@ package de.grimsi.gameyfin.pluginapi.gamemetadata
import java.net.URI
import java.time.Instant
class GameMetadata(
data class GameMetadata(
val originalId: String,
val title: String,
val description: String? = null,
+9 -5
View File
@@ -1,6 +1,10 @@
Manifest-Version: 1.0
Plugin-Version: 1.0.0-alpha6
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
Plugin-Id: igdb
Plugin-Description: IGDB Metadata
Plugin-Version: 1.0.0-alpha5
Plugin-Provider: grimsi
Plugin-Id: de.grimsi.gameyfin.igdb
Plugin-Name: IGDB Metadata
Plugin-Description: Fetches metadata from IGDB.<br>
Requires a Twitch account and IGDB API credentials.<br>
Details see in the [IGDB API docs](https://api-docs.igdb.com/#account-creation).
Plugin-Author: grimsi
Plugin-License: MIT
Plugin-Url: https://github.com/gameyfin
+8 -5
View File
@@ -1,6 +1,9 @@
Manifest-Version: 1.0
Plugin-Version: 1.0.0-alpha7
Plugin-Class: de.grimsi.gameyfin.plugins.steam.SteamPlugin
Plugin-Id: steam
Plugin-Description: Steam Metadata
Plugin-Version: 1.0.0-alpha6
Plugin-Provider: grimsi
Plugin-Id: de.grimsi.gameyfin.steam
Plugin-Name: Steam Metadata
Plugin-Description: Fetches metadata from Steam using undocumented public API endpoints.<br>
This is more of a proof of concept and is prone to breaking when the Steam API changes.
Plugin-Author: grimsi
Plugin-License: MIT
Plugin-Url: https://github.com/gameyfin
@@ -1,6 +1,10 @@
Manifest-Version: 1.0
Plugin-Version: 1.0.0-alpha3
Plugin-Class: de.grimsi.gameyfin.plugins.steamgriddb.SteamGridDbPlugin
Plugin-Id: steamgriddb
Plugin-Description: SteamGridDB covers
Plugin-Version: 1.0.0-alpha2
Plugin-Provider: grimsi
Plugin-Id: de.grimsi.gameyfin.steamgriddb
Plugin-Name: SteamGridDB Covers
Plugin-Description: Fetches covers from SteamGridDB.<br>
Requires a SteamGridDB account and an API key.<br>
The API key can be obtained [here](https://www.steamgriddb.com/profile/preferences/api).
Plugin-Author: grimsi
Plugin-License: MIT
Plugin-Url: https://github.com/gameyfin