Compare commits

...
8 Commits
Author SHA1 Message Date
TIANYAO ZHANG a2982948a7 Merge pull request #71 from EvolvedGhost/main
fix(player): 歌曲结尾后持续卡死不切换下一首歌
2026-05-25 18:56:14 +08:00
EvolvedGhost b7e1f9f30b fix(player): add force trackEnd when pcmBuffer less than PCM_FRAME_BYTES 2026-05-23 22:08:52 +08:00
TIANYAO ZHANG 7fc5186c24 Merge pull request #65 from ZHANGTIANYAO1/fix/bilibili-wbi-search
fix(bilibili): wbi-sign search params — legacy /search/type now anti-bot blocked
2026-05-16 22:31:55 +08:00
saopig1andClaude Opus 4.7 de92c8bcd4 fix(bilibili): wbi-sign search params — legacy /search/type now anti-bot blocked
Closes #64. Bilibili moved the unsigned /x/web-interface/search/type endpoint
behind their anti-bot wall; it now returns an HTML error page (出错啦!) even
with buvid3+buvid4 cookies, causing `play -b` to report "No results found".

Switch search() to /x/web-interface/wbi/search/type with proper wbi signing:
fetch img_key/sub_key from /nav, derive the mixin key via the standard
permutation, and sign each request with wts + w_rid (md5). Keys are cached
for 6h since they rotate ~daily. Other endpoints (view, playurl, popular,
top/rcmd) still work unsigned and are left unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:29:12 +08:00
TIANYAO ZHANG 6b143e1a56 Merge pull request #63 from XuVIIJay/pr/setup-scripts
安装脚本优化(对国内网络环境安装友好)
2026-05-16 02:02:10 +08:00
XuVIIJay 5728209573 fix(setup.sh): add Node.js and npm version check before install 2026-05-15 21:03:28 +08:00
XuVIIJay 02d8b39d75 fix(setup.bat): remove hardcoded personal Node.js paths 2026-05-15 20:56:59 +08:00
XuVIIJay f87aaaf8c3 feat(scripts): optimize setup/start scripts for China network 2026-05-15 20:42:51 +08:00
7 changed files with 516 additions and 129 deletions

No files matched your search

