Basic play functionality

This commit is contained in:
Max Isom 2020-03-10 11:58:09 -05:00
parent 652cc2e5ef
commit 8eb4c8a6c0
7 changed files with 159 additions and 4 deletions

21
src/commands/play.ts Normal file
View file

@ -0,0 +1,21 @@
import {CommandHandler} from '../interfaces';
import {getMostPopularVoiceChannel} from '../utils/channels';
import getYouTubeStream from '../utils/get-youtube-stream';
const play: CommandHandler = {
name: 'play',
description: 'plays a song',
execute: async (msg, args) => {
const url = args[0];
const channel = getMostPopularVoiceChannel(msg.guild!);
const conn = await channel.join();
const stream = await getYouTubeStream(url);
conn.play(stream, {type: 'webm/opus'});
}
};
export default play;

View file

@ -2,7 +2,7 @@ import fs from 'fs';
import path from 'path';
import makeDir from 'make-dir';
import Discord from 'discord.js';
import {DISCORD_TOKEN, DISCORD_CLIENT_ID, DATA_DIR} from './utils/config';
import {DISCORD_TOKEN, DISCORD_CLIENT_ID, DATA_DIR, CACHE_DIR} from './utils/config';
import {Settings} from './models';
import {sequelize} from './utils/db';
import {CommandHandler} from './interfaces';
@ -56,6 +56,7 @@ client.on('message', async (msg: Discord.Message) => {
client.on('ready', async () => {
// Create directory if necessary
await makeDir(DATA_DIR);
await makeDir(CACHE_DIR);
await sequelize.sync({});

38
src/utils/channels.ts Normal file
View file

@ -0,0 +1,38 @@
import {Guild, VoiceChannel} from 'discord.js';
export const getMostPopularVoiceChannel = (guild: Guild, min = 0): VoiceChannel => {
interface PopularResult {
n: number;
channel: VoiceChannel | null;
}
const voiceChannels: PopularResult[] = [];
for (const [_, channel] of guild.channels.cache) {
if (channel.type === 'voice' && channel.members.size >= min) {
voiceChannels.push({
channel: channel as VoiceChannel,
n: channel.members.size
});
}
}
if (voiceChannels.length === 0) {
throw new Error('No voice channels meet minimum size');
}
// Find most popular channel
const popularChannel = voiceChannels.reduce((popular: PopularResult, elem: PopularResult) => {
if (elem.n > popular.n) {
return elem;
}
return popular;
}, {n: -1, channel: null});
if (popularChannel.channel) {
return popularChannel.channel;
}
throw new Error();
};

View file

@ -5,3 +5,4 @@ dotenv.config();
export const DISCORD_TOKEN: string = process.env.DISCORD_TOKEN ? process.env.DISCORD_TOKEN : '';
export const DISCORD_CLIENT_ID: string = process.env.DISCORD_CLIENT_ID ? process.env.DISCORD_CLIENT_ID : '';
export const DATA_DIR = path.resolve(process.env.DATA_DIR ? process.env.DATA_DIR : './data');
export const CACHE_DIR = path.join(DATA_DIR, 'cache');

View file

@ -0,0 +1,51 @@
import {promises as fs, createReadStream, createWriteStream} from 'fs';
import {Readable, PassThrough} from 'stream';
import path from 'path';
import hasha from 'hasha';
import ytdl from 'ytdl-core';
import {CACHE_DIR} from './config';
const nextBestFormat = (formats: ytdl.videoFormat[]): ytdl.videoFormat => {
formats = formats
.filter(format => format.averageBitrate)
.sort((a, b) => b.averageBitrate - a.averageBitrate);
return formats.find(format => !format.bitrate) ?? formats[0];
};
// TODO: are some videos not available in webm/opus?
export default async (url: string): Promise<Readable> => {
const hash = hasha(url);
const cachedPath = path.join(CACHE_DIR, `${hash}.webm`);
const info = await ytdl.getInfo(url);
const {formats} = info;
const filter = (format: ytdl.videoFormat): boolean => format.codecs === 'opus' && format.container === 'webm' && format.audioSampleRate !== undefined && parseInt(format.audioSampleRate, 10) === 48000;
let format = formats.find(filter);
if (!format) {
format = nextBestFormat(info.formats);
}
try {
// Test if file exists
await fs.access(cachedPath);
// If so, return cached stream
return createReadStream(cachedPath);
} catch (_) {
// Not yet cached, must download
const cacheTempPath = path.join('/tmp', `${hash}.webm`);
const cacheStream = createWriteStream(cacheTempPath);
const pass = new PassThrough();
pass.pipe(cacheStream).on('finish', async () => {
await fs.rename(cacheTempPath, cachedPath);
});
return ytdl.downloadFromInfo(info, {format}).pipe(pass);
}
};