// based on https://github.com/discordjs/voice/blob/main/examples/music-bot/src/music/track.ts //import ytdl from 'ytdl-core'; import ytdl from 'ytdl-core'; import play from 'play-dl'; import { AudioResource, createAudioResource, demuxProbe } from '@discordjs/voice'; /** * This is the data required to create a Track object. */ export interface TrackData { url: string; title: string; onStart: () => void; onFinish: () => void; onError: (error: Error) => void; addedBy: string; } // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; /** * A Track represents information about a YouTube video (in this context) that can be added to a queue. * It contains the title and URL of the video, as well as functions onStart, onFinish, onError, that act * as callbacks that are triggered at certain points during the track's lifecycle. * * Rather than creating an AudioResource for each video immediately and then keeping those in a queue, * we use tracks as they don't pre-emptively load the videos. Instead, once a Track is taken from the * queue, it is converted into an AudioResource just in time for playback. */ export class Track implements TrackData { public readonly url: string; public readonly title: string; public readonly onStart: () => void; public readonly onFinish: () => void; public readonly onError: (error: Error) => void; public readonly addedBy: string; private constructor({ url, title, onStart, onFinish, onError, addedBy }: TrackData) { this.url = url; this.title = title; this.onStart = onStart; this.onFinish = onFinish; this.onError = onError; this.addedBy = addedBy; } /** * Creates an AudioResource from this Track. */ public async createAudioResource(): AudioResource { const stream = await play.stream(this.url); return createAudioResource(stream.stream, { metadata: this, inputType: stream.type }); /*return new Promise((resolve, reject) => { const process = youtubedl( this.url, { o: '-', q: '', f: 'bestaudio[ext=webm+acodec=opus+asr=48000]/bestaudio', r: '100K', }, { stdio: ['ignore', 'pipe', 'ignore'] }, ); if (!process.stdout) { reject(new Error('No stdout')); return; } const stream = process.stdout; const onError = (error: Error) => { if (!process.killed) process.kill(); stream.resume(); reject(error); }; process .once('spawn', () => { demuxProbe(stream) .then((probe: { stream: any; type: any; }) => resolve(createAudioResource(probe.stream, { metadata: this, inputType: probe.type }))) .catch(onError); }) .catch(onError); });*/ } /** * Creates a Track from a video URL and lifecycle callback methods. * * @param url The URL of the video * @param methods Lifecycle callbacks * * @returns The created Track */ public static async from(url: string, addedBy: string, methods: Pick): Promise { const info = await ytdl.getInfo(url); // The methods are wrapped so that we can ensure that they are only called once. const wrappedMethods = { onStart() { wrappedMethods.onStart = noop; methods.onStart(); }, onFinish() { wrappedMethods.onFinish = noop; methods.onFinish(); }, onError(error: Error) { wrappedMethods.onError = noop; methods.onError(error); }, }; return new Track({ title: info.videoDetails.title, addedBy: addedBy, url, ...wrappedMethods, }); } }