Add MusicWorkshop application
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
# Metadata Services
|
||||
|
||||
这个目录用来管理 `MusicWorkshop` 的第三方元数据代理,不改主项目前后端代码。
|
||||
|
||||
当前默认地址:
|
||||
|
||||
- 网易云:`http://127.0.0.1:3000`
|
||||
- QQ:`http://127.0.0.1:3300`
|
||||
|
||||
这两个地址与后端默认配置保持一致,可直接填到设置页的 `metadata.netease` 和 `metadata.qq`。
|
||||
|
||||
## 目录说明
|
||||
|
||||
```text
|
||||
services/metadata/
|
||||
README.md
|
||||
versions.lock
|
||||
config/services.env.example
|
||||
scripts/
|
||||
src/ # 第三方源码,已加入 .gitignore
|
||||
```
|
||||
|
||||
运行时日志和 PID 会写到:
|
||||
|
||||
```text
|
||||
.dev-runtime/metadata/
|
||||
```
|
||||
|
||||
## 初始化
|
||||
|
||||
先按需复制一份本地覆盖配置:
|
||||
|
||||
```bash
|
||||
cd /home/liumangmang/GiteaRepos/LiuMangMang/MusicWorkshop/services/metadata
|
||||
cp config/services.env.example .env.local
|
||||
```
|
||||
|
||||
然后执行初始化:
|
||||
|
||||
```bash
|
||||
./scripts/bootstrap.sh
|
||||
```
|
||||
|
||||
它会:
|
||||
|
||||
- 在 `src/netease-api/` 准备一个本地 npm 工作区,并固定安装 `NeteaseCloudMusicApi`
|
||||
- 拉取 QQ 的第三方源码到 `src/qq-music-api/`
|
||||
- 将 QQ 固定到 `versions.lock` 中的 revision
|
||||
- 安装各自依赖
|
||||
- 为 QQ 服务补一个最小端口兼容补丁(仅当上游仍写死 `3200` 时)
|
||||
- 为 QQ 服务补一个 `/search` 兼容层,让返回结构对齐 `MusicWorkshop` 当前后端解析逻辑
|
||||
|
||||
如果 `src/qq-music-api/` 里已经存在你自己的工作区,`bootstrap.sh` 会保留现状,只提示当前 revision 和锁定 revision 是否一致。
|
||||
|
||||
## 启动与停止
|
||||
|
||||
启动:
|
||||
|
||||
```bash
|
||||
./scripts/start.sh
|
||||
```
|
||||
|
||||
查看状态:
|
||||
|
||||
```bash
|
||||
./scripts/status.sh
|
||||
```
|
||||
|
||||
停止:
|
||||
|
||||
```bash
|
||||
./scripts/stop.sh
|
||||
```
|
||||
|
||||
## 设置页填写
|
||||
|
||||
在 MusicWorkshop 设置页中填:
|
||||
|
||||
- 网易云音乐 API:`http://127.0.0.1:3000`
|
||||
- QQ 音乐 API:`http://127.0.0.1:3300`
|
||||
|
||||
保存后,前端会从“尚未检测”进入实际探测状态。
|
||||
|
||||
## 上游来源
|
||||
|
||||
- 网易云默认使用 npm 包 `NeteaseCloudMusicApi`,因为 GitHub 归档仓库主分支已被清空,只保留版权说明
|
||||
- QQ 默认使用 `Rain120/qq-music-api` 的固定 revision,因为当前后端已经兼容它的 `data.song.list` / `result.list`
|
||||
|
||||
如果你要切换到自己的 fork,只需要改 `.env.local` 里的源类型和对应的 `*_REPO_URL` / `*_REF`。
|
||||
@@ -0,0 +1,20 @@
|
||||
# 复制为 services/metadata/.env.local 后按需覆盖。
|
||||
|
||||
NETEASE_HOST="127.0.0.1"
|
||||
NETEASE_PORT="3000"
|
||||
|
||||
QQ_HOST="127.0.0.1"
|
||||
QQ_PORT="3300"
|
||||
|
||||
# 如果你想切换到自己的 fork 或镜像,可以覆盖下面这些值。
|
||||
# NETEASE_SOURCE_TYPE="git"
|
||||
# NETEASE_REPO_URL="https://github.com/your-org/NeteaseCloudMusicApi.git"
|
||||
# NETEASE_REF="your-commit-or-tag"
|
||||
# NETEASE_PACKAGE_NAME="NeteaseCloudMusicApi"
|
||||
# NETEASE_PACKAGE_VERSION="4.28.0"
|
||||
# QQ_REPO_URL="https://github.com/your-org/qq-music-api.git"
|
||||
# QQ_REF="your-commit-or-tag"
|
||||
|
||||
# status.sh 的搜索探针会用这组关键字。
|
||||
SEARCH_SMOKE_KEYWORDS="周杰伦"
|
||||
SEARCH_SMOKE_LIMIT="3"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
load_config
|
||||
ensure_command git
|
||||
ensure_command node
|
||||
ensure_command npm
|
||||
ensure_command python3
|
||||
|
||||
if [[ "$NETEASE_SOURCE_TYPE" == "npm" ]]; then
|
||||
log "准备 网易云音乐 API npm 包工作区: $NETEASE_DIR"
|
||||
prepare_npm_wrapper_dir "netease-proxy" "$NETEASE_PACKAGE_NAME" "$NETEASE_PACKAGE_VERSION" "$NETEASE_DIR"
|
||||
else
|
||||
clone_or_prepare_repo "网易云音乐 API" "$NETEASE_REPO_URL" "$NETEASE_REF" "$NETEASE_DIR"
|
||||
fi
|
||||
|
||||
clone_or_prepare_repo "QQ 音乐 API" "$QQ_REPO_URL" "$QQ_REF" "$QQ_DIR"
|
||||
|
||||
install_node_dependencies "网易云音乐 API" "$NETEASE_DIR"
|
||||
install_node_dependencies "QQ 音乐 API" "$QQ_DIR"
|
||||
ensure_qq_port_env_support
|
||||
ensure_qq_search_compat_route
|
||||
|
||||
log "初始化完成"
|
||||
log "网易云地址: http://$NETEASE_HOST:$NETEASE_PORT"
|
||||
log "QQ 地址: http://$QQ_HOST:$QQ_PORT"
|
||||
log "如需覆盖地址或上游 revision,可复制 config/services.env.example 到 .env.local 后修改"
|
||||
Executable
+436
@@ -0,0 +1,436 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
METADATA_ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PROJECT_ROOT="$(cd "$METADATA_ROOT_DIR/../.." && pwd)"
|
||||
RUNTIME_DIR="$PROJECT_ROOT/.dev-runtime/metadata"
|
||||
|
||||
|
||||
log() {
|
||||
printf '[metadata] %s\n' "$*"
|
||||
}
|
||||
|
||||
|
||||
fail() {
|
||||
printf '[metadata] %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
load_config() {
|
||||
[[ -f "$METADATA_ROOT_DIR/versions.lock" ]] || fail "缺少 versions.lock"
|
||||
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$METADATA_ROOT_DIR/versions.lock"
|
||||
if [[ -f "$METADATA_ROOT_DIR/.env.local" ]]; then
|
||||
# shellcheck disable=SC1091
|
||||
source "$METADATA_ROOT_DIR/.env.local"
|
||||
fi
|
||||
set +a
|
||||
|
||||
: "${NETEASE_SOURCE_TYPE:?}"
|
||||
: "${NETEASE_DIR_REL:?}"
|
||||
: "${NETEASE_HOST:?}"
|
||||
: "${NETEASE_PORT:?}"
|
||||
: "${QQ_SOURCE_TYPE:?}"
|
||||
: "${QQ_REPO_URL:?}"
|
||||
: "${QQ_REF:?}"
|
||||
: "${QQ_DIR_REL:?}"
|
||||
: "${QQ_HOST:?}"
|
||||
: "${QQ_PORT:?}"
|
||||
: "${QQ_DEFAULT_UPSTREAM_PORT:?}"
|
||||
: "${SEARCH_SMOKE_KEYWORDS:?}"
|
||||
: "${SEARCH_SMOKE_LIMIT:?}"
|
||||
|
||||
NETEASE_DIR="$METADATA_ROOT_DIR/$NETEASE_DIR_REL"
|
||||
QQ_DIR="$METADATA_ROOT_DIR/$QQ_DIR_REL"
|
||||
|
||||
if [[ "$NETEASE_SOURCE_TYPE" == "npm" ]]; then
|
||||
: "${NETEASE_PACKAGE_NAME:?}"
|
||||
: "${NETEASE_PACKAGE_VERSION:?}"
|
||||
elif [[ "$NETEASE_SOURCE_TYPE" == "git" ]]; then
|
||||
: "${NETEASE_REPO_URL:?}"
|
||||
: "${NETEASE_REF:?}"
|
||||
else
|
||||
fail "不支持的 NETEASE_SOURCE_TYPE: $NETEASE_SOURCE_TYPE"
|
||||
fi
|
||||
|
||||
if [[ "$QQ_SOURCE_TYPE" == "git" ]]; then
|
||||
: "${QQ_REPO_URL:?}"
|
||||
: "${QQ_REF:?}"
|
||||
else
|
||||
fail "不支持的 QQ_SOURCE_TYPE: $QQ_SOURCE_TYPE"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
ensure_command() {
|
||||
command -v "$1" >/dev/null 2>&1 || fail "当前环境缺少命令: $1"
|
||||
}
|
||||
|
||||
|
||||
ensure_runtime_dir() {
|
||||
mkdir -p "$RUNTIME_DIR"
|
||||
}
|
||||
|
||||
|
||||
pid_file_for() {
|
||||
printf '%s/%s.pid\n' "$RUNTIME_DIR" "$1"
|
||||
}
|
||||
|
||||
|
||||
log_file_for() {
|
||||
printf '%s/%s.log\n' "$RUNTIME_DIR" "$1"
|
||||
}
|
||||
|
||||
|
||||
pid_from_file() {
|
||||
local pid_file="$1"
|
||||
|
||||
[[ -f "$pid_file" ]] || return 1
|
||||
|
||||
local pid
|
||||
pid="$(tr -d '[:space:]' < "$pid_file")"
|
||||
[[ -n "$pid" ]] || return 1
|
||||
printf '%s\n' "$pid"
|
||||
}
|
||||
|
||||
|
||||
is_pid_running() {
|
||||
local pid="$1"
|
||||
kill -0 "$pid" 2>/dev/null
|
||||
}
|
||||
|
||||
|
||||
cleanup_stale_pid_file() {
|
||||
local pid_file="$1"
|
||||
|
||||
if pid="$(pid_from_file "$pid_file" 2>/dev/null)"; then
|
||||
if ! is_pid_running "$pid"; then
|
||||
rm -f "$pid_file"
|
||||
fi
|
||||
elif [[ -f "$pid_file" ]]; then
|
||||
rm -f "$pid_file"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
active_pid_from_file() {
|
||||
local pid_file="$1"
|
||||
|
||||
cleanup_stale_pid_file "$pid_file"
|
||||
|
||||
if pid="$(pid_from_file "$pid_file" 2>/dev/null)" && is_pid_running "$pid"; then
|
||||
printf '%s\n' "$pid"
|
||||
return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
ensure_service_source() {
|
||||
local service_name="$1"
|
||||
local service_dir="$2"
|
||||
|
||||
[[ -d "$service_dir" ]] || fail "$service_name 源码目录不存在: $service_dir,请先执行 bootstrap.sh"
|
||||
[[ -f "$service_dir/package.json" ]] || fail "$service_name 缺少 package.json: $service_dir"
|
||||
[[ -d "$service_dir/node_modules" ]] || fail "$service_name 依赖未安装: $service_dir/node_modules"
|
||||
}
|
||||
|
||||
|
||||
wait_for_url() {
|
||||
local service_name="$1"
|
||||
local pid_file="$2"
|
||||
local url="$3"
|
||||
local timeout_seconds="${4:-30}"
|
||||
|
||||
local elapsed=0
|
||||
while (( elapsed < timeout_seconds )); do
|
||||
if python3 - "$url" <<'PY'
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
url = sys.argv[1]
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=1) as response:
|
||||
status = getattr(response, "status", 200)
|
||||
raise SystemExit(0 if status < 500 else 1)
|
||||
except urllib.error.HTTPError as exc:
|
||||
raise SystemExit(0 if exc.code < 500 else 1)
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! active_pid_from_file "$pid_file" >/dev/null 2>&1; then
|
||||
fail "$service_name 在就绪前已退出,请检查日志"
|
||||
fi
|
||||
|
||||
sleep 1
|
||||
elapsed=$((elapsed + 1))
|
||||
done
|
||||
|
||||
fail "$service_name 在 ${timeout_seconds}s 内未就绪: $url"
|
||||
}
|
||||
|
||||
|
||||
stop_service_process() {
|
||||
local service_name="$1"
|
||||
local pid_file="$2"
|
||||
|
||||
if ! pid="$(active_pid_from_file "$pid_file" 2>/dev/null)"; then
|
||||
log "$service_name 未运行"
|
||||
return 0
|
||||
fi
|
||||
|
||||
log "停止 $service_name (PID $pid)"
|
||||
kill -TERM -- "-$pid" 2>/dev/null || kill -TERM "$pid" 2>/dev/null || true
|
||||
|
||||
local attempt=0
|
||||
while (( attempt < 10 )); do
|
||||
if ! is_pid_running "$pid"; then
|
||||
rm -f "$pid_file"
|
||||
log "$service_name 已停止"
|
||||
return 0
|
||||
fi
|
||||
sleep 1
|
||||
attempt=$((attempt + 1))
|
||||
done
|
||||
|
||||
kill -KILL -- "-$pid" 2>/dev/null || kill -KILL "$pid" 2>/dev/null || true
|
||||
rm -f "$pid_file"
|
||||
log "$service_name 已强制停止"
|
||||
}
|
||||
|
||||
|
||||
clone_or_prepare_repo() {
|
||||
local service_name="$1"
|
||||
local repo_url="$2"
|
||||
local ref="$3"
|
||||
local service_dir="$4"
|
||||
|
||||
mkdir -p "$(dirname "$service_dir")"
|
||||
|
||||
if [[ -d "$service_dir/.git" ]]; then
|
||||
local current_ref
|
||||
current_ref="$(git -C "$service_dir" rev-parse HEAD)"
|
||||
if [[ "$current_ref" == "$ref" ]]; then
|
||||
log "$service_name 源码已存在,revision 已匹配: $current_ref"
|
||||
else
|
||||
log "$service_name 源码已存在,保留当前工作区: $service_dir"
|
||||
log "$service_name 当前 revision: $current_ref"
|
||||
log "$service_name 锁定 revision: $ref"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -e "$service_dir" ]]; then
|
||||
fail "$service_name 目标路径已存在但不是 git 仓库: $service_dir"
|
||||
fi
|
||||
|
||||
log "拉取 $service_name 源码到 $service_dir"
|
||||
git clone "$repo_url" "$service_dir"
|
||||
git -C "$service_dir" checkout "$ref"
|
||||
}
|
||||
|
||||
|
||||
prepare_npm_wrapper_dir() {
|
||||
local service_name="$1"
|
||||
local package_name="$2"
|
||||
local package_version="$3"
|
||||
local service_dir="$4"
|
||||
|
||||
mkdir -p "$service_dir"
|
||||
|
||||
cat > "$service_dir/package.json" <<EOF
|
||||
{
|
||||
"name": "music-workshop-${service_name}",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"start": "node -e \\"require('${package_name}/app.js')\\""
|
||||
},
|
||||
"dependencies": {
|
||||
"${package_name}": "${package_version}"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
|
||||
install_node_dependencies() {
|
||||
local service_name="$1"
|
||||
local service_dir="$2"
|
||||
|
||||
log "安装 $service_name 依赖"
|
||||
(
|
||||
cd "$service_dir"
|
||||
npm install
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
ensure_qq_port_env_support() {
|
||||
local entry_file="$QQ_DIR/app.js"
|
||||
local marker_file="$QQ_DIR/.codex-port-patched"
|
||||
|
||||
[[ -f "$entry_file" ]] || return 0
|
||||
grep -q "process\\.env\\.PORT" "$entry_file" && return 0
|
||||
[[ -f "$marker_file" ]] && return 0
|
||||
|
||||
if grep -q "const PORT = 3200" "$entry_file"; then
|
||||
python3 - "$entry_file" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
entry = Path(sys.argv[1])
|
||||
source = entry.read_text()
|
||||
updated = source.replace(
|
||||
"const PORT = 3200",
|
||||
"const PORT = parseInt(process.env.PORT || '3200', 10)",
|
||||
1,
|
||||
)
|
||||
|
||||
if source != updated:
|
||||
entry.write_text(updated)
|
||||
PY
|
||||
touch "$marker_file"
|
||||
log "已为 QQ API 注入 PORT 环境变量支持"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
ensure_qq_search_compat_route() {
|
||||
local context_dir="$QQ_DIR/routers/context"
|
||||
local compat_file="$context_dir/compatSearch.js"
|
||||
local router_file="$QQ_DIR/routers/router.js"
|
||||
local index_file="$context_dir/index.js"
|
||||
|
||||
[[ -d "$context_dir" ]] || return 0
|
||||
[[ -f "$router_file" ]] || return 0
|
||||
[[ -f "$index_file" ]] || return 0
|
||||
|
||||
cat > "$compat_file" <<'EOF'
|
||||
const axios = require('axios');
|
||||
|
||||
module.exports = async (ctx, next) => {
|
||||
const keywords = (ctx.query.keywords || ctx.query.keyword || '').trim();
|
||||
const limit = Math.max(1, Number(ctx.query.limit) || 10);
|
||||
const page = Math.max(1, Number(ctx.query.page) || 1);
|
||||
|
||||
if (!keywords) {
|
||||
ctx.status = 400;
|
||||
ctx.body = {
|
||||
error: 'keywords is required',
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
'https://u.y.qq.com/cgi-bin/musicu.fcg',
|
||||
{
|
||||
comm: {
|
||||
ct: '19',
|
||||
cv: '1859',
|
||||
uin: '0',
|
||||
},
|
||||
req: {
|
||||
method: 'DoSearchForQQMusicDesktop',
|
||||
module: 'music.search.SearchCgiService',
|
||||
param: {
|
||||
grp: 1,
|
||||
num_per_page: limit,
|
||||
page_num: page,
|
||||
query: keywords,
|
||||
search_type: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0',
|
||||
Accept: 'application/json, text/plain, */*',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.8,en;q=0.5',
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'Sec-Fetch-Dest': 'empty',
|
||||
'Sec-Fetch-Mode': 'cors',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const songPayload = (((response.data || {}).req || {}).data || {}).body || {};
|
||||
ctx.status = 200;
|
||||
ctx.body = {
|
||||
data: {
|
||||
song: songPayload.song || {
|
||||
list: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
ctx.status = error.response?.status || 502;
|
||||
ctx.body = {
|
||||
error: 'qq search failed',
|
||||
detail: error.response?.data || error.message,
|
||||
};
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
EOF
|
||||
|
||||
python3 - "$router_file" "$index_file" <<'PY'
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
router_path = Path(sys.argv[1])
|
||||
index_path = Path(sys.argv[2])
|
||||
|
||||
router_source = router_path.read_text()
|
||||
route_line = "router.get('/search', context.compatSearch);\n"
|
||||
route_anchor = "router.get('/getHotkey', context.getHotKey);\n"
|
||||
if route_line not in router_source:
|
||||
if route_anchor not in router_source:
|
||||
raise SystemExit("QQ router anchor not found")
|
||||
router_source = router_source.replace(
|
||||
route_anchor,
|
||||
route_anchor + "\n" + route_line,
|
||||
1,
|
||||
)
|
||||
router_path.write_text(router_source)
|
||||
|
||||
index_source = index_path.read_text()
|
||||
require_line = "const compatSearch = require('./compatSearch');\n"
|
||||
require_anchor = "const getHotKey = require('./getHotkey');\n"
|
||||
if require_line not in index_source:
|
||||
if require_anchor not in index_source:
|
||||
raise SystemExit("QQ context require anchor not found")
|
||||
index_source = index_source.replace(
|
||||
require_anchor,
|
||||
require_anchor + require_line,
|
||||
1,
|
||||
)
|
||||
|
||||
export_line = "\tcompatSearch,\n"
|
||||
export_anchor = "\tgetHotKey,\n"
|
||||
if export_line not in index_source:
|
||||
if export_anchor not in index_source:
|
||||
raise SystemExit("QQ context export anchor not found")
|
||||
index_source = index_source.replace(
|
||||
export_anchor,
|
||||
export_anchor + export_line,
|
||||
1,
|
||||
)
|
||||
|
||||
index_path.write_text(index_source)
|
||||
PY
|
||||
}
|
||||
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
load_config
|
||||
ensure_command node
|
||||
ensure_command npm
|
||||
ensure_command python3
|
||||
ensure_command setsid
|
||||
ensure_runtime_dir
|
||||
|
||||
ensure_service_source "网易云音乐 API" "$NETEASE_DIR"
|
||||
ensure_service_source "QQ 音乐 API" "$QQ_DIR"
|
||||
ensure_qq_port_env_support
|
||||
ensure_qq_search_compat_route
|
||||
|
||||
start_service() {
|
||||
local service_name="$1"
|
||||
local service_key="$2"
|
||||
local service_dir="$3"
|
||||
local host="$4"
|
||||
local port="$5"
|
||||
|
||||
local pid_file
|
||||
local log_file
|
||||
pid_file="$(pid_file_for "$service_key")"
|
||||
log_file="$(log_file_for "$service_key")"
|
||||
|
||||
if pid="$(active_pid_from_file "$pid_file" 2>/dev/null)"; then
|
||||
log "$service_name 已在运行 (PID $pid)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
: > "$log_file"
|
||||
(
|
||||
cd "$service_dir"
|
||||
exec setsid env HOST="$host" PORT="$port" npm run start
|
||||
) >>"$log_file" 2>&1 &
|
||||
echo "$!" > "$pid_file"
|
||||
|
||||
wait_for_url "$service_name" "$pid_file" "http://$host:$port"
|
||||
log "$service_name 已启动: http://$host:$port"
|
||||
}
|
||||
|
||||
start_service "网易云音乐 API" "netease" "$NETEASE_DIR" "$NETEASE_HOST" "$NETEASE_PORT"
|
||||
start_service "QQ 音乐 API" "qq" "$QQ_DIR" "$QQ_HOST" "$QQ_PORT"
|
||||
Executable
+94
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
load_config
|
||||
ensure_command python3
|
||||
ensure_runtime_dir
|
||||
|
||||
print_runtime_status() {
|
||||
local label="$1"
|
||||
local service_key="$2"
|
||||
|
||||
local pid_file
|
||||
pid_file="$(pid_file_for "$service_key")"
|
||||
|
||||
if pid="$(active_pid_from_file "$pid_file" 2>/dev/null)"; then
|
||||
printf '%s: running (PID %s)\n' "$label" "$pid"
|
||||
else
|
||||
printf '%s: stopped\n' "$label"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
probe_service() {
|
||||
local label="$1"
|
||||
local provider="$2"
|
||||
local url="$3"
|
||||
local keywords="$4"
|
||||
local limit="$5"
|
||||
|
||||
python3 - "$label" "$provider" "$url" "$keywords" "$limit" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
label, provider, base_url, keywords, limit = sys.argv[1:]
|
||||
|
||||
|
||||
def request_json(url: str):
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "MusicWorkshop/metadata-status"})
|
||||
with urllib.request.urlopen(req, timeout=3) as response:
|
||||
return response.status, response.read().decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def parse_root(base: str):
|
||||
try:
|
||||
status, _ = request_json(base)
|
||||
return f"root: ok (HTTP {status})"
|
||||
except urllib.error.HTTPError as exc:
|
||||
if exc.code < 500:
|
||||
return f"root: ok (HTTP {exc.code})"
|
||||
return f"root: failed (HTTP {exc.code})"
|
||||
except Exception as exc:
|
||||
return f"root: failed ({exc})"
|
||||
|
||||
|
||||
def parse_search(provider_name: str, base: str):
|
||||
params = {"keywords": keywords, "limit": limit}
|
||||
if provider_name == "netease":
|
||||
params["type"] = "1"
|
||||
search_url = f"{base.rstrip('/')}/search?{urllib.parse.urlencode(params)}"
|
||||
|
||||
try:
|
||||
_, body = request_json(search_url)
|
||||
payload = json.loads(body)
|
||||
except Exception as exc:
|
||||
return f"search: failed ({exc})"
|
||||
|
||||
if provider_name == "netease":
|
||||
songs = ((payload.get("result") or {}).get("songs")) or []
|
||||
return f"search: ok ({len(songs)} song candidates)" if isinstance(songs, list) else "search: failed (result.songs 缺失)"
|
||||
|
||||
song_list = (
|
||||
((payload.get("data") or {}).get("song") or {}).get("list")
|
||||
or ((payload.get("result") or {}).get("list") or [])
|
||||
)
|
||||
return f"search: ok ({len(song_list)} song candidates)" if isinstance(song_list, list) else "search: failed (data.song.list/result.list 缺失)"
|
||||
|
||||
|
||||
print(f"{label} probes")
|
||||
print(f" {parse_root(base_url)}")
|
||||
print(f" {parse_search(provider, base_url)}")
|
||||
PY
|
||||
}
|
||||
|
||||
print_runtime_status "网易云音乐 API" "netease"
|
||||
probe_service "网易云音乐 API" "netease" "http://$NETEASE_HOST:$NETEASE_PORT" "$SEARCH_SMOKE_KEYWORDS" "$SEARCH_SMOKE_LIMIT"
|
||||
print_runtime_status "QQ 音乐 API" "qq"
|
||||
probe_service "QQ 音乐 API" "qq" "http://$QQ_HOST:$QQ_PORT" "$SEARCH_SMOKE_KEYWORDS" "$SEARCH_SMOKE_LIMIT"
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck disable=SC1091
|
||||
source "$SCRIPT_DIR/common.sh"
|
||||
|
||||
load_config
|
||||
ensure_runtime_dir
|
||||
|
||||
stop_service_process "QQ 音乐 API" "$(pid_file_for qq)"
|
||||
stop_service_process "网易云音乐 API" "$(pid_file_for netease)"
|
||||
@@ -0,0 +1,17 @@
|
||||
NETEASE_SOURCE_TYPE="npm"
|
||||
NETEASE_PACKAGE_NAME="NeteaseCloudMusicApi"
|
||||
NETEASE_PACKAGE_VERSION="4.28.0"
|
||||
NETEASE_DIR_REL="src/netease-api"
|
||||
NETEASE_HOST="127.0.0.1"
|
||||
NETEASE_PORT="3000"
|
||||
|
||||
QQ_SOURCE_TYPE="git"
|
||||
QQ_REPO_URL="https://github.com/Rain120/qq-music-api.git"
|
||||
QQ_REF="d7c09ce9e037ec75b06a99570be1773eef1f41ef"
|
||||
QQ_DIR_REL="src/qq-music-api"
|
||||
QQ_HOST="127.0.0.1"
|
||||
QQ_PORT="3300"
|
||||
QQ_DEFAULT_UPSTREAM_PORT="3200"
|
||||
|
||||
SEARCH_SMOKE_KEYWORDS="周杰伦"
|
||||
SEARCH_SMOKE_LIMIT="3"
|
||||
Reference in New Issue
Block a user