diff --git a/README.md b/README.md index 1eae20e..9c93d9c 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,6 @@ manager.on("NodeConnect", (node) => { manager.on("NodeRaw", async (node) => { console.log(`sent raw data: ${JSON.stringify(node)}`); }); -client.on("ready", () => { - manager.init(); -}); manager.on("PlayerCreate", (player) => { console.log(`Player created in guild ${player.guild}`); }); @@ -106,19 +103,19 @@ manager.on("NodeError" , (node, error) => { console.log(`Node ${node.options.host} has an error: ${error.message}`); }); client.on("messageCreate", async (message) => { - console.log(message.content) + if (message.author.bot) return; + const start = performance.now(); const [command, ...args] = message.content.slice(0).split(/\s+/g); - console.log(command) - console.log(command === 'play') 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 { // Search for tracks using a query or url, using a query searches youtube automatically and the track requester object - res = await manager.search(search, message.author); + res = await manager.search({query: search}); + end = `Time took: ${Math.round(performance.now() - start)}ms.`; // Check the load type as this command is not that advanced for basics if (res.loadType === 'empty') throw res; if (res.loadType === 'playlist') { @@ -142,15 +139,18 @@ client.on("messageCreate", async (message) => { // Connect to the voice channel and add the track to the queue player.connect(); - console.log(res) await player.queue.add(res.tracks[0]); // Checks if the client should play the track if it's the first one added if (!player.playing && !player.paused && !player.queue.size) player.play(); - return message.reply(`enqueuing ${res.tracks[0].title}.`); + return message.reply(`enqueuing ${res.tracks[0].title}. ${end}`); } }); client.on("raw", (data) => manager.updateVoiceState(data)); +client.on("ready" , () => { + manager.init(client.user?.id as string); + console.log(`Logged in as ${client.user?.tag} | Memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`); +}); client.login(process.env.TOKEN); ``` diff --git a/example/src/index.ts b/example/src/index.ts index b2d4049..0af0475 100644 --- a/example/src/index.ts +++ b/example/src/index.ts @@ -1,5 +1,5 @@ import { Client } from "discord.js"; -import { Manager } from "sunday.ts"; +import { Manager } from "../../src"; import "dotenv/config"; let client = new Client({ @@ -33,9 +33,6 @@ manager.on("NodeConnect", (node) => { manager.on("NodeRaw", async (node) => { console.log(`sent raw data: ${JSON.stringify(node)}`); }); -client.on("ready", () => { - manager.init(); -}); manager.on("PlayerCreate", (player) => { console.log(`Player created in guild ${player.guild}`); }); @@ -43,19 +40,19 @@ manager.on("NodeError" , (node, error) => { console.log(`Node ${node.options.host} has an error: ${error.message}`); }); client.on("messageCreate", async (message) => { - console.log(message.content) + if (message.author.bot) return; + const start = performance.now(); const [command, ...args] = message.content.slice(0).split(/\s+/g); - console.log(command) - console.log(command === 'play') 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 { // Search for tracks using a query or url, using a query searches youtube automatically and the track requester object - res = await manager.search(search, message.author); + res = await manager.search({query: search}); + end = `Time took: ${Math.round(performance.now() - start)}ms.`; // Check the load type as this command is not that advanced for basics if (res.loadType === 'empty') throw res; if (res.loadType === 'playlist') { @@ -79,13 +76,16 @@ client.on("messageCreate", async (message) => { // Connect to the voice channel and add the track to the queue player.connect(); - console.log(res) await player.queue.add(res.tracks[0]); // Checks if the client should play the track if it's the first one added if (!player.playing && !player.paused && !player.queue.size) player.play(); - return message.reply(`enqueuing ${res.tracks[0].title}.`); + return message.reply(`enqueuing ${res.tracks[0].title}. ${end}`); } }); client.on("raw", (data) => manager.updateVoiceState(data)); +client.on("ready" , () => { + manager.init(client.user?.id as string); + console.log(`Logged in as ${client.user?.tag} | Memory usage: ${Math.round(process.memoryUsage().heapUsed / 1024 / 1024)}MB`); +}); client.login(process.env.TOKEN); \ No newline at end of file diff --git a/package.json b/package.json index bdbe6a3..f769fc0 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.8-indev", + "version": "1.0.9-indev", "description": "Sunday a lavalink wrapper", "license": "MIT", "author": "FAYStarNext", diff --git a/src/structures/Manager.ts b/src/structures/Manager.ts index 9f51a69..537b64a 100644 --- a/src/structures/Manager.ts +++ b/src/structures/Manager.ts @@ -31,14 +31,21 @@ export class Manager extends TypedEmitter { soundcloud: "scsearch", deezer: "dzsearch", }; + YOUTUBE_REGEX = /https?:\/\/(www\.)?(youtu\.be|youtube\.com|music\.youtube\.com|m\.youtube\.com)/; + SOUNDCLOUD_REGEX = /^(https?:\/\/)?(www\.)?(soundcloud\.com)\/.+$/; + SPOTIFY_REGEX = /^(https?:\/\/)?(open\.spotify\.com)\/.+$/; + BILIBILITV_REGEX = /^(https?:\/\/)?(www\.)?(bilibili\.tv)\/.+$/; + JOOX_REGEX = /^(https?:\/\/)?(www\.)?(joox\.com)\/.+$/; /** The map of players. */ - public readonly players = new Collection(); + public readonly players: Collection = new Collection(); /** The map of nodes. */ - public readonly nodes = new Collection(); + public readonly nodes: Collection = new Collection(); /** The options that were set. */ public readonly options: ManagerOptions; private initiated = false; + /** The map of search. */ + public readonly search_cache: Map = new Map(); /** Returns the nodes that has the least load. */ private get leastLoadNode(): Collection { @@ -127,9 +134,10 @@ export class Manager extends TypedEmitter { } } - if (this.options.nodes) { - for (const nodeOptions of this.options.nodes) new (Structure.get("Node"))(nodeOptions); - } + if (this.options.nodes) this.options.nodes.forEach((nodeOptions) => { return new (Structure.get("Node"))(nodeOptions); }); + setInterval(() => { + this.search_cache.clear(); + }, this.options.cache?.time || 10000); } /** @@ -139,9 +147,7 @@ export class Manager extends TypedEmitter { public init(clientId?: string): this { if (this.initiated) return this; if (typeof clientId !== "undefined") this.options.clientId = clientId; - if (typeof this.options.clientId !== "string") throw new Error('"clientId" set is not type of "string"'); - if (!this.options.clientId) throw new Error('"clientId" is not set. Pass it in Manager#init() or as a option in the constructor.'); for (const node of this.nodes.values()) { @@ -162,53 +168,45 @@ export class Manager extends TypedEmitter { * @param requester * @returns The search result. */ - public async search(query: string | SearchQuery, requester?: User | ClientUser): Promise { - const node = this.useableNodes; - - if (!node) { - throw new Error("No available nodes."); - } - const _query: SearchQuery = typeof query === "string" ? { query } : query; + public async search(options: SearchQuery): Promise { + const node = this.useableNodes; + if (!node) throw new Error("No available nodes."); + 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; - if (!/^https?:\/\//.test(search)) { - search = `${_source}:${search}`; - } + if (!/^https?:\/\//.test(search)) search = `${_source}:${search}`; + if (this.search_cache.get(String(search))) { + let data = await this.search_cache.get(String(search)) + return data + }; try { const res = (await node.rest.get(`/v4/loadtracks?identifier=${encodeURIComponent(search)}`)) as LavalinkResponse; - - if (!res) { - throw new Error("Query not found."); - } + 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; } - const tracks = searchData.map((track) => TrackUtils.build(track, requester)); + const tracks = searchData.map((track) => TrackUtils.build(track, options.requester)); let playlist = null; if (res.loadType === "playlist") { playlist = { name: playlistData!.info.name, - tracks: playlistData!.tracks.map((track) => TrackUtils.build(track, requester)), + tracks: playlistData!.tracks.map((track) => TrackUtils.build(track, options.requester)), duration: playlistData!.tracks.reduce((acc, cur) => acc + (cur.info.length || 0), 0), }; } @@ -222,13 +220,13 @@ export class Manager extends TypedEmitter { if (this.options.replaceYouTubeCredentials) { let tracksToReplace: Track[] = []; if (result.loadType === "playlist") { - tracksToReplace = result.playlist.tracks; + tracksToReplace = result.playlist!.tracks; } else { tracksToReplace = result.tracks; } for (const track of tracksToReplace) { - if (isYouTubeURL(track.uri)) { + if (this.YOUTUBE_REGEX.test(track.uri)) { track.author = track.author.replace("- Topic", ""); track.title = track.title.replace("Topic -", ""); } @@ -240,16 +238,35 @@ export class Manager extends TypedEmitter { } } + if (res.loadType === "search") await this.search_cache.set(String(search), result); + return result; } catch (err) { throw new Error(err); } + } - function isYouTubeURL(uri: string): boolean { - return uri.includes("youtube.com") || uri.includes("youtu.be"); + private CheckURL(uri: string): string { + let data = this.regex_link(uri); + switch (data) { + case "yt": + return "yt:" + uri.replace("https://www.youtube.com/watch?v=", ""); } } + 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"; + if (this.SPOTIFY_REGEX.test(link)) return "sp"; + if (this.BILIBILITV_REGEX.test(link)) return "bi:tv"; + if (this.JOOX_REGEX.test(link)) return "jx"; + if (this.isLink(link)) return "http"; + return null; + } /** * Decodes the base64 encoded tracks and returns a TrackData array. * @param tracks @@ -412,6 +429,12 @@ export interface ManagerOptions { defaultSearchPlatform?: SearchPlatform; /** Whether the YouTube video titles should be replaced if the Author does not exactly match. */ replaceYouTubeCredentials?: boolean; + cache?: { + /** Whether to enable cache. */ + enable: boolean; + /** Clear cache every second */ + time: number; + } /** * Function to send data to the websocket. * @param id @@ -427,6 +450,8 @@ export interface SearchQuery { source?: SearchPlatform | string; /** The query to search for. */ query: string; + requester?: User | ClientUser; + cache?: boolean } export interface LavalinkResponse { @@ -464,22 +489,22 @@ export interface PlaylistData { } export interface ManagerEvents { - NodeCreate: (node: Node) => void; - NodeDestroy: (node: Node) => void; - NodeConnect: (node: Node) => void; - NodeReconnect: (node: Node) => void; - NodeDisconnect: (node: Node, reason: { code?: number; reason?: string }) => void; - NodeError: (node: Node, error: Error) => void; - NodeRaw: (payload: unknown) => void; - PlayerCreate: (player: Player) => void; - PlayerDestroy: (player: Player) => void; - PlayerStateUpdate: (oldPlayer: Player, newPlayer: Player) => void; - PlayerMove: (player: Player, initChannel: string, newChannel: string) => void; - PlayerDisconnect: (player: Player, oldChannel: string) => void; - QueueEnd: (player: Player, track: Track | UnresolvedTrack, payload: TrackEndEvent) => void; - SocketClosed: (player: Player, payload: WebSocketClosedEvent) => void; - TrackStart: (player: Player, track: Track, payload: TrackStartEvent) => void; - TrackEnd: (player: Player, track: Track, payload: TrackEndEvent) => void; - TrackStuck: (player: Player, track: Track, payload: TrackStuckEvent) => void; - TrackError: (player: Player, track: Track | UnresolvedTrack, payload: TrackExceptionEvent) => void; + NodeCreate: (node: Node) => void; + NodeDestroy: (node: Node) => void; + NodeConnect: (node: Node) => void; + NodeReconnect: (node: Node) => void; + NodeDisconnect: (node: Node, reason: { code?: number; reason?: string }) => void; + NodeError: (node: Node, error: Error) => void; + NodeRaw: (payload: unknown) => void; + PlayerCreate: (player: Player) => void; + PlayerDestroy: (player: Player) => void; + PlayerStateUpdate: (oldPlayer: Player, newPlayer: Player) => void; + PlayerMove: (player: Player, initChannel: string, newChannel: string) => void; + PlayerDisconnect: (player: Player, oldChannel: string) => void; + QueueEnd: (player: Player, track: Track | UnresolvedTrack, payload: TrackEndEvent) => void; + SocketClosed: (player: Player, payload: WebSocketClosedEvent) => void; + TrackStart: (player: Player, track: Track, payload: TrackStartEvent) => void; + TrackEnd: (player: Player, track: Track, payload: TrackEndEvent) => void; + TrackStuck: (player: Player, track: Track, payload: TrackStuckEvent) => void; + TrackError: (player: Player, track: Track | UnresolvedTrack, payload: TrackExceptionEvent) => void; }