mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 16:20:04 +00:00
Extend GameMetadataProvider with fetchById
This commit is contained in:
@@ -124,7 +124,7 @@ class GameService(
|
|||||||
// 1. Query all plugins for up to 5 results each
|
// 1. Query all plugins for up to 5 results each
|
||||||
val results = metadataPlugins.flatMap { plugin ->
|
val results = metadataPlugins.flatMap { plugin ->
|
||||||
try {
|
try {
|
||||||
plugin.fetchMetadata(searchTerm, 5)
|
plugin.fetchByTitle(searchTerm, 5)
|
||||||
// Filter out invalid results (null release or coverUrl)
|
// Filter out invalid results (null release or coverUrl)
|
||||||
.filter { it.release != null && it.coverUrl != null }
|
.filter { it.release != null && it.coverUrl != null }
|
||||||
.map { plugin to it }
|
.map { plugin to it }
|
||||||
@@ -240,7 +240,7 @@ class GameService(
|
|||||||
metadataPlugins.associateWith {
|
metadataPlugins.associateWith {
|
||||||
async {
|
async {
|
||||||
try {
|
try {
|
||||||
it.fetchMetadata(gameTitle).firstOrNull()
|
it.fetchByTitle(gameTitle).firstOrNull()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" }
|
log.error(e) { "Error fetching metadata for game with plugin ${it.javaClass.name}" }
|
||||||
null
|
null
|
||||||
|
|||||||
+3
-1
@@ -3,5 +3,7 @@ package de.grimsi.gameyfin.pluginapi.gamemetadata
|
|||||||
import org.pf4j.ExtensionPoint
|
import org.pf4j.ExtensionPoint
|
||||||
|
|
||||||
interface GameMetadataProvider : ExtensionPoint {
|
interface GameMetadataProvider : ExtensionPoint {
|
||||||
fun fetchMetadata(gameId: String, maxResults: Int = 1): List<GameMetadata>
|
fun fetchByTitle(gameTitle: String, maxResults: Int = 1): List<GameMetadata>
|
||||||
|
|
||||||
|
fun fetchById(id: String): GameMetadata?
|
||||||
}
|
}
|
||||||
@@ -126,37 +126,52 @@ class IgdbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wrapper) {
|
|||||||
).joinToString(",")
|
).joinToString(",")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
|
override fun fetchByTitle(gameTitle: String, maxResults: Int): List<GameMetadata> {
|
||||||
try {
|
|
||||||
// Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good
|
// Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good
|
||||||
val searchByNameQuery = APICalypse()
|
val searchByNameQuery = APICalypse()
|
||||||
.fields(QUERY_FIELDS)
|
.fields(QUERY_FIELDS)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.search(gameId)
|
.search(gameTitle)
|
||||||
|
|
||||||
// Use IGDBs search function to get a list of games that match the search query
|
// Use IGDBs search function to get a list of games that match the search query
|
||||||
var games = IGDBWrapper.games(searchByNameQuery)
|
var games = queryIgdbGames(searchByNameQuery)
|
||||||
|
|
||||||
if (games.isEmpty()) return emptyList()
|
if (games.isEmpty()) return emptyList()
|
||||||
|
|
||||||
// Use fuzzy search to find the best matching game name
|
// Use fuzzy search to find the best matching game name
|
||||||
val bestMatchingTitles = FuzzySearch.extractTop(gameId, games.map { it.name }, maxResults)
|
val bestMatchingTitles = FuzzySearch.extractTop(gameTitle, games.map { it.name }, maxResults)
|
||||||
games = bestMatchingTitles.mapNotNull { title -> games.find { it.name == title.string } }
|
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<Game> {
|
||||||
|
return try {
|
||||||
|
IGDBWrapper.games(query)
|
||||||
} catch (e: RequestException) {
|
} catch (e: RequestException) {
|
||||||
// FIXME: Handle rate limit errors with exponential backoff
|
// FIXME: Handle rate limit errors with exponential backoff
|
||||||
if (e.statusCode == 429) {
|
if (e.statusCode == 429) {
|
||||||
val randomInterval = (1..5).random().toLong()
|
val randomInterval = (1..5).random().toLong()
|
||||||
log.warn("IGDB rate limit exceeded, retrying in $randomInterval seconds...")
|
log.warn("IGDB rate limit exceeded, retrying in $randomInterval seconds...")
|
||||||
TimeUnit.SECONDS.sleep(randomInterval)
|
TimeUnit.SECONDS.sleep(randomInterval)
|
||||||
return fetchMetadata(gameId, maxResults)
|
|
||||||
|
queryIgdbGames(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error("Request to IGDB API failed with HTTP ${e.statusCode}")
|
log.error("Request to IGDB API failed with HTTP ${e.statusCode}")
|
||||||
|
emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
return emptyList()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toGameMetadata(game: Game): GameMetadata {
|
private fun toGameMetadata(game: Game): GameMetadata {
|
||||||
|
|||||||
@@ -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
|
* 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
|
* See it more as a proof of concept than a fully functional plugin
|
||||||
**/
|
**/
|
||||||
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
|
override fun fetchByTitle(gameTitle: String, maxResults: Int): List<GameMetadata> {
|
||||||
val searchResult: List<SteamGame> = runBlocking { searchStore(gameId) }
|
val searchResult: List<SteamGame> = runBlocking { searchStore(gameTitle) }
|
||||||
if (searchResult.isEmpty()) return emptyList()
|
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 } }
|
val bestMatches = bestMatchingTitles.mapNotNull { title -> searchResult.find { it.name == title.string } }
|
||||||
|
|
||||||
return runBlocking { bestMatches.map { getGameDetails(it.id) } }.filterNotNull()
|
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<SteamGame> {
|
private suspend fun searchStore(title: String): List<SteamGame> {
|
||||||
val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString())
|
val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString())
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
+23
-2
@@ -72,9 +72,9 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
@Extension
|
@Extension
|
||||||
class SteamGridDBGameCoverProvider : GameMetadataProvider {
|
class SteamGridDBGameCoverProvider : GameMetadataProvider {
|
||||||
|
|
||||||
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
|
override fun fetchByTitle(gameTitle: String, maxResults: Int): List<GameMetadata> {
|
||||||
return runBlocking {
|
return runBlocking {
|
||||||
var searchResults = searchSteamGridDb(gameId)
|
var searchResults = searchSteamGridDb(gameTitle)
|
||||||
|
|
||||||
if (searchResults.isEmpty()) return@runBlocking emptyList()
|
if (searchResults.isEmpty()) return@runBlocking emptyList()
|
||||||
if (searchResults.size > maxResults) searchResults = searchResults.slice(0 until maxResults)
|
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<SteamGridDbGame> {
|
private suspend fun searchSteamGridDb(term: String): List<SteamGridDbGame> {
|
||||||
val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized")
|
val client = client ?: throw PluginConfigError("SteamGridDB API client not initialized")
|
||||||
|
|
||||||
@@ -110,5 +123,13 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
|
|
||||||
return gameDetails.data?.firstOrNull()
|
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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+4
@@ -51,6 +51,10 @@ class SteamGridDbApiClient(private val apiKey: String) {
|
|||||||
}.body()
|
}.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 {
|
private suspend fun get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
|
||||||
return client.get("$BASE_URL/$endpoint".encodeURLPath(encodeEncoded = false)) {
|
return client.get("$BASE_URL/$endpoint".encodeURLPath(encodeEncoded = false)) {
|
||||||
bearerAuth(apiKey)
|
bearerAuth(apiKey)
|
||||||
|
|||||||
Reference in New Issue
Block a user