mirror of
https://github.com/BluemediaGER/muse.git
synced 2024-11-23 01:05:30 +01:00
Start migrating queue command
Also: make it persistent & updating, add buttons ✨
This commit is contained in:
parent
e965c02358
commit
0b20cb3982
|
@ -48,6 +48,7 @@
|
|||
"nodemon": "^2.0.7",
|
||||
"ts-node": "^10.4.0",
|
||||
"type-fest": "^2.5.4",
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typescript": "^4.5.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
|
42
src/bot.ts
42
src/bot.ts
|
@ -18,12 +18,14 @@ import {Routes} from 'discord-api-types/v9';
|
|||
export default class {
|
||||
private readonly client: Client;
|
||||
private readonly token: string;
|
||||
private readonly commands!: Collection<string, Command>;
|
||||
private readonly commandsByName!: Collection<string, Command>;
|
||||
private readonly commandsByButtonId!: Collection<string, Command>;
|
||||
|
||||
constructor(@inject(TYPES.Client) client: Client, @inject(TYPES.Config) config: Config) {
|
||||
this.client = client;
|
||||
this.token = config.DISCORD_TOKEN;
|
||||
this.commands = new Collection();
|
||||
this.commandsByName = new Collection();
|
||||
this.commandsByButtonId = new Collection();
|
||||
}
|
||||
|
||||
public async listen(): Promise<void> {
|
||||
|
@ -31,7 +33,11 @@ export default class {
|
|||
container.getAll<Command>(TYPES.Command).forEach(command => {
|
||||
// TODO: remove !
|
||||
if (command.slashCommand?.name) {
|
||||
this.commands.set(command.slashCommand.name, command);
|
||||
this.commandsByName.set(command.slashCommand.name, command);
|
||||
}
|
||||
|
||||
if (command.handledButtonIds) {
|
||||
command.handledButtonIds.forEach(id => this.commandsByButtonId.set(id, command));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -41,7 +47,7 @@ export default class {
|
|||
return;
|
||||
}
|
||||
|
||||
const command = this.commands.get(interaction.commandName);
|
||||
const command = this.commandsByName.get(interaction.commandName);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
|
@ -72,6 +78,32 @@ export default class {
|
|||
}
|
||||
});
|
||||
|
||||
this.client.on('interactionCreate', async interaction => {
|
||||
if (!interaction.isButton()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = this.commandsByButtonId.get(interaction.customId);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (command.handleButtonInteraction) {
|
||||
await command.handleButtonInteraction(interaction);
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
debug(error);
|
||||
|
||||
if (interaction.replied || interaction.deferred) {
|
||||
await interaction.editReply(errorMsg('something went wrong'));
|
||||
} else {
|
||||
await interaction.reply({content: errorMsg(error as Error), ephemeral: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const spinner = ora('📡 connecting to Discord...').start();
|
||||
|
||||
this.client.once('ready', () => {
|
||||
|
@ -94,7 +126,7 @@ export default class {
|
|||
await rest.put(
|
||||
Routes.applicationGuildCommands(this.client.user!.id, this.client.guilds.cache.first()!.id),
|
||||
// TODO: remove
|
||||
{body: this.commands.map(command => command.slashCommand ? command.slashCommand.toJSON() : null)},
|
||||
{body: this.commandsByName.map(command => command.slashCommand ? command.slashCommand.toJSON() : null)},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||
import {CommandInteraction} from 'discord.js';
|
||||
import {ButtonInteraction, CommandInteraction} from 'discord.js';
|
||||
|
||||
export default interface Command {
|
||||
export default class Command {
|
||||
// TODO: remove
|
||||
name?: string;
|
||||
aliases?: string[];
|
||||
examples?: string[][];
|
||||
readonly slashCommand?: Partial<SlashCommandBuilder> & Pick<SlashCommandBuilder, 'toJSON'>;
|
||||
requiresVC?: boolean;
|
||||
readonly handledButtonIds?: readonly string[];
|
||||
readonly requiresVC?: boolean;
|
||||
executeFromInteraction?: (interaction: CommandInteraction) => Promise<void>;
|
||||
handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import GetSongs from '../services/get-songs.js';
|
|||
export default class implements Command {
|
||||
public readonly slashCommand = new SlashCommandBuilder()
|
||||
.setName('play')
|
||||
// TODO: make sure verb tense is consistent between all command descriptions
|
||||
.setDescription('play a song or resume playback')
|
||||
.addStringOption(option => option
|
||||
.setName('query')
|
||||
|
|
|
@ -1,82 +1,80 @@
|
|||
import {Message, MessageEmbed} from 'discord.js';
|
||||
import getYouTubeID from 'get-youtube-id';
|
||||
import {ButtonInteraction, CommandInteraction} from 'discord.js';
|
||||
import {SlashCommandBuilder} from '@discordjs/builders';
|
||||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import {STATUS} from '../services/player.js';
|
||||
import UpdatingQueueEmbedManager from '../managers/updating-queue-embed.js';
|
||||
import {BUTTON_IDS} from '../services/updating-queue-embed.js';
|
||||
import Command from '.';
|
||||
import getProgressBar from '../utils/get-progress-bar.js';
|
||||
import errorMsg from '../utils/error-msg.js';
|
||||
import {prettyTime} from '../utils/time.js';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
@injectable()
|
||||
export default class implements Command {
|
||||
public name = 'queue';
|
||||
public aliases = ['q'];
|
||||
public examples = [
|
||||
['queue', 'shows current queue'],
|
||||
['queue 2', 'shows second page of queue'],
|
||||
];
|
||||
public readonly slashCommand = new SlashCommandBuilder()
|
||||
.setName('queue')
|
||||
.setDescription('show the current queue');
|
||||
|
||||
public readonly handledButtonIds = Object.values(BUTTON_IDS);
|
||||
|
||||
private readonly playerManager: PlayerManager;
|
||||
private readonly updatingQueueEmbedManager: UpdatingQueueEmbedManager;
|
||||
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Managers.UpdatingQueueEmbed) updatingQueueEmbedManager: UpdatingQueueEmbedManager) {
|
||||
this.playerManager = playerManager;
|
||||
this.updatingQueueEmbedManager = updatingQueueEmbedManager;
|
||||
}
|
||||
|
||||
public async execute(msg: Message, args: string []): Promise<void> {
|
||||
const player = this.playerManager.get(msg.guild!.id);
|
||||
public async executeFromInteraction(interaction: CommandInteraction) {
|
||||
const embed = this.updatingQueueEmbedManager.get(interaction.guild!.id);
|
||||
|
||||
const currentlyPlaying = player.getCurrent();
|
||||
|
||||
if (currentlyPlaying) {
|
||||
const queueSize = player.queueSize();
|
||||
const queuePage = args[0] ? parseInt(args[0], 10) : 1;
|
||||
|
||||
const maxQueuePage = Math.ceil((queueSize + 1) / PAGE_SIZE);
|
||||
|
||||
if (queuePage > maxQueuePage) {
|
||||
await msg.channel.send(errorMsg('the queue isn\'t that big'));
|
||||
return;
|
||||
await embed.createFromInteraction(interaction);
|
||||
}
|
||||
|
||||
const embed = new MessageEmbed();
|
||||
public async handleButtonInteraction(interaction: ButtonInteraction) {
|
||||
const player = this.playerManager.get(interaction.guild!.id);
|
||||
const embed = this.updatingQueueEmbedManager.get(interaction.guild!.id);
|
||||
|
||||
embed.setTitle(currentlyPlaying.title);
|
||||
embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url.length === 11 ? currentlyPlaying.url : getYouTubeID(currentlyPlaying.url) ?? ''}`);
|
||||
const buttonId = interaction.customId as keyof typeof this.handledButtonIds;
|
||||
|
||||
let description = player.status === STATUS.PLAYING ? '⏹️' : '▶️';
|
||||
description += ' ';
|
||||
description += getProgressBar(20, player.getPosition() / currentlyPlaying.length);
|
||||
description += ' ';
|
||||
description += `\`[${prettyTime(player.getPosition())}/${currentlyPlaying.isLive ? 'live' : prettyTime(currentlyPlaying.length)}]\``;
|
||||
description += ' 🔉';
|
||||
description += player.isQueueEmpty() ? '' : '\n\n**Next up:**';
|
||||
// Not entirely sure why this is necessary.
|
||||
// We don't wait for the Promise to resolve here to avoid blocking the
|
||||
// main logic. However, we need to wait for the Promise to be resolved before
|
||||
// throwing as otherwise a race condition pops up when bot.ts tries updating
|
||||
// the interaction.
|
||||
const deferedUpdatePromise = interaction.deferUpdate();
|
||||
|
||||
embed.setDescription(description);
|
||||
try {
|
||||
switch (buttonId) {
|
||||
case BUTTON_IDS.TRACK_BACK:
|
||||
await player.back();
|
||||
break;
|
||||
|
||||
let footer = `Source: ${currentlyPlaying.artist}`;
|
||||
case BUTTON_IDS.TRACK_FORWARD:
|
||||
await player.forward(1);
|
||||
break;
|
||||
|
||||
if (currentlyPlaying.playlist) {
|
||||
footer += ` (${currentlyPlaying.playlist.title})`;
|
||||
case BUTTON_IDS.PAUSE:
|
||||
player.pause();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PLAY:
|
||||
await player.play();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PAGE_BACK:
|
||||
await embed.pageBack();
|
||||
break;
|
||||
|
||||
case BUTTON_IDS.PAGE_FORWARD:
|
||||
await embed.pageForward();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Error('unknown customId');
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
await deferedUpdatePromise;
|
||||
|
||||
embed.setFooter(footer);
|
||||
|
||||
const queuePageBegin = (queuePage - 1) * PAGE_SIZE;
|
||||
const queuePageEnd = queuePageBegin + PAGE_SIZE;
|
||||
|
||||
player.getQueue().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
|
||||
embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false);
|
||||
});
|
||||
|
||||
embed.addField('Page', `${queuePage} out of ${maxQueuePage}`, false);
|
||||
|
||||
await msg.channel.send({embeds: [embed]});
|
||||
} else {
|
||||
await msg.channel.send('queue empty');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import ConfigProvider from './services/config.js';
|
|||
|
||||
// Managers
|
||||
import PlayerManager from './managers/player.js';
|
||||
import UpdatingQueueEmbed from './managers/updating-queue-embed.js';
|
||||
|
||||
// Helpers
|
||||
import GetSongs from './services/get-songs.js';
|
||||
|
@ -46,6 +47,7 @@ container.bind<Client>(TYPES.Client).toConstantValue(new Client({intents}));
|
|||
|
||||
// Managers
|
||||
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
|
||||
container.bind<UpdatingQueueEmbed>(TYPES.Managers.UpdatingQueueEmbed).to(UpdatingQueueEmbed).inSingletonScope();
|
||||
|
||||
// Helpers
|
||||
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
|
||||
|
|
33
src/managers/updating-queue-embed.ts
Normal file
33
src/managers/updating-queue-embed.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import {inject, injectable} from 'inversify';
|
||||
import {TYPES} from '../types.js';
|
||||
import PlayerManager from '../managers/player.js';
|
||||
import UpdatingQueueEmbed from '../services/updating-queue-embed.js';
|
||||
|
||||
@injectable()
|
||||
export default class {
|
||||
private readonly embedsByGuild: Map<string, UpdatingQueueEmbed>;
|
||||
private readonly playerManager: PlayerManager;
|
||||
|
||||
constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
|
||||
this.embedsByGuild = new Map();
|
||||
this.playerManager = playerManager;
|
||||
}
|
||||
|
||||
get(guildId: string): UpdatingQueueEmbed {
|
||||
let embed = this.embedsByGuild.get(guildId);
|
||||
|
||||
if (!embed) {
|
||||
const player = this.playerManager.get(guildId);
|
||||
|
||||
if (!player) {
|
||||
throw new Error('Player does not exist for guild.');
|
||||
}
|
||||
|
||||
embed = new UpdatingQueueEmbed(player);
|
||||
|
||||
this.embedsByGuild.set(guildId, embed);
|
||||
}
|
||||
|
||||
return embed;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
import {VoiceChannel, Snowflake, Client, TextChannel} from 'discord.js';
|
||||
import {Readable} from 'stream';
|
||||
import EventEmitter from 'events';
|
||||
import TypedEmitter from 'typed-emitter';
|
||||
import hasha from 'hasha';
|
||||
import ytdl from 'ytdl-core';
|
||||
import {WriteStream} from 'fs-capacitor';
|
||||
|
@ -29,8 +31,11 @@ export enum STATUS {
|
|||
PAUSED,
|
||||
}
|
||||
|
||||
export default class {
|
||||
public status = STATUS.PAUSED;
|
||||
export interface PlayerEvents {
|
||||
statusChange: (oldStatus: STATUS, newStatus: STATUS) => void;
|
||||
}
|
||||
|
||||
export default class extends (EventEmitter as new () => TypedEmitter<PlayerEvents>) {
|
||||
public voiceConnection: VoiceConnection | null = null;
|
||||
private queue: QueuedSong[] = [];
|
||||
private queuePosition = 0;
|
||||
|
@ -40,11 +45,14 @@ export default class {
|
|||
private lastSongURL = '';
|
||||
|
||||
private positionInSeconds = 0;
|
||||
private internalStatus = STATUS.PAUSED;
|
||||
|
||||
private readonly discordClient: Client;
|
||||
private readonly fileCache: FileCacheProvider;
|
||||
|
||||
constructor(client: Client, fileCache: FileCacheProvider) {
|
||||
// eslint-disable-next-line constructor-super
|
||||
super();
|
||||
this.discordClient = client;
|
||||
this.fileCache = fileCache;
|
||||
}
|
||||
|
@ -203,8 +211,12 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
canGoForward(skip: number) {
|
||||
return (this.queuePosition + skip - 1) < this.queue.length;
|
||||
}
|
||||
|
||||
manualForward(skip: number): void {
|
||||
if ((this.queuePosition + skip - 1) < this.queue.length) {
|
||||
if (this.canGoForward(skip)) {
|
||||
this.queuePosition += skip;
|
||||
this.positionInSeconds = 0;
|
||||
this.stopTrackingPosition();
|
||||
|
@ -213,8 +225,12 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
canGoBack() {
|
||||
return this.queuePosition - 1 >= 0;
|
||||
}
|
||||
|
||||
async back(): Promise<void> {
|
||||
if (this.queuePosition - 1 >= 0) {
|
||||
if (this.canGoBack()) {
|
||||
this.queuePosition--;
|
||||
this.positionInSeconds = 0;
|
||||
this.stopTrackingPosition();
|
||||
|
@ -290,6 +306,17 @@ export default class {
|
|||
return this.queueSize() === 0;
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.internalStatus;
|
||||
}
|
||||
|
||||
set status(newStatus: STATUS) {
|
||||
const previousStatus = this.internalStatus;
|
||||
this.internalStatus = newStatus;
|
||||
|
||||
this.emit('statusChange', previousStatus, newStatus);
|
||||
}
|
||||
|
||||
private getHashForCache(url: string): string {
|
||||
return hasha(url);
|
||||
}
|
||||
|
|
196
src/services/updating-queue-embed.ts
Normal file
196
src/services/updating-queue-embed.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import {CommandInteraction, MessageActionRow, MessageButton, MessageEmbed} from 'discord.js';
|
||||
import getYouTubeID from 'get-youtube-id';
|
||||
import getProgressBar from '../utils/get-progress-bar.js';
|
||||
import {prettyTime} from '../utils/time.js';
|
||||
import Player, {STATUS} from './player.js';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const REFRESH_INTERVAL_MS = 5 * 1000;
|
||||
|
||||
export enum BUTTON_IDS {
|
||||
PAGE_BACK = 'page-back',
|
||||
PAGE_FORWARD = 'page-forward',
|
||||
TRACK_BACK = 'track-back',
|
||||
TRACK_FORWARD = 'track-forward',
|
||||
PAUSE = 'pause',
|
||||
PLAY = 'play',
|
||||
}
|
||||
|
||||
export default class {
|
||||
private readonly player: Player;
|
||||
private interaction?: CommandInteraction;
|
||||
|
||||
// 1-indexed
|
||||
private currentPage = 1;
|
||||
|
||||
private refreshTimeout?: NodeJS.Timeout;
|
||||
|
||||
constructor(player: Player) {
|
||||
this.player = player;
|
||||
|
||||
this.addEventHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates & replies with a new embed from the given interaction.
|
||||
* Starts updating the embed at a regular interval.
|
||||
* Can be called multiple times within the lifecycle of this class.
|
||||
* Calling this method will make it forgot the previous interaction & reply.
|
||||
* @param interaction
|
||||
*/
|
||||
async createFromInteraction(interaction: CommandInteraction) {
|
||||
this.interaction = interaction;
|
||||
this.currentPage = 1;
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [this.buildEmbed()],
|
||||
components: this.buildButtons(this.player),
|
||||
});
|
||||
|
||||
if (!this.refreshTimeout) {
|
||||
this.refreshTimeout = setInterval(async () => this.update(), REFRESH_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
async update(shouldResetPage = false) {
|
||||
if (shouldResetPage) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
|
||||
await this.interaction?.editReply({
|
||||
embeds: [this.buildEmbed()],
|
||||
components: this.buildButtons(this.player),
|
||||
});
|
||||
}
|
||||
|
||||
async pageBack() {
|
||||
if (this.currentPage > 1) {
|
||||
this.currentPage--;
|
||||
}
|
||||
|
||||
await this.update();
|
||||
}
|
||||
|
||||
async pageForward() {
|
||||
if (this.currentPage < this.getMaxPage()) {
|
||||
this.currentPage++;
|
||||
}
|
||||
|
||||
await this.update();
|
||||
}
|
||||
|
||||
private buildButtons(player: Player): MessageActionRow[] {
|
||||
const queuePageControls = new MessageActionRow()
|
||||
.addComponents(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAGE_BACK)
|
||||
.setStyle('SECONDARY')
|
||||
.setDisabled(this.currentPage === 1)
|
||||
.setEmoji('⬅️'),
|
||||
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAGE_FORWARD)
|
||||
.setStyle('SECONDARY')
|
||||
.setDisabled(this.currentPage >= this.getMaxPage())
|
||||
.setEmoji('➡️'),
|
||||
);
|
||||
|
||||
const components = [];
|
||||
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.TRACK_BACK)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.canGoBack())
|
||||
.setEmoji('⏮'));
|
||||
|
||||
if (player.status === STATUS.PLAYING) {
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PAUSE)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.getCurrent())
|
||||
.setEmoji('⏸️'));
|
||||
} else {
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.PLAY)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.getCurrent())
|
||||
.setEmoji('▶️'));
|
||||
}
|
||||
|
||||
components.push(
|
||||
new MessageButton()
|
||||
.setCustomId(BUTTON_IDS.TRACK_FORWARD)
|
||||
.setStyle('PRIMARY')
|
||||
.setDisabled(!player.canGoForward(1))
|
||||
.setEmoji('⏭'),
|
||||
);
|
||||
|
||||
const playerControls = new MessageActionRow().addComponents(components);
|
||||
|
||||
return [queuePageControls, playerControls];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an embed for the current page of the queue.
|
||||
* @returns MessageEmbed
|
||||
*/
|
||||
private buildEmbed() {
|
||||
const currentlyPlaying = this.player.getCurrent();
|
||||
|
||||
if (!currentlyPlaying) {
|
||||
throw new Error('queue is empty');
|
||||
}
|
||||
|
||||
const queueSize = this.player.queueSize();
|
||||
|
||||
if (this.currentPage > this.getMaxPage()) {
|
||||
throw new Error('the queue isn\'t that big');
|
||||
}
|
||||
|
||||
const embed = new MessageEmbed();
|
||||
|
||||
embed.setTitle(currentlyPlaying.title);
|
||||
embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url.length === 11 ? currentlyPlaying.url : getYouTubeID(currentlyPlaying.url) ?? ''}`);
|
||||
|
||||
let description = getProgressBar(20, this.player.getPosition() / currentlyPlaying.length);
|
||||
description += ' ';
|
||||
description += `\`[${prettyTime(this.player.getPosition())}/${currentlyPlaying.isLive ? 'live' : prettyTime(currentlyPlaying.length)}]\``;
|
||||
description += ' 🔉';
|
||||
description += this.player.isQueueEmpty() ? '' : '\n\n**Next up:**';
|
||||
|
||||
embed.setDescription(description);
|
||||
|
||||
let footer = `Source: ${currentlyPlaying.artist}`;
|
||||
|
||||
if (currentlyPlaying.playlist) {
|
||||
footer += ` (${currentlyPlaying.playlist.title})`;
|
||||
}
|
||||
|
||||
embed.setFooter(footer);
|
||||
|
||||
const queuePageBegin = (this.currentPage - 1) * PAGE_SIZE;
|
||||
const queuePageEnd = queuePageBegin + PAGE_SIZE;
|
||||
|
||||
this.player.getQueue().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
|
||||
embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false);
|
||||
});
|
||||
|
||||
embed.addField('Page', `${this.currentPage} out of ${this.getMaxPage()}`, false);
|
||||
|
||||
return embed;
|
||||
}
|
||||
|
||||
private getMaxPage() {
|
||||
return Math.ceil((this.player.queueSize() + 1) / PAGE_SIZE);
|
||||
}
|
||||
|
||||
private addEventHandlers() {
|
||||
this.player.on('statusChange', async () => this.update(true));
|
||||
|
||||
// TODO: also update on other player events
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ export const TYPES = {
|
|||
ThirdParty: Symbol('ThirdParty'),
|
||||
Managers: {
|
||||
Player: Symbol('PlayerManager'),
|
||||
UpdatingQueueEmbed: Symbol('UpdatingQueueEmbed'),
|
||||
},
|
||||
Services: {
|
||||
GetSongs: Symbol('GetSongs'),
|
||||
|
|
|
@ -3496,6 +3496,11 @@ type-fest@^2.5.4:
|
|||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.5.4.tgz#1613bf29a172ff1c66c29325466af9096fe505b5"
|
||||
integrity sha512-zyPomVvb6u7+gJ/GPYUH6/nLDNiTtVOqXVUHtxFv5PmZQh6skgfeRtFYzWC01T5KeNWNIx5/0P111rKFLlkFvA==
|
||||
|
||||
typed-emitter@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/typed-emitter/-/typed-emitter-1.4.0.tgz#38c6bf1224e764906bb20cb0b458fa914100607c"
|
||||
integrity sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg==
|
||||
|
||||
typedarray-to-buffer@^3.1.5:
|
||||
version "3.1.5"
|
||||
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"
|
||||
|
|
Loading…
Reference in a new issue