mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-23 01:05:30 +01:00
Configurable voice channel leave behavior (#514)
Co-authored-by: Max Isom <hi@maxisom.me>
This commit is contained in:
parent
8e5b3cfa43
commit
4dbb55a721
|
@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
### Changed
|
||||||
|
- Muse now stays in a voice channel after the queue finishes for 30 seconds by default. This behavior can be changed with `/config set-wait-after-queue-empties`.
|
||||||
|
|
||||||
## [1.0.0] - 2022-02-05
|
## [1.0.0] - 2022-02-05
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Setting" (
|
||||||
|
"guildId" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"playlistLimit" INTEGER NOT NULL DEFAULT 50,
|
||||||
|
"secondsToWaitAfterQueueEmpties" INTEGER NOT NULL DEFAULT 30,
|
||||||
|
"leaveIfNoListeners" BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
"roleId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Setting" ("createdAt", "guildId", "playlistLimit", "roleId", "updatedAt") SELECT "createdAt", "guildId", "playlistLimit", "roleId", "updatedAt" FROM "Setting";
|
||||||
|
DROP TABLE "Setting";
|
||||||
|
ALTER TABLE "new_Setting" RENAME TO "Setting";
|
||||||
|
PRAGMA foreign_key_check;
|
||||||
|
PRAGMA foreign_keys=ON;
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
|
||||||
"watch": ["dist"],
|
"watch": ["dist"],
|
||||||
"exec": "npm run env:set-database-url -- node --experimental-json-modules dist/src/scripts/start.js",
|
"exec": "npm run env:set-database-url -- node --experimental-json-modules dist/scripts/start.js",
|
||||||
"ext": "js"
|
"ext": "js"
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,13 @@ model KeyValueCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Setting {
|
model Setting {
|
||||||
guildId String @id
|
guildId String @id
|
||||||
playlistLimit Int @default(50)
|
playlistLimit Int @default(50)
|
||||||
roleId String?
|
secondsToWaitAfterQueueEmpties Int @default(30)
|
||||||
createdAt DateTime @default(now())
|
leaveIfNoListeners Boolean @default(true)
|
||||||
updatedAt DateTime @updatedAt
|
roleId String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
model FavoriteQuery {
|
model FavoriteQuery {
|
||||||
|
|
|
@ -24,6 +24,21 @@ export default class implements Command {
|
||||||
.setName('role')
|
.setName('role')
|
||||||
.setDescription('allowed role')
|
.setDescription('allowed role')
|
||||||
.setRequired(true)))
|
.setRequired(true)))
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('set-wait-after-queue-empties')
|
||||||
|
.setDescription('set the time to wait before leaving the voice channel when queue empties')
|
||||||
|
.addIntegerOption(option => option
|
||||||
|
.setName('delay')
|
||||||
|
.setDescription('delay in seconds (set to 0 to never leave)')
|
||||||
|
.setRequired(true)
|
||||||
|
.setMinValue(0)))
|
||||||
|
.addSubcommand(subcommand => subcommand
|
||||||
|
.setName('set-leave-if-no-listeners')
|
||||||
|
.setDescription('set whether to leave when all other participants leave')
|
||||||
|
.addBooleanOption(option => option
|
||||||
|
.setName('value')
|
||||||
|
.setDescription('whether to leave when everyone else leaves')
|
||||||
|
.setRequired(true)))
|
||||||
.addSubcommand(subcommand => subcommand
|
.addSubcommand(subcommand => subcommand
|
||||||
.setName('get')
|
.setName('get')
|
||||||
.setDescription('show all settings'));
|
.setDescription('show all settings'));
|
||||||
|
@ -70,6 +85,40 @@ export default class implements Command {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'set-wait-after-queue-empty': {
|
||||||
|
const delay = interaction.options.getInteger('delay')!;
|
||||||
|
|
||||||
|
await prisma.setting.update({
|
||||||
|
where: {
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
secondsToWaitAfterQueueEmpties: delay,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply('👍 wait delay updated');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'set-leave-if-no-listeners': {
|
||||||
|
const value = interaction.options.getBoolean('value')!;
|
||||||
|
|
||||||
|
await prisma.setting.update({
|
||||||
|
where: {
|
||||||
|
guildId: interaction.guild!.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
leaveIfNoListeners: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply('👍 leave setting updated');
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'get': {
|
case 'get': {
|
||||||
const embed = new MessageEmbed().setTitle('Config');
|
const embed = new MessageEmbed().setTitle('Config');
|
||||||
|
|
||||||
|
@ -82,6 +131,10 @@ export default class implements Command {
|
||||||
const settingsToShow = {
|
const settingsToShow = {
|
||||||
'Playlist Limit': config.playlistLimit,
|
'Playlist Limit': config.playlistLimit,
|
||||||
Role: config.roleId ? `<@&${config.roleId}>` : 'not set',
|
Role: config.roleId ? `<@&${config.roleId}>` : 'not set',
|
||||||
|
'Wait before leaving after queue empty': config.secondsToWaitAfterQueueEmpties === 0
|
||||||
|
? 'never leave'
|
||||||
|
: `${config.secondsToWaitAfterQueueEmpties}s`,
|
||||||
|
'Leave if there are no listeners': config.leaveIfNoListeners ? 'yes' : 'no',
|
||||||
};
|
};
|
||||||
|
|
||||||
let description = '';
|
let description = '';
|
||||||
|
|
|
@ -3,15 +3,23 @@ import container from '../inversify.config.js';
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
import PlayerManager from '../managers/player.js';
|
import PlayerManager from '../managers/player.js';
|
||||||
import {getSizeWithoutBots} from '../utils/channels.js';
|
import {getSizeWithoutBots} from '../utils/channels.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
export default (oldState: VoiceState, _: VoiceState): void => {
|
export default async (oldState: VoiceState, _: VoiceState): Promise<void> => {
|
||||||
const playerManager = container.get<PlayerManager>(TYPES.Managers.Player);
|
const playerManager = container.get<PlayerManager>(TYPES.Managers.Player);
|
||||||
|
|
||||||
const player = playerManager.get(oldState.guild.id);
|
const player = playerManager.get(oldState.guild.id);
|
||||||
|
|
||||||
if (player.voiceConnection) {
|
if (player.voiceConnection) {
|
||||||
const voiceChannel: VoiceChannel = oldState.guild.channels.cache.get(player.voiceConnection.joinConfig.channelId!) as VoiceChannel;
|
const voiceChannel: VoiceChannel = oldState.guild.channels.cache.get(player.voiceConnection.joinConfig.channelId!) as VoiceChannel;
|
||||||
if (!voiceChannel || getSizeWithoutBots(voiceChannel) === 0) {
|
const settings = await prisma.setting.findUnique({where: {guildId: player.guildId}});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error('Could not find settings for guild');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {leaveIfNoListeners} = settings;
|
||||||
|
if (!voiceChannel || (getSizeWithoutBots(voiceChannel) === 0 && leaveIfNoListeners)) {
|
||||||
player.disconnect();
|
player.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {Except} from 'type-fest';
|
||||||
import shuffle from 'array-shuffle';
|
import shuffle from 'array-shuffle';
|
||||||
import {TYPES} from '../types.js';
|
import {TYPES} from '../types.js';
|
||||||
import GetSongs from '../services/get-songs.js';
|
import GetSongs from '../services/get-songs.js';
|
||||||
import {QueuedSong} from './player.js';
|
import {QueuedSong, STATUS} from './player.js';
|
||||||
import PlayerManager from '../managers/player.js';
|
import PlayerManager from '../managers/player.js';
|
||||||
import {prisma} from '../utils/db.js';
|
import {prisma} from '../utils/db.js';
|
||||||
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
|
||||||
|
@ -131,6 +131,9 @@ export default class AddQueryToQueue {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
embeds: [buildPlayingMessageEmbed(player)],
|
embeds: [buildPlayingMessageEmbed(player)],
|
||||||
});
|
});
|
||||||
|
} else if (player.status === STATUS.IDLE) {
|
||||||
|
// Player is idle, start playback instead
|
||||||
|
await player.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build response message
|
// Build response message
|
||||||
|
|
|
@ -8,6 +8,7 @@ import shuffle from 'array-shuffle';
|
||||||
import {AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus} from '@discordjs/voice';
|
import {AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus} from '@discordjs/voice';
|
||||||
import FileCacheProvider from './file-cache.js';
|
import FileCacheProvider from './file-cache.js';
|
||||||
import debug from '../utils/debug.js';
|
import debug from '../utils/debug.js';
|
||||||
|
import {prisma} from '../utils/db.js';
|
||||||
|
|
||||||
export interface QueuedPlaylist {
|
export interface QueuedPlaylist {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -29,6 +30,7 @@ export interface QueuedSong {
|
||||||
export enum STATUS {
|
export enum STATUS {
|
||||||
PLAYING,
|
PLAYING,
|
||||||
PAUSED,
|
PAUSED,
|
||||||
|
IDLE,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlayerEvents {
|
export interface PlayerEvents {
|
||||||
|
@ -50,6 +52,7 @@ export default class {
|
||||||
private positionInSeconds = 0;
|
private positionInSeconds = 0;
|
||||||
|
|
||||||
private readonly fileCache: FileCacheProvider;
|
private readonly fileCache: FileCacheProvider;
|
||||||
|
private disconnectTimer: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
constructor(fileCache: FileCacheProvider, guildId: string) {
|
constructor(fileCache: FileCacheProvider, guildId: string) {
|
||||||
this.fileCache = fileCache;
|
this.fileCache = fileCache;
|
||||||
|
@ -131,6 +134,12 @@ export default class {
|
||||||
throw new Error('Queue empty.');
|
throw new Error('Queue empty.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cancel any pending idle disconnection
|
||||||
|
if (this.disconnectTimer) {
|
||||||
|
clearInterval(this.disconnectTimer);
|
||||||
|
this.disconnectTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
// Resume from paused state
|
// Resume from paused state
|
||||||
if (this.status === STATUS.PAUSED && currentSong.url === this.nowPlaying?.url) {
|
if (this.status === STATUS.PAUSED && currentSong.url === this.nowPlaying?.url) {
|
||||||
if (this.audioPlayer) {
|
if (this.audioPlayer) {
|
||||||
|
@ -206,12 +215,31 @@ export default class {
|
||||||
async forward(skip: number): Promise<void> {
|
async forward(skip: number): Promise<void> {
|
||||||
this.manualForward(skip);
|
this.manualForward(skip);
|
||||||
|
|
||||||
|
console.log(this.getCurrent());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.getCurrent() && this.status !== STATUS.PAUSED) {
|
if (this.getCurrent() && this.status !== STATUS.PAUSED) {
|
||||||
await this.play();
|
await this.play();
|
||||||
} else {
|
} else {
|
||||||
this.status = STATUS.PAUSED;
|
this.audioPlayer?.stop();
|
||||||
this.disconnect();
|
this.status = STATUS.IDLE;
|
||||||
|
|
||||||
|
const settings = await prisma.setting.findUnique({where: {guildId: this.guildId}});
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
throw new Error('Could not find settings for guild');
|
||||||
|
}
|
||||||
|
|
||||||
|
const {secondsToWaitAfterQueueEmpties} = settings;
|
||||||
|
if (secondsToWaitAfterQueueEmpties !== 0) {
|
||||||
|
this.disconnectTimer = setTimeout(() => {
|
||||||
|
// Make sure we are not accidentally playing
|
||||||
|
// when disconnecting
|
||||||
|
if (this.status === STATUS.IDLE) {
|
||||||
|
this.disconnect();
|
||||||
|
}
|
||||||
|
}, secondsToWaitAfterQueueEmpties * 1000);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.queuePosition--;
|
this.queuePosition--;
|
||||||
|
|
Loading…
Reference in a new issue