Compare commits

..
Author SHA1 Message Date
Claude f8a5dfdf09 Handle ECONNREFUSED errors gracefully in play/add commands
When the QQ Music (or other) API server is not running, the bot now
returns a user-friendly message instead of crashing with a raw
ECONNREFUSED error. Applied to cmdPlay, cmdAdd, and the generic
command error handler.

https://claude.ai/code/session_01Niz9kmZ4zqfe3PWc6tjvMw
2026-04-01 03:00:35 +00:00
12 changed files with 73 additions and 278 deletions

No files matched your search

-64
View File
@@ -124,70 +124,6 @@ sudo ./scripts/install.sh
自动安装 Node.js 和依赖,配置 systemd 服务,支持开机自启。 自动安装 Node.js 和依赖,配置 systemd 服务,支持开机自启。
## 更新升级
### Windows 用户
```
1. 双击 scripts\stop.bat 停止运行中的机器人(或手动关闭窗口)
2. 在项目目录打开命令行,执行 git pull
3. 双击 scripts\setup.bat 重新安装依赖并构建
4. 双击 scripts\start.bat 启动
```
### 手动安装用户(所有系统)
```bash
# 停止当前运行的机器人(Ctrl+C 或 kill 进程)
# 拉取最新代码
git pull
# 重新安装依赖(如有新增依赖)
npm install
cd web && npm install && cd ..
# 重新构建
npm run build
# 启动
npm start
```
### Docker 用户
```bash
cd scripts/docker
# 拉取最新代码
git pull
# 重新构建并启动(数据自动保留)
docker-compose up -d --build
```
> 数据(数据库、Cookie、日志)保存在 Docker 命名卷 `tsmusicbot-data` 中,更新不会丢失。
### Linux systemd 用户
```bash
# 停止服务
sudo systemctl stop tsmusicbot
# 拉取最新代码
git pull
# 重新安装依赖并构建
npm install
cd web && npm install && cd ..
npm run build
# 重新启动服务
sudo systemctl start tsmusicbot
```
> **提示:** 更新不会影响你的 `config.json` 配置文件、数据库和登录 Cookie,所有数据会自动保留。
## 使用说明 ## 使用说明
### 首次配置 ### 首次配置
-1
View File
@@ -8,7 +8,6 @@
"build": "tsc && npm run build:web", "build": "tsc && npm run build:web",
"build:web": "cd web && npm run build", "build:web": "cd web && npm run build",
"start": "node dist/index.js", "start": "node dist/index.js",
"play": "node dist/index.js",
"test": "vitest run", "test": "vitest run",
"test:watch": "vitest" "test:watch": "vitest"
}, },
+5 -50
View File
@@ -37,23 +37,15 @@ function ffmpegWorks(bin: string): boolean {
/** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */ /** Resolved once at module load — prefer bundled ffmpeg-static, fall back to system. */
const resolvedFfmpeg: string = (() => { const resolvedFfmpeg: string = (() => {
// On Windows, ffmpeg-static may return a path with backslashes; on Linux/macOS if (ffmpegPath && isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) {
// it may return a Windows .exe path if node_modules was copied cross-platform. return ffmpegPath;
const isWinPath = ffmpegPath ? /\\/.test(ffmpegPath) || ffmpegPath.endsWith(".exe") : false;
const onWindows = process.platform === "win32";
// Only try bundled binary if platform matches
if (ffmpegPath && (onWindows === isWinPath)) {
if (isExecutable(ffmpegPath) && ffmpegWorks(ffmpegPath)) {
return ffmpegPath;
}
} }
// Fall back to system ffmpeg // Fall back to system ffmpeg
if (ffmpegWorks("ffmpeg")) { if (ffmpegWorks("ffmpeg")) {
return "ffmpeg"; return "ffmpeg";
} }
// Last resort: always use "ffmpeg" so spawn error is clear, never use a cross-platform path // Last resort: return whatever we have, will fail at runtime with clear error
return "ffmpeg"; return ffmpegPath ?? "ffmpeg";
})(); })();
/** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */ /** Resolve ffmpeg binary: prefer bundled ffmpeg-static, fall back to system PATH. */
@@ -87,9 +79,6 @@ export class AudioPlayer extends EventEmitter {
private static readonly BUFFER_HIGH_WATER = 960 * 1024; // ~5s of PCM at 48kHz stereo private static readonly BUFFER_HIGH_WATER = 960 * 1024; // ~5s of PCM at 48kHz stereo
private static readonly BUFFER_LOW_WATER = 480 * 1024; // ~2.5s private static readonly BUFFER_LOW_WATER = 480 * 1024; // ~2.5s
private ffmpegPaused = false; private ffmpegPaused = false;
private spawnFailed = false; // true if ffmpeg spawn errored (prevent trackEnd cascade)
private consecutiveFailures = 0;
private static readonly MAX_CONSECUTIVE_FAILURES = 3;
constructor(logger: Logger) { constructor(logger: Logger) {
super(); super();
@@ -105,18 +94,6 @@ export class AudioPlayer extends EventEmitter {
this.seekOffset = seekSeconds; this.seekOffset = seekSeconds;
this.framesPlayed = 0; this.framesPlayed = 0;
this.ffmpegPaused = false; this.ffmpegPaused = false;
this.spawnFailed = false;
// Prevent rapid-fire spawn attempts when ffmpeg is broken
if (this.consecutiveFailures >= AudioPlayer.MAX_CONSECUTIVE_FAILURES) {
this.logger.error(
{ failures: this.consecutiveFailures, ffmpeg: getFfmpegCommand() },
"Too many consecutive ffmpeg failures — ffmpeg binary may be missing or broken. Stopping playback."
);
this.state = "idle";
this.emit("error", new Error("ffmpeg unavailable after repeated failures"));
return;
}
this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback"); this.logger.info({ url: url.slice(0, 80), seek: seekSeconds }, "Starting playback");
@@ -151,14 +128,6 @@ export class AudioPlayer extends EventEmitter {
this.logger.info({ ffmpeg: ffmpegBin }, "Using ffmpeg binary"); this.logger.info({ ffmpeg: ffmpegBin }, "Using ffmpeg binary");
this.ffmpeg = spawn(ffmpegBin, args, { stdio: ["ignore", "pipe", "pipe"] }); this.ffmpeg = spawn(ffmpegBin, args, { stdio: ["ignore", "pipe", "pipe"] });
// Prevent stream errors from crashing the process
this.ffmpeg.stdout!.on("error", (err) => {
this.logger.warn({ err }, "FFmpeg stdout error");
});
this.ffmpeg.stderr!.on("error", (err) => {
this.logger.warn({ err }, "FFmpeg stderr error");
});
let gotFirstData = false; let gotFirstData = false;
this.ffmpeg.stdout!.on("data", (chunk: Buffer) => { this.ffmpeg.stdout!.on("data", (chunk: Buffer) => {
if (!gotFirstData) { if (!gotFirstData) {
@@ -183,8 +152,6 @@ export class AudioPlayer extends EventEmitter {
this.ffmpeg.on("error", (err) => { this.ffmpeg.on("error", (err) => {
this.logger.error({ err }, "FFmpeg error"); this.logger.error({ err }, "FFmpeg error");
if (this.sessionId === playSessionId) { if (this.sessionId === playSessionId) {
this.spawnFailed = true;
this.consecutiveFailures++;
this.emit("error", err); this.emit("error", err);
} }
}); });
@@ -235,13 +202,7 @@ export class AudioPlayer extends EventEmitter {
this.frameLoopRunning = false; this.frameLoopRunning = false;
if (this.state !== "idle") { if (this.state !== "idle") {
this.state = "idle"; this.state = "idle";
// Don't emit trackEnd if ffmpeg spawn failed — prevents infinite retry cascade this.emit("trackEnd");
if (this.spawnFailed) {
this.logger.warn("Suppressing trackEnd due to ffmpeg spawn failure");
} else {
this.consecutiveFailures = 0; // Reset on successful track completion
this.emit("trackEnd");
}
} }
return; return;
} }
@@ -334,18 +295,12 @@ export class AudioPlayer extends EventEmitter {
} }
this.pcmBuffer = Buffer.alloc(0); this.pcmBuffer = Buffer.alloc(0);
this.ffmpegPaused = false; this.ffmpegPaused = false;
this.spawnFailed = false;
this.state = "idle"; this.state = "idle";
this.currentUrl = ""; this.currentUrl = "";
this.seekOffset = 0; this.seekOffset = 0;
this.framesPlayed = 0; this.framesPlayed = 0;
} }
/** Reset the consecutive failure counter (e.g. after user action) */
resetFailures(): void {
this.consecutiveFailures = 0;
}
setVolume(vol: number): void { setVolume(vol: number): void {
this.volume = Math.max(0, Math.min(100, vol)); this.volume = Math.max(0, Math.min(100, vol));
} }
+30 -15
View File
@@ -99,9 +99,7 @@ export class BotInstance extends EventEmitter {
private setupTsEvents(): void { private setupTsEvents(): void {
this.tsClient.on("textMessage", (msg: TS3TextMessage) => { this.tsClient.on("textMessage", (msg: TS3TextMessage) => {
this.handleTextMessage(msg).catch((err) => { this.handleTextMessage(msg);
this.logger.error({ err }, "Unhandled error in text message handler");
});
}); });
this.tsClient.on("disconnected", () => { this.tsClient.on("disconnected", () => {
@@ -146,12 +144,13 @@ export class BotInstance extends EventEmitter {
if (response) { if (response) {
await this.tsClient.sendTextMessage(response); await this.tsClient.sendTextMessage(response);
} }
} catch (err) { } catch (err: any) {
this.logger.error({ err, command: parsed.name }, "Command execution error"); this.logger.error({ err, command: parsed.name }, "Command execution error");
const friendlyMsg = err?.code === "ECONNREFUSED"
? "Error: Music API server is not reachable. Please check if the service is running."
: `Error: ${(err as Error).message}`;
try { try {
await this.tsClient.sendTextMessage( await this.tsClient.sendTextMessage(friendlyMsg);
`Error: ${(err as Error).message}`
);
} catch (sendErr) { } catch (sendErr) {
this.logger.error({ err: sendErr }, "Failed to send error message to chat"); this.logger.error({ err: sendErr }, "Failed to send error message to chat");
} }
@@ -254,7 +253,18 @@ export class BotInstance extends EventEmitter {
private async cmdPlay(cmd: ParsedCommand): Promise<string> { private async cmdPlay(cmd: ParsedCommand): Promise<string> {
if (!cmd.args) return "Usage: !play <song name or URL>"; if (!cmd.args) return "Usage: !play <song name or URL>";
const provider = this.getProvider(cmd.flags); const provider = this.getProvider(cmd.flags);
const result = await provider.search(cmd.args, 1);
let result;
try {
result = await provider.search(cmd.args, 1);
} catch (err: any) {
if (err?.code === "ECONNREFUSED") {
this.logger.error({ err, platform: provider.platform }, "Music API server is not reachable");
return `Music API server for ${provider.platform} is not running. Please check the service.`;
}
throw err;
}
if (result.songs.length === 0) if (result.songs.length === 0)
return `No results found for: ${cmd.args}`; return `No results found for: ${cmd.args}`;
@@ -263,8 +273,6 @@ export class BotInstance extends EventEmitter {
this.queue.add({ ...song, platform: provider.platform }); this.queue.add({ ...song, platform: provider.platform });
this.queue.play(); this.queue.play();
// Reset failure counter on user-initiated play
this.player.resetFailures();
const ok = await this.resolveAndPlay(this.queue.current()!); const ok = await this.resolveAndPlay(this.queue.current()!);
if (!ok) return `Cannot play: ${song.name}`; if (!ok) return `Cannot play: ${song.name}`;
return `Now playing: ${song.name} - ${song.artist}`; return `Now playing: ${song.name} - ${song.artist}`;
@@ -273,7 +281,18 @@ export class BotInstance extends EventEmitter {
private async cmdAdd(cmd: ParsedCommand): Promise<string> { private async cmdAdd(cmd: ParsedCommand): Promise<string> {
if (!cmd.args) return "Usage: !add <song name>"; if (!cmd.args) return "Usage: !add <song name>";
const provider = this.getProvider(cmd.flags); const provider = this.getProvider(cmd.flags);
const result = await provider.search(cmd.args, 1);
let result;
try {
result = await provider.search(cmd.args, 1);
} catch (err: any) {
if (err?.code === "ECONNREFUSED") {
this.logger.error({ err, platform: provider.platform }, "Music API server is not reachable");
return `Music API server for ${provider.platform} is not running. Please check the service.`;
}
throw err;
}
if (result.songs.length === 0) if (result.songs.length === 0)
return `No results found for: ${cmd.args}`; return `No results found for: ${cmd.args}`;
@@ -553,8 +572,4 @@ export class BotInstance extends EventEmitter {
isConnected(): boolean { isConnected(): boolean {
return this.connected; return this.connected;
} }
getIdentityExport(): string | undefined {
return this.tsClient.getIdentityExport();
}
} }
+9 -42
View File
@@ -131,25 +131,12 @@ export class BotManager {
const bot = this.bots.get(id); const bot = this.bots.get(id);
if (!bot) throw new Error(`Bot ${id} not found`); if (!bot) throw new Error(`Bot ${id} not found`);
await bot.connect(); await bot.connect();
// Mark as autoStart so it reconnects on Docker restart, and persist identity
const saved = this.database.getBotInstances().find((i) => i.id === id);
if (saved) {
this.database.saveBotInstance({ ...saved, autoStart: true });
this.persistBotIdentity(saved, bot);
}
} }
stopBot(id: string): void { stopBot(id: string): void {
const bot = this.bots.get(id); const bot = this.bots.get(id);
if (!bot) throw new Error(`Bot ${id} not found`); if (!bot) throw new Error(`Bot ${id} not found`);
bot.disconnect(); bot.disconnect();
// Mark as not autoStart so it stays stopped on Docker restart
const saved = this.database.getBotInstances().find((i) => i.id === id);
if (saved) {
this.database.saveBotInstance({ ...saved, autoStart: false });
}
} }
async loadSavedBots(): Promise<void> { async loadSavedBots(): Promise<void> {
@@ -163,7 +150,6 @@ export class BotManager {
port: saved.serverPort, port: saved.serverPort,
queryPort: 10011, queryPort: 10011,
nickname: saved.nickname, nickname: saved.nickname,
identity: saved.identity || undefined,
defaultChannel: saved.defaultChannel || undefined, defaultChannel: saved.defaultChannel || undefined,
channelPassword: saved.channelPassword || undefined, channelPassword: saved.channelPassword || undefined,
}, },
@@ -177,30 +163,18 @@ export class BotManager {
this.bots.set(saved.id, bot); this.bots.set(saved.id, bot);
// Only auto-connect bots that have autoStart enabled // Auto-connect in background (non-blocking, won't affect other bots)
if (saved.autoStart) { bot.connect().then(() => {
bot.connect().then(() => {
// Persist identity after successful connection for future restarts
this.persistBotIdentity(saved, bot);
this.logger.info(
{ botId: saved.id, name: saved.name },
"Auto-connected saved bot"
);
}).catch((err) => {
this.logger.error(
{ err, botId: saved.id, name: saved.name },
"Failed to auto-connect bot (start manually from Settings)"
);
});
// Stagger connections to avoid overwhelming the TS server
await new Promise((resolve) => setTimeout(resolve, 1000));
} else {
this.logger.info( this.logger.info(
{ botId: saved.id, name: saved.name }, { botId: saved.id, name: saved.name },
"Loaded bot (autoStart disabled, not connecting)" "Auto-connected saved bot"
); );
} }).catch((err) => {
this.logger.error(
{ err, botId: saved.id, name: saved.name },
"Failed to auto-connect bot (start manually from Settings)"
);
});
} }
this.logger.info( this.logger.info(
@@ -209,13 +183,6 @@ export class BotManager {
); );
} }
private persistBotIdentity(saved: import("../data/database.js").BotInstance, bot: BotInstance): void {
const identity = bot.getIdentityExport();
if (identity && identity !== saved.identity) {
this.database.saveBotInstance({ ...saved, identity });
}
}
shutdown(): void { shutdown(): void {
for (const bot of this.bots.values()) { for (const bot of this.bots.values()) {
bot.disconnect(); bot.disconnect();
+6 -24
View File
@@ -24,7 +24,6 @@ export interface BotInstance {
defaultChannel: string; defaultChannel: string;
channelPassword: string; channelPassword: string;
autoStart: boolean; autoStart: boolean;
identity?: string;
} }
export interface BotDatabase { export interface BotDatabase {
@@ -37,15 +36,6 @@ export interface BotDatabase {
close(): void; close(): void;
} }
function migrateSchema(db: Database.Database): void {
// Add identity column if it doesn't exist (migration for existing databases)
const columns = db.prepare("PRAGMA table_info(bot_instances)").all() as Array<{ name: string }>;
const hasIdentity = columns.some((c) => c.name === "identity");
if (!hasIdentity) {
db.exec("ALTER TABLE bot_instances ADD COLUMN identity TEXT");
}
}
function initTables(db: Database.Database): void { function initTables(db: Database.Database): void {
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS play_history ( CREATE TABLE IF NOT EXISTS play_history (
@@ -68,8 +58,7 @@ function initTables(db: Database.Database): void {
nickname TEXT NOT NULL, nickname TEXT NOT NULL,
defaultChannel TEXT NOT NULL, defaultChannel TEXT NOT NULL,
channelPassword TEXT NOT NULL, channelPassword TEXT NOT NULL,
autoStart INTEGER NOT NULL DEFAULT 0, autoStart INTEGER NOT NULL DEFAULT 0
identity TEXT
); );
`); `);
} }
@@ -78,7 +67,6 @@ export function createDatabase(dbPath: string): BotDatabase {
const db = new Database(dbPath); const db = new Database(dbPath);
db.pragma("journal_mode = WAL"); db.pragma("journal_mode = WAL");
initTables(db); initTables(db);
migrateSchema(db);
const insertHistory = db.prepare(` const insertHistory = db.prepare(`
INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl) INSERT INTO play_history (botId, songId, songName, artist, album, platform, coverUrl)
@@ -90,8 +78,8 @@ export function createDatabase(dbPath: string): BotDatabase {
`); `);
const upsertInstance = db.prepare(` const upsertInstance = db.prepare(`
INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart, identity) INSERT INTO bot_instances (id, name, serverAddress, serverPort, nickname, defaultChannel, channelPassword, autoStart)
VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart, @identity) VALUES (@id, @name, @serverAddress, @serverPort, @nickname, @defaultChannel, @channelPassword, @autoStart)
ON CONFLICT(id) DO UPDATE SET ON CONFLICT(id) DO UPDATE SET
name = excluded.name, name = excluded.name,
serverAddress = excluded.serverAddress, serverAddress = excluded.serverAddress,
@@ -99,8 +87,7 @@ export function createDatabase(dbPath: string): BotDatabase {
nickname = excluded.nickname, nickname = excluded.nickname,
defaultChannel = excluded.defaultChannel, defaultChannel = excluded.defaultChannel,
channelPassword = excluded.channelPassword, channelPassword = excluded.channelPassword,
autoStart = excluded.autoStart, autoStart = excluded.autoStart
identity = excluded.identity
`); `);
const selectInstances = db.prepare(`SELECT * FROM bot_instances`); const selectInstances = db.prepare(`SELECT * FROM bot_instances`);
@@ -122,19 +109,14 @@ export function createDatabase(dbPath: string): BotDatabase {
upsertInstance.run({ upsertInstance.run({
...instance, ...instance,
autoStart: instance.autoStart ? 1 : 0, autoStart: instance.autoStart ? 1 : 0,
identity: instance.identity ?? null,
}); });
}, },
getBotInstances() { getBotInstances() {
const rows = selectInstances.all() as Array< const rows = selectInstances.all() as Array<
Omit<BotInstance, "autoStart" | "identity"> & { autoStart: number; identity: string | null } Omit<BotInstance, "autoStart"> & { autoStart: number }
>; >;
return rows.map((r) => ({ return rows.map((r) => ({ ...r, autoStart: r.autoStart === 1 }));
...r,
autoStart: r.autoStart === 1,
identity: r.identity ?? undefined,
}));
}, },
deleteBotInstance(id) { deleteBotInstance(id) {
-8
View File
@@ -25,14 +25,6 @@ async function main() {
saveConfig(CONFIG_PATH, config); saveConfig(CONFIG_PATH, config);
const logger = createLogger(LOG_DIR); const logger = createLogger(LOG_DIR);
// Prevent unhandled errors from crashing the process
process.on("uncaughtException", (err) => {
logger.error({ err }, "Uncaught exception");
});
process.on("unhandledRejection", (reason) => {
logger.error({ reason }, "Unhandled promise rejection");
});
const db = createDatabase(DB_PATH); const db = createDatabase(DB_PATH);
const apiServer = createApiServerManager( const apiServer = createApiServerManager(
+22 -46
View File
@@ -153,41 +153,29 @@ export class QQMusicProvider implements MusicProvider {
async getQrCode(): Promise<QrCodeResult> { async getQrCode(): Promise<QrCodeResult> {
const res = await this.api.get("/getQQLoginQr"); const res = await this.api.get("/getQQLoginQr");
const qrsig = res.data?.qrsig ?? "";
const ptqrtoken = res.data?.ptqrtoken ?? "";
// @sansenjian/qq-music-api's checkQQLoginQr requires BOTH qrsig and
// ptqrtoken, so encode both into the opaque key the caller polls with.
return { return {
qrUrl: "", qrUrl: "",
qrImg: res.data?.img ?? "", qrImg: res.data?.img ?? "",
key: qrsig && ptqrtoken ? `${qrsig}|${ptqrtoken}` : "", key: res.data?.qrsig ?? res.data?.ptqrtoken ?? "",
}; };
} }
async checkQrCodeStatus( async checkQrCodeStatus(
key: string key: string
): Promise<"waiting" | "scanned" | "confirmed" | "expired"> { ): Promise<"waiting" | "scanned" | "confirmed" | "expired"> {
const [qrsig, ptqrtoken] = key.split("|"); const res = await this.api.get("/checkQQLoginQr", {
if (!qrsig || !ptqrtoken) return "expired"; params: { qrsig: key },
try { });
// /checkQQLoginQr is POST, and the success body is const code = res.data?.code ?? res.data?.response?.code;
// { isOk, refresh, message, session: { cookie, ... } } if (code === 0) {
const res = await this.api.post("/checkQQLoginQr", null, { if (res.data?.cookie) {
params: { qrsig, ptqrtoken }, this.cookie = res.data.cookie;
});
if (res.data?.isOk) {
const cookie = res.data?.session?.cookie;
if (cookie) {
this.cookie = cookie;
}
return "confirmed";
} }
if (res.data?.refresh) return "expired"; return "confirmed";
// The upstream lib does not distinguish "scanned" from "waiting"
return "waiting";
} catch {
return "expired";
} }
if (code === 1) return "scanned";
if (code === 2) return "waiting";
return "expired";
} }
setCookie(cookie: string): void { setCookie(cookie: string): void {
@@ -200,32 +188,20 @@ export class QQMusicProvider implements MusicProvider {
async getAuthStatus(): Promise<AuthStatus> { async getAuthStatus(): Promise<AuthStatus> {
if (!this.cookie) return { loggedIn: false }; if (!this.cookie) return { loggedIn: false };
// @sansenjian/qq-music-api does not expose a dedicated "am I logged in"
// endpoint. /user/getUserAvatar only builds a static avatar URL from a
// uin and never talks to QQ, so it cannot be used to validate a cookie.
// Instead, parse the uin from the cookie and round-trip it through
// /user/getUserPlaylists, which actually hits QQ Music's servers using
// the provided cookie. A successful response (code=0) means the cookie
// is still valid.
const uinMatch = this.cookie.match(/(?:^|;\s*)uin=([^;]+)/);
const uin = uinMatch?.[1];
if (!uin) return { loggedIn: false };
try { try {
const res = await this.api.get("/user/getUserPlaylists", { const res = await this.api.get("/getUserAvatar", {
params: { uin, limit: 1, ...this.cookieParams }, params: { ...this.cookieParams },
}); });
if (res.data?.response?.code !== 0) { if (res.data?.response?.data) {
return { loggedIn: false }; return {
loggedIn: true,
nickname: res.data.response.data.nickname,
avatarUrl: res.data.response.data.headpic,
};
} }
return {
loggedIn: true,
nickname: `QQ ${uin}`,
avatarUrl: `https://q.qlogo.cn/headimg_dl?dst_uin=${uin}&spec=140`,
};
} catch { } catch {
return { loggedIn: false }; // ignore
} }
return { loggedIn: false };
} }
} }
-13
View File
@@ -53,18 +53,6 @@ export class TS3Client extends EventEmitter {
} }
async connect(): Promise<void> { async connect(): Promise<void> {
// Clean up any existing connection before creating a new one
if (this.client) {
this.logger.info("Cleaning up previous connection before reconnecting");
try {
await this.client.disconnect();
} catch {
// Ignore errors during cleanup
}
this.client = null;
this.clientId = 0;
}
const addr = `${this.options.host}:${this.options.port}`; const addr = `${this.options.host}:${this.options.port}`;
this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)"); this.logger.info({ addr }, "Connecting to TeamSpeak server (full client protocol)");
@@ -126,7 +114,6 @@ export class TS3Client extends EventEmitter {
await this.client.connect(); await this.client.connect();
await this.client.waitConnected(); await this.client.waitConnected();
this.clientId = this.client.clientID(); this.clientId = this.client.clientID();
this.voiceFramesSent = 0;
this.logger.info({ clientId: this.clientId }, "Logged in (visible client)"); this.logger.info({ clientId: this.clientId }, "Logged in (visible client)");
// Join default channel if specified // Join default channel if specified
-3
View File
@@ -154,7 +154,6 @@ export function createPlayerRouter(
} }
const queue = bot.getQueueManager(); const queue = bot.getQueueManager();
bot.getPlayer().stop(); // Stop current playback first bot.getPlayer().stop(); // Stop current playback first
bot.getPlayer().resetFailures(); // Reset on user-initiated play
const song = queue.playAt(index); const song = queue.playAt(index);
if (!song) { if (!song) {
res.status(400).json({ error: "Invalid queue index" }); res.status(400).json({ error: "Invalid queue index" });
@@ -201,7 +200,6 @@ export function createPlayerRouter(
// Stop current playback // Stop current playback
bot.getPlayer().stop(); bot.getPlayer().stop();
bot.getPlayer().resetFailures();
const songs = await provider.getPlaylistSongs(playlistId); const songs = await provider.getPlaylistSongs(playlistId);
if (songs.length === 0) { if (songs.length === 0) {
@@ -258,7 +256,6 @@ export function createPlayerRouter(
queue.add({ ...song, platform: provider.platform }); queue.add({ ...song, platform: provider.platform });
queue.play(); queue.play();
bot.getPlayer().resetFailures();
const ok = await bot.resolveAndPlay(queue.current()!); const ok = await bot.resolveAndPlay(queue.current()!);
if (!ok) { if (!ok) {
res.json({ message: `Cannot play: ${song.name}` }); res.json({ message: `Cannot play: ${song.name}` });
-7
View File
@@ -67,14 +67,7 @@ export function createWebServer(options: WebServerOptions): WebServer {
}); });
} }
server.on("error", (err) => {
logger.error({ err }, "HTTP server error");
});
const wss = new WebSocketServer({ server, path: "/ws" }); const wss = new WebSocketServer({ server, path: "/ws" });
wss.on("error", (err) => {
logger.error({ err }, "WebSocket server error");
});
const cleanupWs = setupWebSocket(wss, options.botManager, logger); const cleanupWs = setupWebSocket(wss, options.botManager, logger);
return { return {
+1 -5
View File
@@ -39,11 +39,7 @@ export function setupWebSocket(
const message = JSON.stringify(data); const message = JSON.stringify(data);
for (const client of clients) { for (const client of clients) {
if (client.readyState === WebSocket.OPEN) { if (client.readyState === WebSocket.OPEN) {
try { client.send(message);
client.send(message);
} catch {
clients.delete(client);
}
} }
} }
}; };