Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 70 additions & 6 deletions CONFIGURATION.md

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions src/commands/chatInputCommands/lyrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ export default new ChatInputCommandHandler()
const response = await interaction.client.music.rest.execute({
path: `/v4/sessions/${player.api.session.id}/players/${interaction.guildId}/track/lyrics`,
method: 'GET',
headers: {
Authorization: `Bearer ${settings.lavalink.password}`,
},
});
json = (await response.json()) as LavaLyricsResponse;
lyrics = formatLavaLyricsResponse(json, player);
Expand Down
3 changes: 0 additions & 3 deletions src/events/music/trackStart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,9 +236,6 @@ export default {
const response = await queue.player.client.music.rest.execute({
path: `/v4/sessions/${queue.player.api.session.id}/players/${guild.id}/track/lyrics`,
method: 'GET',
headers: {
Authorization: `Bearer ${settings.lavalink.password}`,
},
});
json = (await response.json()) as LavaLyricsResponse;
lyrics = formatLavaLyricsResponse(json, queue.player);
Expand Down
112 changes: 88 additions & 24 deletions src/lib/QuaverClient.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getAbsoluteFileURL } from '@zptxdev/zptx-lib';
import { Client, GatewayDispatchEvents } from 'discord.js';
import { readdirSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import type { Server } from 'socket.io';
import { MessageOptionsBuilderType } from '.';
import type { EventHandler } from './builders';
Expand All @@ -9,12 +11,12 @@ import {
InteractionHandler,
type InteractionHandlerMapsFlat,
} from './interactions';
import { ConnectionHealthMonitor, QuaverNode } from './music';
import { ConnectionHealthMonitor, QuaverNode, QuaverCluster, RegionAffinity } from './music';
import { settings } from './util';

export class QuaverClient extends Client {
io?: Server;
music?: QuaverNode;
music?: QuaverNode | QuaverCluster;
interactionHandler: InteractionHandler;
connectionHealth: ConnectionHealthMonitor;
private lastMediaUnstable: boolean = false;
Expand All @@ -31,39 +33,101 @@ export class QuaverClient extends Client {
}

connectToMusicNode(): void {
this.music = new QuaverNode(
{
info: {
host: settings.lavalink.host,
port: settings.lavalink.port,
auth: settings.lavalink.password,
tls: !!settings.lavalink.secure,
},
ws: {
reconnecting: {
delay: settings.lavalink.reconnect.delay ?? 3000,
tries: settings.lavalink.reconnect.tries ?? 5,
const config = settings.lavalink;

// Detect configuration type and instantiate appropriate class
if ('nodes' in config) {
// Multi-node configuration: use QuaverCluster
const nodes = config.nodes.map(
(
node,
): {
info: { host: string; port: number; auth: string; tls: boolean };
ws: { reconnecting: { delay: number; tries: number } };
region: string;
} => ({
info: {
host: node.host,
port: node.port,
auth: node.password,
tls: !!node.secure,
},
ws: {
reconnecting: {
delay: node.reconnect?.delay ?? 3000,
tries: node.reconnect?.tries ?? 5,
},
},
region: node.region,
}),
);

// Create RegionAffinity for ping-based node selection
const __dirname = dirname(fileURLToPath(import.meta.url));
const databaseUri = settings.database
? `${settings.database.protocol}://${resolve(
__dirname,
'..',
'..',
settings.database.path,
).replace(/\\/g, '/')}`
: `sqlite://${resolve(__dirname, '..', '..', 'database.sqlite').replace(/\\/g, '/')}`;
const regionAffinity = new RegionAffinity(databaseUri);

// Update ConnectionHealthMonitor with RegionAffinity
this.connectionHealth.setRegionAffinity(regionAffinity);

this.music = new QuaverCluster(
{
nodes,
discord: {
sendGatewayCommand: (id, payload): void =>
this.guilds.cache.get(id)?.shard?.send(payload),
},
},
discord: {
sendGatewayCommand: (id, payload): void =>
this.guilds.cache.get(id)?.shard?.send(payload),
this,
regionAffinity,
);
} else {
// Single-node configuration: use QuaverNode
this.music = new QuaverNode(
{
info: {
host: config.host,
port: config.port,
auth: config.password,
tls: !!config.secure,
},
ws: {
reconnecting: {
delay: config.reconnect?.delay ?? 3000,
tries: config.reconnect?.tries ?? 5,
},
},
discord: {
sendGatewayCommand: (id, payload): void =>
this.guilds.cache.get(id)?.shard?.send(payload),
},
},
},
this,
);
this,
);
}

// Setup voice update handlers (work with both Node and Cluster)
this.ws.on(
GatewayDispatchEvents.VoiceServerUpdate,
async (payload): Promise<boolean> => {
// Capture media server endpoint for health monitoring
this.connectionHealth.updateMediaEndpoint(payload.endpoint ?? null);
return this.music.players.handleVoiceUpdate(payload);
// Get the node ID from the player manager
const nodeId = this.music!.players.getNodeIdForGuild(payload.guild_id);
this.connectionHealth.updateMediaEndpoint(payload.endpoint ?? null, nodeId);
return this.music!.players.handleVoiceUpdate(payload);
},
);
this.ws.on(
GatewayDispatchEvents.VoiceStateUpdate,
async (payload): Promise<boolean> =>
this.music.players.handleVoiceUpdate(payload),
this.music!.players.handleVoiceUpdate(payload),
);
}

Expand Down Expand Up @@ -141,7 +205,7 @@ export class QuaverClient extends Client {
const player = await this.music?.players.fetch(guild.id);
if (player?.voice.connected && player.queue.channel) {
// Send warning message to the player's bound text channel
await player.sendMessage(guild.locale('MUSIC.PLAYER.CONNECTION_UNSTABLE'), {
await player.sendMessage(g.locale('MUSIC.PLAYER.CONNECTION_UNSTABLE'), {
type: MessageOptionsBuilderType.Warning,
});
}
Expand Down
Loading
Loading