From 7abbeb6eb350d0d9161a3f8e1864fc76ba41b707 Mon Sep 17 00:00:00 2001 From: atsunatsu Date: Sun, 6 Sep 2026 21:33:41 +0800 Subject: [PATCH] fix: update check via GitHub web endpoint, avoiding API 60 req/hour rate limit --- .../core/data/repository/UpdateRepository.kt | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt index c03ed82f..2408a83f 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/repository/UpdateRepository.kt @@ -5,7 +5,6 @@ import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository import com.rtbishop.look4sat.core.domain.source.IRemoteSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import org.json.JSONObject import java.io.File class UpdateRepository( @@ -13,11 +12,15 @@ class UpdateRepository( ) : IUpdateRepository { override suspend fun getLatestRelease(): LatestRelease? = withContext(Dispatchers.IO) { + // Use the GitHub releases web page instead of the REST API endpoint: + // the API endpoint is rate-limited to 60 requests/hour per IP, which is + // quickly exhausted on shared egress IPs (e.g. VPN proxies), causing 403 + // failures. The web endpoint redirects to the latest tag with no such limit. val result = remoteSource.getNetworkStream(LATEST_RELEASE_URL) val stream = result.stream ?: return@withContext null try { - val json = stream.bufferedReader().use { it.readText() } - parseRelease(json) + val html = stream.bufferedReader().use { it.readText() } + parseRelease(html) } catch (e: Exception) { println("UpdateRepository parse failure: $e") null @@ -37,31 +40,33 @@ class UpdateRepository( } } - private fun parseRelease(json: String): LatestRelease? { - val obj = JSONObject(json) - val tag = obj.optString("tag_name") - if (tag.isBlank()) return null - val assets = obj.optJSONArray("assets") - var apkUrl: String? = null - if (assets != null) { - for (i in 0 until assets.length()) { - val asset = assets.optJSONObject(i) ?: continue - val name = asset.optString("name", "") - if (name.endsWith(".apk")) { - apkUrl = asset.optString("browser_download_url", "").ifBlank { null } - if (apkUrl != null) break - } - } - } + private fun parseRelease(html: String): LatestRelease? { + // The redirect target is the latest release's tag page. Extract the tag + // from the og:url meta tag ("…/releases/tag/v4.4.6-ba7opf.8") — stable and + // unambiguous. The also holds the release name, but that is free + // text and cannot be used to build the asset URL. + val ogUrl = Regex("<meta\\s+property=\"og:url\"\\s+content=\"([^\"]*)\"").find(html) + ?.groupValues?.get(1) ?: return null + val tag = Regex("/releases/tag/(v[0-9][0-9A-Za-z.\\-]*)$").find(ogUrl) + ?.groupValues?.get(1) ?: return null + val title = Regex("<title>(.*?)", RegexOption.DOT_MATCHES_ALL) + .find(html)?.groupValues?.get(1)?.trim() + ?.substringBefore("·")?.removePrefix("Release")?.trim() ?: tag + // The release APK asset follows the fixed naming scheme used by the build: + // Look4Sat--release.apk + val apkUrl = "$DOWNLOAD_BASE_URL/$tag/Look4Sat-${tag.removePrefix("v")}-release.apk" return LatestRelease( versionTag = tag, - title = obj.optString("name", ""), - body = obj.optString("body", ""), + title = title, + body = "", apkUrl = apkUrl ) } private companion object { - const val LATEST_RELEASE_URL = "https://api.github.com/repos/atsunatsu/Look4Sat/releases/latest" + // Web pages are used instead of api.github.com to avoid the 60 req/hour + // anonymous rate limit (see getLatestRelease above). + const val LATEST_RELEASE_URL = "https://github.com/atsunatsu/Look4Sat/releases/latest" + const val DOWNLOAD_BASE_URL = "https://github.com/atsunatsu/Look4Sat/releases/download" } }