diff options
| author | Max Isom <[email protected]> | 2020-03-10 11:58:09 -0500 |
|---|---|---|
| committer | Max Isom <[email protected]> | 2020-03-10 11:58:09 -0500 |
| commit | 8eb4c8a6c06f672cb50efae5ea30215d465000af (patch) | |
| tree | 938a439e254694b4ed283d71a303b442ba739104 /src | |
| parent | 652cc2e5efed6ddef593570ee90634cbc1c452bb (diff) | |
| download | muse-8eb4c8a6c06f672cb50efae5ea30215d465000af.tar.xz muse-8eb4c8a6c06f672cb50efae5ea30215d465000af.zip | |
Basic play functionality
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands/play.ts | 21 | ||||
| -rw-r--r-- | src/index.ts | 3 | ||||
| -rw-r--r-- | src/utils/channels.ts | 38 | ||||
| -rw-r--r-- | src/utils/config.ts | 1 | ||||
| -rw-r--r-- | src/utils/get-youtube-stream.ts | 51 |
5 files changed, 113 insertions, 1 deletions
diff --git a/src/commands/play.ts b/src/commands/play.ts new file mode 100644 index 0000000..cf23d60 --- /dev/null +++ b/src/commands/play.ts @@ -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; diff --git a/src/index.ts b/src/index.ts index 6681d45..b4c760f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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({}); diff --git a/src/utils/channels.ts b/src/utils/channels.ts new file mode 100644 index 0000000..d138c45 --- /dev/null +++ b/src/utils/channels.ts @@ -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(); +}; diff --git a/src/utils/config.ts b/src/utils/config.ts index db70d7f..0b3b33d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -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'); diff --git a/src/utils/get-youtube-stream.ts b/src/utils/get-youtube-stream.ts new file mode 100644 index 0000000..41f687c --- /dev/null +++ b/src/utils/get-youtube-stream.ts @@ -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); + } +}; |
