Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
0cff622
fix: add missing MESSAGE_CLEAR_RADIO_SUCCESS key and refactor ClearCo…
hoehoetvhss Aug 6, 2026
c5f6388
fix: resolve update race condition on skip and add embed text length …
hoehoetvhss Aug 6, 2026
a18989c
fix: prevent negative bot ping display using Math.abs
hoehoetvhss Aug 6, 2026
1dd1a52
Merge branch 'origin/patch' and resolve locale translation conflicts
hoehoetvhss Aug 9, 2026
ef02ad8
Merge branch 'hmes98318:main' into patch
hoehoetvhss Aug 9, 2026
7795a49
Merge branch 'hmes98318:main' into patch
hoehoetvhss Aug 11, 2026
3ff87a2
fix: make playlist lookup case-insensitive, fix playfirst for playlis…
hoehoetvhss Aug 11, 2026
a6e184d
i18n proofreads - #125
hoehoetvhss Aug 12, 2026
4364eee
fix: prevent voice connection flood and sanitize fallback search in p…
hoehoetvhss Aug 12, 2026
1e98ba9
feat: add M3U playlist import toggle flag and command with disabled-b…
hoehoetvhss Aug 12, 2026
92697a4
fix: resolve zombie player leaks, optimize queue restore batching, an…
hoehoetvhss Aug 12, 2026
e4fd15a
fix: replace loose equality, resolve collector race conditions, and g…
hoehoetvhss Aug 12, 2026
02b0152
feat: Add dropdown select menu and choices to nodestatus command
hoehoetvhss Aug 12, 2026
1f92faa
Revert wrong reservable correction - #125
hoehoetvhss Aug 12, 2026
f8f114d
fix(: restore missing playlist translation keys across locales
hoehoetvhss Aug 12, 2026
5ca6dd2
fix: pass requested language context to help command embed and comman…
hoehoetvhss Aug 12, 2026
4ec99ca
fix: display selected command name instead of bot username in select …
hoehoetvhss Aug 12, 2026
2c900d6
refactor: restrict M3U import to Admin/DJ and disallow full queueing
hoehoetvhss Aug 12, 2026
517546b
fix:: restore missing MESSAGE_SKIP_EMPTY_DISCONNECT key in ko-KR locale
hoehoetvhss Aug 12, 2026
d402f36
refactor: relocate generateRandomKey to utils and remove obsolete has…
hoehoetvhss Aug 12, 2026
8845f8b
fix: resolve playlist info duplication, playfirst loop conflict, and …
hoehoetvhss Aug 14, 2026
bd48b12
fix: resolve playlist subcommand conflicts, playfirst loop issue, and…
hoehoetvhss Aug 14, 2026
a1e001c
fix: optimize graceful shutdown handling and sqlite WAL checkpointing
hoehoetvhss Aug 14, 2026
10faa73
fix: allow direct text channel instances in DashboardManager.initialize
hoehoetvhss Aug 14, 2026
83a526e
feat: re-send dashboard to the bottom of the channel on update
hoehoetvhss Aug 14, 2026
7ee9b07
fix: collect all restored tracks before queuing to prevent queue trun…
hoehoetvhss Aug 14, 2026
86e3639
fix: implement smart edit and conditional re-send for dashboard messages
hoehoetvhss Aug 14, 2026
2a8e6c3
fix: ensure unencoded queue tracks are properly serialized and persisted
hoehoetvhss Aug 14, 2026
a82a2ba
fix: prevent race conditions and duplicate updates during track skip
hoehoetvhss Aug 14, 2026
227ef12
fix: prevent empty search query from resolving default YouTube trendi…
hoehoetvhss Aug 14, 2026
3837d84
fix: delete old dashboard message during initialization to prevent du…
hoehoetvhss Aug 14, 2026
d54fb1e
fix: preserve custom volume level across restarts and dashboard updates
hoehoetvhss Aug 14, 2026
37d832c
fix: resolve option name mismatch in language command
hoehoetvhss Aug 14, 2026
278fc12
feat: add M3U playlist import with toggle and admin/DJ restriction
Dum4G Aug 14, 2026
c52523d
fix: make saved queue restore reliable - bulk decode, rate-limit retr…
Dum4G Aug 14, 2026
f172e99
fix: correct saved playlist playback - voice channel join, mix expans…
Dum4G Aug 14, 2026
fad6643
fix: stabilize dashboard messages - smart edit, duplicate prevention,…
Dum4G Aug 14, 2026
20981c4
feat: add node status dropdown menu
Dum4G Aug 14, 2026
2f726d9
fix: polish commands - language context, ping display, help select menu
Dum4G Aug 14, 2026
982feb7
fix: harden shutdown and player lifecycle - zombie leaks, WAL checkpo…
Dum4G Aug 14, 2026
1461a6f
chore: i18n proofreads and missing locale keys
Dum4G Aug 14, 2026
181e0b3
chore: refactor and dependencies - generateRandomKey relocation, depe…
Dum4G Aug 14, 2026
5a507bd
fix: clear single-track repeat before starting radio playback
Dum4G Aug 14, 2026
d9235ff
fix: resolve radio tracks strictly from saved playlist data
Dum4G Aug 14, 2026
33bd821
fix: stop radio session when playing a saved playlist
Dum4G Aug 15, 2026
9294cf9
fix: serialize dashboard lifecycle and always edit in place
Dum4G Aug 15, 2026
99d1620
fix: resolve playfirst player connection, query parsing, and dashboar…
hoehoetvhss Aug 15, 2026
0fa1fc5
fix: enforce connection state check and align with pr-126-clean updates
hoehoetvhss Aug 15, 2026
5d2333e
feat: integrate pr-126-clean dashboard lifecycle, radio playback fixe…
hoehoetvhss Aug 15, 2026
a269a6e
fix(playlist): align bulk decode and error logging with pr-126-clean
hoehoetvhss Aug 15, 2026
d14847f
Merge remote-tracking branch 'dumrailways/pr-126-clean' into patch
hoehoetvhss Aug 15, 2026
6251336
feat(config): restrict playback control commands to DJ and Admin
hoehoetvhss Aug 15, 2026
e5a448a
chore: update globals and clean up package overrides
hoehoetvhss Aug 15, 2026
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
7 changes: 6 additions & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const config = {
command: {
disableCommand: [], // Disabled commands, all enabled by default
adminCommand: ['blacklist', 'language', 'server', 'status', 'volume-default'], // Admin commands, only Admin role user can use
djCommand: ['dj', 'filter'], // DJ commands, only DJ role user can use
djCommand: ['dj', 'filter', 'stop', 'pause', 'resume', 'skip', 'clear', 'leave', 'shuffle', 'loop', 'volume'], // DJ commands, only DJ role user can use
// Supported commands: 'skip', 'seek', 'pause'
// When a command name is listed here, only the requester of the currently playing song may use it.
// Admins can always bypass this restriction regardless.
Expand All @@ -177,6 +177,11 @@ const config = {
enabled : false // Enable/disable persistent queue storage
},

// Playlist settings
playlist: {
enabled : true // Enable/disable playlist feature
},

// SQLite database settings
database: {
path : './data/database.db' // Path to SQLite database file
Expand Down
165 changes: 82 additions & 83 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 6 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,9 @@
"url": "https://github.com/hmes98318/Music-Disc/issues"
},
"homepage": "https://github.com/hmes98318/Music-Disc#readme",
"overrides": {
"body-parser": "^2.2.3",
"brace-expansion": "^5.0.8"
},
"dependencies": {
"bcryptjs": "^3.0.3",
"better-sqlite3": "^13.0.2",
"better-sqlite3": "^13.0.3",
"cookie": "^2.0.1",
"discord.js": "~14.27.0",
"dotenv": "^17.4.2",
Expand All @@ -48,12 +44,12 @@
"@eslint/js": "^10.0.1",
"@types/better-sqlite3": "^9.6.0",
"@types/express": "^5.0.6",
"@types/node": "^26.1.2",
"eslint": "^10.8.0",
"globals": "^17.9.0",
"tsx": "^4.23.5",
"@types/node": "^26.2.0",
"eslint": "^10.8.1",
"globals": "^17.11.0",
"tsx": "^4.23.12",
"typescript": "~6.0.3",
"typescript-eslint": "^8.66.0"
"typescript-eslint": "^8.67.0"
},
"engines": {
"node": ">=22.23.2"
Expand Down
7 changes: 6 additions & 1 deletion src/@types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,17 @@ export type Config = {
bot: BotConfig;
nodeList: NodeOptions[];
spotify: SpotifyConfig;
blacklist: string[],
blacklist: string[];
webDashboard: WebDashboardConfig;
localNode: LocalNodeConfig;
command: CommandConfig;
database: DatabaseConfig;
queuePersistence: QueuePersistenceConfig;
playlist: PlaylistConfig;
};

export type PlaylistConfig = {
enabled: boolean;
};

export type BotConfig = {
Expand Down
35 changes: 24 additions & 11 deletions src/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,12 @@ class App {
* @private
*/
#setShutdownSignalHandlers(): void {
let isShuttingDown = false;

const shutdown = async (signal: string) => {
if (isShuttingDown) return;
isShuttingDown = true;

this.bot.logger.log( this.bot.shardId, `Received ${signal}. Closing server gracefully...`);

const timeout = setTimeout(() => {
Expand All @@ -143,29 +148,33 @@ class App {
}, 30 * 1000);

try {
// Stop periodic saves and save all active queues before shutdown
// Stop periodic saves and save all active queues in parallel before shutdown
if (this.bot.config.queuePersistence.enabled && this.#client.queuePersistence) {
this.bot.logger.log( this.bot.shardId, 'Saving active queues before shutdown...');
this.#client.queuePersistence.stopAllPeriodicSaves();
for (const player of this.#client.lavashark.players.values()) {
await this.#client.queuePersistence.saveQueue(player);
}
const players = Array.from(this.#client.lavashark?.players?.values() ?? []);
await Promise.allSettled(
players.map(player => this.#client.queuePersistence!.saveQueue(player))
);
}

// Close the lavashark players connections
this.bot.logger.log( this.bot.shardId, 'Closing voice channel connection...');
const activePlayers = Array.from(this.#client.lavashark?.players?.values() ?? []);
await Promise.allSettled(
Array.from(this.#client.lavashark.players.values()).map(player => player.destroy())
activePlayers.map(player => player.destroy())
);
await new Promise(resolve => setTimeout(resolve, 2000));
this.bot.logger.log( this.bot.shardId, 'Voice channel connection closed.');

// Close the lavashark nodes connections
this.bot.logger.log( this.bot.shardId, 'Closing lavashark nodes...');
for (const node of this.#client.lavashark.nodes) {
node.disconnect();
if (this.#client.lavashark?.nodes) {
for (const node of this.#client.lavashark.nodes) {
try {
node.disconnect();
} catch (_) {}
}
}
await new Promise(resolve => setTimeout(resolve, 2000));
this.bot.logger.log( this.bot.shardId, 'Lavashark nodes closed.');

// Close discord.js connection
Expand Down Expand Up @@ -227,6 +236,10 @@ const main = async () => {
main();


process.on('unhandledRejection', (error) => {
console.error('Unhandled promise rejection:', error);
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Promise Rejection at:', promise, 'reason:', reason);
});

process.on('uncaughtException', (error) => {
console.error('Uncaught Exception thrown:', error);
});
15 changes: 9 additions & 6 deletions src/commands/BlacklistCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,32 @@ export class BlacklistCommand extends BaseCommand {
/**
* Build blacklist command metadata and slash command options
*/
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
aliases: [],
category: CommandCategory.UTILITY,
description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION'),
description: i18next.t('commands:CONFIG_BLACKLIST_DESCRIPTION', { lng }),
name: 'blacklist',
options: [
this.createUserSubcommand(
'add',
'commands:CONFIG_BLACKLIST_OPTION_ADD',
lng,
),
this.createUserSubcommand(
'remove',
'commands:CONFIG_BLACKLIST_OPTION_REMOVE',
lng,
),
{
description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST'),
description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_LIST', { lng }),
name: 'list',
type: ApplicationCommandOptionType.Subcommand,
},
],
sendTyping: true,
showHelp: true,
usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE'),
usage: i18next.t('commands:CONFIG_BLACKLIST_USAGE', { lng }),
voiceChannel: false,
};
}
Expand Down Expand Up @@ -99,13 +101,14 @@ export class BlacklistCommand extends BaseCommand {
private createUserSubcommand(
name: BlacklistMutationAction,
descriptionKey: string,
lng?: string
): ApplicationCommandSubCommandData {
return {
description: i18next.t(descriptionKey),
description: i18next.t(descriptionKey, { lng }),
name,
options: [
{
description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER'),
description: i18next.t('commands:CONFIG_BLACKLIST_OPTION_USER', { lng }),
name: 'user',
required: true,
type: ApplicationCommandOptionType.User,
Expand Down
8 changes: 4 additions & 4 deletions src/commands/ClearCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js';


export class ClearCommand extends BaseCommand {
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
name: 'clear',
aliases: ['cls'],
description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION'),
usage: i18next.t('commands:CONFIG_CLEAR_USAGE'),
description: i18next.t('commands:CONFIG_CLEAR_DESCRIPTION', { lng }),
usage: i18next.t('commands:CONFIG_CLEAR_USAGE', { lng }),
category: CommandCategory.MUSIC,
voiceChannel: true,
showHelp: true,
Expand Down Expand Up @@ -48,7 +48,7 @@ export class ClearCommand extends BaseCommand {
}
else {
await context.replySuccess(bot, isRadioPlaying
? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS', { defaultValue: '🧹 Cleared queue and stopped radio playback.' })
? context.t('commands:MESSAGE_CLEAR_RADIO_SUCCESS')
: context.t('commands:MESSAGE_CLEAR_SUCCESS'));
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/commands/DashboardCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import type { Bot, CommandMetadata } from '../@types/index.js';


export class DashboardCommand extends BaseCommand {
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
name: 'dashboard',
aliases: ['d', 'console'],
description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION'),
usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE'),
aliases: ['db'],
description: i18next.t('commands:CONFIG_DASHBOARD_DESCRIPTION', { lng }),
usage: i18next.t('commands:CONFIG_DASHBOARD_USAGE', { lng }),
category: CommandCategory.MUSIC,
voiceChannel: true,
showHelp: true,
Expand Down
8 changes: 4 additions & 4 deletions src/commands/DjCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,20 @@ import type { Bot, CommandMetadata } from '../@types/index.js';


export class DjCommand extends BaseCommand {
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
name: 'dj',
aliases: ['adddj'],
description: i18next.t('commands:CONFIG_DJ_DESCRIPTION'),
usage: i18next.t('commands:CONFIG_DJ_USAGE'),
description: i18next.t('commands:CONFIG_DJ_DESCRIPTION', { lng }),
usage: i18next.t('commands:CONFIG_DJ_USAGE', { lng }),
category: CommandCategory.UTILITY,
voiceChannel: false,
showHelp: true,
sendTyping: false,
options: [
{
name: 'user',
description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION'),
description: i18next.t('commands:CONFIG_DJ_OPTION_DESCRIPTION', { lng }),
type: ApplicationCommandOptionType.User,
required: false
}
Expand Down
8 changes: 4 additions & 4 deletions src/commands/FilterCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,20 @@ import type { Bot, CommandMetadata } from '../@types/index.js';


export class FilterCommand extends BaseCommand {
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
name: 'filter',
aliases: ['eq', 'equalizer'],
description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION'),
usage: i18next.t('commands:CONFIG_FILTER_USAGE'),
description: i18next.t('commands:CONFIG_FILTER_DESCRIPTION', { lng }),
usage: i18next.t('commands:CONFIG_FILTER_USAGE', { lng }),
category: CommandCategory.MUSIC,
voiceChannel: true,
showHelp: true,
sendTyping: true,
options: [
{
name: 'filter',
description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION'),
description: i18next.t('commands:CONFIG_FILTER_OPTION_DESCRIPTION', { lng }),
type: 3,
required: true,
choices: [
Expand Down
27 changes: 13 additions & 14 deletions src/commands/HelpCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,20 @@ import type { Bot, CommandMetadata } from '../@types/index.js';


export class HelpCommand extends BaseCommand {
public getMetadata(_bot: Bot): CommandMetadata {
public getMetadata(_bot: Bot, lng?: string): CommandMetadata {
return {
name: 'help',
aliases: ['h'],
description: i18next.t('commands:CONFIG_HELP_DESCRIPTION'),
usage: i18next.t('commands:CONFIG_HELP_USAGE'),
description: i18next.t('commands:CONFIG_HELP_DESCRIPTION', { lng }),
usage: i18next.t('commands:CONFIG_HELP_USAGE', { lng }),
category: CommandCategory.UTILITY,
voiceChannel: false,
showHelp: true,
sendTyping: true,
options: [
{
name: 'command',
description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION'),
description: i18next.t('commands:CONFIG_HELP_OPTION_DESCRIPTION', { lng }),
type: 3,
required: false
}
Expand Down Expand Up @@ -59,15 +59,14 @@ export class HelpCommand extends BaseCommand {
* @private
*/
async #showCommandList(bot: Bot, client: Client, context: CommandContext): Promise<void> {
const title = client.user?.username;
const commands = client.commands.getHelpCommands(bot);
const commands = client.commands.getHelpCommands(bot, context.language);

const musicCommands = commands.filter(cmd => {
const metadata = cmd.getMetadata(bot);
const metadata = cmd.getMetadata(bot, context.language);
return metadata.category === CommandCategory.MUSIC;
});
const utilityCommands = commands.filter(cmd => {
const metadata = cmd.getMetadata(bot);
const metadata = cmd.getMetadata(bot, context.language);
return metadata.category === CommandCategory.UTILITY;
});

Expand All @@ -76,7 +75,7 @@ export class HelpCommand extends BaseCommand {
.setCustomId(SelectButtonId.HelpMusic)
.setPlaceholder(context.t('commands:HELP_SELECT_MUSIC_PLACEHOLDER'))
.setOptions(musicCommands.map(cmd => {
const metadata = cmd.getMetadata(bot);
const metadata = cmd.getMetadata(bot, context.language);
const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE');
return {
label: metadata.name,
Expand All @@ -89,7 +88,7 @@ export class HelpCommand extends BaseCommand {
.setCustomId(SelectButtonId.HelpUtility)
.setPlaceholder(context.t('commands:HELP_SELECT_UTILITY_PLACEHOLDER'))
.setOptions(utilityCommands.map(cmd => {
const metadata = cmd.getMetadata(bot);
const metadata = cmd.getMetadata(bot, context.language);
const aliases = metadata.aliases && metadata.aliases.length > 0 ? metadata.aliases.join(', ') : context.t('commands:HELP_COMMAND_NONE');
return {
label: metadata.name,
Expand Down Expand Up @@ -120,12 +119,12 @@ export class HelpCommand extends BaseCommand {
const selectedCommand = client.commands.get(i.values[0]);
if (!selectedCommand) return;

const metadata = selectedCommand.getMetadata(bot);
const metadata = selectedCommand.getMetadata(bot, context.language);
const usage = `${metadata.description}\n\`\`\`${bot.config.bot.prefix}${metadata.usage}\`\`\``;

await i.deferUpdate();
await msg.edit({
embeds: [embeds.help(bot, title!, usage, context.language)],
embeds: [embeds.help(bot, metadata.name, usage, context.language)],
components: [],
allowedMentions: { repliedUser: false }
}).catch(() => bot.logger.discord( bot.shardId, 'Failed to edit deleted message.'));
Expand All @@ -150,11 +149,11 @@ export class HelpCommand extends BaseCommand {
*/
async #showCommandHelp(bot: Bot, client: Client, context: CommandContext, commandName: string): Promise<void> {
const prefix = bot.config.bot.prefix;
const commands = client.commands.getHelpCommands(bot);
const commands = client.commands.getHelpCommands(bot, context.language);

let found = false;
for (const cmd of commands) {
const metadata = cmd.getMetadata(bot);
const metadata = cmd.getMetadata(bot, context.language);
if (commandName === metadata.name || (metadata.aliases && metadata.aliases.includes(commandName))) {
const description = `${metadata.description}\n\`\`\`${prefix}${metadata.usage}\`\`\``;

Expand Down
Loading