Update dependenicies

Update Plugin-API to allow multiple results per plugin
Add LibraryDetailsModal
Minor refactorings & bugfixes
This commit is contained in:
grimsi
2025-05-01 22:07:55 +02:00
parent 410172fee2
commit 5c0e06c1d7
19 changed files with 1329 additions and 1985 deletions
@@ -12,6 +12,7 @@ import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
import me.xdrop.fuzzywuzzy.FuzzySearch
import org.pf4j.Extension
import org.pf4j.PluginWrapper
import proto.Game
import java.time.Instant
class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
@@ -93,32 +94,40 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
"platforms.platform_logo.image_id"
).joinToString(",")
override fun fetchMetadata(gameId: String): GameMetadata? {
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
val findBySlugQuery = APICalypse()
.fields(QUERY_FIELDS)
.where("slug = \"${guessSlug(gameId)}\"")
// First step: Try to find the game by guessing the slug
var game = IGDBWrapper.games(findBySlugQuery).firstOrNull()
var games = IGDBWrapper.games(findBySlugQuery)
// Second step: Try a fuzzy search
if (game == null) {
// Note: Limit is intentionally set high because IGDBs ranking algorithm is not very good
if (games.isEmpty()) {
val searchByNameQuery = APICalypse()
.fields(QUERY_FIELDS)
.limit(100)
.search(gameId)
// Use IGDBs search function to get a list of games that match the search query
val games = IGDBWrapper.games(searchByNameQuery)
games = IGDBWrapper.games(searchByNameQuery)
if (games.isEmpty()) return null
if (games.isEmpty()) return emptyList()
// Use fuzzy search to find the best matching game name
val bestMatchingName = FuzzySearch.extractOne(gameId, games.map { it.name }).string
game = games.find { it.name == bestMatchingName } ?: return null
val bestMatchingTitles = FuzzySearch.extractTop(gameId, games.map { it.name }, maxResults)
games = bestMatchingTitles.mapNotNull { title -> games.find { it.name == title.string } }
}
return games.map { toGameMetadata(it) }
}
private fun guessSlug(gameId: String): String {
return gameId.replace(" ", "-").lowercase()
}
private fun toGameMetadata(game: Game): GameMetadata {
return GameMetadata(
originalId = game.slug,
title = game.name,
@@ -138,9 +147,5 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
perspectives = game.playerPerspectivesList.map { Mapper.playerPerspective(it) }.toSet()
)
}
private fun guessSlug(gameId: String): String {
return gameId.replace(" ", "-").lowercase()
}
}
}
@@ -52,6 +52,7 @@ class Mapper {
"fantasy" -> Theme.FANTASY
"horror" -> Theme.HORROR
"sci-fi" -> Theme.SCIENCE_FICTION
"science-fiction" -> Theme.SCIENCE_FICTION
"mystery" -> Theme.MYSTERY
"thriller" -> Theme.THRILLER
"survival" -> Theme.SURVIVAL
@@ -82,6 +83,7 @@ class Mapper {
"first-person" -> PlayerPerspective.FIRST_PERSON
"third-person" -> PlayerPerspective.THIRD_PERSON
"bird-view-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
"bird-view-slash-isometric" -> PlayerPerspective.BIRD_VIEW_ISOMETRIC
"side-view" -> PlayerPerspective.SIDE_VIEW
"text" -> PlayerPerspective.TEXT
"auditory" -> PlayerPerspective.AUDITORY
@@ -57,14 +57,14 @@ 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): GameMetadata? {
override fun fetchMetadata(gameId: String, maxResults: Int): List<GameMetadata> {
val searchResult: List<SteamGame> = runBlocking { searchStore(gameId) }
if (searchResult.isEmpty()) return null
if (searchResult.isEmpty()) return emptyList()
val bestMatchingTitle = FuzzySearch.extractOne(gameId, searchResult.map { it.name }).string
val bestMatch = searchResult.find { it.name == bestMatchingTitle } ?: return null
val bestMatchingTitles = FuzzySearch.extractTop(gameId, searchResult.map { it.name }, maxResults)
val bestMatches = bestMatchingTitles.mapNotNull { title -> searchResult.find { it.name == title.string } }
return runBlocking { getGameDetails(bestMatch.id) }
return runBlocking { bestMatches.map { getGameDetails(it.id) } }.filterNotNull()
}
private suspend fun searchStore(title: String): List<SteamGame> {
@@ -105,7 +105,7 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
// This is as much as I can get from the Steam Store API
val metadata = GameMetadata(
originalId = id.toString(),
title = game.name,
title = sanitizeTitle(game.name),
description = game.detailedDescription,
coverUrl = game.headerImage?.let { URI(it) },
release = game.releaseDate?.date,
@@ -119,5 +119,15 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
return metadata
}
/**
* Often titles on Steam copyright symbols which makes matching between different providers harder
* This method removes those symbols
*/
private fun sanitizeTitle(originalTitle: String): String {
val unwantedChars = setOf('™', '©', '®')
return originalTitle.filter { it !in unwantedChars }.trim()
}
}
}