mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-16 00:30:02 +00:00
Merge pull request #602 from gameyfin/feature/improve-plugins
General plugin improvements
This commit is contained in:
@@ -67,6 +67,10 @@ class GameyfinPluginManager(
|
|||||||
return GameyfinManifestPluginDescriptorFinder()
|
return GameyfinManifestPluginDescriptorFinder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createExtensionFactory(): ExtensionFactory {
|
||||||
|
return SingletonExtensionFactory(this)
|
||||||
|
}
|
||||||
|
|
||||||
override fun createExtensionFinder(): ExtensionFinder? {
|
override fun createExtensionFinder(): ExtensionFinder? {
|
||||||
val extensionFinder = GameyfinExtensionFinder(this)
|
val extensionFinder = GameyfinExtensionFinder(this)
|
||||||
addPluginStateListener(extensionFinder)
|
addPluginStateListener(extensionFinder)
|
||||||
|
|||||||
@@ -22,4 +22,5 @@ dependencies {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
implementation("me.xdrop:fuzzywuzzy:1.4.0")
|
||||||
|
implementation("org.jsoup:jsoup:1.20.1")
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,13 @@ import org.gameyfin.plugins.metadata.steam.dto.SteamDetailsResultWrapper
|
|||||||
import org.gameyfin.plugins.metadata.steam.dto.SteamGame
|
import org.gameyfin.plugins.metadata.steam.dto.SteamGame
|
||||||
import org.gameyfin.plugins.metadata.steam.dto.SteamSearchResult
|
import org.gameyfin.plugins.metadata.steam.dto.SteamSearchResult
|
||||||
import org.gameyfin.plugins.metadata.steam.mapper.Mapper
|
import org.gameyfin.plugins.metadata.steam.mapper.Mapper
|
||||||
|
import org.gameyfin.plugins.metadata.steam.util.SteamDateSerializer
|
||||||
|
import org.jsoup.Jsoup
|
||||||
import org.pf4j.Extension
|
import org.pf4j.Extension
|
||||||
import org.pf4j.PluginWrapper
|
import org.pf4j.PluginWrapper
|
||||||
import org.slf4j.LoggerFactory
|
import org.slf4j.LoggerFactory
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
||||||
|
|
||||||
@@ -31,6 +34,8 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
isLenient = true
|
isLenient = true
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val dateSerializer = SteamDateSerializer()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Extension(ordinal = 3)
|
@Extension(ordinal = 3)
|
||||||
@@ -114,9 +119,9 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
val metadata = GameMetadata(
|
val metadata = GameMetadata(
|
||||||
originalId = id.toString(),
|
originalId = id.toString(),
|
||||||
title = sanitizeTitle(game.name),
|
title = sanitizeTitle(game.name),
|
||||||
description = game.detailedDescription,
|
description = game.shortDescription, // Using short description since the detailed description often contains just some ads for the Battle Pass etc.
|
||||||
coverUrls = game.headerImage?.let { URI(it) }?.let { listOf(it) },
|
coverUrls = game.headerImage?.let { URI(it) }?.let { listOf(it) },
|
||||||
release = game.releaseDate?.date,
|
release = parseOriginalReleaseDateFromStorePage(id) ?: game.releaseDate?.date,
|
||||||
developedBy = game.developers?.toSet(),
|
developedBy = game.developers?.toSet(),
|
||||||
publishedBy = game.publishers?.toSet(),
|
publishedBy = game.publishers?.toSet(),
|
||||||
genres = game.genres?.let { genre -> genre.map { Mapper.genre(it) }.toSet() },
|
genres = game.genres?.let { genre -> genre.map { Mapper.genre(it) }.toSet() },
|
||||||
@@ -128,6 +133,28 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
|
|||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The API only provides the release date on Steam, not the original release date.
|
||||||
|
* However, it is possible to get the original release date from the Steam store page.
|
||||||
|
*/
|
||||||
|
private suspend fun parseOriginalReleaseDateFromStorePage(appId: Int): Instant? {
|
||||||
|
val response = client.get("https://store.steampowered.com/app/$appId") {
|
||||||
|
// Set language to English to avoid issues with different languages
|
||||||
|
cookie("Steam_Language", "english")
|
||||||
|
// Skip Steam age check
|
||||||
|
cookie("birthtime", "-2208989360")
|
||||||
|
cookie("lastagecheckage", "1-January-1900")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.status != HttpStatusCode.OK) return null
|
||||||
|
|
||||||
|
val html: String = response.bodyAsText(Charsets.UTF_8)
|
||||||
|
val document = Jsoup.parse(html)
|
||||||
|
val releaseDateText = document.selectFirst("div.release_date div.date") ?: return null
|
||||||
|
|
||||||
|
return dateSerializer.deserialize(releaseDateText.text())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Often titles on Steam contain copyright symbols which makes matching between different providers harder
|
* Often titles on Steam contain copyright symbols which makes matching between different providers harder
|
||||||
|
|||||||
+1
@@ -15,6 +15,7 @@ data class SteamDetailsResultWrapper(
|
|||||||
data class SteamGameDetails(
|
data class SteamGameDetails(
|
||||||
val type: String,
|
val type: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
@SerialName("short_description") val shortDescription: String? = null,
|
||||||
@SerialName("detailed_description") val detailedDescription: String? = null,
|
@SerialName("detailed_description") val detailedDescription: String? = null,
|
||||||
@SerialName("header_image") val headerImage: String? = null,
|
@SerialName("header_image") val headerImage: String? = null,
|
||||||
val developers: List<String>? = null,
|
val developers: List<String>? = null,
|
||||||
|
|||||||
+19
-2
@@ -27,9 +27,26 @@ class SteamDateSerializer : KSerializer<Instant?> {
|
|||||||
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d MMM, yyyy", Locale.ENGLISH)
|
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("d MMM, yyyy", Locale.ENGLISH)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun deserialize(decoder: Decoder): Instant? = fromString(decoder.decodeString())
|
override fun serialize(encoder: Encoder, value: Instant?) {
|
||||||
|
if (value == null) {
|
||||||
|
encoder.encodeNull()
|
||||||
|
} else {
|
||||||
|
encoder.encodeString(value.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun serialize(encoder: Encoder, value: Instant?) = encoder.encodeString(value.toString())
|
override fun deserialize(decoder: Decoder): Instant? {
|
||||||
|
return if (decoder.decodeNotNullMark()) {
|
||||||
|
fromString(decoder.decodeString())
|
||||||
|
} else {
|
||||||
|
decoder.decodeNull()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deserialize(dateString: String): Instant? {
|
||||||
|
return fromString(dateString)
|
||||||
|
}
|
||||||
|
|
||||||
private fun fromString(dateString: String): Instant? {
|
private fun fromString(dateString: String): Instant? {
|
||||||
return try {
|
return try {
|
||||||
|
|||||||
+7
@@ -56,6 +56,11 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun stop() {
|
||||||
|
client?.close()
|
||||||
|
client = null
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun authenticate(apiKey: String? = null) {
|
private suspend fun authenticate(apiKey: String? = null) {
|
||||||
log.debug("Authenticating on SteamGridDB API...")
|
log.debug("Authenticating on SteamGridDB API...")
|
||||||
|
|
||||||
@@ -83,6 +88,7 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
GameMetadata(
|
GameMetadata(
|
||||||
originalId = game.id.toString(),
|
originalId = game.id.toString(),
|
||||||
title = game.name,
|
title = game.name,
|
||||||
|
release = game.releaseDate,
|
||||||
coverUrls = grids?.map { URI(it.url) },
|
coverUrls = grids?.map { URI(it.url) },
|
||||||
headerUrls = heroes?.map { URI(it.url) }
|
headerUrls = heroes?.map { URI(it.url) }
|
||||||
)
|
)
|
||||||
@@ -101,6 +107,7 @@ class SteamGridDbPlugin(wrapper: PluginWrapper) : ConfigurableGameyfinPlugin(wra
|
|||||||
return@runBlocking GameMetadata(
|
return@runBlocking GameMetadata(
|
||||||
originalId = game.id.toString(),
|
originalId = game.id.toString(),
|
||||||
title = game.name,
|
title = game.name,
|
||||||
|
release = game.releaseDate,
|
||||||
coverUrls = grids?.map { URI(it.url) },
|
coverUrls = grids?.map { URI(it.url) },
|
||||||
headerUrls = heroes?.map { URI(it.url) }
|
headerUrls = heroes?.map { URI(it.url) }
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-2
@@ -44,6 +44,10 @@ class SteamGridDbApiClient(private val apiKey: String) {
|
|||||||
return get("search/autocomplete/${term.encodeURLPath(encodeSlash = true, encodeEncoded = false)}", block).body()
|
return get("search/autocomplete/${term.encodeURLPath(encodeSlash = true, encodeEncoded = false)}", block).body()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun game(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGameResult {
|
||||||
|
return get("games/id/$gameId", block).body()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun grids(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGridResult {
|
suspend fun grids(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGridResult {
|
||||||
return get("grids/game/$gameId") {
|
return get("grids/game/$gameId") {
|
||||||
url {
|
url {
|
||||||
@@ -59,8 +63,8 @@ class SteamGridDbApiClient(private val apiKey: String) {
|
|||||||
}.body()
|
}.body()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun game(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGameResult {
|
fun close() {
|
||||||
return get("games/id/$gameId", block).body()
|
client.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
|
private suspend fun get(endpoint: String, block: HttpRequestBuilder.() -> Unit = {}): HttpResponse {
|
||||||
|
|||||||
+7
-1
@@ -1,10 +1,16 @@
|
|||||||
package org.gameyfin.plugins.metadata.steamgriddb.dto
|
package org.gameyfin.plugins.metadata.steamgriddb.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.gameyfin.plugins.metadata.steamgriddb.util.InstantEpochSecondsSerializer
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class SteamGridDbGame(
|
data class SteamGridDbGame(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
val name: String
|
val name: String,
|
||||||
|
@SerialName("release_date")
|
||||||
|
@Serializable(with = InstantEpochSecondsSerializer::class)
|
||||||
|
val releaseDate: Instant? = null
|
||||||
)
|
)
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package org.gameyfin.plugins.metadata.steamgriddb.util
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
object InstantEpochSecondsSerializer : KSerializer<Instant?> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor("InstantEpochSeconds", PrimitiveKind.LONG)
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: Instant?) {
|
||||||
|
if (value == null) {
|
||||||
|
encoder.encodeNull()
|
||||||
|
} else {
|
||||||
|
encoder.encodeLong(value.epochSecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): Instant? {
|
||||||
|
return if (decoder.decodeNotNullMark()) {
|
||||||
|
Instant.ofEpochSecond(decoder.decodeLong())
|
||||||
|
} else {
|
||||||
|
decoder.decodeNull()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user