From 0633bb14e7490e5e4ede422fa38ad2c0ac0bfd8a Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Thu, 12 Jun 2025 19:53:09 +0200 Subject: [PATCH] Extend GameMetadataProvider with fetchById --- .../de/grimsi/gameyfin/games/GameService.kt | 4 +- .../gamemetadata/GameMetadataProvider.kt | 4 +- .../grimsi/gameyfinplugins/igdb/IgdbPlugin.kt | 49 ++++++++++++------- .../gameyfinplugins/steam/SteamPlugin.kt | 11 +++-- .../steamgriddb/SteamGridDbPlugin.kt | 25 +++++++++- .../steamgriddb/api/SteamGridDbApiClient.kt | 4 ++ 6 files changed, 72 insertions(+), 25 deletions(-) diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt index 4299b74..6c0a381 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -124,7 +124,7 @@ class GameService( // 1. Query all plugins for up to 5 results each val results = metadataPlugins.flatMap { plugin -> try { - plugin.fetchMetadata(searchTerm, 5) + plugin.fetchByTitle(searchTerm, 5) // Filter out invalid results (null release or coverUrl) .filter { it.release != null && it.coverUrl != null } .map { plugin to it } @@ -240,7 +240,7 @@ class GameService( metadataPlugins.associateWith { async { try { - it.fetchMetadata(gameTitle).firstOrNull() + it.fetchByTitle(gameTitle).firstOrNull() } catch (e: Exception) { log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" } null diff --git a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt index 5dead4e..0e830cd 100644 --- a/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt +++ b/plugin-api/src/main/kotlin/de/grimsi/gameyfin/pluginapi/gamemetadata/GameMetadataProvider.kt @@ -3,5 +3,7 @@ package de.grimsi.gameyfin.pluginapi.gamemetadata import org.pf4j.ExtensionPoint interface GameMetadataProvider : ExtensionPoint { - fun fetchMetadata(gameId: String, maxResults: Int = 1): List + fun fetchByTitle(gameTitle: String, maxResults: Int = 1): List + + fun fetchById(id: String): GameMetadata? } \ No newline at end of file diff --git a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfinplugins/igdb/IgdbPlugin.kt b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfinplugins/igdb/IgdbPlugin.kt index c65bab1..610cf4e 100644 --- a/plugins/igdb/src/main/kotlin/de/grimsi/gameyfinplugins/igdb/IgdbPlugin.kt +++ b/plugins/igdb/src/main/kotlin/de/grimsi/gameyfinplugins/igdb/IgdbPlugin.kt @@ -126,37 +126,52 @@ class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) { ).joinToString(",") } - override fun fetchMetadata(gameId: String, maxResults: Int): List { - try { - // Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good - val searchByNameQuery = APICalypse() - .fields(QUERY_FIELDS) - .limit(100) - .search(gameId) + override fun fetchByTitle(gameTitle: String, maxResults: Int): List { + // Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good + val searchByNameQuery = APICalypse() + .fields(QUERY_FIELDS) + .limit(100) + .search(gameTitle) - // Use IGDBs search function to get a list of games that match the search query - var games = IGDBWrapper.games(searchByNameQuery) + // Use IGDBs search function to get a list of games that match the search query + var games = queryIgdbGames(searchByNameQuery) - if (games.isEmpty()) return emptyList() + if (games.isEmpty()) return emptyList() - // Use fuzzy search to find the best matching game name - val bestMatchingTitles = FuzzySearch.extractTop(gameId, games.map { it.name }, maxResults) - games = bestMatchingTitles.mapNotNull { title -> games.find { it.name == title.string } } + // Use fuzzy search to find the best matching game name + val bestMatchingTitles = FuzzySearch.extractTop(gameTitle, games.map { it.name }, maxResults) + games = bestMatchingTitles.mapNotNull { title -> games.find { it.name == title.string } } - return games.map { toGameMetadata(it) } + return games.map { toGameMetadata(it) } + } + + override fun fetchById(id: String): GameMetadata? { + // For slug we can limit the results to 1, since slugs are unique + val findBySlugQuery = APICalypse() + .fields(QUERY_FIELDS) + .limit(1) + .where("slug = \"$id\"") + + val game = queryIgdbGames(findBySlugQuery).firstOrNull() + return game?.let { toGameMetadata(it) } + } + + private fun queryIgdbGames(query: APICalypse): List { + return try { + IGDBWrapper.games(query) } catch (e: RequestException) { // FIXME: Handle rate limit errors with exponential backoff if (e.statusCode == 429) { val randomInterval = (1..5).random().toLong() log.warn("IGDB rate limit exceeded, retrying in $randomInterval seconds...") TimeUnit.SECONDS.sleep(randomInterval) - return fetchMetadata(gameId, maxResults) + + queryIgdbGames(query) } log.error("Request to IGDB API failed with HTTP ${e.statusCode}") + emptyList() } - - return emptyList() } private fun toGameMetadata(game: Game): GameMetadata { diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt index 73baeed..4e9bd32 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt @@ -48,16 +48,21 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { * The Steam Store API I am using provides far less info than IGDB for example * See it more as a proof of concept than a fully functional plugin **/ - override fun fetchMetadata(gameId: String, maxResults: Int): List { - val searchResult: List = runBlocking { searchStore(gameId) } + override fun fetchByTitle(gameTitle: String, maxResults: Int): List { + val searchResult: List = runBlocking { searchStore(gameTitle) } if (searchResult.isEmpty()) return emptyList() - val bestMatchingTitles = FuzzySearch.extractTop(gameId, searchResult.map { it.name }, maxResults) + val bestMatchingTitles = FuzzySearch.extractTop(gameTitle, searchResult.map { it.name }, maxResults) val bestMatches = bestMatchingTitles.mapNotNull { title -> searchResult.find { it.name == title.string } } return runBlocking { bestMatches.map { getGameDetails(it.id) } }.filterNotNull() } + override fun fetchById(id: String): GameMetadata? { + val id = id.toIntOrNull() ?: return null + return runBlocking { getGameDetails(id) } + } + private suspend fun searchStore(title: String): List { val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString()) return try { diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/SteamGridDbPlugin.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/SteamGridDbPlugin.kt index 85de4d3..0b4c241 100644 --- a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/SteamGridDbPlugin.kt +++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/SteamGridDbPlugin.kt @@ -72,9 +72,9 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra @Extension class SteamGridDBGameCoverProvider : GameMetadataProvider { - override fun fetchMetadata(gameId: String, maxResults: Int): List { + override fun fetchByTitle(gameTitle: String, maxResults: Int): List { return runBlocking { - var searchResults = searchSteamGridDb(gameId) + var searchResults = searchSteamGridDb(gameTitle) if (searchResults.isEmpty()) return@runBlocking emptyList() if (searchResults.size > maxResults) searchResults = searchResults.slice(0 until maxResults) @@ -91,6 +91,19 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra } } + override fun fetchById(id: String): GameMetadata? { + return runBlocking { + val gameId = id.toIntOrNull() ?: return@runBlocking null + val game = getGameById(gameId) ?: return@runBlocking null + + return@runBlocking GameMetadata( + originalId = game.id.toString(), + title = game.name, + coverUrl = getGridForGame(game.id)?.let { grid -> URI(grid.url) } + ) + } + } + private suspend fun searchSteamGridDb(term: String): List { val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized") @@ -110,5 +123,13 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra return gameDetails.data?.firstOrNull() } + + private suspend fun getGameById(gameId: Int): SteamGridDbGame? { + val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized") + + val gameDetails = client.game(gameId) + + return gameDetails.data?.firstOrNull() + } } } \ No newline at end of file diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt index 1b390e9..6cf7537 100644 --- a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt +++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt @@ -51,6 +51,10 @@ class SteamGridDbApiClient(private val apiKey: String) { }.body() } + suspend fun game(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbSearchResult { + return get("games/id/$gameId", block).body() + } + private suspend fun get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): HttpResponse { return client.get("$BASE_URL/$endpoint".encodeURLPath(encodeEncoded = false)) { bearerAuth(apiKey)