Fix URL encoding of search queries

This commit is contained in:
grimsi
2025-06-13 18:53:30 +02:00
parent da978f1277
commit c74a7e9bf1
4 changed files with 58 additions and 38 deletions
+4 -4
View File
@@ -9,16 +9,16 @@ dependencies {
ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}") ksp("care.better.pf4j:pf4j-kotlin-symbol-processing:${rootProject.extra["pf4jKspVersion"]}")
implementation("io.ktor:ktor-client-core:$ktor_version") { implementation("io.ktor:ktor-client-core:$ktor_version") {
exclude(group = "org.slf4j", module = "slf4j-api") exclude(group = "org.slf4j")
} }
implementation("io.ktor:ktor-client-cio:$ktor_version") { implementation("io.ktor:ktor-client-cio:$ktor_version") {
exclude(group = "org.slf4j", module = "slf4j-api") exclude(group = "org.slf4j")
} }
implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") { implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") {
exclude(group = "org.slf4j", module = "slf4j-api") exclude(group = "org.slf4j")
} }
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") { implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") {
exclude(group = "org.slf4j", module = "slf4j-api") exclude(group = "org.slf4j")
} }
implementation("me.xdrop:fuzzywuzzy:1.4.0") implementation("me.xdrop:fuzzywuzzy:1.4.0")
@@ -10,6 +10,7 @@ import de.grimsi.gameyfinplugins.steam.mapper.Mapper
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.call.* import io.ktor.client.call.*
import io.ktor.client.engine.cio.* import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.* import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.client.statement.* import io.ktor.client.statement.*
@@ -22,8 +23,6 @@ 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.net.URLEncoder
import java.nio.charset.StandardCharsets
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) { class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
@@ -39,6 +38,9 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
private val log = LoggerFactory.getLogger(javaClass) private val log = LoggerFactory.getLogger(javaClass)
val client = HttpClient(CIO) { val client = HttpClient(CIO) {
// Use a fake browser user agent to avoid being blocked by Steam
BrowserUserAgent()
install(ContentNegotiation) { install(ContentNegotiation) {
json(json) json(json)
} }
@@ -64,10 +66,9 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
} }
private suspend fun searchStore(title: String): List<SteamGame> { private suspend fun searchStore(title: String): List<SteamGame> {
val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString())
return try { return try {
val response = client.get("https://store.steampowered.com/api/storesearch") { val response = client.get("https://store.steampowered.com/api/storesearch") {
parameter("term", encodedTitle) parameter("term", title)
parameter("cc", "en") parameter("cc", "en")
parameter("l", "en") parameter("l", "en")
} }
@@ -5,6 +5,8 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.encoding.Encoder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Instant import java.time.Instant
import java.time.LocalDate import java.time.LocalDate
import java.time.ZoneOffset import java.time.ZoneOffset
@@ -13,20 +15,28 @@ import java.util.*
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Instant::class) @Serializer(forClass = Instant::class)
class SteamDateSerializer : KSerializer<Instant> { class SteamDateSerializer : KSerializer<Instant?> {
companion object { companion object {
val log: Logger = LoggerFactory.getLogger(SteamDateSerializer::class.java)
const val COMING_SOON_TEXT = "Coming Soon" const val COMING_SOON_TEXT = "Coming Soon"
val COMING_SOON_FALLBACK_DATE: LocalDate = LocalDate.parse("2999-12-31") val COMING_SOON_FALLBACK_DATE: LocalDate = LocalDate.parse("2999-12-31")
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 deserialize(decoder: Decoder): Instant? = fromString(decoder.decodeString())
override fun serialize(encoder: Encoder, value: Instant) = encoder.encodeString(value.toString()) override fun serialize(encoder: Encoder, value: Instant?) = encoder.encodeString(value.toString())
private fun fromString(dateString: String): Instant? {
return try {
// Return null for empty strings
if (dateString.isBlank()) {
return null
}
private fun fromString(dateString: String): Instant {
// Match "Coming Soon" and return a fallback date // Match "Coming Soon" and return a fallback date
if (dateString.equals(COMING_SOON_TEXT, true)) { if (dateString.equals(COMING_SOON_TEXT, true)) {
return COMING_SOON_FALLBACK_DATE.atStartOfDay().toInstant(ZoneOffset.UTC) return COMING_SOON_FALLBACK_DATE.atStartOfDay().toInstant(ZoneOffset.UTC)
@@ -48,11 +58,20 @@ class SteamDateSerializer : KSerializer<Instant> {
.toInstant(ZoneOffset.UTC) .toInstant(ZoneOffset.UTC)
} }
val localDate = LocalDate.parse(dateString, formatter) // Match year only
return localDate.atStartOfDay().toInstant(ZoneOffset.UTC) val yearMatch = Regex("""^(\d{4})$""").matchEntire(dateString)
if (yearMatch != null) {
val (yearStr) = yearMatch.destructured
return LocalDate.of(yearStr.toInt(), 1, 1)
.atStartOfDay()
.toInstant(ZoneOffset.UTC)
} }
private fun toString(date: Instant): String { val localDate = LocalDate.parse(dateString, formatter)
return formatter.format(date.atZone(ZoneOffset.UTC)) return localDate.atStartOfDay().toInstant(ZoneOffset.UTC)
} catch (_: Exception) {
log.warn("Couldn't parse date string: '$dateString'")
null // Return null if parsing fails
}
} }
} }
@@ -40,7 +40,7 @@ class SteamGridDbApiClient(private val apiKey: String) {
} }
suspend fun search(term: String, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbSearchResult { suspend fun search(term: String, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbSearchResult {
return get("search/autocomplete/$term", block).body() return get("search/autocomplete/${term.encodeURLPath(encodeSlash = true, encodeEncoded = false)}", block).body()
} }
suspend fun grids(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGridResult { suspend fun grids(gameId: Int, block: HttpRequestBuilder.() -> Unit = {}): SteamGridDbGridResult {