From 09b91d36889bd10458fcca07036e65cbf403e3e9 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 1 May 2024 12:19:05 +0700 Subject: [PATCH 01/21] sync: latest version of rainlink --- src/rainlink/Drivers/Lavalink3.ts | 25 ++++++++++++--------- src/rainlink/Drivers/Lavalink4.ts | 17 ++++++++------ src/rainlink/Drivers/Nodelink2.ts | 18 ++++++++------- src/rainlink/Interface/Rest.ts | 3 +++ src/rainlink/Interface/Track.ts | 2 ++ src/rainlink/Manager/RainlinkNodeManager.ts | 14 +++++------- src/rainlink/Node/RainlinkRest.ts | 24 ++++++++++++++------ src/rainlink/Player/RainlinkPlayer.ts | 2 +- src/rainlink/Player/RainlinkTrack.ts | 14 +++++++----- 9 files changed, 72 insertions(+), 47 deletions(-) diff --git a/src/rainlink/Drivers/Lavalink3.ts b/src/rainlink/Drivers/Lavalink3.ts index 0a05f5b4..0154ec32 100644 --- a/src/rainlink/Drivers/Lavalink3.ts +++ b/src/rainlink/Drivers/Lavalink3.ts @@ -51,12 +51,12 @@ export class Lavalink3 extends AbstractDriver { const isResume = this.manager!.rainlinkOptions.options!.resume; const ws = new RainlinkWebsocket(this.wsUrl, { headers: { - Authorization: this.node!.options.auth, - "User-Id": this.manager!.id, - "Client-Name": `${metadata.name}/${metadata.version} (${metadata.github})`, - "Session-Id": this.sessionId !== null && isResume ? this.sessionId : "", + authorization: this.node!.options.auth, + "user-id": this.manager!.id, + "client-name": `${metadata.name}/${metadata.version} (${metadata.github})`, + "session-id": this.sessionId !== null && isResume ? this.sessionId : "", "user-agent": this.manager!.rainlinkOptions.options!.userAgent!, - "Num-Shards": this.manager!.shardCount, + "num-shards": this.manager!.shardCount, }, }); @@ -90,23 +90,24 @@ export class Lavalink3 extends AbstractDriver { return undefined; const lavalinkHeaders = { - Authorization: this.node!.options.auth, - "User-Agent": this.manager!.rainlinkOptions.options!.userAgent!, + authorization: this.node!.options.auth, + "user-agent": this.manager!.rainlinkOptions.options!.userAgent!, ...options.headers, }; options.headers = lavalinkHeaders; if (this.sessionId) url.pathname = "/v3" + url.pathname; - options.path = url.pathname + url.search; - const res = await fetch(url.origin + options.path, options); + const res = await fetch(url, options); if (res.status == 204) { this.debug("Player now destroyed"); return undefined; } if (res.status !== 200) { - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); this.debug( "Something went wrong with lavalink server. " + `Status code: ${res.status}\n Headers: ${util.inspect(options.headers)}` @@ -122,7 +123,9 @@ export class Lavalink3 extends AbstractDriver { finalData = this.convertV4trackResponse(finalData) as D; } - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); return finalData; } diff --git a/src/rainlink/Drivers/Lavalink4.ts b/src/rainlink/Drivers/Lavalink4.ts index bb4b252a..96a96b1e 100644 --- a/src/rainlink/Drivers/Lavalink4.ts +++ b/src/rainlink/Drivers/Lavalink4.ts @@ -43,7 +43,7 @@ export class Lavalink4 extends AbstractDriver { const isResume = this.manager!.rainlinkOptions.options!.resume; const ws = new RainlinkWebsocket(this.wsUrl, { headers: { - Authorization: this.node!.options.auth, + authorization: this.node!.options.auth, "user-id": this.manager!.id, "client-name": `${metadata.name}/${metadata.version} (${metadata.github})`, "session-id": this.sessionId !== null && isResume ? this.sessionId : "", @@ -77,22 +77,23 @@ export class Lavalink4 extends AbstractDriver { } const lavalinkHeaders = { - Authorization: this.node!.options.auth, - "User-Agent": this.manager!.rainlinkOptions.options!.userAgent!, + authorization: this.node!.options.auth, + "user-agent": this.manager!.rainlinkOptions.options!.userAgent!, ...options.headers, }; options.headers = lavalinkHeaders; - options.path = url.pathname + url.search; - const res = await fetch(url.origin + options.path, options); + const res = await fetch(url, options); if (res.status == 204) { this.debug("Player now destroyed"); return undefined; } if (res.status !== 200) { - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); this.debug( "Something went wrong with lavalink server. " + `Status code: ${res.status}\n Headers: ${util.inspect(options.headers)}` @@ -102,7 +103,9 @@ export class Lavalink4 extends AbstractDriver { const finalData = await res.json(); - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); return finalData as D; } diff --git a/src/rainlink/Drivers/Nodelink2.ts b/src/rainlink/Drivers/Nodelink2.ts index c97eb7ad..133a4206 100644 --- a/src/rainlink/Drivers/Nodelink2.ts +++ b/src/rainlink/Drivers/Nodelink2.ts @@ -106,24 +106,24 @@ export class Nodelink2 extends AbstractDriver { } const lavalinkHeaders = { - Authorization: this.node!.options.auth, - "User-Agent": this.manager!.rainlinkOptions.options!.userAgent!, - "Content-Encoding": "brotli, gzip, deflate", + authorization: this.node!.options.auth, + "user-agent": this.manager!.rainlinkOptions.options!.userAgent!, + "content-encoding": "brotli, gzip, deflate", "accept-encoding": "brotli, gzip, deflate", ...options.headers, }; options.headers = lavalinkHeaders; - options.path = url.pathname + url.search; - - const res = await fetch(url.origin + options.path, options); + const res = await fetch(url, options); if (res.status == 204) { this.debug("Player now destroyed"); return undefined; } if (res.status !== 200) { - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); this.debug( "Something went wrong with nodelink server. " + `Status code: ${res.status}\n Headers: ${util.inspect(options.headers)}` @@ -138,7 +138,9 @@ export class Nodelink2 extends AbstractDriver { finalData = this.convertV4trackResponse(finalData) as D; } - this.debug(`${options.method ?? "GET"} ${options.path} payload=${options.body ? String(options.body) : "{}"}`); + this.debug( + `${options.method ?? "GET"} ${url.pathname + url.search} payload=${options.body ? String(options.body) : "{}"}` + ); return finalData; } diff --git a/src/rainlink/Interface/Rest.ts b/src/rainlink/Interface/Rest.ts index ea17cff1..bd7c74ae 100644 --- a/src/rainlink/Interface/Rest.ts +++ b/src/rainlink/Interface/Rest.ts @@ -1,6 +1,7 @@ import { FilterOptions } from "./Player.js"; import { LavalinkLoadType } from "./Constants.js"; import { Exception } from "./LavalinkEvents.js"; +import { LavalinkNodeStatsResponse } from "./Node.js"; export interface RainlinkRequesterOptions extends RequestInit { params?: string | Record; @@ -10,6 +11,8 @@ export interface RainlinkRequesterOptions extends RequestInit { rawReqData?: UpdatePlayerInfo; } +export type LavalinkStats = Omit; + export interface LavalinkPlayer { guildId: string; track?: RawTrack; diff --git a/src/rainlink/Interface/Track.ts b/src/rainlink/Interface/Track.ts index 5a93cc9a..83fda86d 100644 --- a/src/rainlink/Interface/Track.ts +++ b/src/rainlink/Interface/Track.ts @@ -5,4 +5,6 @@ export interface ResolveOptions { overwrite?: boolean; /** Rainlink player property */ player?: RainlinkPlayer; + /** The name of node */ + nodeName?: string; } diff --git a/src/rainlink/Manager/RainlinkNodeManager.ts b/src/rainlink/Manager/RainlinkNodeManager.ts index 89311a4a..dd7b7576 100644 --- a/src/rainlink/Manager/RainlinkNodeManager.ts +++ b/src/rainlink/Manager/RainlinkNodeManager.ts @@ -43,17 +43,15 @@ export class RainlinkNodeManager extends RainlinkDatabase { const onlineNodes = nodes.filter((node) => node.state === RainlinkConnectState.Connected); if (!onlineNodes.length) throw new Error("No nodes are online"); - // .filter((x) => x.manager.node.name === node.node.name) - const temp = await Promise.all( - onlineNodes.map(async (node) => ({ - node, - players: (await node.rest.getPlayers()).filter((x) => this.get(x.guildId)).map((x) => this.get(x.guildId)!) - .length, - })) + onlineNodes.map(async (node) => { + const stats = await node.rest.getStatus(); + return !stats ? { players: 0, node: node } : { players: stats.players, node: node }; + }) ); + temp.sort((a, b) => a.players - b.players); - return temp.reduce((a, b) => (a.players < b.players ? a : b)).node; + return temp[0].node; } /** diff --git a/src/rainlink/Node/RainlinkRest.ts b/src/rainlink/Node/RainlinkRest.ts index 47d1ab6f..7a028644 100644 --- a/src/rainlink/Node/RainlinkRest.ts +++ b/src/rainlink/Node/RainlinkRest.ts @@ -8,6 +8,7 @@ import { RainlinkNode } from "./RainlinkNode.js"; import { LavalinkPlayer, LavalinkResponse, + LavalinkStats, RainlinkRequesterOptions, RawTrack, RoutePlanner, @@ -43,12 +44,23 @@ export class RainlinkRest { public async getPlayers(): Promise { const options: RainlinkRequesterOptions = { path: `/sessions/${this.sessionId}/players`, - params: undefined, - method: "GET", + headers: { "content-type": "application/json" }, }; return (await this.nodeManager.driver.requester(options)) ?? []; } + /** + * Gets current lavalink status + * @returns Promise that resolves to an object of current lavalink status + */ + public async getStatus(): Promise { + const options: RainlinkRequesterOptions = { + path: "/stats", + headers: { "content-type": "application/json" }, + }; + return await this.nodeManager.driver.requester(options); + } + /** * Decode a single track from "encoded" properties * @returns Promise that resolves to an object of raw track @@ -56,9 +68,7 @@ export class RainlinkRest { public async decodeTrack(base64track: string): Promise { const options: RainlinkRequesterOptions = { path: `/decodetrack?encodedTrack=${encodeURIComponent(base64track)}`, - params: undefined, headers: { "content-type": "application/json" }, - method: "GET", }; return await this.nodeManager.driver.requester(options); } @@ -86,7 +96,6 @@ export class RainlinkRest { public destroyPlayer(guildId: string): void { const options: RainlinkRequesterOptions = { path: `/sessions/${this.sessionId}/players/${guildId}`, - params: undefined, headers: { "content-type": "application/json" }, method: "DELETE", }; @@ -122,7 +131,7 @@ export class RainlinkRest { public async getRoutePlannerStatus(): Promise { const options = { path: "/routeplanner/status", - options: {}, + headers: { "content-type": "application/json" }, }; return await this.nodeManager.driver.requester(options); } @@ -135,6 +144,7 @@ export class RainlinkRest { const options = { path: "/routeplanner/free/address", method: "POST", + headers: { "content-type": "application/json" }, data: { address }, }; await this.nodeManager.driver.requester(options); @@ -143,7 +153,7 @@ export class RainlinkRest { /** * Get Lavalink info */ - public getLavalinkInfo(): Promise { + public getInfo(): Promise { const options = { path: "/info", headers: { "content-type": "application/json" }, diff --git a/src/rainlink/Player/RainlinkPlayer.ts b/src/rainlink/Player/RainlinkPlayer.ts index e8cbc8bf..634c997f 100644 --- a/src/rainlink/Player/RainlinkPlayer.ts +++ b/src/rainlink/Player/RainlinkPlayer.ts @@ -241,7 +241,7 @@ export class RainlinkPlayer extends EventEmitter { let errorMessage: string | undefined; - const resolveResult = await current.resolver(this.manager).catch((e: any) => { + const resolveResult = await current.resolver(this.manager, { nodeName: this.node.options.name }).catch((e: any) => { errorMessage = e.message; return null; }); diff --git a/src/rainlink/Player/RainlinkTrack.ts b/src/rainlink/Player/RainlinkTrack.ts index 22e1344f..1ab0b5b1 100644 --- a/src/rainlink/Player/RainlinkTrack.ts +++ b/src/rainlink/Player/RainlinkTrack.ts @@ -3,6 +3,7 @@ import { RainlinkSearchResult, RainlinkSearchResultType } from "../Interface/Man import { RawTrack } from "../Interface/Rest.js"; import { ResolveOptions } from "../Interface/Track.js"; import { Rainlink } from "../Rainlink.js"; +import { RainlinkNode } from "../main.js"; export class RainlinkTrack { /** Encoded string from lavalink */ @@ -120,7 +121,7 @@ export class RainlinkTrack { `[Rainlink] / [Track] | Resolving ${this.source} track ${this.title}; Source: ${this.source}` ); - const result = await this.getTrack(manager); + const result = await this.getTrack(manager, options ? options.nodeName : undefined); if (!result) throw new Error("No results found"); this.encoded = result.encoded; @@ -139,12 +140,12 @@ export class RainlinkTrack { return this; } - protected async getTrack(manager: Rainlink): Promise { - const node = await manager.nodes.getLeastUsed(); + protected async getTrack(manager: Rainlink, nodeName?: string): Promise { + const node = nodeName ? manager.nodes.get(nodeName) : await manager.nodes.getLeastUsed(); if (!node) throw new Error("No nodes available"); - const result = await this.resolverEngine(manager); + const result = await this.resolverEngine(manager, node); if (!result || !result.tracks.length) throw new Error("No results found"); @@ -175,7 +176,7 @@ export class RainlinkTrack { return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, "\\$&"); } - protected async resolverEngine(manager: Rainlink): Promise { + protected async resolverEngine(manager: Rainlink, node: RainlinkNode): Promise { const defaultSearchEngine = manager.rainlinkOptions.options!.defaultSearchEngine; const engine = manager.searchEngines.get(this.source || defaultSearchEngine || "youtube"); const searchQuery = [this.author, this.title].filter((x) => !!x).join(" - "); @@ -184,17 +185,20 @@ export class RainlinkTrack { const prase1 = await manager.search(`directSearch=${this.uri}`, { requester: this.requester, + nodeName: node.options.name, }); if (prase1.tracks.length !== 0) return prase1; const prase2 = await manager.search(`directSearch=${engine}search:${searchQuery}`, { requester: this.requester, + nodeName: node.options.name, }); if (prase2.tracks.length !== 0) return prase2; if (manager.rainlinkOptions.options!.searchFallback?.enable && searchFallbackEngine) { const prase3 = await manager.search(`directSearch=${searchFallbackEngine}search:${searchQuery}`, { requester: this.requester, + nodeName: node.options.name, }); if (prase3.tracks.length !== 0) return prase3; } From 75fdbc6829904d4545685071210bdbb3b2a878fc Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Thu, 2 May 2024 21:52:24 +0700 Subject: [PATCH 02/21] fix: setup not showing duration when only 1 track left --- src/handlers/Player/loadUpdate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/Player/loadUpdate.ts b/src/handlers/Player/loadUpdate.ts index e4cc681d..08c9f130 100644 --- a/src/handlers/Player/loadUpdate.ts +++ b/src/handlers/Player/loadUpdate.ts @@ -47,7 +47,7 @@ export class playerLoadUpdate { const TotalDuration = player.queue.duration; let cSong = player.queue.current; - let qDuration = `${new FormatDuration().parse(TotalDuration)}`; + let qDuration = `${new FormatDuration().parse(TotalDuration + Number(player.queue.current?.duration))}`; function getTitle(tracks: RainlinkTrack): string { if (client.config.lavalink.AVOID_SUSPEND) return tracks.title; From f3646b1c231772cfb7a88fd2bbf11ee9508ca967 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Fri, 3 May 2024 20:53:48 +0700 Subject: [PATCH 03/21] fix: manually disconnect causes bot cannot connect --- src/events/guild/voiceStateUpdate.ts | 5 +++-- src/events/player/playerDestroy.ts | 24 +++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/events/guild/voiceStateUpdate.ts b/src/events/guild/voiceStateUpdate.ts index 6d08f916..5d6ab39d 100644 --- a/src/events/guild/voiceStateUpdate.ts +++ b/src/events/guild/voiceStateUpdate.ts @@ -1,6 +1,7 @@ import { PermissionsBitField, EmbedBuilder, VoiceState, GuildMember, Role, TextChannel } from "discord.js"; import { Manager } from "../../manager.js"; import { AutoReconnectBuilderService } from "../../services/AutoReconnectBuilderService.js"; +import { RainlinkPlayerState } from "../../rainlink/main.js"; export default class { async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { @@ -17,7 +18,7 @@ export default class { if (newState.channelId == null && newState.member?.user.id === client.user?.id) { player.data.set("sudo-destroy", true); - player.voiceId !== null ? player.destroy() : true; + player.state !== RainlinkPlayerState.DESTROYED ? player.destroy() : true; } if (oldState.member?.user.bot || newState.member?.user.bot) return; @@ -38,7 +39,7 @@ export default class { if (!isInVoice || !isInVoice.voice.channelId) { player.data.set("sudo-destroy", true); - player.voiceId !== null ? player.destroy() : true; + player.state !== RainlinkPlayerState.DESTROYED ? player.destroy() : true; } if ( diff --git a/src/events/player/playerDestroy.ts b/src/events/player/playerDestroy.ts index b4bd7786..3cb6a858 100644 --- a/src/events/player/playerDestroy.ts +++ b/src/events/player/playerDestroy.ts @@ -25,19 +25,17 @@ export default class { if (!channel) return; - if (player.state == RainlinkPlayerState.DESTROYED && data !== null && data) { - if (data.twentyfourseven) { - await new AutoReconnectBuilderService(client, player).build247(player.guildId, true, data.voice); - client.rainlink.create({ - guildId: data.guild!, - voiceId: data.voice!, - textId: data.text!, - shardId: guild.shardId ?? 0, - deaf: true, - volume: client.config.lavalink.DEFAULT_VOLUME ?? 100, - }); - } else await client.db.autoreconnect.delete(player.guildId); - } + if (data !== null && data && data.twentyfourseven) { + await new AutoReconnectBuilderService(client, player).build247(player.guildId, true, data.voice); + client.rainlink.players.create({ + guildId: data.guild!, + voiceId: data.voice!, + textId: data.text!, + shardId: guild.shardId ?? 0, + deaf: true, + volume: client.config.lavalink.DEFAULT_VOLUME ?? 100, + }); + } else await client.db.autoreconnect.delete(player.guildId); let guildModel = await client.db.language.get(`${channel.guild.id}`); if (!guildModel) { From 34bfe9484f1fc7caf83d18c0face0dd4fd24e783 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Sat, 4 May 2024 17:57:14 +0700 Subject: [PATCH 04/21] rewrite: websocket server with fastify --- src/@types/Config.ts | 1 + src/manager.ts | 4 +- src/services/ConfigDataService.ts | 1 + src/utilities/webserver.ts | 16 -------- src/web/player.ts | 1 + src/web/server.ts | 61 +++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 17 deletions(-) delete mode 100644 src/utilities/webserver.ts create mode 100644 src/web/player.ts create mode 100644 src/web/server.ts diff --git a/src/@types/Config.ts b/src/@types/Config.ts index 8f543fb2..51b9cc34 100644 --- a/src/@types/Config.ts +++ b/src/@types/Config.ts @@ -53,6 +53,7 @@ export interface Commands { export interface WebServer { enable: boolean; port: number; + auth: string; } export interface Lavalink { diff --git a/src/manager.ts b/src/manager.ts index fee7a4c1..97e65b94 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -15,7 +15,7 @@ import { LoggerService } from "./services/LoggerService.js"; import { ClusterClient, getInfo } from "discord-hybrid-sharding"; import { join, dirname } from "path"; import { fileURLToPath } from "url"; -import { WebServer } from "./utilities/webserver.js"; +import { WebServer } from "./web/server.js"; import { ManifestService } from "./services/ManifestService.js"; import { NormalModeIcons } from "./assets/NormalModeIcons.js"; import { SafeModeIcons } from "./assets/SafeModeIcons.js"; @@ -75,6 +75,7 @@ export class Manager extends Client { plButton: Collection; leaveDelay: Collection; nowPlaying: Collection; + wsId: Collection; websocket?: WebSocket; UpdateMusic!: (player: RainlinkPlayer) => Promise>; UpdateQueueMsg!: (player: RainlinkPlayer) => Promise>; @@ -139,6 +140,7 @@ export class Manager extends Client { this.plButton = new Collection(); this.leaveDelay = new Collection(); this.nowPlaying = new Collection(); + this.wsId = new Collection(); this.isDatabaseConnected = false; // Sharing diff --git a/src/services/ConfigDataService.ts b/src/services/ConfigDataService.ts index 5e059328..ca282c9f 100644 --- a/src/services/ConfigDataService.ts +++ b/src/services/ConfigDataService.ts @@ -127,6 +127,7 @@ export class ConfigDataService { WEB_SERVER: { enable: false, port: 2880, + auth: "youshallnotpass", }, PREMIUM_LOG_CHANNEL: "", GUILD_LOG_CHANNEL: "", diff --git a/src/utilities/webserver.ts b/src/utilities/webserver.ts deleted file mode 100644 index 99207d74..00000000 --- a/src/utilities/webserver.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Manager } from "../manager.js"; -import Fastify from "fastify"; - -export class WebServer { - app: Fastify.FastifyInstance; - constructor(private client: Manager) { - this.app = Fastify({ - logger: false, - }); - - this.app.get("/", (request, reply) => { - reply.send("Alive!"); - }); - this.app.listen({ port: this.client.config.features.WEB_SERVER.port }); - } -} diff --git a/src/web/player.ts b/src/web/player.ts new file mode 100644 index 00000000..e69bc6be --- /dev/null +++ b/src/web/player.ts @@ -0,0 +1 @@ +export class PlayerRoute {} diff --git a/src/web/server.ts b/src/web/server.ts new file mode 100644 index 00000000..b30e7ccb --- /dev/null +++ b/src/web/server.ts @@ -0,0 +1,61 @@ +import { Manager } from "../manager.js"; +import Fastify from "fastify"; +import WebsocketPlugin from "@fastify/websocket"; + +export class WebServer { + app: Fastify.FastifyInstance; + constructor(private client: Manager) { + this.app = Fastify({ + logger: false, + }); + + this.websocketRegister(); + + this.app.get("/neko", (request, reply) => { + client.logger.info( + import.meta.url, + `${request.method} ${request.routeOptions.url} payload=${request.body ? request.body : "{}"}` + ); + reply.send("💀"); + }); + + this.app.listen({ port: this.client.config.features.WEB_SERVER.port }); + } + + websocketRegister() { + this.app.register(WebsocketPlugin); + this.app.register((fastify) => + fastify.get("/websocket", { websocket: true }, (socket, req) => { + this.client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} payload=${req.body ? req.body : "{}"}` + ); + + socket.on("close", (code, reason) => { + this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); + this.client.wsId.delete(String(req.headers["guild-id"])); + }); + + if (!req.headers["guild-id"]) { + socket.send(JSON.stringify({ error: "Missing guild-id" })); + socket.close(1000, JSON.stringify({ error: "Missing guild-id" })); + return; + } + if (!req.headers["authentication"]) { + socket.send(JSON.stringify({ error: "Missing authentication" })); + socket.close(1000, JSON.stringify({ error: "Missing authentication" })); + return; + } + + if (req.headers["authentication"] !== this.client.config.features.WEB_SERVER.auth) { + socket.send(JSON.stringify({ error: "Authentication failed" })); + socket.close(1000, JSON.stringify({ error: "Authentication failed" })); + return; + } + + this.client.logger.websocket(import.meta.url, `Websocket opened for ${req.headers["guild-id"]}`); + this.client.wsId.set(String(req.headers["guild-id"]), true); + }) + ); + } +} From 47c5000a561fee88d4bfff5de479b3440546b705 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Sat, 4 May 2024 22:13:39 +0700 Subject: [PATCH 05/21] add: /v1/players/test | /v1/test for developing --- src/web/player.ts | 13 +++++++- src/web/server.ts | 72 +++++++++++++++++++++----------------------- src/web/websocket.ts | 46 ++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 src/web/websocket.ts diff --git a/src/web/player.ts b/src/web/player.ts index e69bc6be..5842b001 100644 --- a/src/web/player.ts +++ b/src/web/player.ts @@ -1 +1,12 @@ -export class PlayerRoute {} +import { Manager } from "../manager.js"; +import Fastify from "fastify"; + +export class PlayerRoute { + constructor(protected client: Manager) {} + + main(fastify: Fastify.FastifyInstance) { + fastify.get("/test", (req, res) => { + res.send({ message: "Hallo :D" }); + }); + } +} diff --git a/src/web/server.ts b/src/web/server.ts index b30e7ccb..2d7ea4b1 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1,6 +1,8 @@ import { Manager } from "../manager.js"; import Fastify from "fastify"; import WebsocketPlugin from "@fastify/websocket"; +import { WebsocketRoute } from "./websocket.js"; +import { PlayerRoute } from "./player.js"; export class WebServer { app: Fastify.FastifyInstance; @@ -9,7 +11,38 @@ export class WebServer { logger: false, }); - this.websocketRegister(); + this.app.register( + (fastify, _, done) => { + fastify.addHook("preValidation", function hook(req, reply, done) { + if (!req.headers["authorization"]) { + reply.code(401); + reply.send(JSON.stringify({ error: "Missing Authorization" })); + return done(); + } + if (req.headers["authorization"] !== client.config.features.WEB_SERVER.auth) { + reply.code(401); + reply.send(JSON.stringify({ error: "Authorization failed" })); + return done(); + } + done(); + }); + fastify.register(WebsocketPlugin); + fastify.register((fastify, _, done) => { + new WebsocketRoute(client).main(fastify); + done(); + }); + fastify.register( + (fastify, _, done) => { + new PlayerRoute(client).main(fastify); + done(); + }, + { prefix: "players" } + ); + fastify.get("/test", (req, res) => res.send({ message: "Hallo :D" })); + done(); + }, + { prefix: "v1" } + ); this.app.get("/neko", (request, reply) => { client.logger.info( @@ -21,41 +54,4 @@ export class WebServer { this.app.listen({ port: this.client.config.features.WEB_SERVER.port }); } - - websocketRegister() { - this.app.register(WebsocketPlugin); - this.app.register((fastify) => - fastify.get("/websocket", { websocket: true }, (socket, req) => { - this.client.logger.info( - import.meta.url, - `${req.method} ${req.routeOptions.url} payload=${req.body ? req.body : "{}"}` - ); - - socket.on("close", (code, reason) => { - this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); - this.client.wsId.delete(String(req.headers["guild-id"])); - }); - - if (!req.headers["guild-id"]) { - socket.send(JSON.stringify({ error: "Missing guild-id" })); - socket.close(1000, JSON.stringify({ error: "Missing guild-id" })); - return; - } - if (!req.headers["authentication"]) { - socket.send(JSON.stringify({ error: "Missing authentication" })); - socket.close(1000, JSON.stringify({ error: "Missing authentication" })); - return; - } - - if (req.headers["authentication"] !== this.client.config.features.WEB_SERVER.auth) { - socket.send(JSON.stringify({ error: "Authentication failed" })); - socket.close(1000, JSON.stringify({ error: "Authentication failed" })); - return; - } - - this.client.logger.websocket(import.meta.url, `Websocket opened for ${req.headers["guild-id"]}`); - this.client.wsId.set(String(req.headers["guild-id"]), true); - }) - ); - } } diff --git a/src/web/websocket.ts b/src/web/websocket.ts new file mode 100644 index 00000000..5caf0e23 --- /dev/null +++ b/src/web/websocket.ts @@ -0,0 +1,46 @@ +import Fastify from "fastify"; +import { Manager } from "../manager.js"; +import { WebSocket } from "@fastify/websocket"; + +export class WebsocketRoute { + constructor(protected client: Manager) {} + + main(fastify: Fastify.FastifyInstance) { + fastify.get("/websocket", { websocket: true }, (socket, req) => { + this.client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} payload=${req.body ? req.body : "{}"}` + ); + + socket.on("close", (code, reason) => { + this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); + this.client.wsId.delete(String(req.headers["guild-id"])); + }); + + if (!this.checker(socket, req)) return; + + this.client.logger.websocket(import.meta.url, `Websocket opened for ${req.headers["guild-id"]}`); + this.client.wsId.set(String(req.headers["guild-id"]), true); + }); + } + + checker(socket: WebSocket, req: Fastify.FastifyRequest) { + if (!req.headers["guild-id"]) { + socket.send(JSON.stringify({ error: "Missing guild-id" })); + socket.close(1000, JSON.stringify({ error: "Missing guild-id" })); + return false; + } + if (!req.headers["authorization"]) { + socket.send(JSON.stringify({ error: "Missing Authorization" })); + socket.close(1000, JSON.stringify({ error: "Missing Authorization" })); + return false; + } + if (req.headers["authorization"] !== this.client.config.features.WEB_SERVER.auth) { + socket.send(JSON.stringify({ error: "Authorization failed" })); + socket.close(1000, JSON.stringify({ error: "Authorization failed" })); + return false; + } + + return true; + } +} From 877e679b1c4257bc678ac29ea44e7bdc2127758e Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Sun, 5 May 2024 20:02:16 +0700 Subject: [PATCH 06/21] add: some endpoints for webserver --- src/web/README.md | 3 + src/web/player.ts | 19 +++++- src/web/route/getCurrentLoop.ts | 14 ++++ src/web/route/getCurrentPaused.ts | 14 ++++ src/web/route/getCurrentPosition.ts | 14 ++++ src/web/route/getCurrentTrackStatus.ts | 36 ++++++++++ src/web/route/getMemberStatus.ts | 20 ++++++ src/web/route/getQueueStatus.ts | 32 +++++++++ src/web/route/getStatus.ts | 68 +++++++++++++++++++ src/web/route/patchControl.ts | 94 ++++++++++++++++++++++++++ src/web/server.ts | 5 +- src/web/websocket.ts | 5 +- 12 files changed, 313 insertions(+), 11 deletions(-) create mode 100644 src/web/README.md create mode 100644 src/web/route/getCurrentLoop.ts create mode 100644 src/web/route/getCurrentPaused.ts create mode 100644 src/web/route/getCurrentPosition.ts create mode 100644 src/web/route/getCurrentTrackStatus.ts create mode 100644 src/web/route/getMemberStatus.ts create mode 100644 src/web/route/getQueueStatus.ts create mode 100644 src/web/route/getStatus.ts create mode 100644 src/web/route/patchControl.ts diff --git a/src/web/README.md b/src/web/README.md new file mode 100644 index 00000000..529edaf3 --- /dev/null +++ b/src/web/README.md @@ -0,0 +1,3 @@ +# Warining! + +ByteBlaze webserver will not use object oriented programing diff --git a/src/web/player.ts b/src/web/player.ts index 5842b001..2143ef3c 100644 --- a/src/web/player.ts +++ b/src/web/player.ts @@ -1,12 +1,25 @@ import { Manager } from "../manager.js"; import Fastify from "fastify"; +import { getStatus } from "./route/getStatus.js"; +import { getQueueStatus } from "./route/getQueueStatus.js"; +import { getMemberStatus } from "./route/getMemberStatus.js"; +import { getCurrentTrackStatus } from "./route/getCurrentTrackStatus.js"; +import { getCurrentLoop } from "./route/getCurrentLoop.js"; +import { getCurrentPaused } from "./route/getCurrentPaused.js"; +import { getCurrentPosition } from "./route/getCurrentPosition.js"; +import { PatchControl } from "./route/patchControl.js"; export class PlayerRoute { constructor(protected client: Manager) {} main(fastify: Fastify.FastifyInstance) { - fastify.get("/test", (req, res) => { - res.send({ message: "Hallo :D" }); - }); + fastify.get("/:guildId", (req, res) => getStatus(this.client, req, res)); + fastify.get("/:guildId/loop", (req, res) => getCurrentLoop(this.client, req, res)); + fastify.get("/:guildId/pause", (req, res) => getCurrentPaused(this.client, req, res)); + fastify.get("/:guildId/position", (req, res) => getCurrentPosition(this.client, req, res)); + fastify.get("/:guildId/queue", (req, res) => getQueueStatus(this.client, req, res)); + fastify.get("/:guildId/current", (req, res) => getCurrentTrackStatus(this.client, req, res)); + fastify.get("/:guildId/member/:userId", (req, res) => getMemberStatus(this.client, req, res)); + fastify.patch("/:guildId/control", (req, res) => new PatchControl(this.client).main(req, res)); } } diff --git a/src/web/route/getCurrentLoop.ts b/src/web/route/getCurrentLoop.ts new file mode 100644 index 00000000..3c6fc860 --- /dev/null +++ b/src/web/route/getCurrentLoop.ts @@ -0,0 +1,14 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getCurrentLoop(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + res.send({ data: player.loop }); +} diff --git a/src/web/route/getCurrentPaused.ts b/src/web/route/getCurrentPaused.ts new file mode 100644 index 00000000..3da851a4 --- /dev/null +++ b/src/web/route/getCurrentPaused.ts @@ -0,0 +1,14 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getCurrentPaused(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + res.send({ data: player.paused }); +} diff --git a/src/web/route/getCurrentPosition.ts b/src/web/route/getCurrentPosition.ts new file mode 100644 index 00000000..6fa34c8f --- /dev/null +++ b/src/web/route/getCurrentPosition.ts @@ -0,0 +1,14 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getCurrentPosition(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + res.send({ data: player.position }); +} diff --git a/src/web/route/getCurrentTrackStatus.ts b/src/web/route/getCurrentTrackStatus.ts new file mode 100644 index 00000000..ac9845bc --- /dev/null +++ b/src/web/route/getCurrentTrackStatus.ts @@ -0,0 +1,36 @@ +import { User } from "discord.js"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getCurrentTrackStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + const song = player.queue.current; + const requester = song ? (song.requester as User) : null; + + res.send({ + data: song + ? { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: requester + ? { + id: requester.id, + username: requester.username, + globalName: requester.globalName, + defaultAvatarURL: requester.defaultAvatarURL ?? null, + } + : null, + } + : null, + }); +} diff --git a/src/web/route/getMemberStatus.ts b/src/web/route/getMemberStatus.ts new file mode 100644 index 00000000..b9f3799d --- /dev/null +++ b/src/web/route/getMemberStatus.ts @@ -0,0 +1,20 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getMemberStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + let isMemeberInVoice = false; + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + const userId = req.headers["user-id"] as string; + const Guild = await client.guilds.fetch(guildId); + const Member = await Guild.members.fetch(userId); + if (!(!Member.voice.channel || !Member.voice)) isMemeberInVoice = true; + res.send({ status: isMemeberInVoice }); + return; +} diff --git a/src/web/route/getQueueStatus.ts b/src/web/route/getQueueStatus.ts new file mode 100644 index 00000000..df67cfb8 --- /dev/null +++ b/src/web/route/getQueueStatus.ts @@ -0,0 +1,32 @@ +import { User } from "discord.js"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getQueueStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + return player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }); +} diff --git a/src/web/route/getStatus.ts b/src/web/route/getStatus.ts new file mode 100644 index 00000000..644e7094 --- /dev/null +++ b/src/web/route/getStatus.ts @@ -0,0 +1,68 @@ +import { User } from "discord.js"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + let isMemeberInVoice = "notGiven"; + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + if (req.headers["user-id"]) { + const userId = req.headers["user-id"] as string; + const Guild = await client.guilds.fetch(guildId); + const Member = await Guild.members.fetch(userId); + if (!Member.voice.channel || !Member.voice) isMemeberInVoice = "false"; + else isMemeberInVoice = "true"; + } + + const song = player.queue.current; + const requester = song ? (song.requester as User) : null; + + res.send({ + guildId: player.guildId, + loop: player.loop, + pause: player.paused, + member: isMemeberInVoice, + position: player.position, + current: song + ? { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: requester + ? { + id: requester.id, + username: requester.username, + globalName: requester.globalName, + defaultAvatarURL: requester.defaultAvatarURL ?? null, + } + : null, + } + : null, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }); +} diff --git a/src/web/route/patchControl.ts b/src/web/route/patchControl.ts new file mode 100644 index 00000000..1253f108 --- /dev/null +++ b/src/web/route/patchControl.ts @@ -0,0 +1,94 @@ +import util from "node:util"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; +import { RainlinkLoopMode, RainlinkPlayer } from "../../rainlink/main.js"; + +export class PatchControl { + protected skiped: boolean = false; + constructor(protected client: Manager) {} + + async main(req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + this.client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} payload=${req.body ? util.inspect(req.body) : "{}"}` + ); + const isValid = await this.checker(req, res); + if (!isValid) return; + const guildId = (req.params as Record)["guildId"]; + const player = this.client.rainlink.players.get(guildId) as RainlinkPlayer; + const jsonBody = req.body as Record; + const currentKeys = Object.keys(jsonBody); + for (const key of currentKeys) { + const data = await (this as any)[key](req, res, player, jsonBody[key]); + if (!data) return; + } + res.send({ + loop: player.loop, + skip: this.skiped, + position: player.position, + }); + this.resetData(); + } + + async loop(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, mode: string) { + if (!mode || !["song", "queue", "none"].includes(mode)) { + res.code(400); + res.send({ error: `Invalid loop mode, '${mode}' mode does not exist!` }); + return false; + } + player.setLoop(mode as RainlinkLoopMode); + return true; + } + + async skip(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, data: string) { + if (data === undefined || player.queue.length == 0) return true; + await player.skip(); + this.skiped = true; + return true; + } + + async position(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, pos: string) { + if (!pos) return true; + if (isNaN(Number(pos))) { + res.code(400); + res.send({ error: `Position must be a number!` }); + return false; + } + await player.seek(Number(pos)); + return true; + } + + async checker(req: Fastify.FastifyRequest, res: Fastify.FastifyReply): Promise { + const accpetKey: string[] = ["loop", "skip", "position"]; + const guildId = (req.params as Record)["guildId"]; + const player = this.client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return false; + } + if (req.headers["content-type"] !== "application/json") { + res.code(400); + res.send({ error: "content-type must be application/json!" }); + return false; + } + if (!req.body) { + res.code(400); + res.send({ error: "Missing body" }); + return false; + } + const jsonBody = req.body as Record; + for (const key of Object.keys(jsonBody)) { + if (!accpetKey.includes(key)) { + res.code(400); + res.send({ error: `Invalid body, key '${key}' does not exist!` }); + return false; + } + } + return true; + } + + resetData() { + this.skiped = false; + } +} diff --git a/src/web/server.ts b/src/web/server.ts index 2d7ea4b1..78905784 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -45,10 +45,7 @@ export class WebServer { ); this.app.get("/neko", (request, reply) => { - client.logger.info( - import.meta.url, - `${request.method} ${request.routeOptions.url} payload=${request.body ? request.body : "{}"}` - ); + client.logger.info(import.meta.url, `${request.method} ${request.routeOptions.url}`); reply.send("💀"); }); diff --git a/src/web/websocket.ts b/src/web/websocket.ts index 5caf0e23..e8e5357d 100644 --- a/src/web/websocket.ts +++ b/src/web/websocket.ts @@ -7,10 +7,7 @@ export class WebsocketRoute { main(fastify: Fastify.FastifyInstance) { fastify.get("/websocket", { websocket: true }, (socket, req) => { - this.client.logger.info( - import.meta.url, - `${req.method} ${req.routeOptions.url} payload=${req.body ? req.body : "{}"}` - ); + this.client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); socket.on("close", (code, reason) => { this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); From 5b379268df1e67cab17052a6e74e085b67549115 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Sun, 5 May 2024 20:18:31 +0700 Subject: [PATCH 07/21] add: weird health check endpoint --- src/web/server.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/web/server.ts b/src/web/server.ts index 78905784..ee5e70b7 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -44,9 +44,20 @@ export class WebServer { { prefix: "v1" } ); - this.app.get("/neko", (request, reply) => { + this.app.get("/catgirls", (request, reply) => { + const response = [ + "Bro 💀", + "Please stop...", + "This ain't rule 34...", + "💀", + "Can you do something better please -_-", + "Don't be like yandev ._.", + "Why you still here >:v", + "I know catgirls do nothing wrong but why you still here...", + "Bro, I don't have any catgirls collection (or cosplay collection) so please leave..." + ] client.logger.info(import.meta.url, `${request.method} ${request.routeOptions.url}`); - reply.send("💀"); + reply.send({ byteblaze: response[Math.floor(Math.random() * response.length)] }); }); this.app.listen({ port: this.client.config.features.WEB_SERVER.port }); From e24aac4e5c01b19c62c875fcf62f69419a38a0a5 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Mon, 6 May 2024 10:20:51 +0700 Subject: [PATCH 08/21] finish: Add all requrement route for webplayers --- src/web/player.ts | 4 +- src/web/route/deletePlayer.ts | 15 ++++++ src/web/route/getSearch.ts | 44 ++++++++++++++++ src/web/route/patchControl.ts | 98 ++++++++++++++++++++++++++++++----- src/web/server.ts | 15 +++--- 5 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 src/web/route/deletePlayer.ts create mode 100644 src/web/route/getSearch.ts diff --git a/src/web/player.ts b/src/web/player.ts index 2143ef3c..e5f538c2 100644 --- a/src/web/player.ts +++ b/src/web/player.ts @@ -8,18 +8,20 @@ import { getCurrentLoop } from "./route/getCurrentLoop.js"; import { getCurrentPaused } from "./route/getCurrentPaused.js"; import { getCurrentPosition } from "./route/getCurrentPosition.js"; import { PatchControl } from "./route/patchControl.js"; +import { deletePlayer } from "./route/deletePlayer.js"; export class PlayerRoute { constructor(protected client: Manager) {} main(fastify: Fastify.FastifyInstance) { fastify.get("/:guildId", (req, res) => getStatus(this.client, req, res)); + fastify.patch("/:guildId", (req, res) => new PatchControl(this.client).main(req, res)); + fastify.delete("/:guildId", (req, res) => deletePlayer(this.client, req, res)); fastify.get("/:guildId/loop", (req, res) => getCurrentLoop(this.client, req, res)); fastify.get("/:guildId/pause", (req, res) => getCurrentPaused(this.client, req, res)); fastify.get("/:guildId/position", (req, res) => getCurrentPosition(this.client, req, res)); fastify.get("/:guildId/queue", (req, res) => getQueueStatus(this.client, req, res)); fastify.get("/:guildId/current", (req, res) => getCurrentTrackStatus(this.client, req, res)); fastify.get("/:guildId/member/:userId", (req, res) => getMemberStatus(this.client, req, res)); - fastify.patch("/:guildId/control", (req, res) => new PatchControl(this.client).main(req, res)); } } diff --git a/src/web/route/deletePlayer.ts b/src/web/route/deletePlayer.ts new file mode 100644 index 00000000..90c2c386 --- /dev/null +++ b/src/web/route/deletePlayer.ts @@ -0,0 +1,15 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function deletePlayer(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const guildId = (req.params as Record)["guildId"]; + const player = client.rainlink.players.get(guildId); + if (!player) { + res.code(404); + res.send({ error: "Current player not found!" }); + return; + } + await player.destroy(); + res.code(204); +} diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts new file mode 100644 index 00000000..1f7c1ec5 --- /dev/null +++ b/src/web/route/getSearch.ts @@ -0,0 +1,44 @@ +import { User } from "discord.js"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getSearch(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + const query = (req.query as Record)["search"]; + const requester = (req.query as Record)["requester"]; + const source = (req.query as Record)["source"]; + let validSource = "youtube"; + if (source) { + const isSourceExist = client.rainlink.searchEngines.get(source); + if (isSourceExist) validSource = source; + } + const user = await client.users.fetch(requester).catch(() => {}); + if (!query) { + res.code(404); + res.send({ error: "Search param not found" }); + return; + } + const result = await client.rainlink.search(query, { requester: user }); + res.send({ + type: result.type, + playlistName: result.playlistName ?? null, + tracks: result.tracks.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }); +} diff --git a/src/web/route/patchControl.ts b/src/web/route/patchControl.ts index 1253f108..447a3fd3 100644 --- a/src/web/route/patchControl.ts +++ b/src/web/route/patchControl.ts @@ -3,8 +3,19 @@ import { Manager } from "../../manager.js"; import Fastify from "fastify"; import { RainlinkLoopMode, RainlinkPlayer } from "../../rainlink/main.js"; +export type TrackRes = { + title: string; + uri: string; + length: number; + thumbnail: string; + author: string; + requester: null; +}; + export class PatchControl { protected skiped: boolean = false; + protected isPrevious: boolean = false; + protected addedTrack: null | TrackRes = null; constructor(protected client: Manager) {} async main(req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { @@ -19,18 +30,21 @@ export class PatchControl { const jsonBody = req.body as Record; const currentKeys = Object.keys(jsonBody); for (const key of currentKeys) { - const data = await (this as any)[key](req, res, player, jsonBody[key]); + const data = await (this as any)[key](res, player, jsonBody[key]); if (!data) return; } res.send({ - loop: player.loop, - skip: this.skiped, - position: player.position, + loop: jsonBody.loop, + skiped: this.skiped, + previous: this.isPrevious, + position: jsonBody.position, + volume: jsonBody.volume, + added: this.addedTrack, }); this.resetData(); } - async loop(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, mode: string) { + async loop(res: Fastify.FastifyReply, player: RainlinkPlayer, mode: string) { if (!mode || !["song", "queue", "none"].includes(mode)) { res.code(400); res.send({ error: `Invalid loop mode, '${mode}' mode does not exist!` }); @@ -40,15 +54,24 @@ export class PatchControl { return true; } - async skip(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, data: string) { - if (data === undefined || player.queue.length == 0) return true; - await player.skip(); - this.skiped = true; + async skipMode(res: Fastify.FastifyReply, player: RainlinkPlayer, mode: string) { + if (!mode || !["previous", "skip"].includes(mode)) { + res.code(400); + res.send({ error: `Invalid loop mode, '${mode}' mode does not exist!` }); + return false; + } + if (player.queue.length == 0) return true; + if (mode == "skip") { + await player.skip(); + this.skiped = true; + return true; + } + await player.previous(); + this.isPrevious = true; return true; } - async position(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, player: RainlinkPlayer, pos: string) { - if (!pos) return true; + async position(res: Fastify.FastifyReply, player: RainlinkPlayer, pos: string) { if (isNaN(Number(pos))) { res.code(400); res.send({ error: `Position must be a number!` }); @@ -58,8 +81,46 @@ export class PatchControl { return true; } + async volume(res: Fastify.FastifyReply, player: RainlinkPlayer, vol: string) { + if (!vol) return true; + if (isNaN(Number(vol))) { + res.code(400); + res.send({ error: `Volume must be a number!` }); + return false; + } + await player.setVolume(Number(vol)); + return true; + } + + async add(res: Fastify.FastifyReply, player: RainlinkPlayer, uri: string) { + if (!uri) return true; + console.log(uri); + if (!this.isValidHttpUrl(uri)) { + res.code(400); + res.send({ error: `add property must have a link!` }); + return false; + } + const result = await this.client.rainlink.search(uri); + if (result.tracks.length == 0) { + res.code(400); + res.send({ error: `Track not found!` }); + return false; + } + const song = result.tracks[0]; + player.queue.add(song); + this.addedTrack = { + title: song.title, + uri: song.uri || "", + length: song.duration, + thumbnail: song.artworkUrl || "", + author: song.author, + requester: null, + }; + return true; + } + async checker(req: Fastify.FastifyRequest, res: Fastify.FastifyReply): Promise { - const accpetKey: string[] = ["loop", "skip", "position"]; + const accpetKey: string[] = ["loop", "skipMode", "position", "volume", "add"]; const guildId = (req.params as Record)["guildId"]; const player = this.client.rainlink.players.get(guildId); if (!player) { @@ -90,5 +151,18 @@ export class PatchControl { resetData() { this.skiped = false; + this.addedTrack = null; + } + + isValidHttpUrl(string: string) { + let url; + + try { + url = new URL(string); + } catch (_) { + return false; + } + + return url.protocol === "http:" || url.protocol === "https:"; } } diff --git a/src/web/server.ts b/src/web/server.ts index ee5e70b7..8e259741 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -3,6 +3,7 @@ import Fastify from "fastify"; import WebsocketPlugin from "@fastify/websocket"; import { WebsocketRoute } from "./websocket.js"; import { PlayerRoute } from "./player.js"; +import { getSearch } from "./route/getSearch.js"; export class WebServer { app: Fastify.FastifyInstance; @@ -38,7 +39,7 @@ export class WebServer { }, { prefix: "players" } ); - fastify.get("/test", (req, res) => res.send({ message: "Hallo :D" })); + fastify.get("/search", (req, res) => getSearch(client, req, res)); done(); }, { prefix: "v1" } @@ -46,16 +47,16 @@ export class WebServer { this.app.get("/catgirls", (request, reply) => { const response = [ - "Bro 💀", - "Please stop...", - "This ain't rule 34...", + "Bro 💀", + "Please stop...", + "This ain't rule 34...", "💀", - "Can you do something better please -_-", + "Can you do something better please -_-", "Don't be like yandev ._.", "Why you still here >:v", "I know catgirls do nothing wrong but why you still here...", - "Bro, I don't have any catgirls collection (or cosplay collection) so please leave..." - ] + "Bro, I don't have any catgirls collection (or cosplay collection) so please leave...", + ]; client.logger.info(import.meta.url, `${request.method} ${request.routeOptions.url}`); reply.send({ byteblaze: response[Math.floor(Math.random() * response.length)] }); }); From d6ebb1e63153b0baa0a084fc47d5451a5803bb5f Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Mon, 6 May 2024 12:34:32 +0700 Subject: [PATCH 09/21] add: encoded for GET search, status (queue|current) endpoints --- src/web/route/getCurrentTrackStatus.ts | 1 + src/web/route/getQueueStatus.ts | 1 + src/web/route/getSearch.ts | 15 +++++++++++++-- src/web/route/getStatus.ts | 2 ++ src/web/route/patchControl.ts | 1 + 5 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/web/route/getCurrentTrackStatus.ts b/src/web/route/getCurrentTrackStatus.ts index ac9845bc..77d50a44 100644 --- a/src/web/route/getCurrentTrackStatus.ts +++ b/src/web/route/getCurrentTrackStatus.ts @@ -17,6 +17,7 @@ export async function getCurrentTrackStatus(client: Manager, req: Fastify.Fastif res.send({ data: song ? { + encoded: song.encoded, title: song.title, uri: song.uri, length: song.duration, diff --git a/src/web/route/getQueueStatus.ts b/src/web/route/getQueueStatus.ts index df67cfb8..4938848a 100644 --- a/src/web/route/getQueueStatus.ts +++ b/src/web/route/getQueueStatus.ts @@ -14,6 +14,7 @@ export async function getQueueStatus(client: Manager, req: Fastify.FastifyReques return player.queue.map((track) => { const requesterQueue = track.requester as User; return { + encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts index 1f7c1ec5..2ca73347 100644 --- a/src/web/route/getSearch.ts +++ b/src/web/route/getSearch.ts @@ -1,10 +1,11 @@ import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; +import { RainlinkSearchResultType } from "../../rainlink/main.js"; export async function getSearch(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); - const query = (req.query as Record)["search"]; + const query = (req.query as Record)["identifier"]; const requester = (req.query as Record)["requester"]; const source = (req.query as Record)["source"]; let validSource = "youtube"; @@ -18,13 +19,23 @@ export async function getSearch(client: Manager, req: Fastify.FastifyRequest, re res.send({ error: "Search param not found" }); return; } - const result = await client.rainlink.search(query, { requester: user }); + const result = await client.rainlink.search(query, { requester: user, engine: source }).catch(() => ({ + playlistName: "dreamvast@error@noNode", + tracks: [], + type: RainlinkSearchResultType.SEARCH, + })); + if (result.tracks.length == 0 && result.playlistName == "dreamvast@error@noNode") { + res.code(404); + res.send({ error: "No node avaliable!" }); + return; + } res.send({ type: result.type, playlistName: result.playlistName ?? null, tracks: result.tracks.map((track) => { const requesterQueue = track.requester as User; return { + encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/getStatus.ts b/src/web/route/getStatus.ts index 644e7094..10816565 100644 --- a/src/web/route/getStatus.ts +++ b/src/web/route/getStatus.ts @@ -31,6 +31,7 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re position: player.position, current: song ? { + encoded: song.encoded, title: song.title, uri: song.uri, length: song.duration, @@ -49,6 +50,7 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re queue: player.queue.map((track) => { const requesterQueue = track.requester as User; return { + encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/patchControl.ts b/src/web/route/patchControl.ts index 447a3fd3..7e4ca8c3 100644 --- a/src/web/route/patchControl.ts +++ b/src/web/route/patchControl.ts @@ -152,6 +152,7 @@ export class PatchControl { resetData() { this.skiped = false; this.addedTrack = null; + this.isPrevious = false; } isValidHttpUrl(string: string) { From 4b8eb478d5576daa9bdae6855bacdd7eb911abaf Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Tue, 7 May 2024 11:41:13 +0700 Subject: [PATCH 10/21] finish: /v1/players route --- src/@types/Config.ts | 1 + src/commands/Music/Nowplaying.ts | 4 +- src/commands/Playlist/Delete.ts | 2 +- src/commands/Premium/Redeem.ts | 2 +- src/database/setup/client.ts | 7 +- src/database/setup/lavalink.ts | 6 +- src/database/setup/setup.ts | 4 +- src/events/guild/guildCreate.ts | 2 +- src/events/guild/guildDelete.ts | 2 +- src/events/guild/voiceStateUpdate.ts | 28 +- src/events/player/playerCreate.ts | 4 +- src/events/player/playerDestroy.ts | 8 +- src/events/player/playerException.ts | 4 +- src/events/player/playerPause.ts | 4 +- src/events/player/playerResume.ts | 4 +- src/events/player/playerStop.ts | 4 +- src/events/player/queueEmpty.ts | 6 +- src/events/track/trackEnd.ts | 4 +- src/events/track/trackResolveError.ts | 4 +- src/events/track/trackStart.ts | 19 +- src/events/track/trackStuck.ts | 4 +- src/handlers/Player/ButtonCommands/Pause.ts | 4 +- src/handlers/Player/loadContent.ts | 26 +- src/handlers/Player/loadUpdate.ts | 8 +- src/rainlink/Plugin/Apple/Plugin.ts | 479 ++++++++++---------- src/rainlink/Plugin/Deezer/Plugin.ts | 384 ++++++++-------- src/rainlink/Plugin/Nico/Plugin.ts | 305 +++++++------ src/rainlink/Plugin/Spotify/Plugin.ts | 450 +++++++++--------- src/services/CheckPermissionService.ts | 8 +- src/services/ConfigDataService.ts | 1 + src/services/LoggerService.ts | 2 +- src/structures/CommandHandler.ts | 8 +- src/web/player.ts | 2 + src/web/route/deletePlayer.ts | 3 +- src/web/route/getCurrentLoop.ts | 3 +- src/web/route/getCurrentPaused.ts | 3 +- src/web/route/getCurrentPosition.ts | 3 +- src/web/route/getCurrentTrackStatus.ts | 3 +- src/web/route/getMemberStatus.ts | 16 +- src/web/route/getQueueStatus.ts | 4 +- src/web/route/getSearch.ts | 6 +- src/web/route/getStatus.ts | 9 +- src/web/route/patchControl.ts | 51 ++- src/web/route/postCreatePlayer.ts | 58 +++ src/web/server.ts | 8 + 45 files changed, 1040 insertions(+), 927 deletions(-) create mode 100644 src/web/route/postCreatePlayer.ts diff --git a/src/@types/Config.ts b/src/@types/Config.ts index 51b9cc34..4c62721d 100644 --- a/src/@types/Config.ts +++ b/src/@types/Config.ts @@ -54,6 +54,7 @@ export interface WebServer { enable: boolean; port: number; auth: string; + whitelist: string[]; } export interface Lavalink { diff --git a/src/commands/Music/Nowplaying.ts b/src/commands/Music/Nowplaying.ts index 528b7154..3f849ced 100644 --- a/src/commands/Music/Nowplaying.ts +++ b/src/commands/Music/Nowplaying.ts @@ -142,9 +142,9 @@ export default class implements Command { .setTimestamp(); try { - const channel = (await client.channels.fetch(`${handler.channel?.id}`)) as TextChannel; + const channel = (await client.channels.fetch(`${handler.channel?.id}`).catch(() => undefined)) as TextChannel; if (!channel) return; - const message = await channel.messages.fetch(`${currentNPInterval?.msg?.id}`); + const message = await channel.messages.fetch(`${currentNPInterval?.msg?.id}`).catch(() => undefined); if (!message) return; if (currentNPInterval && currentNPInterval.msg) currentNPInterval.msg.edit({ content: " ", embeds: [embeded] }); diff --git a/src/commands/Playlist/Delete.ts b/src/commands/Playlist/Delete.ts index 03ff5673..39ff0e23 100644 --- a/src/commands/Playlist/Delete.ts +++ b/src/commands/Playlist/Delete.ts @@ -103,7 +103,7 @@ export default class implements Command { }); collector?.on("end", async () => { - const checkMsg = await handler.channel?.messages.fetch(String(msg?.id)); + const checkMsg = await handler.channel?.messages.fetch(String(msg?.id)).catch(() => undefined); const embed = new EmbedBuilder() .setDescription(`${client.getString(handler.language, "command.playlist", "delete_no")}`) .setColor(client.color); diff --git a/src/commands/Premium/Redeem.ts b/src/commands/Premium/Redeem.ts index 6beee8bf..c8ce2854 100644 --- a/src/commands/Premium/Redeem.ts +++ b/src/commands/Premium/Redeem.ts @@ -140,7 +140,7 @@ export default class implements Command { .setColor(client.color); try { - const channel = await client.channels.fetch(client.config.features.PREMIUM_LOG_CHANNEL); + const channel = await client.channels.fetch(client.config.features.PREMIUM_LOG_CHANNEL).catch(() => undefined); if (!channel || (channel && !channel.isTextBased())) return; channel.messages.channel.send({ embeds: [embed] }); } catch {} diff --git a/src/database/setup/client.ts b/src/database/setup/client.ts index 8fbac6a4..920e0ec3 100644 --- a/src/database/setup/client.ts +++ b/src/database/setup/client.ts @@ -73,11 +73,12 @@ export class ClientDataService { const fetched_info = this.infoChannelembed; SetupChannel.forEach(async (g) => { - const fetch_channel = g.channel.length !== 0 ? await this.client.channels.fetch(g.channel) : undefined; + const fetch_channel = + g.channel.length !== 0 ? await this.client.channels.fetch(g.channel).catch(() => undefined) : undefined; if (!fetch_channel) return; const text_channel = fetch_channel! as TextChannel; - const interval_text = await text_channel.messages!.fetch(g.statmsg); - await interval_text.edit({ content: ``, embeds: [fetched_info] }); + const interval_text = await text_channel.messages!.fetch(g.statmsg).catch(() => undefined); + interval_text ? await interval_text.edit({ content: ``, embeds: [fetched_info] }) : true; }); }); } diff --git a/src/database/setup/lavalink.ts b/src/database/setup/lavalink.ts index 03f931b2..32064d7a 100644 --- a/src/database/setup/lavalink.ts +++ b/src/database/setup/lavalink.ts @@ -58,9 +58,9 @@ export class AutoReconnectLavalinkService { async connectChannel(data: { id: string; value: AutoReconnect }) { const lavalink_mess = "Lavalink: "; - const channel = await this.client.channels.fetch(data.value.text); - const guild = await this.client.guilds.fetch(data.value.guild); - const voice = (await this.client.channels.fetch(data.value.voice)) as VoiceChannel; + const channel = await this.client.channels.fetch(data.value.text).catch(() => undefined); + const guild = await this.client.guilds.fetch(data.value.guild).catch(() => undefined); + const voice = (await this.client.channels.fetch(data.value.voice).catch(() => undefined)) as VoiceChannel; if (!channel || !voice) { this.client.logger.setup( import.meta.url, diff --git a/src/database/setup/setup.ts b/src/database/setup/setup.ts index fb613fb8..2a628fb9 100644 --- a/src/database/setup/setup.ts +++ b/src/database/setup/setup.ts @@ -22,10 +22,10 @@ export class SongRequesterCleanSetup { } async restore(setupData: Setup) { - let channel = (await this.client.channels.fetch(setupData.channel)) as TextChannel; + let channel = (await this.client.channels.fetch(setupData.channel).catch(() => undefined)) as TextChannel; if (!channel) return; - let playMsg = await channel.messages.fetch(setupData.playmsg); + let playMsg = await channel.messages.fetch(setupData.playmsg).catch(() => undefined); if (!playMsg) return; let guildModel = await this.client.db.language.get(`${setupData.guild}`); diff --git a/src/events/guild/guildCreate.ts b/src/events/guild/guildCreate.ts index dd82116e..ed590be4 100644 --- a/src/events/guild/guildCreate.ts +++ b/src/events/guild/guildCreate.ts @@ -62,7 +62,7 @@ export default class { if (!client.config.features.GUILD_LOG_CHANNEL) return; try { - const eventChannel = await client.channels.fetch(client.config.features.GUILD_LOG_CHANNEL); + const eventChannel = await client.channels.fetch(client.config.features.GUILD_LOG_CHANNEL).catch(() => undefined); if (!eventChannel || !eventChannel.isTextBased()) return; const embed = new EmbedBuilder() .setAuthor({ diff --git a/src/events/guild/guildDelete.ts b/src/events/guild/guildDelete.ts index e07b9c6a..1bdcb42e 100644 --- a/src/events/guild/guildDelete.ts +++ b/src/events/guild/guildDelete.ts @@ -8,7 +8,7 @@ export default class { client.guilds.cache.delete(`${guild!.id}`); if (!client.config.features.GUILD_LOG_CHANNEL) return; try { - const eventChannel = await client.channels.fetch(client.config.features.GUILD_LOG_CHANNEL); + const eventChannel = await client.channels.fetch(client.config.features.GUILD_LOG_CHANNEL).catch(() => undefined); if (!eventChannel || !eventChannel.isTextBased()) return; const owner = await guild.fetchOwner(); const embed = new EmbedBuilder() diff --git a/src/events/guild/voiceStateUpdate.ts b/src/events/guild/voiceStateUpdate.ts index 5d6ab39d..dce93513 100644 --- a/src/events/guild/voiceStateUpdate.ts +++ b/src/events/guild/voiceStateUpdate.ts @@ -35,7 +35,7 @@ export default class { if (data && data.twentyfourseven) return; - const isInVoice = await newState.guild.members.fetch(client.user!.id); + const isInVoice = await newState.guild.members.fetch(client.user!.id).catch(() => undefined); if (!isInVoice || !isInVoice.voice.channelId) { player.data.set("sudo-destroy", true); @@ -55,12 +55,12 @@ export default class { newState.guild.members.me!.voice.setSuppressed(false); if (oldState.id === client.user!.id) return; - const isInOldVoice = await oldState.guild.members.fetch(client.user!.id); + const isInOldVoice = await oldState.guild.members.fetch(client.user!.id).catch(() => undefined); if (!isInOldVoice || !isInOldVoice.voice.channelId) return; const vcRoom = oldState.guild.members.me!.voice.channel!.id; - const leaveEmbed = (await client.channels.fetch(player.textId)) as TextChannel; + const leaveEmbed = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if ( newState.guild.members.me!.voice?.channel && @@ -81,15 +81,17 @@ export default class { player.paused == false ? true : player.setPause(false); if (currentPause !== false && player.track !== null) { - const msg = await leaveEmbed.send({ - embeds: [ - new EmbedBuilder() - .setDescription(`${client.getString(language, "event.player", "leave_resume")}`) - .setColor(client.color), - ], - }); + const msg = leaveEmbed + ? await leaveEmbed.send({ + embeds: [ + new EmbedBuilder() + .setDescription(`${client.getString(language, "event.player", "leave_resume")}`) + .setColor(client.color), + ], + }) + : null; setTimeout( - async () => (!setup || setup == null || setup.channel !== player.textId ? msg.delete() : true), + async () => ((!setup || setup == null || setup.channel !== player.textId) && msg ? msg.delete() : true), client.config.bot.DELETE_MSG_TIMEOUT ); } @@ -114,7 +116,7 @@ export default class { ], }); setTimeout(async () => { - const isChannelAvalible = await client.channels.fetch(msg.channelId); + const isChannelAvalible = await client.channels.fetch(msg.channelId).catch(() => undefined); if (!isChannelAvalible) return; !setup || setup == null || setup.channel !== player.textId ? msg.delete() : true; }, client.config.bot.DELETE_MSG_TIMEOUT); @@ -136,7 +138,7 @@ export default class { .setColor(client.color); try { if (leaveEmbed) { - const msg = newPlayer ? await leaveEmbed.send({ embeds: [TimeoutEmbed] }) : undefined; + const msg = newPlayer && leaveEmbed ? await leaveEmbed.send({ embeds: [TimeoutEmbed] }) : undefined; setTimeout( async () => msg && (!setup || setup == null || setup.channel !== player.textId) ? msg.delete() : undefined, diff --git a/src/events/player/playerCreate.ts b/src/events/player/playerCreate.ts index 16b79443..cbd3af9f 100644 --- a/src/events/player/playerCreate.ts +++ b/src/events/player/playerCreate.ts @@ -3,7 +3,7 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - const guild = await client.guilds.fetch(player.guildId); - client.logger.info(import.meta.url, `Player Created in @ ${guild!.name} / ${player.guildId}`); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); + client.logger.info(import.meta.url, `Player Created in @ ${guild?.name} / ${player.guildId}`); } } diff --git a/src/events/player/playerDestroy.ts b/src/events/player/playerDestroy.ts index 3cb6a858..d9b176a1 100644 --- a/src/events/player/playerDestroy.ts +++ b/src/events/player/playerDestroy.ts @@ -12,14 +12,14 @@ export default class { "The database is not yet connected so this event will temporarily not execute. Please try again later!" ); - const guild = await client.guilds.fetch(player.guildId); - client.logger.info(import.meta.url, `Player Destroy in @ ${guild!.name} / ${player.guildId}`); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); + client.logger.info(import.meta.url, `Player Destroy in @ ${guild?.name} / ${player.guildId}`); /////////// Update Music Setup ////////// await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; client.sentQueue.set(player.guildId, false); let data = await new AutoReconnectBuilderService(client, player).get(player.guildId); @@ -31,7 +31,7 @@ export default class { guildId: data.guild!, voiceId: data.voice!, textId: data.text!, - shardId: guild.shardId ?? 0, + shardId: guild?.shardId ?? 0, deaf: true, volume: client.config.lavalink.DEFAULT_VOLUME ?? 100, }); diff --git a/src/events/player/playerException.ts b/src/events/player/playerException.ts index 4c3be173..fe02ec96 100644 --- a/src/events/player/playerException.ts +++ b/src/events/player/playerException.ts @@ -11,7 +11,7 @@ export default class { /////////// Update Music Setup ////////// await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const fetch_channel = await client.channels.fetch(player.textId); + const fetch_channel = await client.channels.fetch(player.textId).catch(() => undefined); const text_channel = fetch_channel! as TextChannel; if (text_channel) { await text_channel.send({ @@ -24,7 +24,7 @@ export default class { } const data247 = await new AutoReconnectBuilderService(client, player).get(player.guildId); - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (data247 !== null && data247 && data247.twentyfourseven && channel) return new ClearMessageService(client, channel, player); diff --git a/src/events/player/playerPause.ts b/src/events/player/playerPause.ts index 14e5c9e7..e87a886f 100644 --- a/src/events/player/playerPause.ts +++ b/src/events/player/playerPause.ts @@ -15,10 +15,10 @@ export default class { const setup = await client.db.setup.get(`${player.guildId}`); if (setup && setup.playmsg) { - const channel = await client.channels.fetch(setup.channel); + const channel = await client.channels.fetch(setup.channel).catch(() => undefined); if (!channel) return; if (!channel.isTextBased) return; - const msg = await (channel as TextChannel).messages.fetch(setup.playmsg); + const msg = await (channel as TextChannel).messages.fetch(setup.playmsg).catch(() => undefined); if (!msg) return; msg.edit({ components: [client.enSwitch] }); } diff --git a/src/events/player/playerResume.ts b/src/events/player/playerResume.ts index a2606bff..7cb3e8f7 100644 --- a/src/events/player/playerResume.ts +++ b/src/events/player/playerResume.ts @@ -15,10 +15,10 @@ export default class { const setup = await client.db.setup.get(`${player.guildId}`); if (setup && setup.playmsg) { - const channel = await client.channels.fetch(setup.channel); + const channel = await client.channels.fetch(setup.channel).catch(() => undefined); if (!channel) return; if (!channel.isTextBased) return; - const msg = await (channel as TextChannel).messages.fetch(setup.playmsg); + const msg = await (channel as TextChannel).messages.fetch(setup.playmsg).catch(() => undefined); if (!msg) return; msg.edit({ components: [client.enSwitchMod] }); } diff --git a/src/events/player/playerStop.ts b/src/events/player/playerStop.ts index 9845d2ec..de96e217 100644 --- a/src/events/player/playerStop.ts +++ b/src/events/player/playerStop.ts @@ -12,14 +12,14 @@ export default class { "The database is not yet connected so this event will temporarily not execute. Please try again later!" ); - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); client.logger.info(import.meta.url, `Player Stop in @ ${guild!.name} / ${player.guildId}`); /////////// Update Music Setup ////////// await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; client.sentQueue.set(player.guildId, false); const autoreconnectService = new AutoReconnectBuilderService(client, player); let data = await autoreconnectService.get(player.guildId); diff --git a/src/events/player/queueEmpty.ts b/src/events/player/queueEmpty.ts index 97ec99da..2b95662e 100644 --- a/src/events/player/queueEmpty.ts +++ b/src/events/player/queueEmpty.ts @@ -16,7 +16,7 @@ export default class { await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); if (player.data.get("autoplay") === true) { const requester = player.data.get("requester"); @@ -33,7 +33,7 @@ export default class { if (finalRes.length !== 0) { player.queue.add(finalRes.length <= 1 ? finalRes[0] : finalRes[1]); player.play(); - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (channel) return new ClearMessageService(client, channel, player); return; } @@ -42,7 +42,7 @@ export default class { client.logger.info(import.meta.url, `Player Empty in @ ${guild!.name} / ${player.guildId}`); const data = await new AutoReconnectBuilderService(client, player).get(player.guildId); - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (data !== null && data && data.twentyfourseven && channel) return new ClearMessageService(client, channel, player); diff --git a/src/events/track/trackEnd.ts b/src/events/track/trackEnd.ts index 7aedda32..72016122 100644 --- a/src/events/track/trackEnd.ts +++ b/src/events/track/trackEnd.ts @@ -12,14 +12,14 @@ export default class { "The database is not yet connected so this event will temporarily not execute. Please try again later!" ); - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); client.logger.info(import.meta.url, `Player End in @ ${guild!.name} / ${player.guildId}`); /////////// Update Music Setup ////////// await client.UpdateMusic(player); /////////// Update Music Setup /////////// let data = await new AutoReconnectBuilderService(client, player).get(player.guildId); - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (!channel) return; if (data && data.twentyfourseven) return; diff --git a/src/events/track/trackResolveError.ts b/src/events/track/trackResolveError.ts index f0d6f227..6589cad8 100644 --- a/src/events/track/trackResolveError.ts +++ b/src/events/track/trackResolveError.ts @@ -12,7 +12,7 @@ export default class { "The database is not yet connected so this event will temporarily not execute. Please try again later!" ); - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); client.logger.error(import.meta.url, message); @@ -20,7 +20,7 @@ export default class { await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (!channel) return; let guildModel = await client.db.language.get(`${channel.guild.id}`); diff --git a/src/events/track/trackStart.ts b/src/events/track/trackStart.ts index 1f54f056..539b1e38 100644 --- a/src/events/track/trackStart.ts +++ b/src/events/track/trackStart.ts @@ -15,7 +15,7 @@ export default class { "The database is not yet connected so this event will temporarily not execute. Please try again later!" ); - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); client.logger.info(import.meta.url, `Player Started in @ ${guild!.name} / ${player.guildId}`); let SongNoti = await client.db.songNoti.get(`${player.guildId}`); @@ -29,7 +29,7 @@ export default class { /////////// Update Music Setup /////////// - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (!channel) return; const autoreconnect = new AutoReconnectBuilderService(client, player); @@ -113,14 +113,17 @@ export default class { .setThumbnail(`https://img.youtube.com/vi/${track.identifier}/hqdefault.jpg`) .setTimestamp(); - const playing_channel = (await client.channels.fetch(player.textId)) as TextChannel; + const playing_channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; - const nplaying = await playing_channel.send({ - embeds: [embeded], - components: [playerRowOne, playerRowTwo], - // files: client.config.bot.SAFE_PLAYER_MODE ? [] : [attachment], - }); + const nplaying = playing_channel + ? await playing_channel.send({ + embeds: [embeded], + components: [playerRowOne, playerRowTwo], + // files: client.config.bot.SAFE_PLAYER_MODE ? [] : [attachment], + }) + : undefined; + if (!nplaying) return; client.nplayingMsg.set(player.guildId, nplaying); const collector = nplaying.createMessageComponentCollector({ diff --git a/src/events/track/trackStuck.ts b/src/events/track/trackStuck.ts index f385a38a..f1b290eb 100644 --- a/src/events/track/trackStuck.ts +++ b/src/events/track/trackStuck.ts @@ -16,9 +16,9 @@ export default class { await client.UpdateMusic(player); /////////// Update Music Setup /////////// - const guild = await client.guilds.fetch(player.guildId); + const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); - const channel = (await client.channels.fetch(player.textId)) as TextChannel; + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (!channel) return; let guildModel = await client.db.language.get(`${channel.guild.id}`); diff --git a/src/handlers/Player/ButtonCommands/Pause.ts b/src/handlers/Player/ButtonCommands/Pause.ts index 778f941b..3bd50a4d 100644 --- a/src/handlers/Player/ButtonCommands/Pause.ts +++ b/src/handlers/Player/ButtonCommands/Pause.ts @@ -59,9 +59,9 @@ export class ButtonPause { }); return; } else { - const getChannel = await this.client.channels.fetch(data.channel); + const getChannel = await this.client.channels.fetch(data.channel).catch(() => undefined); if (!getChannel) return; - let playMsg = await (getChannel as TextChannel)!.messages.fetch(data.playmsg); + let playMsg = await (getChannel as TextChannel)!.messages.fetch(data.playmsg).catch(() => undefined); if (!playMsg) return; const newPlayer = await this.player.setPause(!this.player.paused); diff --git a/src/handlers/Player/loadContent.ts b/src/handlers/Player/loadContent.ts index 3be87cde..1ab04ac6 100644 --- a/src/handlers/Player/loadContent.ts +++ b/src/handlers/Player/loadContent.ts @@ -36,13 +36,13 @@ export class playerLoadContent { if (!interaction.guild || interaction.user.bot) return; if (!interaction.isButton()) return; const { customId, member } = interaction; - let voiceMember = await interaction.guild.members.fetch((member as GuildMember)!.id); + let voiceMember = await interaction.guild.members.fetch((member as GuildMember)!.id).catch(() => undefined); let channel = voiceMember!.voice.channel; let player = client.rainlink.players.get(interaction.guild.id); if (!player) return; - const playChannel = await client.channels.fetch(player.textId); + const playChannel = await client.channels.fetch(player.textId).catch(() => undefined); if (!playChannel) return; let guildModel = await client.db.language.get(`${player.guildId}`); @@ -82,7 +82,7 @@ export class playerLoadContent { if (!database!.enable) return; - let channel = (await message.guild.channels.fetch(database!.channel)) as TextChannel; + let channel = (await message.guild.channels.fetch(database!.channel).catch(() => undefined)) as TextChannel; if (!channel) return; if (database!.channel != message.channel.id) return; @@ -96,7 +96,11 @@ export class playerLoadContent { if (message.id !== database.playmsg) { const preInterval = setInterval(async () => { - const fetchedMessage = await message.channel.messages.fetch({ limit: 50 }); + const fetchedMessage = await message.channel.messages.fetch({ limit: 50 }).catch(() => undefined); + if (!fetchedMessage) { + clearInterval(preInterval); + return; + } const final = fetchedMessage.filter((msg) => msg.id !== database?.playmsg); if (final.size > 0) (message.channel as TextChannel).bulkDelete(final).catch(() => {}); else clearInterval(preInterval); @@ -124,12 +128,12 @@ export class playerLoadContent { ], }); - let msg = await message.channel.messages.fetch(database!.playmsg); + let msg = await message.channel.messages.fetch(database!.playmsg).catch(() => undefined); const emotes = (str: string) => str.match(/|\p{Extended_Pictographic}/gu); if (emotes(song) !== null) { - msg.reply({ + msg?.reply({ embeds: [ new EmbedBuilder() .setDescription(`${client.getString(language, "event.setup", "play_emoji")}`) @@ -150,7 +154,7 @@ export class playerLoadContent { }); else { if (message.member!.voice.channel !== message.guild!.members.me!.voice.channel) { - msg.reply({ + msg?.reply({ embeds: [ new EmbedBuilder() .setDescription(`${client.getString(language, "error", "no_same_voice")}`) @@ -165,7 +169,7 @@ export class playerLoadContent { const tracks = result.tracks; if (!result.tracks.length) { - msg.edit({ + msg?.edit({ content: `${client.getString(language, "event.setup", "setup_content")}\n${`${client.getString( language, "event.setup", @@ -193,7 +197,7 @@ export class playerLoadContent { })}` ) .setColor(client.color); - msg.reply({ content: " ", embeds: [embed] }); + msg?.reply({ content: " ", embeds: [embed] }); } else if (result.type === "TRACK") { if (!player.playing) player.play(); const embed = new EmbedBuilder() @@ -205,7 +209,7 @@ export class playerLoadContent { })}` ) .setColor(client.color); - msg.reply({ content: " ", embeds: [embed] }); + msg?.reply({ content: " ", embeds: [embed] }); } else if (result.type === "SEARCH") { if (!player.playing) player.play(); const embed = new EmbedBuilder().setColor(client.color).setDescription( @@ -215,7 +219,7 @@ export class playerLoadContent { request: `${result.tracks[0].requester}`, })}` ); - msg.reply({ content: " ", embeds: [embed] }); + msg?.reply({ content: " ", embeds: [embed] }); } function getTitle(tracks: RainlinkTrack[]): string { diff --git a/src/handlers/Player/loadUpdate.ts b/src/handlers/Player/loadUpdate.ts index 08c9f130..87e5f321 100644 --- a/src/handlers/Player/loadUpdate.ts +++ b/src/handlers/Player/loadUpdate.ts @@ -16,10 +16,10 @@ export class playerLoadUpdate { if (!data) return; if (data.enable === false) return; - let channel = (await client.channels.fetch(data.channel)) as TextChannel; + let channel = (await client.channels.fetch(data.channel).catch(() => undefined)) as TextChannel; if (!channel) return; - let playMsg = await channel.messages.fetch(data.playmsg); + let playMsg = await channel.messages.fetch(data.playmsg).catch(() => undefined); if (!playMsg) return; let guildModel = await client.db.language.get(`${player.guildId}`); @@ -105,10 +105,10 @@ export class playerLoadUpdate { if (!data) return; if (data.enable === false) return; - let channel = (await client.channels.fetch(data.channel)) as TextChannel; + let channel = (await client.channels.fetch(data.channel).catch(() => undefined)) as TextChannel; if (!channel) return; - let playMsg = await channel.messages.fetch(data.playmsg); + let playMsg = await channel.messages.fetch(data.playmsg).catch(() => undefined); if (!playMsg) return; let guildModel = await client.db.language.get(`${player.guildId}`); diff --git a/src/rainlink/Plugin/Apple/Plugin.ts b/src/rainlink/Plugin/Apple/Plugin.ts index a70b3833..a35f91a7 100644 --- a/src/rainlink/Plugin/Apple/Plugin.ts +++ b/src/rainlink/Plugin/Apple/Plugin.ts @@ -31,271 +31,274 @@ const credentials = { }; export class RainlinkPlugin extends SourceRainlinkPlugin { - public options: AppleOptions; - private manager: Rainlink | null; - private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; - private readonly methods: Record Promise>; - private credentials: HeaderType; - private fetchURL: string; - private baseURL: string; - public countryCode: string; - public imageWidth: number; - public imageHeight: number; - - /** + public options: AppleOptions; + private manager: Rainlink | null; + private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; + private readonly methods: Record Promise>; + private credentials: HeaderType; + private fetchURL: string; + private baseURL: string; + public countryCode: string; + public imageWidth: number; + public imageHeight: number; + + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return "am"; - } + public sourceIdentify(): string { + return 'am'; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return "apple"; - } + public sourceName(): string { + return 'apple'; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** Name function for getting plugin name */ - public name(): string { - return "rainlink-apple"; - } + /** Name function for getting plugin name */ + public name(): string { + return 'rainlink-apple'; + } - /** + /** * Initialize the plugin. * @param appleOptions The rainlink apple plugin options */ - constructor(appleOptions: AppleOptions) { - super(); - this.methods = { - artist: this.getArtist.bind(this), - album: this.getAlbum.bind(this), - playlist: this.getPlaylist.bind(this), - track: this.getTrack.bind(this), - }; - this.options = appleOptions; - this.manager = null; - this._search = undefined; - this.countryCode = this.options?.countryCode ? this.options?.countryCode : "us"; - this.imageHeight = this.options?.imageHeight ? this.options?.imageHeight : 900; - this.imageWidth = this.options?.imageWidth ? this.options?.imageWidth : 600; - this.baseURL = "https://api.music.apple.com/v1/"; - this.fetchURL = `https://amp-api.music.apple.com/v1/catalog/${this.countryCode}`; - this.credentials = { - Authorization: `Bearer ${credentials.APPLE_TOKEN}`, - origin: "https://music.apple.com", - }; - } - - /** + constructor(appleOptions: AppleOptions) { + super(); + this.methods = { + artist: this.getArtist.bind(this), + album: this.getAlbum.bind(this), + playlist: this.getPlaylist.bind(this), + track: this.getTrack.bind(this), + }; + this.options = appleOptions; + this.manager = null; + this._search = undefined; + this.countryCode = this.options?.countryCode ? this.options?.countryCode : 'us'; + this.imageHeight = this.options?.imageHeight ? this.options?.imageHeight : 900; + this.imageWidth = this.options?.imageWidth ? this.options?.imageWidth : 600; + this.baseURL = 'https://api.music.apple.com/v1/'; + this.fetchURL = `https://amp-api.music.apple.com/v1/catalog/${this.countryCode}`; + this.credentials = { + Authorization: `Bearer ${credentials.APPLE_TOKEN}`, + origin: 'https://music.apple.com', + }; + } + + /** * load the plugin * @param rainlink The rainlink class */ - public load(manager: Rainlink): void { - this.manager = manager; - this._search = manager.search.bind(manager); - manager.search = this.search.bind(this); - } + public load(manager: Rainlink): void { + this.manager = manager; + this._search = manager.search.bind(manager); + manager.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.manager = rainlink; - this.manager.search = rainlink.search.bind(rainlink); - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return await this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.manager = rainlink; + this.manager.search = rainlink.search.bind(rainlink); + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { - let type: string; - let id: string; - let isTrack: boolean = false; - - if (!this.manager || !this._search) throw new Error("rainlink-apple is not loaded yet."); - - if (!query) throw new Error("Query is required"); - - const isUrl = /^https?:\/\//.test(query); - - if (!REGEX_SONG_ONLY.exec(query) || REGEX_SONG_ONLY.exec(query) == null) { - const extract = REGEX.exec(query) || []; - id = extract![4]; - type = extract![1]; - } else { - const extract = REGEX_SONG_ONLY.exec(query) || []; - id = extract![8]; - type = extract![1]; - isTrack = true; - } - - if (type in this.methods) { - try { - this.debug(`Start search from ${this.sourceName()} plugin`); - let _function = this.methods[type]; - if (isTrack) _function = this.methods.track; - const result: Result = await _function(id, options?.requester); - - const loadType = isTrack ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === "apple" && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private async getData(params: string) { - const req = await fetch(`${this.fetchURL}${params}`, { - headers: this.credentials, - }); - const res = (await req.json()) as any; - return res.data as D; - } - - private async searchTrack(query: string, requester: unknown): Promise { - try { - const res = await this.getData(`/search?types=songs&term=${query.replace(/ /g, "+").toLocaleLowerCase()}`).catch( - (e) => { - throw new Error(e); - } - ); - return { - tracks: res.results.songs.data.map((track: Track) => this.buildRainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getTrack(id: string, requester: unknown): Promise { - try { - const track = await this.getData(`/songs/${id}`).catch((e) => { - throw new Error(e); - }); - return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getArtist(id: string, requester: unknown): Promise { - try { - const track = await this.getData(`/artists/${id}/view/top-songs`).catch((e) => { - throw new Error(e); - }); - return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getAlbum(id: string, requester: unknown): Promise { - try { - const album = await this.getData(`/albums/${id}`).catch((e) => { - throw new Error(e); - }); - - const tracks = album[0].relationships.tracks.data - .filter(this.filterNullOrUndefined) - .map((track: Track) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: album[0].attributes.name }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getPlaylist(id: string, requester: unknown): Promise { - try { - const playlist = await this.getData(`/playlists/${id}`).catch((e) => { - throw new Error(e); - }); - - const tracks = playlist[0].relationships.tracks.data - .filter(this.filterNullOrUndefined) - .map((track: any) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: playlist[0].attributes.name }; - } catch (e: any) { - throw new Error(e); - } - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.SEARCH, - }; - } - - private buildRainlinkTrack(appleTrack: Track, requester: unknown) { - const artworkURL = String(appleTrack.attributes.artwork.url) - .replace("{w}", String(this.imageWidth)) - .replace("{h}", String(this.imageHeight)); - return new RainlinkTrack( - { - encoded: "", - info: { - sourceName: this.sourceName(), - identifier: appleTrack.id, - isSeekable: true, - author: appleTrack.attributes.artistName ? appleTrack.attributes.artistName : "Unknown", - length: appleTrack.attributes.durationInMillis, - isStream: false, - position: 0, - title: appleTrack.attributes.name, - uri: appleTrack.attributes.url || "", - artworkUrl: artworkURL ? artworkURL : "", - }, - pluginInfo: { - name: "rainlink@apple", - }, - }, - requester - ); - } - - private debug(logs: string) { - this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [Plugin] / [Apple] | ${logs}`) : true; - } + public async searchDirect( + query: string, + options?: RainlinkSearchOptions | undefined, + ): Promise { + let type: string; + let id: string; + let isTrack: boolean = false; + + if (!this.manager || !this._search) throw new Error('rainlink-apple is not loaded yet.'); + + if (!query) throw new Error('Query is required'); + + const isUrl = /^https?:\/\//.test(query); + + if (!REGEX_SONG_ONLY.exec(query) || REGEX_SONG_ONLY.exec(query) == null) { + const extract = REGEX.exec(query) || []; + id = extract![4]; + type = extract![1]; + } else { + const extract = REGEX_SONG_ONLY.exec(query) || []; + id = extract![8]; + type = extract![1]; + isTrack = true; + } + + if (type in this.methods) { + try { + this.debug(`Start search from ${this.sourceName()} plugin`); + let _function = this.methods[type]; + if (isTrack) _function = this.methods.track; + const result: Result = await _function(id, options?.requester); + + const loadType = isTrack ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === 'apple' && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private async getData(params: string) { + const req = await fetch(`${this.fetchURL}${params}`, { + headers: this.credentials, + }); + const res = (await req.json()) as any; + return res.data as D; + } + + private async searchTrack(query: string, requester: unknown): Promise { + try { + const res = await this.getData( + `/search?types=songs&term=${query.replace(/ /g, '+').toLocaleLowerCase()}`, + ).catch(e => { + throw new Error(e); + }); + return { + tracks: res.results.songs.data.map((track: Track) => this.buildRainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getTrack(id: string, requester: unknown): Promise { + try { + const track = await this.getData(`/songs/${id}`).catch(e => { + throw new Error(e); + }); + return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getArtist(id: string, requester: unknown): Promise { + try { + const track = await this.getData(`/artists/${id}/view/top-songs`).catch(e => { + throw new Error(e); + }); + return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getAlbum(id: string, requester: unknown): Promise { + try { + const album = await this.getData(`/albums/${id}`).catch(e => { + throw new Error(e); + }); + + const tracks = album[0].relationships.tracks.data + .filter(this.filterNullOrUndefined) + .map((track: Track) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: album[0].attributes.name }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getPlaylist(id: string, requester: unknown): Promise { + try { + const playlist = await this.getData(`/playlists/${id}`).catch(e => { + throw new Error(e); + }); + + const tracks = playlist[0].relationships.tracks.data + .filter(this.filterNullOrUndefined) + .map((track: any) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: playlist[0].attributes.name }; + } catch (e: any) { + throw new Error(e); + } + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType, + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.SEARCH, + }; + } + + private buildRainlinkTrack(appleTrack: Track, requester: unknown) { + const artworkURL = String(appleTrack.attributes.artwork.url) + .replace('{w}', String(this.imageWidth)) + .replace('{h}', String(this.imageHeight)); + return new RainlinkTrack( + { + encoded: '', + info: { + sourceName: this.sourceName(), + identifier: appleTrack.id, + isSeekable: true, + author: appleTrack.attributes.artistName ? appleTrack.attributes.artistName : 'Unknown', + length: appleTrack.attributes.durationInMillis, + isStream: false, + position: 0, + title: appleTrack.attributes.name, + uri: appleTrack.attributes.url || '', + artworkUrl: artworkURL ? artworkURL : '', + }, + pluginInfo: { + name: 'rainlink@apple', + }, + }, + requester, + ); + } + + private debug(logs: string) { + this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink] -> [Plugin] -> [Apple] | ${logs}`) : true; + } } // Interfaces @@ -335,4 +338,4 @@ export interface TrackAttributes { name: string; previews: any[]; artistName: string; -} +} \ No newline at end of file diff --git a/src/rainlink/Plugin/Deezer/Plugin.ts b/src/rainlink/Plugin/Deezer/Plugin.ts index 770300df..5a7bf386 100644 --- a/src/rainlink/Plugin/Deezer/Plugin.ts +++ b/src/rainlink/Plugin/Deezer/Plugin.ts @@ -10,221 +10,225 @@ const REGEX = /^https?:\/\/(?:www\.)?deezer\.com\/[a-z]+\/(track|album|playlist) const SHORT_REGEX = /^https:\/\/deezer\.page\.link\/[a-zA-Z0-9]{12}$/; export class RainlinkPlugin extends SourceRainlinkPlugin { - private manager: Rainlink | null; - private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; - private readonly methods: Record Promise>; - /** + private manager: Rainlink | null; + private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; + private readonly methods: Record Promise>; + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return "dz"; - } + public sourceIdentify(): string { + return 'dz'; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return "deezer"; - } + public sourceName(): string { + return 'deezer'; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * Initialize the plugin. */ - constructor() { - super(); - this.methods = { - track: this.getTrack.bind(this), - album: this.getAlbum.bind(this), - playlist: this.getPlaylist.bind(this), - }; - this.manager = null; - this._search = undefined; - } - - /** + constructor() { + super(); + this.methods = { + track: this.getTrack.bind(this), + album: this.getAlbum.bind(this), + playlist: this.getPlaylist.bind(this), + }; + this.manager = null; + this._search = undefined; + } + + /** * load the plugin * @param rainlink The rainlink class */ - public load(manager: Rainlink): void { - this.manager = manager; - this._search = manager.search.bind(manager); - manager.search = this.search.bind(this); - } + public load(manager: Rainlink): void { + this.manager = manager; + this._search = manager.search.bind(manager); + manager.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.manager = rainlink; - this.manager.search = rainlink.search.bind(rainlink); - } - - /** Name function for getting plugin name */ - public name(): string { - return "rainlink-deezer"; - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return await this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.manager = rainlink; + this.manager.search = rainlink.search.bind(rainlink); + } + + /** Name function for getting plugin name */ + public name(): string { + return 'rainlink-deezer'; + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { - if (!this.manager || !this._search) throw new Error("rainlink-deezer is not loaded yet."); - - if (!query) throw new Error("Query is required"); - - const isUrl = /^https?:\/\//.test(query); - - if (SHORT_REGEX.test(query)) { - const url = new URL(query); - const res = await request(url.origin + url.pathname, { method: "HEAD" }); - query = String(res.headers.location); - } - - const [, type, id] = REGEX.exec(query) || []; - - if (type in this.methods) { - this.debug(`Start search from ${this.sourceName()} plugin`); - try { - const _function = this.methods[type]; - const result: Result = await _function(id, options?.requester); - - const loadType = type === "track" ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private async searchTrack(query: string, requester: unknown): Promise { - try { - const req = await fetch(`${API_URL}/search/track?q=${decodeURIComponent(query)}`); - const data = await req.json(); - - const res = data as SearchResult; - return { - tracks: res.data.map((track) => this.buildRainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getTrack(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/track/${id}/`); - const data = await request.json(); - const track = data as DeezerTrack; - - return { tracks: [this.buildRainlinkTrack(track, requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getAlbum(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/album/${id}/`); - const data = await request.json(); - const album = data as Album; - - const tracks = album.tracks.data - .filter(this.filterNullOrUndefined) - .map((track) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: album.title }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getPlaylist(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/playlist/${id}`); - const data = await request.json(); - const playlist = data as Playlist; - - const tracks = playlist.tracks.data - .filter(this.filterNullOrUndefined) - .map((track) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: playlist.title }; - } catch (e: any) { - throw new Error(e); - } - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.SEARCH, - }; - } - - private buildRainlinkTrack(dezzerTrack: any, requester: unknown) { - return new RainlinkTrack( - { - encoded: "", - info: { - sourceName: this.sourceName(), - identifier: dezzerTrack.id, - isSeekable: true, - author: dezzerTrack.artist ? dezzerTrack.artist.name : "Unknown", - length: dezzerTrack.duration * 1000, - isStream: false, - position: 0, - title: dezzerTrack.title, - uri: `https://www.deezer.com/track/${dezzerTrack.id}`, - artworkUrl: dezzerTrack.album ? dezzerTrack.album.cover : "", - }, - pluginInfo: { - name: "rainlink@deezer", - }, - }, - requester - ); - } - - private debug(logs: string) { - this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [Plugin] / [Deezer] | ${logs}`) : true; - } + public async searchDirect( + query: string, + options?: RainlinkSearchOptions | undefined, + ): Promise { + if (!this.manager || !this._search) throw new Error('rainlink-deezer is not loaded yet.'); + + if (!query) throw new Error('Query is required'); + + const isUrl = /^https?:\/\//.test(query); + + if (SHORT_REGEX.test(query)) { + const url = new URL(query); + const res = await fetch(url.origin + url.pathname, { method: 'HEAD' }); + query = String(res.headers.get('location')); + } + + const [, type, id] = REGEX.exec(query) || []; + + if (type in this.methods) { + this.debug(`Start search from ${this.sourceName()} plugin`); + try { + const _function = this.methods[type]; + const result: Result = await _function(id, options?.requester); + + const loadType = + type === 'track' ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private async searchTrack(query: string, requester: unknown): Promise { + try { + const req = await fetch(`${API_URL}/search/track?q=${decodeURIComponent(query)}`); + const data = await req.json(); + + const res = data as SearchResult; + return { + tracks: res.data.map(track => this.buildRainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getTrack(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/track/${id}/`); + const data = await request.json(); + const track = data as DeezerTrack; + + return { tracks: [this.buildRainlinkTrack(track, requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getAlbum(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/album/${id}/`); + const data = await request.json(); + const album = data as Album; + + const tracks = album.tracks.data + .filter(this.filterNullOrUndefined) + .map(track => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: album.title }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getPlaylist(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/playlist/${id}`); + const data = await request.json(); + const playlist = data as Playlist; + + const tracks = playlist.tracks.data + .filter(this.filterNullOrUndefined) + .map(track => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: playlist.title }; + } catch (e: any) { + throw new Error(e); + } + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType, + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.SEARCH, + }; + } + + private buildRainlinkTrack(dezzerTrack: any, requester: unknown) { + return new RainlinkTrack( + { + encoded: '', + info: { + sourceName: this.sourceName(), + identifier: dezzerTrack.id, + isSeekable: true, + author: dezzerTrack.artist ? dezzerTrack.artist.name : 'Unknown', + length: dezzerTrack.duration * 1000, + isStream: false, + position: 0, + title: dezzerTrack.title, + uri: `https://www.deezer.com/track/${dezzerTrack.id}`, + artworkUrl: dezzerTrack.album ? dezzerTrack.album.cover : '', + }, + pluginInfo: { + name: 'rainlink@deezer', + }, + }, + requester, + ); + } + + private debug(logs: string) { + this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink Deezer Plugin]: ${logs}`) : true; + } } // Interfaces @@ -269,4 +273,4 @@ interface SearchResult { name: string; }; data: RainlinkTrack[]; -} +} \ No newline at end of file diff --git a/src/rainlink/Plugin/Nico/Plugin.ts b/src/rainlink/Plugin/Nico/Plugin.ts index 1c687c52..8cedfb55 100644 --- a/src/rainlink/Plugin/Nico/Plugin.ts +++ b/src/rainlink/Plugin/Nico/Plugin.ts @@ -1,14 +1,18 @@ -import { RainlinkEvents, RainlinkPluginType } from "../../Interface/Constants.js"; -import { RainlinkSearchOptions, RainlinkSearchResult, RainlinkSearchResultType } from "../../Interface/Manager.js"; -import { RainlinkTrack } from "../../Player/RainlinkTrack.js"; -import { Rainlink } from "../../Rainlink.js"; -import { SourceRainlinkPlugin } from "../SourceRainlinkPlugin.js"; -import NicoResolver from "./NicoResolver.js"; -import search from "./NicoSearch.js"; +import { RainlinkEvents, RainlinkPluginType } from '../../main.js'; +import { + RainlinkSearchOptions, + RainlinkSearchResult, + RainlinkSearchResultType, +} from '../../main.js'; +import { RainlinkTrack } from '../../main.js'; +import { Rainlink } from '../../main.js'; +import { SourceRainlinkPlugin } from '../../main.js'; +import NicoResolver from './NicoResolver.js'; +import search from './NicoSearch.js'; const REGEX = RegExp( - // https://github.com/ytdl-org/youtube-dl/blob/a8035827177d6b59aca03bd717acb6a9bdd75ada/youtube_dl/extractor/niconico.py#L162 - "https?://(?:www\\.|secure\\.|sp\\.)?nicovideo\\.jp/watch/(?(?:[a-z]{2})?[0-9]+)" + // https://github.com/ytdl-org/youtube-dl/blob/a8035827177d6b59aca03bd717acb6a9bdd75ada/youtube_dl/extractor/niconico.py#L162 + 'https?://(?:www\\.|secure\\.|sp\\.)?nicovideo\\.jp/watch/(?(?:[a-z]{2})?[0-9]+)', ); /** The rainlink nicovideo plugin options */ @@ -18,195 +22,200 @@ export interface NicoOptions { } export class RainlinkPlugin extends SourceRainlinkPlugin { - /** + /** * The options of the plugin. */ - public options: NicoOptions; - private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | undefined; - private rainlink: Rainlink | null; + public options: NicoOptions; + private _search: + | ((query: string, options?: RainlinkSearchOptions) => Promise) + | undefined; + private rainlink: Rainlink | null; - private readonly methods: Record Promise>; + private readonly methods: Record Promise>; - /** + /** * Initialize the plugin. * @param nicoOptions Options for run plugin */ - constructor(nicoOptions: NicoOptions) { - super(); - this.options = nicoOptions; - this.methods = { - track: this.getTrack.bind(this), - }; - this.rainlink = null; - } + constructor(nicoOptions: NicoOptions) { + super(); + this.options = nicoOptions; + this.methods = { + track: this.getTrack.bind(this), + }; + this.rainlink = null; + } - /** + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return "nv"; - } + public sourceIdentify(): string { + return 'nv'; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return "nicovideo"; - } + public sourceName(): string { + return 'nicovideo'; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * load the plugin * @param rainlink The rainlink class */ - public load(rainlink: Rainlink) { - this.rainlink = rainlink; - this._search = rainlink.search.bind(rainlink); - rainlink.search = this.search.bind(this); - } + public load(rainlink: Rainlink) { + this.rainlink = rainlink; + this._search = rainlink.search.bind(rainlink); + rainlink.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.rainlink = rainlink; - rainlink.search = rainlink.search.bind(rainlink); - } + public unload(rainlink: Rainlink) { + this.rainlink = rainlink; + rainlink.search = rainlink.search.bind(rainlink); + } - /** Name function for getting plugin name */ - public name(): string { - return "rainlink-nico"; - } + /** Name function for getting plugin name */ + public name(): string { + return 'rainlink-nico'; + } - private async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return await this.searchDirect(query, options); - else return res; - } + private async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } - /** + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { - if (!this.rainlink || !this._search) throw new Error("rainlink-nico is not loaded yet."); + public async searchDirect( + query: string, + options?: RainlinkSearchOptions | undefined, + ): Promise { + if (!this.rainlink || !this._search) throw new Error('rainlink-nico is not loaded yet.'); - if (!query) throw new Error("Query is required"); - const [, id] = REGEX.exec(query) || []; + if (!query) throw new Error('Query is required'); + const [, id] = REGEX.exec(query) || []; - const isUrl = /^https?:\/\//.test(query); + const isUrl = /^https?:\/\//.test(query); - if (id) { - this.debug(`Start search from ${this.sourceName()} plugin`); - const _function = this.methods.track; - const result: Result = await _function(id, options?.requester); + if (id) { + this.debug(`Start search from ${this.sourceName()} plugin`); + const _function = this.methods.track; + const result: Result = await _function(id, options?.requester); - const loadType = result ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.SEARCH; - const playlistName = result.name ?? undefined; + const loadType = result ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.SEARCH; + const playlistName = result.name ?? undefined; - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks && tracks.length !== 0 ? tracks : [], loadType); - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks && tracks.length !== 0 ? tracks : [], loadType); + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.TRACK, - }; - } + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType, + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.TRACK, + }; + } - private async searchTrack(query: string, requester: unknown) { - try { - const { data } = await search({ - q: query, - targets: ["tagsExact"], - fields: ["contentId"], - sort: "-viewCounter", - limit: 10, - }); + private async searchTrack(query: string, requester: unknown) { + try { + const { data } = await search({ + q: query, + targets: ['tagsExact'], + fields: ['contentId'], + sort: '-viewCounter', + limit: 10, + }); - const res: VideoInfo[] = []; + const res: VideoInfo[] = []; - for (let i = 0; i < data.length; i++) { - const element = data[i]; - const nico = new NicoResolver(`https://www.nicovideo.jp/watch/${element.contentId}`); - const info = await nico.getVideoInfo(); - res.push(info); - } + for (let i = 0; i < data.length; i++) { + const element = data[i]; + const nico = new NicoResolver(`https://www.nicovideo.jp/watch/${element.contentId}`); + const info = await nico.getVideoInfo(); + res.push(info); + } - return { - tracks: res.map((track) => this.buildrainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } + return { + tracks: res.map(track => this.buildrainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } - private async getTrack(id: string, requester: unknown) { - try { - const niconico = new NicoResolver(`https://www.nicovideo.jp/watch/${id}`); - const info = await niconico.getVideoInfo(); + private async getTrack(id: string, requester: unknown) { + try { + const niconico = new NicoResolver(`https://www.nicovideo.jp/watch/${id}`); + const info = await niconico.getVideoInfo(); - return { tracks: [this.buildrainlinkTrack(info, requester)] }; - } catch (e: any) { - throw new Error(e); - } - } + return { tracks: [this.buildrainlinkTrack(info, requester)] }; + } catch (e: any) { + throw new Error(e); + } + } - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } - private buildrainlinkTrack(nicoTrack: any, requester: unknown) { - return new RainlinkTrack( - { - encoded: "", - info: { - sourceName: this.sourceName(), - identifier: nicoTrack.id, - isSeekable: true, - author: nicoTrack.owner ? nicoTrack.owner.nickname : "Unknown", - length: nicoTrack.duration * 1000, - isStream: false, - position: 0, - title: nicoTrack.title, - uri: `https://www.nicovideo.jp/watch/${nicoTrack.id}`, - artworkUrl: nicoTrack.thumbnail ? nicoTrack.thumbnail.url : "", - }, - pluginInfo: { - name: "rainlink.mod@nico", - }, - }, - requester - ); - } + private buildrainlinkTrack(nicoTrack: any, requester: unknown) { + return new RainlinkTrack( + { + encoded: '', + info: { + sourceName: this.sourceName(), + identifier: nicoTrack.id, + isSeekable: true, + author: nicoTrack.owner ? nicoTrack.owner.nickname : 'Unknown', + length: nicoTrack.duration * 1000, + isStream: false, + position: 0, + title: nicoTrack.title, + uri: `https://www.nicovideo.jp/watch/${nicoTrack.id}`, + artworkUrl: nicoTrack.thumbnail ? nicoTrack.thumbnail.url : '', + }, + pluginInfo: { + name: 'rainlink.mod@nico', + }, + }, + requester, + ); + } - private debug(logs: string) { - this.rainlink ? this.rainlink.emit(RainlinkEvents.Debug, `[Rainlink] / [Plugin] / [Nico] | ${logs}`) : true; - } + private debug(logs: string) { + this.rainlink ? this.rainlink.emit(RainlinkEvents.Debug, `[Rainlink Nico Plugin]: ${logs}`) : true; + } } // Interfaces @@ -271,4 +280,4 @@ interface OriginalVideoInfo { /** @ignore */ export interface VideoInfo extends OriginalVideoInfo { owner: OwnerInfo; -} +} \ No newline at end of file diff --git a/src/rainlink/Plugin/Spotify/Plugin.ts b/src/rainlink/Plugin/Spotify/Plugin.ts index 55adfec0..6d47af0f 100644 --- a/src/rainlink/Plugin/Spotify/Plugin.ts +++ b/src/rainlink/Plugin/Spotify/Plugin.ts @@ -28,260 +28,266 @@ export interface SpotifyOptions { } export class RainlinkPlugin extends SourceRainlinkPlugin { - /** + /** * The options of the plugin. */ - public options: SpotifyOptions; + public options: SpotifyOptions; - private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | null; - private rainlink: Rainlink | null; + private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | null; + private rainlink: Rainlink | null; - private readonly methods: Record Promise>; - private requestManager: RequestManager; + private readonly methods: Record Promise>; + private requestManager: RequestManager; - /** + /** * Initialize the plugin. * @param spotifyOptions Options for run plugin */ - constructor(spotifyOptions: SpotifyOptions) { - super(); - this.options = spotifyOptions; - this.requestManager = new RequestManager(spotifyOptions); - - this.methods = { - track: this.getTrack.bind(this), - album: this.getAlbum.bind(this), - artist: this.getArtist.bind(this), - playlist: this.getPlaylist.bind(this), - }; - this.rainlink = null; - this._search = null; - } - - /** + constructor(spotifyOptions: SpotifyOptions) { + super(); + this.options = spotifyOptions; + this.requestManager = new RequestManager(spotifyOptions); + + this.methods = { + track: this.getTrack.bind(this), + album: this.getAlbum.bind(this), + artist: this.getArtist.bind(this), + playlist: this.getPlaylist.bind(this), + }; + this.rainlink = null; + this._search = null; + } + + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return "sp"; - } + public sourceIdentify(): string { + return 'sp'; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return "spotify"; - } + public sourceName(): string { + return 'spotify'; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * load the plugin * @param rainlink The rainlink class */ - public load(rainlink: Rainlink) { - this.rainlink = rainlink; - this._search = rainlink.search.bind(rainlink); - rainlink.search = this.search.bind(this); - } + public load(rainlink: Rainlink) { + this.rainlink = rainlink; + this._search = rainlink.search.bind(rainlink); + rainlink.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.rainlink = rainlink; - rainlink.search = rainlink.search.bind(rainlink); - } - - /** Name function for getting plugin name */ - public name(): string { - return "rainlink-spotify"; - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return await this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.rainlink = rainlink; + rainlink.search = rainlink.search.bind(rainlink); + } + + /** Name function for getting plugin name */ + public name(): string { + return 'rainlink-spotify'; + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { - if (!this.rainlink || !this._search) throw new Error("rainlink-spotify is not loaded yet."); - - if (!query) throw new Error("Query is required"); - - const isUrl = /^https?:\/\//.test(query); - - if (SHORT_REGEX.test(query)) { - const res = await request(query, { method: "HEAD" }); - query = String(res.headers.location); - } - - const [, type, id] = REGEX.exec(query) || []; - - if (type in this.methods) { - try { - const _function = this.methods[type]; - const result: Result = await _function(id, options?.requester); - - const loadType = type === "track" ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.TRACK, - }; - } - - private async searchTrack(query: string, requester: unknown): Promise { - const limit = + public async searchDirect( + query: string, + options?: RainlinkSearchOptions | undefined, + ): Promise { + if (!this.rainlink || !this._search) throw new Error('rainlink-spotify is not loaded yet.'); + + if (!query) throw new Error('Query is required'); + + const isUrl = /^https?:\/\//.test(query); + + if (SHORT_REGEX.test(query)) { + const res = await fetch(query, { method: 'HEAD' }); + query = String(res.headers.get('location')); + } + + const [, type, id] = REGEX.exec(query) || []; + + if (type in this.methods) { + try { + const _function = this.methods[type]; + const result: Result = await _function(id, options?.requester); + + const loadType = + type === 'track' ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType, + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.TRACK, + }; + } + + private async searchTrack(query: string, requester: unknown): Promise { + const limit = this.options.searchLimit && this.options.searchLimit > 0 && this.options.searchLimit < 50 - ? this.options.searchLimit - : 10; - const tracks = await this.requestManager.makeRequest( - `/search?q=${decodeURIComponent(query)}&type=track&limit=${limit}&market=${this.options.searchMarket ?? "US"}` - ); - return { - tracks: tracks.tracks.items.map((track) => this.buildrainlinkTrack(track, requester)), - }; - } - - private async getTrack(id: string, requester: unknown): Promise { - const track = await this.requestManager.makeRequest(`/tracks/${id}`); - return { tracks: [this.buildrainlinkTrack(track, requester)] }; - } - - private async getAlbum(id: string, requester: unknown): Promise { - const album = await this.requestManager.makeRequest( - `/albums/${id}?market=${this.options.searchMarket ?? "US"}` - ); - const tracks = album.tracks.items - .filter(this.filterNullOrUndefined) - .map((track) => this.buildrainlinkTrack(track, requester, album.images[0]?.url)); - - if (album && tracks.length) { - let next = album.tracks.next; - let page = 1; - - while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { - const nextTracks = await this.requestManager.makeRequest(next ?? "", true); - page++; - if (nextTracks.items.length) { - next = nextTracks.next; - tracks.push( - ...nextTracks.items - .filter(this.filterNullOrUndefined) - .filter((a) => a.track) - .map((track) => this.buildrainlinkTrack(track.track!, requester, album.images[0]?.url)) - ); - } - } - } - - return { tracks, name: album.name }; - } - - private async getArtist(id: string, requester: unknown): Promise { - const artist = await this.requestManager.makeRequest(`/artists/${id}`); - const fetchedTracks = await this.requestManager.makeRequest( - `/artists/${id}/top-tracks?market=${this.options.searchMarket ?? "US"}` - ); - - const tracks = fetchedTracks.tracks - .filter(this.filterNullOrUndefined) - .map((track) => this.buildrainlinkTrack(track, requester, artist.images[0]?.url)); - - return { tracks, name: artist.name }; - } - - private async getPlaylist(id: string, requester: unknown): Promise { - const playlist = await this.requestManager.makeRequest( - `/playlists/${id}?market=${this.options.searchMarket ?? "US"}` - ); - - const tracks = playlist.tracks.items - .filter(this.filterNullOrUndefined) - .map((track) => this.buildrainlinkTrack(track.track, requester, playlist.images[0]?.url)); - - if (playlist && tracks.length) { - let next = playlist.tracks.next; - let page = 1; - while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { - const nextTracks = await this.requestManager.makeRequest(next ?? "", true); - page++; - if (nextTracks.items.length) { - next = nextTracks.next; - tracks.push( - ...nextTracks.items - .filter(this.filterNullOrUndefined) - .filter((a) => a.track) - .map((track) => this.buildrainlinkTrack(track.track!, requester, playlist.images[0]?.url)) - ); - } - } - } - return { tracks, name: playlist.name }; - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildrainlinkTrack(spotifyTrack: Track, requester: unknown, thumbnail?: string) { - return new RainlinkTrack( - { - encoded: "", - info: { - sourceName: "spotify", - identifier: spotifyTrack.id, - isSeekable: true, - author: spotifyTrack.artists[0] ? spotifyTrack.artists[0].name : "Unknown", - length: spotifyTrack.duration_ms, - isStream: false, - position: 0, - title: spotifyTrack.name, - uri: `https://open.spotify.com/track/${spotifyTrack.id}`, - artworkUrl: thumbnail ? thumbnail : spotifyTrack.album?.images[0]?.url, - }, - pluginInfo: { - name: this.name(), - }, - }, - requester - ); - } + ? this.options.searchLimit + : 10; + const tracks = await this.requestManager.makeRequest( + `/search?q=${decodeURIComponent( + query, + )}&type=track&limit=${limit}&market=${this.options.searchMarket ?? 'US'}`, + ); + return { + tracks: tracks.tracks.items.map(track => this.buildrainlinkTrack(track, requester)), + }; + } + + private async getTrack(id: string, requester: unknown): Promise { + const track = await this.requestManager.makeRequest(`/tracks/${id}`); + return { tracks: [this.buildrainlinkTrack(track, requester)] }; + } + + private async getAlbum(id: string, requester: unknown): Promise { + const album = await this.requestManager.makeRequest( + `/albums/${id}?market=${this.options.searchMarket ?? 'US'}`, + ); + const tracks = album.tracks.items + .filter(this.filterNullOrUndefined) + .map(track => this.buildrainlinkTrack(track, requester, album.images[0]?.url)); + + if (album && tracks.length) { + let next = album.tracks.next; + let page = 1; + + while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { + const nextTracks = await this.requestManager.makeRequest(next ?? '', true); + page++; + if (nextTracks.items.length) { + next = nextTracks.next; + tracks.push( + ...nextTracks.items + .filter(this.filterNullOrUndefined) + .filter(a => a.track) + .map(track => this.buildrainlinkTrack(track.track!, requester, album.images[0]?.url)), + ); + } + } + } + + return { tracks, name: album.name }; + } + + private async getArtist(id: string, requester: unknown): Promise { + const artist = await this.requestManager.makeRequest(`/artists/${id}`); + const fetchedTracks = await this.requestManager.makeRequest( + `/artists/${id}/top-tracks?market=${this.options.searchMarket ?? 'US'}`, + ); + + const tracks = fetchedTracks.tracks + .filter(this.filterNullOrUndefined) + .map(track => this.buildrainlinkTrack(track, requester, artist.images[0]?.url)); + + return { tracks, name: artist.name }; + } + + private async getPlaylist(id: string, requester: unknown): Promise { + const playlist = await this.requestManager.makeRequest( + `/playlists/${id}?market=${this.options.searchMarket ?? 'US'}`, + ); + + const tracks = playlist.tracks.items + .filter(this.filterNullOrUndefined) + .map(track => this.buildrainlinkTrack(track.track, requester, playlist.images[0]?.url)); + + if (playlist && tracks.length) { + let next = playlist.tracks.next; + let page = 1; + while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { + const nextTracks = await this.requestManager.makeRequest(next ?? '', true); + page++; + if (nextTracks.items.length) { + next = nextTracks.next; + tracks.push( + ...nextTracks.items + .filter(this.filterNullOrUndefined) + .filter(a => a.track) + .map(track => this.buildrainlinkTrack(track.track!, requester, playlist.images[0]?.url)), + ); + } + } + } + return { tracks, name: playlist.name }; + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildrainlinkTrack(spotifyTrack: Track, requester: unknown, thumbnail?: string) { + return new RainlinkTrack( + { + encoded: '', + info: { + sourceName: 'spotify', + identifier: spotifyTrack.id, + isSeekable: true, + author: spotifyTrack.artists[0] ? spotifyTrack.artists[0].name : 'Unknown', + length: spotifyTrack.duration_ms, + isStream: false, + position: 0, + title: spotifyTrack.name, + uri: `https://open.spotify.com/track/${spotifyTrack.id}`, + artworkUrl: thumbnail ? thumbnail : spotifyTrack.album?.images[0]?.url, + }, + pluginInfo: { + name: this.name(), + }, + }, + requester, + ); + } } /** @ignore */ @@ -466,4 +472,4 @@ export interface Track { track_number: number; type: string; uri: string; -} +} \ No newline at end of file diff --git a/src/services/CheckPermissionService.ts b/src/services/CheckPermissionService.ts index adeeed11..15d289e6 100644 --- a/src/services/CheckPermissionService.ts +++ b/src/services/CheckPermissionService.ts @@ -8,11 +8,11 @@ export interface CheckPermissionResultInterface { export class CheckPermissionServices { async interaction(interaction: GlobalInteraction, permArray: bigint[]): Promise { - const voiceChannel = await interaction.guild?.members.fetch(interaction.user.id); + const voiceChannel = await interaction.guild?.members.fetch(interaction.user.id).catch(() => undefined); const isUserInVoice = voiceChannel?.voice.channel; - const isUserInText = await interaction.guild?.channels.fetch(String(interaction.channelId)); + const isUserInText = await interaction.guild?.channels.fetch(String(interaction.channelId)).catch(() => undefined); for (const permBit of permArray) { if (isUserInVoice && !isUserInVoice.permissionsFor(interaction.guild?.members.me!).has(permBit)) { @@ -41,9 +41,9 @@ export class CheckPermissionServices { } async message(message: Message, permArray: bigint[]): Promise { - const voiceChannel = await message.guild?.members.fetch(message.author.id); + const voiceChannel = await message.guild?.members.fetch(message.author.id).catch(() => undefined); const isUserInVoice = voiceChannel?.voice.channel; - const isUserInText = await message.guild?.channels.fetch(String(message.channelId)); + const isUserInText = await message.guild?.channels.fetch(String(message.channelId)).catch(() => undefined); for (const permBit of permArray) { if (isUserInVoice && !isUserInVoice.permissionsFor(message.guild?.members.me!).has(permBit)) { return { diff --git a/src/services/ConfigDataService.ts b/src/services/ConfigDataService.ts index ca282c9f..1b47fa19 100644 --- a/src/services/ConfigDataService.ts +++ b/src/services/ConfigDataService.ts @@ -128,6 +128,7 @@ export class ConfigDataService { enable: false, port: 2880, auth: "youshallnotpass", + whitelist: [], }, PREMIUM_LOG_CHANNEL: "", GUILD_LOG_CHANNEL: "", diff --git a/src/services/LoggerService.ts b/src/services/LoggerService.ts index 69c906d5..a8529247 100644 --- a/src/services/LoggerService.ts +++ b/src/services/LoggerService.ts @@ -190,7 +190,7 @@ export class LoggerService { const channelId = this.client.config.features.LOG_CHANNEL; if (!channelId || channelId.length == 0) return; try { - const channel = (await this.client.channels.fetch(channelId)) as TextChannel; + const channel = (await this.client.channels.fetch(channelId).catch(() => undefined)) as TextChannel; if (!channel || !channel.isTextBased()) return; let embed = null; if (message.length > 4096) { diff --git a/src/structures/CommandHandler.ts b/src/structures/CommandHandler.ts index 871ffd8c..dd130c50 100644 --- a/src/structures/CommandHandler.ts +++ b/src/structures/CommandHandler.ts @@ -168,7 +168,7 @@ export class CommandHandler { public async parseMentions(data: string): Promise { if (this.USERS_PATTERN.test(data)) { const extract = this.USERS_PATTERN.exec(data); - const user = await this.client.users.fetch(extract![1]); + const user = await this.client.users.fetch(extract![1]).catch(() => undefined); if (!user || user == null) return { type: ParseMentionEnum.ERROR, @@ -181,7 +181,7 @@ export class CommandHandler { } if (this.CHANNELS_PATTERN.test(data)) { const extract = this.CHANNELS_PATTERN.exec(data); - const channel = await this.client.channels.fetch(extract![1]); + const channel = await this.client.channels.fetch(extract![1]).catch(() => undefined); if (!channel || channel == null) return { type: ParseMentionEnum.ERROR, @@ -195,8 +195,8 @@ export class CommandHandler { if (this.ROLES_PATTERN.test(data)) { const extract = this.ROLES_PATTERN.exec(data); const role = this.message - ? await this.message.guild?.roles.fetch(extract![1]) - : await this.interaction?.guild?.roles.fetch(extract![1]); + ? await this.message.guild?.roles.fetch(extract![1]).catch(() => undefined) + : await this.interaction?.guild?.roles.fetch(extract![1]).catch(() => undefined); if (!role || role == null) return { type: ParseMentionEnum.ERROR, diff --git a/src/web/player.ts b/src/web/player.ts index e5f538c2..cfff74b8 100644 --- a/src/web/player.ts +++ b/src/web/player.ts @@ -9,6 +9,7 @@ import { getCurrentPaused } from "./route/getCurrentPaused.js"; import { getCurrentPosition } from "./route/getCurrentPosition.js"; import { PatchControl } from "./route/patchControl.js"; import { deletePlayer } from "./route/deletePlayer.js"; +import { PostCreatePlayer } from "./route/postCreatePlayer.js"; export class PlayerRoute { constructor(protected client: Manager) {} @@ -17,6 +18,7 @@ export class PlayerRoute { fastify.get("/:guildId", (req, res) => getStatus(this.client, req, res)); fastify.patch("/:guildId", (req, res) => new PatchControl(this.client).main(req, res)); fastify.delete("/:guildId", (req, res) => deletePlayer(this.client, req, res)); + fastify.post("/", (req, res) => new PostCreatePlayer(this.client).main(req, res)); fastify.get("/:guildId/loop", (req, res) => getCurrentLoop(this.client, req, res)); fastify.get("/:guildId/pause", (req, res) => getCurrentPaused(this.client, req, res)); fastify.get("/:guildId/position", (req, res) => getCurrentPosition(this.client, req, res)); diff --git a/src/web/route/deletePlayer.ts b/src/web/route/deletePlayer.ts index 90c2c386..1e023390 100644 --- a/src/web/route/deletePlayer.ts +++ b/src/web/route/deletePlayer.ts @@ -1,8 +1,9 @@ +import util from 'node:util'; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function deletePlayer(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentLoop.ts b/src/web/route/getCurrentLoop.ts index 3c6fc860..2e56fc51 100644 --- a/src/web/route/getCurrentLoop.ts +++ b/src/web/route/getCurrentLoop.ts @@ -1,8 +1,9 @@ +import util from 'node:util'; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentLoop(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentPaused.ts b/src/web/route/getCurrentPaused.ts index 3da851a4..371e5b98 100644 --- a/src/web/route/getCurrentPaused.ts +++ b/src/web/route/getCurrentPaused.ts @@ -1,8 +1,9 @@ +import util from 'node:util'; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentPaused(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentPosition.ts b/src/web/route/getCurrentPosition.ts index 6fa34c8f..6b65d753 100644 --- a/src/web/route/getCurrentPosition.ts +++ b/src/web/route/getCurrentPosition.ts @@ -1,8 +1,9 @@ +import util from 'node:util'; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentPosition(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentTrackStatus.ts b/src/web/route/getCurrentTrackStatus.ts index 77d50a44..f8e08e88 100644 --- a/src/web/route/getCurrentTrackStatus.ts +++ b/src/web/route/getCurrentTrackStatus.ts @@ -1,9 +1,10 @@ +import util from 'node:util'; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentTrackStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getMemberStatus.ts b/src/web/route/getMemberStatus.ts index b9f3799d..3116dfa1 100644 --- a/src/web/route/getMemberStatus.ts +++ b/src/web/route/getMemberStatus.ts @@ -1,20 +1,26 @@ +import util from 'node:util'; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getMemberStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); let isMemeberInVoice = false; const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } const userId = req.headers["user-id"] as string; - const Guild = await client.guilds.fetch(guildId); - const Member = await Guild.members.fetch(userId); - if (!(!Member.voice.channel || !Member.voice)) isMemeberInVoice = true; + const Guild = await client.guilds.fetch(guildId).catch(() => undefined); + if (!Guild) { + res.code(400); + res.send({ error: "Guild not found" }); + return; + } + const Member = await Guild.members.fetch(userId).catch(() => undefined); + if (!(!Member || !Member.voice.channel || !Member.voice)) isMemeberInVoice = true; res.send({ status: isMemeberInVoice }); return; } diff --git a/src/web/route/getQueueStatus.ts b/src/web/route/getQueueStatus.ts index 4938848a..3356d17a 100644 --- a/src/web/route/getQueueStatus.ts +++ b/src/web/route/getQueueStatus.ts @@ -1,9 +1,10 @@ +import util from 'node:util'; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getQueueStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { @@ -14,7 +15,6 @@ export async function getQueueStatus(client: Manager, req: Fastify.FastifyReques return player.queue.map((track) => { const requesterQueue = track.requester as User; return { - encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts index 2ca73347..ab311063 100644 --- a/src/web/route/getSearch.ts +++ b/src/web/route/getSearch.ts @@ -1,10 +1,11 @@ +import util from 'node:util'; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; import { RainlinkSearchResultType } from "../../rainlink/main.js"; export async function getSearch(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} query=${req.query ? util.inspect(req.query) : "{}"}`); const query = (req.query as Record)["identifier"]; const requester = (req.query as Record)["requester"]; const source = (req.query as Record)["source"]; @@ -13,7 +14,7 @@ export async function getSearch(client: Manager, req: Fastify.FastifyRequest, re const isSourceExist = client.rainlink.searchEngines.get(source); if (isSourceExist) validSource = source; } - const user = await client.users.fetch(requester).catch(() => {}); + const user = await client.users.fetch(requester).catch(() => undefined); if (!query) { res.code(404); res.send({ error: "Search param not found" }); @@ -35,7 +36,6 @@ export async function getSearch(client: Manager, req: Fastify.FastifyRequest, re tracks: result.tracks.map((track) => { const requesterQueue = track.requester as User; return { - encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/getStatus.ts b/src/web/route/getStatus.ts index 10816565..2da3b098 100644 --- a/src/web/route/getStatus.ts +++ b/src/web/route/getStatus.ts @@ -1,9 +1,10 @@ +import util from 'node:util'; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); let isMemeberInVoice = "notGiven"; const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); @@ -15,8 +16,8 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re if (req.headers["user-id"]) { const userId = req.headers["user-id"] as string; const Guild = await client.guilds.fetch(guildId); - const Member = await Guild.members.fetch(userId); - if (!Member.voice.channel || !Member.voice) isMemeberInVoice = "false"; + const Member = await Guild.members.fetch(userId).catch(() => undefined); + if (!Member || !Member.voice.channel || !Member.voice) isMemeberInVoice = "false"; else isMemeberInVoice = "true"; } @@ -31,7 +32,6 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re position: player.position, current: song ? { - encoded: song.encoded, title: song.title, uri: song.uri, length: song.duration, @@ -50,7 +50,6 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re queue: player.queue.map((track) => { const requesterQueue = track.requester as User; return { - encoded: track.encoded, title: track.title, uri: track.uri, length: track.duration, diff --git a/src/web/route/patchControl.ts b/src/web/route/patchControl.ts index 7e4ca8c3..ccaa27db 100644 --- a/src/web/route/patchControl.ts +++ b/src/web/route/patchControl.ts @@ -15,7 +15,7 @@ export type TrackRes = { export class PatchControl { protected skiped: boolean = false; protected isPrevious: boolean = false; - protected addedTrack: null | TrackRes = null; + protected addedTrack: TrackRes[] = []; constructor(protected client: Manager) {} async main(req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { @@ -92,30 +92,31 @@ export class PatchControl { return true; } - async add(res: Fastify.FastifyReply, player: RainlinkPlayer, uri: string) { - if (!uri) return true; - console.log(uri); - if (!this.isValidHttpUrl(uri)) { - res.code(400); - res.send({ error: `add property must have a link!` }); - return false; - } - const result = await this.client.rainlink.search(uri); - if (result.tracks.length == 0) { - res.code(400); - res.send({ error: `Track not found!` }); - return false; + async add(res: Fastify.FastifyReply, player: RainlinkPlayer, uriArray: string) { + if (!uriArray) return true; + for (const uri of uriArray) { + if (!this.isValidHttpUrl(uri)) { + res.code(400); + res.send({ error: `add property must have a link!` }); + return false; + } + const result = await this.client.rainlink.search(uri); + if (result.tracks.length == 0) { + res.code(400); + res.send({ error: `Track not found!` }); + return false; + } + const song = result.tracks[0]; + player.queue.add(song); + this.addedTrack.push({ + title: song.title, + uri: song.uri || "", + length: song.duration, + thumbnail: song.artworkUrl || "", + author: song.author, + requester: null, + }); } - const song = result.tracks[0]; - player.queue.add(song); - this.addedTrack = { - title: song.title, - uri: song.uri || "", - length: song.duration, - thumbnail: song.artworkUrl || "", - author: song.author, - requester: null, - }; return true; } @@ -151,7 +152,7 @@ export class PatchControl { resetData() { this.skiped = false; - this.addedTrack = null; + this.addedTrack = []; this.isPrevious = false; } diff --git a/src/web/route/postCreatePlayer.ts b/src/web/route/postCreatePlayer.ts new file mode 100644 index 00000000..e2832372 --- /dev/null +++ b/src/web/route/postCreatePlayer.ts @@ -0,0 +1,58 @@ +import util from 'node:util'; +import { Guild, GuildMember } from "discord.js"; +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + + +export class PostCreatePlayer { + guild: Guild | null = null; + member: GuildMember | null = null; + constructor(protected client: Manager) {} + + async main(req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + this.client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} payload=${req.body ? util.inspect(req.body) : "{}"}`); + const data = (req.body as Record) + const validBody = await this.checker(data, req, res) + if (!validBody) return + const playerData = { + guildId: this.guild!.id, + voiceId: this.member!.voice.channel!.id, + textId: "", + shardId: this.guild?.shardId ?? 0, + deaf: true, + volume: this.client.config.lavalink.DEFAULT_VOLUME ?? 100, + } + this.client.rainlink.create(playerData); + res.send(playerData) + } + + clean() { + this.guild = null; + this.member = null; + } + + async checker(data: Record, req: Fastify.FastifyRequest, res: Fastify.FastifyReply): Promise { + const reqKey = ["guildId", "userId"] + if (!data) return this.errorRes(req, res, "Missing body") + if (Object.keys(data).length !== reqKey.length) return this.errorRes(req, res, "Missing key") + if (!data["guildId"]) return this.errorRes(req, res, "Missing guildId key") + if (!data["userId"]) return this.errorRes(req, res, "Missing userId key") + const Guild = await this.client.guilds.fetch(data["guildId"]).catch(() => undefined); + if (!Guild) return this.errorRes(req, res, "Guild not found") + const isPlayerExist = this.client.rainlink.players.get(Guild.id) + if (isPlayerExist) return this.errorRes(req, res, "Player existed in this guild") + this.guild = Guild + const Member = await Guild.members.fetch(data["userId"]).catch(() => undefined); + if (!Member) return this.errorRes(req, res, "User not found") + if (!Member.voice.channel || !Member.voice) return this.errorRes(req, res, "User is not in voice") + this.member = Member + return true + } + + async errorRes(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, message: string) { + res.code(400); + res.send({ error: message }); + this.clean() + return false + } +} diff --git a/src/web/server.ts b/src/web/server.ts index 8e259741..b4ac410f 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -25,6 +25,14 @@ export class WebServer { reply.send(JSON.stringify({ error: "Authorization failed" })); return done(); } + if ( + client.config.features.WEB_SERVER.whitelist.length !== 0 && + !client.config.features.WEB_SERVER.whitelist.includes(req.hostname) + ) { + reply.code(401); + reply.send(JSON.stringify({ error: "You're not in whitelist" })); + return done(); + } done(); }); fastify.register(WebsocketPlugin); From 3c3b39dde2ac1b6b1a856200ad169928a8ac3955 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Tue, 7 May 2024 11:47:27 +0700 Subject: [PATCH 11/21] add: /commands route --- src/web/route/getCommands.ts | 16 ++++++++++++++++ src/web/server.ts | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 src/web/route/getCommands.ts diff --git a/src/web/route/getCommands.ts b/src/web/route/getCommands.ts new file mode 100644 index 00000000..2467cc76 --- /dev/null +++ b/src/web/route/getCommands.ts @@ -0,0 +1,16 @@ +import { Manager } from "../../manager.js"; +import Fastify from "fastify"; + +export async function getCommands(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { + client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} payload={}`); + res.send({ + data: client.commands.map(command => ({ + name: command.name.join("-"), + description: command.description, + category: command.category, + accessableby: command.accessableby, + usage: command.usage, + aliases: command.aliases + })) + }) +} diff --git a/src/web/server.ts b/src/web/server.ts index b4ac410f..05b4eb8d 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -4,6 +4,7 @@ import WebsocketPlugin from "@fastify/websocket"; import { WebsocketRoute } from "./websocket.js"; import { PlayerRoute } from "./player.js"; import { getSearch } from "./route/getSearch.js"; +import { getCommands } from "./route/getCommands.js"; export class WebServer { app: Fastify.FastifyInstance; @@ -48,6 +49,7 @@ export class WebServer { { prefix: "players" } ); fastify.get("/search", (req, res) => getSearch(client, req, res)); + fastify.get("/commands", (req, res) => getCommands(client, req, res)); done(); }, { prefix: "v1" } From 3667c74b6b6d5722e495c8d715b55610bf93ea44 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Tue, 7 May 2024 18:41:37 +0700 Subject: [PATCH 12/21] add: websocket event system --- src/commands/Music/ClearQueue.ts | 9 + src/commands/Music/Insert.ts | 25 ++ src/commands/Music/Loop.ts | 10 + src/commands/Music/Remove.ts | 25 ++ src/commands/Music/Shuffle.ts | 29 +- src/commands/Music/Volume.ts | 10 + src/events/player/playerCreate.ts | 1 + src/events/player/playerDestroy.ts | 1 + src/events/player/playerException.ts | 2 +- src/events/player/playerPause.ts | 2 + src/events/player/playerResume.ts | 2 + src/events/player/playerUpdate.ts | 4 +- src/events/player/queueEmpty.ts | 11 +- src/events/track/trackEnd.ts | 18 +- src/events/track/trackResolveError.ts | 14 +- src/events/track/trackStart.ts | 2 + src/events/track/trackStuck.ts | 11 +- src/events/websocket/memberJoin.ts | 18 + src/events/websocket/memberLeave.ts | 17 + src/events/websocket/playerCreate.ts | 9 + src/events/websocket/playerDestroy.ts | 9 + src/events/websocket/playerPause.ts | 13 + src/events/websocket/playerResume.ts | 13 + src/events/websocket/playerUpdate.ts | 15 + src/events/websocket/trackEnd.ts | 31 ++ src/events/websocket/trackStart.ts | 26 ++ src/manager.ts | 4 +- src/rainlink/Plugin/Apple/Plugin.ts | 479 ++++++++++++------------- src/rainlink/Plugin/Deezer/Plugin.ts | 384 ++++++++++---------- src/rainlink/Plugin/Nico/Plugin.ts | 305 ++++++++-------- src/rainlink/Plugin/Spotify/Plugin.ts | 450 ++++++++++++----------- src/web/route/deletePlayer.ts | 7 +- src/web/route/getCommands.ts | 8 +- src/web/route/getCurrentLoop.ts | 7 +- src/web/route/getCurrentPaused.ts | 7 +- src/web/route/getCurrentPosition.ts | 7 +- src/web/route/getCurrentTrackStatus.ts | 8 +- src/web/route/getMemberStatus.ts | 7 +- src/web/route/getQueueStatus.ts | 43 ++- src/web/route/getSearch.ts | 7 +- src/web/route/getStatus.ts | 7 +- src/web/route/postCreatePlayer.ts | 56 +-- src/web/websocket.ts | 6 +- 43 files changed, 1187 insertions(+), 932 deletions(-) create mode 100644 src/events/websocket/memberJoin.ts create mode 100644 src/events/websocket/memberLeave.ts create mode 100644 src/events/websocket/playerCreate.ts create mode 100644 src/events/websocket/playerDestroy.ts create mode 100644 src/events/websocket/playerPause.ts create mode 100644 src/events/websocket/playerResume.ts create mode 100644 src/events/websocket/playerUpdate.ts create mode 100644 src/events/websocket/trackEnd.ts create mode 100644 src/events/websocket/trackStart.ts diff --git a/src/commands/Music/ClearQueue.ts b/src/commands/Music/ClearQueue.ts index 87320a26..52ebeddd 100644 --- a/src/commands/Music/ClearQueue.ts +++ b/src/commands/Music/ClearQueue.ts @@ -29,5 +29,14 @@ export default class implements Command { .setDescription(`${client.getString(handler.language, "command.music", "clearqueue_msg")}`) .setColor(client.color); await handler.editReply({ content: " ", embeds: [cleared] }); + + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerClearQueue", + guild: handler.guild!.id, + }) + ) + : true; } } diff --git a/src/commands/Music/Insert.ts b/src/commands/Music/Insert.ts index 7feab4f4..2ac7c765 100644 --- a/src/commands/Music/Insert.ts +++ b/src/commands/Music/Insert.ts @@ -92,6 +92,31 @@ export default class implements Command { ) .setColor(client.color); + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerQueueInsert", + guild: handler.guild!.id, + track: { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: track.requester + ? { + id: (track.requester as any).id, + username: (track.requester as any).username, + globalName: (track.requester as any).globalName, + defaultAvatarURL: (track.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: position - 1, + }) + ) + : true; + return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Loop.ts b/src/commands/Music/Loop.ts index 6037d335..1af7b36f 100644 --- a/src/commands/Music/Loop.ts +++ b/src/commands/Music/Loop.ts @@ -100,6 +100,16 @@ export default class implements Command { .setColor(client.color); handler.editReply({ content: " ", embeds: [looped] }); } + + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: handler.guild!.id, + mode: mode, + }) + ) + : true; } async setLoop247(client: Manager, player: RainlinkPlayer, loop: string) { diff --git a/src/commands/Music/Remove.ts b/src/commands/Music/Remove.ts index 0be978ce..a3e4e2b4 100644 --- a/src/commands/Music/Remove.ts +++ b/src/commands/Music/Remove.ts @@ -73,6 +73,31 @@ export default class implements Command { ) .setColor(client.color); + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerQueueRemove", + guild: handler.guild!.id, + track: { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: song.requester + ? { + id: (song.requester as any).id, + username: (song.requester as any).username, + globalName: (song.requester as any).globalName, + defaultAvatarURL: (song.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: Number(tracks) - 1, + }) + ) + : true; + return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Shuffle.ts b/src/commands/Music/Shuffle.ts index 5eb4b9cf..9a0dcd54 100644 --- a/src/commands/Music/Shuffle.ts +++ b/src/commands/Music/Shuffle.ts @@ -1,4 +1,4 @@ -import { EmbedBuilder } from "discord.js"; +import { EmbedBuilder, User } from "discord.js"; import { Manager } from "../../manager.js"; import { Accessableby, Command } from "../../structures/Command.js"; import { CommandHandler } from "../../structures/CommandHandler.js"; @@ -75,6 +75,33 @@ export default class implements Command { pages.push(embed); } + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerQueueShuffle", + guild: handler.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }) + ) + : true; + if (pages.length == pagesNum && newQueue.length > 10) { if (handler.message) { await new PageQueue(client, pages, 60000, newQueue.length, handler.language).prefixPage( diff --git a/src/commands/Music/Volume.ts b/src/commands/Music/Volume.ts index 87e2b4f6..6e9c0395 100644 --- a/src/commands/Music/Volume.ts +++ b/src/commands/Music/Volume.ts @@ -60,6 +60,16 @@ export default class implements Command { await player.setVolume(Number(value)); + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerVolume", + guild: handler.guild!.id, + volume: player.volume, + }) + ) + : true; + const changevol = new EmbedBuilder() .setDescription( `${client.getString(handler.language, "command.music", "volume_msg", { diff --git a/src/events/player/playerCreate.ts b/src/events/player/playerCreate.ts index cbd3af9f..cf9cd5b9 100644 --- a/src/events/player/playerCreate.ts +++ b/src/events/player/playerCreate.ts @@ -5,5 +5,6 @@ export default class { async execute(client: Manager, player: RainlinkPlayer) { const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); client.logger.info(import.meta.url, `Player Created in @ ${guild?.name} / ${player.guildId}`); + client.emit("playerCreate", player); } } diff --git a/src/events/player/playerDestroy.ts b/src/events/player/playerDestroy.ts index d9b176a1..12f57e2f 100644 --- a/src/events/player/playerDestroy.ts +++ b/src/events/player/playerDestroy.ts @@ -19,6 +19,7 @@ export default class { await client.UpdateMusic(player); /////////// Update Music Setup /////////// + client.emit("playerDestroy", player); const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; client.sentQueue.set(player.guildId, false); let data = await new AutoReconnectBuilderService(client, player).get(player.guildId); diff --git a/src/events/player/playerException.ts b/src/events/player/playerException.ts index fe02ec96..0a7ab52d 100644 --- a/src/events/player/playerException.ts +++ b/src/events/player/playerException.ts @@ -26,7 +26,7 @@ export default class { const data247 = await new AutoReconnectBuilderService(client, player).get(player.guildId); const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (data247 !== null && data247 && data247.twentyfourseven && channel) - return new ClearMessageService(client, channel, player); + new ClearMessageService(client, channel, player); const currentPlayer = client.rainlink.players.get(player.guildId) as RainlinkPlayer; if (!currentPlayer) return; diff --git a/src/events/player/playerPause.ts b/src/events/player/playerPause.ts index e87a886f..8b0bb5f4 100644 --- a/src/events/player/playerPause.ts +++ b/src/events/player/playerPause.ts @@ -14,6 +14,8 @@ export default class { const setup = await client.db.setup.get(`${player.guildId}`); + client.emit("playerPause", player); + if (setup && setup.playmsg) { const channel = await client.channels.fetch(setup.channel).catch(() => undefined); if (!channel) return; diff --git a/src/events/player/playerResume.ts b/src/events/player/playerResume.ts index 7cb3e8f7..68a0fef3 100644 --- a/src/events/player/playerResume.ts +++ b/src/events/player/playerResume.ts @@ -14,6 +14,8 @@ export default class { const setup = await client.db.setup.get(`${player.guildId}`); + client.emit("playerResume", player); + if (setup && setup.playmsg) { const channel = await client.channels.fetch(setup.channel).catch(() => undefined); if (!channel) return; diff --git a/src/events/player/playerUpdate.ts b/src/events/player/playerUpdate.ts index 94c8056a..cc203f37 100644 --- a/src/events/player/playerUpdate.ts +++ b/src/events/player/playerUpdate.ts @@ -2,5 +2,7 @@ import { Manager } from "../../manager.js"; import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { - async execute(client: Manager, player: RainlinkPlayer, data: unknown) {} + async execute(client: Manager, player: RainlinkPlayer, data: unknown) { + client.emit("playerUpdate", player); + } } diff --git a/src/events/player/queueEmpty.ts b/src/events/player/queueEmpty.ts index 2b95662e..b0464998 100644 --- a/src/events/player/queueEmpty.ts +++ b/src/events/player/queueEmpty.ts @@ -23,8 +23,6 @@ export default class { const identifier = player.data.get("identifier"); const search = `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`; let res = await player.search(search, { requester: requester }); - // console.log(player.queue.previous) - // console.log(player.queue.current) const finalRes = res.tracks.filter((track) => { const req1 = !player.queue.some((s) => s.encoded === track.encoded); const req2 = !player.queue.previous.some((s) => s.encoded === track.encoded); @@ -43,13 +41,8 @@ export default class { const data = await new AutoReconnectBuilderService(client, player).get(player.guildId); const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; - if (data !== null && data && data.twentyfourseven && channel) - return new ClearMessageService(client, channel, player); + if (data !== null && data && data.twentyfourseven && channel) new ClearMessageService(client, channel, player); - const currentPlayer = client.rainlink.players.get(player.guildId) as RainlinkPlayer; - if (!currentPlayer) return; - if (currentPlayer.voiceId !== null) { - await player.destroy(); - } + await player.destroy(); } } diff --git a/src/events/track/trackEnd.ts b/src/events/track/trackEnd.ts index 72016122..229e8496 100644 --- a/src/events/track/trackEnd.ts +++ b/src/events/track/trackEnd.ts @@ -18,20 +18,18 @@ export default class { /////////// Update Music Setup ////////// await client.UpdateMusic(player); /////////// Update Music Setup /////////// - let data = await new AutoReconnectBuilderService(client, player).get(player.guildId); - const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; - if (!channel) return; - if (data && data.twentyfourseven) return; + client.emit("playerEnd", player); - if (player.queue.length || player!.queue!.current) return new ClearMessageService(client, channel, player); + let data = await new AutoReconnectBuilderService(client, player).get(player.guildId); + const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; + if (channel) { + if (data && data.twentyfourseven) return; - if (player.loop !== "none") return new ClearMessageService(client, channel, player); + if (player.queue.length || player!.queue!.current) return new ClearMessageService(client, channel, player); - const currentPlayer = client.rainlink.players.get(player.guildId); - if (!currentPlayer) return; - if (currentPlayer.voiceId !== null) { - await player.destroy(); + if (player.loop !== "none") return new ClearMessageService(client, channel, player); } + await player.destroy(); } } diff --git a/src/events/track/trackResolveError.ts b/src/events/track/trackResolveError.ts index 6589cad8..ff9ba999 100644 --- a/src/events/track/trackResolveError.ts +++ b/src/events/track/trackResolveError.ts @@ -21,11 +21,10 @@ export default class { /////////// Update Music Setup /////////// const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; - if (!channel) return; - let guildModel = await client.db.language.get(`${channel.guild.id}`); + let guildModel = await client.db.language.get(`${player.guildId}`); if (!guildModel) { - guildModel = await client.db.language.set(`${channel.guild.id}`, client.config.bot.LANGUAGE); + guildModel = await client.db.language.set(`${player.guildId}`, client.config.bot.LANGUAGE); } const language = guildModel; @@ -46,12 +45,9 @@ export default class { client.logger.error(import.meta.url, `Track Error in ${guild!.name} / ${player.guildId}.`); const data247 = await new AutoReconnectBuilderService(client, player).get(player.guildId); - if (data247 !== null && data247 && data247.twentyfourseven) return new ClearMessageService(client, channel, player); + if (data247 !== null && data247 && data247.twentyfourseven && channel) + new ClearMessageService(client, channel, player); - const currentPlayer = client.rainlink.players.get(player.guildId); - if (!currentPlayer) return; - if (currentPlayer.voiceId !== null) { - await player.destroy(); - } + await player.destroy(); } } diff --git a/src/events/track/trackStart.ts b/src/events/track/trackStart.ts index 539b1e38..624512f8 100644 --- a/src/events/track/trackStart.ts +++ b/src/events/track/trackStart.ts @@ -32,6 +32,8 @@ export default class { const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (!channel) return; + client.emit("trackStart", player); + const autoreconnect = new AutoReconnectBuilderService(client, player); if (await autoreconnect.get(player.guildId)) { diff --git a/src/events/track/trackStuck.ts b/src/events/track/trackStuck.ts index f1b290eb..bfa56f67 100644 --- a/src/events/track/trackStuck.ts +++ b/src/events/track/trackStuck.ts @@ -19,7 +19,7 @@ export default class { const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; - if (!channel) return; + if (!channel) return player.destroy(); let guildModel = await client.db.language.get(`${channel.guild.id}`); if (!guildModel) { @@ -45,12 +45,7 @@ export default class { const data247 = await new AutoReconnectBuilderService(client, player).get(player.guildId); if (data247 !== null && data247 && data247.twentyfourseven && channel) - return new ClearMessageService(client, channel, player); - - const currentPlayer = client.rainlink.players.get(player.guildId); - if (!currentPlayer) return; - if (currentPlayer.voiceId !== null) { - await player.destroy(); - } + new ClearMessageService(client, channel, player); + await player.destroy(); } } diff --git a/src/events/websocket/memberJoin.ts b/src/events/websocket/memberJoin.ts new file mode 100644 index 00000000..6741f528 --- /dev/null +++ b/src/events/websocket/memberJoin.ts @@ -0,0 +1,18 @@ +import { VoiceState } from "discord.js"; +import { Manager } from "../../manager.js"; + +export default class { + async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { + if (!client.websocket) return; + + if (oldState.channel === null && oldState.id !== client.user!.id) { + client.websocket.send( + JSON.stringify({ + op: "memberJoin", + guild: newState.guild.id, + userId: newState.member?.id, + }) + ); + } + } +} diff --git a/src/events/websocket/memberLeave.ts b/src/events/websocket/memberLeave.ts new file mode 100644 index 00000000..5226e8e9 --- /dev/null +++ b/src/events/websocket/memberLeave.ts @@ -0,0 +1,17 @@ +import { VoiceState } from "discord.js"; +import { Manager } from "../../manager.js"; + +export default class { + async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { + if (!client.websocket) return; + + if (newState.channel === null && newState.id !== client.user!.id) + client.websocket.send( + JSON.stringify({ + op: "memberLeave", + guild: oldState.guild.id, + userId: oldState.member?.id, + }) + ); + } +} diff --git a/src/events/websocket/playerCreate.ts b/src/events/websocket/playerCreate.ts new file mode 100644 index 00000000..2ebe9043 --- /dev/null +++ b/src/events/websocket/playerCreate.ts @@ -0,0 +1,9 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + client.websocket.send(JSON.stringify({ op: "playerCreate", guild: player.guildId })); + } +} diff --git a/src/events/websocket/playerDestroy.ts b/src/events/websocket/playerDestroy.ts new file mode 100644 index 00000000..0c4ac6fa --- /dev/null +++ b/src/events/websocket/playerDestroy.ts @@ -0,0 +1,9 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + client.websocket.send(JSON.stringify({ op: "playerDestroy", guild: player.guildId })); + } +} diff --git a/src/events/websocket/playerPause.ts b/src/events/websocket/playerPause.ts new file mode 100644 index 00000000..93a1812f --- /dev/null +++ b/src/events/websocket/playerPause.ts @@ -0,0 +1,13 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + const data = JSON.stringify({ + op: "playerPause", + guild: player.guildId, + }); + client.websocket.send(data); + } +} diff --git a/src/events/websocket/playerResume.ts b/src/events/websocket/playerResume.ts new file mode 100644 index 00000000..c0ff3e13 --- /dev/null +++ b/src/events/websocket/playerResume.ts @@ -0,0 +1,13 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + const data = JSON.stringify({ + op: "playerResume", + guild: player.guildId, + }); + client.websocket.send(data); + } +} diff --git a/src/events/websocket/playerUpdate.ts b/src/events/websocket/playerUpdate.ts new file mode 100644 index 00000000..e27bf18f --- /dev/null +++ b/src/events/websocket/playerUpdate.ts @@ -0,0 +1,15 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + client.websocket.send( + JSON.stringify({ + op: "playerUpdate", + guild: player.guildId, + position: player.position, + }) + ); + } +} diff --git a/src/events/websocket/trackEnd.ts b/src/events/websocket/trackEnd.ts new file mode 100644 index 00000000..77238077 --- /dev/null +++ b/src/events/websocket/trackEnd.ts @@ -0,0 +1,31 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + + const prevoiusIndex = player.queue.previous.length - 1; + + const song = player.queue.previous[prevoiusIndex === -1 ? 0 : prevoiusIndex]; + + const currentData = song + ? { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: song.requester, + } + : null; + + client.websocket.send( + JSON.stringify({ + op: "playerEnd", + guild: player.guildId, + data: currentData, + }) + ); + } +} diff --git a/src/events/websocket/trackStart.ts b/src/events/websocket/trackStart.ts new file mode 100644 index 00000000..5cd0e252 --- /dev/null +++ b/src/events/websocket/trackStart.ts @@ -0,0 +1,26 @@ +import { Manager } from "../../manager.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; + +export default class { + async execute(client: Manager, player: RainlinkPlayer) { + if (!client.websocket) return; + const song = player.queue.current; + + const currentData = { + title: song!.title, + uri: song!.uri, + length: song!.duration, + thumbnail: song!.artworkUrl, + author: song!.author, + requester: song!.requester, + }; + + client.websocket.send( + JSON.stringify({ + op: "trackStart", + guild: player.guildId, + data: currentData, + }) + ); + } +} diff --git a/src/manager.ts b/src/manager.ts index 97e65b94..7be1a772 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -34,8 +34,8 @@ import { PlayerButton } from "./@types/Button.js"; import { GlobalMsg } from "./structures/CommandHandler.js"; import { RainlinkPlayer } from "./rainlink/main.js"; import { IconType } from "./@types/Emoji.js"; -import { WebSocket } from "ws"; import { TopggService } from "./services/TopggService.js"; +import { WebSocket } from "@fastify/websocket"; config(); const __dirname = dirname(fileURLToPath(import.meta.url)); const configData = new ConfigDataService().data; @@ -75,7 +75,6 @@ export class Manager extends Client { plButton: Collection; leaveDelay: Collection; nowPlaying: Collection; - wsId: Collection; websocket?: WebSocket; UpdateMusic!: (player: RainlinkPlayer) => Promise>; UpdateQueueMsg!: (player: RainlinkPlayer) => Promise>; @@ -140,7 +139,6 @@ export class Manager extends Client { this.plButton = new Collection(); this.leaveDelay = new Collection(); this.nowPlaying = new Collection(); - this.wsId = new Collection(); this.isDatabaseConnected = false; // Sharing diff --git a/src/rainlink/Plugin/Apple/Plugin.ts b/src/rainlink/Plugin/Apple/Plugin.ts index a35f91a7..d97c913b 100644 --- a/src/rainlink/Plugin/Apple/Plugin.ts +++ b/src/rainlink/Plugin/Apple/Plugin.ts @@ -31,274 +31,271 @@ const credentials = { }; export class RainlinkPlugin extends SourceRainlinkPlugin { - public options: AppleOptions; - private manager: Rainlink | null; - private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; - private readonly methods: Record Promise>; - private credentials: HeaderType; - private fetchURL: string; - private baseURL: string; - public countryCode: string; - public imageWidth: number; - public imageHeight: number; - - /** + public options: AppleOptions; + private manager: Rainlink | null; + private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; + private readonly methods: Record Promise>; + private credentials: HeaderType; + private fetchURL: string; + private baseURL: string; + public countryCode: string; + public imageWidth: number; + public imageHeight: number; + + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return 'am'; - } + public sourceIdentify(): string { + return "am"; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return 'apple'; - } + public sourceName(): string { + return "apple"; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** Name function for getting plugin name */ - public name(): string { - return 'rainlink-apple'; - } + /** Name function for getting plugin name */ + public name(): string { + return "rainlink-apple"; + } - /** + /** * Initialize the plugin. * @param appleOptions The rainlink apple plugin options */ - constructor(appleOptions: AppleOptions) { - super(); - this.methods = { - artist: this.getArtist.bind(this), - album: this.getAlbum.bind(this), - playlist: this.getPlaylist.bind(this), - track: this.getTrack.bind(this), - }; - this.options = appleOptions; - this.manager = null; - this._search = undefined; - this.countryCode = this.options?.countryCode ? this.options?.countryCode : 'us'; - this.imageHeight = this.options?.imageHeight ? this.options?.imageHeight : 900; - this.imageWidth = this.options?.imageWidth ? this.options?.imageWidth : 600; - this.baseURL = 'https://api.music.apple.com/v1/'; - this.fetchURL = `https://amp-api.music.apple.com/v1/catalog/${this.countryCode}`; - this.credentials = { - Authorization: `Bearer ${credentials.APPLE_TOKEN}`, - origin: 'https://music.apple.com', - }; - } - - /** + constructor(appleOptions: AppleOptions) { + super(); + this.methods = { + artist: this.getArtist.bind(this), + album: this.getAlbum.bind(this), + playlist: this.getPlaylist.bind(this), + track: this.getTrack.bind(this), + }; + this.options = appleOptions; + this.manager = null; + this._search = undefined; + this.countryCode = this.options?.countryCode ? this.options?.countryCode : "us"; + this.imageHeight = this.options?.imageHeight ? this.options?.imageHeight : 900; + this.imageWidth = this.options?.imageWidth ? this.options?.imageWidth : 600; + this.baseURL = "https://api.music.apple.com/v1/"; + this.fetchURL = `https://amp-api.music.apple.com/v1/catalog/${this.countryCode}`; + this.credentials = { + Authorization: `Bearer ${credentials.APPLE_TOKEN}`, + origin: "https://music.apple.com", + }; + } + + /** * load the plugin * @param rainlink The rainlink class */ - public load(manager: Rainlink): void { - this.manager = manager; - this._search = manager.search.bind(manager); - manager.search = this.search.bind(this); - } + public load(manager: Rainlink): void { + this.manager = manager; + this._search = manager.search.bind(manager); + manager.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.manager = rainlink; - this.manager.search = rainlink.search.bind(rainlink); - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.manager = rainlink; + this.manager.search = rainlink.search.bind(rainlink); + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect( - query: string, - options?: RainlinkSearchOptions | undefined, - ): Promise { - let type: string; - let id: string; - let isTrack: boolean = false; - - if (!this.manager || !this._search) throw new Error('rainlink-apple is not loaded yet.'); - - if (!query) throw new Error('Query is required'); - - const isUrl = /^https?:\/\//.test(query); - - if (!REGEX_SONG_ONLY.exec(query) || REGEX_SONG_ONLY.exec(query) == null) { - const extract = REGEX.exec(query) || []; - id = extract![4]; - type = extract![1]; - } else { - const extract = REGEX_SONG_ONLY.exec(query) || []; - id = extract![8]; - type = extract![1]; - isTrack = true; - } - - if (type in this.methods) { - try { - this.debug(`Start search from ${this.sourceName()} plugin`); - let _function = this.methods[type]; - if (isTrack) _function = this.methods.track; - const result: Result = await _function(id, options?.requester); - - const loadType = isTrack ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === 'apple' && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private async getData(params: string) { - const req = await fetch(`${this.fetchURL}${params}`, { - headers: this.credentials, - }); - const res = (await req.json()) as any; - return res.data as D; - } - - private async searchTrack(query: string, requester: unknown): Promise { - try { - const res = await this.getData( - `/search?types=songs&term=${query.replace(/ /g, '+').toLocaleLowerCase()}`, - ).catch(e => { - throw new Error(e); - }); - return { - tracks: res.results.songs.data.map((track: Track) => this.buildRainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getTrack(id: string, requester: unknown): Promise { - try { - const track = await this.getData(`/songs/${id}`).catch(e => { - throw new Error(e); - }); - return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getArtist(id: string, requester: unknown): Promise { - try { - const track = await this.getData(`/artists/${id}/view/top-songs`).catch(e => { - throw new Error(e); - }); - return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getAlbum(id: string, requester: unknown): Promise { - try { - const album = await this.getData(`/albums/${id}`).catch(e => { - throw new Error(e); - }); - - const tracks = album[0].relationships.tracks.data - .filter(this.filterNullOrUndefined) - .map((track: Track) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: album[0].attributes.name }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getPlaylist(id: string, requester: unknown): Promise { - try { - const playlist = await this.getData(`/playlists/${id}`).catch(e => { - throw new Error(e); - }); - - const tracks = playlist[0].relationships.tracks.data - .filter(this.filterNullOrUndefined) - .map((track: any) => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: playlist[0].attributes.name }; - } catch (e: any) { - throw new Error(e); - } - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType, - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.SEARCH, - }; - } - - private buildRainlinkTrack(appleTrack: Track, requester: unknown) { - const artworkURL = String(appleTrack.attributes.artwork.url) - .replace('{w}', String(this.imageWidth)) - .replace('{h}', String(this.imageHeight)); - return new RainlinkTrack( - { - encoded: '', - info: { - sourceName: this.sourceName(), - identifier: appleTrack.id, - isSeekable: true, - author: appleTrack.attributes.artistName ? appleTrack.attributes.artistName : 'Unknown', - length: appleTrack.attributes.durationInMillis, - isStream: false, - position: 0, - title: appleTrack.attributes.name, - uri: appleTrack.attributes.url || '', - artworkUrl: artworkURL ? artworkURL : '', - }, - pluginInfo: { - name: 'rainlink@apple', - }, - }, - requester, - ); - } - - private debug(logs: string) { - this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink] -> [Plugin] -> [Apple] | ${logs}`) : true; - } + public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { + let type: string; + let id: string; + let isTrack: boolean = false; + + if (!this.manager || !this._search) throw new Error("rainlink-apple is not loaded yet."); + + if (!query) throw new Error("Query is required"); + + const isUrl = /^https?:\/\//.test(query); + + if (!REGEX_SONG_ONLY.exec(query) || REGEX_SONG_ONLY.exec(query) == null) { + const extract = REGEX.exec(query) || []; + id = extract![4]; + type = extract![1]; + } else { + const extract = REGEX_SONG_ONLY.exec(query) || []; + id = extract![8]; + type = extract![1]; + isTrack = true; + } + + if (type in this.methods) { + try { + this.debug(`Start search from ${this.sourceName()} plugin`); + let _function = this.methods[type]; + if (isTrack) _function = this.methods.track; + const result: Result = await _function(id, options?.requester); + + const loadType = isTrack ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === "apple" && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private async getData(params: string) { + const req = await fetch(`${this.fetchURL}${params}`, { + headers: this.credentials, + }); + const res = (await req.json()) as any; + return res.data as D; + } + + private async searchTrack(query: string, requester: unknown): Promise { + try { + const res = await this.getData(`/search?types=songs&term=${query.replace(/ /g, "+").toLocaleLowerCase()}`).catch( + (e) => { + throw new Error(e); + } + ); + return { + tracks: res.results.songs.data.map((track: Track) => this.buildRainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getTrack(id: string, requester: unknown): Promise { + try { + const track = await this.getData(`/songs/${id}`).catch((e) => { + throw new Error(e); + }); + return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getArtist(id: string, requester: unknown): Promise { + try { + const track = await this.getData(`/artists/${id}/view/top-songs`).catch((e) => { + throw new Error(e); + }); + return { tracks: [this.buildRainlinkTrack(track[0], requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getAlbum(id: string, requester: unknown): Promise { + try { + const album = await this.getData(`/albums/${id}`).catch((e) => { + throw new Error(e); + }); + + const tracks = album[0].relationships.tracks.data + .filter(this.filterNullOrUndefined) + .map((track: Track) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: album[0].attributes.name }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getPlaylist(id: string, requester: unknown): Promise { + try { + const playlist = await this.getData(`/playlists/${id}`).catch((e) => { + throw new Error(e); + }); + + const tracks = playlist[0].relationships.tracks.data + .filter(this.filterNullOrUndefined) + .map((track: any) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: playlist[0].attributes.name }; + } catch (e: any) { + throw new Error(e); + } + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.SEARCH, + }; + } + + private buildRainlinkTrack(appleTrack: Track, requester: unknown) { + const artworkURL = String(appleTrack.attributes.artwork.url) + .replace("{w}", String(this.imageWidth)) + .replace("{h}", String(this.imageHeight)); + return new RainlinkTrack( + { + encoded: "", + info: { + sourceName: this.sourceName(), + identifier: appleTrack.id, + isSeekable: true, + author: appleTrack.attributes.artistName ? appleTrack.attributes.artistName : "Unknown", + length: appleTrack.attributes.durationInMillis, + isStream: false, + position: 0, + title: appleTrack.attributes.name, + uri: appleTrack.attributes.url || "", + artworkUrl: artworkURL ? artworkURL : "", + }, + pluginInfo: { + name: "rainlink@apple", + }, + }, + requester + ); + } + + private debug(logs: string) { + this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink] -> [Plugin] -> [Apple] | ${logs}`) : true; + } } // Interfaces @@ -338,4 +335,4 @@ export interface TrackAttributes { name: string; previews: any[]; artistName: string; -} \ No newline at end of file +} diff --git a/src/rainlink/Plugin/Deezer/Plugin.ts b/src/rainlink/Plugin/Deezer/Plugin.ts index 5a7bf386..48fbc2b9 100644 --- a/src/rainlink/Plugin/Deezer/Plugin.ts +++ b/src/rainlink/Plugin/Deezer/Plugin.ts @@ -10,225 +10,221 @@ const REGEX = /^https?:\/\/(?:www\.)?deezer\.com\/[a-z]+\/(track|album|playlist) const SHORT_REGEX = /^https:\/\/deezer\.page\.link\/[a-zA-Z0-9]{12}$/; export class RainlinkPlugin extends SourceRainlinkPlugin { - private manager: Rainlink | null; - private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; - private readonly methods: Record Promise>; - /** + private manager: Rainlink | null; + private _search?: (query: string, options?: RainlinkSearchOptions) => Promise; + private readonly methods: Record Promise>; + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return 'dz'; - } + public sourceIdentify(): string { + return "dz"; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return 'deezer'; - } + public sourceName(): string { + return "deezer"; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * Initialize the plugin. */ - constructor() { - super(); - this.methods = { - track: this.getTrack.bind(this), - album: this.getAlbum.bind(this), - playlist: this.getPlaylist.bind(this), - }; - this.manager = null; - this._search = undefined; - } - - /** + constructor() { + super(); + this.methods = { + track: this.getTrack.bind(this), + album: this.getAlbum.bind(this), + playlist: this.getPlaylist.bind(this), + }; + this.manager = null; + this._search = undefined; + } + + /** * load the plugin * @param rainlink The rainlink class */ - public load(manager: Rainlink): void { - this.manager = manager; - this._search = manager.search.bind(manager); - manager.search = this.search.bind(this); - } + public load(manager: Rainlink): void { + this.manager = manager; + this._search = manager.search.bind(manager); + manager.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.manager = rainlink; - this.manager.search = rainlink.search.bind(rainlink); - } - - /** Name function for getting plugin name */ - public name(): string { - return 'rainlink-deezer'; - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.manager = rainlink; + this.manager.search = rainlink.search.bind(rainlink); + } + + /** Name function for getting plugin name */ + public name(): string { + return "rainlink-deezer"; + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect( - query: string, - options?: RainlinkSearchOptions | undefined, - ): Promise { - if (!this.manager || !this._search) throw new Error('rainlink-deezer is not loaded yet.'); - - if (!query) throw new Error('Query is required'); - - const isUrl = /^https?:\/\//.test(query); - - if (SHORT_REGEX.test(query)) { - const url = new URL(query); - const res = await fetch(url.origin + url.pathname, { method: 'HEAD' }); - query = String(res.headers.get('location')); - } - - const [, type, id] = REGEX.exec(query) || []; - - if (type in this.methods) { - this.debug(`Start search from ${this.sourceName()} plugin`); - try { - const _function = this.methods[type]; - const result: Result = await _function(id, options?.requester); - - const loadType = - type === 'track' ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private async searchTrack(query: string, requester: unknown): Promise { - try { - const req = await fetch(`${API_URL}/search/track?q=${decodeURIComponent(query)}`); - const data = await req.json(); - - const res = data as SearchResult; - return { - tracks: res.data.map(track => this.buildRainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getTrack(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/track/${id}/`); - const data = await request.json(); - const track = data as DeezerTrack; - - return { tracks: [this.buildRainlinkTrack(track, requester)] }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getAlbum(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/album/${id}/`); - const data = await request.json(); - const album = data as Album; - - const tracks = album.tracks.data - .filter(this.filterNullOrUndefined) - .map(track => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: album.title }; - } catch (e: any) { - throw new Error(e); - } - } - - private async getPlaylist(id: string, requester: unknown): Promise { - try { - const request = await fetch(`${API_URL}/playlist/${id}`); - const data = await request.json(); - const playlist = data as Playlist; - - const tracks = playlist.tracks.data - .filter(this.filterNullOrUndefined) - .map(track => this.buildRainlinkTrack(track, requester)); - - return { tracks, name: playlist.title }; - } catch (e: any) { - throw new Error(e); - } - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType, - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.SEARCH, - }; - } - - private buildRainlinkTrack(dezzerTrack: any, requester: unknown) { - return new RainlinkTrack( - { - encoded: '', - info: { - sourceName: this.sourceName(), - identifier: dezzerTrack.id, - isSeekable: true, - author: dezzerTrack.artist ? dezzerTrack.artist.name : 'Unknown', - length: dezzerTrack.duration * 1000, - isStream: false, - position: 0, - title: dezzerTrack.title, - uri: `https://www.deezer.com/track/${dezzerTrack.id}`, - artworkUrl: dezzerTrack.album ? dezzerTrack.album.cover : '', - }, - pluginInfo: { - name: 'rainlink@deezer', - }, - }, - requester, - ); - } - - private debug(logs: string) { - this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink Deezer Plugin]: ${logs}`) : true; - } + public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { + if (!this.manager || !this._search) throw new Error("rainlink-deezer is not loaded yet."); + + if (!query) throw new Error("Query is required"); + + const isUrl = /^https?:\/\//.test(query); + + if (SHORT_REGEX.test(query)) { + const url = new URL(query); + const res = await fetch(url.origin + url.pathname, { method: "HEAD" }); + query = String(res.headers.get("location")); + } + + const [, type, id] = REGEX.exec(query) || []; + + if (type in this.methods) { + this.debug(`Start search from ${this.sourceName()} plugin`); + try { + const _function = this.methods[type]; + const result: Result = await _function(id, options?.requester); + + const loadType = type === "track" ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private async searchTrack(query: string, requester: unknown): Promise { + try { + const req = await fetch(`${API_URL}/search/track?q=${decodeURIComponent(query)}`); + const data = await req.json(); + + const res = data as SearchResult; + return { + tracks: res.data.map((track) => this.buildRainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getTrack(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/track/${id}/`); + const data = await request.json(); + const track = data as DeezerTrack; + + return { tracks: [this.buildRainlinkTrack(track, requester)] }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getAlbum(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/album/${id}/`); + const data = await request.json(); + const album = data as Album; + + const tracks = album.tracks.data + .filter(this.filterNullOrUndefined) + .map((track) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: album.title }; + } catch (e: any) { + throw new Error(e); + } + } + + private async getPlaylist(id: string, requester: unknown): Promise { + try { + const request = await fetch(`${API_URL}/playlist/${id}`); + const data = await request.json(); + const playlist = data as Playlist; + + const tracks = playlist.tracks.data + .filter(this.filterNullOrUndefined) + .map((track) => this.buildRainlinkTrack(track, requester)); + + return { tracks, name: playlist.title }; + } catch (e: any) { + throw new Error(e); + } + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.SEARCH, + }; + } + + private buildRainlinkTrack(dezzerTrack: any, requester: unknown) { + return new RainlinkTrack( + { + encoded: "", + info: { + sourceName: this.sourceName(), + identifier: dezzerTrack.id, + isSeekable: true, + author: dezzerTrack.artist ? dezzerTrack.artist.name : "Unknown", + length: dezzerTrack.duration * 1000, + isStream: false, + position: 0, + title: dezzerTrack.title, + uri: `https://www.deezer.com/track/${dezzerTrack.id}`, + artworkUrl: dezzerTrack.album ? dezzerTrack.album.cover : "", + }, + pluginInfo: { + name: "rainlink@deezer", + }, + }, + requester + ); + } + + private debug(logs: string) { + this.manager ? this.manager.emit(RainlinkEvents.Debug, `[Rainlink Deezer Plugin]: ${logs}`) : true; + } } // Interfaces @@ -273,4 +269,4 @@ interface SearchResult { name: string; }; data: RainlinkTrack[]; -} \ No newline at end of file +} diff --git a/src/rainlink/Plugin/Nico/Plugin.ts b/src/rainlink/Plugin/Nico/Plugin.ts index 8cedfb55..7c6998ce 100644 --- a/src/rainlink/Plugin/Nico/Plugin.ts +++ b/src/rainlink/Plugin/Nico/Plugin.ts @@ -1,18 +1,14 @@ -import { RainlinkEvents, RainlinkPluginType } from '../../main.js'; -import { - RainlinkSearchOptions, - RainlinkSearchResult, - RainlinkSearchResultType, -} from '../../main.js'; -import { RainlinkTrack } from '../../main.js'; -import { Rainlink } from '../../main.js'; -import { SourceRainlinkPlugin } from '../../main.js'; -import NicoResolver from './NicoResolver.js'; -import search from './NicoSearch.js'; +import { RainlinkEvents, RainlinkPluginType } from "../../main.js"; +import { RainlinkSearchOptions, RainlinkSearchResult, RainlinkSearchResultType } from "../../main.js"; +import { RainlinkTrack } from "../../main.js"; +import { Rainlink } from "../../main.js"; +import { SourceRainlinkPlugin } from "../../main.js"; +import NicoResolver from "./NicoResolver.js"; +import search from "./NicoSearch.js"; const REGEX = RegExp( - // https://github.com/ytdl-org/youtube-dl/blob/a8035827177d6b59aca03bd717acb6a9bdd75ada/youtube_dl/extractor/niconico.py#L162 - 'https?://(?:www\\.|secure\\.|sp\\.)?nicovideo\\.jp/watch/(?(?:[a-z]{2})?[0-9]+)', + // https://github.com/ytdl-org/youtube-dl/blob/a8035827177d6b59aca03bd717acb6a9bdd75ada/youtube_dl/extractor/niconico.py#L162 + "https?://(?:www\\.|secure\\.|sp\\.)?nicovideo\\.jp/watch/(?(?:[a-z]{2})?[0-9]+)" ); /** The rainlink nicovideo plugin options */ @@ -22,200 +18,195 @@ export interface NicoOptions { } export class RainlinkPlugin extends SourceRainlinkPlugin { - /** + /** * The options of the plugin. */ - public options: NicoOptions; - private _search: - | ((query: string, options?: RainlinkSearchOptions) => Promise) - | undefined; - private rainlink: Rainlink | null; + public options: NicoOptions; + private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | undefined; + private rainlink: Rainlink | null; - private readonly methods: Record Promise>; + private readonly methods: Record Promise>; - /** + /** * Initialize the plugin. * @param nicoOptions Options for run plugin */ - constructor(nicoOptions: NicoOptions) { - super(); - this.options = nicoOptions; - this.methods = { - track: this.getTrack.bind(this), - }; - this.rainlink = null; - } + constructor(nicoOptions: NicoOptions) { + super(); + this.options = nicoOptions; + this.methods = { + track: this.getTrack.bind(this), + }; + this.rainlink = null; + } - /** + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return 'nv'; - } + public sourceIdentify(): string { + return "nv"; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return 'nicovideo'; - } + public sourceName(): string { + return "nicovideo"; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * load the plugin * @param rainlink The rainlink class */ - public load(rainlink: Rainlink) { - this.rainlink = rainlink; - this._search = rainlink.search.bind(rainlink); - rainlink.search = this.search.bind(this); - } + public load(rainlink: Rainlink) { + this.rainlink = rainlink; + this._search = rainlink.search.bind(rainlink); + rainlink.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.rainlink = rainlink; - rainlink.search = rainlink.search.bind(rainlink); - } + public unload(rainlink: Rainlink) { + this.rainlink = rainlink; + rainlink.search = rainlink.search.bind(rainlink); + } - /** Name function for getting plugin name */ - public name(): string { - return 'rainlink-nico'; - } + /** Name function for getting plugin name */ + public name(): string { + return "rainlink-nico"; + } - private async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return this.searchDirect(query, options); - else return res; - } + private async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } - /** + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect( - query: string, - options?: RainlinkSearchOptions | undefined, - ): Promise { - if (!this.rainlink || !this._search) throw new Error('rainlink-nico is not loaded yet.'); + public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { + if (!this.rainlink || !this._search) throw new Error("rainlink-nico is not loaded yet."); - if (!query) throw new Error('Query is required'); - const [, id] = REGEX.exec(query) || []; + if (!query) throw new Error("Query is required"); + const [, id] = REGEX.exec(query) || []; - const isUrl = /^https?:\/\//.test(query); + const isUrl = /^https?:\/\//.test(query); - if (id) { - this.debug(`Start search from ${this.sourceName()} plugin`); - const _function = this.methods.track; - const result: Result = await _function(id, options?.requester); + if (id) { + this.debug(`Start search from ${this.sourceName()} plugin`); + const _function = this.methods.track; + const result: Result = await _function(id, options?.requester); - const loadType = result ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.SEARCH; - const playlistName = result.name ?? undefined; + const loadType = result ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.SEARCH; + const playlistName = result.name ?? undefined; - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks && tracks.length !== 0 ? tracks : [], loadType); - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks && tracks.length !== 0 ? tracks : [], loadType); + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType, - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.TRACK, - }; - } + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.TRACK, + }; + } - private async searchTrack(query: string, requester: unknown) { - try { - const { data } = await search({ - q: query, - targets: ['tagsExact'], - fields: ['contentId'], - sort: '-viewCounter', - limit: 10, - }); + private async searchTrack(query: string, requester: unknown) { + try { + const { data } = await search({ + q: query, + targets: ["tagsExact"], + fields: ["contentId"], + sort: "-viewCounter", + limit: 10, + }); - const res: VideoInfo[] = []; + const res: VideoInfo[] = []; - for (let i = 0; i < data.length; i++) { - const element = data[i]; - const nico = new NicoResolver(`https://www.nicovideo.jp/watch/${element.contentId}`); - const info = await nico.getVideoInfo(); - res.push(info); - } + for (let i = 0; i < data.length; i++) { + const element = data[i]; + const nico = new NicoResolver(`https://www.nicovideo.jp/watch/${element.contentId}`); + const info = await nico.getVideoInfo(); + res.push(info); + } - return { - tracks: res.map(track => this.buildrainlinkTrack(track, requester)), - }; - } catch (e: any) { - throw new Error(e); - } - } + return { + tracks: res.map((track) => this.buildrainlinkTrack(track, requester)), + }; + } catch (e: any) { + throw new Error(e); + } + } - private async getTrack(id: string, requester: unknown) { - try { - const niconico = new NicoResolver(`https://www.nicovideo.jp/watch/${id}`); - const info = await niconico.getVideoInfo(); + private async getTrack(id: string, requester: unknown) { + try { + const niconico = new NicoResolver(`https://www.nicovideo.jp/watch/${id}`); + const info = await niconico.getVideoInfo(); - return { tracks: [this.buildrainlinkTrack(info, requester)] }; - } catch (e: any) { - throw new Error(e); - } - } + return { tracks: [this.buildrainlinkTrack(info, requester)] }; + } catch (e: any) { + throw new Error(e); + } + } - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } - private buildrainlinkTrack(nicoTrack: any, requester: unknown) { - return new RainlinkTrack( - { - encoded: '', - info: { - sourceName: this.sourceName(), - identifier: nicoTrack.id, - isSeekable: true, - author: nicoTrack.owner ? nicoTrack.owner.nickname : 'Unknown', - length: nicoTrack.duration * 1000, - isStream: false, - position: 0, - title: nicoTrack.title, - uri: `https://www.nicovideo.jp/watch/${nicoTrack.id}`, - artworkUrl: nicoTrack.thumbnail ? nicoTrack.thumbnail.url : '', - }, - pluginInfo: { - name: 'rainlink.mod@nico', - }, - }, - requester, - ); - } + private buildrainlinkTrack(nicoTrack: any, requester: unknown) { + return new RainlinkTrack( + { + encoded: "", + info: { + sourceName: this.sourceName(), + identifier: nicoTrack.id, + isSeekable: true, + author: nicoTrack.owner ? nicoTrack.owner.nickname : "Unknown", + length: nicoTrack.duration * 1000, + isStream: false, + position: 0, + title: nicoTrack.title, + uri: `https://www.nicovideo.jp/watch/${nicoTrack.id}`, + artworkUrl: nicoTrack.thumbnail ? nicoTrack.thumbnail.url : "", + }, + pluginInfo: { + name: "rainlink.mod@nico", + }, + }, + requester + ); + } - private debug(logs: string) { - this.rainlink ? this.rainlink.emit(RainlinkEvents.Debug, `[Rainlink Nico Plugin]: ${logs}`) : true; - } + private debug(logs: string) { + this.rainlink ? this.rainlink.emit(RainlinkEvents.Debug, `[Rainlink Nico Plugin]: ${logs}`) : true; + } } // Interfaces @@ -280,4 +271,4 @@ interface OriginalVideoInfo { /** @ignore */ export interface VideoInfo extends OriginalVideoInfo { owner: OwnerInfo; -} \ No newline at end of file +} diff --git a/src/rainlink/Plugin/Spotify/Plugin.ts b/src/rainlink/Plugin/Spotify/Plugin.ts index 6d47af0f..e95308a3 100644 --- a/src/rainlink/Plugin/Spotify/Plugin.ts +++ b/src/rainlink/Plugin/Spotify/Plugin.ts @@ -28,266 +28,260 @@ export interface SpotifyOptions { } export class RainlinkPlugin extends SourceRainlinkPlugin { - /** + /** * The options of the plugin. */ - public options: SpotifyOptions; + public options: SpotifyOptions; - private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | null; - private rainlink: Rainlink | null; + private _search: ((query: string, options?: RainlinkSearchOptions) => Promise) | null; + private rainlink: Rainlink | null; - private readonly methods: Record Promise>; - private requestManager: RequestManager; + private readonly methods: Record Promise>; + private requestManager: RequestManager; - /** + /** * Initialize the plugin. * @param spotifyOptions Options for run plugin */ - constructor(spotifyOptions: SpotifyOptions) { - super(); - this.options = spotifyOptions; - this.requestManager = new RequestManager(spotifyOptions); - - this.methods = { - track: this.getTrack.bind(this), - album: this.getAlbum.bind(this), - artist: this.getArtist.bind(this), - playlist: this.getPlaylist.bind(this), - }; - this.rainlink = null; - this._search = null; - } - - /** + constructor(spotifyOptions: SpotifyOptions) { + super(); + this.options = spotifyOptions; + this.requestManager = new RequestManager(spotifyOptions); + + this.methods = { + track: this.getTrack.bind(this), + album: this.getAlbum.bind(this), + artist: this.getArtist.bind(this), + playlist: this.getPlaylist.bind(this), + }; + this.rainlink = null; + this._search = null; + } + + /** * Source identify of the plugin * @returns string */ - public sourceIdentify(): string { - return 'sp'; - } + public sourceIdentify(): string { + return "sp"; + } - /** + /** * Source name of the plugin * @returns string */ - public sourceName(): string { - return 'spotify'; - } + public sourceName(): string { + return "spotify"; + } - /** + /** * Type of the plugin * @returns RainlinkPluginType */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver; - } + public type(): RainlinkPluginType { + return RainlinkPluginType.SourceResolver; + } - /** + /** * load the plugin * @param rainlink The rainlink class */ - public load(rainlink: Rainlink) { - this.rainlink = rainlink; - this._search = rainlink.search.bind(rainlink); - rainlink.search = this.search.bind(this); - } + public load(rainlink: Rainlink) { + this.rainlink = rainlink; + this._search = rainlink.search.bind(rainlink); + rainlink.search = this.search.bind(this); + } - /** + /** * Unload the plugin * @param rainlink The rainlink class */ - public unload(rainlink: Rainlink) { - this.rainlink = rainlink; - rainlink.search = rainlink.search.bind(rainlink); - } - - /** Name function for getting plugin name */ - public name(): string { - return 'rainlink-spotify'; - } - - protected async search(query: string, options?: RainlinkSearchOptions): Promise { - const res = await this._search!(query, options); - if (!this.directSearchChecker(query)) return res; - if (res.tracks.length == 0) return this.searchDirect(query, options); - else return res; - } - - /** + public unload(rainlink: Rainlink) { + this.rainlink = rainlink; + rainlink.search = rainlink.search.bind(rainlink); + } + + /** Name function for getting plugin name */ + public name(): string { + return "rainlink-spotify"; + } + + protected async search(query: string, options?: RainlinkSearchOptions): Promise { + const res = await this._search!(query, options); + if (!this.directSearchChecker(query)) return res; + if (res.tracks.length == 0) return this.searchDirect(query, options); + else return res; + } + + /** * Directly search from plugin * @param query URI or track name query * @param options search option like RainlinkSearchOptions * @returns RainlinkSearchResult */ - public async searchDirect( - query: string, - options?: RainlinkSearchOptions | undefined, - ): Promise { - if (!this.rainlink || !this._search) throw new Error('rainlink-spotify is not loaded yet.'); - - if (!query) throw new Error('Query is required'); - - const isUrl = /^https?:\/\//.test(query); - - if (SHORT_REGEX.test(query)) { - const res = await fetch(query, { method: 'HEAD' }); - query = String(res.headers.get('location')); - } - - const [, type, id] = REGEX.exec(query) || []; - - if (type in this.methods) { - try { - const _function = this.methods[type]; - const result: Result = await _function(id, options?.requester); - - const loadType = - type === 'track' ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; - const playlistName = result.name ?? undefined; - - const tracks = result.tracks.filter(this.filterNullOrUndefined); - return this.buildSearch(playlistName, tracks, loadType); - } catch (e) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - } else if (options?.engine === this.sourceName() && !isUrl) { - const result = await this.searchTrack(query, options?.requester); - - return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); - } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); - } - - private buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType, - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.TRACK, - }; - } - - private async searchTrack(query: string, requester: unknown): Promise { - const limit = + public async searchDirect(query: string, options?: RainlinkSearchOptions | undefined): Promise { + if (!this.rainlink || !this._search) throw new Error("rainlink-spotify is not loaded yet."); + + if (!query) throw new Error("Query is required"); + + const isUrl = /^https?:\/\//.test(query); + + if (SHORT_REGEX.test(query)) { + const res = await fetch(query, { method: "HEAD" }); + query = String(res.headers.get("location")); + } + + const [, type, id] = REGEX.exec(query) || []; + + if (type in this.methods) { + try { + const _function = this.methods[type]; + const result: Result = await _function(id, options?.requester); + + const loadType = type === "track" ? RainlinkSearchResultType.TRACK : RainlinkSearchResultType.PLAYLIST; + const playlistName = result.name ?? undefined; + + const tracks = result.tracks.filter(this.filterNullOrUndefined); + return this.buildSearch(playlistName, tracks, loadType); + } catch (e) { + return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + } else if (options?.engine === this.sourceName() && !isUrl) { + const result = await this.searchTrack(query, options?.requester); + + return this.buildSearch(undefined, result.tracks, RainlinkSearchResultType.SEARCH); + } else return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH); + } + + private buildSearch( + playlistName?: string, + tracks: RainlinkTrack[] = [], + type?: RainlinkSearchResultType + ): RainlinkSearchResult { + return { + playlistName, + tracks, + type: type ?? RainlinkSearchResultType.TRACK, + }; + } + + private async searchTrack(query: string, requester: unknown): Promise { + const limit = this.options.searchLimit && this.options.searchLimit > 0 && this.options.searchLimit < 50 - ? this.options.searchLimit - : 10; - const tracks = await this.requestManager.makeRequest( - `/search?q=${decodeURIComponent( - query, - )}&type=track&limit=${limit}&market=${this.options.searchMarket ?? 'US'}`, - ); - return { - tracks: tracks.tracks.items.map(track => this.buildrainlinkTrack(track, requester)), - }; - } - - private async getTrack(id: string, requester: unknown): Promise { - const track = await this.requestManager.makeRequest(`/tracks/${id}`); - return { tracks: [this.buildrainlinkTrack(track, requester)] }; - } - - private async getAlbum(id: string, requester: unknown): Promise { - const album = await this.requestManager.makeRequest( - `/albums/${id}?market=${this.options.searchMarket ?? 'US'}`, - ); - const tracks = album.tracks.items - .filter(this.filterNullOrUndefined) - .map(track => this.buildrainlinkTrack(track, requester, album.images[0]?.url)); - - if (album && tracks.length) { - let next = album.tracks.next; - let page = 1; - - while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { - const nextTracks = await this.requestManager.makeRequest(next ?? '', true); - page++; - if (nextTracks.items.length) { - next = nextTracks.next; - tracks.push( - ...nextTracks.items - .filter(this.filterNullOrUndefined) - .filter(a => a.track) - .map(track => this.buildrainlinkTrack(track.track!, requester, album.images[0]?.url)), - ); - } - } - } - - return { tracks, name: album.name }; - } - - private async getArtist(id: string, requester: unknown): Promise { - const artist = await this.requestManager.makeRequest(`/artists/${id}`); - const fetchedTracks = await this.requestManager.makeRequest( - `/artists/${id}/top-tracks?market=${this.options.searchMarket ?? 'US'}`, - ); - - const tracks = fetchedTracks.tracks - .filter(this.filterNullOrUndefined) - .map(track => this.buildrainlinkTrack(track, requester, artist.images[0]?.url)); - - return { tracks, name: artist.name }; - } - - private async getPlaylist(id: string, requester: unknown): Promise { - const playlist = await this.requestManager.makeRequest( - `/playlists/${id}?market=${this.options.searchMarket ?? 'US'}`, - ); - - const tracks = playlist.tracks.items - .filter(this.filterNullOrUndefined) - .map(track => this.buildrainlinkTrack(track.track, requester, playlist.images[0]?.url)); - - if (playlist && tracks.length) { - let next = playlist.tracks.next; - let page = 1; - while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { - const nextTracks = await this.requestManager.makeRequest(next ?? '', true); - page++; - if (nextTracks.items.length) { - next = nextTracks.next; - tracks.push( - ...nextTracks.items - .filter(this.filterNullOrUndefined) - .filter(a => a.track) - .map(track => this.buildrainlinkTrack(track.track!, requester, playlist.images[0]?.url)), - ); - } - } - } - return { tracks, name: playlist.name }; - } - - private filterNullOrUndefined(obj: unknown): obj is unknown { - return obj !== undefined && obj !== null; - } - - private buildrainlinkTrack(spotifyTrack: Track, requester: unknown, thumbnail?: string) { - return new RainlinkTrack( - { - encoded: '', - info: { - sourceName: 'spotify', - identifier: spotifyTrack.id, - isSeekable: true, - author: spotifyTrack.artists[0] ? spotifyTrack.artists[0].name : 'Unknown', - length: spotifyTrack.duration_ms, - isStream: false, - position: 0, - title: spotifyTrack.name, - uri: `https://open.spotify.com/track/${spotifyTrack.id}`, - artworkUrl: thumbnail ? thumbnail : spotifyTrack.album?.images[0]?.url, - }, - pluginInfo: { - name: this.name(), - }, - }, - requester, - ); - } + ? this.options.searchLimit + : 10; + const tracks = await this.requestManager.makeRequest( + `/search?q=${decodeURIComponent(query)}&type=track&limit=${limit}&market=${this.options.searchMarket ?? "US"}` + ); + return { + tracks: tracks.tracks.items.map((track) => this.buildrainlinkTrack(track, requester)), + }; + } + + private async getTrack(id: string, requester: unknown): Promise { + const track = await this.requestManager.makeRequest(`/tracks/${id}`); + return { tracks: [this.buildrainlinkTrack(track, requester)] }; + } + + private async getAlbum(id: string, requester: unknown): Promise { + const album = await this.requestManager.makeRequest( + `/albums/${id}?market=${this.options.searchMarket ?? "US"}` + ); + const tracks = album.tracks.items + .filter(this.filterNullOrUndefined) + .map((track) => this.buildrainlinkTrack(track, requester, album.images[0]?.url)); + + if (album && tracks.length) { + let next = album.tracks.next; + let page = 1; + + while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { + const nextTracks = await this.requestManager.makeRequest(next ?? "", true); + page++; + if (nextTracks.items.length) { + next = nextTracks.next; + tracks.push( + ...nextTracks.items + .filter(this.filterNullOrUndefined) + .filter((a) => a.track) + .map((track) => this.buildrainlinkTrack(track.track!, requester, album.images[0]?.url)) + ); + } + } + } + + return { tracks, name: album.name }; + } + + private async getArtist(id: string, requester: unknown): Promise { + const artist = await this.requestManager.makeRequest(`/artists/${id}`); + const fetchedTracks = await this.requestManager.makeRequest( + `/artists/${id}/top-tracks?market=${this.options.searchMarket ?? "US"}` + ); + + const tracks = fetchedTracks.tracks + .filter(this.filterNullOrUndefined) + .map((track) => this.buildrainlinkTrack(track, requester, artist.images[0]?.url)); + + return { tracks, name: artist.name }; + } + + private async getPlaylist(id: string, requester: unknown): Promise { + const playlist = await this.requestManager.makeRequest( + `/playlists/${id}?market=${this.options.searchMarket ?? "US"}` + ); + + const tracks = playlist.tracks.items + .filter(this.filterNullOrUndefined) + .map((track) => this.buildrainlinkTrack(track.track, requester, playlist.images[0]?.url)); + + if (playlist && tracks.length) { + let next = playlist.tracks.next; + let page = 1; + while (next && (!this.options.playlistPageLimit ? true : page < this.options.playlistPageLimit ?? 1)) { + const nextTracks = await this.requestManager.makeRequest(next ?? "", true); + page++; + if (nextTracks.items.length) { + next = nextTracks.next; + tracks.push( + ...nextTracks.items + .filter(this.filterNullOrUndefined) + .filter((a) => a.track) + .map((track) => this.buildrainlinkTrack(track.track!, requester, playlist.images[0]?.url)) + ); + } + } + } + return { tracks, name: playlist.name }; + } + + private filterNullOrUndefined(obj: unknown): obj is unknown { + return obj !== undefined && obj !== null; + } + + private buildrainlinkTrack(spotifyTrack: Track, requester: unknown, thumbnail?: string) { + return new RainlinkTrack( + { + encoded: "", + info: { + sourceName: "spotify", + identifier: spotifyTrack.id, + isSeekable: true, + author: spotifyTrack.artists[0] ? spotifyTrack.artists[0].name : "Unknown", + length: spotifyTrack.duration_ms, + isStream: false, + position: 0, + title: spotifyTrack.name, + uri: `https://open.spotify.com/track/${spotifyTrack.id}`, + artworkUrl: thumbnail ? thumbnail : spotifyTrack.album?.images[0]?.url, + }, + pluginInfo: { + name: this.name(), + }, + }, + requester + ); + } } /** @ignore */ @@ -472,4 +466,4 @@ export interface Track { track_number: number; type: string; uri: string; -} \ No newline at end of file +} diff --git a/src/web/route/deletePlayer.ts b/src/web/route/deletePlayer.ts index 1e023390..f44326f3 100644 --- a/src/web/route/deletePlayer.ts +++ b/src/web/route/deletePlayer.ts @@ -1,9 +1,12 @@ -import util from 'node:util'; +import util from "node:util"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function deletePlayer(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCommands.ts b/src/web/route/getCommands.ts index 2467cc76..7cd2fa27 100644 --- a/src/web/route/getCommands.ts +++ b/src/web/route/getCommands.ts @@ -4,13 +4,13 @@ import Fastify from "fastify"; export async function getCommands(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} payload={}`); res.send({ - data: client.commands.map(command => ({ + data: client.commands.map((command) => ({ name: command.name.join("-"), description: command.description, category: command.category, accessableby: command.accessableby, usage: command.usage, - aliases: command.aliases - })) - }) + aliases: command.aliases, + })), + }); } diff --git a/src/web/route/getCurrentLoop.ts b/src/web/route/getCurrentLoop.ts index 2e56fc51..87be66c4 100644 --- a/src/web/route/getCurrentLoop.ts +++ b/src/web/route/getCurrentLoop.ts @@ -1,9 +1,12 @@ -import util from 'node:util'; +import util from "node:util"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentLoop(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentPaused.ts b/src/web/route/getCurrentPaused.ts index 371e5b98..d03c0567 100644 --- a/src/web/route/getCurrentPaused.ts +++ b/src/web/route/getCurrentPaused.ts @@ -1,9 +1,12 @@ -import util from 'node:util'; +import util from "node:util"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentPaused(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentPosition.ts b/src/web/route/getCurrentPosition.ts index 6b65d753..651155c0 100644 --- a/src/web/route/getCurrentPosition.ts +++ b/src/web/route/getCurrentPosition.ts @@ -1,9 +1,12 @@ -import util from 'node:util'; +import util from "node:util"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentPosition(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { diff --git a/src/web/route/getCurrentTrackStatus.ts b/src/web/route/getCurrentTrackStatus.ts index f8e08e88..8acc95c2 100644 --- a/src/web/route/getCurrentTrackStatus.ts +++ b/src/web/route/getCurrentTrackStatus.ts @@ -1,10 +1,13 @@ -import util from 'node:util'; +import util from "node:util"; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getCurrentTrackStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { @@ -18,7 +21,6 @@ export async function getCurrentTrackStatus(client: Manager, req: Fastify.Fastif res.send({ data: song ? { - encoded: song.encoded, title: song.title, uri: song.uri, length: song.duration, diff --git a/src/web/route/getMemberStatus.ts b/src/web/route/getMemberStatus.ts index 3116dfa1..5f373653 100644 --- a/src/web/route/getMemberStatus.ts +++ b/src/web/route/getMemberStatus.ts @@ -1,9 +1,12 @@ -import util from 'node:util'; +import util from "node:util"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getMemberStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); let isMemeberInVoice = false; const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); diff --git a/src/web/route/getQueueStatus.ts b/src/web/route/getQueueStatus.ts index 3356d17a..28233c14 100644 --- a/src/web/route/getQueueStatus.ts +++ b/src/web/route/getQueueStatus.ts @@ -1,10 +1,13 @@ -import util from 'node:util'; +import util from "node:util"; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getQueueStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { @@ -12,22 +15,24 @@ export async function getQueueStatus(client: Manager, req: Fastify.FastifyReques res.send({ error: "Current player not found!" }); return; } - return player.queue.map((track) => { - const requesterQueue = track.requester as User; - return { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: requesterQueue - ? { - id: requesterQueue.id, - username: requesterQueue.username, - globalName: requesterQueue.globalName, - defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, - } - : null, - }; + return res.send({ + data: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), }); } diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts index ab311063..bbf102f1 100644 --- a/src/web/route/getSearch.ts +++ b/src/web/route/getSearch.ts @@ -1,11 +1,14 @@ -import util from 'node:util'; +import util from "node:util"; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; import { RainlinkSearchResultType } from "../../rainlink/main.js"; export async function getSearch(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} query=${req.query ? util.inspect(req.query) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} query=${req.query ? util.inspect(req.query) : "{}"}` + ); const query = (req.query as Record)["identifier"]; const requester = (req.query as Record)["requester"]; const source = (req.query as Record)["source"]; diff --git a/src/web/route/getStatus.ts b/src/web/route/getStatus.ts index 2da3b098..f1277fb2 100644 --- a/src/web/route/getStatus.ts +++ b/src/web/route/getStatus.ts @@ -1,10 +1,13 @@ -import util from 'node:util'; +import util from "node:util"; import { User } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; export async function getStatus(client: Manager, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}`); + client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} params=${req.params ? util.inspect(req.params) : "{}"}` + ); let isMemeberInVoice = "notGiven"; const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); diff --git a/src/web/route/postCreatePlayer.ts b/src/web/route/postCreatePlayer.ts index e2832372..36b7dd69 100644 --- a/src/web/route/postCreatePlayer.ts +++ b/src/web/route/postCreatePlayer.ts @@ -1,19 +1,21 @@ -import util from 'node:util'; +import util from "node:util"; import { Guild, GuildMember } from "discord.js"; import { Manager } from "../../manager.js"; import Fastify from "fastify"; - export class PostCreatePlayer { guild: Guild | null = null; member: GuildMember | null = null; constructor(protected client: Manager) {} async main(req: Fastify.FastifyRequest, res: Fastify.FastifyReply) { - this.client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url} payload=${req.body ? util.inspect(req.body) : "{}"}`); - const data = (req.body as Record) - const validBody = await this.checker(data, req, res) - if (!validBody) return + this.client.logger.info( + import.meta.url, + `${req.method} ${req.routeOptions.url} payload=${req.body ? util.inspect(req.body) : "{}"}` + ); + const data = req.body as Record; + const validBody = await this.checker(data, req, res); + if (!validBody) return; const playerData = { guildId: this.guild!.id, voiceId: this.member!.voice.channel!.id, @@ -21,9 +23,9 @@ export class PostCreatePlayer { shardId: this.guild?.shardId ?? 0, deaf: true, volume: this.client.config.lavalink.DEFAULT_VOLUME ?? 100, - } + }; this.client.rainlink.create(playerData); - res.send(playerData) + res.send(playerData); } clean() { @@ -31,28 +33,32 @@ export class PostCreatePlayer { this.member = null; } - async checker(data: Record, req: Fastify.FastifyRequest, res: Fastify.FastifyReply): Promise { - const reqKey = ["guildId", "userId"] - if (!data) return this.errorRes(req, res, "Missing body") - if (Object.keys(data).length !== reqKey.length) return this.errorRes(req, res, "Missing key") - if (!data["guildId"]) return this.errorRes(req, res, "Missing guildId key") - if (!data["userId"]) return this.errorRes(req, res, "Missing userId key") + async checker( + data: Record, + req: Fastify.FastifyRequest, + res: Fastify.FastifyReply + ): Promise { + const reqKey = ["guildId", "userId"]; + if (!data) return this.errorRes(req, res, "Missing body"); + if (Object.keys(data).length !== reqKey.length) return this.errorRes(req, res, "Missing key"); + if (!data["guildId"]) return this.errorRes(req, res, "Missing guildId key"); + if (!data["userId"]) return this.errorRes(req, res, "Missing userId key"); const Guild = await this.client.guilds.fetch(data["guildId"]).catch(() => undefined); - if (!Guild) return this.errorRes(req, res, "Guild not found") - const isPlayerExist = this.client.rainlink.players.get(Guild.id) - if (isPlayerExist) return this.errorRes(req, res, "Player existed in this guild") - this.guild = Guild + if (!Guild) return this.errorRes(req, res, "Guild not found"); + const isPlayerExist = this.client.rainlink.players.get(Guild.id); + if (isPlayerExist) return this.errorRes(req, res, "Player existed in this guild"); + this.guild = Guild; const Member = await Guild.members.fetch(data["userId"]).catch(() => undefined); - if (!Member) return this.errorRes(req, res, "User not found") - if (!Member.voice.channel || !Member.voice) return this.errorRes(req, res, "User is not in voice") - this.member = Member - return true + if (!Member) return this.errorRes(req, res, "User not found"); + if (!Member.voice.channel || !Member.voice) return this.errorRes(req, res, "User is not in voice"); + this.member = Member; + return true; } - + async errorRes(req: Fastify.FastifyRequest, res: Fastify.FastifyReply, message: string) { res.code(400); res.send({ error: message }); - this.clean() - return false + this.clean(); + return false; } } diff --git a/src/web/websocket.ts b/src/web/websocket.ts index e8e5357d..6ccf6e12 100644 --- a/src/web/websocket.ts +++ b/src/web/websocket.ts @@ -8,16 +8,12 @@ export class WebsocketRoute { main(fastify: Fastify.FastifyInstance) { fastify.get("/websocket", { websocket: true }, (socket, req) => { this.client.logger.info(import.meta.url, `${req.method} ${req.routeOptions.url}`); - socket.on("close", (code, reason) => { this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); - this.client.wsId.delete(String(req.headers["guild-id"])); }); - if (!this.checker(socket, req)) return; - + this.client.websocket = socket; this.client.logger.websocket(import.meta.url, `Websocket opened for ${req.headers["guild-id"]}`); - this.client.wsId.set(String(req.headers["guild-id"]), true); }); } From 59fa477955e8c4e1e719eeedfbc3987280796ed1 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Tue, 7 May 2024 20:09:24 +0700 Subject: [PATCH 13/21] add: events for websocket --- src/buttons/Clear.ts | 9 ++++ src/buttons/Loop.ts | 29 ++++++++++- src/buttons/Previous.ts | 2 + src/buttons/Shuffle.ts | 28 +++++++++- src/buttons/VolumeDown.ts | 11 +++- src/buttons/VolumeUp.ts | 12 ++++- src/commands/Music/ClearQueue.ts | 15 +++--- src/commands/Music/Insert.ts | 47 +++++++++-------- src/commands/Music/Loop.ts | 17 +++---- src/commands/Music/Previous.ts | 2 + src/commands/Music/Remove.ts | 47 +++++++++-------- src/commands/Music/Shuffle.ts | 51 +++++++++---------- src/commands/Music/Volume.ts | 17 +++---- src/events/websocket/trackEnd.ts | 1 + src/handlers/Player/ButtonCommands/Loop.ts | 27 ++++++++++ .../Player/ButtonCommands/Previous.ts | 2 + 16 files changed, 213 insertions(+), 104 deletions(-) diff --git a/src/buttons/Clear.ts b/src/buttons/Clear.ts index 6d78c9e0..be43bf22 100644 --- a/src/buttons/Clear.ts +++ b/src/buttons/Clear.ts @@ -21,6 +21,15 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "clear_msg")}`); + client.websocket + ? client.websocket.send( + JSON.stringify({ + op: "playerClearQueue", + guild: message.guild!.id, + }) + ) + : true; + return; } } diff --git a/src/buttons/Loop.ts b/src/buttons/Loop.ts index 99208802..9696754c 100644 --- a/src/buttons/Loop.ts +++ b/src/buttons/Loop.ts @@ -32,15 +32,33 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "loop_current")}`); + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: message.guild!.id, + mode: "song", + }) + ); + break; case "song": - await player.setLoop(RainlinkLoopMode.QUEUE); + player.setLoop(RainlinkLoopMode.QUEUE); setLoop247(RainlinkLoopMode.QUEUE); new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "loop_all")}`); + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: message.guild!.id, + mode: "queue", + }) + ); + break; case "queue": @@ -50,6 +68,15 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "unloop_all")}`); + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: message.guild!.id, + mode: "none", + }) + ); + break; } } diff --git a/src/buttons/Previous.ts b/src/buttons/Previous.ts index 6c9ae2dc..13ee4571 100644 --- a/src/buttons/Previous.ts +++ b/src/buttons/Previous.ts @@ -28,6 +28,8 @@ export default class implements PlayerButton { player.previous(); + player.data.set("endMode", "previous") + await new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "previous_msg")}`); return; } diff --git a/src/buttons/Shuffle.ts b/src/buttons/Shuffle.ts index dc73d993..97552f8b 100644 --- a/src/buttons/Shuffle.ts +++ b/src/buttons/Shuffle.ts @@ -1,4 +1,4 @@ -import { ButtonInteraction, CacheType, EmbedBuilder, InteractionCollector, Message } from "discord.js"; +import { ButtonInteraction, CacheType, EmbedBuilder, InteractionCollector, Message, User } from "discord.js"; import { PlayerButton } from "../@types/Button.js"; import { Manager } from "../manager.js"; import { FormatDuration } from "../utilities/FormatDuration.js"; @@ -67,6 +67,32 @@ export default class implements PlayerButton { pages.push(embed); } + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerQueueShuffle", + guild: message.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }) + ); + if (pages.length == pagesNum && newQueue.length > 10) { await new PageQueue(client, pages, 60000, newQueue.length, language).buttonPage(message, qduration); } else message.reply({ embeds: [pages[0]], ephemeral: true }); diff --git a/src/buttons/VolumeDown.ts b/src/buttons/VolumeDown.ts index 276d5b85..b75d2d76 100644 --- a/src/buttons/VolumeDown.ts +++ b/src/buttons/VolumeDown.ts @@ -33,7 +33,16 @@ export default class implements PlayerButton { player.setVolume(player.volume - 10); - await new ReplyInteractionService(client, message, reply_msg); + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerVolume", + guild: message.guild!.id, + volume: player.volume, + }) + ); + + new ReplyInteractionService(client, message, reply_msg); return; } } diff --git a/src/buttons/VolumeUp.ts b/src/buttons/VolumeUp.ts index beba5ce0..fa70d909 100644 --- a/src/buttons/VolumeUp.ts +++ b/src/buttons/VolumeUp.ts @@ -32,7 +32,17 @@ export default class implements PlayerButton { } player.setVolume(player.volume + 10); - await new ReplyInteractionService(client, message, reply_msg); + + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerVolume", + guild: message.guild!.id, + volume: player.volume, + }) + ); + + new ReplyInteractionService(client, message, reply_msg); return; } } diff --git a/src/commands/Music/ClearQueue.ts b/src/commands/Music/ClearQueue.ts index 52ebeddd..5db3624d 100644 --- a/src/commands/Music/ClearQueue.ts +++ b/src/commands/Music/ClearQueue.ts @@ -30,13 +30,12 @@ export default class implements Command { .setColor(client.color); await handler.editReply({ content: " ", embeds: [cleared] }); - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerClearQueue", - guild: handler.guild!.id, - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerClearQueue", + guild: handler.guild!.id, + }) + ); } } diff --git a/src/commands/Music/Insert.ts b/src/commands/Music/Insert.ts index 2ac7c765..9e9f1fc3 100644 --- a/src/commands/Music/Insert.ts +++ b/src/commands/Music/Insert.ts @@ -92,30 +92,29 @@ export default class implements Command { ) .setColor(client.color); - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerQueueInsert", - guild: handler.guild!.id, - track: { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: track.requester - ? { - id: (track.requester as any).id, - username: (track.requester as any).username, - globalName: (track.requester as any).globalName, - defaultAvatarURL: (track.requester as any).defaultAvatarURL ?? null, - } - : null, - }, - index: position - 1, - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerQueueInsert", + guild: handler.guild!.id, + track: { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: track.requester + ? { + id: (track.requester as any).id, + username: (track.requester as any).username, + globalName: (track.requester as any).globalName, + defaultAvatarURL: (track.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: position - 1, + }) + ); return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Loop.ts b/src/commands/Music/Loop.ts index 1af7b36f..3ae9542d 100644 --- a/src/commands/Music/Loop.ts +++ b/src/commands/Music/Loop.ts @@ -101,15 +101,14 @@ export default class implements Command { handler.editReply({ content: " ", embeds: [looped] }); } - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: handler.guild!.id, - mode: mode, - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: handler.guild!.id, + mode: mode, + }) + ); } async setLoop247(client: Manager, player: RainlinkPlayer, loop: string) { diff --git a/src/commands/Music/Previous.ts b/src/commands/Music/Previous.ts index 02ecdbca..cb170198 100644 --- a/src/commands/Music/Previous.ts +++ b/src/commands/Music/Previous.ts @@ -40,6 +40,8 @@ export default class implements Command { player.previous(); + player.data.set("endMode", "previous") + const embed = new EmbedBuilder() .setDescription(`${client.getString(handler.language, "command.music", "previous_msg")}`) .setColor(client.color); diff --git a/src/commands/Music/Remove.ts b/src/commands/Music/Remove.ts index a3e4e2b4..0f5de911 100644 --- a/src/commands/Music/Remove.ts +++ b/src/commands/Music/Remove.ts @@ -73,30 +73,29 @@ export default class implements Command { ) .setColor(client.color); - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerQueueRemove", - guild: handler.guild!.id, - track: { - title: song.title, - uri: song.uri, - length: song.duration, - thumbnail: song.artworkUrl, - author: song.author, - requester: song.requester - ? { - id: (song.requester as any).id, - username: (song.requester as any).username, - globalName: (song.requester as any).globalName, - defaultAvatarURL: (song.requester as any).defaultAvatarURL ?? null, - } - : null, - }, - index: Number(tracks) - 1, - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerQueueRemove", + guild: handler.guild!.id, + track: { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: song.requester + ? { + id: (song.requester as any).id, + username: (song.requester as any).username, + globalName: (song.requester as any).globalName, + defaultAvatarURL: (song.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: Number(tracks) - 1, + }) + ); return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Shuffle.ts b/src/commands/Music/Shuffle.ts index 9a0dcd54..54a8ec9b 100644 --- a/src/commands/Music/Shuffle.ts +++ b/src/commands/Music/Shuffle.ts @@ -75,32 +75,31 @@ export default class implements Command { pages.push(embed); } - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerQueueShuffle", - guild: handler.guild!.id, - queue: player.queue.map((track) => { - const requesterQueue = track.requester as User; - return { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: requesterQueue - ? { - id: requesterQueue.id, - username: requesterQueue.username, - globalName: requesterQueue.globalName, - defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, - } - : null, - }; - }), - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerQueueShuffle", + guild: handler.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }) + ); if (pages.length == pagesNum && newQueue.length > 10) { if (handler.message) { diff --git a/src/commands/Music/Volume.ts b/src/commands/Music/Volume.ts index 6e9c0395..4d83700b 100644 --- a/src/commands/Music/Volume.ts +++ b/src/commands/Music/Volume.ts @@ -60,15 +60,14 @@ export default class implements Command { await player.setVolume(Number(value)); - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerVolume", - guild: handler.guild!.id, - volume: player.volume, - }) - ) - : true; + if (client.websocket) + client.websocket.send( + JSON.stringify({ + op: "playerVolume", + guild: handler.guild!.id, + volume: player.volume, + }) + ); const changevol = new EmbedBuilder() .setDescription( diff --git a/src/events/websocket/trackEnd.ts b/src/events/websocket/trackEnd.ts index 77238077..e4bb9123 100644 --- a/src/events/websocket/trackEnd.ts +++ b/src/events/websocket/trackEnd.ts @@ -25,6 +25,7 @@ export default class { op: "playerEnd", guild: player.guildId, data: currentData, + mode: player.data.get("endMode") ?? "normal" }) ); } diff --git a/src/handlers/Player/ButtonCommands/Loop.ts b/src/handlers/Player/ButtonCommands/Loop.ts index e6aa0451..5a69062c 100644 --- a/src/handlers/Player/ButtonCommands/Loop.ts +++ b/src/handlers/Player/ButtonCommands/Loop.ts @@ -34,6 +34,15 @@ export class ButtonLoop { content: " ", embeds: [looptrack], }); + + if (this.client.websocket) + this.client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "song", + }) + ); break; case "song": @@ -48,6 +57,15 @@ export class ButtonLoop { content: " ", embeds: [loopall], }); + + if (this.client.websocket) + this.client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "queue", + }) + ); break; case "queue": @@ -62,6 +80,15 @@ export class ButtonLoop { content: " ", embeds: [unloopall], }); + + if (this.client.websocket) + this.client.websocket.send( + JSON.stringify({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "none", + }) + ); break; } } diff --git a/src/handlers/Player/ButtonCommands/Previous.ts b/src/handlers/Player/ButtonCommands/Previous.ts index 34939895..9bc94df4 100644 --- a/src/handlers/Player/ButtonCommands/Previous.ts +++ b/src/handlers/Player/ButtonCommands/Previous.ts @@ -57,6 +57,8 @@ export class ButtonPrevious { } else { this.player.previous(); + this.player.data.set("endMode", "previous") + const embed = new EmbedBuilder() .setDescription(`${this.client.getString(this.language, "button.music", "previous_msg")}`) .setColor(this.client.color); From 5f05a3b84fe9cdf8b74f20bcf9163fda5b5339e6 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Tue, 7 May 2024 20:17:48 +0700 Subject: [PATCH 14/21] =?UTF-8?q?Fix=20=E1=BB=A9=20not=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/handlers/loadEvents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/loadEvents.ts b/src/handlers/loadEvents.ts index 420c5bb1..84f71396 100644 --- a/src/handlers/loadEvents.ts +++ b/src/handlers/loadEvents.ts @@ -13,7 +13,7 @@ export class loadMainEvents { this.loader(); } async loader() { - await chillout.forEach(["client", "guild", "shard"], async (path) => { + await chillout.forEach(["client", "guild", "shard", "websocket"], async (path) => { let eventsPath = resolve(join(__dirname, "..", "events", path)); let eventsFile = await readdirRecursive(eventsPath); await this.registerPath(eventsFile); From b9faae9364a8f85f63376b86fc54f37408722de7 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 09:51:33 +0700 Subject: [PATCH 15/21] add: multi connection support --- src/buttons/Clear.ts | 12 ++--- src/buttons/Loop.ts | 39 ++++++--------- src/buttons/Previous.ts | 2 +- src/buttons/Shuffle.ts | 47 +++++++++---------- src/buttons/VolumeDown.ts | 13 ++--- src/buttons/VolumeUp.ts | 13 ++--- src/commands/Music/ClearQueue.ts | 11 ++--- src/commands/Music/Insert.ts | 43 ++++++++--------- src/commands/Music/Loop.ts | 13 ++--- src/commands/Music/Previous.ts | 2 +- src/commands/Music/Remove.ts | 43 ++++++++--------- src/commands/Music/Shuffle.ts | 47 +++++++++---------- src/commands/Music/Volume.ts | 13 ++--- src/events/websocket/memberJoin.ts | 17 +++---- src/events/websocket/memberLeave.ts | 14 ++---- src/events/websocket/playerCreate.ts | 6 ++- src/events/websocket/playerDestroy.ts | 6 ++- src/events/websocket/playerPause.ts | 4 +- src/events/websocket/playerResume.ts | 4 +- src/events/websocket/playerUpdate.ts | 13 ++--- src/events/websocket/trackEnd.ts | 16 +++---- src/events/websocket/trackStart.ts | 13 ++--- src/handlers/Player/ButtonCommands/Loop.ts | 39 ++++++--------- .../Player/ButtonCommands/Previous.ts | 2 +- src/manager.ts | 3 +- src/web/websocket.ts | 4 +- 26 files changed, 186 insertions(+), 253 deletions(-) diff --git a/src/buttons/Clear.ts b/src/buttons/Clear.ts index be43bf22..dc14d734 100644 --- a/src/buttons/Clear.ts +++ b/src/buttons/Clear.ts @@ -21,14 +21,10 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "clear_msg")}`); - client.websocket - ? client.websocket.send( - JSON.stringify({ - op: "playerClearQueue", - guild: message.guild!.id, - }) - ) - : true; + client.wsl.get(message.guild!.id)?.send({ + op: "playerClearQueue", + guild: message.guild!.id, + }); return; } diff --git a/src/buttons/Loop.ts b/src/buttons/Loop.ts index 9696754c..43b3231e 100644 --- a/src/buttons/Loop.ts +++ b/src/buttons/Loop.ts @@ -32,14 +32,11 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "loop_current")}`); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: message.guild!.id, - mode: "song", - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerLoop", + guild: message.guild!.id, + mode: "song", + }); break; @@ -50,14 +47,11 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "loop_all")}`); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: message.guild!.id, - mode: "queue", - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerLoop", + guild: message.guild!.id, + mode: "queue", + }); break; @@ -68,14 +62,11 @@ export default class implements PlayerButton { new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "unloop_all")}`); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: message.guild!.id, - mode: "none", - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerLoop", + guild: message.guild!.id, + mode: "none", + }); break; } diff --git a/src/buttons/Previous.ts b/src/buttons/Previous.ts index 13ee4571..dfb8ba59 100644 --- a/src/buttons/Previous.ts +++ b/src/buttons/Previous.ts @@ -28,7 +28,7 @@ export default class implements PlayerButton { player.previous(); - player.data.set("endMode", "previous") + player.data.set("endMode", "previous"); await new ReplyInteractionService(client, message, `${client.getString(language, "button.music", "previous_msg")}`); return; diff --git a/src/buttons/Shuffle.ts b/src/buttons/Shuffle.ts index 97552f8b..95f676ef 100644 --- a/src/buttons/Shuffle.ts +++ b/src/buttons/Shuffle.ts @@ -67,31 +67,28 @@ export default class implements PlayerButton { pages.push(embed); } - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerQueueShuffle", - guild: message.guild!.id, - queue: player.queue.map((track) => { - const requesterQueue = track.requester as User; - return { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: requesterQueue - ? { - id: requesterQueue.id, - username: requesterQueue.username, - globalName: requesterQueue.globalName, - defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, - } - : null, - }; - }), - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerQueueShuffle", + guild: message.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }); if (pages.length == pagesNum && newQueue.length > 10) { await new PageQueue(client, pages, 60000, newQueue.length, language).buttonPage(message, qduration); diff --git a/src/buttons/VolumeDown.ts b/src/buttons/VolumeDown.ts index b75d2d76..4005b1b0 100644 --- a/src/buttons/VolumeDown.ts +++ b/src/buttons/VolumeDown.ts @@ -33,14 +33,11 @@ export default class implements PlayerButton { player.setVolume(player.volume - 10); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerVolume", - guild: message.guild!.id, - volume: player.volume, - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerVolume", + guild: message.guild!.id, + volume: player.volume, + }); new ReplyInteractionService(client, message, reply_msg); return; diff --git a/src/buttons/VolumeUp.ts b/src/buttons/VolumeUp.ts index fa70d909..6fe1399b 100644 --- a/src/buttons/VolumeUp.ts +++ b/src/buttons/VolumeUp.ts @@ -33,14 +33,11 @@ export default class implements PlayerButton { player.setVolume(player.volume + 10); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerVolume", - guild: message.guild!.id, - volume: player.volume, - }) - ); + client.wsl.get(message.guild!.id)?.send({ + op: "playerVolume", + guild: message.guild!.id, + volume: player.volume, + }); new ReplyInteractionService(client, message, reply_msg); return; diff --git a/src/commands/Music/ClearQueue.ts b/src/commands/Music/ClearQueue.ts index 5db3624d..c4189efc 100644 --- a/src/commands/Music/ClearQueue.ts +++ b/src/commands/Music/ClearQueue.ts @@ -30,12 +30,9 @@ export default class implements Command { .setColor(client.color); await handler.editReply({ content: " ", embeds: [cleared] }); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerClearQueue", - guild: handler.guild!.id, - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerClearQueue", + guild: handler.guild!.id, + }); } } diff --git a/src/commands/Music/Insert.ts b/src/commands/Music/Insert.ts index 9e9f1fc3..c337c80b 100644 --- a/src/commands/Music/Insert.ts +++ b/src/commands/Music/Insert.ts @@ -92,29 +92,26 @@ export default class implements Command { ) .setColor(client.color); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerQueueInsert", - guild: handler.guild!.id, - track: { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: track.requester - ? { - id: (track.requester as any).id, - username: (track.requester as any).username, - globalName: (track.requester as any).globalName, - defaultAvatarURL: (track.requester as any).defaultAvatarURL ?? null, - } - : null, - }, - index: position - 1, - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerQueueInsert", + guild: handler.guild!.id, + track: { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: track.requester + ? { + id: (track.requester as any).id, + username: (track.requester as any).username, + globalName: (track.requester as any).globalName, + defaultAvatarURL: (track.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: position - 1, + }); return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Loop.ts b/src/commands/Music/Loop.ts index 3ae9542d..638389d4 100644 --- a/src/commands/Music/Loop.ts +++ b/src/commands/Music/Loop.ts @@ -101,14 +101,11 @@ export default class implements Command { handler.editReply({ content: " ", embeds: [looped] }); } - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: handler.guild!.id, - mode: mode, - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerLoop", + guild: handler.guild!.id, + mode: mode, + }); } async setLoop247(client: Manager, player: RainlinkPlayer, loop: string) { diff --git a/src/commands/Music/Previous.ts b/src/commands/Music/Previous.ts index cb170198..025260f1 100644 --- a/src/commands/Music/Previous.ts +++ b/src/commands/Music/Previous.ts @@ -40,7 +40,7 @@ export default class implements Command { player.previous(); - player.data.set("endMode", "previous") + player.data.set("endMode", "previous"); const embed = new EmbedBuilder() .setDescription(`${client.getString(handler.language, "command.music", "previous_msg")}`) diff --git a/src/commands/Music/Remove.ts b/src/commands/Music/Remove.ts index 0f5de911..8420cd90 100644 --- a/src/commands/Music/Remove.ts +++ b/src/commands/Music/Remove.ts @@ -73,29 +73,26 @@ export default class implements Command { ) .setColor(client.color); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerQueueRemove", - guild: handler.guild!.id, - track: { - title: song.title, - uri: song.uri, - length: song.duration, - thumbnail: song.artworkUrl, - author: song.author, - requester: song.requester - ? { - id: (song.requester as any).id, - username: (song.requester as any).username, - globalName: (song.requester as any).globalName, - defaultAvatarURL: (song.requester as any).defaultAvatarURL ?? null, - } - : null, - }, - index: Number(tracks) - 1, - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerQueueRemove", + guild: handler.guild!.id, + track: { + title: song.title, + uri: song.uri, + length: song.duration, + thumbnail: song.artworkUrl, + author: song.author, + requester: song.requester + ? { + id: (song.requester as any).id, + username: (song.requester as any).username, + globalName: (song.requester as any).globalName, + defaultAvatarURL: (song.requester as any).defaultAvatarURL ?? null, + } + : null, + }, + index: Number(tracks) - 1, + }); return handler.editReply({ embeds: [embed] }); } diff --git a/src/commands/Music/Shuffle.ts b/src/commands/Music/Shuffle.ts index 54a8ec9b..d049daac 100644 --- a/src/commands/Music/Shuffle.ts +++ b/src/commands/Music/Shuffle.ts @@ -75,31 +75,28 @@ export default class implements Command { pages.push(embed); } - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerQueueShuffle", - guild: handler.guild!.id, - queue: player.queue.map((track) => { - const requesterQueue = track.requester as User; - return { - title: track.title, - uri: track.uri, - length: track.duration, - thumbnail: track.artworkUrl, - author: track.author, - requester: requesterQueue - ? { - id: requesterQueue.id, - username: requesterQueue.username, - globalName: requesterQueue.globalName, - defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, - } - : null, - }; - }), - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerQueueShuffle", + guild: handler.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }); if (pages.length == pagesNum && newQueue.length > 10) { if (handler.message) { diff --git a/src/commands/Music/Volume.ts b/src/commands/Music/Volume.ts index 4d83700b..5f59e11a 100644 --- a/src/commands/Music/Volume.ts +++ b/src/commands/Music/Volume.ts @@ -60,14 +60,11 @@ export default class implements Command { await player.setVolume(Number(value)); - if (client.websocket) - client.websocket.send( - JSON.stringify({ - op: "playerVolume", - guild: handler.guild!.id, - volume: player.volume, - }) - ); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerVolume", + guild: handler.guild!.id, + volume: player.volume, + }); const changevol = new EmbedBuilder() .setDescription( diff --git a/src/events/websocket/memberJoin.ts b/src/events/websocket/memberJoin.ts index 6741f528..89a81af8 100644 --- a/src/events/websocket/memberJoin.ts +++ b/src/events/websocket/memberJoin.ts @@ -3,16 +3,11 @@ import { Manager } from "../../manager.js"; export default class { async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { - if (!client.websocket) return; - - if (oldState.channel === null && oldState.id !== client.user!.id) { - client.websocket.send( - JSON.stringify({ - op: "memberJoin", - guild: newState.guild.id, - userId: newState.member?.id, - }) - ); - } + if (oldState.channel === null && oldState.id !== client.user!.id) + client.wsl.get(newState.guild.id)?.send({ + op: "memberJoin", + guild: newState.guild.id, + userId: newState.member?.id, + }); } } diff --git a/src/events/websocket/memberLeave.ts b/src/events/websocket/memberLeave.ts index 5226e8e9..9ed5e19a 100644 --- a/src/events/websocket/memberLeave.ts +++ b/src/events/websocket/memberLeave.ts @@ -3,15 +3,11 @@ import { Manager } from "../../manager.js"; export default class { async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { - if (!client.websocket) return; - if (newState.channel === null && newState.id !== client.user!.id) - client.websocket.send( - JSON.stringify({ - op: "memberLeave", - guild: oldState.guild.id, - userId: oldState.member?.id, - }) - ); + client.wsl.get(oldState.guild.id)?.send({ + op: "memberLeave", + guild: oldState.guild.id, + userId: oldState.member?.id, + }); } } diff --git a/src/events/websocket/playerCreate.ts b/src/events/websocket/playerCreate.ts index 2ebe9043..e3175087 100644 --- a/src/events/websocket/playerCreate.ts +++ b/src/events/websocket/playerCreate.ts @@ -3,7 +3,9 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - client.websocket.send(JSON.stringify({ op: "playerCreate", guild: player.guildId })); + client.wsl.get(player.guildId)?.send({ + op: "playerCreate", + guild: player.guildId, + }); } } diff --git a/src/events/websocket/playerDestroy.ts b/src/events/websocket/playerDestroy.ts index 0c4ac6fa..433c01b0 100644 --- a/src/events/websocket/playerDestroy.ts +++ b/src/events/websocket/playerDestroy.ts @@ -3,7 +3,9 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - client.websocket.send(JSON.stringify({ op: "playerDestroy", guild: player.guildId })); + client.wsl.get(player.guildId)?.send({ + op: "playerDestroy", + guild: player.guildId, + }); } } diff --git a/src/events/websocket/playerPause.ts b/src/events/websocket/playerPause.ts index 93a1812f..2702bdbf 100644 --- a/src/events/websocket/playerPause.ts +++ b/src/events/websocket/playerPause.ts @@ -3,11 +3,9 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - const data = JSON.stringify({ + client.wsl.get(player.guildId)?.send({ op: "playerPause", guild: player.guildId, }); - client.websocket.send(data); } } diff --git a/src/events/websocket/playerResume.ts b/src/events/websocket/playerResume.ts index c0ff3e13..f0507b26 100644 --- a/src/events/websocket/playerResume.ts +++ b/src/events/websocket/playerResume.ts @@ -3,11 +3,9 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - const data = JSON.stringify({ + client.wsl.get(player.guildId)?.send({ op: "playerResume", guild: player.guildId, }); - client.websocket.send(data); } } diff --git a/src/events/websocket/playerUpdate.ts b/src/events/websocket/playerUpdate.ts index e27bf18f..4aa5ab15 100644 --- a/src/events/websocket/playerUpdate.ts +++ b/src/events/websocket/playerUpdate.ts @@ -3,13 +3,10 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - client.websocket.send( - JSON.stringify({ - op: "playerUpdate", - guild: player.guildId, - position: player.position, - }) - ); + client.wsl.get(player.guildId)?.send({ + op: "playerUpdate", + guild: player.guildId, + position: player.position, + }); } } diff --git a/src/events/websocket/trackEnd.ts b/src/events/websocket/trackEnd.ts index e4bb9123..829235fb 100644 --- a/src/events/websocket/trackEnd.ts +++ b/src/events/websocket/trackEnd.ts @@ -3,8 +3,6 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; - const prevoiusIndex = player.queue.previous.length - 1; const song = player.queue.previous[prevoiusIndex === -1 ? 0 : prevoiusIndex]; @@ -20,13 +18,11 @@ export default class { } : null; - client.websocket.send( - JSON.stringify({ - op: "playerEnd", - guild: player.guildId, - data: currentData, - mode: player.data.get("endMode") ?? "normal" - }) - ); + client.wsl.get(player.guildId)?.send({ + op: "playerEnd", + guild: player.guildId, + data: currentData, + mode: player.data.get("endMode") ?? "normal", + }); } } diff --git a/src/events/websocket/trackStart.ts b/src/events/websocket/trackStart.ts index 5cd0e252..96938ff7 100644 --- a/src/events/websocket/trackStart.ts +++ b/src/events/websocket/trackStart.ts @@ -3,7 +3,6 @@ import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - if (!client.websocket) return; const song = player.queue.current; const currentData = { @@ -15,12 +14,10 @@ export default class { requester: song!.requester, }; - client.websocket.send( - JSON.stringify({ - op: "trackStart", - guild: player.guildId, - data: currentData, - }) - ); + client.wsl.get(player.guildId)?.send({ + op: "trackStart", + guild: player.guildId, + data: currentData, + }); } } diff --git a/src/handlers/Player/ButtonCommands/Loop.ts b/src/handlers/Player/ButtonCommands/Loop.ts index 5a69062c..b68c3190 100644 --- a/src/handlers/Player/ButtonCommands/Loop.ts +++ b/src/handlers/Player/ButtonCommands/Loop.ts @@ -35,14 +35,11 @@ export class ButtonLoop { embeds: [looptrack], }); - if (this.client.websocket) - this.client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: this.interaction.guild!.id, - mode: "song", - }) - ); + this.client.wsl.get(this.interaction.guild!.id)?.send({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "song", + }); break; case "song": @@ -58,14 +55,11 @@ export class ButtonLoop { embeds: [loopall], }); - if (this.client.websocket) - this.client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: this.interaction.guild!.id, - mode: "queue", - }) - ); + this.client.wsl.get(this.interaction.guild!.id)?.send({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "queue", + }); break; case "queue": @@ -81,14 +75,11 @@ export class ButtonLoop { embeds: [unloopall], }); - if (this.client.websocket) - this.client.websocket.send( - JSON.stringify({ - op: "playerLoop", - guild: this.interaction.guild!.id, - mode: "none", - }) - ); + this.client.wsl.get(this.interaction.guild!.id)?.send({ + op: "playerLoop", + guild: this.interaction.guild!.id, + mode: "none", + }); break; } } diff --git a/src/handlers/Player/ButtonCommands/Previous.ts b/src/handlers/Player/ButtonCommands/Previous.ts index 9bc94df4..3d8843bd 100644 --- a/src/handlers/Player/ButtonCommands/Previous.ts +++ b/src/handlers/Player/ButtonCommands/Previous.ts @@ -57,7 +57,7 @@ export class ButtonPrevious { } else { this.player.previous(); - this.player.data.set("endMode", "previous") + this.player.data.set("endMode", "previous"); const embed = new EmbedBuilder() .setDescription(`${this.client.getString(this.language, "button.music", "previous_msg")}`) diff --git a/src/manager.ts b/src/manager.ts index 7be1a772..79a2143c 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -75,7 +75,7 @@ export class Manager extends Client { plButton: Collection; leaveDelay: Collection; nowPlaying: Collection; - websocket?: WebSocket; + wsl: Collection) => void }>; UpdateMusic!: (player: RainlinkPlayer) => Promise>; UpdateQueueMsg!: (player: RainlinkPlayer) => Promise>; enSwitch!: ActionRowBuilder; @@ -139,6 +139,7 @@ export class Manager extends Client { this.plButton = new Collection(); this.leaveDelay = new Collection(); this.nowPlaying = new Collection(); + this.wsl = new Collection) => void }>(); this.isDatabaseConnected = false; // Sharing diff --git a/src/web/websocket.ts b/src/web/websocket.ts index 6ccf6e12..a85de92c 100644 --- a/src/web/websocket.ts +++ b/src/web/websocket.ts @@ -12,8 +12,8 @@ export class WebsocketRoute { this.client.logger.websocket(import.meta.url, `Closed with code: ${code}, reason: ${reason}`); }); if (!this.checker(socket, req)) return; - this.client.websocket = socket; - this.client.logger.websocket(import.meta.url, `Websocket opened for ${req.headers["guild-id"]}`); + this.client.wsl.set(String(req.headers["guild-id"]), { send: (data) => socket.send(JSON.stringify(data)) }); + this.client.logger.websocket(import.meta.url, `Websocket opened for guildId: ${req.headers["guild-id"]}`); }); } From 348da4c9a50604ebd2459c9a2765b67eae63750f Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 12:31:36 +0700 Subject: [PATCH 16/21] add: Drestroy connection if already exist --- src/web/websocket.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/web/websocket.ts b/src/web/websocket.ts index a85de92c..6bec5969 100644 --- a/src/web/websocket.ts +++ b/src/web/websocket.ts @@ -33,7 +33,11 @@ export class WebsocketRoute { socket.close(1000, JSON.stringify({ error: "Authorization failed" })); return false; } - + if (this.client.wsl.get(String(req.headers["guild-id"]))) { + socket.send(JSON.stringify({ error: "Alreary hae connection on this guild" })); + socket.close(1000, JSON.stringify({ error: "Alreary hae connection on this guild" })); + return false; + } return true; } } From c9652f3b8ff65e865b47794c4c17cc4308267b20 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 12:33:11 +0700 Subject: [PATCH 17/21] mode: 404 to 400 --- src/web/route/getCurrentLoop.ts | 2 +- src/web/route/getCurrentPaused.ts | 2 +- src/web/route/getCurrentPosition.ts | 2 +- src/web/route/getCurrentTrackStatus.ts | 2 +- src/web/route/getQueueStatus.ts | 2 +- src/web/route/getSearch.ts | 2 +- src/web/route/getStatus.ts | 2 +- src/web/server.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/web/route/getCurrentLoop.ts b/src/web/route/getCurrentLoop.ts index 87be66c4..534c800f 100644 --- a/src/web/route/getCurrentLoop.ts +++ b/src/web/route/getCurrentLoop.ts @@ -10,7 +10,7 @@ export async function getCurrentLoop(client: Manager, req: Fastify.FastifyReques const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/route/getCurrentPaused.ts b/src/web/route/getCurrentPaused.ts index d03c0567..212603d9 100644 --- a/src/web/route/getCurrentPaused.ts +++ b/src/web/route/getCurrentPaused.ts @@ -10,7 +10,7 @@ export async function getCurrentPaused(client: Manager, req: Fastify.FastifyRequ const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/route/getCurrentPosition.ts b/src/web/route/getCurrentPosition.ts index 651155c0..281f5e99 100644 --- a/src/web/route/getCurrentPosition.ts +++ b/src/web/route/getCurrentPosition.ts @@ -10,7 +10,7 @@ export async function getCurrentPosition(client: Manager, req: Fastify.FastifyRe const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/route/getCurrentTrackStatus.ts b/src/web/route/getCurrentTrackStatus.ts index 8acc95c2..392d4a30 100644 --- a/src/web/route/getCurrentTrackStatus.ts +++ b/src/web/route/getCurrentTrackStatus.ts @@ -11,7 +11,7 @@ export async function getCurrentTrackStatus(client: Manager, req: Fastify.Fastif const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/route/getQueueStatus.ts b/src/web/route/getQueueStatus.ts index 28233c14..e0bb17b7 100644 --- a/src/web/route/getQueueStatus.ts +++ b/src/web/route/getQueueStatus.ts @@ -11,7 +11,7 @@ export async function getQueueStatus(client: Manager, req: Fastify.FastifyReques const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts index bbf102f1..94cc4ff6 100644 --- a/src/web/route/getSearch.ts +++ b/src/web/route/getSearch.ts @@ -19,7 +19,7 @@ export async function getSearch(client: Manager, req: Fastify.FastifyRequest, re } const user = await client.users.fetch(requester).catch(() => undefined); if (!query) { - res.code(404); + res.code(400); res.send({ error: "Search param not found" }); return; } diff --git a/src/web/route/getStatus.ts b/src/web/route/getStatus.ts index f1277fb2..8cb200d9 100644 --- a/src/web/route/getStatus.ts +++ b/src/web/route/getStatus.ts @@ -12,7 +12,7 @@ export async function getStatus(client: Manager, req: Fastify.FastifyRequest, re const guildId = (req.params as Record)["guildId"]; const player = client.rainlink.players.get(guildId); if (!player) { - res.code(404); + res.code(400); res.send({ error: "Current player not found!" }); return; } diff --git a/src/web/server.ts b/src/web/server.ts index 05b4eb8d..e3301147 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -17,7 +17,7 @@ export class WebServer { (fastify, _, done) => { fastify.addHook("preValidation", function hook(req, reply, done) { if (!req.headers["authorization"]) { - reply.code(401); + reply.code(400); reply.send(JSON.stringify({ error: "Missing Authorization" })); return done(); } From 6fd1c369e7097f5e9817c29a620f258f4889bf63 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 17:18:50 +0700 Subject: [PATCH 18/21] add: full source support for autoplay, move search to queueEmpty --- src/commands/Music/Autoplay.ts | 38 +++++++++++++-------------------- src/events/player/queueEmpty.ts | 14 +++++++++--- src/manifest.xml | 2 +- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/commands/Music/Autoplay.ts b/src/commands/Music/Autoplay.ts index ba4d1397..426fc2e3 100644 --- a/src/commands/Music/Autoplay.ts +++ b/src/commands/Music/Autoplay.ts @@ -2,6 +2,7 @@ import { EmbedBuilder } from "discord.js"; import { Manager } from "../../manager.js"; import { Accessableby, Command } from "../../structures/Command.js"; import { CommandHandler } from "../../structures/CommandHandler.js"; +import { RainlinkPlayer } from "../../rainlink/main.js"; // Main code export default class implements Command { @@ -21,13 +22,13 @@ export default class implements Command { public async execute(client: Manager, handler: CommandHandler) { await handler.deferReply(); - const player = client.rainlink.players.get(handler.guild!.id); + const player = client.rainlink.players.get(handler.guild!.id) as RainlinkPlayer; - if (player!.data.get("autoplay") === true) { - player!.data.set("autoplay", false); - player!.data.set("identifier", null); - player!.data.set("requester", null); - player!.queue.clear(); + if (player.data.get("autoplay") === true) { + player.data.set("autoplay", false); + player.data.set("identifier", null); + player.data.set("requester", null); + player.queue.clear(); const off = new EmbedBuilder() .setDescription( @@ -39,23 +40,14 @@ export default class implements Command { await handler.editReply({ content: " ", embeds: [off] }); } else { - const identifier = player!.queue.current!.identifier; - const search = `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`; - const res = await player!.search(search, { requester: handler.user }); - - const finalRes = res.tracks.filter( - (track) => - !player!.queue.some((s) => s.encoded === track.encoded) && - !player!.queue.previous.some((s) => s.encoded === track.encoded) - ); - - player!.data.set("autoplay", true); - - player!.data.set("identifier", identifier); - - player!.data.set("requester", handler.user); - - player!.queue.add(finalRes[1]); + const identifier = player.queue.current!.identifier; + + player.data.set("autoplay", true); + player.data.set("identifier", identifier); + player.data.set("requester", handler.user); + player.data.set("source", player.queue.current?.source); + player.data.set("author", player.queue.current?.author); + player.data.set("title", player.queue.current?.title); const on = new EmbedBuilder() .setDescription( diff --git a/src/events/player/queueEmpty.ts b/src/events/player/queueEmpty.ts index b0464998..d809fc18 100644 --- a/src/events/player/queueEmpty.ts +++ b/src/events/player/queueEmpty.ts @@ -19,8 +19,17 @@ export default class { const guild = await client.guilds.fetch(player.guildId).catch(() => undefined); if (player.data.get("autoplay") === true) { + const author = player.data.get("author"); + const title = player.data.get("title"); const requester = player.data.get("requester"); - const identifier = player.data.get("identifier"); + let identifier = player.data.get("identifier"); + const source = String(player.data.get("source")); + if (source.toLowerCase() !== "youtube") { + const findQuery = "directSearch=ytsearch:" + [author, title].filter((x) => !!x).join(" - "); + const preRes = await player.search(findQuery, { requester: requester }); + if (preRes.tracks.length !== 0) true; + else identifier = preRes.tracks[0].identifier; + } const search = `https://www.youtube.com/watch?v=${identifier}&list=RD${identifier}`; let res = await player.search(search, { requester: requester }); const finalRes = res.tracks.filter((track) => { @@ -29,8 +38,7 @@ export default class { return req1 && req2; }); if (finalRes.length !== 0) { - player.queue.add(finalRes.length <= 1 ? finalRes[0] : finalRes[1]); - player.play(); + player.play(finalRes.length <= 1 ? finalRes[0] : finalRes[1]); const channel = (await client.channels.fetch(player.textId).catch(() => undefined)) as TextChannel; if (channel) return new ClearMessageService(client, channel, player); return; diff --git a/src/manifest.xml b/src/manifest.xml index 8ce116a2..c851a00b 100644 --- a/src/manifest.xml +++ b/src/manifest.xml @@ -7,7 +7,7 @@ - 5.1.6 + 5.2.0 5.1.0 vocaloid_kaito From 997651f2f036f29fa042af1fed3ae00ce763054c Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 17:32:17 +0700 Subject: [PATCH 19/21] finish: ws event for dashboard --- src/commands/Music/Skipto.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/commands/Music/Skipto.ts b/src/commands/Music/Skipto.ts index 367a10b6..1444431b 100644 --- a/src/commands/Music/Skipto.ts +++ b/src/commands/Music/Skipto.ts @@ -1,4 +1,4 @@ -import { ApplicationCommandOptionType, EmbedBuilder } from "discord.js"; +import { ApplicationCommandOptionType, EmbedBuilder, User } from "discord.js"; import { Manager } from "../../manager.js"; import { Accessableby, Command } from "../../structures/Command.js"; import { CommandHandler } from "../../structures/CommandHandler.js"; @@ -54,6 +54,28 @@ export default class implements Command { player.queue.current ? player.queue.previous.unshift(player.queue.current) : true; await player.play(nowCurrentTrack); player.queue.shift(); + client.wsl.get(handler.guild!.id)?.send({ + op: "playerQueueSkip", + guild: handler.guild!.id, + queue: player.queue.map((track) => { + const requesterQueue = track.requester as User; + return { + title: track.title, + uri: track.uri, + length: track.duration, + thumbnail: track.artworkUrl, + author: track.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + }; + }), + }); const skipped = new EmbedBuilder() .setDescription(`${client.getString(handler.language, "command.music", "skip_msg")}`) .setColor(client.color); From dd3c8602fb3a98c9007d060ab71dcaa028da7f36 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 19:35:11 +0700 Subject: [PATCH 20/21] add: requester field on trackEnd, trackStart ws event --- src/commands/Music/Skipto.ts | 2 +- src/events/websocket/trackEnd.ts | 15 +++++++++++---- src/events/websocket/trackStart.ts | 11 ++++++++++- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/commands/Music/Skipto.ts b/src/commands/Music/Skipto.ts index 1444431b..e980e13a 100644 --- a/src/commands/Music/Skipto.ts +++ b/src/commands/Music/Skipto.ts @@ -75,7 +75,7 @@ export default class implements Command { : null, }; }), - }); + }); const skipped = new EmbedBuilder() .setDescription(`${client.getString(handler.language, "command.music", "skip_msg")}`) .setColor(client.color); diff --git a/src/events/websocket/trackEnd.ts b/src/events/websocket/trackEnd.ts index 829235fb..13723bb9 100644 --- a/src/events/websocket/trackEnd.ts +++ b/src/events/websocket/trackEnd.ts @@ -1,11 +1,11 @@ +import { User } from "discord.js"; import { Manager } from "../../manager.js"; import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { - const prevoiusIndex = player.queue.previous.length - 1; - - const song = player.queue.previous[prevoiusIndex === -1 ? 0 : prevoiusIndex]; + const song = player.queue.previous.at(-1); + const requesterQueue = song!.requester as User; const currentData = song ? { @@ -14,7 +14,14 @@ export default class { length: song.duration, thumbnail: song.artworkUrl, author: song.author, - requester: song.requester, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, } : null; diff --git a/src/events/websocket/trackStart.ts b/src/events/websocket/trackStart.ts index 96938ff7..25434cc0 100644 --- a/src/events/websocket/trackStart.ts +++ b/src/events/websocket/trackStart.ts @@ -1,9 +1,11 @@ +import { User } from "discord.js"; import { Manager } from "../../manager.js"; import { RainlinkPlayer } from "../../rainlink/main.js"; export default class { async execute(client: Manager, player: RainlinkPlayer) { const song = player.queue.current; + const requesterQueue = song!.requester as User; const currentData = { title: song!.title, @@ -11,7 +13,14 @@ export default class { length: song!.duration, thumbnail: song!.artworkUrl, author: song!.author, - requester: song!.requester, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, }; client.wsl.get(player.guildId)?.send({ From 5318617ca9eb871833720a7e0dce4ce78c652b25 Mon Sep 17 00:00:00 2001 From: RainyXeon / Date: Wed, 8 May 2024 21:05:07 +0700 Subject: [PATCH 21/21] move: status to data in /v1/players/:guildId/member/:userId --- src/web/route/getMemberStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/web/route/getMemberStatus.ts b/src/web/route/getMemberStatus.ts index 5f373653..655e7675 100644 --- a/src/web/route/getMemberStatus.ts +++ b/src/web/route/getMemberStatus.ts @@ -24,6 +24,6 @@ export async function getMemberStatus(client: Manager, req: Fastify.FastifyReque } const Member = await Guild.members.fetch(userId).catch(() => undefined); if (!(!Member || !Member.voice.channel || !Member.voice)) isMemeberInVoice = true; - res.send({ status: isMemeberInVoice }); + res.send({ data: isMemeberInVoice }); return; }