From 432b27adfce2faca870ea2ea091587ff2895ec70 Mon Sep 17 00:00:00 2001
From: grimsi <9295182+grimsi@users.noreply.github.com>
Date: Sat, 10 May 2025 13:00:14 +0200
Subject: [PATCH] Add plugin for SteamGridDB game covers
---
.../general/modals/PluginPrioritiesModal.tsx | 9 +-
plugins/steam/build.gradle.kts | 6 +-
plugins/steamgriddb/build.gradle.kts | 15 +++
.../plugins/steamgriddb/SteamGridDbPlugin.kt | 104 ++++++++++++++++++
.../steamgriddb/api/SteamGridDbApiClient.kt | 54 +++++++++
.../dto/SteamGridDbGameOverview.kt | 15 +++
.../dto/SteamGridDbGridsDetails.kt | 17 +++
.../src/main/resources/MANIFEST.MF | 6 +
.../steamgriddb/src/main/resources/logo.svg | 1 +
settings.gradle.kts | 1 +
10 files changed, 218 insertions(+), 10 deletions(-)
create mode 100644 plugins/steamgriddb/build.gradle.kts
create mode 100644 plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt
create mode 100644 plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/api/SteamGridDbApiClient.kt
create mode 100644 plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGameOverview.kt
create mode 100644 plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGridsDetails.kt
create mode 100644 plugins/steamgriddb/src/main/resources/MANIFEST.MF
create mode 100644 plugins/steamgriddb/src/main/resources/logo.svg
diff --git a/gameyfin/src/main/frontend/components/general/modals/PluginPrioritiesModal.tsx b/gameyfin/src/main/frontend/components/general/modals/PluginPrioritiesModal.tsx
index 82a9c56..7339fd8 100644
--- a/gameyfin/src/main/frontend/components/general/modals/PluginPrioritiesModal.tsx
+++ b/gameyfin/src/main/frontend/components/general/modals/PluginPrioritiesModal.tsx
@@ -37,10 +37,8 @@ export default function PluginPrioritiesModal({plugins, isOpen, onOpenChange}: P
}
let {dragAndDropHooks} = useDragAndDrop({
- // @ts-ignore
getItems: (keys) =>
- // @ts-ignore
- [...keys].map((key) => ({'text/plain': sortedPlugins.getItem(key).name})),
+ [...keys].map((key) => ({'text/plain': sortedPlugins.getItem(key)!.name})),
onReorder(e) {
if (e.keys.has(e.target.key)) {
return; // Avoid placing a plugin before or after itself
@@ -105,8 +103,9 @@ export default function PluginPrioritiesModal({plugins, isOpen, onOpenChange}: P
key={plugin.id}
className="flex flex-row p-2 rounded-lg justify-between items-center bg-foreground/5">
-
{sortedPlugins.items.length - plugin.priority + 1}
+
+ {sortedPlugins.items.length - plugin.priority + 1}
+
{plugin.name}
diff --git a/plugins/steam/build.gradle.kts b/plugins/steam/build.gradle.kts
index 9c19c9c..bc0700e 100644
--- a/plugins/steam/build.gradle.kts
+++ b/plugins/steam/build.gradle.kts
@@ -1,14 +1,10 @@
-val ktor_version = "3.1.2"
+val ktor_version = "3.1.3"
plugins {
id("com.google.devtools.ksp")
kotlin("plugin.serialization")
}
-repositories {
- maven(url = "https://jitpack.io")
-}
-
dependencies {
ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}")
diff --git a/plugins/steamgriddb/build.gradle.kts b/plugins/steamgriddb/build.gradle.kts
new file mode 100644
index 0000000..7d24bfc
--- /dev/null
+++ b/plugins/steamgriddb/build.gradle.kts
@@ -0,0 +1,15 @@
+val ktor_version = "3.1.3"
+
+plugins {
+ id("com.google.devtools.ksp")
+ kotlin("plugin.serialization")
+}
+
+dependencies {
+ ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}")
+
+ implementation("io.ktor:ktor-client-core:${ktor_version}")
+ implementation("io.ktor:ktor-client-cio:${ktor_version}")
+ implementation("io.ktor:ktor-client-content-negotiation:${ktor_version}")
+ implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}")
+}
\ No newline at end of file
diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt
new file mode 100644
index 0000000..55529bd
--- /dev/null
+++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/SteamGridDbPlugin.kt
@@ -0,0 +1,104 @@
+package de.grimsi.gameyfin.plugins.steamgriddb
+
+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.GameMetadataProvider
+import de.grimsi.gameyfin.plugins.steamgriddb.api.SteamGridDbApiClient
+import de.grimsi.gameyfin.plugins.steamgriddb.dto.SteamGridDbGame
+import de.grimsi.gameyfin.plugins.steamgriddb.dto.SteamGridDbGrid
+import kotlinx.coroutines.runBlocking
+import org.pf4j.Extension
+import org.pf4j.PluginWrapper
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.net.URI
+
+class SteamGridDbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
+
+ companion object {
+ private var client: SteamGridDbApiClient? = null
+ }
+
+ val log: Logger = LoggerFactory.getLogger(javaClass)
+
+ override val configMetadata: List = listOf(
+ PluginConfigElement("apiKey", "SteamGridDB API key", "Your SteamGridDB API key", true)
+ )
+
+ override fun validateConfig(config: Map): Boolean {
+ try {
+ runBlocking { authenticate() }
+ return true
+ } catch (e: PluginConfigError) {
+ log.error(e.message)
+ return false
+ }
+ }
+
+ override fun start() {
+ try {
+ runBlocking { authenticate() }
+ } catch (e: PluginConfigError) {
+ log.error(e.message)
+ }
+ }
+
+ private suspend fun authenticate() {
+ log.debug("Authenticating on SteamGridDB API...")
+
+ val apiKey: String = config["apiKey"] ?: throw PluginConfigError("SteamGridDB API key not set")
+ val client = SteamGridDbApiClient(apiKey)
+
+ if (!client.isApiKeyValid()) {
+ throw PluginConfigError("Failed to authenticate on SteamGridDB API with provided credentials")
+ }
+
+ SteamGridDbPlugin.client = client
+ log.debug("Authentication successful")
+ }
+
+ @Extension
+ class SteamGridDBGameCoverProvider : GameMetadataProvider {
+
+ override fun fetchMetadata(gameId: String, maxResults: Int): List {
+ return runBlocking {
+ var searchResults = searchSteamGridDb(gameId)
+
+ if (searchResults.isEmpty()) return@runBlocking emptyList()
+ if (searchResults.size > maxResults) searchResults = searchResults.slice(0 until maxResults)
+
+ return@runBlocking searchResults
+ .map { game ->
+ GameMetadata(
+ originalId = game.id.toString(),
+ title = game.name,
+ coverUrl = getGridForGame(game.id)?.let { grid -> URI(grid.url) }
+ )
+ }
+ .filter { it.coverUrl != null }
+ }
+ }
+
+ private suspend fun searchSteamGridDb(term: String): List {
+ val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized")
+
+ val searchResult = client.search(term)
+
+ return if (searchResult.success && searchResult.data !== null) {
+ searchResult.data
+ } else {
+ emptyList()
+ }
+ }
+
+ private suspend fun getGridForGame(gameId: Int): SteamGridDbGrid? {
+ val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized")
+
+ val gameDetails = client.grids(gameId)
+
+ return gameDetails.data?.firstOrNull()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/api/SteamGridDbApiClient.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/api/SteamGridDbApiClient.kt
new file mode 100644
index 0000000..795d463
--- /dev/null
+++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/api/SteamGridDbApiClient.kt
@@ -0,0 +1,54 @@
+package de.grimsi.gameyfin.plugins.steamgriddb.api
+
+import de.grimsi.gameyfin.plugins.steamgriddb.dto.SteamGridDbGridResult
+import de.grimsi.gameyfin.plugins.steamgriddb.dto.SteamGridDbSearchResult
+import io.ktor.client.*
+import io.ktor.client.call.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.plugins.contentnegotiation.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.serialization.kotlinx.json.*
+import kotlinx.serialization.json.Json
+
+
+class SteamGridDbApiClient(private val apiKey: String) {
+ companion object {
+ private val json = Json {
+ isLenient = true
+ ignoreUnknownKeys = true
+ }
+ private const val BASE_URL = "https://www.steamgriddb.com/api/v2"
+ }
+
+ private val client = HttpClient(CIO) {
+ install(ContentNegotiation) {
+ json(json)
+ }
+ }
+
+ suspend fun isApiKeyValid(): Boolean {
+ return try {
+ val response = get("grids/game/1")
+ response.status == HttpStatusCode.OK
+ } catch (_: Exception) {
+ false
+ }
+ }
+
+ suspend fun search(term: String, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbSearchResult {
+ return get("search/autocomplete/$term", block).body()
+ }
+
+ suspend fun grids(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGridResult {
+ return get("grids/game/$gameId", block).body()
+ }
+
+ private suspend fun get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
+ return client.get("$BASE_URL/$endpoint".encodeURLPath(encodeEncoded = false)) {
+ bearerAuth(apiKey)
+ block()
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGameOverview.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGameOverview.kt
new file mode 100644
index 0000000..ac29014
--- /dev/null
+++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGameOverview.kt
@@ -0,0 +1,15 @@
+package de.grimsi.gameyfin.plugins.steamgriddb.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SteamGridDbSearchResult(
+ val success: Boolean,
+ val data: List?
+)
+
+@Serializable
+data class SteamGridDbGame(
+ val id: Int,
+ val name: String
+)
\ No newline at end of file
diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGridsDetails.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGridsDetails.kt
new file mode 100644
index 0000000..50b9c7d
--- /dev/null
+++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfin/plugins/steamgriddb/dto/SteamGridDbGridsDetails.kt
@@ -0,0 +1,17 @@
+package de.grimsi.gameyfin.plugins.steamgriddb.dto
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class SteamGridDbGridResult(
+ val success: Boolean,
+ val data: List?
+)
+
+@Serializable
+data class SteamGridDbGrid(
+ val id: Int,
+ val width: Int,
+ val height: Int,
+ val url: String
+)
\ No newline at end of file
diff --git a/plugins/steamgriddb/src/main/resources/MANIFEST.MF b/plugins/steamgriddb/src/main/resources/MANIFEST.MF
new file mode 100644
index 0000000..af74eb6
--- /dev/null
+++ b/plugins/steamgriddb/src/main/resources/MANIFEST.MF
@@ -0,0 +1,6 @@
+Manifest-Version: 1.0
+Plugin-Class: de.grimsi.gameyfin.plugins.steamgriddb.SteamGridDbPlugin
+Plugin-Id: steamgriddb
+Plugin-Description: Steam Grid DB covers
+Plugin-Version: 1.0.0-alpha1
+Plugin-Provider: grimsi
diff --git a/plugins/steamgriddb/src/main/resources/logo.svg b/plugins/steamgriddb/src/main/resources/logo.svg
new file mode 100644
index 0000000..ed8e149
--- /dev/null
+++ b/plugins/steamgriddb/src/main/resources/logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5272b9b..9d65d3d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -25,3 +25,4 @@ include(":plugins")
include(":plugins:igdb")
include(":plugins:steam")
+include(":plugins:steamgriddb")
\ No newline at end of file