Commit Graph
13 Commits
Author SHA1 Message Date
zijunchiang d01269e917 取址改为官方源优先+自建源兜底,只有官方源成功才上报(V1.6.2/45,未发布)
需求一:流程反转 + 上报绑定官方源成功
- 默认 sourceHosts 反转为 ["http://zk.ktv1.top:6677", "https://mdmuse.kasugano.cn"];
  SourceHosts.DEFAULT / SongUrlResolver.DEFAULT_HOSTS(收敛为引用前者)/ ktv_api.js
  CONFIG.HOSTS+HOST 三处一致。
- 取址结果改为 SourcedSongUrl(url, sourceHost)(**显式来源标志**,不靠 URL 猜):
  新增 SongSourceFlow(回落链 firstAvailable / 上报判定 shouldReportToSelfHost /
  全失败信号);JS 侧新增 android.onSongSource(requestId, host) 回传命中的源。
- 汇合点 SongApiClient.getSongDownloadUrl 只在"地址来自官方源"时才 POST /push_download;
  自建源自己给的地址与 DB 回落都不上报。
- 推送默认目标改为"从歌曲源列表里找第一个自建源"(自建源不再排第一,旧写法会直接关掉推送)。
- 1.6.1 落盘的旧默认顺序(自建源优先)在 KtvStore.load() 里一次性迁移;自定义列表不动。
- 业务码 404(自建源没有这首歌)仍只当"该源没地址",不置 sawForbidden、不进冷却、不污染受限统计。

需求二:6 条隐患
1) 设置页改源对 JS 立即生效:KtvJsBridge.applySourceSettingsNow 重新注入 KtvApiConfig.*
   并把已构造实例的 window.api.sourceOrder 重置为新列表;SongApiClient.applySourceSettings 接线。
2) JS 路径识别业务码 403:新增 KtvJsBridge.riskForbidden(status, body)(纯函数,手写判定
   避免 org.json 在单测里是空壳),HTTP 403 与 HTTP 200+{"code":403} 都算风控,与原生 sawForbidden 同口径。
3) isTrustedDownloadUrl 移除广告域名 ac16.vip 白名单,并改为黑名单优先(不再依赖调用顺序)。
4) MainActivity.download(song)(第五条地址消费点)补 AdVideoFilter.isTrustedSongUrl 闸门,
   不可信按"源失效"处理(Eac19WindowGuard.noteAllSourcesFailed),不再静默下载。
5) SongApiClient.fetchFromDb 不再静默吞异常:曲库打不开/查询异常各记一条 MiliLog.localWarn(含编号与异常)。
6) 删除死代码 streamDownloadAndPlay(全仓 grep 无调用点,且内部取址不过最终闸门)。

需求三:死代码/漂移清理(均先全仓搜索确认无引用)
- AdVideoFilter.replacementUrl 无任何写入点(恒为 null)→ 删除,resolve() 只保留"屏蔽"语义。
- ktv_api.js CONFIG.VER/VN 由"从不生效的死值"改为实际使用值 2.0 / 4.1.3.03281430。
- CONFIG.HOST 改为被 getToken 真正读取(不再"注入但从未被读")。
- KTV_BRIDGE_API 14 → 15(JS 两处 + KtvJsBridge 两处校验);AGENTS.md / README.md / README.en.md
  的陈旧标记统一到 15。
- do.php 的 limit 与 i.php 的 retry/ordertype/end_date/heart_time 确认无消费方 → 不新增解析(保持现状)。

需求四:测试与打包
- 新增 SourceOrderConsistencyTest / SongSourceFlowTest / JsBridgeGateTest / DownloadEntryGateTest,
  更新 SourceHostsTest / AdVideoFilterTest;app/build.gradle 打开 unitTests.returnDefaultValues。
- testReleaseUnitTest --rerun-tasks --offline:12 个测试类 / 87 例全过。
- assembleRelease --offline 通过(lintVital NewApi 0 条);版本 1.6.1/44 → 1.6.2/45。

