fix: update check via GitHub web endpoint, avoiding API 60 req/hour rate limit

This commit is contained in:
atsunatsu committed 2026-09-06 21:33:41 +08:00
1 parent 5c73d5ed7a
commit 7abbeb6eb3
1 file changed
+27 -22
@@ -5,7 +5,6 @@ import com.rtbishop.look4sat.core.domain.repository.IUpdateRepository
import com.rtbishop.look4sat.core.domain.source.IRemoteSource import com.rtbishop.look4sat.core.domain.source.IRemoteSource
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File import java.io.File
class UpdateRepository( class UpdateRepository(
@@ -13,11 +12,15 @@ class UpdateRepository(
) : IUpdateRepository { ) : IUpdateRepository {
override suspend fun getLatestRelease(): LatestRelease? = withContext(Dispatchers.IO) { 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 result = remoteSource.getNetworkStream(LATEST_RELEASE_URL)
val stream = result.stream ?: return@withContext null val stream = result.stream ?: return@withContext null
try { try {
val json = stream.bufferedReader().use { it.readText() } val html = stream.bufferedReader().use { it.readText() }
parseRelease(json) parseRelease(html)
} catch (e: Exception) { } catch (e: Exception) {
println("UpdateRepository parse failure: $e") println("UpdateRepository parse failure: $e")
null null
@@ -37,31 +40,33 @@ class UpdateRepository(
} }
} }
private fun parseRelease(json: String): LatestRelease? { private fun parseRelease(html: String): LatestRelease? {
val obj = JSONObject(json) // The redirect target is the latest release's tag page. Extract the tag
val tag = obj.optString("tag_name") // from the og:url meta tag ("…/releases/tag/v4.4.6-ba7opf.8") — stable and
if (tag.isBlank()) return null // unambiguous. The <title> also holds the release name, but that is free
val assets = obj.optJSONArray("assets") // text and cannot be used to build the asset URL.
var apkUrl: String? = null val ogUrl = Regex("<meta\\s+property=\"og:url\"\\s+content=\"([^\"]*)\"").find(html)
if (assets != null) { ?.groupValues?.get(1) ?: return null
for (i in 0 until assets.length()) { val tag = Regex("/releases/tag/(v[0-9][0-9A-Za-z.\\-]*)$").find(ogUrl)
val asset = assets.optJSONObject(i) ?: continue ?.groupValues?.get(1) ?: return null
val name = asset.optString("name", "") val title = Regex("<title>(.*?)</title>", RegexOption.DOT_MATCHES_ALL)
if (name.endsWith(".apk")) { .find(html)?.groupValues?.get(1)?.trim()
apkUrl = asset.optString("browser_download_url", "").ifBlank { null } ?.substringBefore("·")?.removePrefix("Release")?.trim() ?: tag
if (apkUrl != null) break // The release APK asset follows the fixed naming scheme used by the build:
} // Look4Sat-<tag without leading v>-release.apk
} val apkUrl = "$DOWNLOAD_BASE_URL/$tag/Look4Sat-${tag.removePrefix("v")}-release.apk"
}
return LatestRelease( return LatestRelease(
versionTag = tag, versionTag = tag,
title = obj.optString("name", ""), title = title,
body = obj.optString("body", ""), body = "",
apkUrl = apkUrl apkUrl = apkUrl
) )
} }
private companion object { 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"
} }
} }