diff --git a/plugins/steam/build.gradle.kts b/plugins/steam/build.gradle.kts index 502237b..5ca6034 100644 --- a/plugins/steam/build.gradle.kts +++ b/plugins/steam/build.gradle.kts @@ -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") diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt index 4e9bd32..25fc461 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/SteamPlugin.kt @@ -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 { - 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") } diff --git a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/util/SteamDateSerializer.kt b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/util/SteamDateSerializer.kt index bbbd744..3fca1bc 100644 --- a/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/util/SteamDateSerializer.kt +++ b/plugins/steam/src/main/kotlin/de/grimsi/gameyfinplugins/steam/util/SteamDateSerializer.kt @@ -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 { +class SteamDateSerializer : KSerializer { 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)) } } \ No newline at end of file diff --git a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt index 343c737..17760d0 100644 --- a/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt +++ b/plugins/steamgriddb/src/main/kotlin/de/grimsi/gameyfinplugins/steamgriddb/api/SteamGridDbApiClient.kt @@ -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 {