V1.4.7: 新增原生取址兜底 —— 修复 WebView 不可用的老盒子无法点歌

证据(用户导出的 1.4.6 远程日志):
- 两条失败记录只有 Kotlin 侧那一行
  「点歌编号=... 详情=无法生成下载地址」,
  而 JS ...(本版新加的 console 桥)、RAW-ERR、NET-ERR、点歌失败 一条都没有。
- 即 JS 侧从未执行 —— 隐藏 WebView 在这台盒子上从未就绪
  (每条失败前正好有约 15s 的「等桥就绪」超时,时间吻合)。
- 于是 SongApiClient 只能回退 DB 的 cloud_url,取不到就报「无法生成下载地址」。

根因: 取址完全依赖 WebView 里的 ktv_api.js;GMUI2.0/Android 5.1.1 上
WebView 不可用(无法导出 window.KtvApi),桥永远 not ready。

修复:
- 新增 SongUrlResolver: 用 Kotlin 复刻 ktv_api.js 的协议
  (GET <host>/i.php 取 token → GET <host>/music/do.php 取地址),
  镜像多源排序(失败源沉底)与 isDemoUrl 校验(wb66/demo/纯IP/r2.dev/ts编号不一致)。
- 走与 JS 完全相同的裸 Socket「干净请求」通道(KtvJsBridge.rawCleanGet),
  请求特征一致故风控表现一致,且不依赖 WebView 是否就绪。
- SongApiClient.getSongDownloadUrl 三处回退点改为:
  桥未就绪 / JS 未取到地址 / 主线程调用 → 先走原生解析,再回退 DB。
- 补齐桥的生命周期可观测性: WebView 初始化失败、KtvApi 未导出、
  15s 未就绪 均写入远程日志(以前只在 logcat,这就是本轮一开始没有线索的原因)。

已用 App 的真实身份( mac=800b510bf4fd / sn=2d7bb4a7d7f7 / VER=2.0 )
从本机复现协议: i.php 200 → do.php code=200 且返回
  http://txog.ktvsky.com/.../480p/7303235.ts (与点歌编号一致),
证明参数顺序与签名算法正确。

版本 1.4.6(26) 到 1.4.7(27);签名证书不变。ktv_api.js 未改动,KTV_BRIDGE_API 仍为 8。
This commit is contained in:
zijunchiang
2026-09-26 17:08:38 +08:00
parent 691e56f617
commit 1687f87eca
5 changed files with 262 additions and 13 deletions
+20 -2
View File
@@ -49,7 +49,7 @@ class KtvJsBridge(context: Context) {
// mac 取本机「当前在用网卡」的硬件地址(读不到时回退历史抓包值, 见 DeviceMac);
// sn 沿用抓包注册值, 保证 mac 变化后仍能对上原有账号体系。
private val DEVICE_MAC: String get() = DeviceMac.effective()
private const val DEVICE_SN = "2d7bb4a7d7f7"
internal const val DEVICE_SN = "2d7bb4a7d7f7"
/**
* 断网重连后重置歌曲源会话。
@@ -98,6 +98,7 @@ class KtvJsBridge(context: Context) {
updateCache(cached)
} catch (error: Throwable) {
Log.e(TAG, "WebView init failed", error)
MiliLog.error("JS-BRIDGE WebView 初始化失败: ${error.javaClass.simpleName}: ${error.message}")
callback(false)
}
}
@@ -138,6 +139,7 @@ class KtvJsBridge(context: Context) {
if (result == "true") {
ready = true
Log.i(TAG, "JS runtime ready (${if (loadedFromCache) "cache" else "asset"})")
MiliLog.info("JS-BRIDGE ready (${if (loadedFromCache) "cache" else "asset"})")
callback(true)
} else if (loadedFromCache && !assetRetryAttempted) {
Log.w(TAG, "Cached JS failed to load; falling back to packaged asset")
@@ -146,6 +148,9 @@ class KtvJsBridge(context: Context) {
createWebView(readAssetScript(), callback)
} else {
Log.e(TAG, "KtvApi was not exported by JS")
// 老电视盒子上 WebView 不可用/被裁剪时常走这里:桥永远不就绪,
// 取址只能回退 DB → 「无法生成下载地址」。必须能远程看到。
MiliLog.error("JS-BRIDGE KtvApi 未导出,WebView 不可用")
callback(false)
}
}
@@ -158,6 +163,18 @@ class KtvJsBridge(context: Context) {
/** 桥当前是否可用。热更新重载期间会短暂为 false,调用方应据此等待而非立即回退。 */
fun isReady(): Boolean = ready && !destroyed
/**
* 供**原生兜底取址解析器**([SongUrlResolver])复用的「干净请求」通道。
*
* 与 JS 的 `android.httpGet` 走的是**完全同一条**裸 Socket 路径(只发 Host + Accept,
* 不带 UA/Connection/Accept-Encoding),因此请求特征与风控表现一致;
* 且**不依赖 WebView 是否就绪** —— 正是 WebView 不可用的老设备所需要的。
*/
fun rawCleanGet(url: String, timeoutMs: Int = 8_000): String {
val headers = JSONObject().put("Accept", "*/*").toString()
return NativeBridge().request("GET", url, null, headers, timeoutMs)
}
fun isDestroyed(): Boolean = destroyed
/**
@@ -396,7 +413,8 @@ class KtvJsBridge(context: Context) {
}
}
private fun request(method: String, url: String, body: String?, headersJson: String, timeoutMs: Int): String {
/** 供 [rawCleanGet] 与 [NativeBridge.httpGet]/[httpPost] 共用的请求入口。 */
internal fun request(method: String, url: String, body: String?, headersJson: String, timeoutMs: Int): String {
// 歌曲源(如 w.w345.my)的 do.php 风控只放行"干净"请求(原版仅 Host+Accept;
// 带任何 UA/Connection/Accept-Encoding 头都会被拒)。裸 Socket 只发 Host+JS 头。
// 任何 HTTP 状态(200/403 等)都直接用裸 Socket 结果,不再用 HttpURLConnection