UNPKG

magmastream

Version:

A user-friendly Lavalink client designed for NodeJS.

538 lines (537 loc) 22.6 kB
"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;