From 7f39642c49b55dc1c29eb07962c79797eee66270 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Wed, 18 Mar 2020 22:29:43 -0500 Subject: [PATCH] Add queue output, various bug fixes --- src/commands/play.ts | 18 ++++---- src/commands/queue.ts | 55 ++++++++++++++++++++--- src/services/get-songs.ts | 4 +- src/services/natural-language-commands.ts | 7 ++- src/services/player.ts | 23 ++++++++-- src/services/queue.ts | 10 ++++- src/utils/get-progress-bar.ts | 15 +++++++ src/utils/time.ts | 15 +++++++ 8 files changed, 123 insertions(+), 24 deletions(-) create mode 100644 src/utils/get-progress-bar.ts create mode 100644 src/utils/time.ts diff --git a/src/commands/play.ts b/src/commands/play.ts index eceac94..eb972a8 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -48,11 +48,13 @@ export default class implements Command { } const queue = this.queueManager.get(msg.guild!.id); + const player = this.playerManager.get(msg.guild!.id); const queueOldSize = queue.size(); + const wasPlayingSong = queue.getCurrent() !== null; if (args.length === 0) { - if (this.playerManager.get(msg.guild!.id).status === STATUS.PLAYING) { + if (player.status === STATUS.PLAYING) { await res.stop(errorMsg('already playing, give me a song name')); return; } @@ -63,8 +65,8 @@ export default class implements Command { return; } - await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel); - await this.playerManager.get(msg.guild!.id).play(); + await player.connect(targetVoiceChannel); + await player.play(); await res.stop('the stop-and-go light is now green'); return; @@ -141,13 +143,13 @@ export default class implements Command { await res.stop(`u betcha, **${firstSong.title}** and ${newSongs.length - 1} other songs were added to the queue${extraMsg}`); } - if (this.playerManager.get(msg.guild!.id).voiceConnection === null) { - await this.playerManager.get(msg.guild!.id).connect(targetVoiceChannel); + if (player.voiceConnection === null) { + await player.connect(targetVoiceChannel); } - if (queueOldSize === 0) { - // Only auto-play if queue was empty before - await this.playerManager.get(msg.guild!.id).play(); + if (queueOldSize === 0 && !wasPlayingSong) { + // Only auto-play if queue was empty before and nothing was playing + await player.play(); } } } diff --git a/src/commands/queue.ts b/src/commands/queue.ts index 6c5cc63..1ec2016 100644 --- a/src/commands/queue.ts +++ b/src/commands/queue.ts @@ -1,8 +1,15 @@ -import {Message} from 'discord.js'; +import {Message, MessageEmbed} from 'discord.js'; import {TYPES} from '../types'; import {inject, injectable} from 'inversify'; import QueueManager from '../managers/queue'; +import PlayerManager from '../managers/player'; +import {STATUS} from '../services/player'; import Command from '.'; +import getProgressBar from '../utils/get-progress-bar'; +import errorMsg from '../utils/error-msg'; +import {prettyTime} from '../utils/time'; + +const PAGE_SIZE = 10; @injectable() export default class implements Command { @@ -13,16 +20,54 @@ export default class implements Command { ]; private readonly queueManager: QueueManager; + private readonly playerManager: PlayerManager; - constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager) { + constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager) { this.queueManager = queueManager; + this.playerManager = playerManager; } - public async execute(msg: Message, _: string []): Promise { + public async execute(msg: Message, args: string []): Promise { const queue = this.queueManager.get(msg.guild!.id); + const player = this.playerManager.get(msg.guild!.id); - await msg.channel.send('`' + JSON.stringify(queue.getCurrent()) + '`'); + const currentlyPlaying = queue.getCurrent(); - await msg.channel.send('`' + JSON.stringify(queue.get().slice(0, 10)) + '`'); + if (currentlyPlaying) { + const queueSize = queue.size(); + const queuePage = args[0] ? parseInt(args[0], 10) : 1; + + if (queuePage * PAGE_SIZE > queueSize && queuePage > Math.ceil((queueSize + 1) / PAGE_SIZE)) { + await msg.channel.send(errorMsg('the queue isn\'t that big')); + return; + } + + const embed = new MessageEmbed(); + + embed.setTitle(currentlyPlaying.title); + embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url}`); + embed.setFooter(`Source: ${currentlyPlaying.artist}`); + + let description = player.status === STATUS.PLAYING ? '⏚ī¸' : 'â–ļī¸'; + description += ' '; + description += getProgressBar(20, player.getPosition() / currentlyPlaying.length); + description += ' '; + description += `\`[${prettyTime(player.getPosition())}/${prettyTime(currentlyPlaying.length)}]\``; + description += ' 🔉'; + description += queue.isEmpty() ? '' : '\n\n**Next up:**'; + + embed.setDescription(description); + + const queuePageBegin = (queuePage - 1) * PAGE_SIZE; + const queuePageEnd = queuePageBegin + PAGE_SIZE; + + queue.get().slice(queuePageBegin, queuePageEnd).forEach((song, i) => { + embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false); + }); + + await msg.channel.send(embed); + } else { + await msg.channel.send('queue empty'); + } } } diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts index 347d9d6..08d5e90 100644 --- a/src/services/get-songs.ts +++ b/src/services/get-songs.ts @@ -100,7 +100,7 @@ export default class { case 'playlist': { const uri = parsed as spotifyURI.Playlist; - let [{body: playlistResponse}, {body: tracksResponse}] = await Promise.all([this.spotify.getPlaylist(uri.id), this.spotify.getPlaylistTracks(uri.id, {limit: 1})]); + let [{body: playlistResponse}, {body: tracksResponse}] = await Promise.all([this.spotify.getPlaylist(uri.id), this.spotify.getPlaylistTracks(uri.id, {limit: 50})]); playlist = {title: playlistResponse.name, source: playlistResponse.href}; @@ -109,7 +109,7 @@ export default class { while (tracksResponse.next) { // eslint-disable-next-line no-await-in-loop ({body: tracksResponse} = await this.spotify.getPlaylistTracks(uri.id, { - limit: parseInt(new URL(tracksResponse.next).searchParams.get('limit') ?? '1', 10), + limit: parseInt(new URL(tracksResponse.next).searchParams.get('limit') ?? '50', 10), offset: parseInt(new URL(tracksResponse.next).searchParams.get('offset') ?? '0', 10) })); diff --git a/src/services/natural-language-commands.ts b/src/services/natural-language-commands.ts index 5d5b7d5..af5ae0c 100644 --- a/src/services/natural-language-commands.ts +++ b/src/services/natural-language-commands.ts @@ -39,7 +39,7 @@ export default class { await player.connect(channel); } - const isPlaying = queue.getCurrent(); + const isPlaying = queue.getCurrent() !== null; let oldPosition = 0; queue.add({title: 'GO PACKERS!', artist: 'Unknown', url: 'https://www.youtube.com/watch?v=qkdtID7mY3E', length: 204, playlist: null, isLive: false}); @@ -54,12 +54,11 @@ export default class { return new Promise((resolve, reject) => { try { setTimeout(async () => { - if (isPlaying) { - queue.back(); + queue.removeCurrent(); + if (isPlaying) { await player.seek(oldPosition); } else { - queue.forward(); player.disconnect(); } diff --git a/src/services/player.ts b/src/services/player.ts index e4d54a6..19e01bb 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -21,6 +21,7 @@ export default class { private dispatcher: StreamDispatcher | null = null; private nowPlaying: QueuedSong | null = null; private playPositionInterval: NodeJS.Timeout | undefined; + private lastSongURL = ''; private positionInSeconds = 0; @@ -100,6 +101,7 @@ export default class { if (this.dispatcher) { this.dispatcher.resume(); this.status = STATUS.PLAYING; + this.startTrackingPosition(); return; } @@ -115,7 +117,13 @@ export default class { this.status = STATUS.PLAYING; this.nowPlaying = currentSong; - this.startTrackingPosition(); + if (currentSong.url === this.lastSongURL) { + this.startTrackingPosition(); + } else { + // Reset position counter + this.startTrackingPosition(0); + this.lastSongURL = currentSong.url; + } } pause(): void { @@ -235,7 +243,7 @@ export default class { } private startTrackingPosition(initalPosition?: number): void { - if (initalPosition) { + if (initalPosition !== undefined) { this.positionInSeconds = initalPosition; } @@ -270,9 +278,16 @@ export default class { this.dispatcher.on('speaking', async isSpeaking => { // Automatically advance queued song at end if (!isSpeaking && this.status === STATUS.PLAYING) { - if (this.queue.get().length > 0) { + if (this.queue.size() >= 0) { this.queue.forward(); - await this.play(); + + this.positionInSeconds = 0; + + if (this.queue.getCurrent()) { + await this.play(); + } else { + this.status = STATUS.PAUSED; + } } } }); diff --git a/src/services/queue.ts b/src/services/queue.ts index 5db3b74..c5c8722 100644 --- a/src/services/queue.ts +++ b/src/services/queue.ts @@ -52,7 +52,7 @@ export default class { this.queue.push(song); } else { // Not from playlist, add immediately - let insertAt = 0; + let insertAt = this.position; // Loop until playlist song this.queue.some(song => { @@ -83,6 +83,14 @@ export default class { this.queue = newQueue; } + removeCurrent(): void { + this.queue = [...this.queue.slice(0, this.position), ...this.queue.slice(this.position + 1)]; + + if (this.position !== 0) { + this.position--; + } + } + size(): number { return this.get().length; } diff --git a/src/utils/get-progress-bar.ts b/src/utils/get-progress-bar.ts new file mode 100644 index 0000000..7db3dd0 --- /dev/null +++ b/src/utils/get-progress-bar.ts @@ -0,0 +1,15 @@ +export default (width: number, progress: number): string => { + const dotPosition = Math.floor(width * progress); + + let res = ''; + + for (let i = 0; i < width; i++) { + if (i === dotPosition) { + res += '🔘'; + } else { + res += 'â–Ŧ'; + } + } + + return res; +}; diff --git a/src/utils/time.ts b/src/utils/time.ts new file mode 100644 index 0000000..a54bfa7 --- /dev/null +++ b/src/utils/time.ts @@ -0,0 +1,15 @@ +export const prettyTime = (seconds: number): string => { + const nSeconds = seconds % 60; + const nMinutes = Math.floor(seconds / 60); + const nHours = Math.floor(nMinutes / 60); + + let res = ''; + + if (nHours !== 0) { + res += `${Math.round(nHours).toString().padStart(2, '0')}:`; + } + + res += `${Math.round(nMinutes).toString().padStart(2, '0')}:${Math.round(nSeconds).toString().padStart(2, '0')}`; + + return res; +};