feat: enhance configuration management and add error handling
This commit is contained in:
+39
-54
@@ -1,61 +1,46 @@
|
||||
// Configuration for the bot
|
||||
export interface AppConfig {
|
||||
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;
|
||||
}
|
||||
import { z } from "zod";
|
||||
import { ConfigError } from "./errors";
|
||||
|
||||
export function parseBoolean(
|
||||
value: string | undefined,
|
||||
fallback: boolean,
|
||||
): boolean {
|
||||
if (value === "true") return true;
|
||||
if (value === "false") return false;
|
||||
return fallback;
|
||||
}
|
||||
const configSchema = z.object({
|
||||
DISCORD_TOKEN: z.string().min(1, "DISCORD_TOKEN is required"),
|
||||
VOICE_CHANNEL_ID: z.string().min(1, "VOICE_CHANNEL_ID is required"),
|
||||
GUILD_ID: z.string().min(1, "GUILD_ID is required"),
|
||||
VERBOSE: z
|
||||
.string()
|
||||
.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(
|
||||
value: string | undefined,
|
||||
fallback: number,
|
||||
): number {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
||||
}
|
||||
export type AppConfig = z.infer<typeof configSchema>;
|
||||
|
||||
export function loadConfig(env: NodeJS.ProcessEnv = process.env): AppConfig {
|
||||
return {
|
||||
verbose: parseBoolean(env.VERBOSE, false),
|
||||
recordingsDir: env.RECORDINGS_DIR ?? "./recordings",
|
||||
recordingSegmentMs: parsePositiveNumber(env.RECORDING_SEGMENT_MS, 5_000),
|
||||
decoderRotateMs: parsePositiveNumber(env.DECODER_ROTATE_MS, 5_000),
|
||||
decoderCooldownMs: parsePositiveNumber(env.DECODER_COOLDOWN_MS, 30_000),
|
||||
webserverPort: parsePositiveNumber(env.WEBSERVER_PORT, 3000),
|
||||
voiceConnectionTimeoutMs: parsePositiveNumber(
|
||||
env.VOICE_CONNECTION_TIMEOUT_MS,
|
||||
15_000,
|
||||
),
|
||||
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),
|
||||
};
|
||||
try {
|
||||
return configSchema.parse(env);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const messages = error.issues
|
||||
.map((e) => `${e.path.join(".")}: ${e.message}`)
|
||||
.join("\n");
|
||||
throw new ConfigError(`Configuration validation failed:\n${messages}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export const config = loadConfig();
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
+9
-12
@@ -1,24 +1,21 @@
|
||||
import "./mock-crc";
|
||||
import "libsodium-wrappers";
|
||||
import "@snazzah/davey";
|
||||
import "dotenv/config";
|
||||
import { getVoiceConnection } from "@discordjs/voice";
|
||||
import { Client } from "discord.js-selfbot-v13";
|
||||
import { config } from "./config";
|
||||
import { AppError } from "./errors";
|
||||
import { createChildLogger } from "./logger";
|
||||
import { discordPlayer } from "./player";
|
||||
import { startRecording, stopRecording } from "./recorder";
|
||||
import { startWebserver } from "./webserver";
|
||||
import { createChildLogger } from "./logger";
|
||||
|
||||
const logger = createChildLogger("bot");
|
||||
|
||||
// Validasi environment variables
|
||||
const token = process.env.DISCORD_TOKEN;
|
||||
const voiceChannelId = process.env.VOICE_CHANNEL_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");
|
||||
const token = config.DISCORD_TOKEN;
|
||||
const voiceChannelId = config.VOICE_CHANNEL_ID;
|
||||
const guildId = config.GUILD_ID;
|
||||
|
||||
// Inisialisasi selfbot client
|
||||
const client = new Client();
|
||||
@@ -76,7 +73,7 @@ async function gracefulShutdown(signal: string) {
|
||||
}
|
||||
|
||||
client.on("ready", async () => {
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info({ user: client.user?.tag }, "Bot logged in");
|
||||
}
|
||||
|
||||
@@ -97,7 +94,7 @@ client.on("ready", async () => {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info(
|
||||
{ channelName: channel.name, channelId: channel.id },
|
||||
"Joining voice channel",
|
||||
@@ -114,7 +111,7 @@ client.on("ready", async () => {
|
||||
}
|
||||
|
||||
// Start Webserver
|
||||
startWebserver(config.webserverPort);
|
||||
startWebserver(config.WEBSERVER_PORT);
|
||||
});
|
||||
|
||||
client.on("error", (err) => {
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
}
|
||||
+26
-30
@@ -9,6 +9,7 @@ import {
|
||||
} from "@discordjs/voice";
|
||||
import type { Client, VoiceChannel } from "discord.js-selfbot-v13";
|
||||
import { config } from "./config";
|
||||
import { createChildLogger } from "./logger";
|
||||
import { PacketFilter } from "./packetFilter";
|
||||
import { subscribeToAudioStream } from "./recorder/audioStream";
|
||||
import { OpusDecoder } from "./recorder/decoder";
|
||||
@@ -17,13 +18,12 @@ import {
|
||||
createSegmentMetadata,
|
||||
} from "./recorder/metadata";
|
||||
import { SegmentManager } from "./recorder/segment";
|
||||
import type { PcmBroadcaster } from "./types";
|
||||
import { createChildLogger } from "./logger";
|
||||
import { retryWithBackoff } from "./retry";
|
||||
import type { PcmBroadcaster } from "./types";
|
||||
|
||||
const logger = createChildLogger("recorder");
|
||||
|
||||
const recordingsDir = config.recordingsDir;
|
||||
const recordingsDir = config.RECORDINGS_DIR;
|
||||
|
||||
// Pastikan folder recordings ada
|
||||
if (!fs.existsSync(recordingsDir)) {
|
||||
@@ -49,7 +49,7 @@ export async function startRecording(
|
||||
logger.info({ channelName: channel.name }, "Joining voice channel");
|
||||
|
||||
connection.on("debug", (msg) => {
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.debug({ message: msg }, "Voice debug");
|
||||
}
|
||||
});
|
||||
@@ -65,7 +65,7 @@ export async function startRecording(
|
||||
entersState(
|
||||
connection,
|
||||
VoiceConnectionStatus.Ready,
|
||||
config.voiceConnectionTimeoutMs,
|
||||
config.VOICE_CONNECTION_TIMEOUT_MS,
|
||||
),
|
||||
{
|
||||
retries: 3,
|
||||
@@ -87,7 +87,10 @@ export async function startRecording(
|
||||
// Dengarkan siapapun yang mulai bicara
|
||||
receiver.speaking.on("start", async (userId) => {
|
||||
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
|
||||
broadcaster.updateActiveUser?.(userId, {
|
||||
@@ -109,7 +112,9 @@ export async function startRecording(
|
||||
|
||||
try {
|
||||
// --- 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, {
|
||||
end: {
|
||||
behavior: EndBehaviorType.AfterSilence,
|
||||
@@ -119,13 +124,13 @@ export async function startRecording(
|
||||
const oggPacketStream = audioStream.pipe(packetFilterForOgg);
|
||||
const segmentManager = new SegmentManager(
|
||||
userDir,
|
||||
config.recordingSegmentMs,
|
||||
config.RECORDING_SEGMENT_MS,
|
||||
);
|
||||
|
||||
// --- Web broadcast: prism decoder with safe restart and cooldown ---
|
||||
const decoder = new OpusDecoder({
|
||||
cooldownMs: config.decoderCooldownMs,
|
||||
rotateMs: config.decoderRotateMs,
|
||||
cooldownMs: config.DECODER_COOLDOWN_MS,
|
||||
rotateMs: config.DECODER_ROTATE_MS,
|
||||
onData: (pcm) => {
|
||||
if (!broadcaster.broadcastPcmToWeb) return;
|
||||
// Downsample 48kHz stereo → 24kHz mono (left channel, every 2nd sample)
|
||||
@@ -139,7 +144,7 @@ export async function startRecording(
|
||||
|
||||
let currentSegment = segmentManager.open(oggPacketStream);
|
||||
currentSegment.out.on("finish", () => {
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info({ filename: currentSegment.filename }, "Segment saved");
|
||||
}
|
||||
const metadata = createSegmentMetadata(
|
||||
@@ -147,13 +152,13 @@ export async function startRecording(
|
||||
currentSegment,
|
||||
sessionId,
|
||||
sessionStartTime,
|
||||
config.recordingSegmentMs,
|
||||
config.RECORDING_SEGMENT_MS,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
currentSegment.jsonFilename,
|
||||
JSON.stringify(metadata, null, 2),
|
||||
);
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info(
|
||||
{ jsonFile: currentSegment.jsonFilename },
|
||||
"Metadata saved",
|
||||
@@ -162,10 +167,7 @@ export async function startRecording(
|
||||
});
|
||||
|
||||
currentSegment.out.on("error", (err) => {
|
||||
logger.error(
|
||||
{ userId, error: err.message },
|
||||
"File write error",
|
||||
);
|
||||
logger.error({ userId, error: err.message }, "File write error");
|
||||
});
|
||||
|
||||
// Feed Opus packets one-by-one
|
||||
@@ -189,19 +191,13 @@ export async function startRecording(
|
||||
onError: (error) => {
|
||||
segmentManager.close(oggPacketStream);
|
||||
decoder.destroy();
|
||||
logger.error(
|
||||
{ userId, error: error.message },
|
||||
"Audio stream error",
|
||||
);
|
||||
logger.error({ userId, error: error.message }, "Audio stream error");
|
||||
},
|
||||
});
|
||||
|
||||
packetFilterForOgg.on("error", (err) => {
|
||||
segmentManager.close(oggPacketStream);
|
||||
logger.error(
|
||||
{ userId, error: err.message },
|
||||
"PacketFilter error",
|
||||
);
|
||||
logger.error({ userId, error: err.message }, "PacketFilter error");
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(
|
||||
@@ -213,7 +209,7 @@ export async function startRecording(
|
||||
|
||||
// Handle disconnect yang tidak disengaja
|
||||
connection.on(VoiceConnectionStatus.Disconnected, async () => {
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.warn("Disconnected from voice channel. Reconnecting...");
|
||||
}
|
||||
try {
|
||||
@@ -221,12 +217,12 @@ export async function startRecording(
|
||||
entersState(
|
||||
connection,
|
||||
VoiceConnectionStatus.Signalling,
|
||||
config.reconnectTimeoutMs,
|
||||
config.RECONNECT_TIMEOUT_MS,
|
||||
),
|
||||
entersState(
|
||||
connection,
|
||||
VoiceConnectionStatus.Connecting,
|
||||
config.reconnectTimeoutMs,
|
||||
config.RECONNECT_TIMEOUT_MS,
|
||||
),
|
||||
]);
|
||||
// Berhasil reconnect
|
||||
@@ -237,7 +233,7 @@ export async function startRecording(
|
||||
});
|
||||
|
||||
connection.on(VoiceConnectionStatus.Destroyed, () => {
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info("Voice connection destroyed");
|
||||
}
|
||||
});
|
||||
@@ -250,7 +246,7 @@ export function stopRecording(guildId: string): void {
|
||||
const connection = getVoiceConnection(guildId);
|
||||
if (connection) {
|
||||
connection.destroy();
|
||||
if (config.verbose) {
|
||||
if (config.VERBOSE) {
|
||||
logger.info("Recording stopped and disconnected");
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -15,7 +15,7 @@ export function subscribeToAudioStream(
|
||||
const audioStream = receiver.subscribe(userId, {
|
||||
end: {
|
||||
behavior: EndBehaviorType.AfterSilence,
|
||||
duration: config.audioStreamSilenceDurationMs,
|
||||
duration: config.AUDIO_STREAM_SILENCE_DURATION_MS,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -25,9 +25,14 @@ export class OpusDecoder {
|
||||
options.createDecoder ??
|
||||
(() =>
|
||||
new prism.opus.Decoder({
|
||||
frameSize: config.opusFrameSize,
|
||||
channels: config.audioChannels as 1 | 2,
|
||||
rate: config.audioSampleRate as 8000 | 12000 | 16000 | 24000 | 48000,
|
||||
frameSize: config.OPUS_FRAME_SIZE,
|
||||
channels: config.AUDIO_CHANNELS as 1 | 2,
|
||||
rate: config.AUDIO_SAMPLE_RATE as
|
||||
| 8000
|
||||
| 12000
|
||||
| 16000
|
||||
| 24000
|
||||
| 48000,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export async function collectUserMetadata(
|
||||
avatarUrl:
|
||||
user?.displayAvatarURL({
|
||||
format: "png",
|
||||
size: config.avatarSize as
|
||||
size: config.AVATAR_SIZE as
|
||||
| 16
|
||||
| 32
|
||||
| 64
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+27
-11
@@ -1,12 +1,14 @@
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import http from "http";
|
||||
import path from "path";
|
||||
import pinoHttp from "pino-http";
|
||||
import prism from "prism-media";
|
||||
import { WebSocketServer } from "ws";
|
||||
import { createChildLogger, logger } from "./logger";
|
||||
import { discordPlayer } from "./player";
|
||||
import { createChildLogger } from "./logger";
|
||||
|
||||
const logger = createChildLogger("webserver");
|
||||
const wsLogger = createChildLogger("webserver");
|
||||
|
||||
const activeUsers = new Map<
|
||||
string,
|
||||
@@ -45,10 +47,27 @@ export function startWebserver(port: number = 3000) {
|
||||
|
||||
const wsPort = port + 1;
|
||||
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")));
|
||||
|
||||
// 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
|
||||
(global as any).broadcastPcmToWeb = (chunk: Buffer, userId: string) => {
|
||||
let hash = 0;
|
||||
@@ -125,10 +144,7 @@ export function startWebserver(port: number = 3000) {
|
||||
setInterval(() => {
|
||||
if (dbCount > 0) {
|
||||
const avg = dbAccum / dbCount;
|
||||
logger.info(
|
||||
{ level: avg.toFixed(1), frames: dbCount },
|
||||
"Audio level",
|
||||
);
|
||||
wsLogger.info({ level: avg.toFixed(1), frames: dbCount }, "Audio level");
|
||||
dbAccum = 0;
|
||||
dbCount = 0;
|
||||
}
|
||||
@@ -152,7 +168,7 @@ export function startWebserver(port: number = 3000) {
|
||||
if (playerPaused) {
|
||||
discordPlayer.unpause();
|
||||
playerPaused = false;
|
||||
logger.info("Transmitting — Discord indicator ON");
|
||||
wsLogger.info("Transmitting — Discord indicator ON");
|
||||
}
|
||||
} else if (msSinceAudio < SILENCE_TAIL_MS && msSinceAudio > 0) {
|
||||
// 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
|
||||
discordPlayer.pause();
|
||||
playerPaused = true;
|
||||
logger.info("Stopped — Discord indicator OFF");
|
||||
wsLogger.info("Stopped — Discord indicator OFF");
|
||||
return;
|
||||
} else {
|
||||
return; // already paused, nothing to do
|
||||
@@ -175,7 +191,7 @@ export function startWebserver(port: number = 3000) {
|
||||
}, 20);
|
||||
|
||||
wss.on("connection", (ws) => {
|
||||
logger.info({ wsPort }, "New WebSocket connection");
|
||||
wsLogger.info({ wsPort }, "New WebSocket connection");
|
||||
wsClients.add(ws);
|
||||
|
||||
ws.send(
|
||||
@@ -210,6 +226,6 @@ export function startWebserver(port: number = 3000) {
|
||||
});
|
||||
|
||||
server.listen(port, "0.0.0.0", () => {
|
||||
logger.info({ port }, "Web interface listening");
|
||||
wsLogger.info({ port }, "Web interface listening");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user