UNPKG

magmastream

Version:

A user-friendly Lavalink client designed for NodeJS.

634 lines (633 loc) 23.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsonQueue = void 0; const tslib_1 = require("tslib"); const Enums_1 = require("../structures/Enums"); const path_1 = tslib_1.__importDefault(require("path")); const fs_1 = require("fs"); 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 JsonQueue { guildId; manager; /** * The base path for the queue files. */ basePath; /** * Whether the queue has been destroyed. */ destroyed = false; /** * @param guildId The guild ID. * @param manager The manager. */ constructor(guildId, manager) { this.guildId = guildId; this.manager = manager; const base = manager.options.stateStorage?.jsonConfig?.path ?? path_1.default.join(process.cwd(), "magmastream", "sessionData", "players"); this.basePath = path_1.default.join(base, this.guildId); } // #region Public /** * @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. */ async add(track, offset) { try { const isArray = Array.isArray(track); const inputTracks = isArray ? track : [track]; const tracks = [...inputTracks]; const queue = await this.getQueue(); const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; // Set first track as current if none is active if (!(await this.getCurrent())) { const current = tracks.shift(); if (current) { await this.setCurrent(current); } } if (typeof offset === "number" && !isNaN(offset)) { queue.splice(offset, 0, ...tracks); } else { queue.push(...tracks); } await this.setQueue(queue); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] Added ${tracks.length} track(s) to queue`); 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; } } } 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, }, }); } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to add tracks to JSON queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * @param track The track to add. */ async addPrevious(track) { try { const max = this.manager.options.maxPreviousTracks; const tracks = Array.isArray(track) ? track : [track]; if (!tracks.length) return; const current = (await this.getPrevious()).filter((p) => p !== null); const validTracks = tracks.filter((t) => t !== null && typeof t.uri === "string"); if (!validTracks.length) return; const newTracks = validTracks.filter((t) => !current.some((p) => p.uri === t.uri)); if (!newTracks.length) return; const updated = [...current, ...newTracks]; const trimmed = updated.slice(-max); await this.writeJSON(this.previousPath, trimmed); } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to add tracks to JSON queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Clears the queue. */ async clear() { try { const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; await this.deleteFile(this.queuePath); 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: [], }, }); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] 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_JSON_ERROR, message: `Failed to clear JSON queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Clears the previous tracks. */ async clearPrevious() { await this.deleteFile(this.previousPath); } /** * Removes the first track from the queue. */ async dequeue() { try { const queue = await this.getQueue(); const track = queue.shift(); await this.setQueue(queue); return track; } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to dequeue track for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Destroys the queue and releases all resources. * After calling this method, the queue must not be used again. */ async destroy() { if (this.destroyed) return; this.destroyed = true; try { await Promise.all([this.deleteFile(this.queuePath), this.deleteFile(this.currentPath), this.deleteFile(this.previousPath)]); } catch (err) { console.error(err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to destroy JSONQueue for guild ${this.guildId}`, cause: err, })); } } /** * @returns The total duration of the queue. */ async duration() { try { const queue = await this.getQueue(); const current = await this.getCurrent(); const currentDuration = current?.duration || 0; const total = queue.reduce((acc, track) => acc + (track.duration || 0), currentDuration); return total; } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to get duration for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Adds a track to the front of the queue. */ async enqueueFront(track) { try { const tracks = Array.isArray(track) ? track : [track]; const queue = await this.getQueue(); await this.setQueue([...tracks.reverse(), ...queue]); } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to enqueue front track for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Tests whether all elements in the queue pass the test implemented by the provided function. */ async everyAsync(callback) { const queue = await this.getQueue(); return queue.every(callback); } /** * Filters the queue. */ async filterAsync(callback) { const queue = await this.getQueue(); return queue.filter(callback); } /** * Finds the first track in the queue that satisfies the provided testing function. */ async findAsync(callback) { const queue = await this.getQueue(); return queue.find(callback); } /** * @returns The current track. */ async getCurrent() { const track = await this.readJSON(this.currentPath); return track ? Utils_1.TrackUtils.revive(track) : null; } /** * @returns The previous tracks. */ async getPrevious() { const data = await this.readJSON(this.previousPath); return Array.isArray(data) ? data.map(Utils_1.TrackUtils.revive) : []; } /** * @returns The tracks in the queue from start to end. */ async getSlice(start = 0, end = -1) { const queue = await this.getQueue(); if (end === -1) return queue.slice(start); return queue.slice(start, end); } /** * @returns The tracks in the queue. */ async getTracks() { return await this.getQueue(); } /** * Maps the queue to a new array. */ async mapAsync(callback) { const queue = await this.getQueue(); return queue.map(callback); } /** * Modifies the queue at the specified index. */ async modifyAt(start, deleteCount = 0, ...items) { const queue = await this.getQueue(); const removed = queue.splice(start, deleteCount, ...items); await this.setQueue(queue); return removed; } /** * @returns The newest track. */ async popPrevious() { try { const current = await this.getPrevious(); if (!current.length) return null; const popped = current.pop(); await this.writeJSON(this.previousPath, current); return popped; } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to pop previous track for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } async remove(startOrPos = 0, end) { try { const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; const queue = await this.getQueue(); let removed = []; if (typeof end === "number") { if (startOrPos >= end || startOrPos >= queue.length) throw new RangeError("Invalid range."); removed = queue.splice(startOrPos, end - startOrPos); } else { removed = queue.splice(startOrPos, 1); } await this.setQueue(queue); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] Removed ${removed.length} track(s) from position ${startOrPos}${end ? ` 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: removed, }, }); return removed; } catch (err) { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to remove track for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * Shuffles the queue by round-robin. */ async roundRobinShuffle() { try { const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; const queue = await this.getQueue(); const userMap = new Map(); for (const track of queue) { const userId = track.requester.id.toString(); if (!userMap.has(userId)) userMap.set(userId, []); userMap.get(userId).push(track); } // Shuffle each user's tracks for (const tracks of userMap.values()) { 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]]; } } const users = [...userMap.keys()]; const queues = users.map((id) => userMap.get(id)); const shuffledQueue = []; while (queues.some((q) => q.length > 0)) { for (const q of queues) { const track = q.shift(); if (track) shuffledQueue.push(track); } } await this.setQueue(shuffledQueue); this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), { changeType: Enums_1.PlayerStateEventTypes.QueueChange, details: { type: "queue", action: "roundRobin", }, }); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] 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_JSON_ERROR, message: `Failed to round robin shuffle queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * @param track The track to set. */ async setCurrent(track) { if (track) { await this.writeJSON(this.currentPath, track); } else { await this.deleteFile(this.currentPath); } } /** * @param track The track to set. */ async setPrevious(track) { const tracks = Array.isArray(track) ? track : [track]; if (!tracks.length) return; await this.writeJSON(this.previousPath, tracks); } /** * Shuffles the queue. */ async shuffle() { try { const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; const queue = await this.getQueue(); for (let i = queue.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [queue[i], queue[j]] = [queue[j], queue[i]]; } await this.setQueue(queue); this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), { changeType: Enums_1.PlayerStateEventTypes.QueueChange, details: { type: "queue", action: "shuffle", }, }); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] 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_JSON_ERROR, message: `Failed to shuffle queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } /** * @returns The size of the queue. */ async size() { const queue = await this.getQueue(); return queue.length; } /** * Tests whether at least one element in the queue passes the test implemented by the provided function. */ async someAsync(callback) { const queue = await this.getQueue(); return queue.some(callback); } /** * @returns The total size of the queue. */ async totalSize() { const size = await this.size(); return (await this.getCurrent()) ? size + 1 : size; } /** * Shuffles the queue by user. */ async userBlockShuffle() { try { const oldPlayer = this.manager.players.get(this.guildId) ? { ...this.manager.players.get(this.guildId) } : null; const queue = await this.getQueue(); const userMap = new Map(); for (const track of queue) { const userId = track.requester.id.toString(); if (!userMap.has(userId)) userMap.set(userId, []); userMap.get(userId).push(track); } const shuffledQueue = []; while (shuffledQueue.length < queue.length) { for (const [, tracks] of userMap) { const track = tracks.shift(); if (track) shuffledQueue.push(track); } } await this.setQueue(shuffledQueue); this.manager.emit(Enums_1.ManagerEventTypes.PlayerStateUpdate, oldPlayer, this.manager.players.get(this.guildId), { changeType: Enums_1.PlayerStateEventTypes.QueueChange, details: { type: "queue", action: "userBlock", }, }); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] 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_JSON_ERROR, message: `Failed to user block shuffle queue for guild ${this.guildId}: ${err.message}`, cause: err, }); console.error(error); } } // #endregion Public // #region Private /** * @returns The current path. */ get currentPath() { return path_1.default.join(this.basePath, "current.json"); } /** * @param filePath The file path. */ async deleteFile(filePath) { try { await fs_1.promises.unlink(filePath); } catch (err) { if (err.code !== "ENOENT") { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to delete file: ${filePath}`, cause: err, }); console.error(error); this.manager.emit(Enums_1.ManagerEventTypes.Debug, `[JSONQUEUE] Failed to delete file: ${filePath}`); } } } /** * Ensures the directory exists. */ async ensureDir() { await fs_1.promises.mkdir(this.basePath, { recursive: true }); } /** * @returns The queue. */ async getQueue() { const data = await this.readJSON(this.queuePath); return Array.isArray(data) ? data.map(Utils_1.TrackUtils.revive) : []; } /** * @returns The previous path. */ get previousPath() { return path_1.default.join(this.basePath, "previous.json"); } /** * @returns The queue path. */ get queuePath() { return path_1.default.join(this.basePath, "queue.json"); } /** * @param filePath The file path. * @returns The JSON data. */ async readJSON(filePath) { try { const raw = await fs_1.promises.readFile(filePath, "utf-8"); if (!raw || !raw.trim()) return null; return JSON.parse(raw); } catch (err) { if (err.code !== "ENOENT") { const error = err instanceof MagmastreamError_1.MagmaStreamError ? err : new MagmastreamError_1.MagmaStreamError({ code: Enums_1.MagmaStreamErrorCode.QUEUE_JSON_ERROR, message: `Failed to read file: ${filePath}`, cause: err, }); console.error(error); } return null; } } /** * @param queue The queue. */ async setQueue(queue) { await this.deleteFile(this.queuePath); await this.writeJSON(this.queuePath, queue); } /** * @param filePath The file path. * @param data The data to write. */ async writeJSON(filePath, data) { await this.ensureDir(); await fs_1.promises.writeFile(filePath, Utils_1.JSONUtils.safe(data, 2), "utf-8"); } } exports.JsonQueue = JsonQueue;