aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Isom <[email protected]>2020-03-20 20:47:04 -0500
committerMax Isom <[email protected]>2020-03-20 20:47:04 -0500
commit9c91ce1a13cad701bc8876fd07ac4d513522a215 (patch)
tree582d5c7a17f2f476c563fd5aa635db02bb0c9394 /src
parent646f030781df0f4fe03437d0ed2294a10fa6c177 (diff)
downloadmuse-9c91ce1a13cad701bc8876fd07ac4d513522a215.tar.xz
muse-9c91ce1a13cad701bc8876fd07ac4d513522a215.zip
Merge Player and Queue services
Diffstat (limited to 'src')
-rw-r--r--src/bot.ts4
-rw-r--r--src/commands/clear.ts10
-rw-r--r--src/commands/fseek.ts13
-rw-r--r--src/commands/play.ts24
-rw-r--r--src/commands/queue.ts28
-rw-r--r--src/commands/seek.ts11
-rw-r--r--src/commands/shuffle.ts15
-rw-r--r--src/commands/skip.ts24
-rw-r--r--src/commands/unskip.ts9
-rw-r--r--src/inversify.config.ts2
-rw-r--r--src/managers/player.ts7
-rw-r--r--src/managers/queue.ts23
-rw-r--r--src/services/get-songs.ts5
-rw-r--r--src/services/natural-language-commands.ts17
-rw-r--r--src/services/player.ts183
-rw-r--r--src/services/queue.ts97
-rw-r--r--src/types.ts3
-rw-r--r--src/utils/loading-message.ts8
-rw-r--r--src/utils/time.ts2
19 files changed, 230 insertions, 255 deletions
diff --git a/src/bot.ts b/src/bot.ts
index 434a514..4ddbfcb 100644
--- a/src/bot.ts
+++ b/src/bot.ts
@@ -90,8 +90,8 @@ export default class {
await handler.execute(msg, args);
} catch (error) {
- console.error(error);
- await msg.channel.send(errorMsg('¯\\_(ツ)_/¯'));
+ debug(error);
+ await msg.channel.send(errorMsg(error.message.toLowerCase()));
}
});
diff --git a/src/commands/clear.ts b/src/commands/clear.ts
index a585af4..f9b1853 100644
--- a/src/commands/clear.ts
+++ b/src/commands/clear.ts
@@ -1,7 +1,7 @@
import {Message} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
-import QueueManager from '../managers/queue';
+import PlayerManager from '../managers/player';
import Command from '.';
@injectable()
@@ -14,14 +14,14 @@ export default class implements Command {
public requiresVC = true;
- private readonly queueManager: QueueManager;
+ private readonly playerManager: PlayerManager;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
+ this.playerManager = playerManager;
}
public async execute(msg: Message, _: string []): Promise<void> {
- this.queueManager.get(msg.guild!.id).clear();
+ this.playerManager.get(msg.guild!.id).clear();
await msg.channel.send('clearer than a field after a fresh harvest');
}
diff --git a/src/commands/fseek.ts b/src/commands/fseek.ts
index 584c581..fe8ee93 100644
--- a/src/commands/fseek.ts
+++ b/src/commands/fseek.ts
@@ -2,7 +2,6 @@ import {Message, TextChannel} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player';
-import QueueManager from '../managers/queue';
import LoadingMessage from '../utils/loading-message';
import errorMsg from '../utils/error-msg';
import Command from '.';
@@ -18,17 +17,15 @@ export default class implements Command {
public requiresVC = true;
private readonly playerManager: PlayerManager;
- private readonly queueManager: QueueManager;
- constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Managers.Queue) queueManager: QueueManager) {
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
- this.queueManager = queueManager;
}
public async execute(msg: Message, args: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id);
+ const player = this.playerManager.get(msg.guild!.id);
- const currentSong = queue.getCurrent();
+ const currentSong = player.getCurrent();
if (!currentSong) {
await msg.channel.send(errorMsg('nothing is playing'));
@@ -42,7 +39,7 @@ export default class implements Command {
const seekTime = parseInt(args[0], 10);
- if (seekTime + this.playerManager.get(msg.guild!.id).getPosition() > currentSong.length) {
+ if (seekTime + player.getPosition() > currentSong.length) {
await msg.channel.send(errorMsg('can\'t seek past the end of the song'));
return;
}
@@ -52,7 +49,7 @@ export default class implements Command {
await loading.start();
try {
- await this.playerManager.get(msg.guild!.id).forwardSeek(seekTime);
+ await player.forwardSeek(seekTime);
await loading.stop();
} catch (error) {
diff --git a/src/commands/play.ts b/src/commands/play.ts
index 30f6314..10db7d2 100644
--- a/src/commands/play.ts
+++ b/src/commands/play.ts
@@ -2,9 +2,8 @@ import {TextChannel, Message} from 'discord.js';
import {URL} from 'url';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
-import {QueuedSong} from '../services/queue';
+import {QueuedSong} from '../services/player';
import {STATUS} from '../services/player';
-import QueueManager from '../managers/queue';
import PlayerManager from '../managers/player';
import {getMostPopularVoiceChannel} from '../utils/channels';
import LoadingMessage from '../utils/loading-message';
@@ -28,12 +27,10 @@ export default class implements Command {
public requiresVC = true;
- private readonly queueManager: QueueManager;
private readonly playerManager: PlayerManager;
private readonly getSongs: GetSongs;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Services.GetSongs) getSongs: GetSongs) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Services.GetSongs) getSongs: GetSongs) {
this.playerManager = playerManager;
this.getSongs = getSongs;
}
@@ -44,11 +41,10 @@ export default class implements Command {
const res = new LoadingMessage(msg.channel as TextChannel);
await res.start();
- 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;
+ const queueOldSize = player.queueSize();
+ const wasPlayingSong = player.getCurrent() !== null;
if (args.length === 0) {
if (player.status === STATUS.PLAYING) {
@@ -57,7 +53,7 @@ export default class implements Command {
}
// Must be resuming play
- if (queue.get().length === 0 && !queue.getCurrent()) {
+ if (!wasPlayingSong) {
await res.stop(errorMsg('nothing to play'));
return;
}
@@ -101,11 +97,15 @@ export default class implements Command {
extraMsg = 'a random sample of 50 songs was taken';
}
+ if (totalSongs > 50 && nSongsNotFound !== 0) {
+ extraMsg += ' and ';
+ }
+
if (nSongsNotFound !== 0) {
if (nSongsNotFound === 1) {
- extraMsg += 'and 1 song was not found';
+ extraMsg += '1 song was not found';
} else {
- extraMsg += `and ${nSongsNotFound.toString()} songs were not found`;
+ extraMsg += `${nSongsNotFound.toString()} songs were not found`;
}
}
@@ -130,7 +130,7 @@ export default class implements Command {
return;
}
- newSongs.forEach(song => queue.add(song));
+ newSongs.forEach(song => player.add(song));
const firstSong = newSongs[0];
diff --git a/src/commands/queue.ts b/src/commands/queue.ts
index 1ec2016..67f88a4 100644
--- a/src/commands/queue.ts
+++ b/src/commands/queue.ts
@@ -1,13 +1,13 @@
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';
+import getYouTubeID from 'get-youtube-id';
const PAGE_SIZE = 10;
@@ -19,22 +19,19 @@ export default class implements Command {
['queue', 'shows current queue']
];
- private readonly queueManager: QueueManager;
private readonly playerManager: PlayerManager;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
}
public async execute(msg: Message, args: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id);
const player = this.playerManager.get(msg.guild!.id);
- const currentlyPlaying = queue.getCurrent();
+ const currentlyPlaying = player.getCurrent();
if (currentlyPlaying) {
- const queueSize = queue.size();
+ const queueSize = player.queueSize();
const queuePage = args[0] ? parseInt(args[0], 10) : 1;
if (queuePage * PAGE_SIZE > queueSize && queuePage > Math.ceil((queueSize + 1) / PAGE_SIZE)) {
@@ -45,23 +42,30 @@ export default class implements Command {
const embed = new MessageEmbed();
embed.setTitle(currentlyPlaying.title);
- embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url}`);
- embed.setFooter(`Source: ${currentlyPlaying.artist}`);
+ embed.setURL(`https://www.youtube.com/watch?v=${currentlyPlaying.url.length === 11 ? currentlyPlaying.url : getYouTubeID(currentlyPlaying.url) ?? ''}`);
let description = player.status === STATUS.PLAYING ? 'âšī¸' : 'â–ļī¸';
description += ' ';
description += getProgressBar(20, player.getPosition() / currentlyPlaying.length);
description += ' ';
- description += `\`[${prettyTime(player.getPosition())}/${prettyTime(currentlyPlaying.length)}]\``;
+ description += `\`[${prettyTime(player.getPosition())}/${currentlyPlaying.isLive ? 'live' : prettyTime(currentlyPlaying.length)}]\``;
description += ' 🔉';
- description += queue.isEmpty() ? '' : '\n\n**Next up:**';
+ description += 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 = (queuePage - 1) * PAGE_SIZE;
const queuePageEnd = queuePageBegin + PAGE_SIZE;
- queue.get().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
+ player.getQueue().slice(queuePageBegin, queuePageEnd).forEach((song, i) => {
embed.addField(`${(i + 1 + queuePageBegin).toString()}/${queueSize.toString()}`, song.title, false);
});
diff --git a/src/commands/seek.ts b/src/commands/seek.ts
index 24fde7c..89e174e 100644
--- a/src/commands/seek.ts
+++ b/src/commands/seek.ts
@@ -2,7 +2,6 @@ import {Message, TextChannel} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player';
-import QueueManager from '../managers/queue';
import LoadingMessage from '../utils/loading-message';
import errorMsg from '../utils/error-msg';
import Command from '.';
@@ -20,17 +19,15 @@ export default class implements Command {
public requiresVC = true;
private readonly playerManager: PlayerManager;
- private readonly queueManager: QueueManager;
- constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Managers.Queue) queueManager: QueueManager) {
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
- this.queueManager = queueManager;
}
public async execute(msg: Message, args: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id);
+ const player = this.playerManager.get(msg.guild!.id);
- const currentSong = queue.getCurrent();
+ const currentSong = player.getCurrent();
if (!currentSong) {
await msg.channel.send(errorMsg('nothing is playing'));
@@ -69,7 +66,7 @@ export default class implements Command {
await loading.start();
try {
- await this.playerManager.get(msg.guild!.id).seek(seekTime);
+ await player.seek(seekTime);
await loading.stop();
} catch (error) {
diff --git a/src/commands/shuffle.ts b/src/commands/shuffle.ts
index a100b9b..c9a007d 100644
--- a/src/commands/shuffle.ts
+++ b/src/commands/shuffle.ts
@@ -1,7 +1,7 @@
import {Message} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
-import QueueManager from '../managers/queue';
+import PlayerManager from '../managers/player';
import errorMsg from '../utils/error-msg';
import Command from '.';
@@ -15,23 +15,22 @@ export default class implements Command {
public requiresVC = true;
- private readonly queueManager: QueueManager;
+ private readonly playerManager: PlayerManager;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
+ this.playerManager = playerManager;
}
public async execute(msg: Message, _: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id).get();
+ const player = this.playerManager.get(msg.guild!.id);
- if (queue.length <= 2) {
+ if (player.isQueueEmpty()) {
await msg.channel.send(errorMsg('not enough songs to shuffle'));
return;
}
- this.queueManager.get(msg.guild!.id).shuffle();
+ player.shuffle();
- // TODO: better response
await msg.channel.send('shuffled');
}
}
diff --git a/src/commands/skip.ts b/src/commands/skip.ts
index 1eb23a0..72b3f46 100644
--- a/src/commands/skip.ts
+++ b/src/commands/skip.ts
@@ -1,9 +1,10 @@
-import {Message} from 'discord.js';
+import {Message, TextChannel} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player';
-import QueueManager from '../managers/queue';
import Command from '.';
+import LoadingMessage from '../utils/loading-message';
+import errorMsg from '../utils/error-msg';
@injectable()
export default class implements Command {
@@ -15,29 +16,24 @@ export default class implements Command {
public requiresVC = true;
- private readonly queueManager: QueueManager;
private readonly playerManager: PlayerManager;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
}
public async execute(msg: Message, _: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id);
const player = this.playerManager.get(msg.guild!.id);
- try {
- queue.forward();
- player.resetPosition();
+ const loader = new LoadingMessage(msg.channel as TextChannel);
- if (queue.isEmpty() && !queue.getCurrent()) {
- player.disconnect();
- }
+ try {
+ await loader.start();
+ await player.forward();
- await msg.channel.send('keep \'er movin\'');
+ await loader.stop('keep \'er movin\'');
} catch (_) {
- await msg.channel.send('no song to skip to');
+ await loader.stop(errorMsg('no song to skip to'));
}
}
}
diff --git a/src/commands/unskip.ts b/src/commands/unskip.ts
index a48d819..60147b0 100644
--- a/src/commands/unskip.ts
+++ b/src/commands/unskip.ts
@@ -2,7 +2,6 @@ import {Message} from 'discord.js';
import {TYPES} from '../types';
import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player';
-import QueueManager from '../managers/queue';
import errorMsg from '../utils/error-msg';
import Command from '.';
@@ -16,21 +15,17 @@ export default class implements Command {
public requiresVC = true;
- private readonly queueManager: QueueManager;
private readonly playerManager: PlayerManager;
- constructor(@inject(TYPES.Managers.Queue) queueManager: QueueManager, @inject(TYPES.Managers.Player) playerManager: PlayerManager) {
- this.queueManager = queueManager;
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
}
public async execute(msg: Message, _: string []): Promise<void> {
- const queue = this.queueManager.get(msg.guild!.id);
const player = this.playerManager.get(msg.guild!.id);
try {
- queue.back();
- player.resetPosition();
+ await player.back();
await msg.channel.send('back \'er up\'');
} catch (_) {
diff --git a/src/inversify.config.ts b/src/inversify.config.ts
index 2e53f14..6c1553e 100644
--- a/src/inversify.config.ts
+++ b/src/inversify.config.ts
@@ -17,7 +17,6 @@ import {
// Managers
import PlayerManager from './managers/player';
-import QueueManager from './managers/queue';
// Helpers
import GetSongs from './services/get-songs';
@@ -47,7 +46,6 @@ container.bind<Client>(TYPES.Client).toConstantValue(new Client());
// Managers
container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope();
-container.bind<QueueManager>(TYPES.Managers.Queue).to(QueueManager).inSingletonScope();
// Helpers
container.bind<GetSongs>(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope();
diff --git a/src/managers/player.ts b/src/managers/player.ts
index e8bdf76..b05d506 100644
--- a/src/managers/player.ts
+++ b/src/managers/player.ts
@@ -1,25 +1,22 @@
import {inject, injectable} from 'inversify';
import {TYPES} from '../types';
import Player from '../services/player';
-import QueueManager from './queue';
@injectable()
export default class {
private readonly guildPlayers: Map<string, Player>;
private readonly cacheDir: string;
- private readonly queueManager: QueueManager;
- constructor(@inject(TYPES.Config.CACHE_DIR) cacheDir: string, @inject(TYPES.Managers.Queue) queueManager: QueueManager) {
+ constructor(@inject(TYPES.Config.CACHE_DIR) cacheDir: string) {
this.guildPlayers = new Map();
this.cacheDir = cacheDir;
- this.queueManager = queueManager;
}
get(guildId: string): Player {
let player = this.guildPlayers.get(guildId);
if (!player) {
- player = new Player(this.queueManager.get(guildId), this.cacheDir);
+ player = new Player(this.cacheDir);
this.guildPlayers.set(guildId, player);
}
diff --git a/src/managers/queue.ts b/src/managers/queue.ts
deleted file mode 100644
index 6c12232..0000000
--- a/src/managers/queue.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {injectable} from 'inversify';
-import Queue from '../services/queue';
-
-@injectable()
-export default class {
- private readonly guildQueues: Map<string, Queue>;
-
- constructor() {
- this.guildQueues = new Map();
- }
-
- get(guildId: string): Queue {
- let queue = this.guildQueues.get(guildId);
-
- if (!queue) {
- queue = new Queue();
-
- this.guildQueues.set(guildId, queue);
- }
-
- return queue;
- }
-}
diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts
index ed1d125..4e8d955 100644
--- a/src/services/get-songs.ts
+++ b/src/services/get-songs.ts
@@ -8,8 +8,9 @@ import ytsr from 'ytsr';
import YouTube from 'youtube.ts';
import pLimit from 'p-limit';
import uniqueRandomArray from 'unique-random-array';
-import {QueuedSong, QueuedPlaylist} from '../services/queue';
+import {QueuedSong, QueuedPlaylist} from '../services/player';
import {TYPES} from '../types';
+import {parseTime} from '../utils/time';
@injectable()
export default class {
@@ -186,7 +187,7 @@ export default class {
return {
title: video.title,
artist: track.artists[0].name,
- length: track.duration_ms / 1000,
+ length: parseTime(video.duration),
url: video.link,
playlist,
isLive: video.live
diff --git a/src/services/natural-language-commands.ts b/src/services/natural-language-commands.ts
index 2fe610b..7171a87 100644
--- a/src/services/natural-language-commands.ts
+++ b/src/services/natural-language-commands.ts
@@ -2,17 +2,14 @@ import {inject, injectable} from 'inversify';
import {Message} from 'discord.js';
import {TYPES} from '../types';
import PlayerManager from '../managers/player';
-import QueueManager from '../managers/queue';
import {getMostPopularVoiceChannel} from '../utils/channels';
@injectable()
export default class {
private readonly playerManager: PlayerManager;
- private readonly queueManager: QueueManager;
- constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager, @inject(TYPES.Managers.Queue) queueManager: QueueManager) {
+ constructor(@inject(TYPES.Managers.Player) playerManager: PlayerManager) {
this.playerManager = playerManager;
- this.queueManager = queueManager;
}
async execute(msg: Message): Promise<boolean> {
@@ -24,7 +21,6 @@ export default class {
}
if (msg.content.includes('packers')) {
- const queue = this.queueManager.get(msg.guild!.id);
const player = this.playerManager.get(msg.guild!.id);
const [channel, n] = getMostPopularVoiceChannel(msg.guild!);
@@ -39,14 +35,15 @@ export default class {
await player.connect(channel);
}
- const isPlaying = queue.getCurrent() !== null;
+ const isPlaying = player.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});
+ player.add({title: 'GO PACKERS!', artist: 'Unknown', url: 'https://www.youtube.com/watch?v=qkdtID7mY3E', length: 204, playlist: null, isLive: false}, {immediate: true});
if (isPlaying) {
oldPosition = player.getPosition();
- queue.forward();
+
+ await player.forward();
}
await player.seek(8);
@@ -54,10 +51,10 @@ export default class {
return new Promise((resolve, reject) => {
try {
setTimeout(async () => {
- queue.removeCurrent();
+ player.removeCurrent();
if (isPlaying) {
- queue.back();
+ await player.back();
await player.seek(oldPosition);
} else {
player.disconnect();
diff --git a/src/services/player.ts b/src/services/player.ts
index 0fd7227..1f24d93 100644
--- a/src/services/player.ts
+++ b/src/services/player.ts
@@ -6,7 +6,21 @@ import hasha from 'hasha';
import ytdl from 'ytdl-core';
import {WriteStream} from 'fs-capacitor';
import ffmpeg from 'fluent-ffmpeg';
-import Queue, {QueuedSong} from './queue';
+import shuffle from 'array-shuffle';
+
+export interface QueuedPlaylist {
+ title: string;
+ source: string;
+}
+
+export interface QueuedSong {
+ title: string;
+ artist: string;
+ url: string;
+ length: number;
+ playlist: QueuedPlaylist | null;
+ isLive: boolean;
+}
export enum STATUS {
PLAYING,
@@ -16,7 +30,8 @@ export enum STATUS {
export default class {
public status = STATUS.PAUSED;
public voiceConnection: VoiceConnection | null = null;
- private readonly queue: Queue;
+ private queue: QueuedSong[] = [];
+ private queuePosition = 0;
private readonly cacheDir: string;
private dispatcher: StreamDispatcher | null = null;
private nowPlaying: QueuedSong | null = null;
@@ -25,8 +40,7 @@ export default class {
private positionInSeconds = 0;
- constructor(queue: Queue, cacheDir: string) {
- this.queue = queue;
+ constructor(cacheDir: string) {
this.cacheDir = cacheDir;
}
@@ -58,7 +72,7 @@ export default class {
throw new Error('Not connected to a voice channel.');
}
- const currentSong = this.queue.getCurrent();
+ const currentSong = this.getCurrent();
if (!currentSong) {
throw new Error('No song currently playing');
@@ -90,14 +104,14 @@ export default class {
throw new Error('Not connected to a voice channel.');
}
- const currentSong = this.queue.getCurrent();
+ const currentSong = this.getCurrent();
if (!currentSong) {
throw new Error('Queue empty.');
}
// Resume from paused state
- if (this.status === STATUS.PAUSED && this.getPosition() !== 0 && currentSong.url === this.nowPlaying?.url) {
+ if (this.status === STATUS.PAUSED && currentSong.url === this.nowPlaying?.url) {
if (this.dispatcher) {
this.dispatcher.resume();
this.status = STATUS.PLAYING;
@@ -109,20 +123,25 @@ export default class {
return this.seek(this.getPosition());
}
- const stream = await this.getStream(currentSong.url);
- this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'});
+ try {
+ const stream = await this.getStream(currentSong.url);
+ this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'});
- this.attachListeners();
+ this.attachListeners();
- this.status = STATUS.PLAYING;
- this.nowPlaying = currentSong;
+ this.status = STATUS.PLAYING;
+ this.nowPlaying = currentSong;
- if (currentSong.url === this.lastSongURL) {
- this.startTrackingPosition();
- } else {
- // Reset position counter
- this.startTrackingPosition(0);
- this.lastSongURL = currentSong.url;
+ if (currentSong.url === this.lastSongURL) {
+ this.startTrackingPosition();
+ } else {
+ // Reset position counter
+ this.startTrackingPosition(0);
+ this.lastSongURL = currentSong.url;
+ }
+ } catch (error) {
+ this.removeCurrent();
+ throw error;
}
}
@@ -140,8 +159,103 @@ export default class {
this.stopTrackingPosition();
}
- resetPosition(): void {
- this.positionInSeconds = 0;
+ async forward(): Promise<void> {
+ if (this.queuePosition < this.queueSize() + 1) {
+ this.queuePosition++;
+
+ try {
+ if (this.getCurrent() && this.status !== STATUS.PAUSED) {
+ await this.play();
+ } else {
+ this.status = STATUS.PAUSED;
+ this.disconnect();
+ }
+ } catch (error) {
+ this.queuePosition--;
+ throw error;
+ }
+ } else {
+ throw new Error('No songs in queue to forward to.');
+ }
+ }
+
+ async back(): Promise<void> {
+ if (this.queuePosition - 1 >= 0) {
+ this.queuePosition--;
+ this.positionInSeconds = 0;
+
+ if (this.status !== STATUS.PAUSED) {
+ await this.play();
+ }
+ } else {
+ throw new Error('No songs in queue to go back to.');
+ }
+ }
+
+ getCurrent(): QueuedSong | null {
+ if (this.queue[this.queuePosition]) {
+ return this.queue[this.queuePosition];
+ }
+
+ return null;
+ }
+
+ getQueue(): QueuedSong[] {
+ return this.queue.slice(this.queuePosition + 1);
+ }
+
+ add(song: QueuedSong, {immediate = false} = {}): void {
+ if (song.playlist) {
+ // Add to end of queue
+ this.queue.push(song);
+ } else {
+ // Not from playlist, add immediately
+ let insertAt = this.queuePosition + 1;
+
+ if (!immediate) {
+ // Loop until playlist song
+ this.queue.some(song => {
+ if (song.playlist) {
+ return true;
+ }
+
+ insertAt++;
+ return false;
+ });
+ }
+
+ this.queue = [...this.queue.slice(0, insertAt), song, ...this.queue.slice(insertAt)];
+ }
+ }
+
+ shuffle(): void {
+ this.queue = [...this.queue.slice(0, this.queuePosition + 1), ...shuffle(this.queue.slice(this.queuePosition + 1))];
+ }
+
+ clear(): void {
+ const newQueue = [];
+
+ // Don't clear curently playing song
+ const current = this.getCurrent();
+
+ if (current) {
+ newQueue.push(current);
+ }
+
+ this.queuePosition = 0;
+ this.queue = newQueue;
+ }
+
+ removeCurrent(): void {
+ this.queue = [...this.queue.slice(0, this.queuePosition), ...this.queue.slice(this.queuePosition + 1)];
+ }
+
+ queueSize(): number {
+ return this.getQueue().length;
+ }
+
+ isQueueEmpty(): boolean {
+ return this.queueSize() === 0;
}
private getCachedPath(url: string): string {
@@ -271,30 +385,23 @@ export default class {
return;
}
- this.voiceConnection.on('disconnect', () => {
- this.disconnect(false);
- });
+ this.voiceConnection.on('disconnect', this.onVoiceConnectionDisconnect.bind(this));
if (!this.dispatcher) {
return;
}
- this.dispatcher.on('speaking', async isSpeaking => {
- // Automatically advance queued song at end
- if (!isSpeaking && this.status === STATUS.PLAYING) {
- if (this.queue.size() >= 0) {
- this.queue.forward();
+ this.dispatcher.on('speaking', this.onVoiceConnectionSpeaking.bind(this));
+ }
- this.positionInSeconds = 0;
+ private onVoiceConnectionDisconnect(): void {
+ this.disconnect(false);
+ }
- if (this.queue.getCurrent()) {
- await this.play();
- } else {
- this.status = STATUS.PAUSED;
- this.disconnect();
- }
- }
- }
- });
+ private async onVoiceConnectionSpeaking(isSpeaking: boolean): Promise<void> {
+ // Automatically advance queued song at end
+ if (!isSpeaking && this.status === STATUS.PLAYING) {
+ await this.forward();
+ }
}
}
diff --git a/src/services/queue.ts b/src/services/queue.ts
deleted file mode 100644
index 70d9a58..0000000
--- a/src/services/queue.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-import shuffle from 'array-shuffle';
-
-export interface QueuedPlaylist {
- title: string;
- source: string;
-}
-
-export interface QueuedSong {
- title: string;
- artist: string;
- url: string;
- length: number;
- playlist: QueuedPlaylist | null;
- isLive: boolean;
-}
-
-export default class {
- private queue: QueuedSong[] = [];
- private position = 0;
-
- forward(): void {
- if (this.position < this.size() + 1) {
- this.position++;
- } else {
- throw new Error('No songs in queue to forward to.');
- }
- }
-
- back(): void {
- if (this.position - 1 >= 0) {
- this.position--;
- } else {
- throw new Error('No songs in queue to go back to.');
- }
- }
-
- getCurrent(): QueuedSong | null {
- if (this.queue[this.position]) {
- return this.queue[this.position];
- }
-
- return null;
- }
-
- get(): QueuedSong[] {
- return this.queue.slice(this.position + 1);
- }
-
- add(song: QueuedSong): void {
- if (song.playlist) {
- // Add to end of queue
- this.queue.push(song);
- } else {
- // Not from playlist, add immediately
- let insertAt = this.position;
-
- // Loop until playlist song
- this.queue.some(song => {
- if (song.playlist) {
- return true;
- }
-
- insertAt++;
- return false;
- });
-
- this.queue = [...this.queue.slice(0, insertAt), song, ...this.queue.slice(insertAt)];
- }
- }
-
- shuffle(): void {
- this.queue = [...this.queue.slice(0, this.position), this.queue[this.position], this.queue[0], ...shuffle(this.queue.slice(this.position + 1))];
- }
-
- clear(): void {
- const newQueue = [];
-
- // Don't clear curently playing song
- if (this.queue.length > 0) {
- newQueue.push(this.queue[this.position]);
- }
-
- this.queue = newQueue;
- }
-
- removeCurrent(): void {
- this.queue = [...this.queue.slice(0, this.position), ...this.queue.slice(this.position + 1)];
- }
-
- size(): number {
- return this.get().length;
- }
-
- isEmpty(): boolean {
- return this.get().length === 0;
- }
-}
diff --git a/src/types.ts b/src/types.ts
index ee18fb8..7ed076d 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -14,8 +14,7 @@ export const TYPES = {
Spotify: Symbol('Spotify')
},
Managers: {
- Player: Symbol('PlayerManager'),
- Queue: Symbol('QueueManager')
+ Player: Symbol('PlayerManager')
},
Services: {
GetSongs: Symbol('GetSongs'),
diff --git a/src/utils/loading-message.ts b/src/utils/loading-message.ts
index dd3e9b9..115aff2 100644
--- a/src/utils/loading-message.ts
+++ b/src/utils/loading-message.ts
@@ -60,10 +60,16 @@ export default class {
}
async stop(str = 'u betcha'): Promise<Message> {
+ const wasAlreadyStopped = this.isStopped;
+
this.isStopped = true;
if (str) {
- await Promise.all([this.msg.reactions.removeAll(), this.msg.edit(str)]);
+ if (wasAlreadyStopped) {
+ await this.msg.edit(str);
+ } else {
+ await Promise.all([this.msg.reactions.removeAll(), this.msg.edit(str)]);
+ }
} else {
await this.msg.reactions.removeAll();
}
diff --git a/src/utils/time.ts b/src/utils/time.ts
index a54bfa7..734cb00 100644
--- a/src/utils/time.ts
+++ b/src/utils/time.ts
@@ -13,3 +13,5 @@ export const prettyTime = (seconds: number): string => {
return res;
};
+
+export const parseTime = (str: string): number => str.split(':').reduce((acc, time) => (60 * acc) + parseInt(time, 10), 0);