+161
View File
@@ -0,0 +1,161 @@
#!/usr/bin/env node
/**
* Download native binaries (ffmpeg + @discordjs/opus) from npmmirror CDN.
* Called by setup.bat after npm install --ignore-scripts.
*
* Usage: node scripts/download-binaries.mjs [cdn_base_url]
*/
import { existsSync, mkdirSync, writeFileSync, statSync } from "node:fs";
import { tmpdir } from "node:os";
import { join, dirname } from "node:path";
import { createGunzip } from "node:zlib";
import { pipeline } from "node:stream/promises";
import { createWriteStream } from "node:fs";
import { get } from "node:https";
import { Readable } from "node:stream";
import { execSync } from "node:child_process";
import { createRequire } from "node:module";
import { fileURLToPath } from "node:url";
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
const CDN = process.argv[2] || "https://cdn.npmmirror.com/binaries";
const PLATFORM = process.platform;
const ARCH = process.arch;
const NODE_ABI = process.versions.modules;
function download(url) {
return new Promise((resolve, reject) => {
const req = get(url, { timeout: 120000 }, (res) => {
if (res.statusCode < 200 || res.statusCode >= 400) {
reject(new Error(`HTTP ${res.statusCode}: ${url}`));
return;
}
const chunks = [];
res.on("data", (c) => chunks.push(c));
res.on("end", () => resolve(Buffer.concat(chunks)));
});
req.on("error", reject);
req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
});
}
function log(msg) {
console.log(` [binary] ${msg}`);
}
function isValidSize(filePath, minBytes) {
try { return statSync(filePath).size >= minBytes; } catch { return false; }
}
async function downloadFfmpeg() {
const ffDir = join(ROOT, "node_modules", "ffmpeg-static");
const ffName = PLATFORM === "win32" ? "ffmpeg.exe" : "ffmpeg";
const ffDest = join(ffDir, ffName);
if (!existsSync(ffDir)) { log("ffmpeg-static not installed, skipping"); return false; }
if (existsSync(ffDest)) {
if (isValidSize(ffDest, 50 * 1024 * 1024)) {
log("ffmpeg already exists, skipping");
return true;
}
log("ffmpeg exists but seems corrupted (too small), re-downloading...");
}
const url = `${CDN}/ffmpeg-static/b6.1.1/ffmpeg-${PLATFORM}-${ARCH}.gz`;
log("Downloading ffmpeg...");
const buf = await download(url);
await pipeline(Readable.from(buf), createGunzip(), createWriteStream(ffDest));
try { execSync(`chmod +x "${ffDest}"`); } catch {}
const size = ((await statSync(ffDest)).size / 1024 / 1024).toFixed(1);
log(`ffmpeg OK (${size} MB)`);
return true;
}
async function downloadOpus() {
const opusDir = join(ROOT, "node_modules", "@discordjs", "opus");
const prebuildName = `node-v${NODE_ABI}-napi-v3-${PLATFORM}-${ARCH}-unknown-unknown`;
const opusDest = join(opusDir, "prebuild", prebuildName, "opus.node");
if (!existsSync(opusDir)) { log("@discordjs/opus not installed, skipping"); return false; }
if (existsSync(opusDest)) {
if (isValidSize(opusDest, 100 * 1024)) {
log("@discordjs/opus already exists, skipping");
return true;
}
log("@discordjs/opus exists but seems corrupted (too small), re-downloading...");
}
const url = `${CDN}/@discordjs/opus/v0.10.0/opus-v0.10.0-node-v${NODE_ABI}-napi-v3-${PLATFORM}-${ARCH}-unknown-unknown.tar.gz`;
log("Downloading @discordjs/opus...");
try {
const buf = await download(url);
mkdirSync(dirname(opusDest), { recursive: true });
const require = createRequire(import.meta.url);
const tar = require("tar");
const tmpFile = join(tmpdir(), `discordjs-opus-${Date.now()}.tar.gz`);
writeFileSync(tmpFile, buf);
await tar.extract({ cwd: join(opusDir, "prebuild"), file: tmpFile });
log("@discordjs/opus OK");
return true;
} catch (err) {
log(`CDN download failed (${err.message}), trying to build from source...`);
try {
execSync("npm rebuild @discordjs/opus", { cwd: ROOT, stdio: "inherit" });
if (existsSync(opusDest) && isValidSize(opusDest, 100 * 1024)) {
log("@discordjs/opus built from source OK");
return true;
}
log("Source build completed but .node file not found");
return false;
} catch (buildErr) {
log(`Source build failed: ${buildErr.message}`);
log("Install build tools: sudo apt install build-essential (Ubuntu/Debian)");
log(" sudo yum groupinstall 'Development Tools' (CentOS/RHEL)");
return false;
}
}
}
async function downloadBetterSqlite3() {
const pkgDir = join(ROOT, "node_modules", "better-sqlite3");
const dest = join(pkgDir, "build", "Release", "better_sqlite3.node");
if (!existsSync(pkgDir)) { log("better-sqlite3 not installed, skipping"); return false; }
if (existsSync(dest)) {
if (isValidSize(dest, 500 * 1024)) {
log("better-sqlite3 already exists, skipping");
return true;
}
log("better-sqlite3 exists but seems corrupted (too small), re-downloading...");
}
const version = "12.8.0";
const url = `${CDN}/better-sqlite3/v${version}/better-sqlite3-v${version}-node-v${NODE_ABI}-${PLATFORM}-${ARCH}.tar.gz`;
log("Downloading better-sqlite3...");
const buf = await download(url);
const require = createRequire(import.meta.url);
const tar = require("tar");
const tmpFile = join(tmpdir(), `better-sqlite3-${Date.now()}.tar.gz`);
writeFileSync(tmpFile, buf);
mkdirSync(dirname(dest), { recursive: true });
await tar.extract({ cwd: pkgDir, file: tmpFile });
if (existsSync(dest)) {
log(`better-sqlite3 OK (${((await statSync(dest)).size / 1024).toFixed(0)} KB)`);
return true;
}
log("better-sqlite3 extracted but .node file not found at expected path");
return false;
}
try {
const results = await Promise.all([downloadFfmpeg(), downloadOpus(), downloadBetterSqlite3()]);
if (results.some(Boolean)) {
console.log(" [binary] All downloads complete");
}
} catch (e) {
console.error(` [binary] ERROR: ${e.message}`);
process.exit(1);
}
+80 -107
View File
@@ -4,14 +4,13 @@ chcp 65001 >nul
title TSMusicBot Setup title TSMusicBot Setup
:: ============================================================ :: ============================================================
:: TSMusicBot Setup Script (Robust Edition) :: TSMusicBot Setup Script (Windows)
:: - Auto-detect China network, switch to npmmirror :: - Auto-detect China network, switch to npmmirror
:: - Strict error checking at every step :: - Download native binaries from CDN (避开 GitHub)
:: - Detailed logging to setup.log :: - 自动修复 PowerShell 环境变量
:: - Skip already-completed steps on re-run
:: ============================================================ :: ============================================================
set "SCRIPT_VERSION=2.0" set "SCRIPT_VERSION=2.1"
set "MIN_NODE_MAJOR=20" set "MIN_NODE_MAJOR=20"
set "LOG_FILE=%~dp0..\setup.log" set "LOG_FILE=%~dp0..\setup.log"
set "FAILED=0" set "FAILED=0"
@@ -44,42 +43,29 @@ echo.
:: ============================================================ :: ============================================================
:: Step 1: Check Node.js :: Step 1: Check Node.js
:: ============================================================ :: ============================================================
call :step "1/6" "Checking Node.js" call :step "1/7" "Checking Node.js"
where node >nul 2>&1 where node >nul 2>&1
if errorlevel 1 ( if not errorlevel 1 goto :check_node_version
call :error "Node.js not found in PATH." call :error "Node.js not found in PATH."
echo. echo.
echo Please install Node.js %MIN_NODE_MAJOR% LTS or newer from one of: echo Please install Node.js %MIN_NODE_MAJOR% LTS or newer from:
echo - https://nodejs.org/ ^(official^) echo https://nodejs.org/ (official)
echo - https://nodejs.cn/ ^(China mirror, recommended for CN users^) echo https://nodejs.cn/ (China mirror, recommended)
echo.
echo After installation:
echo 1. Close this window completely
echo 2. Open a NEW Command Prompt
echo 3. Run scripts\setup.bat again
echo. echo.
pause pause
exit /b 1 exit /b 1
)
:: Check Node version >= 20 :check_node_version
for /f "tokens=1 delims=v." %%a in ('node --version 2^>nul') do set "NODE_RAW=%%a"
for /f "tokens=1 delims=v." %%a in ('node --version 2^>nul') do (
for /f "tokens=1 delims=." %%b in ("%%a") do set "NODE_MAJOR=%%b"
)
:: Robust version parse
for /f "delims=" %%v in ('node --version 2^>nul') do set "NODE_VER=%%v" for /f "delims=" %%v in ('node --version 2^>nul') do set "NODE_VER=%%v"
set "NODE_VER_NUM=%NODE_VER:v=%" for /f "tokens=1 delims=v." %%a in ("%NODE_VER%") do set "NODE_MAJOR=%%a"
for /f "tokens=1 delims=." %%a in ("%NODE_VER_NUM%") do set "NODE_MAJOR=%%a"
call :log "Node.js version: %NODE_VER%" call :log "Node.js version: %NODE_VER%"
echo [OK] Node.js found: %NODE_VER% echo [OK] Node.js found: %NODE_VER%
if %NODE_MAJOR% LSS %MIN_NODE_MAJOR% ( if %NODE_MAJOR% LSS %MIN_NODE_MAJOR% (
call :error "Node.js version too old. Need %MIN_NODE_MAJOR%+, found %NODE_VER%." call :error "Node.js version too old. Need %MIN_NODE_MAJOR%+, found %NODE_VER%."
echo Please upgrade Node.js to version %MIN_NODE_MAJOR% LTS or newer.
pause pause
exit /b 1 exit /b 1
) )
@@ -88,12 +74,11 @@ echo.
:: ============================================================ :: ============================================================
:: Step 2: Check npm :: Step 2: Check npm
:: ============================================================ :: ============================================================
call :step "2/6" "Checking npm" call :step "2/7" "Checking npm"
where npm >nul 2>&1 where npm >nul 2>&1
if errorlevel 1 ( if errorlevel 1 (
call :error "npm not found. This is unusual since Node.js is installed." call :error "npm not found."
echo Please reinstall Node.js to fix this.
pause pause
exit /b 1 exit /b 1
) )
@@ -106,117 +91,91 @@ echo.
:: ============================================================ :: ============================================================
:: Step 3: Detect network and configure mirror :: Step 3: Detect network and configure mirror
:: ============================================================ :: ============================================================
call :step "3/6" "Checking network" call :step "3/7" "Checking network"
set "USE_MIRROR=0" set "USE_MIRROR=0"
set "MIRROR_REGISTRY=https://registry.npmjs.org"
:: Try reaching npm registry with a short timeout echo Testing connection to npm registry...
echo Testing connection to registry.npmjs.org...
call :log "Testing npm registry connectivity..." call :log "Testing npm registry connectivity..."
:: Use curl if available (more reliable than ping for HTTPS) ping -n 1 -w 4000 registry.npmjs.org >nul 2>&1
where curl >nul 2>&1
if not errorlevel 1 (
curl -s -o nul -m 5 -w "%%{http_code}" https://registry.npmjs.org/ > "%TEMP%\npmtest.txt" 2>nul
set /p HTTP_CODE=<"%TEMP%\npmtest.txt"
del "%TEMP%\npmtest.txt" >nul 2>&1
if "!HTTP_CODE!"=="200" (
echo [OK] npm registry reachable.
call :log "npm registry HTTP 200 OK"
) else (
echo [WARN] npm registry slow or unreachable ^(code: !HTTP_CODE!^).
call :log "npm registry returned: !HTTP_CODE!"
set "USE_MIRROR=1"
)
) else (
:: Fallback to ping
ping -n 1 -w 3000 registry.npmjs.org >nul 2>&1
if errorlevel 1 ( if errorlevel 1 (
echo [WARN] Cannot reach npm registry quickly. echo [WARN] Cannot reach npm registry quickly, using China mirror.
call :log "npm registry unreachable via ping"
set "USE_MIRROR=1" set "USE_MIRROR=1"
) else ( ) else (
echo [OK] npm registry reachable. echo [OK] npm registry reachable.
) call :log "npm registry reachable"
) )
if "%USE_MIRROR%"=="1" ( if "%USE_MIRROR%"=="1" (
echo. echo.
echo Slow connection detected. Switching to China mirror ^(npmmirror.com^)... echo [INFO] Using China mirror (npmmirror.com)
call :log "Switching to npmmirror.com" call :log "Switching to npmmirror.com"
call npm config set registry https://registry.npmmirror.com >>"%LOG_FILE%" 2>&1 set "MIRROR_REGISTRY=https://registry.npmmirror.com"
call npm config set disturl https://registry.npmmirror.com/-/binary/node >>"%LOG_FILE%" 2>&1 set "CDN_MIRROR=https://cdn.npmmirror.com/binaries"
call npm config set electron_mirror https://registry.npmmirror.com/-/binary/electron/ >>"%LOG_FILE%" 2>&1 ) else (
call npm config set sqlite3_binary_host_mirror https://registry.npmmirror.com/-/binary/better-sqlite3 >>"%LOG_FILE%" 2>&1 set "CDN_MIRROR="
call npm config set node_sqlite3_binary_host_mirror https://registry.npmmirror.com/-/binary/better-sqlite3 >>"%LOG_FILE%" 2>&1
call npm config set sharp_binary_host https://registry.npmmirror.com/-/binary/sharp >>"%LOG_FILE%" 2>&1
call npm config set sharp_libvips_binary_host https://registry.npmmirror.com/-/binary/sharp-libvips >>"%LOG_FILE%" 2>&1
call npm config set FFMPEG_BINARIES_URL https://registry.npmmirror.com/-/binary/ffmpeg-static >>"%LOG_FILE%" 2>&1
call npm config set @discordjs:registry https://registry.npmmirror.com >>"%LOG_FILE%" 2>&1
echo [OK] Mirror configured.
) )
echo. echo.
:: ============================================================ :: ============================================================
:: Step 4: Install backend dependencies :: Step 4: Install backend dependencies (跳过二进制)
:: ============================================================ :: ============================================================
call :step "4/6" "Installing backend dependencies" call :step "4/7" "Installing backend dependencies"
if exist "node_modules\.package-lock.json" ( if exist "node_modules\.package-lock.json" (
echo Found existing node_modules. Checking integrity... echo Found existing node_modules. Checking integrity...
call :log "Existing node_modules detected, running npm install to verify"
) )
echo Running: npm install ^(this can take 5-15 minutes on slow networks^) echo Running: npm install --ignore-scripts (跳过 GitHub 二进制下载)
echo Press Ctrl+C to abort.
echo. echo.
call npm install >>"%LOG_FILE%" 2>&1 call npm install --registry=%MIRROR_REGISTRY% --ignore-scripts >>"%LOG_FILE%" 2>&1
if errorlevel 1 ( if errorlevel 1 (
call :error "Backend npm install failed." call :error "Backend npm install failed."
echo. echo Check the log: %LOG_FILE%
echo Common causes:
echo - Network timeout ^(retry with VPN or check %LOG_FILE%^)
echo - Native module compile failure ^(missing Python/VS Build Tools^)
echo - Disk space full
echo.
echo Try manually:
echo cd /d "%PROJECT_ROOT%"
echo npm install --verbose
echo.
pause pause
exit /b 1 exit /b 1
) )
echo [OK] Backend dependencies installed. echo [OK] Backend dependencies installed.
echo. echo.
:: ============================================================
:: Step 4b: Download native binaries from CDN
:: ============================================================
call :step "4b/7" "Downloading native binaries"
node scripts/download-binaries.mjs %CDN_MIRROR% >>"%LOG_FILE%" 2>&1
if errorlevel 1 (
echo [WARN] Binary download had issues. Check %LOG_FILE% for details.
) else (
echo [OK] Native binaries installed.
)
echo.
:: ============================================================ :: ============================================================
:: Step 5: Install frontend dependencies :: Step 5: Install frontend dependencies
:: ============================================================ :: ============================================================
call :step "5/6" "Installing frontend dependencies" call :step "5/7" "Installing frontend dependencies"
if not exist "web\package.json" ( if not exist "web\package.json" (
call :error "web\package.json not found. Repository may be incomplete." call :error "web\package.json not found."
echo Please re-clone the repository:
echo git clone https://github.com/ZHANGTIANYAO1/teamspeak-music-bot.git
pause pause
exit /b 1 exit /b 1
) )
echo Running: npm install ^(in web/ directory^) echo Running: npm install (in web/)
echo. echo.
pushd web >nul pushd web >nul
call npm install >>"%LOG_FILE%" 2>&1 call npm install --registry=%MIRROR_REGISTRY% >>"%LOG_FILE%" 2>&1
set "WEB_INSTALL_RESULT=!errorlevel!" set "WEB_INSTALL_RESULT=!errorlevel!"
popd >nul popd >nul
if !WEB_INSTALL_RESULT! neq 0 ( if !WEB_INSTALL_RESULT! neq 0 (
call :error "Frontend npm install failed ^(exit code !WEB_INSTALL_RESULT!^)." call :error "Frontend npm install failed."
echo.
echo Try manually:
echo cd /d "%PROJECT_ROOT%\web"
echo npm install --verbose
echo.
pause pause
exit /b 1 exit /b 1
) )
@@ -224,29 +183,48 @@ echo [OK] Frontend dependencies installed.
echo. echo.
:: ============================================================ :: ============================================================
:: Step 6: Build project (backend + frontend) :: Step 6: Build project
:: ============================================================ :: ============================================================
call :step "6/6" "Building project" call :step "6/7" "Building project"
echo Running: npm run build echo Running: npm run build
echo. echo.
call npm run build >>"%LOG_FILE%" 2>&1 call npm run build >>"%LOG_FILE%" 2>&1
if errorlevel 1 ( if errorlevel 1 (
call :error "Build failed." call :error "Build failed. Check: %LOG_FILE%"
echo.
echo Check the log file for details: %LOG_FILE%
echo.
echo Try manually:
echo cd /d "%PROJECT_ROOT%"
echo npm run build
echo.
pause pause
exit /b 1 exit /b 1
) )
echo [OK] Build succeeded. echo [OK] Build succeeded.
echo. echo.
:: ============================================================
:: Step 7: Ensure PowerShell in PATH (修复 jdymusic CDN 播放)
:: ============================================================
call :step "7/7" "Checking PowerShell PATH"
where powershell >nul 2>&1
if errorlevel 1 (
echo [WARN] PowerShell not found in PATH.
echo Attempting to fix...
set "POWERSHELL_PATH=C:\Windows\System32\WindowsPowerShell\v1.0"
if exist "!POWERSHELL_PATH!\powershell.exe" (
:: 为用户添加永久 PATH 环境变量
echo [INFO] Adding PowerShell to user PATH...
call setx PATH "!POWERSHELL_PATH!;%PATH%" >nul 2>&1
echo [OK] PowerShell added to PATH. Please restart your terminal.
) else (
echo [WARN] Could not find powershell.exe on this system.
echo If you encounter playback issues with some NetEase songs,
echo run: set PATH=%%PATH%%;C:\Windows\System32\WindowsPowerShell\v1.0\
echo before running scripts\start.bat
)
) else (
echo [OK] PowerShell found in PATH.
)
echo.
:: ============================================================ :: ============================================================
:: Verify build outputs :: Verify build outputs
:: ============================================================ :: ============================================================
@@ -263,18 +241,13 @@ if not exist "web\dist" (
) )
if "!BUILD_OK!"=="0" ( if "!BUILD_OK!"=="0" (
echo.
echo Build completed but expected output is missing. echo Build completed but expected output is missing.
echo Check %LOG_FILE% for details.
pause pause
exit /b 1 exit /b 1
) )
echo [OK] Build outputs verified. echo [OK] Build outputs verified.
echo. echo.
:: ============================================================
:: Optional: config.json hint
:: ============================================================
if not exist "config.json" ( if not exist "config.json" (
echo [INFO] config.json will be auto-generated on first launch. echo [INFO] config.json will be auto-generated on first launch.
) else ( ) else (
@@ -294,9 +267,8 @@ echo.
echo Next steps: echo Next steps:
echo 1. Run: scripts\start.bat echo 1. Run: scripts\start.bat
echo 2. Open: http://localhost:3000 echo 2. Open: http://localhost:3000
echo 3. Follow the in-browser setup wizard.
echo. echo.
echo Setup log saved to: %LOG_FILE% echo Setup log: %LOG_FILE%
echo. echo.
pause pause
exit /b 0 exit /b 0
@@ -319,3 +291,4 @@ goto :eof
:log :log
echo [%time%] %~1 >> "%LOG_FILE%" echo [%time%] %~1 >> "%LOG_FILE%"
goto :eof goto :eof
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
set -euo pipefail
#
# TSMusicBot Setup Script (Linux/macOS)
# - Auto-detect China network, switch to npmmirror
# - Download native binaries from CDN (避开 GitHub)
# - One-click setup, same as setup.bat for Windows
#
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
LOG_FILE="$PROJECT_DIR/setup.log"
echo "============================================"
echo " TSMusicBot - First-Time Setup (Linux)"
echo "============================================"
echo ""
echo "Log file: $LOG_FILE"
echo ""
# ---- Check Node.js ----
if ! command -v node &>/dev/null; then
echo "[ERROR] Node.js not found. Please install Node.js 20+ from https://nodejs.org"
echo " or https://nodejs.cn/ (China mirror)."
exit 1
fi
echo "[OK] Node.js $(node -v)"
if ! command -v npm &>/dev/null; then
echo "[ERROR] npm not found."
exit 1
fi
echo "[OK] npm v$(npm -v)"
echo ""
# ---- Detect China network ----
USE_MIRROR=0
MIRROR_REGISTRY="https://registry.npmjs.org"
CDN_MIRROR=""
echo "Testing connection to npm registry..."
if ping -c 1 -W 4 registry.npmjs.org &>/dev/null; then
echo "[OK] npm registry reachable."
else
echo "[WARN] Cannot reach npm registry, using China mirror."
USE_MIRROR=1
fi
if [ "$USE_MIRROR" = "1" ]; then
echo "[INFO] Using China mirror (npmmirror.com)"
MIRROR_REGISTRY="https://registry.npmmirror.com"
CDN_MIRROR="https://cdn.npmmirror.com/binaries"
export npm_config_registry="$MIRROR_REGISTRY"
fi
echo ""
# ---- Check build tools (needed for native module fallback) ----
if ! command -v gcc &>/dev/null && ! command -v clang &>/dev/null; then
echo "[INFO] No C compiler found. If CDN binaries are unavailable,"
echo " native modules may fail. Install build tools:"
echo " sudo apt install build-essential (Ubuntu/Debian)"
echo " sudo yum groupinstall 'Development Tools' (CentOS/RHEL)"
echo ""
fi
# ---- Step 1: Install dependencies (skip GitHub binaries) ----
echo "---- 1/5: Installing Node.js dependencies ----"
echo ""
cd "$PROJECT_DIR"
npm install --registry="$MIRROR_REGISTRY" --ignore-scripts 2>&1 | tee -a "$LOG_FILE"
echo "[OK] Dependencies installed."
echo ""
# ---- Step 2: Download native binaries from CDN ----
echo "---- 2/5: Downloading native binaries ----"
echo ""
if node scripts/download-binaries.mjs $CDN_MIRROR 2>&1 | tee -a "$LOG_FILE"; then
echo "[OK] Native binaries installed."
else
echo "[WARN] Some native binaries had issues (will try source build as fallback)."
fi
echo ""
# ---- Step 3: Install web panel dependencies ----
echo "---- 3/5: Installing web panel dependencies ----"
echo ""
if [ -f "web/package.json" ]; then
cd "$PROJECT_DIR/web"
npm install --registry="$MIRROR_REGISTRY" 2>&1 | tee -a "$LOG_FILE"
cd "$PROJECT_DIR"
echo "[OK] Web panel dependencies installed."
else
echo "[SKIP] web/package.json not found."
fi
echo ""
# ---- Step 4: Build project ----
echo "---- 4/5: Building project ----"
echo ""
npm run build 2>&1 | tee -a "$LOG_FILE"
echo "[OK] Build succeeded."
echo ""
# ---- Step 5: Verify ----
echo "---- 5/5: Verifying build ----"
echo ""
BUILD_OK=1
if [ ! -d "dist" ]; then
echo "[ERROR] dist/ directory missing."
BUILD_OK=0
fi
if [ -d "web" ] && [ ! -d "web/dist" ]; then
echo "[ERROR] web/dist/ directory missing."
BUILD_OK=0
fi
if [ "$BUILD_OK" = "0" ]; then
echo "Build completed but expected output is missing."
exit 1
fi
echo "[OK] Build outputs verified."
echo ""
if [ ! -f "config.json" ]; then
echo "[INFO] config.json will be auto-generated on first launch."
fi
echo ""
echo "============================================"
echo " Setup Complete!"
echo "============================================"
echo ""
echo "Next steps:"
echo " 1. Run: npm start"
echo " 2. Open: http://localhost:3000"
echo ""
echo "Setup log: $LOG_FILE"
echo ""
+13 -16
View File
@@ -7,7 +7,7 @@ echo.
where node >nul 2>&1 where node >nul 2>&1
if %errorlevel% neq 0 ( if %errorlevel% neq 0 (
echo Node.js is not installed. echo Node.js is not installed.
echo Run scripts\setup.bat for automatic installation, or install Node.js 20+ from https://nodejs.org echo Run scripts\setup.bat first.
pause pause
exit /b 1 exit /b 1
) )
@@ -15,33 +15,30 @@ if %errorlevel% neq 0 (
:: Resolve project root (one level up from scripts/) :: Resolve project root (one level up from scripts/)
cd /d "%~dp0.." cd /d "%~dp0.."
:: Install dependencies if needed :: Check if dependencies are installed
if not exist "node_modules" ( if not exist "node_modules" (
echo Installing dependencies... echo Dependencies not found. Please run scripts\setup.bat first.
call npm install --production
if %errorlevel% neq 0 (
echo Failed to install dependencies.
pause pause
exit /b 1 exit /b 1
) )
)
:: Build if dist/ doesn't exist :: Check if build output exists
if not exist "dist" ( if not exist "dist" (
echo Building project... echo Build not found. Please run scripts\setup.bat first.
call npx tsc
if %errorlevel% neq 0 (
echo Build failed.
pause pause
exit /b 1 exit /b 1
) )
)
:: FFmpeg is bundled via ffmpeg-static — no PATH check needed. :: Ensure PowerShell is in PATH (fix for jdymusic CDN playback on some systems)
echo FFmpeg is bundled via node_modules (ffmpeg-static). where powershell >nul 2>&1
echo. if errorlevel 1 (
if exist "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" (
set "PATH=%PATH%;C:\Windows\System32\WindowsPowerShell\v1.0\"
)
)
:: Start the application :: Start the application
node dist/index.js node dist/index.js
pause pause
+54 -2
View File
@@ -136,6 +136,9 @@ export class AudioPlayer extends EventEmitter {
private static readonly HEALTHY_FRAME_RESET = 50; // ~1 second of audio private static readonly HEALTHY_FRAME_RESET = 50; // ~1 second of audio
private downloader: ChildProcess | null = null; private downloader: ChildProcess | null = null;
private currentTempDir: string | null = null; private currentTempDir: string | null = null;
private emptyFrameAttempts = 0;
private static readonly MAX_EMPTY_ATTEMPTS = 250; // ~5秒的20ms帧循环(增加容错)
private currentSongDuration = 0; // 当前歌曲总时长(秒)
constructor(logger: Logger) { constructor(logger: Logger) {
super(); super();
@@ -143,7 +146,7 @@ export class AudioPlayer extends EventEmitter {
this.logger = logger; this.logger = logger;
} }
play(url: string, seekSeconds = 0): void { play(url: string, seekSeconds = 0, songDuration = 0): void {
// 1. 停止当前所有播放,自增 sessionId 屏蔽旧回调 ( // 1. 停止当前所有播放,自增 sessionId 屏蔽旧回调 (
this.stop(); this.stop();
@@ -154,6 +157,8 @@ export class AudioPlayer extends EventEmitter {
this.healthyFrames = 0; this.healthyFrames = 0;
this.ffmpegPaused = false; this.ffmpegPaused = false;
this.spawnFailed = false; this.spawnFailed = false;
this.emptyFrameAttempts = 0;
this.currentSongDuration = songDuration;
if (this.consecutiveFailures >= AudioPlayer.MAX_CONSECUTIVE_FAILURES) { if (this.consecutiveFailures >= AudioPlayer.MAX_CONSECUTIVE_FAILURES) {
this.logger.error({ failures: this.consecutiveFailures }, "FFmpeg failures limit reached"); this.logger.error({ failures: this.consecutiveFailures }, "FFmpeg failures limit reached");
@@ -420,6 +425,49 @@ export class AudioPlayer extends EventEmitter {
if (this.state === "playing") this.sendNextFrame(); if (this.state === "playing") this.sendNextFrame();
else if (this.state === "paused") this.nextFrameTime = performance.now(); else if (this.state === "paused") this.nextFrameTime = performance.now();
// 检测pcmBuffer不足PCM_FRAME_BYTES导致连续循环卡死:
// 条件1: FFmpeg仍在运行但缓冲区不足一帧,且连续多次无法获取数据
// 条件2: 已播放时间接近歌曲结尾(最后5秒内)或未知时长
const elapsed = this.getElapsed();
const isNearEnd = this.currentSongDuration > 0
? (this.currentSongDuration - elapsed) <= 5 // 距离结尾不足5秒
: true; // 未知时长时保守处理
if (this.ffmpeg !== null && this.pcmBuffer.length < PCM_FRAME_BYTES) {
this.emptyFrameAttempts++;
// 只有同时满足:达到空帧阈值 + 接近结尾,才判定为播放结束
if (this.emptyFrameAttempts >= AudioPlayer.MAX_EMPTY_ATTEMPTS && isNearEnd) {
this.logger.info({
sessionId: this.sessionId,
emptyAttempts: this.emptyFrameAttempts,
bufferSize: this.pcmBuffer.length,
elapsed: Math.round(elapsed),
duration: this.currentSongDuration,
remaining: Math.round(this.currentSongDuration - elapsed)
}, "FFmpeg stopped outputting data near end, ending track");
this.frameLoopRunning = false;
if (this.state !== "idle") {
this.state = "idle";
// 清理FFmpeg进程
if (this.ffmpeg) {
const procToKill = this.ffmpeg;
const pidToKill = procToKill.pid;
this.ffmpeg = null;
if (pidToKill) {
this.forceCleanup(procToKill, pidToKill);
}
}
this.consecutiveFailures = 0;
this.emit("trackEnd");
}
return;
}
} else {
// 成功获取数据或FFmpeg已结束,重置计数器
this.emptyFrameAttempts = 0;
}
if (!this.ffmpeg && this.pcmBuffer.length < PCM_FRAME_BYTES) { if (!this.ffmpeg && this.pcmBuffer.length < PCM_FRAME_BYTES) {
this.frameLoopRunning = false; this.frameLoopRunning = false;
if (this.state !== "idle") { if (this.state !== "idle") {
@@ -472,7 +520,11 @@ export class AudioPlayer extends EventEmitter {
} }
getElapsed(): number { return this.seekOffset + (this.framesPlayed * FRAME_DURATION_MS) / 1000; } getElapsed(): number { return this.seekOffset + (this.framesPlayed * FRAME_DURATION_MS) / 1000; }
seek(seconds: number): void { if (this.currentUrl && Number.isFinite(seconds) && seconds >= 0) this.play(this.currentUrl, seconds); } seek(seconds: number): void {
if (this.currentUrl && Number.isFinite(seconds) && seconds >= 0) {
this.play(this.currentUrl, seconds, this.currentSongDuration);
}
}
pause(): void { if (this.state === "playing") this.state = "paused"; } pause(): void { if (this.state === "playing") this.state = "paused"; }
resume(): void { if (this.state === "paused") { this.state = "playing"; this.nextFrameTime = performance.now(); } } resume(): void { if (this.state === "paused") { this.state = "playing"; this.nextFrameTime = performance.now(); } }
resetFailures(): void { this.consecutiveFailures = 0; } resetFailures(): void { this.consecutiveFailures = 0; }
+1 -1
View File
@@ -380,7 +380,7 @@ export class BotInstance extends EventEmitter {
return false; return false;
} }
song.url = url; song.url = url;
this.player.play(url); this.player.play(url, 0, song.duration);
this.database.addPlayHistory({ this.database.addPlayHistory({
botId: this.id, botId: this.id,
songId: song.id, songId: song.id,
+62 -3
View File
@@ -1,3 +1,4 @@
import { createHash } from "node:crypto";
import axios, { type AxiosInstance } from "axios"; import axios, { type AxiosInstance } from "axios";
import type { import type {
MusicProvider, MusicProvider,
@@ -15,6 +16,16 @@ const BILIBILI_HEADERS = {
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
}; };
// Permutation used by B站 to derive the wbi mixin key from img_key+sub_key.
const WBI_MIXIN_KEY_ENC_TAB = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40, 61,
26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11, 36,
20, 34, 44, 52,
];
const WBI_KEY_TTL_MS = 6 * 60 * 60 * 1000; // wbi keys rotate ~daily; refresh every 6h
export class BiliBiliProvider implements MusicProvider { export class BiliBiliProvider implements MusicProvider {
readonly platform = "bilibili" as const; readonly platform = "bilibili" as const;
private api: AxiosInstance; private api: AxiosInstance;
@@ -24,6 +35,8 @@ export class BiliBiliProvider implements MusicProvider {
private cidCache = new Map<string, number>(); private cidCache = new Map<string, number>();
private buvidCookie = ""; // anonymous session cookie (buvid3) for anti-412 private buvidCookie = ""; // anonymous session cookie (buvid3) for anti-412
private buvidInitialized = false; private buvidInitialized = false;
private wbiMixinKey = "";
private wbiKeyFetchedAt = 0;
constructor() { constructor() {
this.api = axios.create({ this.api = axios.create({
@@ -62,6 +75,50 @@ export class BiliBiliProvider implements MusicProvider {
return combined ? { Cookie: combined } : {}; return combined ? { Cookie: combined } : {};
} }
/**
* Fetch wbi img_key/sub_key from /x/web-interface/nav and derive the
* mixin key used to sign search params. Required since B站 moved the
* search endpoint behind wbi signing — unsigned /search/type now
* returns an anti-bot HTML page.
*/
private async ensureWbiKeys(): Promise<void> {
if (this.wbiMixinKey && Date.now() - this.wbiKeyFetchedAt < WBI_KEY_TTL_MS) {
return;
}
const res = await this.api.get("/x/web-interface/nav", {
headers: this.cookieHeaders,
validateStatus: () => true, // nav returns -101 when not logged in but still includes wbi_img
});
const wbi = res.data?.data?.wbi_img;
const imgUrl: string = wbi?.img_url ?? "";
const subUrl: string = wbi?.sub_url ?? "";
const imgKey = imgUrl.split("/").pop()?.split(".")[0] ?? "";
const subKey = subUrl.split("/").pop()?.split(".")[0] ?? "";
if (!imgKey || !subKey) {
throw new Error("Bilibili wbi keys unavailable");
}
const raw = imgKey + subKey;
this.wbiMixinKey = WBI_MIXIN_KEY_ENC_TAB.map((i) => raw[i] ?? "")
.join("")
.slice(0, 32);
this.wbiKeyFetchedAt = Date.now();
}
/** Sign params for wbi-protected endpoints. Returns a new params object including wts and w_rid. */
private signWbi(params: Record<string, string | number>): Record<string, string> {
const withTs: Record<string, string> = {};
for (const [k, v] of Object.entries(params)) withTs[k] = String(v);
withTs.wts = String(Math.floor(Date.now() / 1000));
const sorted = Object.keys(withTs)
.sort()
.map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(withTs[k])}`)
.join("&");
withTs.w_rid = createHash("md5")
.update(sorted + this.wbiMixinKey)
.digest("hex");
return withTs;
}
setQuality(quality: string): void { setQuality(quality: string): void {
this.quality = quality; this.quality = quality;
} }
@@ -91,12 +148,14 @@ export class BiliBiliProvider implements MusicProvider {
async search(query: string, limit = 20): Promise<SearchResult> { async search(query: string, limit = 20): Promise<SearchResult> {
await this.ensureBuvidCookie(); await this.ensureBuvidCookie();
const res = await this.api.get("/x/web-interface/search/type", { await this.ensureWbiKeys();
params: { const signed = this.signWbi({
search_type: "video", search_type: "video",
keyword: query, keyword: query,
page_size: limit, page_size: limit,
}, });
const res = await this.api.get("/x/web-interface/wbi/search/type", {
params: signed,
headers: this.cookieHeaders, headers: this.cookieHeaders,
}); });