New jukebox plugin, only supports Discord at the moment
This commit is contained in:
parent
1ba5e3c9f0
commit
f7262f2ca5
9
jukebox/plugin.json
Normal file
9
jukebox/plugin.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"main": "plugin.js",
|
||||||
|
"name": "jukebox",
|
||||||
|
"description": "Jukebox plugin for Icecast/Discord",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"tags": ["commands", "jukebox", "music"],
|
||||||
|
"dependencies": ["simplecommands"],
|
||||||
|
"npmDependencies": ["ytdl-core@^4.1.3","@discordjs/opus@^0.3.3","discord.js@^12.5.1","ytsr@1.0.4"]
|
||||||
|
}
|
299
jukebox/plugin.ts
Normal file
299
jukebox/plugin.ts
Normal file
@ -0,0 +1,299 @@
|
|||||||
|
import { fullIDMatcher } from '@squeebot/core/lib/common';
|
||||||
|
import { logger } from '@squeebot/core/lib/core';
|
||||||
|
import {
|
||||||
|
Plugin,
|
||||||
|
Configurable,
|
||||||
|
EventListener,
|
||||||
|
DependencyLoad
|
||||||
|
} from '@squeebot/core/lib/plugin';
|
||||||
|
|
||||||
|
import { IMessage } from '@squeebot/core/lib/types';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
|
import ytdl from 'ytdl-core';
|
||||||
|
import ytsr, { Video } from 'ytsr';
|
||||||
|
|
||||||
|
import { Client, VoiceChannel } from 'discord.js';
|
||||||
|
|
||||||
|
interface Configuration {
|
||||||
|
control: string[];
|
||||||
|
voice?: string;
|
||||||
|
leaveAfter?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Cached {
|
||||||
|
playing: boolean;
|
||||||
|
config: Configuration;
|
||||||
|
readable: Readable;
|
||||||
|
url: string;
|
||||||
|
connection?: any;
|
||||||
|
queue: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Discord config example: {
|
||||||
|
"control": ["discord/testing/s:<guild id>/*"],
|
||||||
|
"voice": "<voice channel id>",
|
||||||
|
"leaveAfter": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
Icecast config example: {
|
||||||
|
"control": ["irc/icynet/#music"],
|
||||||
|
"icecast": {
|
||||||
|
"server": "http://localhost:8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Configurable({
|
||||||
|
youtube: true,
|
||||||
|
discord: [],
|
||||||
|
icecast: [],
|
||||||
|
})
|
||||||
|
class JukeboxPlugin extends Plugin {
|
||||||
|
public cache: Cached[] = [];
|
||||||
|
|
||||||
|
@EventListener('pluginUnload')
|
||||||
|
public unloadEventHandler(plugin: string | Plugin): void {
|
||||||
|
if (plugin === this.name || plugin === this) {
|
||||||
|
this.emit('pluginUnloaded', this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigByMessage(msg: IMessage): Configuration | null {
|
||||||
|
const rm = msg.fullRoomID;
|
||||||
|
if (!rm) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const cfg of this.config.get('discord', []) as Configuration[]) {
|
||||||
|
let roomMatch = false;
|
||||||
|
for (const room of cfg.control) {
|
||||||
|
if (fullIDMatcher(rm, room)) {
|
||||||
|
roomMatch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!roomMatch) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCachedConfig(cfg: Configuration): Cached | null {
|
||||||
|
for (const ch of this.cache) {
|
||||||
|
if (ch.config === cfg) {
|
||||||
|
return ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getStream(link: string): Promise<Readable> {
|
||||||
|
if (link.indexOf('http') !== 0) {
|
||||||
|
const search = await ytsr(link, { limit: 1 });
|
||||||
|
link = (search.items[0] as Video).link;
|
||||||
|
}
|
||||||
|
return ytdl(link, { quality: 'highestaudio', dlChunkSize: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async playToDiscord(discord: Client, cfg: Configuration, link: string, msg: IMessage): Promise<void> {
|
||||||
|
const cached = this.getCachedConfig(cfg);
|
||||||
|
if (cached && cached.readable) {
|
||||||
|
cached.readable.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
const channel = await discord.channels.fetch(cfg.voice as string);
|
||||||
|
if (!channel || channel.type !== 'voice') {
|
||||||
|
throw new Error('Invalid voice channel.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const vc = await (channel as VoiceChannel).join();
|
||||||
|
const str = await this.getStream(link);
|
||||||
|
vc.play(str);
|
||||||
|
|
||||||
|
str.on('end', () => setTimeout(() => {
|
||||||
|
str.destroy();
|
||||||
|
|
||||||
|
const cacheCurrent = this.getCachedConfig(cfg);
|
||||||
|
if (!cacheCurrent) {
|
||||||
|
vc.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheCurrent.playing = false;
|
||||||
|
|
||||||
|
if (!cacheCurrent.queue || !cacheCurrent.queue.length) {
|
||||||
|
vc.disconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = cacheCurrent.queue.shift() as string;
|
||||||
|
this.playToDiscord(discord, cfg, next, msg).catch((e) => {
|
||||||
|
logger.error('[%s] Autoplay threw an error:', this.name, e.stack);
|
||||||
|
msg.resolve('Playing of the next track in the queue failed.');
|
||||||
|
});
|
||||||
|
}, 5000));
|
||||||
|
|
||||||
|
if (cached) {
|
||||||
|
cached.playing = true;
|
||||||
|
cached.readable = str;
|
||||||
|
cached.url = link;
|
||||||
|
} else {
|
||||||
|
this.cache.push({
|
||||||
|
playing: true,
|
||||||
|
readable: str,
|
||||||
|
url: link,
|
||||||
|
config: cfg,
|
||||||
|
queue: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DependencyLoad('simplecommands')
|
||||||
|
addCommands(cmd: any): void {
|
||||||
|
const roomsWithJukeboxes = [];
|
||||||
|
for (const cfg of this.config.get('discord', []) as Configuration[]) {
|
||||||
|
roomsWithJukeboxes.push(...cfg.control);
|
||||||
|
}
|
||||||
|
cmd.registerCommand([{
|
||||||
|
name: 'play',
|
||||||
|
plugin: this.name,
|
||||||
|
execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
|
||||||
|
if (!simplified[0]) {
|
||||||
|
msg.resolve('Please provide an URL or search term to play!');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vcConfig = this.getConfigByMessage(msg);
|
||||||
|
if (!vcConfig) {
|
||||||
|
msg.resolve('This channel has no configuration for jukeboxes.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.source.type !== 'DiscordProtocol') {
|
||||||
|
msg.resolve('Currently, only Discord is supported.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.playToDiscord((msg.source as any).client, vcConfig, simplified.join(' '), msg);
|
||||||
|
} catch (e) {
|
||||||
|
msg.resolve('Something went wrong!');
|
||||||
|
logger.error('[%s] Threw an error:', this.name, e.stack);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
source: roomsWithJukeboxes,
|
||||||
|
usage: '<url>',
|
||||||
|
description: 'Play something',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'stop',
|
||||||
|
plugin: this.name,
|
||||||
|
execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
|
||||||
|
const vcConfig = this.getConfigByMessage(msg);
|
||||||
|
if (!vcConfig) {
|
||||||
|
msg.resolve('This channel has no configuration for jukeboxes.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.source.type !== 'DiscordProtocol') {
|
||||||
|
msg.resolve('Currently, only Discord is supported.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.getCachedConfig(vcConfig);
|
||||||
|
if (cached && cached.playing) {
|
||||||
|
cached.playing = false;
|
||||||
|
cached.readable.destroy();
|
||||||
|
msg.resolve('Stopped currently playing.');
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
source: roomsWithJukeboxes,
|
||||||
|
description: 'Stop the currently playing track',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'enqueue',
|
||||||
|
plugin: this.name,
|
||||||
|
execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
|
||||||
|
if (!simplified[0]) {
|
||||||
|
msg.resolve('Please provide an URL or search term to play!');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vcConfig = this.getConfigByMessage(msg);
|
||||||
|
if (!vcConfig) {
|
||||||
|
msg.resolve('This channel has no configuration for jukeboxes.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.source.type !== 'DiscordProtocol') {
|
||||||
|
msg.resolve('Currently, only Discord is supported.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const track = simplified.join(' ');
|
||||||
|
const cached = this.getCachedConfig(vcConfig);
|
||||||
|
if (cached && cached.playing) {
|
||||||
|
cached.queue.push(track);
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await this.playToDiscord((msg.source as any).client, vcConfig, track, msg);
|
||||||
|
} catch (e) {
|
||||||
|
msg.resolve('Something went wrong!');
|
||||||
|
logger.error('[%s] Threw an error:', this.name, e.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
source: roomsWithJukeboxes,
|
||||||
|
usage: '<url>',
|
||||||
|
description: 'Queue a track',
|
||||||
|
aliases: ['queue'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'skip',
|
||||||
|
plugin: this.name,
|
||||||
|
execute: async (msg: IMessage, spec: any, prefix: string, ...simplified: any[]): Promise<boolean> => {
|
||||||
|
const vcConfig = this.getConfigByMessage(msg);
|
||||||
|
if (!vcConfig) {
|
||||||
|
msg.resolve('This channel has no configuration for jukeboxes.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.source.type !== 'DiscordProtocol') {
|
||||||
|
msg.resolve('Currently, only Discord is supported.');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cached = this.getCachedConfig(vcConfig);
|
||||||
|
if (cached) {
|
||||||
|
if (cached.playing) {
|
||||||
|
cached.readable.destroy();
|
||||||
|
cached.playing = false;
|
||||||
|
}
|
||||||
|
if (cached.queue.length) {
|
||||||
|
const next = cached.queue.shift() as string;
|
||||||
|
try {
|
||||||
|
await this.playToDiscord((msg.source as any).client, vcConfig, next, msg);
|
||||||
|
} catch (e) {
|
||||||
|
msg.resolve('Something went wrong!');
|
||||||
|
logger.error('[%s] Threw an error:', this.name, e.stack);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg.resolve('The queue is empty.');
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
source: roomsWithJukeboxes,
|
||||||
|
description: 'Skip the current track',
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = JukeboxPlugin;
|
@ -21,6 +21,10 @@
|
|||||||
"name": "gamedig",
|
"name": "gamedig",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "jukebox",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "timezone",
|
"name": "timezone",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user