From dd145b466f14951bea08815241068171896b2a70 Mon Sep 17 00:00:00 2001 From: grimsi <9295182+grimsi@users.noreply.github.com> Date: Sun, 30 Mar 2025 12:58:56 +0200 Subject: [PATCH] Improve game matching algorithm --- gameyfin/build.gradle.kts | 5 ++- .../kotlin/de/grimsi/gameyfin/core/Utils.kt | 6 +++- .../de/grimsi/gameyfin/games/GameService.kt | 35 +++++++++++++++++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/gameyfin/build.gradle.kts b/gameyfin/build.gradle.kts index 9d0435b..fcef41b 100644 --- a/gameyfin/build.gradle.kts +++ b/gameyfin/build.gradle.kts @@ -55,7 +55,6 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("com.github.paulcwarren:spring-content-fs-boot-starter:3.0.15") implementation("commons-io:commons-io:2.18.0") - implementation("org.apache.tika:tika-core:3.1.0") // SSO implementation("org.springframework.boot:spring-boot-starter-oauth2-client") @@ -70,6 +69,10 @@ dependencies { implementation(project(":plugin-api")) ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}") + // Utils + implementation("org.apache.tika:tika-core:3.1.0") + implementation("me.xdrop:fuzzywuzzy:1.4.0") + // Development developmentOnly("org.springframework.boot:spring-boot-devtools") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") diff --git a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt index e988d1c..d4aa5dc 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/core/Utils.kt @@ -9,6 +9,7 @@ import org.springframework.web.context.request.RequestContextHolder import org.springframework.web.context.request.ServletRequestAttributes import java.io.InputStream + class Utils { companion object { private val tika = Tika() @@ -48,4 +49,7 @@ class Utils { .body(inputStreamResource) } } -} \ No newline at end of file +} + +@Suppress("UNCHECKED_CAST") +fun Map.filterValuesNotNull() = filterValues { it != null } as Map \ No newline at end of file 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 0938531..2395c57 100644 --- a/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt +++ b/gameyfin/src/main/kotlin/de/grimsi/gameyfin/games/GameService.kt @@ -1,5 +1,6 @@ package de.grimsi.gameyfin.games +import de.grimsi.gameyfin.core.filterValuesNotNull import de.grimsi.gameyfin.core.plugins.management.PluginManagementService import de.grimsi.gameyfin.games.dto.GameDto import de.grimsi.gameyfin.games.dto.GameMetadataDto @@ -14,6 +15,7 @@ import io.github.oshai.kotlinlogging.KotlinLogging import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking +import me.xdrop.fuzzywuzzy.FuzzySearch import org.pf4j.PluginManager import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -43,17 +45,29 @@ class GameService( } fun createFromFile(path: Path): GameDto { - val metadataResults = queryPlugins(path.fileName.toString()) - val validResults = metadataResults.filterValues { it != null } + val query = path.fileName.toString() + + // Step 0: Query all metadata plugins for metadata on the provided game title + val metadataResults = queryPlugins(query) + + // Step 1: Filter out invalid (empty) results + val validResults = metadataResults.filterValuesNotNull() if (validResults.isEmpty()) { throw NoMatchException("Could not match game at $path") } - val sortedResults = validResults.entries.sortedByDescending { + // Step 2: Filter results to find the best matching title + val filteredResults = filterResults(query, validResults) + + // Step 3: Sort results by plugin priority + val sortedResults = filteredResults.entries.sortedByDescending { pluginManagementService.getPluginManagementEntry(it.key.javaClass).priority } + // Step 4: Merge results into a single Game entity val mergedGame = mergeResults(sortedResults, path) + + // Step 5: Save the new game val savedGame = createOrUpdate(mergedGame) return toDto(savedGame) @@ -94,6 +108,21 @@ class GameService( } } + /** + * Determines the closest matching title from the results and filters out any other results + */ + private fun filterResults( + originalQuery: String, + results: Map + ): Map { + val availableTitles = results.map { it.value.title } + val bestMatchingTitle = FuzzySearch.extractOne(originalQuery, availableTitles).string + + log.info { "Best matching title: '$bestMatchingTitle' for '$originalQuery' determined from $availableTitles" } + + return results.filter { it.value.title == bestMatchingTitle } + } + private fun mergeResults(results: List>, path: Path): Game { val mergedGame = Game(path = path.toString()) val metadataMap = mutableMapOf()