Configurable voice channel leave behavior (#514)

Co-authored-by: Max Isom <hi@maxisom.me>
This commit is contained in:
Johannes Vääräkangas 2022-02-12 04:05:02 +02:00 committed by GitHub
parent 8e5b3cfa43
commit 4dbb55a721
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 123 additions and 11 deletions

View file

@ -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).
## [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
### Changed

View file

@ -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;

View file

@ -1,6 +1,6 @@
{
"ignore": ["**/*.test.ts", "**/*.spec.ts", ".git", "node_modules"],
"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"
}

View file

@ -24,11 +24,13 @@ model KeyValueCache {
}
model Setting {
guildId String @id
playlistLimit Int @default(50)
roleId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
guildId String @id
playlistLimit Int @default(50)
secondsToWaitAfterQueueEmpties Int @default(30)
leaveIfNoListeners Boolean @default(true)
roleId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FavoriteQuery {

View file

@ -24,6 +24,21 @@ export default class implements Command {
.setName('role')
.setDescription('allowed role')
.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
.setName('get')
.setDescription('show all settings'));
@ -70,6 +85,40 @@ export default class implements Command {
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': {
const embed = new MessageEmbed().setTitle('Config');
@ -82,6 +131,10 @@ export default class implements Command {
const settingsToShow = {
'Playlist Limit': config.playlistLimit,
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 = '';

View file

@ -3,15 +3,23 @@ import container from '../inversify.config.js';
import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.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 player = playerManager.get(oldState.guild.id);
if (player.voiceConnection) {
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();
}
}

View file

@ -4,7 +4,7 @@ import {Except} from 'type-fest';
import shuffle from 'array-shuffle';
import {TYPES} from '../types.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 {prisma} from '../utils/db.js';
import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
@ -131,6 +131,9 @@ export default class AddQueryToQueue {
await interaction.editReply({
embeds: [buildPlayingMessageEmbed(player)],
});
} else if (player.status === STATUS.IDLE) {
// Player is idle, start playback instead
await player.play();
}
// Build response message

View file

@ -8,6 +8,7 @@ import shuffle from 'array-shuffle';
import {AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus} from '@discordjs/voice';
import FileCacheProvider from './file-cache.js';
import debug from '../utils/debug.js';
import {prisma} from '../utils/db.js';
export interface QueuedPlaylist {
title: string;
@ -29,6 +30,7 @@ export interface QueuedSong {
export enum STATUS {
PLAYING,
PAUSED,
IDLE,
}
export interface PlayerEvents {
@ -50,6 +52,7 @@ export default class {
private positionInSeconds = 0;
private readonly fileCache: FileCacheProvider;
private disconnectTimer: NodeJS.Timeout | null = null;
constructor(fileCache: FileCacheProvider, guildId: string) {
this.fileCache = fileCache;
@ -131,6 +134,12 @@ export default class {
throw new Error('Queue empty.');
}
// Cancel any pending idle disconnection
if (this.disconnectTimer) {
clearInterval(this.disconnectTimer);
this.disconnectTimer = null;
}
// Resume from paused state
if (this.status === STATUS.PAUSED && currentSong.url === this.nowPlaying?.url) {
if (this.audioPlayer) {
@ -206,12 +215,31 @@ export default class {
async forward(skip: number): Promise<void> {
this.manualForward(skip);
console.log(this.getCurrent());
try {
if (this.getCurrent() && this.status !== STATUS.PAUSED) {
await this.play();
} else {
this.status = STATUS.PAUSED;
this.disconnect();
this.audioPlayer?.stop();
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) {
this.queuePosition--;