mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-17 00:30:04 +00:00
Fix URL encoding of search queries
This commit is contained in:
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
+48
-29
@@ -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,46 +15,63 @@ 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 {
|
private fun fromString(dateString: String): Instant? {
|
||||||
// Match "Coming Soon" and return a fallback date
|
return try {
|
||||||
if (dateString.equals(COMING_SOON_TEXT, true)) {
|
// Return null for empty strings
|
||||||
return COMING_SOON_FALLBACK_DATE.atStartOfDay().toInstant(ZoneOffset.UTC)
|
if (dateString.isBlank()) {
|
||||||
}
|
return null
|
||||||
|
|
||||||
// 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()
|
// Match "Coming Soon" and return a fallback date
|
||||||
.toInstant(ZoneOffset.UTC)
|
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user