commit 3e5af0395b7a056fa02aaf7356a171b579ea7d86
parent 5ea48e22b8e1d867c33b678a5f157560ece5e9d4
Author: Demonstrandum <moi@knutsen.co>
Date:   Wed, 11 Nov 2020 03:10:48 +0000
Prefetch media and deal with stream ending without explicit call to .end()
Diffstat:
2 files changed, 121 insertions(+), 60 deletions(-)
diff --git a/lib/commands/vc.ts b/lib/commands/vc.ts
@@ -1,8 +1,11 @@
 import { TextChannel } from 'discord.js';
 import ytdl from 'ytdl-core';
 
-
-const DL_OPTIONS : any = { filter: 'audioonly', dlChunkSize: 0 };
+const DL_OPTIONS : any = {
+	filter: 'audioonly',
+	dlChunkSize: 0,
+	quality: 'highestaudio'
+};
 
 export default async(home_scope: HomeScope) => {
 	const { message, args, CONFIG, CLIENT, INSTANCE_VARIABLES } = home_scope;
@@ -17,69 +20,124 @@ export default async(home_scope: HomeScope) => {
 
 	if(!CONFIG.vc_queue) CONFIG.vc_queue = [];
 
+	const attempt_prefetch = (url: string): boolean => {
+		let stream = null;
+		try {
+			stream = ytdl(url, DL_OPTIONS);
+		} catch (e) { console.log(e); }
+
+		if (stream) {
+			GID.vc_prefetch[url] = stream;
+			return true;
+		}
+		return false;
+	};
+
 	switch (args[0]) {
-		case "join":
-			if (message.member.voice.channel) {
-				GID.vc = await message.member.voice.channel.join();
-				CONFIG.vc_channel = message.channel.id;
-			} else {
-				message.reply("Join A Channel First.");
-			}
-			break;
-		case "leave":
-			try {
-				GID.vc.disconnect();
-			} catch (error) {
-				message.answer("```" + `${error}` + "```");
+	case "join": {
+		if (message.member.voice.channel) {
+			GID.vc = await message.member.voice.channel.join();
+			CONFIG.vc_channel = message.channel.id;
+			message.reply("Joined your voice chat.");
+		} else {
+			message.reply("Join a voice channel first.");
+		}
+		break;
+	} case "leave": {
+		try {
+			GID.vc.disconnect();
+			GID.vc.channel.leave();
+		} catch (error) {
+			message.answer("```" + `${error}` + "```");
+		}
+		break;
+	} case "pause": {
+		if (GID.vc_dispatcher) {
+			GID.vc_dispatcher.pause();
+			message.answer("Paused playback.");
+		}
+		else {
+			message.answer("Nothing is playing");
+		}
+		break;
+	} case "play": {
+		if (GID.vc_dispatcher) {
+			GID.vc_dispatcher.resume();
+			message.answer("Resuming playback.");
+		} else {
+			if (CONFIG.vc_queue.length === 0) {
+				message.answer("Please add a URL to the queue first.");
+				return;
 			}
-			break;
-		case "pause":
-			if (GID.vc_dispatcher) GID.vc_dispatcher.pause();
-			else message.answer("Nothing is playng");
-			break;
-		case "play":
-			if (GID.vc_dispatcher) {
-				GID.vc_dispatcher.resume();
-			} else {
-				GID.vc_dispatcher = GID.vc.play(
-					ytdl(CONFIG.vc_queue.pop(), DL_OPTIONS));
 
-				GID.vc_dispatcher.on("finish", () => {
-					GID.vc_dispatcher.destroy();
-					const next = CONFIG.vc_queue.pop();
-					if (next) {
-						GID.vc_dispatcher = GID.vc.play(
-							ytdl(next, DL_OPTIONS));
-						CLIENT.channels.fetch(CONFIG.vc_channel)
-							.then((ch: TextChannel) =>
-								ch.send(`Now playing: ${next}`));
-					}
-				})
+			const stream = GID.vc_prefetch[CONFIG.vc_queue.pop()];
+			GID.vc_current_stream = stream;
+			GID.vc_dispatcher = GID.vc.play(stream);
+			message.channel.send("Playing media from queue...");
+
+			const end_handler = () => {
+				GID.vc_dispatcher.destroy();
+				const next = CONFIG.vc_queue.pop();
+				if (next) {
+					const stream = GID.vc_prefetch[next];
+					GID.vc_current_stream = stream;
+					GID.vc_dispatcher = GID.vc.play(stream);
+					CLIENT.channels.fetch(CONFIG.vc_channel)
+						.then((ch: TextChannel) =>
+							ch.send(`Now playing: ${next}`));
+				}
 			}
-			break;
-		case "d":
-			CONFIG.vc_queue.splice(Number(args[1]) - 1, 1);
-			break;
-		case "i":
-			CONFIG.vc_queue.splice(Number(args[1]) - 1, 0, args[2]);
-			break;
-		case "ls":
-			message.answer(ls(CONFIG.vc_queue));
-			break;
-		case "requeue":
-			CONFIG.vc_queue = [];
-			message.answer("Queue cleared");
-			GID.vc_dispatcher.end();
-			break;
-		case "skip":
-			GID.vc_dispatcher.end();
-			break;
-		default:
-			// TODO: Add checking for valid URIs?
-			CONFIG.vc_queue.push(args[0]);
+
+			GID.vc_dispatcher.on('finish', end_handler);
+			GID.vc_current_stream.on('end', () => {
+				GID.vc_dispatcher.end();
+			});
+		}
+		break;
+	} case "d": {
+		const pos = Number(args[1]);
+		CONFIG.vc_queue.splice(pos - 1, 1);
+		message.answer(`Removed media from queue at index ${pos}.`);
+		break;
+	} case "i": {
+		const pos = Number(args[1]);
+		const url = args[2];
+		const success = attempt_prefetch(url);
+		if (success) {
+			CONFIG.vc_queue.splice(pos - 1, 0, url);
+			message.answer(`Inserted into queue at index ${pos}.`);
+		} else {
+			message.answer("URL or media-type not valid.");
+		}
+		break;
+	} case "ls": {
+		message.answer(ls(CONFIG.vc_queue));
+		break;
+	} case "requeue": {
+		CONFIG.vc_queue = [];
+		GID.vc_current_stream = null;
+		message.answer("Queue cleared");
+		GID.vc_dispatcher.end();
+		break;
+	} case "skip": {
+		GID.vc_dispatcher.end();
+		GID.vc_current_stream.destroy();
+		message.answer("Skipping...");
+		break;
+	} default: {
+		const url = args[0];
+		const success = attempt_prefetch(url);
+		if (success) {
+			CONFIG.vc_queue.push(url);
+			message.answer("Succesfully added media to queue.");
+		} else {
+			message.answer("URL or media-type not valid.");
+		}
+	}
 	}
 }
+
 function ls(queue : string[]) {  // TODO: This could be more sophisticated.
 	return "Queue size: " + queue.length
-		+ queue.map((e, i) => `\n ${i + 1}: ${e}`).join('');
+		+ queue.map((e, i) => `\n ${i + 1}. ${e}`).join('');
 }
diff --git a/lib/extensions.ts b/lib/extensions.ts
@@ -1,6 +1,7 @@
 import { SimpOMatic } from './main';
 import { Client } from '@typeit/discord';
 import { VoiceConnection, StreamDispatcher } from 'discord.js';
+import stream from 'stream';
 
 // Global Extensions:
 declare global {
@@ -43,7 +44,9 @@ declare global {
 
 		export type GuildInstanceData = {
 			vc: VoiceConnection,
-			vc_dispatcher: StreamDispatcher
+			vc_dispatcher: StreamDispatcher,
+			vc_prefetch: { [key: string]: stream.Readable },
+			vc_current_stream: stream.Readable
 		};
 
 		export type InstanceVariables = {