feat: enhance configuration management and add error handling

This commit is contained in:
MythEclipse
2026-05-13 16:57:07 +07:00
parent 673a06376c
commit 978c2c468d
14 changed files with 243 additions and 115 deletions

View File

@@ -26,3 +26,8 @@ WEBSERVER_PORT=3000
# Connection Configuration # Connection Configuration
VOICE_CONNECTION_TIMEOUT_MS=15000 VOICE_CONNECTION_TIMEOUT_MS=15000
RECONNECT_TIMEOUT_MS=5000 RECONNECT_TIMEOUT_MS=5000
# Logging Configuration
LOG_LEVEL=info
NODE_ENV=development

BIN
bun.lockb

Binary file not shown.

View File

@@ -16,15 +16,21 @@
"@discordjs/opus": "^0.10.0", "@discordjs/opus": "^0.10.0",
"@discordjs/voice": "^0.19.1", "@discordjs/voice": "^0.19.1",
"@snazzah/davey": "^0.1.10", "@snazzah/davey": "^0.1.10",
"class-transformer": "^0.5.1",
"class-validator": "^0.15.1",
"discord.js-selfbot-v13": "^3.7.1", "discord.js-selfbot-v13": "^3.7.1",
"dotenv": "^17.4.2",
"express": "^5.2.1", "express": "^5.2.1",
"fluent-ffmpeg": "^2.1.3", "fluent-ffmpeg": "^2.1.3",
"helmet": "^8.1.0",
"libsodium-wrappers": "^0.8.2", "libsodium-wrappers": "^0.8.2",
"p-retry": "^6.2.0",
"pino": "^9.4.0",
"pino-http": "^11.0.0",
"prism-media": "2.0.0-alpha.0", "prism-media": "2.0.0-alpha.0",
"sodium-native": "^4.3.2", "sodium-native": "^4.3.2",
"ws": "^8.20.1", "ws": "^8.20.1",
"pino": "^9.4.0", "zod": "^4.4.3"
"p-retry": "^6.2.0"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "latest", "@biomejs/biome": "latest",

View File

@@ -1,61 +1,46 @@
// Configuration for the bot import { z } from "zod";
export interface AppConfig { import { ConfigError } from "./errors";
verbose: boolean;
recordingsDir: string;
recordingSegmentMs: number;
decoderRotateMs: number;
decoderCooldownMs: number;
webserverPort: number;
voiceConnectionTimeoutMs: number;
reconnectTimeoutMs: number;
audioStreamSilenceDurationMs: number;
packetFilterMinSize: number;
opusFrameSize: number;
audioSampleRate: number;
audioChannels: number;
avatarSize: number;
}
export function parseBoolean( const configSchema = z.object({
value: string | undefined, DISCORD_TOKEN: z.string().min(1, "DISCORD_TOKEN is required"),
fallback: boolean, VOICE_CHANNEL_ID: z.string().min(1, "VOICE_CHANNEL_ID is required"),
): boolean { GUILD_ID: z.string().min(1, "GUILD_ID is required"),
if (value === "true") return true; VERBOSE: z
if (value === "false") return false; .string()
return fallback; .optional()
} .transform((v) => v === "true")
.default(false),
RECORDINGS_DIR: z.string().default("./recordings"),
RECORDING_SEGMENT_MS: z.coerce.number().positive().default(5000),
DECODER_ROTATE_MS: z.coerce.number().positive().default(5000),
DECODER_COOLDOWN_MS: z.coerce.number().positive().default(30000),
WEBSERVER_PORT: z.coerce.number().positive().default(3000),
VOICE_CONNECTION_TIMEOUT_MS: z.coerce.number().positive().default(15000),
RECONNECT_TIMEOUT_MS: z.coerce.number().positive().default(5000),
AUDIO_STREAM_SILENCE_DURATION_MS: z.coerce.number().positive().default(3000),
PACKET_FILTER_MIN_SIZE: z.coerce.number().positive().default(8),
OPUS_FRAME_SIZE: z.coerce.number().positive().default(960),
AUDIO_SAMPLE_RATE: z.coerce.number().positive().default(48000),
AUDIO_CHANNELS: z.coerce.number().positive().default(2),
AVATAR_SIZE: z.coerce.number().positive().default(64),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
NODE_ENV: z.enum(["development", "production"]).default("development"),
});
export function parsePositiveNumber( export type AppConfig = z.infer<typeof configSchema>;
value: string | undefined,
fallback: number,
): number {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig { export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
return { try {
verbose: parseBoolean(env.VERBOSE, false), return configSchema.parse(env);
recordingsDir: env.RECORDINGS_DIR ?? "./recordings", } catch (error) {
recordingSegmentMs: parsePositiveNumber(env.RECORDING_SEGMENT_MS, 5_000), if (error instanceof z.ZodError) {
decoderRotateMs: parsePositiveNumber(env.DECODER_ROTATE_MS, 5_000), const messages = error.issues
decoderCooldownMs: parsePositiveNumber(env.DECODER_COOLDOWN_MS, 30_000), .map((e) => `${e.path.join(".")}: ${e.message}`)
webserverPort: parsePositiveNumber(env.WEBSERVER_PORT, 3000), .join("\n");
voiceConnectionTimeoutMs: parsePositiveNumber( throw new ConfigError(`Configuration validation failed:\n${messages}`);
env.VOICE_CONNECTION_TIMEOUT_MS, }
15_000, throw error;
), }
reconnectTimeoutMs: parsePositiveNumber(env.RECONNECT_TIMEOUT_MS, 5_000),
audioStreamSilenceDurationMs: parsePositiveNumber(
env.AUDIO_STREAM_SILENCE_DURATION_MS,
3000,
),
packetFilterMinSize: parsePositiveNumber(env.PACKET_FILTER_MIN_SIZE, 8),
opusFrameSize: parsePositiveNumber(env.OPUS_FRAME_SIZE, 960),
audioSampleRate: parsePositiveNumber(env.AUDIO_SAMPLE_RATE, 48000),
audioChannels: parsePositiveNumber(env.AUDIO_CHANNELS, 2),
avatarSize: parsePositiveNumber(env.AVATAR_SIZE, 64),
};
} }
export const config = loadConfig(); export const config = loadConfig();

