mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Proof of concept implementation for a Steam metadata provider plugin
Mainly for testing purposes
This commit is contained in:
@@ -7,4 +7,7 @@ dependencies {
|
||||
|
||||
// IGDB API client
|
||||
implementation("io.github.husnjak:igdb-api-jvm:1.2.0")
|
||||
|
||||
// Fuzzy string matching
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
}
|
||||
@@ -8,7 +8,8 @@ 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.GameMetadataFetcher
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import org.pf4j.Extension
|
||||
import org.pf4j.PluginWrapper
|
||||
import java.time.Instant
|
||||
@@ -48,15 +49,32 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
}
|
||||
|
||||
@Extension
|
||||
class IgdbMetadataFetcher : GameMetadataFetcher {
|
||||
override fun fetchMetadata(gameId: String): GameMetadata {
|
||||
val findGameByName = APICalypse()
|
||||
class IgdbMetadataProvider : GameMetadataProvider {
|
||||
override fun fetchMetadata(gameId: String): GameMetadata? {
|
||||
val findBySlugQuery = APICalypse()
|
||||
.fields("*")
|
||||
.limit(100)
|
||||
.search(gameId)
|
||||
.where("slug = \"${guessSlug(gameId)}\"")
|
||||
|
||||
val game = IGDBWrapper.games(findGameByName).filter { it.slug == gameId.lowercase() }.firstOrNull()
|
||||
?: throw IllegalArgumentException("Could not match game with ID '$gameId'")
|
||||
// First step: Try to find the game by guessing the slug
|
||||
var game = IGDBWrapper.games(findBySlugQuery).firstOrNull()
|
||||
|
||||
// Second step: Try a fuzzy search
|
||||
if (game == null) {
|
||||
val searchByNameQuery = APICalypse()
|
||||
.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)
|
||||
|
||||
if (games.isEmpty()) return null
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
return GameMetadata(
|
||||
title = game.name,
|
||||
@@ -74,5 +92,9 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
perspectives = listOf()
|
||||
)
|
||||
}
|
||||
|
||||
private fun guessSlug(gameId: String): String {
|
||||
return gameId.replace(" ", "-").lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
Manifest-Version: 1.0
|
||||
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
|
||||
Plugin-Id: igdb
|
||||
Plugin-Description: IGDB Plugin
|
||||
Plugin-Description: IGDB Metadata
|
||||
Plugin-Version: 1.0.0-alpha1
|
||||
Plugin-Provider: grimsi
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
val ktor_version = "3.0.0"
|
||||
|
||||
plugins {
|
||||
id("com.google.devtools.ksp") version "2.0.20-1.0.24"
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven(url = "https://jitpack.io")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:2.0.20-1.0.1")
|
||||
|
||||
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")
|
||||
|
||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package de.grimsi.gameyfin.plugins.steam
|
||||
|
||||
import de.grimsi.gameyfin.pluginapi.core.GameyfinPlugin
|
||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigElement
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.engine.cio.CIO
|
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import me.xdrop.fuzzywuzzy.FuzzySearch
|
||||
import org.pf4j.Extension
|
||||
import org.pf4j.PluginWrapper
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.time.Instant
|
||||
import java.time.LocalDate
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
|
||||
@Serializable
|
||||
data class SteamSearchResult(
|
||||
val total: Int,
|
||||
val items: List<SteamGame>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SteamGame(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val id: Int,
|
||||
val price: Price?,
|
||||
val tiny_image: String,
|
||||
val metascore: String?,
|
||||
val platforms: Platforms,
|
||||
val streamingvideo: Boolean,
|
||||
val controller_support: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Price(
|
||||
val currency: String,
|
||||
val initial: Int,
|
||||
val final: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Platforms(
|
||||
val windows: Boolean,
|
||||
val mac: Boolean,
|
||||
val linux: Boolean
|
||||
)
|
||||
|
||||
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||
override val configMetadata: List<PluginConfigElement> = emptyList()
|
||||
|
||||
@Extension
|
||||
class SteamMetadataProvider : GameMetadataProvider {
|
||||
val client = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json()
|
||||
}
|
||||
}
|
||||
|
||||
override fun fetchMetadata(gameId: String): GameMetadata? {
|
||||
val searchResult: List<SteamGame> = runBlocking { searchStore(gameId) }
|
||||
if (searchResult.isEmpty()) return null
|
||||
|
||||
val bestMatchingTitle = FuzzySearch.extractOne(gameId, searchResult.map { it.name }).string
|
||||
val bestMatch = searchResult.find { it.name == bestMatchingTitle } ?: return null
|
||||
|
||||
return runBlocking { getGameDetails(bestMatch.id) }
|
||||
}
|
||||
|
||||
private suspend fun searchStore(title: String): List<SteamGame> {
|
||||
val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString())
|
||||
val url = "https://store.steampowered.com/api/storesearch?term=$encodedTitle&cc=en"
|
||||
return try {
|
||||
val searchResult: SteamSearchResult = client.get(url).body()
|
||||
searchResult.items
|
||||
} catch (e: Exception) {
|
||||
println(e.message)
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getGameDetails(id: Int): GameMetadata? {
|
||||
val url = "https://store.steampowered.com/api/appdetails?appids=$id"
|
||||
val response: JsonObject = (client.get(url).body() as JsonObject)[id.toString()]?.jsonObject ?: return null
|
||||
|
||||
if (response["success"]?.jsonPrimitive?.boolean == false) return null
|
||||
|
||||
val game = response["data"]?.jsonObject ?: return null
|
||||
|
||||
val metadata = GameMetadata(
|
||||
title = string(game, "name"),
|
||||
description = string(game, "detailed_description"),
|
||||
release = date(game["release_date"]?.jsonObject["date"]?.jsonPrimitive?.content!!),
|
||||
userRating = 0,
|
||||
criticRating = 0,
|
||||
developedBy = stringList(game, "developers"),
|
||||
publishedBy = stringList(game, "publishers"),
|
||||
genres = emptyList(),
|
||||
themes = emptyList(),
|
||||
screenshotUrls = emptyList(),
|
||||
videoUrls = emptyList(),
|
||||
features = emptyList(),
|
||||
perspectives = emptyList()
|
||||
)
|
||||
|
||||
return metadata
|
||||
}
|
||||
|
||||
private fun string(json: JsonObject, key: String): String {
|
||||
return json[key]?.jsonPrimitive?.content ?: ""
|
||||
}
|
||||
|
||||
private fun stringList(json: JsonObject, key: String): List<String> {
|
||||
return json[key]?.jsonArray?.map { it.jsonPrimitive.content } ?: emptyList()
|
||||
}
|
||||
|
||||
fun date(dateString: String): Instant {
|
||||
val formatter = DateTimeFormatter.ofPattern("dd MMM, yyyy", Locale.ENGLISH)
|
||||
val localDate = LocalDate.parse(dateString, formatter)
|
||||
return localDate.atStartOfDay().toInstant(ZoneOffset.UTC)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
Manifest-Version: 1.0
|
||||
Plugin-Class: de.grimsi.gameyfin.plugins.steam.SteamPlugin
|
||||
Plugin-Id: steam
|
||||
Plugin-Description: Steam Metadata
|
||||
Plugin-Version: 1.0.0-alpha1
|
||||
Plugin-Provider: grimsi
|
||||
Reference in New Issue
Block a user