合并自建歌曲源支线(selfhost-source)+ 版本号按约定修正为 1.6.0

版本号:
- 1.5.11 越界(规则 3 规定 patch ≤ 10)→ 修正为 1.6.0(进位到 minor、patch 归 0)
- versionCode 43 不变:43 从未对外发布,本次修正只是把命名拉回约定

合并支线(原在工作区根目录、App 仓库之外,DESIGN.md 自称"支线"):
- selfhost-source/server/   自建歌曲源服务器(纯 Python 标准库、零 pip 依赖)
- selfhost-source/deploy/、DESIGN.md、LIBRARY.md、build_inventory.py
- selfhost-source/inventory/ 小清单(playlists.csv、song-stats.md 等)
- ⚠️ inventory 生成物(songs-*.csv / singers-top.csv,共 84MB)不入库,
  已写入 .gitignore;需要时用 build_inventory.py 依据 muse.db 重新生成

同时入库的 App 侧支线改动(此前为工作树未提交状态):
- Whoami.kt(whoami 设备心跳)、DeviceIdentity.kt(设备身份伪装)
- PushDownload.kt / PushDownloadClient.kt(点歌推送)
- AdVideoFilter 带 query/fragment 的地址闸门;KTV_BRIDGE_API 11 → 13
- 修 ktv_api.js 的 global.KtvBridgeApiVersion(12 → 13,与注释及 Kotlin 两处校验一致)
- 对应单测:WhoamiTest / DeviceIdentityTest / PushDownloadTest / AdVideoFilterTest(共 39 例)

文档:
- DESIGN.md 的"在 App 仓库之外"约定改为"已并入仓库统一版本化"
- AGENTS.md 版本号行更新为 1.6.0/43,并新增支线并入说明

验证:assembleRelease + testReleaseUnitTest 全过(39 例)
This commit is contained in:
zijunchiang
2026-10-14 02:06:28 +08:00
parent e060ae5ebe
commit 87aa979abd
68 changed files with 21305 additions and 105 deletions
@@ -47,11 +47,12 @@ class KtvJsBridge(context: Context) {
private const val JS_REQUEST_TIMEOUT_MS = 180_000
/** WebView 初始化看门狗:超过这个时间还没就绪就判定本机 WebView 不可用。 */
private const val BRIDGE_INIT_TIMEOUT_MS = 20_000L
// 歌曲源(w.w345.my 与 e.ac19.cn 同后台)按 device 识别; 随机/变化的 device 会被风控。
// mac 取本机「当前在用网卡」的硬件地址(读不到时回退历史抓包值, 见 DeviceMac);
// sn 沿用抓包注册值, 保证 mac 变化后仍能对上原有账号体系。
private val DEVICE_MAC: String get() = DeviceMac.effective()
internal const val DEVICE_SN = "2d7bb4a7d7f7"
// 歌曲源按 device(mac + sn) 识别;随机/变化的 device 会被判"异常行为",因此这两个值
// 来自**生成一次、落盘后固定不变**的设备身份(DeviceIdentity:伪装成一台安卓盒子)。
// ⚠️ 以前 mac 取本机网卡硬件地址、sn 是抓包常量:真实机型/真实网卡一眼就不是盒子,
// 而且 Android 10+ 也拿不到真实 MAC。真实值只留在本地日志里(见 MainActivity)。
private val DEVICE_MAC: String get() = DeviceIdentity.effective().mac
internal val DEVICE_SN: String get() = DeviceIdentity.effective().sn
/**
* 断网重连后重置歌曲源会话。
@@ -358,7 +359,7 @@ class KtvJsBridge(context: Context) {
runCatching {
file.isFile && file.length() in 1_001..500_000 &&
file.readText(Charsets.UTF_8).let {
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 12")
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 13")
}
}.getOrDefault(false)
@@ -384,7 +385,7 @@ class KtvJsBridge(context: Context) {
val script = String(bytes, StandardCharsets.UTF_8)
require(
script.length > 1000 && script.contains("KtvApi") &&
script.contains("KTV_BRIDGE_API: 12"),
script.contains("KTV_BRIDGE_API: 13"),
) { "Invalid or incompatible JS file" }
tempFile.writeText(script, Charsets.UTF_8)
if (!tempFile.renameTo(cacheFile)) {