V1.5.5: 加入源 a.ktv8.my;"所有源失效"改为明确弹窗提示
一、加入歌曲源 a.ktv8.my(置于 gz.ac16.vip 之后)
实测(代理修复后,DNS 直连真实 IP 20~42ms):
- 真实 IP 154.219.102.181 —— 与 w345.my **同一台服务器**(同一后台的兄弟入口);
- GET / → 200(917B,主机面板默认占位页);
- i.php → 200,170B,与其它 5 个入口响应**逐字节一致**:
code=200 / msg=ok / ordertype=0 / end_date=2026-10-05 14:58:21 / retry=5 / heart_time=5400,token 正常下发;
- 自身媒体路径 /<编号>.ts 与 /ad_files/my_ad_video.ts 均 404(138B,与 e.ac19.cn 的 404 逐字节相同)
→ 它不托管广告媒体;
- ⚠️ do.php 取址行为未能定性: 测试期间本机出口 IP(与门店设备同一个 188.253.112.231)
的配额被反复探测耗尽,表现为"成功一两次后长时间静默丢包"(与代码里记录的
trial_times:2 × trial_duration:5 吻合)。故置于次位,由运行时升降序自动调整。
KTV_BRIDGE_API 10 → 11(ktv_api.js 两处标记 + KtvJsBridge 两处校验)。
二、"所有源都取不到地址"给出明确提示
新增 Eac19WindowGuard.onAllFailed ← noteAllSourcesFailed(),由 SongUrlResolver.resolve()
在**本轮彻底失败**时调用(403 / 只返回广告 / 网络异常都算),弹窗文案:
「远程源已全部失效,等待新版本修复」
- 与既有的 onBlocked(风控 403 → "换公网 IP")常常同时触发,因此在 onBlocked 处理器里
用 lastAllFailedAt() 做 60 秒去重,不会同时弹两个框;两者均 15 分钟节流。
- 以前这里只有一句"无法生成下载地址"的 toast,用户看起来像界面没反应。
版本 1.5.4(36) → 1.5.5(37);签名证书不变。测试 9 例全通过。
This commit is contained in:
@@ -165,7 +165,7 @@ $BT/apksigner verify 目标名.apk
|
|||||||
|
|
||||||
### 版本号
|
### 版本号
|
||||||
|
|
||||||
- `app/build.gradle`:`versionName "1.5.4"` / `versionCode 36`(18~21 被本机临时诊断包占用过)
|
- `app/build.gradle`:`versionName "1.5.5"` / `versionCode 37`(18~21 被本机临时诊断包占用过)
|
||||||
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
- App 内显示统一用纯版本号(不加 `(versionCode)` 括号)
|
||||||
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
- ⚠️ **发布构建已开启 NewApi 闸门**(`lint { checkReleaseBuilds true; fatal += ['NewApi'] }`):
|
||||||
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
任何「调用了高于 `minSdk 21` 且未做版本守卫的 API」都会让 `assembleRelease` 直接失败。
|
||||||
@@ -271,6 +271,13 @@ $BT/apksigner verify 目标名.apk
|
|||||||
**判定原则**:广告黑名单只作用于**返回地址(媒体 URL)的 host**,
|
**判定原则**:广告黑名单只作用于**返回地址(媒体 URL)的 host**,
|
||||||
**不作用于"取址入口"的 host** —— 两者不能混为一谈,否则会误杀唯一可用的源。
|
**不作用于"取址入口"的 host** —— 两者不能混为一谈,否则会误杀唯一可用的源。
|
||||||
- 该改动同时把 `KTV_BRIDGE_API` 由 9 提升到 **10**(`ktv_api.js` 两处标记 + `KtvJsBridge` 两处校验)。
|
- 该改动同时把 `KTV_BRIDGE_API` 由 9 提升到 **10**(`ktv_api.js` 两处标记 + `KtvJsBridge` 两处校验)。
|
||||||
|
- **`a.ktv8.my`(2026-10 加入,置于 `gz.ac16.vip` 之后)**:实测与 `w345.my` **同一台服务器**
|
||||||
|
(`154.219.102.181`),`GET /` 是主机面板默认页,`i.php` 返回与其它入口**逐字节一致**的
|
||||||
|
170B 响应(`code=200/end_date=2026-10-05/retry=5`)→ 同一后台的兄弟入口;
|
||||||
|
它自身媒体路径(`/<编号>.ts`、`/ad_files/…`)均 404 → **不托管广告媒体**。
|
||||||
|
⚠️ 其 `do.php` 取址行为**尚未定性**(测试时本机出口 IP 配额已被消耗殆尽),
|
||||||
|
因此置于次位、由运行时自动升降序。
|
||||||
|
- 该条改动把 `KTV_BRIDGE_API` 由 10 提升到 **11**(同样四处)。
|
||||||
- **⚠️ 取址有两条实现,改协议必须同时改**:JS 主路径(`ktv_api.js`)+ 原生兜底
|
- **⚠️ 取址有两条实现,改协议必须同时改**:JS 主路径(`ktv_api.js`)+ 原生兜底
|
||||||
(`SongUrlResolver.kt`,供 **WebView 不可用**的老盒子使用 —— 实测极米 H1S/GMUI2.0 上
|
(`SongUrlResolver.kt`,供 **WebView 不可用**的老盒子使用 —— 实测极米 H1S/GMUI2.0 上
|
||||||
WebView 导不出 `window.KtvApi`,桥永远 not ready,只靠 JS 时表现为
|
WebView 导不出 `window.KtvApi`,桥永远 not ready,只靠 JS 时表现为
|
||||||
@@ -388,6 +395,13 @@ $BT/apksigner verify 目标名.apk
|
|||||||
`APP-INIT ... logFile=<路径>` 指明位置;
|
`APP-INIT ... logFile=<路径>` 指明位置;
|
||||||
4. `MiliLog.init` 已提到 **`KtvApplication.onCreate`**(早于 `MainActivity`),
|
4. `MiliLog.init` 已提到 **`KtvApplication.onCreate`**(早于 `MainActivity`),
|
||||||
这样即使更早的初始化抛异常,也已经有日志可查。
|
这样即使更早的初始化抛异常,也已经有日志可查。
|
||||||
|
- ⚠️ **"所有源都取不到地址"必须给用户一句明确的话(2026-10 用户要求)**:
|
||||||
|
`Eac19WindowGuard.onAllFailed` ← `noteAllSourcesFailed()`,
|
||||||
|
由 `SongUrlResolver.resolve()` 在**本轮彻底失败**时调用(403 / 只发广告 / 网络异常都算),
|
||||||
|
弹窗文案固定为 **「远程源已全部失效,等待新版本修复」**。
|
||||||
|
它与 `onBlocked`(风控 403 → "换公网 IP")常常同时触发,因此 `onBlocked` 处理器里
|
||||||
|
用 `lastAllFailedAt()` 做了 60 秒去重,**不允许同时弹两个框**;两者都按 15 分钟节流。
|
||||||
|
以前这里只有一句"无法生成下载地址"的 toast,用户看着像界面没反应。
|
||||||
- 一段典型现场日志应为:`BOOT` → `STORAGE` → (点歌)`点歌编号=... 请求源=... 状态=成功 urlHost=...`
|
- 一段典型现场日志应为:`BOOT` → `STORAGE` → (点歌)`点歌编号=... 请求源=... 状态=成功 urlHost=...`
|
||||||
→ `PLAY ... src=local` → 每 6h `HEALTH ...`;异常时才有 `RAW-ERR`/`NET-ERR`/`NATIVE-SONG ...失败`。
|
→ `PLAY ... src=local` → 每 6h `HEALTH ...`;异常时才有 `RAW-ERR`/`NET-ERR`/`NATIVE-SONG ...失败`。
|
||||||
- **网络状态与断网恢复**:`KtvNetworkMonitor`(object,走 `ConnectivityManager` 回调、不轮询)
|
- **网络状态与断网恢复**:`KtvNetworkMonitor`(object,走 `ConnectivityManager` 回调、不轮询)
|
||||||
|
|||||||
+2
-2
@@ -11,8 +11,8 @@ android {
|
|||||||
applicationId "com.local.ktv"
|
applicationId "com.local.ktv"
|
||||||
minSdk 21
|
minSdk 21
|
||||||
targetSdk 25
|
targetSdk 25
|
||||||
versionCode 36
|
versionCode 37
|
||||||
versionName "1.5.4"
|
versionName "1.5.5"
|
||||||
ndk {
|
ndk {
|
||||||
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
// 双 ABI 打包:兼容 64 位(Exynos 1580 等 arm64-v8a)与 32 位设备(极米 Play 超越版
|
||||||
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
// 的 Amlogic 950X2/S905X2 采用 32 位 Android 用户态,必须包含 armeabi-v7a,否则
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
(function(global) {
|
(function(global) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
// KTV_BRIDGE_API: 10 (Android 缓存校验标记,请勿删除)
|
// KTV_BRIDGE_API: 11 (Android 缓存校验标记,请勿删除)
|
||||||
|
|
||||||
// ─── 配置 ──────────────────────────────────────────
|
// ─── 配置 ──────────────────────────────────────────
|
||||||
const CONFIG = {
|
const CONFIG = {
|
||||||
@@ -26,6 +26,9 @@
|
|||||||
// 2026-10 实测: 其余入口对每首歌统一返回广告/测试资源时,本入口仍稳定下发真源 CDN 地址,
|
// 2026-10 实测: 其余入口对每首歌统一返回广告/测试资源时,本入口仍稳定下发真源 CDN 地址,
|
||||||
// 故置于首位;它自己域名下的媒体始终是广告,由 AdVideoFilter 继续拦截。
|
// 故置于首位;它自己域名下的媒体始终是广告,由 AdVideoFilter 继续拦截。
|
||||||
'http://gz.ac16.vip',
|
'http://gz.ac16.vip',
|
||||||
|
// 与 w345.my 同一台服务器(154.219.102.181)的兄弟入口,登录层正常;
|
||||||
|
// 取址行为尚未定性 → 置于次位,由运行时自动升降序。
|
||||||
|
'http://a.ktv8.my',
|
||||||
'http://w.w345.my',
|
'http://w.w345.my',
|
||||||
'http://desktok.top',
|
'http://desktok.top',
|
||||||
'http://mm.kk456.top',
|
'http://mm.kk456.top',
|
||||||
@@ -444,7 +447,7 @@
|
|||||||
// ─── 导出 ──────────────────────────────────────────
|
// ─── 导出 ──────────────────────────────────────────
|
||||||
global.KtvApi = KtvApi;
|
global.KtvApi = KtvApi;
|
||||||
global.KtvApiConfig = CONFIG;
|
global.KtvApiConfig = CONFIG;
|
||||||
global.KtvBridgeApiVersion = 10;
|
global.KtvBridgeApiVersion = 11;
|
||||||
|
|
||||||
if (typeof module !== 'undefined' && module.exports) {
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
module.exports = {KtvApi: KtvApi, CONFIG: CONFIG};
|
module.exports = {KtvApi: KtvApi, CONFIG: CONFIG};
|
||||||
|
|||||||
@@ -27,12 +27,22 @@ object Eac19WindowGuard {
|
|||||||
/** UI 层订阅: 窗口到期(风控 403)时回调(已在主线程)。 */
|
/** UI 层订阅: 窗口到期(风控 403)时回调(已在主线程)。 */
|
||||||
@Volatile var onBlocked: (() -> Unit)? = null
|
@Volatile var onBlocked: (() -> Unit)? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI 层订阅:**所有源都取不到可用地址**时回调(已在主线程)。
|
||||||
|
*
|
||||||
|
* 与 [onBlocked](风控 403 / 只发广告)的区别:这里是"这一轮取址彻底失败"的总信号,
|
||||||
|
* 无论原因是 403、只返回广告、还是网络异常 —— 用户此时什么都点不了,
|
||||||
|
* 需要一句明确的话,而不是让界面看起来像没反应。
|
||||||
|
*/
|
||||||
|
@Volatile var onAllFailed: (() -> Unit)? = null
|
||||||
|
|
||||||
/** 距离上次提示的最小间隔, 防止连续失败时反复弹窗。 */
|
/** 距离上次提示的最小间隔, 防止连续失败时反复弹窗。 */
|
||||||
private const val MIN_PROMPT_INTERVAL_MS = 15 * 60_000L
|
private const val MIN_PROMPT_INTERVAL_MS = 15 * 60_000L
|
||||||
|
|
||||||
private val lock = Any()
|
private val lock = Any()
|
||||||
private var lastPromptAt = 0L
|
private var lastPromptAt = 0L
|
||||||
private var blockedEpisodeActive = false
|
private var blockedEpisodeActive = false
|
||||||
|
private var lastAllFailedAt = 0L
|
||||||
|
|
||||||
/** 由 JS 桥在 do.php 返回风控 403 时调用(任意线程)。 */
|
/** 由 JS 桥在 do.php 返回风控 403 时调用(任意线程)。 */
|
||||||
fun noteRiskBlocked() {
|
fun noteRiskBlocked() {
|
||||||
@@ -54,6 +64,26 @@ object Eac19WindowGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 由取址层在"**所有源都失败**"时调用(任意线程)。
|
||||||
|
* 与 [noteRiskBlocked] 同样按 15 分钟节流,避免每次点歌都弹。
|
||||||
|
*/
|
||||||
|
fun noteAllSourcesFailed() {
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val shouldNotify: Boolean
|
||||||
|
synchronized(lock) {
|
||||||
|
shouldNotify = now - lastAllFailedAt >= MIN_PROMPT_INTERVAL_MS
|
||||||
|
if (shouldNotify) lastAllFailedAt = now
|
||||||
|
}
|
||||||
|
if (shouldNotify) {
|
||||||
|
Log.w(TAG, "all song sources failed (no usable url)")
|
||||||
|
onAllFailed?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 最近一次"全部源失效"提示的时间(0 = 从未);供 403 提示去重,避免同时弹两个框。 */
|
||||||
|
fun lastAllFailedAt(): Long = lastAllFailedAt
|
||||||
|
|
||||||
/** 取到有效歌曲 URL(窗口恢复)时调用, 复位本次到期 episode。 */
|
/** 取到有效歌曲 URL(窗口恢复)时调用, 复位本次到期 episode。 */
|
||||||
fun noteUrlOk() {
|
fun noteUrlOk() {
|
||||||
synchronized(lock) {
|
synchronized(lock) {
|
||||||
|
|||||||
@@ -335,7 +335,7 @@ class KtvJsBridge(context: Context) {
|
|||||||
runCatching {
|
runCatching {
|
||||||
file.isFile && file.length() in 1_001..500_000 &&
|
file.isFile && file.length() in 1_001..500_000 &&
|
||||||
file.readText(Charsets.UTF_8).let {
|
file.readText(Charsets.UTF_8).let {
|
||||||
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 10")
|
it.contains("KtvApi") && it.contains("KTV_BRIDGE_API: 11")
|
||||||
}
|
}
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ class KtvJsBridge(context: Context) {
|
|||||||
val script = String(bytes, StandardCharsets.UTF_8)
|
val script = String(bytes, StandardCharsets.UTF_8)
|
||||||
require(
|
require(
|
||||||
script.length > 1000 && script.contains("KtvApi") &&
|
script.length > 1000 && script.contains("KtvApi") &&
|
||||||
script.contains("KTV_BRIDGE_API: 10"),
|
script.contains("KTV_BRIDGE_API: 11"),
|
||||||
) { "Invalid or incompatible JS file" }
|
) { "Invalid or incompatible JS file" }
|
||||||
tempFile.writeText(script, Charsets.UTF_8)
|
tempFile.writeText(script, Charsets.UTF_8)
|
||||||
if (!tempFile.renameTo(cacheFile)) {
|
if (!tempFile.renameTo(cacheFile)) {
|
||||||
|
|||||||
@@ -524,6 +524,8 @@ class MainActivity : AppCompatActivity() {
|
|||||||
Eac19WindowGuard.onBlocked = {
|
Eac19WindowGuard.onBlocked = {
|
||||||
main.post {
|
main.post {
|
||||||
if (isFinishing || isDestroyed) return@post
|
if (isFinishing || isDestroyed) return@post
|
||||||
|
// 刚刚弹过"全部源失效"就不再叠加第二个框(两者常常同时触发)。
|
||||||
|
if (System.currentTimeMillis() - Eac19WindowGuard.lastAllFailedAt() < 60_000L) return@post
|
||||||
AlertDialog.Builder(this)
|
AlertDialog.Builder(this)
|
||||||
.setTitle("歌曲源临时受限")
|
.setTitle("歌曲源临时受限")
|
||||||
.setMessage(
|
.setMessage(
|
||||||
@@ -538,6 +540,19 @@ class MainActivity : AppCompatActivity() {
|
|||||||
.showForTv(DialogInterface.BUTTON_POSITIVE)
|
.showForTv(DialogInterface.BUTTON_POSITIVE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 所有源都取不到地址(403 / 只发广告 / 网络异常都算):给用户一句明确的话。
|
||||||
|
// 以前这里只有"无法生成下载地址"的 toast,用户看着像没反应。
|
||||||
|
Eac19WindowGuard.onAllFailed = {
|
||||||
|
main.post {
|
||||||
|
if (isFinishing || isDestroyed) return@post
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setTitle("歌曲源不可用")
|
||||||
|
.setMessage("远程源已全部失效,等待新版本修复")
|
||||||
|
.setPositiveButton("知道了", null)
|
||||||
|
.create()
|
||||||
|
.showForTv(DialogInterface.BUTTON_POSITIVE)
|
||||||
|
}
|
||||||
|
}
|
||||||
// 初始化新布局的 UI 组件
|
// 初始化新布局的 UI 组件
|
||||||
initTvLayout()
|
initTvLayout()
|
||||||
val startupSong = restoredState.currentSong ?: orderQueue!!.firstOrNull()
|
val startupSong = restoredState.currentSong ?: orderQueue!!.firstOrNull()
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ object SongUrlResolver {
|
|||||||
private val DEFAULT_HOSTS = listOf(
|
private val DEFAULT_HOSTS = listOf(
|
||||||
// 与其他入口同一后台、同一协议;2026-10 实测唯一仍下发真源 CDN 地址的入口,放首位。
|
// 与其他入口同一后台、同一协议;2026-10 实测唯一仍下发真源 CDN 地址的入口,放首位。
|
||||||
"http://gz.ac16.vip",
|
"http://gz.ac16.vip",
|
||||||
|
// 与 w345.my 同机(154.219.102.181)的兄弟入口;实测登录层正常,取址行为待观察。
|
||||||
|
"http://a.ktv8.my",
|
||||||
"http://w.w345.my",
|
"http://w.w345.my",
|
||||||
"http://desktok.top",
|
"http://desktok.top",
|
||||||
"http://mm.kk456.top",
|
"http://mm.kk456.top",
|
||||||
@@ -199,6 +201,9 @@ object SongUrlResolver {
|
|||||||
Eac19WindowGuard.noteRiskBlocked()
|
Eac19WindowGuard.noteRiskBlocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 本轮取址彻底失败(无论原因是 403、只返回广告、还是网络异常):
|
||||||
|
// 发出"全部源失效"总信号,让界面明确告知用户,而不是看起来没反应。
|
||||||
|
Eac19WindowGuard.noteAllSourcesFailed()
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user