UNPKG

lavalink-client

Version:

Easy, flexible and feature-rich lavalink@v4 Client. Both for Beginners and Proficients.

384 lines (383 loc) 17.1 kB
import { ManagerUtils, MiniMap, QueueSymbol } from "./Utils.js"; export class QueueSaver { /** * The queue store manager */ _; /** * The options for the queue saver */ options; constructor(options) { this._ = options?.queueStore || new DefaultQueueStore(); this.options = { maxPreviousTracks: options?.maxPreviousTracks || 25, }; } /** * Get the queue for a guild * @param guildId The guild ID * @returns The queue for the guild */ async get(guildId) { return this._.parse(await this._.get(guildId)); } /** * Delete the queue for a guild * @param guildId The guild ID * @returns The queue for the guild */ async delete(guildId) { return this._.delete(guildId); } /** * Set the queue for a guild * @param guildId The guild ID * @param valueToStringify The queue to set * @returns The queue for the guild */ async set(guildId, valueToStringify) { return this._.set(guildId, await this._.stringify(valueToStringify)); } /** * Sync the queue for a guild * @param guildId The guild ID * @returns The queue for the guild */ async sync(guildId) { return this.get(guildId); } } export class DefaultQueueStore { data = new MiniMap(); constructor() { } /** * Get the queue for a guild * @param guildId The guild ID * @returns The queue for the guild */ async get(guildId) { return this.data.get(guildId); } /** * Set the queue for a guild * @param guildId The guild ID * @param valueToStringify The queue to set * @returns The queue for the guild */ async set(guildId, valueToStringify) { return this.data.set(guildId, valueToStringify) ? true : false; } /** * Delete the queue for a guild * @param guildId The guild ID * @returns The queue for the guild */ async delete(guildId) { return this.data.delete(guildId); } /** * Stringify the queue for a guild * @param value The queue to stringify * @returns The stringified queue */ async stringify(value) { return value; // JSON.stringify(value); } /** * Parse the queue for a guild * @param value The queue to parse * @returns The parsed queue */ async parse(value) { return value; // JSON.parse(value) } } export class Queue { tracks = []; previous = []; current = null; options = { maxPreviousTracks: 25 }; guildId = ""; QueueSaver = null; managerUtils = new ManagerUtils(); queueChanges; /** * Create a new Queue * @param guildId The guild ID * @param data The data to initialize the queue with * @param QueueSaver The queue saver to use * @param queueOptions */ constructor(guildId, data = {}, QueueSaver, queueOptions) { this.queueChanges = queueOptions.queueChangesWatcher || null; this.guildId = guildId; this.QueueSaver = QueueSaver; this.options.maxPreviousTracks = this.QueueSaver?.options?.maxPreviousTracks ?? this.options.maxPreviousTracks; this.current = this.managerUtils.isTrack(data.current) ? data.current : null; this.previous = Array.isArray(data.previous) && data.previous.some(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track)) ? data.previous.filter(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track)) : []; this.tracks = Array.isArray(data.tracks) && data.tracks.some(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track)) ? data.tracks.filter(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track)) : []; Object.defineProperty(this, QueueSymbol, { configurable: true, value: true }); } /** * Utils for a Queue */ utils = { /** * Save the current cached Queue on the database/server (overides the server) */ save: async () => { if (this.previous.length > this.options.maxPreviousTracks) this.previous.splice(this.options.maxPreviousTracks, this.previous.length); return await this.QueueSaver.set(this.guildId, this.utils.toJSON()); }, /** * Sync the current queue database/server with the cached one * @returns {void} */ sync: async (override = true, dontSyncCurrent = true) => { const data = await this.QueueSaver.get(this.guildId); if (!data) throw new Error(`No data found to sync for guildId: ${this.guildId}`); if (!dontSyncCurrent && !this.current && (this.managerUtils.isTrack(data.current))) this.current = data.current; if (Array.isArray(data.tracks) && data?.tracks.length && data.tracks.some(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track))) this.tracks.splice(override ? 0 : this.tracks.length, override ? this.tracks.length : 0, ...data.tracks.filter(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track))); if (Array.isArray(data.previous) && data?.previous.length && data.previous.some(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track))) this.previous.splice(0, override ? this.tracks.length : 0, ...data.previous.filter(track => this.managerUtils.isTrack(track) || this.managerUtils.isUnresolvedTrack(track))); await this.utils.save(); return; }, destroy: async () => { return await this.QueueSaver.delete(this.guildId); }, /** * @returns {{current:Track|null, previous:Track[], tracks:Track[]}}The Queue, but in a raw State, which allows easier handling for the QueueStoreManager */ toJSON: () => { if (this.previous.length > this.options.maxPreviousTracks) this.previous.splice(this.options.maxPreviousTracks, this.previous.length); return { current: this.current ? { ...this.current } : null, previous: this.previous ? [...this.previous] : [], tracks: this.tracks ? [...this.tracks] : [], }; }, /** * Get the Total Duration of the Queue-Songs summed up * @returns {number} */ totalDuration: () => { return this.tracks.reduce((acc, cur) => acc + (cur.info.duration || 0), this.current?.info.duration || 0); } }; /** * Shuffles the current Queue, then saves it * @returns Amount of Tracks in the Queue */ async shuffle() { const oldStored = typeof this.queueChanges?.shuffled === "function" ? this.utils.toJSON() : null; if (this.tracks.length <= 1) return this.tracks.length; // swap #1 and #2 if only 2 tracks. if (this.tracks.length === 2) { [this.tracks[0], this.tracks[1]] = [this.tracks[1], this.tracks[0]]; } else { // randomly swap places. for (let i = this.tracks.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.tracks[i], this.tracks[j]] = [this.tracks[j], this.tracks[i]]; } } // LOG if (typeof this.queueChanges?.shuffled === "function") this.queueChanges.shuffled(this.guildId, oldStored, this.utils.toJSON()); await this.utils.save(); return this.tracks.length; } /** * Add a Track to the Queue, and after saved in the "db" it returns the amount of the Tracks * @param {Track | Track[]} TrackOrTracks * @param {number} index At what position to add the Track * @returns {number} Queue-Size (for the next Tracks) */ async add(TrackOrTracks, index) { if (typeof index === "number" && index >= 0 && index < this.tracks.length) return await this.splice(index, 0, ...(Array.isArray(TrackOrTracks) ? TrackOrTracks : [TrackOrTracks]).filter(v => this.managerUtils.isTrack(v) || this.managerUtils.isUnresolvedTrack(v))); const oldStored = typeof this.queueChanges?.tracksAdd === "function" ? this.utils.toJSON() : null; // add the track(s) this.tracks.push(...(Array.isArray(TrackOrTracks) ? TrackOrTracks : [TrackOrTracks]).filter(v => this.managerUtils.isTrack(v) || this.managerUtils.isUnresolvedTrack(v))); // log if available if (typeof this.queueChanges?.tracksAdd === "function") try { this.queueChanges.tracksAdd(this.guildId, (Array.isArray(TrackOrTracks) ? TrackOrTracks : [TrackOrTracks]).filter(v => this.managerUtils.isTrack(v) || this.managerUtils.isUnresolvedTrack(v)), this.tracks.length, oldStored, this.utils.toJSON()); } catch { /* */ } // save the queue await this.utils.save(); // return the amount of the tracks return this.tracks.length; } /** * Splice the tracks in the Queue * @param {number} index Where to remove the Track * @param {number} amount How many Tracks to remove? * @param {Track | Track[]} TrackOrTracks Want to Add more Tracks? * @returns {Track} Spliced Track */ async splice(index, amount, TrackOrTracks) { const oldStored = typeof this.queueChanges?.tracksAdd === "function" || typeof this.queueChanges?.tracksRemoved === "function" ? this.utils.toJSON() : null; // if no tracks to splice, add the tracks if (!this.tracks.length) { if (TrackOrTracks) return await this.add(TrackOrTracks); return null; } // Log if available if ((TrackOrTracks) && typeof this.queueChanges?.tracksAdd === "function") try { this.queueChanges.tracksAdd(this.guildId, (Array.isArray(TrackOrTracks) ? TrackOrTracks : [TrackOrTracks]).filter(v => this.managerUtils.isTrack(v) || this.managerUtils.isUnresolvedTrack(v)), index, oldStored, this.utils.toJSON()); } catch { /* */ } // remove the tracks (and add the new ones) let spliced = TrackOrTracks ? this.tracks.splice(index, amount, ...(Array.isArray(TrackOrTracks) ? TrackOrTracks : [TrackOrTracks]).filter(v => this.managerUtils.isTrack(v) || this.managerUtils.isUnresolvedTrack(v))) : this.tracks.splice(index, amount); // get the spliced array spliced = (Array.isArray(spliced) ? spliced : [spliced]); // Log if available if (typeof this.queueChanges?.tracksRemoved === "function") try { this.queueChanges.tracksRemoved(this.guildId, spliced, index, oldStored, this.utils.toJSON()); } catch { /* */ } // save the queue await this.utils.save(); // return the things return spliced.length === 1 ? spliced[0] : spliced; } /** * Remove stuff from the queue.tracks array * - single Track | UnresolvedTrack * - multiple Track | UnresovedTrack * - at the index or multiple indexes * @param removeQueryTrack * @returns null (if nothing was removed) / { removed } where removed is an array with all removed elements * * @example * ```js * // remove single track * * const track = player.queue.tracks[4]; * await player.queue.remove(track); * * // if you already have the index you can straight up pass it too * await player.queue.remove(4); * * * // if you want to remove multiple tracks, e.g. from position 4 to position 10 you can do smt like this * await player.queue.remove(player.queue.tracks.slice(4, 10)) // get's the tracks from 4 - 10, which then get's found in the remove function to be removed * * // I still highly suggest to use .splice! * * await player.queue.splice(4, 10); // removes at index 4, 10 tracks * * await player.queue.splice(1, 1); // removes at index 1, 1 track * * await player.queue.splice(4, 0, ...tracks) // removes 0 tracks at position 4, and then inserts all tracks after position 4. * ``` */ async remove(removeQueryTrack) { const oldStored = typeof this.queueChanges?.tracksRemoved === "function" ? this.utils.toJSON() : null; if (typeof removeQueryTrack === "number") { const toRemove = this.tracks[removeQueryTrack]; if (!toRemove) return null; const removed = this.tracks.splice(removeQueryTrack, 1); // Log if available if (typeof this.queueChanges?.tracksRemoved === "function") try { this.queueChanges.tracksRemoved(this.guildId, removed, removeQueryTrack, oldStored, this.utils.toJSON()); } catch { /* */ } await this.utils.save(); return { removed }; } if (Array.isArray(removeQueryTrack)) { if (removeQueryTrack.every(v => typeof v === "number")) { const removed = []; for (const i of removeQueryTrack) { if (this.tracks[i]) { removed.push(...this.tracks.splice(i, 1)); } } if (!removed.length) return null; // Log if available if (typeof this.queueChanges?.tracksRemoved === "function") try { this.queueChanges.tracksRemoved(this.guildId, removed, removeQueryTrack, oldStored, this.utils.toJSON()); } catch { /* */ } await this.utils.save(); return { removed }; } const tracksToRemove = this.tracks.map((v, i) => ({ v, i })).filter(({ v, i }) => removeQueryTrack.find(t => typeof t === "number" && (t === i) || typeof t === "object" && (t.encoded && t.encoded === v.encoded || t.info?.identifier && t.info.identifier === v.info?.identifier || t.info?.uri && t.info.uri === v.info?.uri || t.info?.title && t.info.title === v.info?.title || t.info?.isrc && t.info.isrc === v.info?.isrc || t.info?.artworkUrl && t.info.artworkUrl === v.info?.artworkUrl))); if (!tracksToRemove.length) return null; const removed = []; for (const { i } of tracksToRemove) { if (this.tracks[i]) { removed.push(...this.tracks.splice(i, 1)); } } // Log if available if (typeof this.queueChanges?.tracksRemoved === "function") try { this.queueChanges.tracksRemoved(this.guildId, removed, tracksToRemove.map(v => v.i), oldStored, this.utils.toJSON()); } catch { /* */ } await this.utils.save(); return { removed }; } const toRemove = this.tracks.findIndex((v) => removeQueryTrack.encoded && removeQueryTrack.encoded === v.encoded || removeQueryTrack.info?.identifier && removeQueryTrack.info.identifier === v.info?.identifier || removeQueryTrack.info?.uri && removeQueryTrack.info.uri === v.info?.uri || removeQueryTrack.info?.title && removeQueryTrack.info.title === v.info?.title || removeQueryTrack.info?.isrc && removeQueryTrack.info.isrc === v.info?.isrc || removeQueryTrack.info?.artworkUrl && removeQueryTrack.info.artworkUrl === v.info?.artworkUrl); if (toRemove < 0) return null; const removed = this.tracks.splice(toRemove, 1); // Log if available if (typeof this.queueChanges?.tracksRemoved === "function") try { this.queueChanges.tracksRemoved(this.guildId, removed, toRemove, oldStored, this.utils.toJSON()); } catch { /* */ } await this.utils.save(); return { removed }; } /** * Shifts the previous array, to return the last previous track & thus remove it from the previous queue * @returns * * @example * ```js * // example on how to play the previous track again * const previous = await player.queue.shiftPrevious(); // get the previous track and remove it from the previous queue array!! * if(!previous) return console.error("No previous track found"); * await player.play({ clientTrack: previous }); // play it again * ``` */ async shiftPrevious() { const removed = this.previous.shift(); if (removed) await this.utils.save(); return removed ?? null; } }