mirror of
https://github.com/BluemediaDev/muse.git
synced 2025-05-13 04:51:35 +02:00
Start migrating queue command
Also: make it persistent & updating, add buttons ✨
This commit is contained in:
parent
e965c02358
commit
0b20cb3982
11 changed files with 370 additions and 72 deletions
|
@ -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
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue