diff --git a/.github/workflows/pluginapi-release.yml b/.github/workflows/pluginapi-release.yml new file mode 100644 index 0000000..5d45f2e --- /dev/null +++ b/.github/workflows/pluginapi-release.yml @@ -0,0 +1,35 @@ +name: Plugin-API Release + +on: + push: + tags: + - '*' + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: gradle + + - name: Decrypt and import GPG key + run: | + echo "$GPG_PRIVATE_KEY" | gpg --batch --import + env: + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: Build and deploy with JReleaser + run: ./gradlew jreleaserFullRelease + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVENCENTRAL_USERNAME: ${{ secrets.MAVENCENTRAL_USERNAME }} + MAVENCENTRAL_TOKEN: ${{ secrets.MAVENCENTRAL_TOKEN }} + JRELEASER_GITHUB_TOKEN: ${{ GITHUB_TOKEN }} diff --git a/.run/Publish Plugin API.run.xml b/.run/Publish Plugin API.run.xml new file mode 100644 index 0000000..67a2c3f --- /dev/null +++ b/.run/Publish Plugin API.run.xml @@ -0,0 +1,25 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/plugin-api/build.gradle.kts b/plugin-api/build.gradle.kts index e34370a..c07376a 100644 --- a/plugin-api/build.gradle.kts +++ b/plugin-api/build.gradle.kts @@ -1,23 +1,49 @@ +import com.vanniktech.maven.publish.SonatypeHost + plugins { kotlin("jvm") - `maven-publish` + `java-library` + id("com.vanniktech.maven.publish") version "0.32.0" } group = "org.gameyfin" -repositories { - mavenCentral() -} - -publishing { - publications { - create("maven") { - from(components["java"]) - } - } -} - dependencies { // PF4J (shared) api("org.pf4j:pf4j:${rootProject.extra["pf4jVersion"]}") +} + +mavenPublishing { + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + signAllPublications() + + coordinates(project.group.toString(), project.name, project.version.toString()) + + pom { + name = "Gameyfin Plugin API" + description = + "The Gameyfin Plugin API provides the necessary interfaces and classes to create plugins for Gameyfin." + url = "https://gameyfin.org/" + + licenses { + license { + name = "GNU Affero General Public License v3.0" + url = "https://www.gnu.org/licenses/agpl-3.0.en.html" + } + } + + developers { + developer { + id = "grimsi" + name = "Simon Grimme" + url = "https://github.com/grimsi" + } + } + + scm { + url = "https://github.com/gameyfin/gameyfin" + connection = "scm:git:git://github.com/gameyfin/gameyfin.git" + developerConnection = "scm:git:ssh://git@github.com/gameyfin/gameyfin.git" + } + } } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/ConfigMetadata.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/ConfigMetadata.kt index 33ca38f..ba3f767 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/ConfigMetadata.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/ConfigMetadata.kt @@ -2,8 +2,24 @@ package org.gameyfin.pluginapi.core.config import java.io.Serializable +/** + * Alias for a list of ConfigMetadata objects for plugin configuration. + */ typealias PluginConfigMetadata = List> +/** + * Represents metadata for a configuration property. + * + * @param T The type of the configuration value, must be Serializable. + * @property key The unique (in the scope of the plugin) key for the configuration property. + * @property type The class type of the configuration value. + * @property label A human-readable label for the configuration property. + * @property description A short description of the configuration property. + * @property default The default value for the configuration property, if any. + * @property isSecret Whether the configuration value is secret (e.g., password). Affects how the value is displayed in the UI. + * @property isRequired Whether the configuration property is required. + * @property allowedValues The allowed values for the configuration property, if applicable (e.g., for enums). Will be populated automatically if the type is an enum. + */ data class ConfigMetadata( val key: String, val type: Class, @@ -13,6 +29,9 @@ data class ConfigMetadata( val isSecret: Boolean = false, val isRequired: Boolean = true, ) { + /** + * List of allowed values for the configuration property, if the type is an enum. + */ var allowedValues: List? = null init { diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/Configurable.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/Configurable.kt index 204a01c..8d22a43 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/Configurable.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/Configurable.kt @@ -2,14 +2,52 @@ package org.gameyfin.pluginapi.core.config import java.io.Serializable +/** + * Interface for classes that can be configured using plugin configuration metadata. + * Provides methods for loading, validating, and accessing configuration values. + */ interface Configurable { + /** + * The metadata describing the configuration options for this configurable instance. + */ val configMetadata: PluginConfigMetadata + /** + * Loads configuration values from the provided map. + * + * @param config A map of configuration keys to their string values (values are nullable). + */ fun loadConfig(config: Map) + /** + * Validates the current configuration state. + * + * @return The result of the configuration validation. + */ fun validateConfig(): PluginConfigValidationResult + + /** + * Validates the provided configuration map. + * + * @param config A map of configuration keys to their string values (nullable). + * @return The result of the configuration validation. + */ fun validateConfig(config: Map): PluginConfigValidationResult + /** + * Retrieves a required configuration value by key. + * + * @param key The configuration key. + * @return The configuration value of type T. + * @throws Exception if the key is missing or the value is invalid. + */ fun config(key: String): T + + /** + * Retrieves an optional configuration value by key. + * + * @param key The configuration key. + * @return The configuration value of type T, or null if not present. + */ fun optionalConfig(key: String): T? } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigError.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigError.kt index 0ea47b7..709c8d3 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigError.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigError.kt @@ -1,3 +1,11 @@ package org.gameyfin.pluginapi.core.config +/** + * Exception thrown when there is an error in plugin configuration. + * + * This exception is used to indicate problems such as missing, invalid, or malformed configuration values + * when loading or validating plugin configuration. + * + * @param message The detail message describing the configuration error. + */ class PluginConfigError(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt index 8429a3f..a116c8d 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/config/PluginConfigValidationResult.kt @@ -1,22 +1,50 @@ package org.gameyfin.pluginapi.core.config +/** + * Represents the result of plugin configuration validation. + * + * @property result The type of validation result (VALID, INVALID, UNKNOWN). + * @property errors A map of configuration keys to error messages, present if validation failed. + */ data class PluginConfigValidationResult( val result: PluginConfigValidationResultType, val errors: Map? = null ) { companion object { + /** + * A valid configuration result with no errors. + */ val VALID = PluginConfigValidationResult(PluginConfigValidationResultType.VALID) + + /** + * An unknown configuration validation result. + */ val UNKNOWN = PluginConfigValidationResult(PluginConfigValidationResultType.UNKNWOWN) + + /** + * Creates an invalid configuration result with the specified errors. + * + * @param errors A map of configuration keys to error messages. + * @return An invalid PluginConfigValidationResult instance. + */ fun INVALID(errors: Map): PluginConfigValidationResult { return PluginConfigValidationResult(PluginConfigValidationResultType.INVALID, errors) } } + /** + * Checks if the configuration is valid. + * + * @return True if the result is VALID, false otherwise. + */ fun isValid(): Boolean { return result == PluginConfigValidationResultType.VALID } } +/** + * Enum representing the possible types of plugin configuration validation results. + */ enum class PluginConfigValidationResultType { VALID, INVALID, diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt index e6be0b0..6e65da0 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/ConfigurableGameyfinPlugin.kt @@ -7,17 +7,47 @@ import org.gameyfin.pluginapi.core.config.PluginConfigValidationResult import org.pf4j.PluginWrapper import java.io.Serializable +/** + * Abstract base class for Gameyfin plugins that support configuration. + * + * This class implements the [Configurable] interface and provides default logic for loading, + * validating, and accessing plugin configuration values using metadata. + * + * @constructor Creates a configurable Gameyfin plugin with the given [PluginWrapper]. + * @param wrapper The plugin wrapper provided by the Gameyfin application. + */ @Suppress("UNCHECKED_CAST") abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper), Configurable { + /** + * The current configuration map, where keys are configuration property names and values are their string representations. + */ private var config: Map = emptyMap() + /** + * Loads configuration values from the provided map. + * + * @param config A map of configuration keys to their string values (nullable). + */ override fun loadConfig(config: Map) { this.config = config } + /** + * Validates the current configuration state. + * + * @return The result of the configuration validation. + */ override fun validateConfig(): PluginConfigValidationResult = validateConfig(config) + /** + * Validates the provided configuration map. + * The validation covers basic checks such as required fields, type casting, and allowed values. + * It's recommended to override this method in subclasses to implement additional validation logic specific to the plugin. + * + * @param config A map of configuration keys to their string values (nullable). + * @return The result of the configuration validation. + */ override fun validateConfig(config: Map): PluginConfigValidationResult { val errors = mutableMapOf() @@ -43,6 +73,13 @@ abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlug } } + /** + * Retrieves an optional configuration value by key. + * + * @param key The configuration key. + * @return The configuration value of type T, or null if not present or invalid. + * @throws PluginConfigError if the value cannot be cast to the expected type. + */ override fun optionalConfig(key: String): T? { val meta = resolveMetadata(key) val value = resolveValue(key) @@ -55,6 +92,13 @@ abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlug } } + /** + * Retrieves a required configuration value by key. + * + * @param key The configuration key. + * @return The configuration value of type T. + * @throws PluginConfigError if the key is missing or the value is invalid. + */ override fun config(key: String): T { val value = optionalConfig(key) if (value == null) { @@ -63,6 +107,16 @@ abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlug return value } + /** + * Casts a configuration value to the expected type defined in the metadata. + * + * Handles enums, common primitive types, and attempts to use valueOf/parse methods via reflection for custom types. + * + * @param meta The configuration metadata describing the expected type. + * @param value The value to cast. + * @return The cast value, or throws PluginConfigError if casting fails. + * @throws PluginConfigError if the value cannot be cast to the expected type. + */ private fun castConfigValue(meta: ConfigMetadata<*>, value: Any): Any? { val expectedType = meta.type @@ -106,11 +160,25 @@ abstract class ConfigurableGameyfinPlugin(wrapper: PluginWrapper) : GameyfinPlug } } + /** + * Resolves the configuration metadata for a given key. + * + * @param key The configuration key to look up. + * @return The corresponding ConfigMetadata instance. + * @throws PluginConfigError if the key is unknown. + */ private fun resolveMetadata(key: String): ConfigMetadata<*> { return configMetadata.find { it.key == key } ?: throw PluginConfigError("Unknown configuration key: $key") } + /** + * Resolves the value for a configuration key, optionally using an override map. + * + * @param key The configuration key to resolve. + * @param configOverride An optional map to override the current configuration. + * @return The resolved value, or the default from metadata if not present. + */ private fun resolveValue(key: String, configOverride: Map? = null): Serializable? { val meta = resolveMetadata(key) val conf = configOverride ?: config diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt index f9b0447..ffef149 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/core/wrapper/GameyfinPlugin.kt @@ -3,21 +3,48 @@ package org.gameyfin.pluginapi.core.wrapper import org.pf4j.Plugin import org.pf4j.PluginWrapper +/** + * Abstract base class for all Gameyfin plugins. + * + * This class extends the PF4J [Plugin] class and provides utility methods for plugin logo management. + * It also maintains a static reference to the current plugin instance. + * + * @constructor Creates a Gameyfin plugin with the given [PluginWrapper]. + * @param wrapper The plugin wrapper provided by the Gameyfin application. + */ @Suppress("DEPRECATION") abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { companion object { + /** + * The base name of the logo file (without extension). + */ const val LOGO_FILE_NAME: String = "logo" + + /** + * Supported logo file formats. + */ val SUPPORTED_LOGO_FORMATS: List = listOf("png", "jpg", "jpeg", "gif", "svg", "webp") + /** + * Reference to the current plugin instance. + */ lateinit var plugin: GameyfinPlugin private set } + /** + * Initializes the plugin and sets the static plugin reference. + */ init { plugin = this } + /** + * Checks if the plugin contains a logo file in any supported format. + * + * @return True if a logo file is found, false otherwise. + */ fun hasLogo(): Boolean { for (format in SUPPORTED_LOGO_FORMATS) { val resourcePath = "$LOGO_FILE_NAME.$format" @@ -30,6 +57,11 @@ abstract class GameyfinPlugin(wrapper: PluginWrapper) : Plugin(wrapper) { return false } + /** + * Retrieves the logo file as a byte array, if present. + * + * @return The logo file as a byte array, or null if not found. + */ fun getLogo(): ByteArray? { for (format in SUPPORTED_LOGO_FORMATS) { val resourcePath = "$LOGO_FILE_NAME.$format" diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/Download.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/Download.kt index e61b82e..1d270e0 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/Download.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/Download.kt @@ -2,15 +2,29 @@ package org.gameyfin.pluginapi.download import java.io.InputStream +/** + * Represents a downloadable resource, which can be either a file or a link. + */ sealed interface Download +/** + * Represents a file-based download. + * + * @property data The input stream containing the file data. + * @property fileExtension The file extension (e.g., "zip", "png"), or null if none. + * @property size The size of the file in bytes, or null if unknown. + */ data class FileDownload( val data: InputStream, val fileExtension: String? = null, val size: Long? = null ) : Download +/** + * Represents a link-based download. + * + * @property url The URL to the downloadable resource. + */ data class LinkDownload( val url: String ) : Download - diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/DownloadProvider.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/DownloadProvider.kt index 6354a92..db8b55e 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/DownloadProvider.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/download/DownloadProvider.kt @@ -3,7 +3,19 @@ package org.gameyfin.pluginapi.download import org.pf4j.ExtensionPoint import java.nio.file.Path +/** + * Extension point for providing downloadable resources. + * + * Implementations of this interface are responsible for handling download requests for specific paths. + * This is typically used to allow plugins to serve files or links for download. + */ interface DownloadProvider : ExtensionPoint { + /** + * Downloads a resource for the given path. + * + * @param path The path to the resource to download. + * @return A [Download] representing the downloadable resource (file or link). + */ fun download(path: Path): Download } \ No newline at end of file diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadata.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadata.kt index feb516d..4fb23ff 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadata.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadata.kt @@ -3,6 +3,26 @@ package org.gameyfin.pluginapi.gamemetadata import java.net.URI import java.time.Instant +/** + * Represents metadata for a game, including identifiers, descriptive information, media, ratings, and categorization. + * + * @property originalId The unique identifier for the game from the original source. + * @property title The title of the game. + * @property description A description of the game, or null if not available. + * @property coverUrl The URI to the game's cover image, or null if not available. + * @property release The release date and time of the game, or null if not available. + * @property userRating The user rating for the game, or null if not available. + * @property criticRating The critic rating for the game, or null if not available. + * @property developedBy The set of developer names, or null if not available. + * @property publishedBy The set of publisher names, or null if not available. + * @property genres The set of genres associated with the game, or null if not available. + * @property themes The set of themes associated with the game, or null if not available. + * @property keywords The set of keywords associated with the game, or null if not available. + * @property screenshotUrls The set of URIs to screenshots, or null if not available. + * @property videoUrls The set of URIs to videos, or null if not available. + * @property features The set of features associated with the game, or null if not available. + * @property perspectives The set of player perspectives, or null if not available. + */ data class GameMetadata( val originalId: String, val title: String, @@ -22,6 +42,9 @@ data class GameMetadata( val perspectives: Set? = null ) +/** + * Enum representing the genre of a game. + */ enum class Genre { UNKNOWN, ACTION, @@ -51,6 +74,9 @@ enum class Genre { QUIZ_TRIVIA } +/** + * Enum representing the theme of a game. + */ enum class Theme { UNKNOWN, ACTION, diff --git a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt index 564832d..8bc1b86 100644 --- a/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt +++ b/plugin-api/src/main/kotlin/org/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt @@ -2,8 +2,27 @@ package org.gameyfin.pluginapi.gamemetadata import org.pf4j.ExtensionPoint +/** + * Extension point for providing game metadata. + * + * Implementations of this interface are responsible for fetching game metadata by title or by unique identifier. + * This is typically used to allow plugins to provide metadata for games from various sources. + */ interface GameMetadataProvider : ExtensionPoint { + /** + * Fetches a list of game metadata entries matching the given game title. + * + * @param gameTitle The title of the game to search for. + * @param maxResults The maximum number of results to return. Defaults to 1. + * @return A list of [GameMetadata] objects matching the title, or an empty list if none found. + */ fun fetchByTitle(gameTitle: String, maxResults: Int = 1): List + /** + * Fetches game metadata by its unique identifier. + * + * @param id The unique identifier of the game. + * @return The [GameMetadata] for the given id, or null if not found. + */ fun fetchById(id: String): GameMetadata? } \ No newline at end of file