permanent-waves/track.ts
2024-07-20 14:41:35 -05:00

119 lines
3.6 KiB
TypeScript

// 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<Track> {
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<Track, 'onStart' | 'onFinish' | 'onError'>): Promise<Track> {
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,
});
}
}