不发布:未推 Gitea/Gitee、未动 Release/tag。
2026-10-14 03:45:07 +08:00
zijunchiang e00a1ace79 接入自建歌曲源服务器:源列表自建优先 + App 令牌 + 媒体地址允许清单 + 推送默认目标(V1.6.1/44,未发布)
源列表(自建源优先、官方源兜底)
- SongUrlResolver.DEFAULT_HOSTS 与 ktv_api.js 的 CONFIG.HOSTS/HOST 改为
  https://mdmuse.kasugano.cn + http://zk.ktv1.top:6677;
- 新增设置项 KtvStore.sourceHosts(一行一个可编辑)与纯 JVM SourceHosts(解析/归一化/
  自建源判定),App 与 JS 都以它为准(JS 走既有配置注入:KtvJsBridge 覆写 window.KtvApiConfig)。

App 令牌(自适应,不破坏官方源/老服务端)
- 新增纯 JVM AppToken/AppTokenSession(取/缓存/404-405 记忆不支持/401 清缓存重试一次)
  与胶水 AppTokenClient(同一条干净裸 Socket、8 秒超时、失败只写 MiliLog.local*);
- /music/do.php 与 /push_download 前先对当前源 GET /token,取到即带 X-App-Token
  (expires_in 提前 30 秒过期);官方源 404/405 记住后不再尝试;401 只重试一次;
- /i.php、/music/do.php 的参数与签名算法一个字未改(令牌只加一个请求头);
- JS 侧同步:android.appToken(host)/invalidateAppToken(host) + 401 立即重试一次。

媒体地址闸门(自建源适配)
- 新增设置项 KtvStore.allowedMediaHosts(默认 md.kasugano.cn + 192.168.100.232);
- AdVideoFilter.isTrustedSongUrl(url, musicno, allowedHosts) 只对命中主机放行"非域名"
  限制(裸 IP);黑名单 wb66/demo/ac16.vip/ad_files/my_ad_video/pub-*.r2.dev/ss 与
  ".ts 文件名必须等于编号"一律不变且优先级更高(命中清单也拒);
- 四处调用点同口径:KtvJsBridge.isTrustedDownloadUrl、SongApiClient.isLikelyDemoUrl、
  JS isDemoUrl、下载兜底。

点歌推送默认目标
- PushDownload.effectiveServerUrl:显式配置优先;留空且首源是自建源则默认用它;
  off/none/关闭 等显式关闭;pushToken 逻辑不变(服务端接受 X-App-Token 或 push_token)。

版本与测试
- versionName 1.6.1 / versionCode 44;KTV_BRIDGE_API 13 -> 14(JS 两处 + KtvJsBridge 两处);
- 设置页「数据设置」新增「歌曲源」「媒体地址允许清单」两项;
- 新增单测 AppTokenTest(11)/SourceHostsTest(7)/MediaHostAllowlistTest(8),共 65 例全过。
2026-10-14 02:46:23 +08:00
zijunchiang 87aa979abd 合并自建歌曲源支线(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 例)
2026-10-14 02:06:28 +08:00
zijunchiang 4576a89f9a 按 HAR 抓包对齐原版协议: 补 api.php 取配置 + 识别业务码 403 + 按试用配额限源
依据用户提供的 HAR(e.ac19.cn, Reqable): 原版客户端是三步
  1) POST /api.php  action=get_config&device_id=<mac>  (表单 + Dalvik UA)
  2) GET /i.php          参数与 App 完全一致
  3) GET /music/do.php   参数与 App 完全一致, 返回真源 CDN 地址

发现并修复三处差异:
1. 我们从未调 api.php(原版取址前必调)
   → KtvJsBridge.rawCleanPost(): 干净 POST + Content-Type 表单 + Dalvik UA
     (UA 由本机 Build.VERSION.RELEASE/MODEL/ID 拼出, 与 HAR 同形态)
   → SongUrlResolver.ensureConfig(): 缓存 30 分钟, 桥不可用则跳过
   → SongApiClient 注入 POST 通道
2. 风控拒绝的真实形态是 HTTP 200 + 业务码 403
   {"code":403,"msg":"请求被拒绝:检测到异常行为"} —— 不是 HTTP 403
   → i.php / do.php 两处业务码 403 都置 sawForbidden(诊断不再误判为"没有地址")
3. 无视试用配额: api.php 给出 trial_times=2 / trial_duration=5
   → 一次取址最多尝试 trial_times 个源(原先 9 个源全扫会把额度瞬间打光)

