### Install plugin JAR path Source: https://gameyfin.org/plugins/tutorial Example path where the generated JAR file should be copied to install the plugin. The plugins directory may need to be created if it does not exist. ```text /path/to/gameyfin/plugins/plugin-helloworld.jar ``` -------------------------------- ### Start Gameyfin with Docker Compose Source: https://gameyfin.org/installation/docker Starts the Gameyfin container in detached mode using the docker-compose.yml file. ```bash docker-compose up -d ``` -------------------------------- ### View Gameyfin logs Source: https://gameyfin.org/installation/docker Follows the logs of the Gameyfin container to verify it starts correctly. The logs should contain 'Loaded plugins: ' when ready. ```bash docker-compose logs -f gameyfin ``` -------------------------------- ### Build plugin with ./gradlew build Source: https://gameyfin.org/plugins/tutorial Runs the Gradle build task to compile the plugin and create a JAR file. The resulting JAR is placed in the build/libs directory. ```bash ./gradlew build ``` -------------------------------- ### Docker Compose configuration for Gameyfin Source: https://gameyfin.org/installation/docker A docker-compose.yml file that runs Gameyfin with the required APP_KEY, optional APP_URL and PUID/PGID, volume mounts for persistence, and port mappings. Uncomment the torrent plugin ports if needed. ```yaml services: gameyfin: image: ghcr.io/gameyfin/gameyfin:2 container_name: gameyfin restart: unless-stopped environment: # Generate a new APP_KEY using the command "openssl rand -base64 32" or similar. APP_KEY: # (optional) Set the URL of your Gameyfin instance if you are using a reverse proxy. # Currently, this is only used for generating links in notification emails and the log line at first run. # APP_URL: https://gameyfin.example.com # (optional) Set the user and group ID to run Gameyfin with a specific user. # PUID: 1000 # Change this to your user ID if needed # PGID: 1000 # Change this to your group ID if needed volumes: - "./db:/opt/gameyfin/db" - "./data:/opt/gameyfin/data" - "./plugindata:/opt/gameyfin/plugindata" - "./logs:/opt/gameyfin/logs" - ports: - "8080:8080" # If you plan to use the included torrent plugin, uncomment the following lines: # - "6969:6969" # - "6881:6881" ``` -------------------------------- ### Create MANIFEST.MF for a Gameyfin plugin Source: https://gameyfin.org/plugins/tutorial Required manifest file for a Gameyfin plugin. Place it in src/main/resources. The Plugin-Class must point to the main plugin class, and Plugin-Id should be unique. ```properties Plugin-Version: 1.0.0 Plugin-Class: org.gameyfin.plugins.TutorialPlugin Plugin-Id: org.gameyfin.plugins.helloworld Plugin-Name: Gameyfin Example Plugin Plugin-Description: Demonstrates how to implement a simple game metadata plugin.
Always returns a hardcoded game with the name "Hello World" and the original ID "hello-world-game". Plugin-Short-Description: Example plugin for Gameyfin Plugin-Author: grimsi Plugin-License: MIT Plugin-Url: https://github.com/gameyfin/plugin-template ``` -------------------------------- ### Create TutorialPlugin.kt with configMetadata Source: https://gameyfin.org/plugins/tutorial Main plugin class that extends ConfigurableGameyfinPlugin. Defines an enum and overrides configMetadata to declare configuration keys; secret keys are shown as password fields, optional keys are not required, and enum keys appear as dropdowns. ```kotlin // ... Package definition and imports enum class ExampleEnum { OPTION_ONE, OPTION_TWO, OPTION_THREE } class TutorialPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) { /** * This is the configuration metadata for the plugin. * All configuration values set by the admin(s) are stored encrypted in the Gameyfin database. */ override val configMetadata: PluginConfigMetadata = listOf( ConfigMetadata( key = "exampleConfigProperty", type = String::class.java, label = "Example Configuration Key", description = "This is an example configuration key for a Gameyfin plugin.", ), ConfigMetadata( key = "secretExampleConfigProperty", type = String::class.java, label = "Secret Example Configuration Key", description = "This is a secret configuration key for a Gameyfin plugin. It will be displayed as a password field in the UI.", isSecret = true ), ConfigMetadata( key = "optionalExampleConfigProperty", type = String::class.java, label = "Optional Example Configuration Key", description = "This is an optional configuration key for a Gameyfin plugin. It is not required to be set.", isRequired = false ), ConfigMetadata( key = "exampleConfigPropertyWithDefault", type = String::class.java, label = "Example Configuration Key with Default Value", description = "This is an example configuration key with a default value.", default = "default" ), ConfigMetadata( key = "exampleEnumConfigProperty", type = ExampleEnum::class.java, label = "Example Enum Configuration Key", description = "This is an example configuration key with an enum value and a default value. This will be displayed as a dropdown in the UI.", default = ExampleEnum.OPTION_ONE ) ) } ``` -------------------------------- ### Implement GameMetadataProvider extension with @Extension Source: https://gameyfin.org/plugins/tutorial Defines a Gameyfin plugin extension annotated with @Extension that implements GameMetadataProvider. It returns a hardcoded GameMetadata with title "Hello World Game" and originalId "hello-world-game" from fetchByTitle, and returns the same title with the passed id from fetchById. The companion object uses LoggerFactory for debug logging. ```kotlin @Extension class TutorialMetadataProvider : GameMetadataProvider { companion object { private val log = LoggerFactory.getLogger(this::class.java) } /** * Implement this method to provide game metadata from your plugin. * * The method receives a "gameId" which is usually the title of a video game. * The task of your plugin is to match this gameId to one or more video games and return metadata about it. * The metadata should be returned as a list of GameMetadata objects with the best match first and the worst match last. * If you could not find any matches, return an empty list. * * The required metadata fields are: * - title: The title of the game. * - originalId: The unique identifier of the game from your data source (e.g. "slug" from IGDB, "AppID" from Steam, ...). * * The remaining fields are optional, but you should try to fill them if possible: * - description: A short description of the game. * - coverUrl: A URL to the cover image of the game. * - screenshotUrls: A list of URLs to screenshots of the game. * - videoUrls: A list of URLs to videos of the game. * - releaseDate: The release date of the game. * - userRating: The user rating of the game (0-100). * - criticsRating: The critics rating of the game (0-100). * - developedBy: The developer(s) of the game. * - publishedBy: The publisher(s) of the game. * - genres: A list of genres the game belongs to. * - themes: A list of themes the game belongs to. * - keywords: A list of keywords associated with the game. * - features: A list of features the game has. * - perspectives: A list of perspectives the game can be played from. * * @param gameId This is the name of a file or folder that Gameyfin found in a game library. * @param maxResults The maximum number of results to return. You can return fewer results if you want. */ override fun fetchByTitle(gameTitle: String, maxResults: Int): List { // Log the gameId and maxResults for debugging purposes. log.debug("HelloWorldMetadataProvider: Fetching metadata for gameTitle: {} with maxResults: {}", gameTitle, maxResults) // For demonstration purposes, we will return a hardcoded example result. val exampleResult = GameMetadata( title = "Hello World Game", originalId = "hello-world-game" ) // Return a list containing the example result. return listOf(exampleResult) } /** * Implement this method to provide game metadata by its unique identifier. * * The method receives an "id" which is usually the unique identifier of a video game from your data source. * The task of your plugin is to match this id to a video game and return metadata about it. * If you could not find any matches, return null. * * @param id The unique identifier of the game in the source your plugin implements. * @return A GameMetadata object containing the metadata of the game, or null if no match was found. */ override fun fetchById(id: String): GameMetadata? { return GameMetadata( title = "Hello World Game", originalId = id ) } } ``` -------------------------------- ### Generate APP_KEY with openssl Source: https://gameyfin.org/installation/docker Generates a 32-byte Base64 encoded app key required for the APP_KEY environment variable. The key must be 16, 24, or 32 bytes long. ```bash openssl rand -base64 32 ``` -------------------------------- ### Configure build script for plugin JAR in build.gradle.kts Source: https://gameyfin.org/plugins/tutorial Configures the JAR task to build a plugin JAR with the manifest, dependencies, classes, resources, and logo. Also registers a copyDependencyClasses task and makes the build task depend on it. ```kotlin tasks.jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE isZip64 = true archiveBaseName.set("plugin-${project.name}") manifest { from("./src/main/resources/MANIFEST.MF") } from(configurations.runtimeClasspath.get().map { project.zipTree(it) }) { exclude("META-INF/*.SF") exclude("META-INF/*.DSA") exclude("META-INF/*.RSA") } from(sourceSets["main"].output.classesDirs) from(sourceSets["main"].resources) // Include logo file under META-INF/resources from("src/main/resources") { include("logo.*") into("META-INF/resources") } } tasks.register("copyDependencyClasses") { dependsOn(tasks.jar) duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(configurations.runtimeClasspath.get().map { project.zipTree(it) }) { include("**/*.class") } into(layout.buildDirectory.get().asFile.resolve("classes/kotlin/main")) } tasks.build { dependsOn("copyDependencyClasses") } ``` -------------------------------- ### Read version from MANIFEST.MF in build.gradle.kts Source: https://gameyfin.org/plugins/tutorial Reads the plugin version from the MANIFEST.MF file in resources, falling back to "1.0-SNAPSHOT" if the file does not exist. Add this to your build.gradle.kts to set the project version dynamically. ```kotlin // Read the version from the MANIFEST.MF file in resources val manifestFile = file("src/main/resources/MANIFEST.MF") val manifestVersion: String? = if (manifestFile.exists()) { Manifest(manifestFile.inputStream()).mainAttributes.getValue("Plugin-Version") } else null version = manifestVersion ?: "1.0-SNAPSHOT" ``` -------------------------------- ### Add plugin dependencies to build.gradle.kts Source: https://gameyfin.org/plugins/tutorial Adds the required dependencies for plugin development: the PF4J Kotlin symbol processor and the Gameyfin plugin API. Versions must match your Kotlin and Gameyfin versions respectively. ```kotlin dependencies { // ... other dependencies ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:2.1.21-1.0.2") // match with your Kotlin version compileOnly("org.gameyfin:plugin-api:2.0.0") // match with your Gameyfin version } ``` -------------------------------- ### Authentik property mapping expression for Gameyfin roles Source: https://gameyfin.org/guides/sso/authentik Property mapping expression for Authentik to assign Gameyfin roles based on group membership. Replace the group names with the ones you created. Users not in either group will automatically be assigned the "User" role. ```python if ak_is_group_member(request.user, name=""): return { "roles": ["GAMEYFIN_SUPERADMIN"] } if ak_is_group_member(request.user, name=""): return { "roles": ["GAMEYFIN_ADMIN"] } ``` -------------------------------- ### Add KSP plugin to build.gradle.kts Source: https://gameyfin.org/plugins/tutorial Adds the KSP (Kotlin Symbol Processing) plugin to your build.gradle.kts. The version must match your Kotlin version. ```kotlin plugins { //... other plugins id("com.google.devtools.ksp") version "2.1.21-2.0.1" // match with your Kotlin version } ``` -------------------------------- ### Override validateConfig for custom plugin config validation Source: https://gameyfin.org/plugins/tutorial Override validateConfig to add custom validation after the built-in checks. The built-in validation is optional but recommended; if it fails, return early. Use a mutable map to collect errors and return PluginConfigValidationResult.INVALID(errors) or VALID. ```kotlin override fun validateConfig(config: Map): PluginConfigValidationResult { // Use the built-in validation first (optional) val validationResult = super.validateConfig(config) if (!validationResult.isValid()) { // Return early if the built-in validation failed (also optional) return validationResult } // Custom validation logic // Create a mutable map to hold validation errors (or use the one from the previous validation) val errors = mutableMapOf() val exampleConfigValue = config["exampleConfigProperty"] if (exampleConfigValue != "helloworld") { errors["exampleConfigProperty"] = "Value must be 'helloworld'" } val exampleEnumConfigProperty = config["exampleEnumConfigProperty"] if(exampleEnumConfigProperty == null) { errors["exampleEnumConfigProperty"] = "This field is required" } else if(ExampleEnum.valueOf(exampleEnumConfigProperty) == ExampleEnum.OPTION_THREE) { errors["exampleEnumConfigProperty"] = "Option THREE is deprecated" } val secretExampleConfigValue = config["secretExampleConfigProperty"] if(secretExampleConfigValue == null) { errors["secretExampleConfigProperty"] = "This field is required" } else if (secretExampleConfigValue.length < 5) { errors["secretExampleConfigProperty"] = "Must be at least 5 characters long" } // If there are validation errors, return an invalid result with the errors // Otherwise, return a valid result return if(errors.isNotEmpty()) { PluginConfigValidationResult.INVALID(errors) } else { PluginConfigValidationResult.VALID } } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.