magmastream
Version:
A user-friendly Lavalink client designed for NodeJS.
538 lines (537 loc) • 22.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MemoryQueue = void 0;
const Enums_1 = require("../structures/Enums");
const Utils_1 = require("../structures/Utils");
const MagmastreamError_1 = require("../structures/MagmastreamError");
/**
* The player's queue, the `current` property is the currently playing track, think of the rest as the up-coming tracks.
*/
class MemoryQueue extends Array {
/** The current track */
current = null;
/** The previous tracks */
previous = [];
/** The Manager instance. */
manager;
/** The guild ID property. */
guildId;
/**
* Whether the queue has been destroyed.
*/
destroyed = false;
/**
* Constructs a new Queue.
* @param guildId The guild ID.
* @param manager The Manager instance.
*/
constructor(guildId, manager) {
super();
/** The Manager instance. */
this.manager = manager;
/** The guild property. */
this.guildId = guildId;
}
// #region Public
/**
* Adds a track to the queue.
* @param track The track or tracks to add. Can be a single `Track` or an array of `Track`s.
* @param [offset=null] The position to add the track(s) at. If not provided, the track(s) will be added at the end of the queue.
*/
add(track, offset) {
try {
const isArray = Array.isArray(track);
const tracks = isArray ? [...track] : [track];
// Get the track info as a string
const trackInfo = isArray ? tracks.map((t) => Utils_1.JSONUtils.safe(t, 2)).join(", ") : Utils_1.JSONUtils.safe(track, 2);
// Emit a debug message
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Added ${tracks.length} track(s) to queue: ${trackInfo}`);
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
// If the queue is empty, set the track as the current track
if (!this.current) {
if (isArray) {
this.current = tracks.shift() || null;
this.push(...tracks);
}
else {
this.current = track;
}
}
else {
// If an offset is provided, add the track(s) at that position
if (typeof offset !== "undefined" && typeof offset === "number") {
// Validate the offset
if (isNaN(offset)) {
throw new RangeError("Offset must be a number.");
}
// Make sure the offset is between 0 and the length of the queue
if (offset < 0 || offset > this.length) {
throw new RangeError(`Offset must be between 0 and ${this.length}.`);
}
// Add the track(s) at the offset position
if (isArray) {
this.splice(offset, 0, ...tracks);
}
else {
this.splice(offset, 0, track);
}
}
else {
// If no offset is provided, add the track(s) at the end of the queue
if (isArray) {
this.push(...tracks);
}
else {
this.push(track);
}
}
}
if (this.manager.players.has(this.guildId) && this.manager.players.get(this.guildId).isAutoplay) {
if (!isArray) {
if (track.isAutoplay) {
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "autoPlayAdd",
tracks: [track],
},
});
return;
}
}
}
// Emit a player state update event with the added track(s)
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "add",
tracks: isArray ? tracks : [track],
},
});
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to add tracks to queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* Adds a track to the previous tracks.
* @param track The track or tracks to add. Can be a single `Track` or an array of `Track`s.
*/
addPrevious(track) {
try {
const max = this.manager.options.maxPreviousTracks;
this.previous = this.previous.filter((p) => p !== null);
if (Array.isArray(track)) {
const newTracks = track.filter((t) => !this.previous.some((p) => p?.identifier === t.identifier));
this.previous.push(...newTracks);
}
else {
const exists = this.previous.some((p) => p?.identifier === track.identifier);
if (!exists) {
this.previous.push(track);
}
}
if (this.previous.length > max) {
this.previous = this.previous.slice(-max);
}
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to add tracks to previous tracks for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* Clears the queue.
* This will remove all tracks from the queue and emit a state update event.
*/
clear() {
try {
// Capture the current state of the player for event emission.
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
// Remove all items from the queue.
this.splice(0);
// Emit an event to update the player state indicating the queue has been cleared.
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "clear",
tracks: [], // No tracks are left after clearing
},
});
// Emit a debug message indicating the queue has been cleared for a specific guild ID.
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Cleared the queue for: ${this.guildId}`);
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to clear queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* Clears the previous tracks.
*/
clearPrevious() {
this.previous = [];
}
/**
* Removes the first element from the queue.
*/
dequeue() {
return super.shift();
}
/**
* Destroys the queue and releases all resources.
* After calling this method, the queue must not be used again.
*/
destroy() {
if (this.destroyed)
return;
this.destroyed = true;
this.splice(0);
this.previous.length = 0;
this.current = null;
}
/**
* The total duration of the queue in milliseconds.
* This includes the duration of the currently playing track.
*/
duration() {
const current = this.current?.duration ?? 0;
return this.reduce((acc, cur) => acc + (cur.duration || 0), current);
}
/**
* Adds the specified track or tracks to the front of the queue.
* @param track The track or tracks to add.
*/
enqueueFront(track) {
if (Array.isArray(track)) {
this.unshift(...track);
}
else {
this.unshift(track);
}
}
/**
* @returns Whether all elements in the queue satisfy the provided testing function.
*/
everyAsync(callback) {
return this.every(callback);
}
/**
* @returns A new array with all elements that pass the test implemented by the provided function.
*/
filterAsync(callback) {
return this.filter(callback);
}
/**
* @returns The first element in the queue that satisfies the provided testing function.
*/
findAsync(callback) {
return this.find(callback);
}
/**
* @returns The current track.
*/
getCurrent() {
this.current = Utils_1.TrackUtils.revive(this.current);
return this.current;
}
/**
* @returns The previous tracks.
*/
getPrevious() {
this.previous = this.previous.map((t) => Utils_1.TrackUtils.revive(t));
return [...this.previous];
}
/**
* @returns The tracks in the queue from start to end.
*/
getSlice(start, end) {
return this.slice(start, end); // Native sync method, still wrapped in a Promise
}
/**
* @returns The tracks in the queue.
*/
getTracks() {
this.forEach((t) => Utils_1.TrackUtils.revive(t));
return [...this]; // clone to avoid direct mutation
}
/**
* @returns A new array with the results of calling a provided function on every element in the queue.
*/
mapAsync(callback) {
return this.map(callback);
}
/**
* Modifies the queue at the specified index.
* @param start The index at which to start modifying the queue.
* @param deleteCount The number of elements to remove from the queue.
* @param items The elements to add to the queue.
* @returns The modified queue.
*/
modifyAt(start, deleteCount = 0, ...items) {
return super.splice(start, deleteCount, ...items);
}
/**
* @returns The newest track.
*/
popPrevious() {
return this.previous.pop() || null; // get newest track
}
remove(startOrPosition = 0, end) {
try {
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
if (typeof end !== "undefined") {
// Validate input for `start` and `end`
if (isNaN(Number(startOrPosition)) || isNaN(Number(end))) {
throw new RangeError(`Invalid "start" or "end" parameter: start = ${startOrPosition}, end = ${end}`);
}
if (startOrPosition >= end || startOrPosition >= this.length) {
throw new RangeError("Invalid range: start should be less than end and within queue length.");
}
const removedTracks = this.splice(startOrPosition, end - startOrPosition);
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Removed ${removedTracks.length} track(s) from player: ${this.guildId} from position ${startOrPosition} to ${end}.`);
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "remove",
tracks: removedTracks,
},
});
return removedTracks;
}
// Single item removal when no end specified
const removedTrack = this.splice(startOrPosition, 1);
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Removed 1 track from player: ${this.guildId} from position ${startOrPosition}: ${Utils_1.JSONUtils.safe(removedTrack[0], 2)}`);
// Ensure removedTrack is an array for consistency
const tracksToEmit = removedTrack.length > 0 ? removedTrack : [];
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "remove",
tracks: tracksToEmit,
},
});
return removedTrack;
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to remove track(s) from queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* Shuffles the queue to play tracks requested by each user one by one.
*/
roundRobinShuffle() {
try {
// Capture the current state of the player for event emission.
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
// Group the tracks in the queue by the user that requested them.
const userTracks = new Map();
// Group the tracks in the queue by the user that requested them.
this.forEach((track) => {
const userId = track.requester.id.toString();
if (!userTracks.has(userId)) {
userTracks.set(userId, []);
}
userTracks.get(userId).push(track);
});
// Shuffle the tracks of each user.
userTracks.forEach((tracks) => {
for (let i = tracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[tracks[i], tracks[j]] = [tracks[j], tracks[i]];
}
});
// Create a new array for the shuffled queue.
const shuffledQueue = [];
// Add the shuffled tracks to the queue in a round-robin fashion.
const users = Array.from(userTracks.keys());
const userQueues = users.map((user) => userTracks.get(user));
const userCount = users.length;
while (userQueues.some((queue) => queue.length > 0)) {
for (let i = 0; i < userCount; i++) {
const queue = userQueues[i];
if (queue.length > 0) {
shuffledQueue.push(queue.shift());
}
}
}
// Clear the queue and add the shuffled tracks.
this.splice(0);
this.add(shuffledQueue);
// Emit an event to update the player state indicating the queue has been shuffled.
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "roundRobin",
},
});
// Emit a debug message indicating the queue has been shuffled for a specific guild ID.
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] roundRobinShuffled the queue for: ${this.guildId}`);
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to shuffle queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* @param track The track to set.
*/
setCurrent(track) {
this.current = track;
}
/**
* @param tracks The tracks to set.
*/
setPrevious(tracks) {
this.previous = [...tracks];
}
/**
* Shuffles the queue.
* This will randomize the order of the tracks in the queue and emit a state update event.
*/
shuffle() {
try {
// Capture the current state of the player for event emission.
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
// Shuffle the queue.
for (let i = this.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this[i], this[j]] = [this[j], this[i]];
}
// Emit an event to update the player state indicating the queue has been shuffled.
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "shuffle",
},
});
// Emit a debug message indicating the queue has been shuffled for a specific guild ID.
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] Shuffled the queue for: ${this.guildId}`);
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to shuffle queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
/**
* The size of tracks in the queue.
* This does not include the currently playing track.
* @returns The size of tracks in the queue.
*/
size() {
return this.length;
}
/**
* @returns Whether at least one element in the queue satisfies the provided testing function.
*/
someAsync(callback) {
return this.some(callback);
}
/**
* The total size of tracks in the queue including the current track.
* This includes the current track if it is not null.
* @returns The total size of tracks in the queue including the current track.
*/
totalSize() {
return this.length + (this.current ? 1 : 0);
}
/**
* Shuffles the queue to play tracks requested by each user one block at a time.
*/
userBlockShuffle() {
try {
// Capture the current state of the player for event emission.
const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null;
// Group the tracks in the queue by the user that requested them.
const userTracks = new Map();
this.forEach((track) => {
const userId = track.requester.id.toString();
if (!userTracks.has(userId)) {
userTracks.set(userId, []);
}
userTracks.get(userId).push(track);
});
// Create a new array for the shuffled queue.
const shuffledQueue = [];
// Iterate over the user tracks and add one track from each user to the shuffled queue.
// This will ensure that all the tracks requested by each user are played in a block order.
while (shuffledQueue.length < this.length) {
userTracks.forEach((tracks) => {
const track = tracks.shift();
if (track) {
shuffledQueue.push(track);
}
});
}
// Clear the queue and add the shuffled tracks.
this.splice(0);
this.add(shuffledQueue);
// Emit an event to update the player state indicating the queue has been shuffled.
this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), {
changeType: Enums_1.PlayerStateEventTypes.QueueChange,
details: {
type: "queue",
action: "userBlock",
},
});
// Emit a debug message indicating the queue has been shuffled for a specific guild ID.
this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[MEMORYQUEUE] userBlockShuffled the queue for: ${this.guildId}`);
}
catch (err) {
const error = err instanceof MagmastreamError_1.MagmaStreamError
? err
: new MagmastreamError_1.MagmaStreamError({
code: Enums_1.MagmaStreamErrorCode.QUEUE_MEMORY_ERROR,
message: `Failed to add tracks to queue for guild ${this.guildId}: ${err.message}`,
cause: err,
});
console.error(error);
}
}
}
exports.MemoryQueue = MemoryQueue;