合并自建歌曲源支线(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:
@@ -50,7 +50,35 @@ object SongApiClient {
|
||||
musicno?.takeIf(String::isNotBlank)?.let { getSongDownloadUrl(it, "720", false) }
|
||||
|
||||
@JvmStatic
|
||||
fun getSongDownloadUrl(musicno: String, resolution: String, h265: Boolean): String? {
|
||||
fun getSongDownloadUrl(musicno: String, resolution: String, h265: Boolean): String? =
|
||||
getSongDownloadUrl(musicno, resolution, h265, "")
|
||||
|
||||
/**
|
||||
* 取址(**四条路径唯一的汇合点**)+ 「App 点歌推送」切入点。
|
||||
*
|
||||
* 为什么把推送挂在这里(而不是 JS/原生各自的成功分支):能拿到下载地址的路径有四条
|
||||
* (JS 桥 / 原生兜底 / DB 回退 / 主线程 DB 兜底),只有本方法能保证"**只要用户真的拿到了
|
||||
* 一条可信下载地址,就恰好推送一次**";挂在别处必然漏掉某条路径(历史上广告闸门就是这么漏的)。
|
||||
*
|
||||
* 推送本身是 fire-and-forget(见 [PushDownloadClient]):默认关闭、后台线程、8 秒超时、
|
||||
* 失败只写本地日志,**不阻塞播放/下载,也不改返回值**。
|
||||
*
|
||||
* @param songName 歌名(可选,仅用于推送日志与服务器核对,不入库)
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getSongDownloadUrl(musicno: String, resolution: String, h265: Boolean, songName: String): String? {
|
||||
val url = resolveSongDownloadUrl(musicno, resolution, h265)
|
||||
if (!url.isNullOrEmpty() && musicno.isNotBlank()) {
|
||||
runCatching { PushDownloadClient.offer(musicno.trim(), url, songName) }
|
||||
.onFailure { Log.w(TAG, "push download skipped", it) }
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
/** 真正的取址实现(推送不参与其逻辑,见上面的入口)。 */
|
||||
private fun resolveSongDownloadUrl(musicno: String, resolution: String, h265: Boolean): String? {
|
||||
// 空编号必须提前挡掉(推送的 `tid` 也要求非空),否则会拿空编号去扫一遍上游源
|
||||
if (musicno.isBlank()) return null
|
||||
// 主线程同步等待 WebView 回调会死锁;当前播放/下载流程会在 IO 线程调用这里。
|
||||
// ⚠️ 主线程**绝不能**走网络路径:原生兜底最多 7 个源 × 8 秒超时,会把 UI 线程
|
||||
// 卡死到 ANR(1.4.7 曾把这里改成先 resolveNatively 再回退,属于隐患)。
|
||||
@@ -127,9 +155,15 @@ object SongApiClient {
|
||||
return null
|
||||
}
|
||||
|
||||
/** 快速判断是否为 demo URL (不含 JS 的完整逻辑, 仅做硬特征兜底) */
|
||||
/**
|
||||
* 快速判断是否为 demo URL (不含 JS 的完整逻辑, 仅做硬特征兜底)。
|
||||
*
|
||||
* ⚠️ host 一律用 [AdVideoFilter.hostOf](**已剥离 query/fragment,且 URI 解析失败会退化**):
|
||||
* 旧实现直接 `java.net.URI(url).host`,query 里出现 `|`/空格这类字符时 `URI()` 抛异常,
|
||||
* 被 `?: return true` 当成 demo —— 合法真源地址会因此被拒("取址失败"的隐蔽来源)。
|
||||
*/
|
||||
private fun isLikelyDemoUrl(url: String): Boolean {
|
||||
val host = runCatching { java.net.URI(url).host }.getOrNull() ?: return true
|
||||
val host = AdVideoFilter.hostOf(url) ?: return true
|
||||
// 纯 IP 地址 → 高概率 demo
|
||||
if (host.matches(Regex("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}$"))) return true
|
||||
// Cloudflare R2 demo bucket (pub-*.r2.dev)
|
||||
|
||||
Reference in New Issue
Block a user