接入自建歌曲源服务器:源列表自建优先 + 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 例全过。
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
package com.local.ktv;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* 歌曲源列表 / 媒体地址允许清单 / 推送默认目标的纯 JVM 单测。
|
||||
*
|
||||
* 覆盖需求点名的两组:
|
||||
* ② 源列表解析(`sourceHosts` 为空时回落默认;含自建源优先);
|
||||
* ④ 推送默认目标(`pushServerUrl` 为空且首源为自建源时使用它,否则尊重显式配置)。
|
||||
*/
|
||||
public class SourceHostsTest {
|
||||
|
||||
// ── ② 源列表解析 ──
|
||||
|
||||
/** 空(旧配置没有这个键 / 用户清空)→ 回落默认:**自建源第一、官方源兜底**。 */
|
||||
@Test
|
||||
public void emptySourceListFallsBackToDefaultWithSelfHostFirst() {
|
||||
assertEquals(SourceHosts.DEFAULT, SourceHosts.parse(null));
|
||||
assertEquals(SourceHosts.DEFAULT, SourceHosts.parse(""));
|
||||
assertEquals(SourceHosts.DEFAULT, SourceHosts.parse(" \n \n"));
|
||||
assertEquals(SourceHosts.DEFAULT, SourceHosts.parseList(null));
|
||||
assertEquals(SourceHosts.DEFAULT, SourceHosts.parseList(Collections.<String>emptyList()));
|
||||
assertEquals("https://mdmuse.kasugano.cn", SourceHosts.DEFAULT.get(0));
|
||||
assertEquals("http://zk.ktv1.top:6677", SourceHosts.DEFAULT.get(1));
|
||||
assertTrue("默认列表里自建源必须排第一", SourceHosts.isSelfHost(SourceHosts.DEFAULT.get(0)));
|
||||
}
|
||||
|
||||
/** 设置页"一行一个":去空白/尾斜杠、按顺序去重、忽略非 http(s) 的错行。 */
|
||||
@Test
|
||||
public void parsesMultilineTextInOrder() {
|
||||
List<String> hosts = SourceHosts.parse(
|
||||
" https://mdmuse.kasugano.cn/ \n"
|
||||
+ "\n"
|
||||
+ "http://192.168.100.232:6677 \n"
|
||||
+ "zk.ktv1.top:6677\n" // 没写协议 → 忽略
|
||||
+ "https://mdmuse.kasugano.cn\n"); // 重复 → 去重
|
||||
assertEquals(Arrays.asList(
|
||||
"https://mdmuse.kasugano.cn",
|
||||
"http://192.168.100.232:6677"), hosts);
|
||||
// 逗号/分号也当分隔符(便于整条粘贴)
|
||||
assertEquals(2, SourceHosts.parse("https://a.example.com, http://b.example.com").size());
|
||||
assertEquals(2, SourceHosts.parse("https://a.example.com;http://b.example.com").size());
|
||||
}
|
||||
|
||||
/** 自建源优先:自建源放第一 + 官方源兜底,两份实现(JS/原生)都读这个顺序。 */
|
||||
@Test
|
||||
public void selfHostFirstThenOfficialFallback() {
|
||||
List<String> hosts = SourceHosts.parseList(Arrays.asList(
|
||||
"https://mdmuse.kasugano.cn",
|
||||
"http://zk.ktv1.top:6677"));
|
||||
assertTrue(SourceHosts.isSelfHost(hosts.get(0)));
|
||||
assertFalse(SourceHosts.isSelfHost(hosts.get(1)));
|
||||
// 主机名判定与 scheme/端口无关(内网直连也是自建源)
|
||||
assertTrue(SourceHosts.isSelfHost("http://mdmuse.kasugano.cn:6677"));
|
||||
assertFalse(SourceHosts.isSelfHost(null));
|
||||
assertFalse(SourceHosts.isSelfHost(""));
|
||||
}
|
||||
|
||||
// ── 允许清单解析 ──
|
||||
|
||||
@Test
|
||||
public void allowedMediaHostsDefaultAndNormalization() {
|
||||
assertEquals(SourceHosts.DEFAULT_ALLOWED_MEDIA_HOSTS, SourceHosts.parseAllowed(null));
|
||||
assertEquals(SourceHosts.DEFAULT_ALLOWED_MEDIA_HOSTS, SourceHosts.parseAllowed(" "));
|
||||
assertTrue(SourceHosts.DEFAULT_ALLOWED_MEDIA_HOSTS.contains("md.kasugano.cn"));
|
||||
assertTrue(SourceHosts.DEFAULT_ALLOWED_MEDIA_HOSTS.contains("192.168.100.232"));
|
||||
|
||||
// 整条粘贴 / 大写 / 端口 都要能归一化成主机名
|
||||
assertEquals(Arrays.asList("md.kasugano.cn", "192.168.100.232"),
|
||||
SourceHosts.parseAllowed("https://MD.Kasugano.cn:4433/media\n192.168.100.232:6677"));
|
||||
assertEquals("md.kasugano.cn", SourceHosts.normalizeAllowedHost(" md.kasugano.cn "));
|
||||
assertNull(SourceHosts.normalizeAllowedHost(" "));
|
||||
}
|
||||
|
||||
// ── ④ 推送默认目标 ──
|
||||
|
||||
@Test
|
||||
public void pushTargetAutoUsesFirstSelfHostSource() {
|
||||
// 留空 + 首源是自建源 → 用它(自建源接入后的默认行为)
|
||||
assertEquals("https://mdmuse.kasugano.cn",
|
||||
PushDownload.effectiveServerUrl("", SourceHosts.DEFAULT));
|
||||
assertEquals("http://192.168.100.232:6677",
|
||||
PushDownload.effectiveServerUrl(null, Collections.singletonList("http://192.168.100.232:6677")));
|
||||
// 空配置也照样能拼出端点
|
||||
assertEquals("https://mdmuse.kasugano.cn/push_download",
|
||||
PushDownload.endpoint(PushDownload.effectiveServerUrl("", SourceHosts.DEFAULT)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pushTargetRespectsExplicitConfigAndOff() {
|
||||
// 显式覆盖优先(现有用户配过地址的完全不受影响)
|
||||
assertEquals("http://192.168.1.10:6677",
|
||||
PushDownload.effectiveServerUrl(" http://192.168.1.10:6677 ", SourceHosts.DEFAULT));
|
||||
assertEquals("http://192.168.1.10:6677/push_download",
|
||||
PushDownload.endpoint(PushDownload.effectiveServerUrl("http://192.168.1.10:6677", SourceHosts.DEFAULT)));
|
||||
// 显式关闭:留空已被"自动"占用,所以用 off/none/关闭 明确关掉
|
||||
for (String off : new String[]{"off", "OFF", "none", "no", "0", "false", "关闭", "禁用"}) {
|
||||
assertEquals("显式关闭必须为空: " + off, "",
|
||||
PushDownload.effectiveServerUrl(off, SourceHosts.DEFAULT));
|
||||
assertNull("关闭时不能拼出端点", PushDownload.endpoint(
|
||||
PushDownload.effectiveServerUrl(off, SourceHosts.DEFAULT)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pushTargetStaysOffWhenFirstSourceIsOfficial() {
|
||||
List<String> officialFirst = new ArrayList<String>();
|
||||
officialFirst.add("http://zk.ktv1.top:6677");
|
||||
officialFirst.add("https://mdmuse.kasugano.cn");
|
||||
assertEquals("首源不是自建源 → 不自动开推送", "",
|
||||
PushDownload.effectiveServerUrl("", officialFirst));
|
||||
assertNull(PushDownload.endpoint(PushDownload.effectiveServerUrl("", officialFirst)));
|
||||
assertEquals("没有任何源 → 关闭", "",
|
||||
PushDownload.effectiveServerUrl("", Collections.<String>emptyList()));
|
||||
// 配置非法(漏写协议)仍按关闭处理(沿用旧口径)
|
||||
assertNull(PushDownload.endpoint(PushDownload.effectiveServerUrl("192.168.1.10:6677", SourceHosts.DEFAULT)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user