@discordx/music
Version:
Powerful Discord music player library using YTDL
392 lines (388 loc) • 10.1 kB
JavaScript
import {
AudioNodeEvent,
ParentProcessEvent,
QueueEvent,
WorkerOperation
} from "./chunk-WGQSBO3W.mjs";
// src/node.ts
import { dirname, isESM } from "@discordx/importer";
import { EventEmitter } from "events";
import { Worker } from "worker_threads";
var Node = class extends EventEmitter {
constructor(client) {
super();
this.client = client;
const folder = isESM() ? dirname(import.meta.url) : __dirname;
this.worker = new Worker(
`${folder}/worker/${isESM() ? "index.mjs" : "index.js"}`
);
this.setupEventListeners();
this.setupWorkerMessageHandler();
}
worker;
sendOp(payload) {
this.worker.postMessage(payload);
}
setupEventListeners() {
this.client.on("raw", (event) => {
if (event.t === "VOICE_STATE_UPDATE") {
this.sendOp({ data: event.d, op: "ON_VOICE_STATE_UPDATE" /* OnVoiceStateUpdate */ });
} else if (event.t === "VOICE_SERVER_UPDATE") {
this.sendOp({
data: event.d,
op: "ON_VOICE_SERVER_UPDATE" /* OnVoiceServerUpdate */
});
}
});
}
setupWorkerMessageHandler() {
this.worker.on("message", (payload) => {
this.emit("PARENT_PROCESS_EVENT" /* ParentProcessEvent */, payload);
switch (payload.op) {
case "VOICE_STATE_UPDATE" /* VoiceStateUpdate */:
void this.handleWorkerVoiceStateUpdate(payload.data);
break;
case "CONNECTION_DESTROY" /* ConnectionDestroy */:
this.handleWorkerConnectionDestroy(payload.data);
break;
default:
break;
}
});
}
async handleWorkerVoiceStateUpdate(data) {
const guild = await this.client.guilds.fetch(data.guildId);
guild.shard.send(data.payload);
}
handleWorkerConnectionDestroy(data) {
const { channelId } = data;
this.client.voice.adapters.get(channelId)?.destroy();
}
join(data) {
this.sendOp({ data, op: "JOIN" /* Join */ });
}
play(data) {
this.sendOp({ data, op: "PLAY" /* Play */ });
}
pingPlaybackInfo(data) {
this.sendOp({ data, op: "PING_PLAYBACK_INFO" /* PingPlaybackInfo */ });
}
setVolume(data) {
this.sendOp({ data, op: "SET_VOLUME" /* SetVolume */ });
}
pause(data) {
this.sendOp({ data, op: "PAUSE" /* Pause */ });
}
resume(data) {
this.sendOp({ data, op: "RESUME" /* Resume */ });
}
stop(data) {
this.sendOp({ data, op: "STOP" /* Stop */ });
}
disconnect(data) {
this.sendOp({ data, op: "DISCONNECT" /* Disconnect */ });
}
disconnectAll() {
this.sendOp({ op: "DISCONNECT_ALL" /* DisconnectAll */ });
}
};
// src/queue.ts
import { AudioPlayerStatus } from "@discordjs/voice";
import shuffle from "lodash/shuffle.js";
var RepeatMode = /* @__PURE__ */ ((RepeatMode2) => {
RepeatMode2["OFF"] = "OFF";
RepeatMode2["REPEAT_ALL"] = "REPEAT_ALL";
RepeatMode2["REPEAT_ONE"] = "REPEAT_ONE";
return RepeatMode2;
})(RepeatMode || {});
var Queue = class {
constructor(node, guildId) {
this.node = node;
this.guildId = guildId;
}
_leaveOnFinish = true;
_currentPlaybackTrack = null;
_playbackInfo = null;
_playerState = AudioPlayerStatus.Idle;
_repeatMode = "OFF" /* OFF */;
_volume = 100;
_tracks = [];
_pingIntervalId = null;
get leaveOnFinish() {
return this._leaveOnFinish;
}
get client() {
return this.node.client;
}
get tracks() {
return this._tracks;
}
get currentPlaybackTrack() {
return this._currentPlaybackTrack;
}
get nextTrack() {
return this.tracks[0] ?? null;
}
get playbackInfo() {
return this._playbackInfo;
}
get playerState() {
return this._playerState;
}
get isPlaying() {
return this.playerState === AudioPlayerStatus.Playing;
}
get repeatMode() {
return this._repeatMode;
}
get volume() {
return this._volume;
}
get size() {
return this.tracks.length;
}
/**
* Starts a ping to keep the playback information updated.
*/
startPing() {
this.stopPing();
this._pingIntervalId = setInterval(() => {
this.node.pingPlaybackInfo({ guildId: this.guildId });
}, 1e3);
}
/**
* Stops the ping for updating playback information.
*/
stopPing() {
if (this._pingIntervalId !== null) {
clearInterval(this._pingIntervalId);
this._pingIntervalId = null;
}
}
/**
* Adds tracks to the end of the queue.
* @param track - The tracks to be added.
*/
addTrack(...track) {
this._tracks.push(...track);
}
/**
* Adds tracks to the beginning of the queue.
* @param track - The tracks to be added.
*/
addTrackFirst(...track) {
this._tracks.unshift(...track);
}
/**
* Changes the position of a track in the queue.
* @param oldIndex - The current index of the track.
* @param newIndex - The new index for the track.
* @throws Will throw an error if the indices are out of bounds.
*/
changeTrackPosition(oldIndex, newIndex) {
if (oldIndex < 0 || oldIndex >= this.size || newIndex < 0 || newIndex >= this.size) {
throw new Error("Invalid track position");
}
const [movedTrack] = this._tracks.splice(oldIndex, 1);
if (movedTrack) {
this._tracks.splice(newIndex, 0, movedTrack);
}
}
/**
* Exits the player, clearing the queue and destroying the LavaPlayer instance.
*/
exit() {
this._currentPlaybackTrack = null;
this.removeAllTracks();
this.leave();
}
/**
* Joins a voice channel.
* @param data - The data required to join the channel.
*/
join(data) {
this.node.join({ ...data, guildId: this.guildId });
}
/**
* Leaves the voice channel.
*/
leave() {
this.stopPing();
this.node.disconnect({ guildId: this.guildId });
}
/**
* Pauses the playback.
*/
pause() {
this.node.pause({ guildId: this.guildId });
}
/**
* Plays the next track in the queue.
* @returns The next track that was played or null if the queue is empty.
*/
playNext() {
if (this._currentPlaybackTrack !== null && this.repeatMode !== "OFF" /* OFF */) {
if (this.repeatMode === "REPEAT_ALL" /* REPEAT_ALL */) {
this.addTrack(this._currentPlaybackTrack);
} else {
this.addTrackFirst(this._currentPlaybackTrack);
}
}
this._currentPlaybackTrack = null;
const nextTrack = this._tracks.shift();
if (!nextTrack) {
return null;
}
this._currentPlaybackTrack = nextTrack;
this.node.play({
guildId: this.guildId,
payload: {
initialVolume: this.volume,
query: nextTrack.url,
seek: nextTrack.seek
}
});
return nextTrack;
}
/**
* Removes tracks from the queue by their indices.
* @param indices - The indices of the tracks to be removed.
*/
removeTracks(...indices) {
indices.sort((a, b) => b - a);
for (const index of indices) {
if (index >= 0 && index < this.size) {
this._tracks.splice(index, 1);
}
}
}
/**
* Removes all tracks from the queue.
*/
removeAllTracks() {
this._tracks = [];
}
/**
* Resumes the playback.
*/
resume() {
this.node.resume({ guildId: this.guildId });
}
/**
* Seek the playback to specific position
* @param seconds in milliseconds
*/
seek(seconds) {
if (!this.currentPlaybackTrack) {
return null;
}
this.node.play({
guildId: this.guildId,
payload: {
initialVolume: this.volume,
query: this.currentPlaybackTrack.url,
seek: seconds
}
});
return this.currentPlaybackTrack;
}
/**
* Set leave on finish state
*/
setLeaveOnFinish(value) {
this._leaveOnFinish = value;
}
/**
* Set playback info
*/
setPlaybackInfo(info) {
this._playbackInfo = info;
}
/**
* set player state
*/
setPlayerState(state) {
this._playerState = state;
}
/**
* Sets the repeat mode for the queue.
* @param mode - The repeat mode to be set.
*/
setRepeatMode(mode) {
this._repeatMode = mode;
}
/**
* Sets the volume for playback.
* @param volume - The volume level to be set.
*/
setVolume(volume) {
this._volume = volume;
this.node.setVolume({ guildId: this.guildId, volume });
}
/**
* Shuffles the tracks in the queue.
*/
shuffleTracks() {
this._tracks = shuffle(this.tracks);
}
/**
* Skips the current track.
*/
skip() {
this.node.stop({ guildId: this.guildId });
}
};
// src/queue-manager.ts
import { AudioPlayerStatus as AudioPlayerStatus2 } from "@discordjs/voice";
import { Collection } from "discord.js";
var QueueManager = class {
constructor(node) {
this.node = node;
node.on("PARENT_PROCESS_EVENT" /* ParentProcessEvent */, (payload) => {
const queue = this.queues.get(payload.data.guildId);
if (!queue) {
return;
}
if (payload.op === "AUDIO_NODE_EVENT" /* AudioNodeEvent */) {
if (payload.data.payload.type === "PLAYBACK_INFO" /* PlaybackInfo */) {
queue.setPlaybackInfo(payload.data.payload);
}
if (payload.data.payload.type === "STATE_CHANGE" /* StateChange */) {
const { newState } = payload.data.payload;
queue.setPlayerState(newState);
if (newState === AudioPlayerStatus2.Playing) {
queue.startPing();
} else {
queue.stopPing();
}
if (newState === AudioPlayerStatus2.Idle || newState === AudioPlayerStatus2.AutoPaused) {
queue.playNext();
}
if (queue.leaveOnFinish && !queue.currentPlaybackTrack && !queue.tracks.length) {
queue.leave();
}
}
}
});
}
queues = new Collection();
queue(guildId, resolver) {
const queue = this.queues.get(guildId);
if (queue) {
return queue;
}
const clazz = resolver ? resolver() : new Queue(this.node, guildId);
this.queues.set(guildId, clazz);
return clazz;
}
};
export {
AudioNodeEvent,
Node,
ParentProcessEvent,
Queue,
QueueEvent,
QueueManager,
RepeatMode,
WorkerOperation
};