请求头分工与 HAR 一致: i.php/do.php 只有 Host+Accept 的干净 GET;
仅 api.php 带表单 Content-Type 与 Dalvik UA。
日志: 远程一条 SOURCE-CONFIG trial_enabled=… trial_times=…; 原始 JSON/卡密提示只写本地。

本机实测: api.php 返回与 HAR 相同配置; do.php 返回 code=403 检测到异常行为
(本出口 IP 处于风控/未授权: network_verification=true + 卡密机制, 额度按 IP 计)。

版本号按规则 3: 1.5.8(40) → 1.5.9(41); 按要求未打包
验证: compileReleaseKotlin + 单元测试(9 例全过)
2026-10-08 03:56:59 +08:00
zijunchiang eadc2d4de1 V1.5.0: 修复"公放切歌失效"——上一版清空歌曲缓存把公放曲库删了(新设备回归)
现象: 新设备公放时按「切歌」没有任何反应。

根因(我上一版引入的回归):
- 1.4.10 为了让"被当成歌曲缓存下来的广告视频"消失,在启动时做了一次
  **整目录清理**(删 cloud-song 下所有 .ts/.ls/.mp4/.mkv)。
- 但 MuseDatabase.songDirectories() **只有 cloudSongsDir 一个目录**,
  SongOkDownloadManager.getLocalFile() 也落在那里 —— 那等于**把公放曲库整个删掉**。
- 后果链: 库空 → playRandomLocalSong() 里 allLocalSongs 为空 →
  RandomPlayQueue.next() 返回 null → 状态栏"本地暂无可播放歌曲"、currentSong 保持 null
  → 按「切歌」时 advancePlayback 落进「取消随机续播、进入空闲态」分支 → 按钮无反应。
  (RandomPlayQueue.next 本身有"只剩一首也能播"的兜底,队列逻辑没问题。)

修复:
- 删除整目录清理,改为 purgeInjectedAdDuplicatesOnce(): **只删能证明是注入广告的文件** ——
  广告对每首歌下发同一个视频,特征是"多首不同歌曲的本地文件内容完全相同"
  (按文件大小 + 头部 4MB 指纹分组,只清理 size>=2 的完全相同组)。
  正常歌曲内容各不相同,绝不会被误删;公放曲库得以保留。

同时按用户要求,复查"老设备适配期间改动对新设备的影响",另修正两处:
- SongApiClient.getSongDownloadUrl 的主线程分支: 1.4.7 起会先走 resolveNatively()
  (最多 7 个源 × 8 秒超时的阻塞网络请求)—— 一旦有主线程调用者就会卡死 UI 到 ANR。
  现在主线程只做本地 DB 兜底,网络取址一律留在后台线程。
- KtvJsBridge 裸 Socket 的 HTTPS 分支: 1.4.6 改用了无参 createSocket(),拿到的 SSLSocket
  **不带主机名**(影响 SNI 与证书主机名校验,共享 CDN/反代多证书时会握手失败)。
  改为先 connect 普通 socket(保留显式连接超时)再 createSocket(plain, host, port, true)。
- buildDownloadUrl 的文件名比对只在点歌编号"可信"时启用: 只有从 song.filename 推出的编号才
  参与比对;song.id 可能是数据库主键,拿它比对会误杀正常地址(真源被判"文件名不符")。
  (另已实测 7 个源: 可用的 5 个真源 URL 文件名全部严格等于点歌编号,规则不会误杀。)

顺带记录用户给出的版本号约定(已写入 AGENTS.md):
versionName = <major>.<minor>.<patch>,patch 0~10、minor 1~10;
1.4.10 之后是 1.5.0,1.10.1 之后是 2.1.0;versionCode 始终 +1。

版本 1.4.10(30) → 1.5.0(31);签名证书不变。测试 8 例全通过。
2026-09-26 21:25:37 +08:00
zijunchiang 669766ba60 V1.4.10: 修复广告过滤失效(gz.ac16.vip 广告被当成歌曲下载播放)
现场事故(新设备 SM-S9180 / Android 16,WebView 正常 → 走 JS 取址):
  277  JS [KtvApi] URL OK: 7303235 ls=1 via http://w.w345.my
  278  点歌编号=7303235 标题=情歌(HD) 请求源=http://w.w345.my 状态=成功 urlHost=gz.ac16.vip
  279  PLAY id=7303235 title=情歌(HD) src=local
