From 20a516aeb037940e35834fa8667f70b2cd6cdad2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FAY=E3=82=B7?= <103030954+FAYStarNext@users.noreply.github.com> Date: Sat, 10 Aug 2024 07:54:05 +0700 Subject: [PATCH 1/3] Refactor REST and Player classes for improved code organization and readability --- example/src/index.ts | 69 ++++++++------ package.json | 2 +- src/structures/Manager.ts | 183 ++++++++++++++++++++++++-------------- src/structures/Node.ts | 28 ++---- src/structures/Player.ts | 15 ++-- src/structures/Queue.ts | 4 +- src/structures/Rest.ts | 4 +- 7 files changed, 176 insertions(+), 129 deletions(-) diff --git a/example/src/index.ts b/example/src/index.ts index 617f6e4..955f93b 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -40,35 +40,54 @@ manager.on("PlayerCreate", (player) => { manager.on("NodeError" , (node, error) => { console.log(`Node ${node.options.host} has an error: ${error.message}`); }); + +// Helper function to handle the 'play' command +async function handlePlayCommand(message: any, args: string[]) { + if (!message.member?.voice.channel) return message.reply('you need to join a voice channel.'); + if (!args.length) return message.reply('you need to give me a URL or a search term.'); + + const search = args.join(' '); + const start = performance.now(); + let res; + let end; + + try { + res = await searchTracks(search); + end = `Time took: ${Math.round(performance.now() - start)}ms.`; + } catch (err) { + return message.reply(`there was an error while searching: ${err}`); + } + + if (res.loadType === 'error') return message.reply('there was no tracks found with that query.'); + + const player = manager.create({ + guild: message.guild?.id as string, + voiceChannel: message.member?.voice.channel.id, + textChannel: message.channel.id, + volume: 100, + }); + + player.connect(); + player.queue.add(res.tracks[0]); + if (!player.playing && !player.paused && !player.queue.size) player.play(); + + return message.reply(`enqueuing ${res.tracks[0].title}. ${end}`); +} + +// Helper function to handle the search logic +async function searchTracks(search: string) { + const res = await manager.search({ query: search }); + if (res.loadType === 'empty') throw res; + if (res.loadType === 'playlist') throw Error('Playlists are not supported with this command.'); + return res; +} + client.on("messageCreate", async (message) => { if (message.author.bot) return; - const start = performance.now(); + const [command, ...args] = message.content.slice(0).split(/\s+/g); if (command === 'play') { - if (!message.member?.voice.channel) return message.reply('you need to join a voice channel.'); - if (!args.length) return message.reply('you need to give me a URL or a search term.'); - const search = args.join(' '); - let res; - let end; - try { - res = await manager.search({query: search}); - end = `Time took: ${Math.round(performance.now() - start)}ms.`; - if (res.loadType === 'empty') throw res; - if (res.loadType === 'playlist') throw Error('Playlists are not supported with this command.'); - } catch (err) { - return message.reply(`there was an error while searching: ${err}`); - } - if (res.loadType === 'error') return message.reply('there was no tracks found with that query.'); - const player = manager.create({ - guild: message.guild?.id as string, - voiceChannel: message.member?.voice.channel.id, - textChannel: message.channel.id, - volume: 100, - }); - player.connect(); - player.queue.add(res.tracks[0]); - if (!player.playing && !player.paused && !player.queue.size) player.play(); - return message.reply(`enqueuing ${res.tracks[0].title}. ${end}`); + await handlePlayCommand(message, args); } }); manager.on("SearchCacheClear" , (data) => { diff --git a/package.json b/package.json index aeda9ba..26b61b6 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "sunday.ts", "main": "dist/index.js", "types": "dist/index.d.ts", - "version": "1.0.16", + "version": "1.0.17", "description": "Sunday a lavalink wrapper", "license": "MIT", "author": "FAYStarNext", diff --git a/src/structures/Manager.ts b/src/structures/Manager.ts index 1d06448..b0193f2 100644 --- a/src/structures/Manager.ts +++ b/src/structures/Manager.ts @@ -143,7 +143,7 @@ export class Manager extends TypedEmitter { } if (this.options.nodes) this.options.nodes.forEach((nodeOptions) => { return new (Structure.get("Node"))(nodeOptions); }); - if (this.options.caches.enabled){ + if (this.options.caches.enabled) { setInterval(() => { if (this.search_cache.clear() === undefined) return; this.emit("SearchCacheClear", this.search_cache.values().next().value); @@ -180,96 +180,135 @@ export class Manager extends TypedEmitter { * @param requester * @returns The search result. */ + // TypeScript public async search(options: SearchQuery): Promise { const node = this.useableNodes; if (!node) throw new Error("No available nodes."); + const { search, code } = this.prepareQuery(options); + const cachedResult = this.getCachedResult(options, code); + if (cachedResult) return cachedResult; + + try { + const res = await this.fetchTracks(node, search); + const result = this.handleResponse(res, options); + + if (options.cache !== false && this.options.caches.enabled !== false) { + this.cacheResult(res, code, result); + } + + return result; + } catch (err) { + throw new Error(err); + } + } + + private prepareQuery(options: SearchQuery): { search: string, code: string } { const _query: SearchQuery = typeof options.query === "string" ? { query: options.query } : options.query; const _source = Manager.DEFAULT_SOURCES[_query.source ?? this.options.defaultSearchPlatform] ?? _query.source; let search = _query.query; let code = this.CheckURL(options.query); if (!/^https?:\/\//.test(search)) search = `${_source}:${search}`; + return { search, code }; + } + + private getCachedResult(options: SearchQuery, code: string): SearchResult | null { if (options.cache !== false && this.options.caches.enabled !== false) { - if (this.search_cache.get(code)) return this.search_cache.get(code); + const cachedResult = this.search_cache.get(code); + if (cachedResult) return cachedResult; } - try { - const res = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`)) as LavalinkResponse; - if (!res) throw new Error("Query not found."); - - let searchData = []; - let playlistData: PlaylistRawData | undefined; - switch (res.loadType) { - case "search": - searchData = res.data as TrackData[]; - break; - case "track": - searchData = [res.data as TrackData[]]; - break; - case "playlist": - playlistData = res.data as PlaylistRawData; - break; - } + return null; + } - const tracks = searchData.map((track) => TrackUtils.build(track, options.requester)); - let playlist = null; + private async fetchTracks(node: any, search: string): Promise { + const res = await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`) as LavalinkResponse; + if (!res) throw new Error("Query not found."); + return res; + } - if (res.loadType === "playlist") { - playlist = { - name: playlistData!.info.name, - tracks: playlistData!.tracks.map((track) => TrackUtils.build(track, options.requester)), - duration: playlistData!.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0), - }; - } + private handleResponse(res: LavalinkResponse, options: SearchQuery): SearchResult { + let searchData = []; + let playlistData: PlaylistRawData | undefined; + + switch (res.loadType) { + case "search": + searchData = res.data as TrackData[]; + break; + case "track": + searchData = [res.data as TrackData[]]; + break; + case "playlist": + playlistData = res.data as PlaylistRawData; + break; + } + + const tracks = searchData.map((track) => TrackUtils.build(track, options.requester)); + let playlist = null; - const result: SearchResult = { - loadType: res.loadType, - tracks, - playlist, + if (res.loadType === "playlist") { + playlist = { + name: playlistData.info.name, + tracks: playlistData.tracks.map((track) => TrackUtils.build(track, options.requester)), + duration: playlistData.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0), }; + } - if (this.options.replaceYouTubeCredentials) { - let tracksToReplace: Track[] = []; - if (result.loadType === "playlist") { - tracksToReplace = result.playlist!.tracks; - } else { - tracksToReplace = result.tracks; - } + const result: SearchResult = { + loadType: res.loadType, + tracks, + playlist, + }; + + this.replaceYouTubeCredentials(result); + + return result; + } - for (const track of tracksToReplace) { - if (this.YOUTUBE_REGEX.test(track.uri)) { - track.author = track.author.replace("- Topic", ""); - track.title = track.title.replace("Topic -", ""); - } - if (track.title.includes("-")) { - const [author, title] = track.title.split("-").map((str: string) => str.trim()); - track.author = author; - track.title = title; - } + private replaceYouTubeCredentials(result: SearchResult): void { + if (this.options.replaceYouTubeCredentials) { + let tracksToReplace: Track[] = []; + if (result.loadType === "playlist") { + tracksToReplace = result.playlist.tracks; + } else { + tracksToReplace = result.tracks; + } + + for (const track of tracksToReplace) { + if (this.YOUTUBE_REGEX.test(track.uri)) { + track.author = track.author.replace("- Topic", ""); + track.title = track.title.replace("Topic -", ""); + } + if (track.title.includes("-")) { + const [author, title] = track.title.split("-").map((str: string) => str.trim()); + track.author = author; + track.title = title; } } - if (options.cache !== false && this.options.caches.enabled !== false) if (res.loadType === "search" || "track") this.search_cache.set(code, result); - return result; - } catch (err) { - throw new Error(err); + } + } + + private cacheResult(res: LavalinkResponse, code: string, result: SearchResult): void { + if (res.loadType === "search" || res.loadType === "track") { + this.search_cache.set(code, result); } } private CheckURL(uri: string): string { let data = this.regex_link(uri); - switch (data) { - case "yt": - const videoCode = uri.match(/v=([^&]+)/)?.[1]; - const playlistCode = uri.match(/list=([^&]+)/)?.[1]; - if (playlistCode) { - return "yt:playlist:" + playlistCode; - } - return "yt:" + (videoCode ?? ""); + if (!data) return uri; + if (data === "yt") { + const videoCode = uri.match(/v=([^&]+)/)?.[1]; + const playlistCode = uri.match(/list=([^&]+)/)?.[1]; + if (playlistCode) { + return `yt:playlist:${playlistCode}`; + } + return "yt:" + (videoCode ?? ""); } return uri; } - + private isLink(link: string) { return /^(https?:\/\/)?(www\.)?([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)\/.+$/.test(link); } - + private regex_link(link: string) { if (this.YOUTUBE_REGEX.test(link)) return "yt"; if (this.SOUNDCLOUD_REGEX.test(link)) return "sc"; @@ -277,7 +316,7 @@ export class Manager extends TypedEmitter { if (this.isLink(link)) return "http"; return null; } - + /** * Decodes the base64 encoded tracks and returns a TrackData array. * @param tracks @@ -395,9 +434,17 @@ export class Manager extends TypedEmitter { this.emit("PlayerDisconnect", player, player.voiceChannel); player.voiceChannel = null; - player.voiceState = Object.assign({}); - player.destroy(); - return; + player.voiceState = { + channel_id: null, + guildId: undefined, + sessionId: null, + event: null, + op: "voiceUpdate", + session_id: null, + user_id: null, + guild_id: null, + } + return player.destroy(); } } @@ -456,7 +503,7 @@ string literals. The type can only have one of the specified values: export interface SearchQuery { /** The source to search from. */ - source?: SearchPlatform | string; + source?: SearchPlatform; /** The query to search for. */ query: string; requester?: User | ClientUser; diff --git a/src/structures/Node.ts b/src/structures/Node.ts index d237247..ba72d29 100644 --- a/src/structures/Node.ts +++ b/src/structures/Node.ts @@ -44,12 +44,9 @@ export class Node { if (!this.manager) this.manager = Structure.get("Node")._manager; if (!this.manager) throw new RangeError("Manager has not been initiated."); - if (this.manager.nodes.has(options.identifier || options.host)) { - return this.manager.nodes.get(options.identifier || options.host); - } + if (this.manager.nodes.has(options.identifier || options.host)) return this.manager.nodes.get(options.identifier || options.host); nodeCheck(options); - this.options = { port: 2333, password: "youshallnotpass", @@ -96,12 +93,12 @@ export class Node { public connect(): void { if (this.connected) return; - const headers = Object.assign({ + const headers = { Authorization: this.options.password, "Num-Shards": String(this.manager.options.shards), "User-Id": this.manager.options.clientId, "Client-Name": this.manager.options.clientName, - }); + }; this.socket = new WebSocket(`ws${this.options.secure ? "s" : ""}://${this.address}/v4/websocket`, { headers }); this.socket.on("open", this.open.bind(this)); @@ -276,38 +273,27 @@ export class Node { // Handle autoplay private async handleAutoplay(player: Player, track: Track) { const previousTrack = player.queue.previous; - if (!player.isAutoplay || !previousTrack) return; - const hasYouTubeURL = ["youtube.com", "youtu.be"].some((url) => previousTrack.uri.includes(url)); - let videoID = previousTrack.uri.substring(previousTrack.uri.indexOf("=") + 1); if (!hasYouTubeURL) { const res = await player.search(`${previousTrack.author} - ${previousTrack.title}`); - videoID = res.tracks[0].uri.substring(res.tracks[0].uri.indexOf("=") + 1); } - let randomIndex: number; let searchURI: string; - do { randomIndex = Math.floor(Math.random() * 23) + 2; searchURI = `https://www.youtube.com/watch?v=${videoID}&list=RD${videoID}&index=${randomIndex}`; } while (track.uri.includes(searchURI)); const res = await player.search(searchURI, player.get("Internal_BotUser")); - if (res.loadType === "empty" || res.loadType === "error") return; - let tracks = res.tracks; - - if (res.loadType === "playlist") { - tracks = res.playlist.tracks; - } - - const foundTrack = tracks.sort(() => Math.random() - 0.5).find((shuffledTrack) => shuffledTrack.uri !== track.uri); + if (res.loadType === "playlist") tracks = res.playlist.tracks; + const sortedTracks = tracks.toSorted(() => Math.random() - 0.5); + const foundTrack = sortedTracks.find((shuffledTrack) => shuffledTrack.uri !== track.uri); if (foundTrack) { player.queue.add(foundTrack); @@ -345,7 +331,7 @@ export class Node { this.manager.emit("TrackEnd", player, track, payload); - if (payload.reason === "stopped" && !(queue.current = queue.shift())) { + if (payload.reason === "stopped") { this.queueEnd(player, track, payload); return; } diff --git a/src/structures/Player.ts b/src/structures/Player.ts index 31f1aa8..46fcfd0 100644 --- a/src/structures/Player.ts +++ b/src/structures/Player.ts @@ -8,7 +8,7 @@ import { ClientUser, Message, User } from "discord.js"; export class Player { /** The Queue for the Player. */ - public readonly queue = new (Structure.get("Queue"))() as Queue; + public readonly queue: Queue = new (Structure.get("Queue"))(); /** The filters applied to the audio. */ public filters: Filters; /** Whether the queue repeats the track. */ @@ -80,9 +80,7 @@ export class Player { if (!this.manager) this.manager = Structure.get("Player")._manager; if (!this.manager) throw new RangeError("Manager has not been initiated."); - if (this.manager.players.has(options.guild)) { - return this.manager.players.get(options.guild); - } + if (this.manager.players.has(options.guild)) return this.manager.players.get(options.guild); playerCheck(options); @@ -97,9 +95,7 @@ export class Player { const node = this.manager.nodes.get(options.node); this.node = node || this.manager.useableNodes; - if (!this.node) throw new RangeError("No available nodes."); - this.manager.players.set(options.guild, this); this.manager.emit("PlayerCreate", this); this.setVolume(options.volume ?? 100); @@ -196,10 +192,9 @@ export class Player { /** Sets the now playing message. */ public setNowPlayingMessage(message: Message): Message { - if (!message) { - throw new TypeError("You must provide the message of the now playing message."); - } - return (this.nowPlayingMessage = message); + if (!message) throw new TypeError("You must provide the message of the now playing message."); + this.nowPlayingMessage = message; + return message; } /** Plays the next track. */ diff --git a/src/structures/Queue.ts b/src/structures/Queue.ts index d5358b2..c5110f1 100644 --- a/src/structures/Queue.ts +++ b/src/structures/Queue.ts @@ -36,7 +36,6 @@ export class Queue extends Array { if (!TrackUtils.validate(track)) { throw new RangeError('Track must be a "Track" or "Track[]".'); } - if (!this.current) { if (Array.isArray(track)) { this.current = track.shift() || null; @@ -60,7 +59,8 @@ export class Queue extends Array { this.splice(offset, 0, track); } } else { - if (Array.isArray(track)) { + const isArray = Array.isArray(track); + if (isArray) { this.push(...track); } else { this.push(track); diff --git a/src/structures/Rest.ts b/src/structures/Rest.ts index 150f30d..17582b1 100644 --- a/src/structures/Rest.ts +++ b/src/structures/Rest.ts @@ -33,7 +33,7 @@ export class Rest { } /** Sends a PATCH request to update player related data. */ - public async updatePlayer(options: playOptions): Promise { + public async updatePlayer(options: PlayOptions): Promise { return await this.patch(`/v4/sessions/${this.sessionId}/players/${options.guildId}?noReplace=false`, options.data); } @@ -85,7 +85,7 @@ export class Rest { } } -interface playOptions { +interface PlayOptions { guildId: string; data: { /** The base64 encoded track. */ From 6db42a5080ea1a23212506669df922fab0bd554a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FAY=E3=82=B7?= <103030954+FAYStarNext@users.noreply.github.com> Date: Sat, 10 Aug 2024 08:01:52 +0700 Subject: [PATCH 2/3] Refactor REST and Player classes for improved code organization and readability --- eslint.config.js | 8 ++++---- package.json | 12 ++++++------ src/structures/Manager.ts | 6 +++--- src/structures/Node.ts | 2 -- src/structures/Utils.ts | 8 ++++---- 5 files changed, 17 insertions(+), 19 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 984be61..9a7ea4e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,9 @@ -import globals from "globals"; -import pluginJs from "@eslint/js"; -import tseslint from "typescript-eslint"; +const globals = require("globals"); +const pluginJs = require( "@eslint/js"); +const tseslint = require( "typescript-eslint"); -export default [ +module.exports = [ { files: ["**/*.{js,mjs,cjs,ts}"] }, { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } }, { languageOptions: { globals: globals.browser } }, diff --git a/package.json b/package.json index 26b61b6..01e8c77 100644 --- a/package.json +++ b/package.json @@ -14,14 +14,14 @@ "test:player": "bun test/player.ts" }, "devDependencies": { - "@eslint/js": "^9.7.0", + "@eslint/js": "^9.9.0", "@types/bun": "latest", - "eslint": "^9.7.0", - "globals": "^15.8.0", - "typescript-eslint": "^8.0.0", + "eslint": "^9.9.0", + "globals": "^15.9.0", + "typescript-eslint": "^8.0.1", "@babel/cli": "^7.24.8", - "@babel/core": "^7.24.9", - "@babel/preset-env": "^7.24.8", + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.25.3", "@babel/preset-typescript": "^7.24.7" }, "publishConfig": { diff --git a/src/structures/Manager.ts b/src/structures/Manager.ts index b0193f2..f3fb71a 100644 --- a/src/structures/Manager.ts +++ b/src/structures/Manager.ts @@ -206,7 +206,7 @@ export class Manager extends TypedEmitter { const _query: SearchQuery = typeof options.query === "string" ? { query: options.query } : options.query; const _source = Manager.DEFAULT_SOURCES[_query.source ?? this.options.defaultSearchPlatform] ?? _query.source; let search = _query.query; - let code = this.CheckURL(options.query); + const code = this.CheckURL(options.query); if (!/^https?:\/\//.test(search)) search = `${_source}:${search}`; return { search, code }; } @@ -219,7 +219,7 @@ export class Manager extends TypedEmitter { return null; } - private async fetchTracks(node: any, search: string): Promise { + private async fetchTracks(node: Node, search: string): Promise { const res = await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`) as LavalinkResponse; if (!res) throw new Error("Query not found."); return res; @@ -292,7 +292,7 @@ export class Manager extends TypedEmitter { } } private CheckURL(uri: string): string { - let data = this.regex_link(uri); + const data = this.regex_link(uri); if (!data) return uri; if (data === "yt") { const videoCode = uri.match(/v=([^&]+)/)?.[1]; diff --git a/src/structures/Node.ts b/src/structures/Node.ts index ba72d29..c5cc0e1 100644 --- a/src/structures/Node.ts +++ b/src/structures/Node.ts @@ -325,10 +325,8 @@ export class Node { } else if (queueRepeat) { queue.add(queue.current); } - queue.previous = queue.current; queue.current = queue.shift(); - this.manager.emit("TrackEnd", player, track, payload); if (payload.reason === "stopped") { diff --git a/src/structures/Utils.ts b/src/structures/Utils.ts index c1e9b0b..00fa1b8 100644 --- a/src/structures/Utils.ts +++ b/src/structures/Utils.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unused-vars, @typescript-eslint/no-var-requires*/ +/* eslint-disable @typescript-eslint/no-unused-vars*/ import { ClientUser, User } from "discord.js"; import { Manager } from "./Manager"; import { Node, NodeStats } from "./Node"; @@ -219,9 +219,9 @@ export class Plugin { } const structures = { - Player: require("./Player").Player, - Queue: require("./Queue").Queue, - Node: require("./Node").Node, + Player: Player, + Queue: Queue, + Node: Node, }; export interface UnresolvedQuery { From eec6e4e3d69af133840724e1111675f5d97f7e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?FAY=E3=82=B7?= <103030954+FAYStarNext@users.noreply.github.com> Date: Sat, 10 Aug 2024 08:03:17 +0700 Subject: [PATCH 3/3] chore: Remove unused "node-fetch" dependency --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 01e8c77..e575b32 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,6 @@ "dependencies": { "@discordjs/collection": "^2.1.0", "discord.js": "^14.15.3", - "node-fetch": "^3.3.2", "tiny-typed-emitter": "^2.1.0", "ws": "^8.18.0" },