mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
Proof of concept implementation for a Steam metadata provider plugin
Mainly for testing purposes
This commit is contained in:
@@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Rebuild all" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="build" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="Rebuild plugins" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$/plugins" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value="build" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@@ -1,9 +1,21 @@
|
|||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Button} from "@nextui-org/react";
|
import {Button, Input} from "@nextui-org/react";
|
||||||
import {toast} from "sonner";
|
import {toast} from "sonner";
|
||||||
import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints.js";
|
import {LibraryEndpoint, SystemEndpoint} from "Frontend/generated/endpoints.js";
|
||||||
|
import {useState} from "react";
|
||||||
|
import Game from "Frontend/generated/de/grimsi/gameyfin/games/Game";
|
||||||
|
|
||||||
export default function TestView() {
|
export default function TestView() {
|
||||||
|
const [gameTitle, setGameTitle] = useState("");
|
||||||
|
const [game, setGame] = useState<Game>();
|
||||||
|
|
||||||
|
function getGame() {
|
||||||
|
LibraryEndpoint.test(gameTitle).then(game => {
|
||||||
|
if (game == undefined) return;
|
||||||
|
setGame(game);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grow justify-center mt-12">
|
<div className="grow justify-center mt-12">
|
||||||
<div className="flex flex-col items-center gap-6">
|
<div className="flex flex-col items-center gap-6">
|
||||||
@@ -38,7 +50,11 @@ export default function TestView() {
|
|||||||
})}>Toast (Error)</Button>
|
})}>Toast (Error)</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button onPress={() => SystemEndpoint.restart()}>Restart</Button>
|
<Button onPress={() => SystemEndpoint.restart()}>Restart</Button>
|
||||||
<Button onPress={() => LibraryEndpoint.test("Tetris")}>Test IGDB plugin</Button>
|
<div className="flex flex-row gap-4 items-center">
|
||||||
|
<Input label="Game title" onValueChange={setGameTitle}/>
|
||||||
|
<Button onPress={getGame} size="lg">Match</Button>
|
||||||
|
</div>
|
||||||
|
{game && <>{JSON.stringify(game, null, 2)}</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package de.grimsi.gameyfin.games
|
package de.grimsi.gameyfin.games
|
||||||
|
|
||||||
import jakarta.persistence.*
|
import jakarta.persistence.*
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
class Game(
|
class Game(
|
||||||
@@ -18,6 +19,16 @@ class Game(
|
|||||||
@Column(columnDefinition = "CLOB")
|
@Column(columnDefinition = "CLOB")
|
||||||
val summary: String,
|
val summary: String,
|
||||||
|
|
||||||
|
val release: Instant,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val publishers: List<String>,
|
||||||
|
|
||||||
|
@ElementCollection
|
||||||
|
val developers: List<String>,
|
||||||
|
|
||||||
@Column(unique = true)
|
@Column(unique = true)
|
||||||
val path: String
|
val path: String,
|
||||||
|
|
||||||
|
val source: String
|
||||||
)
|
)
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
package de.grimsi.gameyfin.games
|
package de.grimsi.gameyfin.games
|
||||||
|
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataFetcher
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
||||||
|
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadataProvider
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.pf4j.PluginManager
|
import org.pf4j.PluginManager
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -14,8 +18,8 @@ class GameService(
|
|||||||
) {
|
) {
|
||||||
private val log = KotlinLogging.logger {}
|
private val log = KotlinLogging.logger {}
|
||||||
|
|
||||||
private val metadataPlugins: List<GameMetadataFetcher>
|
private val metadataPlugins: List<GameMetadataProvider>
|
||||||
get() = pluginManager.getExtensions(GameMetadataFetcher::class.java)
|
get() = pluginManager.getExtensions(GameMetadataProvider::class.java)
|
||||||
|
|
||||||
fun createOrUpdate(game: Game): Game {
|
fun createOrUpdate(game: Game): Game {
|
||||||
gameRepository.findByPath(game.path)?.let {
|
gameRepository.findByPath(game.path)?.let {
|
||||||
@@ -25,12 +29,34 @@ class GameService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun createFromFile(path: Path): Game {
|
fun createFromFile(path: Path): Game {
|
||||||
val metadata = metadataPlugins.first().fetchMetadata(path.fileName.toString())
|
val metadataResults: Map<GameMetadataProvider, GameMetadata?> = runBlocking {
|
||||||
|
coroutineScope {
|
||||||
|
metadataPlugins.associateWith {
|
||||||
|
async {
|
||||||
|
try {
|
||||||
|
it.fetchMetadata(path.fileName.toString())
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error(e) { "Error fetching metadata with plugin ${it.javaClass.name}" }
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}.await()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (plugin, metadata) = metadataResults.entries.firstOrNull { it.value != null }
|
||||||
|
?: throw NoMatchException("Could not match game at $path")
|
||||||
|
|
||||||
val game = Game(
|
val game = Game(
|
||||||
title = metadata.title,
|
title = metadata!!.title,
|
||||||
summary = metadata.description,
|
summary = metadata.description,
|
||||||
path = path.toString()
|
release = metadata.release,
|
||||||
|
publishers = metadata.publishedBy,
|
||||||
|
developers = metadata.developedBy,
|
||||||
|
path = path.toString(),
|
||||||
|
source = plugin.javaClass.name
|
||||||
)
|
)
|
||||||
|
|
||||||
return createOrUpdate(game)
|
return createOrUpdate(game)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package de.grimsi.gameyfin.games
|
||||||
|
|
||||||
|
class NoMatchException(message: String) : RuntimeException(message)
|
||||||
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.libraries
|
|||||||
|
|
||||||
import com.vaadin.hilla.Endpoint
|
import com.vaadin.hilla.Endpoint
|
||||||
import de.grimsi.gameyfin.core.Role
|
import de.grimsi.gameyfin.core.Role
|
||||||
|
import de.grimsi.gameyfin.games.Game
|
||||||
import jakarta.annotation.security.RolesAllowed
|
import jakarta.annotation.security.RolesAllowed
|
||||||
|
|
||||||
@Endpoint
|
@Endpoint
|
||||||
@@ -19,7 +20,7 @@ class LibraryEndpoint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RolesAllowed(Role.Names.ADMIN)
|
@RolesAllowed(Role.Names.ADMIN)
|
||||||
fun test(testString: String) {
|
fun test(testString: String): Game {
|
||||||
libraryService.test(testString)
|
return libraryService.test(testString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package de.grimsi.gameyfin.libraries
|
|||||||
|
|
||||||
import de.grimsi.gameyfin.config.ConfigProperties
|
import de.grimsi.gameyfin.config.ConfigProperties
|
||||||
import de.grimsi.gameyfin.config.ConfigService
|
import de.grimsi.gameyfin.config.ConfigService
|
||||||
|
import de.grimsi.gameyfin.games.Game
|
||||||
import de.grimsi.gameyfin.games.GameService
|
import de.grimsi.gameyfin.games.GameService
|
||||||
import org.springframework.data.repository.findByIdOrNull
|
import org.springframework.data.repository.findByIdOrNull
|
||||||
import org.springframework.stereotype.Service
|
import org.springframework.stereotype.Service
|
||||||
@@ -17,8 +18,8 @@ class LibraryService(
|
|||||||
private val gameService: GameService,
|
private val gameService: GameService,
|
||||||
private val config: ConfigService
|
private val config: ConfigService
|
||||||
) {
|
) {
|
||||||
fun test(testString: String) {
|
fun test(testString: String): Game {
|
||||||
gameService.createFromFile(Path(testString))
|
return gameService.createFromFile(Path(testString))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOrUpdate(library: LibraryDto): LibraryDto {
|
fun createOrUpdate(library: LibraryDto): LibraryDto {
|
||||||
|
|||||||
-7
@@ -1,7 +0,0 @@
|
|||||||
package de.grimsi.gameyfin.pluginapi.gamemetadata
|
|
||||||
|
|
||||||
import org.pf4j.ExtensionPoint
|
|
||||||
|
|
||||||
interface GameMetadataFetcher : ExtensionPoint {
|
|
||||||
fun fetchMetadata(gameId: String): GameMetadata
|
|
||||||
}
|
|
||||||
+7
@@ -0,0 +1,7 @@
|
|||||||
|
package de.grimsi.gameyfin.pluginapi.gamemetadata
|
||||||
|
|
||||||
|
import org.pf4j.ExtensionPoint
|
||||||
|
|
||||||
|
interface GameMetadataProvider : ExtensionPoint {
|
||||||
|
fun fetchMetadata(gameId: String): GameMetadata?
|
||||||
|
}
|
||||||
@@ -7,4 +7,7 @@ dependencies {
|
|||||||
|
|
||||||
// IGDB API client
|
// IGDB API client
|
||||||
implementation("io.github.husnjak:igdb-api-jvm:1.2.0")
|
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.PluginConfigElement
|
||||||
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
import de.grimsi.gameyfin.pluginapi.core.PluginConfigError
|
||||||
import de.grimsi.gameyfin.pluginapi.gamemetadata.GameMetadata
|
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.Extension
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -48,15 +49,32 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Extension
|
@Extension
|
||||||
class IgdbMetadataFetcher : GameMetadataFetcher {
|
class IgdbMetadataProvider : GameMetadataProvider {
|
||||||
override fun fetchMetadata(gameId: String): GameMetadata {
|
override fun fetchMetadata(gameId: String): GameMetadata? {
|
||||||
val findGameByName = APICalypse()
|
val findBySlugQuery = APICalypse()
|
||||||
.fields("*")
|
.fields("*")
|
||||||
.limit(100)
|
.where("slug = \"${guessSlug(gameId)}\"")
|
||||||
.search(gameId)
|
|
||||||
|
|
||||||
val game = IGDBWrapper.games(findGameByName).filter { it.slug == gameId.lowercase() }.firstOrNull()
|
// First step: Try to find the game by guessing the slug
|
||||||
?: throw IllegalArgumentException("Could not match game with ID '$gameId'")
|
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(
|
return GameMetadata(
|
||||||
title = game.name,
|
title = game.name,
|
||||||
@@ -74,5 +92,9 @@ class IgdbPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
perspectives = listOf()
|
perspectives = listOf()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun guessSlug(gameId: String): String {
|
||||||
|
return gameId.replace(" ", "-").lowercase()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
Manifest-Version: 1.0
|
Manifest-Version: 1.0
|
||||||
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
|
Plugin-Class: de.grimsi.gameyfin.plugins.igdb.IgdbPlugin
|
||||||
Plugin-Id: igdb
|
Plugin-Id: igdb
|
||||||
Plugin-Description: IGDB Plugin
|
Plugin-Description: IGDB Metadata
|
||||||
Plugin-Version: 1.0.0-alpha1
|
Plugin-Version: 1.0.0-alpha1
|
||||||
Plugin-Provider: grimsi
|
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
|
||||||
@@ -12,6 +12,7 @@ pluginManagement {
|
|||||||
kotlin("jvm") version extra["kotlinVersion"] as String
|
kotlin("jvm") version extra["kotlinVersion"] as String
|
||||||
kotlin("plugin.spring") version extra["kotlinVersion"] as String
|
kotlin("plugin.spring") version extra["kotlinVersion"] as String
|
||||||
kotlin("plugin.jpa") version extra["kotlinVersion"] as String
|
kotlin("plugin.jpa") version extra["kotlinVersion"] as String
|
||||||
|
kotlin("plugin.serialization") version extra["kotlinVersion"] as String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,3 +23,4 @@ include(":gameyfin")
|
|||||||
include(":plugins")
|
include(":plugins")
|
||||||
|
|
||||||
include(":plugins:igdb")
|
include(":plugins:igdb")
|
||||||
|
include(":plugins:steam")
|
||||||
|
|||||||
Reference in New Issue
Block a user