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"]}")
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") {
exclude(group = "org.slf4j", module = "slf4j-api")
exclude(group = "org.slf4j")
}
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") {
exclude(group = "org.slf4j", module = "slf4j-api")
exclude(group = "org.slf4j")
}
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.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
@@ -22,8 +23,6 @@ import org.pf4j.Extension
import org.pf4j.PluginWrapper
import org.slf4j.LoggerFactory
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
@@ -39,6 +38,9 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
private val log = LoggerFactory.getLogger(javaClass)
val client = HttpClient(CIO) {
// Use a fake browser user agent to avoid being blocked by Steam
BrowserUserAgent()
install(ContentNegotiation) {
json(json)
}
@@ -64,10 +66,9 @@ class SteamPlugin(wrapper: PluginWrapper) : GameyfinPlugin(wrapper) {
}
private suspend fun searchStore(title: String): List<SteamGame> {
val encodedTitle = URLEncoder.encode(title, StandardCharsets.UTF_8.toString())
return try {
val response = client.get("https://store.steampowered.com/api/storesearch") {
parameter("term", encodedTitle)
parameter("term", title)
parameter("cc", "en")
parameter("l", "en")
}
@@ -5,6 +5,8 @@ import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializer
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneOffset
@@ -13,46 +15,63 @@ import java.util.*
@OptIn(ExperimentalSerializationApi::class)
@Serializer(forClass = Instant::class)
class SteamDateSerializer : KSerializer<Instant> {
class SteamDateSerializer : KSerializer<Instant?> {
companion object {
val log: Logger = LoggerFactory.getLogger(SteamDateSerializer::class.java)
const val COMING_SOON_TEXT = "Coming Soon"
val COMING_SOON_FALLBACK_DATE: LocalDate = LocalDate.parse("2999-12-31")
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 {
// Match "Coming Soon" and return a fallback date
if (dateString.equals(COMING_SOON_TEXT, true)) {
return COMING_SOON_FALLBACK_DATE.atStartOfDay().toInstant(ZoneOffset.UTC)
}
// Match quarters like "Q1 2023", "Q2 2023", etc.
val quarterMatch = Regex("""Q([1-4]) (\d{4})""").matchEntire(dateString)
if (quarterMatch != null) {
val (qStr, yearStr) = quarterMatch.destructured
val month = when (qStr.toInt()) {
1 -> 1
2 -> 4
3 -> 7
4 -> 10
else -> 1
private fun fromString(dateString: String): Instant? {
return try {
// Return null for empty strings
if (dateString.isBlank()) {
return null
}
return LocalDate.of(yearStr.toInt(), month, 1)
.atStartOfDay()
.toInstant(ZoneOffset.UTC)
// Match "Coming Soon" and return a fallback date
if (dateString.equals(COMING_SOON_TEXT, true)) {
return COMING_SOON_FALLBACK_DATE.atStartOfDay().toInstant(ZoneOffset.UTC)
}
// Match quarters like "Q1 2023", "Q2 2023", etc.
val quarterMatch = Regex("""Q([1-4]) (\d{4})""").matchEntire(dateString)
if (quarterMatch != null) {
val (qStr, yearStr) = quarterMatch.destructured
val month = when (qStr.toInt()) {
1 -> 1
2 -> 4
3 -> 7
4 -> 10
else -> 1
}
return LocalDate.of(yearStr.toInt(), month, 1)
.atStartOfDay()
.toInstant(ZoneOffset.UTC)
}
// Match year only
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)
}
val localDate = LocalDate.parse(dateString, formatter)
return localDate.atStartOfDay().toInstant(ZoneOffset.UTC)
} catch (_: Exception) {
log.warn("Couldn't parse date string: '$dateString'")
null // Return null if parsing fails
}
val localDate = LocalDate.parse(dateString, formatter)
return localDate.atStartOfDay().toInstant(ZoneOffset.UTC)
}
private fun toString(date: Instant): String {
return formatter.format(date.atZone(ZoneOffset.UTC))
}
}
@@ -40,7 +40,7 @@ class SteamGridDbApiClient(private val apiKey: String) {
}
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 {