即 gz.ac16.vip 的广告视频被当成歌曲下载、缓存并播放。

三处漏洞叠加:
1. ktv_api.js 的 isDemoUrl 不认识广告域名;且"ts 文件名与编号比对"用的是
   /\/(\d+)\.ts/,只匹配纯数字名 —— my_ad_video.ts 这类非数字名字直接放行;
2. SongOkDownloadManager.buildDownloadUrl 的**成功分支完全没有广告校验**
   (AdVideoFilter.isAdVideo 全项目只在"缓存地址"与"原生解析"两处被调用);
3. SongApiClient.fetchFromDb 只做 isLikelyDemoUrl(纯 IP / r2.dev),挡不住广告域名。
且**内容校验挡不住广告**: 该广告通过 SongFileValidator(≥6MB/≥60s/TS 包完整),
被 isDownloaded() 一直复用,广告会反复播。

修复(单一闸门,全链路把关):
- AdVideoFilter 新增 isTrustedSongUrl(url, musicno):非 http(s)、命中广告特征
  (路径 /ad_files/、my_ad_video、**域名 *.ac16.vip**)、或 URL 里 .ts 文件名不等于
  点歌编号 → 一律不可信。真源恒为 /<musicno>.ts(已实测 txog.ktvsky.com 与
  download.origjoy.com 两个 CDN 域共 6 首全部相符)。
- 该闸门接入全部出入口: JS isDemoUrl(同步加固 + KTV_BRIDGE_API 8→9)、
  SongUrlResolver.isValidSongUrl、SongApiClient.fetchFromDb、
  以及最终兜底 SongOkDownloadManager.buildDownloadUrl(成功/缓存两条分支)。
- 判定不可信时**同时删除已落盘的歌曲文件**(purgeCachedSongFile),
  否则 isDownloaded() 会一直复用那个广告文件。
- 新增 SongOkDownloadManager.purgeLegacySongCacheOnce(): 升级后一次性清理历史遗留的
  可疑歌曲缓存(marker: filesDir/song_cache_purged_v1)。只是让歌曲重新下载,不动用户数据。
- 新增回归测试 AdVideoFilterTest(6 例,用日志里的真实 URL 形态,含两个真源 CDN 域);
  该测试当场抓到本修复自身的一个 bug:正则把扩展名也捕获进去导致真源被误杀,已修正。

版本 1.4.9(29) 到 1.4.10(30);签名证书不变。
2026-09-26 21:17:04 +08:00
zijunchiang b3b17210f1 V1.4.9: 修复启动被误判为"断网恢复"(并补齐 WebView 失效原因取证)
现场日志(1.4.8)显示启动瞬间就出现:
  20:46:59.636  NET-UP 设备网络已恢复, 重建歌曲源会话
  20:46:59.647  NET-REFRESH reason=net-up retried=0
  20:46:59.980  BOOT
即 NET-UP 出现在 BOOT 之前 —— 启动被当成了"断网后恢复"。

根因: KtvNetworkMonitor.start() 末尾无条件 onChange(initial),
注册 ConnectivityManager 回调时系统会回报一次初始状态,上层把它当成了状态跃迁。
而启动流程(onCreate)已经做过 startRemoteServer()/updateMobileQrOverlay()/
refreshLogDeviceContext()/DeviceMac 解析,所以这次"恢复"是纯粹重复劳动,且有副作用:
- SongApiClient.resetSourceSession() 白清掉刚建立的歌曲源会话;
- Eac19WindowGuard.noteUrlOk() 语义错误(还没取到任何地址就标记窗口正常,
  可能掩盖真正的歌曲源窗口到期提示);
- 状态栏被写成"网络已恢复"(启动瞬间就显示,用户可见的错误文案)。

修复:
- KtvNetworkMonitor.start/refresh 的回调签名改为 (online, initial),
  initial=true 表示"注册时的初始状态";
- MainActivity.onNetworkStateChanged(online, initial): 初始状态**只更新断网角标**,
  不做任何恢复动作;恢复动作(重建会话/重启点歌服务/刷新二维码/重试下载)
  只在真正的 offline→online 跃迁时执行。