42
src/errors.ts Normal file
View File

@@ -0,0 +1,42 @@
export class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500,
) {
super(message);
this.name = "AppError";
Error.captureStackTrace(this, this.constructor);
}
}
export class ConfigError extends AppError {
constructor(message: string) {
super(message, "CONFIG_ERROR", 500);
this.name = "ConfigError";
}
}
export class AudioError extends AppError {
constructor(message: string) {
super(message, "AUDIO_ERROR", 500);
this.name = "AudioError";
}
}
export class VoiceConnectionError extends AppError {
constructor(message: string) {
super(message, "VOICE_CONNECTION_ERROR", 500);
this.name = "VoiceConnectionError";
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public details?: Record<string, string[]>,
) {
super(message, "VALIDATION_ERROR", 400);
this.name = "ValidationError";
}
}

View File

@@ -1,24 +1,21 @@
import "./mock-crc"; import "./mock-crc";
import "libsodium-wrappers"; import "libsodium-wrappers";
import "@snazzah/davey"; import "@snazzah/davey";
import "dotenv/config";
import { getVoiceConnection } from "@discordjs/voice"; import { getVoiceConnection } from "@discordjs/voice";
import { Client } from "discord.js-selfbot-v13"; import { Client } from "discord.js-selfbot-v13";
import { config } from "./config"; import { config } from "./config";
import { AppError } from "./errors";
import { createChildLogger } from "./logger";
import { discordPlayer } from "./player"; import { discordPlayer } from "./player";
import { startRecording, stopRecording } from "./recorder"; import { startRecording, stopRecording } from "./recorder";
import { startWebserver } from "./webserver"; import { startWebserver } from "./webserver";
import { createChildLogger } from "./logger";
const logger = createChildLogger("bot"); const logger = createChildLogger("bot");
// Validasi environment variables const token = config.DISCORD_TOKEN;
const token = process.env.DISCORD_TOKEN; const voiceChannelId = config.VOICE_CHANNEL_ID;
const voiceChannelId = process.env.VOICE_CHANNEL_ID; const guildId = config.GUILD_ID;
const guildId = process.env.GUILD_ID;
if (!token) throw new Error("Missing DISCORD_TOKEN in .env");
if (!voiceChannelId) throw new Error("Missing VOICE_CHANNEL_ID in .env");
if (!guildId) throw new Error("Missing GUILD_ID in .env");
// Inisialisasi selfbot client // Inisialisasi selfbot client
const client = new Client(); const client = new Client();
@@ -76,7 +73,7 @@ async function gracefulShutdown(signal: string) {
} }
client.on("ready", async () => { client.on("ready", async () => {
if (config.verbose) { if (config.VERBOSE) {
logger.info({ user: client.user?.tag }, "Bot logged in"); logger.info({ user: client.user?.tag }, "Bot logged in");
} }
@@ -97,7 +94,7 @@ client.on("ready", async () => {
process.exit(1); process.exit(1);
} }
if (config.verbose) { if (config.VERBOSE) {
logger.info( logger.info(
{ channelName: channel.name, channelId: channel.id }, { channelName: channel.name, channelId: channel.id },
"Joining voice channel", "Joining voice channel",
@@ -114,7 +111,7 @@ client.on("ready", async () => {
} }
// Start Webserver // Start Webserver
startWebserver(config.webserverPort); startWebserver(config.WEBSERVER_PORT);
}); });
client.on("error", (err) => { client.on("error", (err) => {

36
src/middleware.ts Normal file
View File

@@ -0,0 +1,36 @@
import { NextFunction, Request, Response } from "express";
import { AppError } from "./errors";
import { createChildLogger } from "./logger";
const logger = createChildLogger("middleware");
export function errorHandler(
err: Error,
_req: Request,
res: Response,
_next: NextFunction,
) {
if (err instanceof AppError) {
logger.error(
{ code: err.code, statusCode: err.statusCode, message: err.message },
"Application error",
);
return res.status(err.statusCode).json({
error: err.code,
message: err.message,
});
}
logger.error({ error: err.message, stack: err.stack }, "Unexpected error");
res.status(500).json({
error: "INTERNAL_SERVER_ERROR",
message: "An unexpected error occurred",
});
}
export function notFoundHandler(_req: Request, res: Response) {
res.status(404).json({
error: "NOT_FOUND",
message: "Endpoint not found",
});
}

View File

@@ -9,6 +9,7 @@ import {
} from "@discordjs/voice"; } from "@discordjs/voice";
import type { Client, VoiceChannel } from "discord.js-selfbot-v13"; import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
import { config } from "./config"; import { config } from "./config";
import { createChildLogger } from "./logger";
import { PacketFilter } from "./packetFilter"; import { PacketFilter } from "./packetFilter";
import { subscribeToAudioStream } from "./recorder/audioStream"; import { subscribeToAudioStream } from "./recorder/audioStream";
import { OpusDecoder } from "./recorder/decoder"; import { OpusDecoder } from "./recorder/decoder";
@@ -17,13 +18,12 @@ import {
createSegmentMetadata, createSegmentMetadata,
} from "./recorder/metadata"; } from "./recorder/metadata";
import { SegmentManager } from "./recorder/segment"; import { SegmentManager } from "./recorder/segment";
import type { PcmBroadcaster } from "./types";
import { createChildLogger } from "./logger";
import { retryWithBackoff } from "./retry"; import { retryWithBackoff } from "./retry";
import type { PcmBroadcaster } from "./types";
const logger = createChildLogger("recorder"); const logger = createChildLogger("recorder");
const recordingsDir = config.recordingsDir; const recordingsDir = config.RECORDINGS_DIR;
// Pastikan folder recordings ada // Pastikan folder recordings ada
if (!fs.existsSync(recordingsDir)) { if (!fs.existsSync(recordingsDir)) {
@@ -49,7 +49,7 @@ export async function startRecording(
logger.info({ channelName: channel.name }, "Joining voice channel"); logger.info({ channelName: channel.name }, "Joining voice channel");
connection.on("debug", (msg) => { connection.on("debug", (msg) => {
if (config.verbose) { if (config.VERBOSE) {
logger.debug({ message: msg }, "Voice debug"); logger.debug({ message: msg }, "Voice debug");
} }
}); });
@@ -65,7 +65,7 @@ export async function startRecording(
entersState( entersState(
connection, connection,
VoiceConnectionStatus.Ready, VoiceConnectionStatus.Ready,
config.voiceConnectionTimeoutMs, config.VOICE_CONNECTION_TIMEOUT_MS,
), ),
{ {
retries: 3, retries: 3,
@@ -87,7 +87,10 @@ export async function startRecording(
// Dengarkan siapapun yang mulai bicara // Dengarkan siapapun yang mulai bicara
receiver.speaking.on("start", async (userId) => { receiver.speaking.on("start", async (userId) => {
const userMetadata = await collectUserMetadata(client, userId, channel); const userMetadata = await collectUserMetadata(client, userId, channel);
logger.info({ userId, username: userMetadata.username }, "Voice activity detected"); logger.info(
{ userId, username: userMetadata.username },
"Voice activity detected",
);
// Notify webserver // Notify webserver
broadcaster.updateActiveUser?.(userId, { broadcaster.updateActiveUser?.(userId, {
@@ -109,7 +112,9 @@ export async function startRecording(
try { try {
// --- OGG file recording with segment rotation --- // --- OGG file recording with segment rotation ---
const packetFilterForOgg = new PacketFilter(config.packetFilterMinSize); const packetFilterForOgg = new PacketFilter(
config.PACKET_FILTER_MIN_SIZE,
);
const audioStream = receiver.subscribe(userId, { const audioStream = receiver.subscribe(userId, {
end: { end: {
behavior: EndBehaviorType.AfterSilence, behavior: EndBehaviorType.AfterSilence,
@@ -119,13 +124,13 @@ export async function startRecording(
const oggPacketStream = audioStream.pipe(packetFilterForOgg); const oggPacketStream = audioStream.pipe(packetFilterForOgg);
const segmentManager = new SegmentManager( const segmentManager = new SegmentManager(
userDir, userDir,
config.recordingSegmentMs, config.RECORDING_SEGMENT_MS,
); );
// --- Web broadcast: prism decoder with safe restart and cooldown --- // --- Web broadcast: prism decoder with safe restart and cooldown ---
const decoder = new OpusDecoder({ const decoder = new OpusDecoder({
cooldownMs: config.decoderCooldownMs, cooldownMs: config.DECODER_COOLDOWN_MS,
rotateMs: config.decoderRotateMs, rotateMs: config.DECODER_ROTATE_MS,
onData: (pcm) => { onData: (pcm) => {
if (!broadcaster.broadcastPcmToWeb) return; if (!broadcaster.broadcastPcmToWeb) return;
// Downsample 48kHz stereo → 24kHz mono (left channel, every 2nd sample) // Downsample 48kHz stereo → 24kHz mono (left channel, every 2nd sample)
@@ -139,7 +144,7 @@ export async function startRecording(
let currentSegment = segmentManager.open(oggPacketStream); let currentSegment = segmentManager.open(oggPacketStream);
currentSegment.out.on("finish", () => { currentSegment.out.on("finish", () => {
if (config.verbose) { if (config.VERBOSE) {
logger.info({ filename: currentSegment.filename }, "Segment saved"); logger.info({ filename: currentSegment.filename }, "Segment saved");
} }
const metadata = createSegmentMetadata( const metadata = createSegmentMetadata(
@@ -147,13 +152,13 @@ export async function startRecording(
currentSegment, currentSegment,
sessionId, sessionId,
sessionStartTime, sessionStartTime,
config.recordingSegmentMs, config.RECORDING_SEGMENT_MS,
); );
fs.writeFileSync( fs.writeFileSync(
currentSegment.jsonFilename, currentSegment.jsonFilename,
JSON.stringify(metadata, null, 2), JSON.stringify(metadata, null, 2),
); );
if (config.verbose) { if (config.VERBOSE) {
logger.info( logger.info(
{ jsonFile: currentSegment.jsonFilename }, { jsonFile: currentSegment.jsonFilename },
"Metadata saved", "Metadata saved",
@@ -162,10 +167,7 @@ export async function startRecording(
}); });
currentSegment.out.on("error", (err) => { currentSegment.out.on("error", (err) => {
logger.error( logger.error({ userId, error: err.message }, "File write error");
{ userId, error: err.message },
"File write error",
);
}); });
// Feed Opus packets one-by-one // Feed Opus packets one-by-one
@@ -189,19 +191,13 @@ export async function startRecording(
onError: (error) => { onError: (error) => {
segmentManager.close(oggPacketStream); segmentManager.close(oggPacketStream);
decoder.destroy(); decoder.destroy();
logger.error( logger.error({ userId, error: error.message }, "Audio stream error");
{ userId, error: error.message },
"Audio stream error",
);
}, },
}); });
packetFilterForOgg.on("error", (err) => { packetFilterForOgg.on("error", (err) => {
segmentManager.close(oggPacketStream); segmentManager.close(oggPacketStream);
logger.error( logger.error({ userId, error: err.message }, "PacketFilter error");
{ userId, error: err.message },
"PacketFilter error",
);
}); });
} catch (e) { } catch (e) {
logger.error( logger.error(
@@ -213,7 +209,7 @@ export async function startRecording(
// Handle disconnect yang tidak disengaja // Handle disconnect yang tidak disengaja
connection.on(VoiceConnectionStatus.Disconnected, async () => { connection.on(VoiceConnectionStatus.Disconnected, async () => {
if (config.verbose) { if (config.VERBOSE) {
logger.warn("Disconnected from voice channel. Reconnecting..."); logger.warn("Disconnected from voice channel. Reconnecting...");
} }
try { try {
@@ -221,12 +217,12 @@ export async function startRecording(
entersState( entersState(
connection, connection,
VoiceConnectionStatus.Signalling, VoiceConnectionStatus.Signalling,
config.reconnectTimeoutMs, config.RECONNECT_TIMEOUT_MS,
), ),
entersState( entersState(
connection, connection,
VoiceConnectionStatus.Connecting, VoiceConnectionStatus.Connecting,
config.reconnectTimeoutMs, config.RECONNECT_TIMEOUT_MS,
), ),
]); ]);
// Berhasil reconnect // Berhasil reconnect
@@ -237,7 +233,7 @@ export async function startRecording(
}); });
connection.on(VoiceConnectionStatus.Destroyed, () => { connection.on(VoiceConnectionStatus.Destroyed, () => {
if (config.verbose) { if (config.VERBOSE) {
logger.info("Voice connection destroyed"); logger.info("Voice connection destroyed");
} }
}); });
@@ -250,7 +246,7 @@ export function stopRecording(guildId: string): void {
const connection = getVoiceConnection(guildId); const connection = getVoiceConnection(guildId);
if (connection) { if (connection) {
connection.destroy(); connection.destroy();
if (config.verbose) { if (config.VERBOSE) {
logger.info("Recording stopped and disconnected"); logger.info("Recording stopped and disconnected");
} }
} else { } else {

View File

@@ -15,7 +15,7 @@ export function subscribeToAudioStream(
const audioStream = receiver.subscribe(userId, { const audioStream = receiver.subscribe(userId, {
end: { end: {
behavior: EndBehaviorType.AfterSilence, behavior: EndBehaviorType.AfterSilence,
duration: config.audioStreamSilenceDurationMs, duration: config.AUDIO_STREAM_SILENCE_DURATION_MS,
}, },
}); });

View File

@@ -25,9 +25,14 @@ export class OpusDecoder {
options.createDecoder ?? options.createDecoder ??
(() => (() =>
new prism.opus.Decoder({ new prism.opus.Decoder({
frameSize: config.opusFrameSize, frameSize: config.OPUS_FRAME_SIZE,
channels: config.audioChannels as 1 | 2, channels: config.AUDIO_CHANNELS as 1 | 2,
rate: config.audioSampleRate as 8000 | 12000 | 16000 | 24000 | 48000, rate: config.AUDIO_SAMPLE_RATE as
| 8000
| 12000
| 16000
| 24000
| 48000,
})); }));
} }

View File

@@ -33,7 +33,7 @@ export async function collectUserMetadata(
avatarUrl: avatarUrl:
user?.displayAvatarURL({ user?.displayAvatarURL({
format: "png", format: "png",
size: config.avatarSize as size: config.AVATAR_SIZE as
| 16 | 16
| 32 | 32
| 64 | 64

38
src/validation.ts Normal file
View File

@@ -0,0 +1,38 @@
import { plainToClass } from "class-transformer";
import { IsBoolean, IsString, validate } from "class-validator";
export class UserStateUpdate {
@IsString()
userId!: string;
@IsString()
username!: string;
@IsString()
avatar!: string;
@IsBoolean()
speaking!: boolean;
}
export class AudioMessage {
data!: Buffer;
userId!: string;
}
export async function validateUserStateUpdate(
data: unknown,
): Promise<UserStateUpdate | null> {
if (typeof data !== "object" || data === null) {
return null;
}
const obj = plainToClass(UserStateUpdate, data);
const errors = await validate(obj);
if (errors.length > 0) {
return null;
}
return obj;
}

View File

@@ -1,12 +1,14 @@
import express from "express"; import express from "express";
import helmet from "helmet";
import http from "http"; import http from "http";
import path from "path"; import path from "path";
import pinoHttp from "pino-http";
import prism from "prism-media"; import prism from "prism-media";
import { WebSocketServer } from "ws"; import { WebSocketServer } from "ws";
import { createChildLogger, logger } from "./logger";
import { discordPlayer } from "./player"; import { discordPlayer } from "./player";
import { createChildLogger } from "./logger";
const logger = createChildLogger("webserver"); const wsLogger = createChildLogger("webserver");
const activeUsers = new Map< const activeUsers = new Map<
string, string,
@@ -45,10 +47,27 @@ export function startWebserver(port: number = 3000) {
const wsPort = port + 1; const wsPort = port + 1;
const wss = new WebSocketServer({ port: wsPort, host: "0.0.0.0" }); const wss = new WebSocketServer({ port: wsPort, host: "0.0.0.0" });
logger.info({ wsPort }, "WebSocket server listening"); wsLogger.info({ wsPort }, "WebSocket server listening");
// Security headers
app.use(helmet());
// HTTP request logging
app.use(pinoHttp({ logger }));
app.use(express.static(path.join(__dirname, "../public"))); app.use(express.static(path.join(__dirname, "../public")));
// Health check endpoint
app.get("/health", (_req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
activeUsers: activeUsers.size,
wsClients: wsClients.size,
});
});
// Inbound: Discord PCM → tagged chunks → browser // Inbound: Discord PCM → tagged chunks → browser
(global as any).broadcastPcmToWeb = (chunk: Buffer, userId: string) => { (global as any).broadcastPcmToWeb = (chunk: Buffer, userId: string) => {
let hash = 0; let hash = 0;
@@ -125,10 +144,7 @@ export function startWebserver(port: number = 3000) {
setInterval(() => { setInterval(() => {
if (dbCount > 0) { if (dbCount > 0) {
const avg = dbAccum / dbCount; const avg = dbAccum / dbCount;
logger.info( wsLogger.info({ level: avg.toFixed(1), frames: dbCount }, "Audio level");
{ level: avg.toFixed(1), frames: dbCount },
"Audio level",
);
dbAccum = 0; dbAccum = 0;
dbCount = 0; dbCount = 0;
} }
@@ -152,7 +168,7 @@ export function startWebserver(port: number = 3000) {
if (playerPaused) { if (playerPaused) {
discordPlayer.unpause(); discordPlayer.unpause();
playerPaused = false; playerPaused = false;
logger.info("Transmitting — Discord indicator ON"); wsLogger.info("Transmitting — Discord indicator ON");
} }
} else if (msSinceAudio < SILENCE_TAIL_MS && msSinceAudio > 0) { } else if (msSinceAudio < SILENCE_TAIL_MS && msSinceAudio > 0) {
// Buffer drained but audio was recent — pad silence to avoid OGG gap // Buffer drained but audio was recent — pad silence to avoid OGG gap
@@ -161,7 +177,7 @@ export function startWebserver(port: number = 3000) {
// No audio for a while — pause Discord indicator // No audio for a while — pause Discord indicator
discordPlayer.pause(); discordPlayer.pause();
playerPaused = true; playerPaused = true;
logger.info("Stopped — Discord indicator OFF"); wsLogger.info("Stopped — Discord indicator OFF");
return; return;
} else { } else {
return; // already paused, nothing to do return; // already paused, nothing to do
@@ -175,7 +191,7 @@ export function startWebserver(port: number = 3000) {
}, 20); }, 20);
wss.on("connection", (ws) => { wss.on("connection", (ws) => {
logger.info({ wsPort }, "New WebSocket connection"); wsLogger.info({ wsPort }, "New WebSocket connection");
wsClients.add(ws); wsClients.add(ws);
ws.send( ws.send(
@@ -210,6 +226,6 @@ export function startWebserver(port: number = 3000) {
}); });
server.listen(port, "0.0.0.0", () => { server.listen(port, "0.0.0.0", () => {
logger.info({ port }, "Web interface listening"); wsLogger.info({ port }, "Web interface listening");
}); });
} }

View File

@@ -7,7 +7,9 @@
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"outDir": "dist", "outDir": "dist",
"rootDir": "src" "rootDir": "src",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"]