合并自建歌曲源支线(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
@@ -0,0 +1,98 @@
#!/bin/bash
# 目标机(Debian 13)上一键部署:ktv 用户/目录 → 安装代码 → 写配置 → 装 systemd 单元 → 启动自检
# 以 root 运行:bash deploy-root.sh
set -euo pipefail
# ⚠️ su -c 继承的 PATH 不含 /usr/sbin(useradd/ss 等都在那儿)
export PATH="$PATH:/usr/sbin:/sbin"
SRC="${SRC:-/home/zijun/ktv-src}" # 已由 zijun 用户上传的代码目录
DST=/opt/ktv-source
LIB=/var/lib/ktv-source
ETC=/etc/ktv-source
CFG="$ETC/server-config.json"
# ⚠️ 服务运行身份可配:数据落在 FUSE/云盘挂载(如 /home/<user>/…)时必须用该目录属主
# (FUSE 挂载只认挂载参数里的 uid),否则会出现"只读数据库 / 打不开数据库"。
SVC_USER="${SVC_USER:-ktv}"
echo "── 1/6 用户与目录(身份=$SVC_USER;已存在的目录不改归属,避免夺权)"
if [ "$SVC_USER" = "ktv" ]; then
id ktv >/dev/null 2>&1 || useradd --system --home "$LIB" --shell /usr/sbin/nologin ktv
fi
# install -d 会顺带 chown 已存在的目录 → 这里只在缺失时创建,绝不动现场目录的归属
[ -d "$DST" ] || install -d -o "$SVC_USER" -g "$SVC_USER" -m 0755 "$DST"
[ -d "$LIB" ] || install -d -o "$SVC_USER" -g "$SVC_USER" -m 0755 "$LIB" "$LIB/media" "$LIB/media/tmp"
[ -d "$LIB/media" ] || install -d -o "$SVC_USER" -g "$SVC_USER" -m 0755 "$LIB/media" "$LIB/media/tmp"
[ -d "$ETC" ] || install -d -o "$SVC_USER" -g "$SVC_USER" -m 0750 "$ETC"
echo "── 2/6 安装代码(从 $SRC → $DST)"
[ -f "$SRC/run.py" ] || { echo "缺少 $SRC/run.py,请先上传代码"; exit 1; }
find "$DST" -mindepth 1 -maxdepth 1 -exec rm -rf {} + 2>/dev/null || true
cp -a "$SRC/." "$DST/"
find "$DST" -name '__pycache__' -type d -prune -exec rm -rf {} + 2>/dev/null || true
chown -R "$SVC_USER":"$SVC_USER" "$DST"
chmod -R a+rX "$DST"
echo "── 3/6 配置(已存在则不覆盖,保留界面里改过的设置)"
if [ ! -f "$CFG" ]; then
cat > "$CFG" <<'JSON'
{
"service_host": "0.0.0.0",
"service_port": 6677,
"admin_host": "0.0.0.0",
"admin_port": 6678,
"admin_token": "",
"data_dir": "/var/lib/ktv-source",
"media_dir": "/var/lib/ktv-source/media",
"db_path": "/var/lib/ktv-source/server.db",
"log_path": "/var/lib/ktv-source/server.log",
"public_media_base": "http://192.168.100.232:6677/media",
"public_host": "192.168.100.232:6677",
"upstream_hosts": ["http://zk.ktv1.top:6677"],
"upstream_resolution": "720",
"upstream_mac": "800b510bf4fd",
"upstream_sn": "2d7bb4a7d7f7",
"upstream_request_interval_ms": 300,
"muse_login_url": "https://mconn.cherryonline.cn/mls-api/v1/login",
"muse_sync_url": "https://mconn.cherryonline.cn/mls-api/v1/sqlite/sync",
"muse_device_id": "s00e04b021b2d",
"muse_music_file_type": 214,
"muse_auto_sync_hours": 24,
"concurrency": 10,
"download_batch": 200,
"download_order": "hot",
"download_order_desc": true,
"verify_md5": false,
"min_file_bytes": 6291456,
"max_attempts": 3,
"retry_backoff_sec": 20,
"timeout_sec": 60,
"user_agent": "MaidongKTV/1.2"
}
JSON
chown "$SVC_USER":"$SVC_USER" "$CFG"
chmod 640 "$CFG"
fi
echo "── 4/6 systemd 单元"
# ⚠️ 已存在的单元不覆盖:现场可能改过 User/ProtectHome/ReadWritePaths(例如数据目录在 FUSE 挂载上)
if [ ! -f /etc/systemd/system/ktv-source.service ]; then
install -m 644 "$DST/deploy/ktv-source.service" /etc/systemd/system/ktv-source.service
echo " 已安装默认单元"
else
echo " 单元已存在,保留现场配置(如需重置请手动删除后重跑)"
fi
systemctl daemon-reload
systemctl enable ktv-source >/dev/null 2>&1 || true
systemctl restart ktv-source
sleep 2
echo "── 5/6 服务状态"
systemctl is-active ktv-source || { journalctl -u ktv-source -n 30 --no-pager; exit 1; }
ss -lntp 2>/dev/null | grep -E ':(6677|6678)' || echo " ⚠️ 没看到监听端口"
echo "── 6/6 本机自检"
curl -s -m 5 http://127.0.0.1:6677/healthz || true; echo
curl -s -m 5 -X POST -d 'action=get_config&device_id=deploy-test' http://127.0.0.1:6677/api.php || true; echo
curl -s -m 5 'http://127.0.0.1:6677/i.php?appid=x&mac=800b510bf4fd_2d7bb4a7d7f7&sn=2d7bb4a7d7f7&time=1&ver=2.0&vn=4.1.3.03281430&sign=x' || true; echo
curl -s -m 5 http://127.0.0.1:6678/api/stats | head -c 300 || true; echo
echo "── 完成。管理界面: http://192.168.100.232:6678/ 服务接口: http://192.168.100.232:6677/"
@@ -0,0 +1,34 @@
[Unit]
Description=麦动KTV 自建歌曲源服务器(服务端口 + 管理端口)
Documentation=file:/opt/ktv-source/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=ktv
Group=ktv
WorkingDirectory=/opt/ktv-source
Environment=PYTHONUNBUFFERED=1
Environment=KTV_CONFIG=/etc/ktv-source/server-config.json
ExecStart=/usr/bin/python3 /opt/ktv-source/run.py --config /etc/ktv-source/server-config.json
# 管理端口与服务端口由同一进程监听;需要彻底隔离时拆成两个单元(用 --no-download 只跑服务)
Restart=always
RestartSec=3
TimeoutStopSec=20
KillSignal=SIGTERM
# ⚠️ 数据目录布局两种(本部署用的是第二种):
# A) 数据在 /var/lib/ktv-source → User=ktv + ProtectHome=yes
# B) 数据在 /home/<user>/… (尤其 FUSE/云盘/网络盘)→ 必须 User=<该目录属主> + ProtectHome=no,
# 并把 db_journal_mode 设为 DELETE(WAL 需要 mmap -shm,这类文件系统上会 SIGBUS 打死进程)
# 安全加固(Debian 13 / systemd 256+)
NoNewPrivileges=true
PrivateTmp=true
# true 只保护 /usr 与 /boot;full 会把 /etc 变只读,导致管理界面无法保存 server-config.json
ProtectSystem=true
ProtectHome=true
ReadWritePaths=/var/lib/ktv-source /etc/ktv-source
LimitNOFILE=65535
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,32 @@
# 可选:把媒体分发交给 nginx(吞吐更高、支持断点续传与限速)
# 用法:把本文件放到 /etc/nginx/sites-available/ktv-media 并 ln -s 到 sites-enabled,
# 同时把 server-config.json 的 public_media_base 指向 nginx 的域名/端口。
#
# ⚠️ App 只接受**域名**形式的媒体地址,且路径必须以 /<编号>.ts 结尾 —— 保持 location 不变。
# ⚠️ 不要让 nginx 返回 403 来表达"文件不存在"(App 会当成源站风控):
# 这里用 try_files 落到 404。
server {
listen 6677;
server_name ktv.example.com; # 换成你的域名
# 媒体:Range/206、缓存、限速(保护店内带宽)
location /media/ {
alias /var/lib/ktv-source/media/;
try_files $uri =404; # 不存在 → 404(不是 403)
add_header Accept-Ranges bytes always;
add_header Cache-Control "public, max-age=86400" always;
sendfile on;
tcp_nopush on;
# limit_rate 8m; # 需要时打开:单连接限速 8MB/s
# limit_conn perip 4; # 单 IP 并发连接限制
}
# App 接口仍由 Python 服务处理
location / {
proxy_pass http://127.0.0.1:6687; # 把 Python 的服务端口改到 6687(避免与自己冲突),对外仍是 6677
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 60s;
}
}