同时补齐 WebView 失效原因的取证(旧日志只有"KtvApi 未导出"这一句,无法定因):
- wrapHtml 注入特性探测: 上报 WEBVIEW ua=... asyncFunction=yes|no promise=...
  (用 new Function 探测,避免探测语句自身引起语法错误)。Android 5.x 的 WebView
  可能是 Chrome 39~4x,不支持 async/await 会让**整个 ktv_api.js 语法报错** →
  window.KtvApi 未定义、桥永远不就绪,这条日志可直接确证;
- window.onerror 上报脚本解析/运行错误(JS-ERROR ... @line:col),语法错误也会触发;
- 合并重复的失败行: "JS-BRIDGE 初始化返回 false" 与
  "JS-BRIDGE 判定本机 WebView 不可用" 只保留后者(含原因)。

版本 1.4.8(28) 到 1.4.9(29);签名证书不变。ktv_api.js 未改动,KTV_BRIDGE_API 仍为 8。
2026-09-26 21:00:21 +08:00
zijunchiang 906fa210e4 V1.4.8: 按现场日志复盘优化取址等待与远程日志(含 15 秒白等修复)
依据用户提供的 1.4.7 现场日志(21 行,5 次点歌全部成功)复盘出四点:

1. 【行为,最大】每次点歌白等 15 秒。
   日志里每首歌前都有一条「JS-BRIDGE 15s 未就绪」,紧跟着「NATIVE-SONG 取址成功 用时=3xx~7xxms」——
   即取址本身只要 0.3~0.7 秒,但用户从按下点歌到开始下载要等 15.7 秒。
   原因: 桥一旦不可用,SongApiClient 每次都仍等满 15 秒。
   修复: KtvJsBridge 新增「已判定不可用」状态(markUnavailable/isUnavailable),
   首次判定后后续点歌直接走原生通道;桥万一恢复(热更新重载成功)自动清除该状态。

2. 【日志去噪】持久状态只记一次。
   原来「15s 未就绪」按每次点歌一条 WARN 输出,一天几百条且无新增信息。
   现在只在状态首次变化时记一条 ERROR。

3. 【可定位】补上"为什么不可用"。
   旧日志里没有任何 KtvApi 未导出/WebView 初始化失败的行,说明 init 回调根本没触发。
   KtvJsBridge.init 增加 20 秒看门狗: onPageFinished 未回调时明确上报
   「初始化超时:onPageFinished 未回调或 KtvApi 未导出」;
   init 异常与 KtvApi 未导出也统一收敛到 markUnavailable。

4. 【上下文完整】启动关键行不再有 undefined。
   旧日志的 STORAGE 行 serverip/clientip/user/traceid 全是 undefined(记在设备上下文之前),
   而且 AppPaths.storageFacts() 会真实建目录、写探针、读 /proc/mounts —— 却跑在主线程。
   现在把 STORAGE 移入 reportDeviceBootInfo() 的后台线程、在 refreshLogDeviceContext()
   与 MiliLog.awaitServerIp() 之后上报: 上下文完整且不再阻塞主线程。

5. 【新增状态样本】每 6 小时一条 HEALTH(版本/在线/可用空间/曲库是否存在及大小/根目录)。
   日志此前只有事件没有状态样本,无法排查"随时间变化"的问题(空间变少、曲库丢失、长期离线)。

6. 【字段一致】「请求源=」不再为空: 走原生兜底时 JS 的 reportSongResult 不会触发,
   现在 SongUrlResolver 记录 lastHost,SongApiClient.lastSongSourceHost() 依次回退。

AGENTS.md 增补「写日志的五条约定」(只记一次/上下文完整/不做重活/可自我定位/状态样本+事件)。

版本 1.4.7(27) 到 1.4.8(28);签名证书不变。ktv_api.js 未改动,KTV_BRIDGE_API 仍为 8。
2026-09-26 20:38:52 +08:00
zijunchiang 1687f87eca 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。
2026-09-26 17:08:38 +08:00
zijunchiang 942ec527f1 V1.4.0: 修复断网重连后无法下载 + 网络状态检测维护
根因(断网重连后再也下载不了、日志却显示"歌曲源获取成功"):
SongApiClient.clearTokenCache() 一直是**空实现**。JS 侧 window.api.tokens 与
_apiInitPromise 因此被一直复用; 而歌曲源按「出口 IP + device」识别、试用窗口
也记在出口 IP 上(见 Eac19WindowGuard), 断网/重连(尤其换出口 IP)后旧 token 已失效 ——
取址这一步看起来成功, 真正下载却 403/失败。

