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