Add plugin for SteamGridDB game covers

This commit is contained in:
grimsi
2025-05-10 13:00:14 +02:00
parent e49f61a1db
commit 432b27adfc
10 changed files with 218 additions and 10 deletions
@@ -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">
<div className="flex flex-row gap-2 items-center">
<Chip size="sm"
color="primary">{sortedPlugins.items.length - plugin.priority + 1}</Chip>
<Chip size="sm" color="primary">
{sortedPlugins.items.length - plugin.priority + 1}
</Chip>
<p className="font-normal text-small">{plugin.name}</p>
</div>
<CaretUpDown/>
+1 -5
View File
@@ -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"]}")
+15
View File
@@ -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}")
}
@@ -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<PluginConfigElement> = listOf(
PluginConfigElement("apiKey", "SteamGridDB API key", "Your SteamGridDB API key", true)
)
override fun validateConfig(config: Map<String, String?>): 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<GameMetadata> {
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<SteamGridDbGame> {
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()
}
}
}
@@ -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()
}
}
}
@@ -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<SteamGridDbGame>?
)
@Serializable
data class SteamGridDbGame(
val id: Int,
val name: String
)
@@ -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<SteamGridDbGrid>?
)
@Serializable
data class SteamGridDbGrid(
val id: Int,
val width: Int,
val height: Int,
val url: String
)
@@ -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
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1" viewBox="0 0 50 24"><path fill="#305b79" d="M0 0h41v18H0V0z"/><path fill="#4787b4" d="M3 2h41v18H3V2z"/><path fill="#5fb4f0" d="M6 4h41v18H6V4z"/><path fill="#3a6e92" d="M9 6h41v18H9V6z"/></svg>

After

Width:  |  Height:  |  Size: 246 B

+1
View File
@@ -25,3 +25,4 @@ include(":plugins")
include(":plugins:igdb")
include(":plugins:steam")
include(":plugins:steamgriddb")