1) 网络状态检测(新增 KtvNetworkMonitor)
   走 ConnectivityManager 回调(不轮询)维护全局 online, 状态变化才回调主线程;
   全局可读 online 供下载层判断失败是否由断网引起。

2) 播放页右上角「网络未连接」提示
   全屏播放页控制层 header 内、标题右侧新增红色徽标(不遮挡右上角二维码),
   断网显示、恢复自动隐藏。

3) 网络恢复/换网后的统一维护 refreshNetworkState()
   - 重建歌曲源会话: clearTokenCache() 真正下发 KtvJsBridge.resetApiSession()
     (置空 window.api 与 _apiInitPromise, 下次取址重新构造 API 实例、回到默认源顺序
      并重新登录拿新 token);
   - 复位 Eac19WindowGuard 窗口守卫; 重解析网卡 MAC + 刷新日志上下文;
   - 重启本地点歌服务并刷新二维码;
   - 自动重试断网期间失败的下载(最多 8 首)。
   断网恢复走该路径(带提示), 换网换 IP 也走同一路径(静默)。

4) 下载层网络失败跟踪(SongOkDownloadManager)
   结合 KtvNetworkMonitor.online 与异常类型(UnknownHost/Connect/SocketTimeout/
   NoRouteToHost/SSL/EOF)及 HTTP 0 判定网络类失败, 记入 networkFailed;
   成功后清除、手动取消不入集合; retryNetworkFailed() 供恢复时调用。

5) 顺带修掉一个隐患: 离线时 localIp() 为空会让 remoteNetworkMonitor 每 5s 误判
   "IP 变化"而反复重启本地点歌服务; 现已加空值短路, 离线只等网络回调。

版本号 1.3.9 -> 1.4.0(versionCode 15 -> 16)。按约定本次不发布 Release, 仅本地打包测试。
2026-09-21 03:34:29 +08:00
zijunchiang ba31caaa38 全量迁移到 Gitea: App更新检查/JS热更新端点上链 Gitea, 文档同步
- AppUpdateManager: releases/latest 改用 gitea.kasugano.cn API
- KtvJsBridge: JS 热更新改用 Gitea raw(/raw/branch/master/...)
- MainActivity/SongApiClient/DatabaseBootstrapper/MuseDbSync: 文案与注释更新
- README.md/README.en.md: 标注本仓库地址(Gitea), 保留上游框架归属链接
- AGENTS.md: 运行时端点迁移状态
2026-09-11 23:08:46 +08:00
zijunchiang 2d3751064f 歌曲源: 新增6源(desktok.top/mm.kk456.top/w.w345.my:66/z.w345.my/b.sktv.top/w345.my) + 多源自动排序
- ktv_api.js: HOSTS 扩为 7 源(同后台多入口), 缓存标记 7→8
- 自动排序: 解析失败的源 demote 到队尾, 成功源 promote 到队首(sourceOrder 运行时可变)
- JS→Kotlin 上报实际命中源(reportSongResult), 远程日志请求源不再写死
- SongApiClient/KtvJsBridge: lastSongSourceHost 供日志使用
2026-09-04 23:06:05 +08:00
zijunchiang 0c26f6da68 麦动KTV: 移除Gitee数据库(改官方Muse同步), 歌曲源切w.w345.my, 远程日志+广告屏蔽
- DatabaseBootstrapper: 删除 Gitee 分片/manifest/blob 下载逻辑, 曲库只走官方 MuseDbSync
- build.gradle: 移除 GITEE_DATABASE_* 构建参数
- MainActivity: 重置数据库文案改为官方同步
- ktv_api.js: 歌曲源仅 w.w345.my, 缓存标记 7, 广告/错号资源拦截+重试
- 远程日志: 点歌记录 编号/标题/请求源/状态
2026-09-04 21:23:21 +08:00