magmastream
Version:
A user-friendly Lavalink client designed for NodeJS.
634 lines (633 loc) • 23.8 kB
JavaScript
"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;