diff --git a/docs/README.md b/docs/README.md index edbe65e2..b60c61e4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -91,6 +91,7 @@ ![song_req](https://ucarecdn.com/25e8fc92-842d-40c2-a653-d1c0224804ae/Picsart_240825_081626013.jpg) ![playlist_info](https://ucarecdn.com/1f759973-8cc8-49c5-babb-0e60c297ab2e/Screenshot_2024_0825_075240.jpg) ![player](https://ucarecdn.com/2ef47700-0d6c-4114-86c6-6c98544aa116/Picsart_240825_082538385.jpg) + ## 📋 Requirements - ![Node.js](https://img.shields.io/badge/Node.js-026E00?style=for-the-badge) Node.js Version 18.0.0+ [Download](https://nodejs.org/en/download) diff --git a/docs/README_VI.md b/docs/README_VI.md index 2d53d685..2f482490 100644 --- a/docs/README_VI.md +++ b/docs/README_VI.md @@ -92,7 +92,6 @@ ![playlist_info](https://ucarecdn.com/1f759973-8cc8-49c5-babb-0e60c297ab2e/Screenshot_2024_0825_075240.jpg) ![player](https://ucarecdn.com/2ef47700-0d6c-4114-86c6-6c98544aa116/Picsart_240825_082538385.jpg) - ## 📋 Yêu cầu - ![Node.js](https://img.shields.io/badge/Node.js-026E00?style=for-the-badge) Node.js phiên bản 18.0.0+ [Download](https://nodejs.org/en/download) diff --git a/docs/README_hi.md b/docs/README_hi.md index 5114701a..bdf3f716 100644 --- a/docs/README_hi.md +++ b/docs/README_hi.md @@ -39,28 +39,28 @@ ## 🎶 समर्थित स्रोत -| संगीत स्रोत | लावालिंक प्लगइन के बिना | लावालिंक प्लगइन के साथ | -| :------------------------------: | :---------------------: | :--------------------: | -| यूट्यूब | ✅ | ✅ | -| साउंडक्लाउड | ✅ | ✅ | -| (LS) स्पॉटिफाई | ⚠️ | ✅ | -| HTTP | ✅ | ✅ | -| (LS) डीज़र | ⚠️ | ✅ | -| ट्विच | ✅ | ✅ | -| बैंडकैम्प | ✅ | ✅ | -| निकोवीडियो | ⚠️ | ⚠️ | -| (LS) एप्पल म्यूजिक | ⚠️ | ✅ | -| (LS) यांडेक्स म्यूजिक | ❌ | ✅ | -| (LS) फ्लोवेरी टीटीएस | ❌ | ✅ | -| (DB) मिक्सक्लाउड | ❌ | ✅ | -| (DB) OC रीमिक्स | ❌ | ✅ | -| (DB) क्लीप.इट | ❌ | ✅ | -| (DB) रेडिट | ❌ | ✅ | -| (DB) गेटयार्न | ❌ | ✅ | -| (DB) टेक्स्ट टू स्पीच | ❌ | ✅ | -| (DB) टिक्टोक (बीटा) | ❌ | ✅ | -| (DB) पी\*\*nhub (अनुशंसित नहीं) | ❌ | ✅ | -| (DB) साउंडगैसम | ❌ | ✅ | +| संगीत स्रोत | लावालिंक प्लगइन के बिना | लावालिंक प्लगइन के साथ | +| :-----------------------------: | :---------------------: | :--------------------: | +| यूट्यूब | ✅ | ✅ | +| साउंडक्लाउड | ✅ | ✅ | +| (LS) स्पॉटिफाई | ⚠️ | ✅ | +| HTTP | ✅ | ✅ | +| (LS) डीज़र | ⚠️ | ✅ | +| ट्विच | ✅ | ✅ | +| बैंडकैम्प | ✅ | ✅ | +| निकोवीडियो | ⚠️ | ⚠️ | +| (LS) एप्पल म्यूजिक | ⚠️ | ✅ | +| (LS) यांडेक्स म्यूजिक | ❌ | ✅ | +| (LS) फ्लोवेरी टीटीएस | ❌ | ✅ | +| (DB) मिक्सक्लाउड | ❌ | ✅ | +| (DB) OC रीमिक्स | ❌ | ✅ | +| (DB) क्लीप.इट | ❌ | ✅ | +| (DB) रेडिट | ❌ | ✅ | +| (DB) गेटयार्न | ❌ | ✅ | +| (DB) टेक्स्ट टू स्पीच | ❌ | ✅ | +| (DB) टिक्टोक (बीटा) | ❌ | ✅ | +| (DB) पी\*\*nhub (अनुशंसित नहीं) | ❌ | ✅ | +| (DB) साउंडगैसम | ❌ | ✅ | - ✅ **डिफ़ॉल्ट लावालिंक कॉन्फ़िगरेशन के साथ पूर्ण समर्थन** - ⚠️ **समर्थित है लेकिन केवल यूट्यूब या साउंडक्लाउड से प्राप्त होता है** @@ -77,11 +77,11 @@ ## 🔉 समर्थित लावालिंक/नोडलिंक संस्करण -| प्रकार | समर्थित संस्करण | ड्राइवर का नाम | -| -------- | ---------------- | ----------------- | -| लावालिंक | v4.0.0 - v4.x.x | lavalink/v4/koinu | -| लावालिंक | v3.0.0 - v3.7.x | lavalink/v3/koto | -| नोडलिंक | v2.0.0 - v2.x.x | nodelink/v2/nari | +| प्रकार | समर्थित संस्करण | ड्राइवर का नाम | +| -------- | --------------- | ----------------- | +| लावालिंक | v4.0.0 - v4.x.x | lavalink/v4/koinu | +| लावालिंक | v3.0.0 - v3.7.x | lavalink/v3/koto | +| नोडलिंक | v2.0.0 - v2.x.x | nodelink/v2/nari | ## 🖼️ शोकेस diff --git a/docs/README_pt-BR.md b/docs/README_pt-BR.md index 878771f9..ac4b147b 100644 --- a/docs/README_pt-BR.md +++ b/docs/README_pt-BR.md @@ -92,7 +92,6 @@ ![playlist_info](https://ucarecdn.com/1f759973-8cc8-49c5-babb-0e60c297ab2e/Screenshot_2024_0825_075240.jpg) ![player](https://ucarecdn.com/2ef47700-0d6c-4114-86c6-6c98544aa116/Picsart_240825_082538385.jpg) - ## 📋 Requisitos - ![Node.js](https://img.shields.io/badge/Node.js-026E00?style=for-the-badge) Versão do Node.js 18.0.0+ [Download](https://nodejs.org/en/download) diff --git a/package.json b/package.json index 4a54ec0a..3d4aa3b3 100644 --- a/package.json +++ b/package.json @@ -48,29 +48,35 @@ "chalk": "^5.3.0", "chillout": "^5.0.0", "common-tags": "^1.8.2", - "discord.js": "^14.14.1", + "discord.js": "^14.16.1", "dreamvast.quick.db": "10.0.0-unsupported", - "fast-xml-parser": "^4.3.6", - "fastify": "^4.26.2", - "humanize-duration": "^3.31.0", + "fast-xml-parser": "^4.5.0", + "fastify": "^4.28.1", + "humanize-duration": "^3.32.1", "js-yaml": "^4.1.0", "lodash": "^4.17.21", "markdown-it": "^14.1.0", - "mongoose": "^8.2.3", - "mysql2": "^3.9.2", + "mongoose": "^8.6.1", + "mysql2": "^3.11.0", "node-cron": "^3.0.3", - "node-html-parser": "^6.1.12", - "nodemon": "^3.1.0", + "node-html-parser": "^6.1.13", + "nodemon": "^3.1.4", "os": "^0.1.2", - "pg": "^8.11.3", + "pg": "^8.12.0", + "pidusage": "^3.0.2", "plsargs": "^0.1.6", - "pm2": "^5.3.1", - "pretty-ms": "^9.0.0", + "pm2": "^5.4.2", + "pretty-ms": "^9.1.0", + "rainlink": "^1.0.7", + "rainlink-apple": "^1.0.5", + "rainlink-deezer": "^1.0.8", + "rainlink-nico": "^1.0.7", + "rainlink-spotify": "^1.0.5", "recursive-readdir": "^2.2.3", - "stuffs": "^0.1.37", - "undici": "^6.10.1", + "stuffs": "^0.1.40", + "undici": "^6.19.8", "voucher-code-generator": "^1.3.0", - "winston": "^3.12.0", + "winston": "^3.14.2", "write-file-atomic": "^5.0.1" }, "devDependencies": { @@ -78,21 +84,22 @@ "@types/common-tags": "^1.8.4", "@types/fs-extra": "^11.0.4", "@types/js-yaml": "^4.0.9", - "@types/lodash": "^4.17.0", - "@types/markdown-it": "^13.0.7", - "@types/node": "^20.11.30", + "@types/lodash": "^4.17.7", + "@types/markdown-it": "^13.0.9", + "@types/node": "^20.16.4", "@types/node-cron": "^3.0.11", "@types/node-fetch": "^2.6.11", + "@types/pidusage": "^2.0.5", "@types/recursive-readdir": "^2.2.4", "@types/voucher-code-generator": "^1.1.3", "copy-dir": "^1.3.0", "dir-archiver": "^2.1.0", "dotenv": "^16.4.5", "fs-extra": "^11.2.0", - "prettier": "^3.2.5", - "tsx": "^4.7.1", - "typescript": "^5.4.3", - "undici-types": "^6.10.0" + "prettier": "^3.3.3", + "tsx": "^4.19.0", + "typescript": "^5.5.4", + "undici-types": "^6.19.8" }, "pnpm": { "overrides": { diff --git a/src/@types/Button.ts b/src/@types/Button.ts index 8358f9d3..763b6a9d 100644 --- a/src/@types/Button.ts +++ b/src/@types/Button.ts @@ -1,6 +1,6 @@ -import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'discord.js' +import { ButtonInteraction, InteractionCollector, Message } from 'discord.js' import { Manager } from '../manager.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class PlayerButton { name: string = '' diff --git a/src/@types/Cluster.ts b/src/@types/Cluster.ts new file mode 100644 index 00000000..2d0c8114 --- /dev/null +++ b/src/@types/Cluster.ts @@ -0,0 +1,30 @@ +import { Worker } from 'node:cluster' +import { ClusterManager } from '../cluster/core.js' + +export interface ClusterManagerOptions { + shardsPerClusters: number + totalClusters: number +} + +export interface WorkerMessage { + cmd: string + args: Record +} + +export interface WorkerResponse { + response: unknown +} + +export abstract class ClusterCommand { + public get name(): string { + throw new Error(`This command doesn't have name`) + } + + public async execute( + manager: ClusterManager, + worker: Worker, + message: WorkerMessage + ): Promise { + throw new Error(`This command doesn't have execute function`) + } +} diff --git a/src/@types/Config.ts b/src/@types/Config.ts index 4e62b2d0..97f8bc9c 100644 --- a/src/@types/Config.ts +++ b/src/@types/Config.ts @@ -1,5 +1,5 @@ import { ClusterManagerOptions } from '../cluster/core.js' -import { RainlinkNodeOptions } from '../rainlink/main.js' +import { RainlinkNodeOptions } from 'rainlink' export interface Config { bot: Bot diff --git a/src/autofix/CheckLavalinkServer.ts b/src/autofix/CheckLavalinkServer.ts index 387b2ea3..0100522b 100644 --- a/src/autofix/CheckLavalinkServer.ts +++ b/src/autofix/CheckLavalinkServer.ts @@ -1,7 +1,7 @@ import { Manager } from '../manager.js' import { Headers } from '../@types/Lavalink.js' import { GetLavalinkServer } from './GetLavalinkServer.js' -import { RainlinkWebsocket } from '../rainlink/main.js' +import { RainlinkWebsocket } from 'rainlink' export class CheckLavalinkServer { client: Manager diff --git a/src/buttons/Clear.ts b/src/buttons/Clear.ts index 688993ea..6dbc889f 100644 --- a/src/buttons/Clear.ts +++ b/src/buttons/Clear.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'clear' diff --git a/src/buttons/Loop.ts b/src/buttons/Loop.ts index 865d13b4..c1d7ad46 100644 --- a/src/buttons/Loop.ts +++ b/src/buttons/Loop.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkLoopMode, RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkLoopMode, RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'loop' diff --git a/src/buttons/Pause.ts b/src/buttons/Pause.ts index 485733d7..fa2bb57d 100644 --- a/src/buttons/Pause.ts +++ b/src/buttons/Pause.ts @@ -8,7 +8,7 @@ import { playerRowTwo, } from '../utilities/PlayerControlButton.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'pause' diff --git a/src/buttons/Previous.ts b/src/buttons/Previous.ts index eb663ddb..826e5b65 100644 --- a/src/buttons/Previous.ts +++ b/src/buttons/Previous.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'replay' diff --git a/src/buttons/Queue.ts b/src/buttons/Queue.ts index 3fe8df4c..82a4d2ca 100644 --- a/src/buttons/Queue.ts +++ b/src/buttons/Queue.ts @@ -8,7 +8,7 @@ import { import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { formatDuration } from '../utilities/FormatDuration.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../utilities/GetTitle.js' export default class implements PlayerButton { diff --git a/src/buttons/Shuffle.ts b/src/buttons/Shuffle.ts index 193eedb7..da2c610b 100644 --- a/src/buttons/Shuffle.ts +++ b/src/buttons/Shuffle.ts @@ -10,7 +10,7 @@ import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { formatDuration } from '../utilities/FormatDuration.js' import { PageQueue } from '../structures/PageQueue.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../utilities/GetTitle.js' export default class implements PlayerButton { diff --git a/src/buttons/Skip.ts b/src/buttons/Skip.ts index 79395e28..cf1e9463 100644 --- a/src/buttons/Skip.ts +++ b/src/buttons/Skip.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'skip' diff --git a/src/buttons/Stop.ts b/src/buttons/Stop.ts index d3343c8a..aec26013 100644 --- a/src/buttons/Stop.ts +++ b/src/buttons/Stop.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'stop' diff --git a/src/buttons/VolumeDown.ts b/src/buttons/VolumeDown.ts index 58644dba..5c9bbca7 100644 --- a/src/buttons/VolumeDown.ts +++ b/src/buttons/VolumeDown.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'voldown' diff --git a/src/buttons/VolumeUp.ts b/src/buttons/VolumeUp.ts index 6d69fb80..061c8960 100644 --- a/src/buttons/VolumeUp.ts +++ b/src/buttons/VolumeUp.ts @@ -2,7 +2,7 @@ import { ButtonInteraction, CacheType, InteractionCollector, Message } from 'dis import { PlayerButton } from '../@types/Button.js' import { Manager } from '../manager.js' import { ReplyInteractionService } from '../services/ReplyInteractionService.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements PlayerButton { name = 'volup' diff --git a/src/cluster/commands/getAllWWorkerPID.ts b/src/cluster/commands/getAllWWorkerPID.ts new file mode 100644 index 00000000..b6f60635 --- /dev/null +++ b/src/cluster/commands/getAllWWorkerPID.ts @@ -0,0 +1,19 @@ +import { ClusterCommand, WorkerMessage, WorkerResponse } from '../../@types/Cluster.js' +import { ClusterManager } from '../core.js' +import { Worker } from 'node:cluster' + +export default class extends ClusterCommand { + public get name(): string { + return 'all_worker_pid' + } + + public async execute( + manager: ClusterManager, + worker: Worker, + message: WorkerMessage + ): Promise { + return { + response: manager.workerPID.full.map((value) => value[1].process.pid), + } + } +} diff --git a/src/cluster/commands/getAllWorkerID.ts b/src/cluster/commands/getAllWorkerID.ts new file mode 100644 index 00000000..17c4fc70 --- /dev/null +++ b/src/cluster/commands/getAllWorkerID.ts @@ -0,0 +1,19 @@ +import { ClusterCommand, WorkerMessage, WorkerResponse } from '../../@types/Cluster.js' +import { ClusterManager } from '../core.js' +import { Worker } from 'node:cluster' + +export default class extends ClusterCommand { + public get name(): string { + return 'all_worker_id' + } + + public async execute( + manager: ClusterManager, + worker: Worker, + message: WorkerMessage + ): Promise { + return { + response: manager.workerPID.full.map((value) => value[0]), + } + } +} diff --git a/src/cluster/core.ts b/src/cluster/core.ts index d1048714..fe560c40 100644 --- a/src/cluster/core.ts +++ b/src/cluster/core.ts @@ -1,7 +1,16 @@ -import cluster from 'node:cluster' +import cluster, { Worker } from 'node:cluster' import process from 'node:process' import { config } from 'dotenv' import { bootBot } from './bot.js' +import pidusage, { Status } from 'pidusage' +import { Collection } from '../structures/Collection.js' +import chillout from 'chillout' +import readdirRecursive from 'recursive-readdir' +import { resolve } from 'path' +import { join, dirname } from 'path' +import { fileURLToPath, pathToFileURL } from 'url' +import { ClusterCommand, WorkerResponse } from '../@types/Cluster.js' +const __dirname = dirname(fileURLToPath(import.meta.url)) config() export interface ClusterManagerOptions { @@ -10,19 +19,44 @@ export interface ClusterManagerOptions { } export class ClusterManager { + public readonly workerPID: Collection = new Collection() + public readonly commands: Collection = new Collection() public readonly clusterShardList: Record = {} public readonly totalShards: number = 0 + public customData?: { + id: number + shard: number[] + shardCount: number + } constructor(public readonly options: ClusterManagerOptions) { this.totalShards = this.options.totalClusters * this.options.shardsPerClusters const shardArrayID = this.arrayRange(0, this.totalShards - 1, 1) this.arrayChunk(shardArrayID, this.options.shardsPerClusters).map((value, index) => { this.clusterShardList[String(index + 1)] = value }) + console.log(this.options.totalClusters) } public async start() { if (cluster.isPrimary) { this.log('INFO', `Primary process ${process.pid} is running`) + + await this.commandLoader() + + cluster.on('exit', (worker) => { + this.log('WARN', `worker ${worker.process.pid} / ${worker.id} died x.x`) + }) + cluster.on('message', async (worker, message) => { + const jsonMsg = JSON.parse(message) + const command = this.commands.get(jsonMsg.cmd) + if (!command) + return worker.send( + JSON.stringify({ error: { code: 404, message: 'Command not found!' } }) + ) + const getRes = await command.execute(this, worker, message) + worker.send(JSON.stringify(getRes)) + }) + for (let i = 0; i < this.options.totalClusters; i++) { cluster.fork() } @@ -37,10 +71,43 @@ export class ClusterManager { } } + public getWorkerInfo(clusterId: number) { + return this.workerPID.get(String(clusterId)) + } + + public async getWorkerStatus(clusterId: number): Promise { + const workerData = this.workerPID.get(String(clusterId)) + if (!workerData) return null + return new Promise((resolve, reject) => + pidusage(workerData.process.pid, function (err, stats) { + if (err) reject(null) + resolve(stats) + }) + ) + } + public getShard(clusterId: number) { return this.clusterShardList[String(clusterId)] } + public async sendMaster( + cmd: string, + args: Record = {} + ): Promise { + return new Promise((resolve, reject) => { + const fullData = { cmd, args } + cluster.worker.on('message', (message) => { + const jsonMsg = JSON.parse(message) + if (jsonMsg.err) return reject(null) + resolve(message) + }) + cluster.worker.on('error', () => { + return reject(null) + }) + cluster.worker.send(JSON.stringify(fullData)) + }) + } + protected arrayRange(start: number, stop: number, step: number) { return Array.from({ length: (stop - start) / step + 1 }, (_, index) => start + index * step) } @@ -54,10 +121,29 @@ export class ClusterManager { ) } - protected log(level: string, msg: string, pad: number = 9) { + public log(level: string, msg: string, pad: number = 9) { const date = new Date(Date.now()).toISOString() const prettyLevel = level.toUpperCase().padEnd(pad) const prettyClass = 'ClusterManager'.padEnd(28) console.log(`${date} - ${prettyLevel} - ${prettyClass} - ${msg}`) } + + protected async commandLoader() { + let eventsPath = resolve(join(__dirname, 'commands')) + let eventsFile = await readdirRecursive(eventsPath) + await chillout.forEach(eventsFile, async (path) => await this.registerCommand(path)) + this.log('INFO', `Cluster command loaded successfully`) + } + + protected async registerCommand(path: string) { + const command = new (await import(pathToFileURL(path).toString())).default() as ClusterCommand + + if (!command.execute) + return this.log( + 'WARN', + `Clister command [${command.name}] doesn't have exeture function on the class, Skipping...` + ) + + this.commands.set(command.name, command) + } } diff --git a/src/commands/Filter/Filter.ts b/src/commands/Filter/Filter.ts index 8e1c0148..b5be59d3 100644 --- a/src/commands/Filter/Filter.ts +++ b/src/commands/Filter/Filter.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, EmbedBuilder } from 'discord.js' import { Manager } from '../../manager.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkFilterData, RainlinkFilterMode } from '../../rainlink/main.js' +import { RainlinkFilterData, RainlinkFilterMode } from 'rainlink' export default class implements Command { public name = ['filter'] diff --git a/src/commands/Music/Autoplay.ts b/src/commands/Music/Autoplay.ts index b8d72b59..a1e8ee2e 100644 --- a/src/commands/Music/Autoplay.ts +++ b/src/commands/Music/Autoplay.ts @@ -2,7 +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' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/ClearQueue.ts b/src/commands/Music/ClearQueue.ts index 3077fff5..78ebd708 100644 --- a/src/commands/Music/ClearQueue.ts +++ b/src/commands/Music/ClearQueue.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Forward.ts b/src/commands/Music/Forward.ts index 328de872..635692fc 100644 --- a/src/commands/Music/Forward.ts +++ b/src/commands/Music/Forward.ts @@ -3,7 +3,7 @@ import { formatDuration } from '../../utilities/FormatDuration.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' +import { RainlinkPlayer } from 'rainlink' const fastForwardNum = 10 // Main code diff --git a/src/commands/Music/Insert.ts b/src/commands/Music/Insert.ts index 5a8d65e7..363068d1 100644 --- a/src/commands/Music/Insert.ts +++ b/src/commands/Music/Insert.ts @@ -9,7 +9,7 @@ import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' import { convertTime } from '../../utilities/ConvertTime.js' import { AutocompleteInteractionChoices, GlobalInteraction } from '../../@types/Interaction.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' // Main code diff --git a/src/commands/Music/Loop.ts b/src/commands/Music/Loop.ts index 20630e57..c21a62ba 100644 --- a/src/commands/Music/Loop.ts +++ b/src/commands/Music/Loop.ts @@ -3,7 +3,7 @@ import { Manager } from '../../manager.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkLoopMode, RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkLoopMode, RainlinkPlayer } from 'rainlink' export default class implements Command { public name = ['loop'] diff --git a/src/commands/Music/Nowplaying.ts b/src/commands/Music/Nowplaying.ts index cfadb660..23429a74 100644 --- a/src/commands/Music/Nowplaying.ts +++ b/src/commands/Music/Nowplaying.ts @@ -3,7 +3,7 @@ import { EmbedBuilder, TextChannel } from 'discord.js' import { formatDuration } from '../../utilities/FormatDuration.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' // Main code diff --git a/src/commands/Music/Pause.ts b/src/commands/Music/Pause.ts index 515a441b..9d70175a 100644 --- a/src/commands/Music/Pause.ts +++ b/src/commands/Music/Pause.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class implements Command { public name = ['pause'] diff --git a/src/commands/Music/Play.ts b/src/commands/Music/Play.ts index 68b3abe5..153d644c 100644 --- a/src/commands/Music/Play.ts +++ b/src/commands/Music/Play.ts @@ -9,7 +9,7 @@ import { Manager } from '../../manager.js' import { Accessableby, Command } from '../../structures/Command.js' import { AutocompleteInteractionChoices, GlobalInteraction } from '../../@types/Interaction.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer, RainlinkSearchResultType, RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkSearchResultType, RainlinkTrack } from 'rainlink' export default class implements Command { public name = ['play'] diff --git a/src/commands/Music/Previous.ts b/src/commands/Music/Previous.ts index 2ee9103c..fa63ce38 100644 --- a/src/commands/Music/Previous.ts +++ b/src/commands/Music/Previous.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, Message } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Queue.ts b/src/commands/Music/Queue.ts index bb8b1c11..0c26807a 100644 --- a/src/commands/Music/Queue.ts +++ b/src/commands/Music/Queue.ts @@ -4,7 +4,7 @@ import { PageQueue } from '../../structures/PageQueue.js' import { Manager } from '../../manager.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer, RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkTrack } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' // Main code diff --git a/src/commands/Music/Remove.ts b/src/commands/Music/Remove.ts index ff798a72..000fdcc4 100644 --- a/src/commands/Music/Remove.ts +++ b/src/commands/Music/Remove.ts @@ -4,7 +4,7 @@ import { convertTime } from '../../utilities/ConvertTime.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' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' // Main code diff --git a/src/commands/Music/Replay.ts b/src/commands/Music/Replay.ts index 30d16253..d49aa363 100644 --- a/src/commands/Music/Replay.ts +++ b/src/commands/Music/Replay.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, Message } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Resume.ts b/src/commands/Music/Resume.ts index 80f67ebf..90f2ce0b 100644 --- a/src/commands/Music/Resume.ts +++ b/src/commands/Music/Resume.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, Message } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Rewind.ts b/src/commands/Music/Rewind.ts index 001c22db..d70c5514 100644 --- a/src/commands/Music/Rewind.ts +++ b/src/commands/Music/Rewind.ts @@ -3,7 +3,7 @@ import { formatDuration } from '../../utilities/FormatDuration.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' +import { RainlinkPlayer } from 'rainlink' const rewindNum = 10 // Main code diff --git a/src/commands/Music/Seek.ts b/src/commands/Music/Seek.ts index 78cd708c..e6f20571 100644 --- a/src/commands/Music/Seek.ts +++ b/src/commands/Music/Seek.ts @@ -3,7 +3,7 @@ import { formatDuration } from '../../utilities/FormatDuration.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' +import { RainlinkPlayer } from 'rainlink' const time_regex = /(^[0-9][\d]{0,3}):(0[0-9]{1}$|[1-5]{1}[0-9])/ // Main code diff --git a/src/commands/Music/Shuffle.ts b/src/commands/Music/Shuffle.ts index 427e4714..911bb044 100644 --- a/src/commands/Music/Shuffle.ts +++ b/src/commands/Music/Shuffle.ts @@ -4,7 +4,7 @@ import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' import { formatDuration } from '../../utilities/FormatDuration.js' import { PageQueue } from '../../structures/PageQueue.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' // Main code diff --git a/src/commands/Music/Skip.ts b/src/commands/Music/Skip.ts index b2eaf759..c5f79887 100644 --- a/src/commands/Music/Skip.ts +++ b/src/commands/Music/Skip.ts @@ -2,7 +2,7 @@ import { EmbedBuilder, Message } 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' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Skipto.ts b/src/commands/Music/Skipto.ts index 56d5057e..aa2fc494 100644 --- a/src/commands/Music/Skipto.ts +++ b/src/commands/Music/Skipto.ts @@ -2,7 +2,7 @@ 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' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Stop.ts b/src/commands/Music/Stop.ts index cd5679fd..82bbb82d 100644 --- a/src/commands/Music/Stop.ts +++ b/src/commands/Music/Stop.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder } from 'discord.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Music/Volume.ts b/src/commands/Music/Volume.ts index dced758b..bcca6a21 100644 --- a/src/commands/Music/Volume.ts +++ b/src/commands/Music/Volume.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, EmbedBuilder, Message } 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' +import { RainlinkPlayer } from 'rainlink' // Main code export default class implements Command { diff --git a/src/commands/Playlist/Add.ts b/src/commands/Playlist/Add.ts index ad63e3a9..1c8bb549 100644 --- a/src/commands/Playlist/Add.ts +++ b/src/commands/Playlist/Add.ts @@ -9,7 +9,7 @@ import { Manager } from '../../manager.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' import { AutocompleteInteractionChoices, GlobalInteraction } from '../../@types/Interaction.js' -import { RainlinkSearchResultType, RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkSearchResultType, RainlinkTrack } from 'rainlink' const TrackAdd: RainlinkTrack[] = [] diff --git a/src/commands/Playlist/SaveQueue.ts b/src/commands/Playlist/SaveQueue.ts index 97a14235..64e40a03 100644 --- a/src/commands/Playlist/SaveQueue.ts +++ b/src/commands/Playlist/SaveQueue.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, EmbedBuilder } from 'discord.js' import { Manager } from '../../manager.js' import { Accessableby, Command } from '../../structures/Command.js' import { CommandHandler } from '../../structures/CommandHandler.js' -import { RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkTrack } from 'rainlink' const TrackAdd: RainlinkTrack[] = [] const TrackExist: string[] = [] diff --git a/src/commands/Utils/MaxLength.ts b/src/commands/Utils/MaxLength.ts index 605cf5e5..ea07929d 100644 --- a/src/commands/Utils/MaxLength.ts +++ b/src/commands/Utils/MaxLength.ts @@ -2,7 +2,7 @@ import { ApplicationCommandOptionType, 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' +import { RainlinkPlayer } from 'rainlink' const time_regex = /(^[0-9][\d]{0,3}):(0[0-9]{1}$|[1-5]{1}[0-9])/ // Main code diff --git a/src/database/keyChecker.ts b/src/database/keyChecker.ts index d973d39f..4880dc97 100644 --- a/src/database/keyChecker.ts +++ b/src/database/keyChecker.ts @@ -21,7 +21,7 @@ export class keyChecker { } execute() { - const logger = new LoggerService(this.client, cluster.worker.id) + const logger = new LoggerService(this.client, this.client.cluster.id) const objReqKey = Object.keys(this.sampleConfig) const res = this.checkEngine() diff --git a/src/database/setup/lavalink.ts b/src/database/setup/lavalink.ts index 5be00dd7..9223525a 100644 --- a/src/database/setup/lavalink.ts +++ b/src/database/setup/lavalink.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { AutoReconnect } from '../schema/AutoReconnect.js' import chillout from 'chillout' import { VoiceChannel } from 'discord.js' -import { RainlinkLoopMode, RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkLoopMode, RainlinkPlayer } from 'rainlink' export class AutoReconnectLavalinkService { client: Manager diff --git a/src/events/guild/voiceStateUpdate.ts b/src/events/guild/voiceStateUpdate.ts index 81ddb525..715d12e3 100644 --- a/src/events/guild/voiceStateUpdate.ts +++ b/src/events/guild/voiceStateUpdate.ts @@ -8,7 +8,7 @@ import { } from 'discord.js' import { Manager } from '../../manager.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' -import { RainlinkPlayerState } from '../../rainlink/main.js' +import { RainlinkPlayerState } from 'rainlink' export default class { async execute(client: Manager, oldState: VoiceState, newState: VoiceState) { diff --git a/src/events/node/nodeClosed.ts b/src/events/node/nodeClosed.ts index 21530e7b..a527c9ef 100644 --- a/src/events/node/nodeClosed.ts +++ b/src/events/node/nodeClosed.ts @@ -1,6 +1,6 @@ import { AutoFixLavalink } from '../../autofix/AutoFixLavalink.js' import { Manager } from '../../manager.js' -import { RainlinkNode } from '../../rainlink/main.js' +import { RainlinkNode } from 'rainlink' export default class { async execute(client: Manager, node: RainlinkNode) { diff --git a/src/events/node/nodeConnect.ts b/src/events/node/nodeConnect.ts index ee27de58..3084c882 100644 --- a/src/events/node/nodeConnect.ts +++ b/src/events/node/nodeConnect.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkNode } from '../../rainlink/main.js' +import { RainlinkNode } from 'rainlink' export default class { execute(client: Manager, node: RainlinkNode) { diff --git a/src/events/node/nodeDisconnect.ts b/src/events/node/nodeDisconnect.ts index f2fec65a..054069c4 100644 --- a/src/events/node/nodeDisconnect.ts +++ b/src/events/node/nodeDisconnect.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkNode } from '../../rainlink/main.js' +import { RainlinkNode } from 'rainlink' export default class { execute(client: Manager, node: RainlinkNode, code: number, reason: Buffer) { diff --git a/src/events/node/nodeError.ts b/src/events/node/nodeError.ts index 06d4511a..debc6f15 100644 --- a/src/events/node/nodeError.ts +++ b/src/events/node/nodeError.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkNode } from '../../rainlink/main.js' +import { RainlinkNode } from 'rainlink' export default class { async execute(client: Manager, node: RainlinkNode, error: Error) { diff --git a/src/events/player/playerCreate.ts b/src/events/player/playerCreate.ts index 7109c623..96dd035b 100644 --- a/src/events/player/playerCreate.ts +++ b/src/events/player/playerCreate.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/player/playerDestroy.ts b/src/events/player/playerDestroy.ts index 9f2bab0f..081e5c82 100644 --- a/src/events/player/playerDestroy.ts +++ b/src/events/player/playerDestroy.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, TextChannel } from 'discord.js' import { ClearMessageService } from '../../services/ClearMessageService.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' -import { RainlinkPlayer, RainlinkPlayerState } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkPlayerState } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/player/playerException.ts b/src/events/player/playerException.ts index c6920592..9d8aa6ad 100644 --- a/src/events/player/playerException.ts +++ b/src/events/player/playerException.ts @@ -3,7 +3,7 @@ import { EmbedBuilder, TextChannel } from 'discord.js' import util from 'node:util' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { ClearMessageService } from '../../services/ClearMessageService.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer, data: Record) { diff --git a/src/events/player/playerPause.ts b/src/events/player/playerPause.ts index 40b67c2a..b3bcc59b 100644 --- a/src/events/player/playerPause.ts +++ b/src/events/player/playerPause.ts @@ -1,7 +1,7 @@ import { playerRowOneEdited, playerRowTwo } from '../../utilities/PlayerControlButton.js' import { Manager } from '../../manager.js' import { TextChannel } from 'discord.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/player/playerResume.ts b/src/events/player/playerResume.ts index f5dd90aa..b201be95 100644 --- a/src/events/player/playerResume.ts +++ b/src/events/player/playerResume.ts @@ -1,7 +1,7 @@ import { playerRowOne, playerRowTwo } from '../../utilities/PlayerControlButton.js' import { Manager } from '../../manager.js' import { TextChannel } from 'discord.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/player/playerStop.ts b/src/events/player/playerStop.ts index ee005c39..2ca74260 100644 --- a/src/events/player/playerStop.ts +++ b/src/events/player/playerStop.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, TextChannel } from 'discord.js' import { ClearMessageService } from '../../services/ClearMessageService.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/player/playerUpdate.ts b/src/events/player/playerUpdate.ts index 7eecb1c3..02d9f480 100644 --- a/src/events/player/playerUpdate.ts +++ b/src/events/player/playerUpdate.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer, data: unknown) { diff --git a/src/events/player/queueEmpty.ts b/src/events/player/queueEmpty.ts index 21afa2f7..b7716f6e 100644 --- a/src/events/player/queueEmpty.ts +++ b/src/events/player/queueEmpty.ts @@ -2,7 +2,7 @@ import { TextChannel } from 'discord.js' import { Manager } from '../../manager.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { ClearMessageService } from '../../services/ClearMessageService.js' -import { RainlinkPlayer, RainlinkPlayerState } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkPlayerState } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/track/trackEnd.ts b/src/events/track/trackEnd.ts index 894854fc..d801f498 100644 --- a/src/events/track/trackEnd.ts +++ b/src/events/track/trackEnd.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { TextChannel } from 'discord.js' import { ClearMessageService } from '../../services/ClearMessageService.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' -import { RainlinkPlayer, RainlinkPlayerState } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkPlayerState } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/track/trackResolveError.ts b/src/events/track/trackResolveError.ts index c381f931..08b5a100 100644 --- a/src/events/track/trackResolveError.ts +++ b/src/events/track/trackResolveError.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { TextChannel, EmbedBuilder } from 'discord.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { ClearMessageService } from '../../services/ClearMessageService.js' -import { RainlinkPlayer, RainlinkPlayerState, RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkPlayerState, RainlinkTrack } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer, track: RainlinkTrack, message: string) { diff --git a/src/events/track/trackStart.ts b/src/events/track/trackStart.ts index ffe7e4df..bc8652bf 100644 --- a/src/events/track/trackStart.ts +++ b/src/events/track/trackStart.ts @@ -5,7 +5,7 @@ import { formatDuration } from '../../utilities/FormatDuration.js' import { filterSelect, playerRowOne, playerRowTwo } from '../../utilities/PlayerControlButton.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { SongNotiEnum } from '../../database/schema/SongNoti.js' -import { RainlinkFilterMode, RainlinkPlayer, RainlinkTrack } from '../../rainlink/main.js' +import { RainlinkFilterMode, RainlinkPlayer, RainlinkTrack } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' export default class { @@ -42,7 +42,12 @@ export default class { const getData = await autoreconnect.get(player.guildId) if (!getData) await autoreconnect.playerBuild(player.guildId) else { - await client.db.autoreconnect.set(`${player.guildId}.current`, player.queue.current?.uri) + player.queue.current + ? await client.db.autoreconnect.set( + `${player.guildId}.current`, + player.queue.current?.uri + ) + : true await client.db.autoreconnect.set(`${player.guildId}.config.loop`, player.loop) function queueUri() { @@ -102,17 +107,11 @@ export default class { value: `${song!.requester}`, inline: true, }, - { - name: `${client.i18n.get(language, 'event.player', 'download_title')}`, - value: `**[${song!.title} - 000tube.com](https://www.000tube.com/watch?v=${song?.identifier})**`, - inline: false, - }, ]) .setColor(client.color) .setThumbnail( track.artworkUrl ?? `https://img.youtube.com/vi/${track.identifier}/hqdefault.jpg` ) - .setTimestamp() const playing_channel = (await client.channels .fetch(player.textId) diff --git a/src/events/track/trackStuck.ts b/src/events/track/trackStuck.ts index 8f640286..51c9dd40 100644 --- a/src/events/track/trackStuck.ts +++ b/src/events/track/trackStuck.ts @@ -2,7 +2,7 @@ import { Manager } from '../../manager.js' import { TextChannel, EmbedBuilder } from 'discord.js' import { AutoReconnectBuilderService } from '../../services/AutoReconnectBuilderService.js' import { ClearMessageService } from '../../services/ClearMessageService.js' -import { RainlinkPlayer, RainlinkPlayerState } from '../../rainlink/main.js' +import { RainlinkPlayer, RainlinkPlayerState } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer, data: Record) { diff --git a/src/events/websocket/playerCreate.ts b/src/events/websocket/playerCreate.ts index 65ea86d0..08ac0426 100644 --- a/src/events/websocket/playerCreate.ts +++ b/src/events/websocket/playerCreate.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/playerDestroy.ts b/src/events/websocket/playerDestroy.ts index 069e5784..bc298f72 100644 --- a/src/events/websocket/playerDestroy.ts +++ b/src/events/websocket/playerDestroy.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/playerPause.ts b/src/events/websocket/playerPause.ts index 296b62e2..6be28da7 100644 --- a/src/events/websocket/playerPause.ts +++ b/src/events/websocket/playerPause.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/playerResume.ts b/src/events/websocket/playerResume.ts index 566650c2..09b7424a 100644 --- a/src/events/websocket/playerResume.ts +++ b/src/events/websocket/playerResume.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/playerUpdate.ts b/src/events/websocket/playerUpdate.ts index a4b8c211..9dfba7f3 100644 --- a/src/events/websocket/playerUpdate.ts +++ b/src/events/websocket/playerUpdate.ts @@ -1,5 +1,5 @@ import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/trackEnd.ts b/src/events/websocket/trackEnd.ts index c4ab205e..eb72f3c0 100644 --- a/src/events/websocket/trackEnd.ts +++ b/src/events/websocket/trackEnd.ts @@ -1,6 +1,6 @@ import { User } from 'discord.js' import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { diff --git a/src/events/websocket/trackStart.ts b/src/events/websocket/trackStart.ts index 39e20a1e..c088ce56 100644 --- a/src/events/websocket/trackStart.ts +++ b/src/events/websocket/trackStart.ts @@ -1,27 +1,29 @@ import { User } from 'discord.js' import { Manager } from '../../manager.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export default class { async execute(client: Manager, player: RainlinkPlayer) { const song = player.queue.current - const requesterQueue = song!.requester as User + const requesterQueue = song && song.requester ? (song!.requester as User) : null - const currentData = { - title: song!.title, - uri: song!.uri, - length: song!.duration, - thumbnail: song!.artworkUrl, - author: song!.author, - requester: requesterQueue - ? { - id: requesterQueue.id, - username: requesterQueue.username, - globalName: requesterQueue.globalName, - defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, - } - : null, - } + const currentData = song + ? { + title: song!.title, + uri: song!.uri, + length: song!.duration, + thumbnail: song!.artworkUrl, + author: song!.author, + requester: requesterQueue + ? { + id: requesterQueue.id, + username: requesterQueue.username, + globalName: requesterQueue.globalName, + defaultAvatarURL: requesterQueue.defaultAvatarURL ?? null, + } + : null, + } + : null client.wsl.get(player.guildId)?.send({ op: 'trackStart', diff --git a/src/handlers/Player/ButtonCommands/Loop.ts b/src/handlers/Player/ButtonCommands/Loop.ts index 8ddecafc..36215e81 100644 --- a/src/handlers/Player/ButtonCommands/Loop.ts +++ b/src/handlers/Player/ButtonCommands/Loop.ts @@ -1,7 +1,7 @@ import { ButtonInteraction, EmbedBuilder } from 'discord.js' import { Manager } from '../../../manager.js' import { AutoReconnectBuilderService } from '../../../services/AutoReconnectBuilderService.js' -import { RainlinkLoopMode, RainlinkPlayer } from '../../../rainlink/main.js' +import { RainlinkLoopMode, RainlinkPlayer } from 'rainlink' export class ButtonLoop { client: Manager diff --git a/src/handlers/Player/ButtonCommands/Pause.ts b/src/handlers/Player/ButtonCommands/Pause.ts index c68bec0f..d24d3f08 100644 --- a/src/handlers/Player/ButtonCommands/Pause.ts +++ b/src/handlers/Player/ButtonCommands/Pause.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, EmbedBuilder, TextChannel, VoiceBasedChannel } from 'discord.js' import { Manager } from '../../../manager.js' -import { RainlinkPlayer } from '../../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class ButtonPause { client: Manager diff --git a/src/handlers/Player/ButtonCommands/Previous.ts b/src/handlers/Player/ButtonCommands/Previous.ts index 0560fb43..1cfb29c0 100644 --- a/src/handlers/Player/ButtonCommands/Previous.ts +++ b/src/handlers/Player/ButtonCommands/Previous.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, EmbedBuilder, VoiceBasedChannel } from 'discord.js' import { Manager } from '../../../manager.js' -import { RainlinkPlayer } from '../../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class ButtonPrevious { client: Manager diff --git a/src/handlers/Player/ButtonCommands/Skip.ts b/src/handlers/Player/ButtonCommands/Skip.ts index e0f5c9cb..fa7be6ea 100644 --- a/src/handlers/Player/ButtonCommands/Skip.ts +++ b/src/handlers/Player/ButtonCommands/Skip.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, EmbedBuilder, VoiceBasedChannel } from 'discord.js' import { Manager } from '../../../manager.js' -import { RainlinkPlayer } from '../../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class ButtonSkip { client: Manager diff --git a/src/handlers/Player/ButtonCommands/Stop.ts b/src/handlers/Player/ButtonCommands/Stop.ts index b4355a6c..db96fae7 100644 --- a/src/handlers/Player/ButtonCommands/Stop.ts +++ b/src/handlers/Player/ButtonCommands/Stop.ts @@ -1,6 +1,6 @@ import { ButtonInteraction, EmbedBuilder, VoiceBasedChannel } from 'discord.js' import { Manager } from '../../../manager.js' -import { RainlinkPlayer } from '../../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class ButtonStop { client: Manager diff --git a/src/handlers/Player/loadContent.ts b/src/handlers/Player/loadContent.ts index 5bc4d840..1e4b0dd8 100644 --- a/src/handlers/Player/loadContent.ts +++ b/src/handlers/Player/loadContent.ts @@ -178,7 +178,7 @@ export class PlayerContentLoader { let voiceChannel = await message.member!.voice.channel if (!voiceChannel) - return message.channel.send({ + return (message.channel as TextChannel).send({ embeds: [ new EmbedBuilder() .setDescription(`${client.i18n.get(language, 'error', 'no_in_voice')}`) diff --git a/src/handlers/Player/loadEvent.ts b/src/handlers/Player/loadEvent.ts index 76f41869..d224a07c 100644 --- a/src/handlers/Player/loadEvent.ts +++ b/src/handlers/Player/loadEvent.ts @@ -4,6 +4,7 @@ import { resolve } from 'path' import { join, dirname } from 'path' import { fileURLToPath, pathToFileURL } from 'url' import { Manager } from '../../manager.js' +import { RainlinkEventsInterface } from 'rainlink' const __dirname = dirname(fileURLToPath(import.meta.url)) export class PlayerEventLoader { @@ -44,7 +45,7 @@ export class PlayerEventLoader { `Event [${eName}] doesn't have exeture function on the class, Skipping...` ) - this.client.rainlink.on(eName as 'voiceEndSpeaking', (...args: unknown[]) => + this.client.rainlink.on(eName as keyof RainlinkEventsInterface, (...args: unknown[]) => events.execute(this.client, ...args) ) diff --git a/src/handlers/Player/loadUpdate.ts b/src/handlers/Player/loadUpdate.ts index f90f5cbb..de80a341 100644 --- a/src/handlers/Player/loadUpdate.ts +++ b/src/handlers/Player/loadUpdate.ts @@ -1,7 +1,7 @@ import { Manager } from '../../manager.js' import { EmbedBuilder, TextChannel } from 'discord.js' import { formatDuration } from '../../utilities/FormatDuration.js' -import { RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' import { getTitle } from '../../utilities/GetTitle.js' export class PlayerUpdateLoader { diff --git a/src/manager.ts b/src/manager.ts index f044826a..91cef0e7 100644 --- a/src/manager.ts +++ b/src/manager.ts @@ -26,16 +26,16 @@ import { Metadata } from './@types/Metadata.js' import { Config, Emojis } from './@types/Config.js' import { DatabaseTable } from './database/@types.js' import { LavalinkDataType, LavalinkUsingDataType } from './@types/Lavalink.js' -import { Rainlink } from './rainlink/Rainlink.js' +import { Rainlink } from 'rainlink' import { Command } from './structures/Command.js' import { PlayerButton } from './@types/Button.js' import { GlobalMsg } from './structures/CommandHandler.js' -import { RainlinkFilterData, RainlinkPlayer } from './rainlink/main.js' +import { RainlinkFilterData, RainlinkPlayer } from 'rainlink' import { TopggService } from './services/TopggService.js' import { Collection } from './structures/Collection.js' import { Localization } from './structures/Localization.js' import { ClusterManager } from './cluster/core.js' -import cluster from 'node:cluster' +import cluster, { Cluster } from 'node:cluster' config() function getShard(clusterManager: ClusterManager) { @@ -47,6 +47,7 @@ function getShard(clusterManager: ClusterManager) { } export class Manager extends Client { + public cluster: { id: number | 0; data: Cluster | null } public metadata: Metadata public logger: LoggerService public db!: DatabaseTable @@ -110,7 +111,11 @@ export class Manager extends Client { // Initial basic bot config const __dirname = dirname(fileURLToPath(import.meta.url)) - this.logger = new LoggerService(this, cluster.worker.id) + this.cluster = { + data: clusterManager ? cluster : null, + id: clusterManager ? cluster.worker.id : 0, + } + this.logger = new LoggerService(this, this.cluster.id) this.metadata = new ManifestService().data.metadata.bot this.owner = this.config.bot.OWNER_ID this.color = (this.config.bot.EMBED_COLOR || '#2b2d31') as ColorResolvable diff --git a/src/rainlink/Drivers/AbstractDriver.ts b/src/rainlink/Drivers/AbstractDriver.ts deleted file mode 100644 index 32a7fd83..00000000 --- a/src/rainlink/Drivers/AbstractDriver.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { RainlinkRequesterOptions } from '../Interface/Rest.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { RainlinkWebsocket } from '../Utilities/RainlinkWebsocket.js' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import { Rainlink } from '../Rainlink.js' - -export abstract class AbstractDriver { - /** The id for the driver*/ - abstract id: string - /** Ws url for dealing connection to lavalink/nodelink server */ - abstract wsUrl: string - /** Http url for dealing rest request to lavalink/nodelink server */ - abstract httpUrl: string - /** The lavalink server season id to resume */ - abstract sessionId: string | null - /** All function to extend support driver on RainlinkPlayer class */ - abstract playerFunctions: RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown> - /** All function to extend support driver on Rainlink class */ - abstract functions: RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown> - /** Rainlink manager class */ - abstract manager: Rainlink | null - /** Rainlink reuqested lavalink/nodelink server */ - abstract node: RainlinkNode | null - - /** - * Setup data and credentials for connect to lavalink/nodelink server - * @returns void - */ - abstract initial(manager: Rainlink, node: RainlinkNode): void - /** - * Connect to lavalink/nodelink server - * @returns WebSocket - */ - abstract connect(): RainlinkWebsocket - /** - * Fetch function for dealing rest request to lavalink/nodelink server - * @returns Promise - */ - abstract requester(options: RainlinkRequesterOptions): Promise - /** - * Close the lavalink/nodelink server - * @returns void - */ - abstract wsClose(): void - /** - * Update a season to resume able or not - * @returns void - */ - abstract updateSession(sessionId: string, mode: boolean, timeout: number): Promise -} diff --git a/src/rainlink/Drivers/Lavalink3.ts b/src/rainlink/Drivers/Lavalink3.ts deleted file mode 100644 index 9f3bf5b4..00000000 --- a/src/rainlink/Drivers/Lavalink3.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { Rainlink } from '../Rainlink.js' -import { metadata } from '../metadata.js' -import { LavalinkLoadType, RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkRequesterOptions, UpdatePlayerInfo } from '../Interface/Rest.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { AbstractDriver } from './AbstractDriver.js' -import util from 'node:util' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import { RainlinkWebsocket } from '../Utilities/RainlinkWebsocket.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' - -export enum Lavalink3loadType { - TRACK_LOADED = 'TRACK_LOADED', - PLAYLIST_LOADED = 'PLAYLIST_LOADED', - SEARCH_RESULT = 'SEARCH_RESULT', - NO_MATCHES = 'NO_MATCHES', - LOAD_FAILED = 'LOAD_FAILED', -} - -export class Lavalink3 extends AbstractDriver { - public id: string = 'lavalink/v3/koto' - public wsUrl: string = '' - public httpUrl: string = '' - public sessionId: string | null - public playerFunctions: RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown> - public functions: RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown> - protected wsClient?: RainlinkWebsocket - public manager: Rainlink | null = null - public node: RainlinkNode | null = null - - constructor() { - super() - this.playerFunctions = new RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown>() - this.functions = new RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown>() - this.sessionId = null - } - - public get isRegistered(): boolean { - return ( - this.manager !== null && - this.node !== null && - this.wsUrl.length !== 0 && - this.httpUrl.length !== 0 - ) - } - - public initial(manager: Rainlink, node: RainlinkNode): void { - this.manager = manager - this.node = node - this.wsUrl = `${this.node.options.secure ? 'wss' : 'ws'}://${this.node.options.host}:${this.node.options.port}/` - this.httpUrl = `${this.node.options.secure ? 'https://' : 'http://'}${this.node.options.host}:${this.node.options.port}` - } - - public connect(): RainlinkWebsocket { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - 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 : '', - 'user-agent': this.manager!.rainlinkOptions.options!.userAgent!, - 'num-shards': this.manager!.shardCount, - }, - }) - - ws.on('open', () => { - this.node!.wsOpenEvent() - }) - ws.on('message', (data: string) => this.wsMessageEvent(data)) - ws.on('error', (err) => this.node!.wsErrorEvent(err)) - ws.on('close', (code: number, reason: Buffer) => { - this.node!.wsCloseEvent(code, reason) - ws.removeAllListeners() - }) - this.wsClient = ws - return ws - } - - public async requester(options: RainlinkRequesterOptions): Promise { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - const url = new URL(`${this.httpUrl}${options.path}`) - if (options.params) url.search = new URLSearchParams(options.params).toString() - if (options.rawReqData && options.path.includes('/sessions')) { - this.convertToV3websocket(options.rawReqData) - return - } - if (options.data) { - this.convertToV3request(options.data as Record) - options.body = JSON.stringify(options.data) - } - if (options.path.includes('/sessions//')) return undefined - if ( - /\/sessions\/(.*)\/players\/(.*)/.test(options.path) || - (options.method && options.method == 'DELETE') - ) - return undefined - - const lavalinkHeaders = { - 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 - - 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'} ${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)}` - ) - return undefined - } - - const preFinalData = await res.json() - - let finalData: any = preFinalData - - if (finalData.loadType) { - finalData = this.convertV4trackResponse(finalData) as D - } - - this.debug( - `${options.method ?? 'GET'} ${url.pathname + url.search} payload=${options.body ? String(options.body) : '{}'}` - ) - - return finalData - } - - protected convertToV3websocket(data: UpdatePlayerInfo) { - let isPlaySent - if (!data) return - - // Voice update - if (data.playerOptions.voice) - this.wsSendData({ - op: 'voiceUpdate', - guildId: data.guildId, - sessionId: data.playerOptions.voice.sessionId, - event: data.playerOptions.voice, - }) - - // Play track - if ( - data.playerOptions.track && - data.playerOptions.track.encoded && - data.playerOptions.track.length !== 0 - ) { - isPlaySent = true - this.wsSendData({ - op: 'play', - guildId: data.guildId, - track: data.playerOptions.track.encoded, - startTime: data.playerOptions.position, - endTime: data.playerOptions.track.length, - volume: data.playerOptions.volume, - noReplace: data.noReplace, - pause: data.playerOptions.paused, - }) - } - - // Destroy player - if ( - data.playerOptions.track && - data.playerOptions.track.encoded == null && - data.playerOptions.track.length === 0 - ) - this.wsSendData({ - op: 'destroy', - guildId: data.guildId, - }) - - // Destroy player - if (data.playerOptions.track && data.playerOptions.track.encoded == null) - this.wsSendData({ - op: 'stop', - guildId: data.guildId, - }) - - if (isPlaySent) return (isPlaySent = false) - - // Pause player - if (data.playerOptions.paused === false || data.playerOptions.paused === true) - this.wsSendData({ - op: 'pause', - guildId: data.guildId, - pause: data.playerOptions.paused, - }) - - // Seek player - if (data.playerOptions.position) - this.wsSendData({ - op: 'seek', - guildId: data.guildId, - position: data.playerOptions.position, - }) - - // Voice player - if (data.playerOptions.volume) - this.wsSendData({ - op: 'volume', - guildId: data.guildId, - volume: data.playerOptions.volume, - }) - - // Filter player - if (data.playerOptions.filters) - this.wsSendData({ - op: 'filters', - guildId: data.guildId, - ...data.playerOptions.filters, - }) - } - - protected checkUpdateExist(data: Record) { - return ( - data.track || - data.identifier || - data.position || - data.endTime || - data.volume || - data.paused || - data.filters || - data.voice - ) - } - - protected wsSendData(data: Record): void { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - if (!this.wsClient) return - const jsonData = JSON.stringify(data) - this.wsClient.send(jsonData) - return - } - - protected wsMessageEvent(data: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - const wsData = JSON.parse(data.toString()) - if (wsData.reason) wsData.reason = (wsData.reason as string).toLowerCase() - if (wsData.reason == 'LOAD_FAILED') wsData.reason = 'loadFailed' - this.node!.wsMessageEvent(wsData) - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async updateSession(sessionId: string, mode: boolean, timeout: number): Promise { - if (!sessionId) { - this.wsSendData({ - op: 'configureResuming', - key: 'rainlink/lavalink/v3/koto/legacy', - timeout: 60, - }) - this.debug(`Session updated! resume: ${mode}, timeout: ${timeout}`) - return - } - const options: RainlinkRequesterOptions = { - path: `/sessions/${sessionId}`, - headers: { 'content-type': 'application/json' }, - method: 'PATCH', - data: { - resumingKey: sessionId, - timeout: timeout, - }, - } - - await this.requester<{ resuming: boolean; timeout: number }>(options) - this.debug(`Session updated! resume: ${mode}, timeout: ${timeout}`) - return - } - - protected debug(logs: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - this.manager!.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Node @ ${this.node?.options.name}] / [Driver] / [Lavalink3] | ${logs}` - ) - } - - public wsClose(): void { - if (this.wsClient) this.wsClient.close(1006, 'Self closed') - } - - protected testJSON(text: string) { - if (typeof text !== 'string') { - return false - } - try { - JSON.parse(text) - return true - } catch (error) { - return false - } - } - - protected convertToV3request(data?: Record) { - if (!data) return - if (data.track && data.track.encoded !== undefined) { - data.encodedTrack = data.track.encoded - delete data.track - } - return - } - - protected convertV4trackResponse(v3data: Record): Record { - if (!v3data) return {} - if (v3data.loadType == Lavalink3loadType.LOAD_FAILED) v3data.loadType = LavalinkLoadType.ERROR - if (v3data.loadType.includes('PLAYLIST_LOADED')) { - v3data.loadType = LavalinkLoadType.PLAYLIST - const convertedArray = [] - for (let i = 0; i < v3data.tracks.length; i++) { - convertedArray.push(this.buildV4track(v3data.tracks[i])) - } - v3data.data = { - info: v3data.playlistInfo, - tracks: convertedArray, - } - delete v3data.tracks - return v3data - } - if (v3data.loadType == Lavalink3loadType.SEARCH_RESULT) { - v3data.loadType = LavalinkLoadType.SEARCH - v3data.data = v3data.tracks - for (let i = 0; i < v3data.data.length; i++) { - v3data.data[i] = this.buildV4track(v3data.data[i]) - } - delete v3data.tracks - delete v3data.playlistInfo - } - if (v3data.loadType == Lavalink3loadType.TRACK_LOADED) { - v3data.loadType = LavalinkLoadType.TRACK - v3data.data = this.buildV4track(v3data.tracks[0]) - delete v3data.tracks - } - if (v3data.loadType == Lavalink3loadType.NO_MATCHES) v3data.loadType = LavalinkLoadType.EMPTY - return v3data - } - - protected buildV4track(v3data: Record) { - return { - encoded: v3data.track, - info: { - sourceName: v3data.info.sourceName, - identifier: v3data.info.identifier, - isSeekable: v3data.info.isSeekable, - author: v3data.info.author, - length: v3data.info.length, - isStream: v3data.info.isStream, - position: v3data.info.position, - title: v3data.info.title, - uri: v3data.info.uri, - artworkUrl: undefined, - }, - pluginInfo: undefined, - } - } -} diff --git a/src/rainlink/Drivers/Lavalink4.ts b/src/rainlink/Drivers/Lavalink4.ts deleted file mode 100644 index 02f212b9..00000000 --- a/src/rainlink/Drivers/Lavalink4.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { Rainlink } from '../Rainlink.js' -import { metadata } from '../metadata.js' -import { RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkRequesterOptions } from '../Interface/Rest.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { AbstractDriver } from './AbstractDriver.js' -import util from 'node:util' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import { RainlinkWebsocket } from '../Utilities/RainlinkWebsocket.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' - -export class Lavalink4 extends AbstractDriver { - public id: string = 'lavalink/v4/koinu' - public wsUrl: string = '' - public httpUrl: string = '' - public sessionId: string | null - public playerFunctions: RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown> - public functions: RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown> - protected wsClient?: RainlinkWebsocket - public manager: Rainlink | null = null - public node: RainlinkNode | null = null - - constructor() { - super() - this.playerFunctions = new RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown>() - this.functions = new RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown>() - this.sessionId = null - } - - public get isRegistered(): boolean { - return ( - this.manager !== null && - this.node !== null && - this.wsUrl.length !== 0 && - this.httpUrl.length !== 0 - ) - } - - public initial(manager: Rainlink, node: RainlinkNode): void { - this.manager = manager - this.node = node - this.wsUrl = `${this.node.options.secure ? 'wss' : 'ws'}://${this.node.options.host}:${this.node.options.port}/v4/websocket` - this.httpUrl = `${this.node.options.secure ? 'https://' : 'http://'}${this.node.options.host}:${this.node.options.port}/v4` - } - - public connect(): RainlinkWebsocket { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - 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 : '', - 'user-agent': this.manager!.rainlinkOptions.options!.userAgent!, - 'num-shards': this.manager!.shardCount, - }, - }) - - ws.on('open', () => { - this.node!.wsOpenEvent() - }) - ws.on('message', (data) => this.wsMessageEvent(data)) - ws.on('error', (err) => this.node!.wsErrorEvent(err)) - ws.on('close', (code: number, reason: Buffer) => { - this.node!.wsCloseEvent(code, reason) - ws.removeAllListeners() - }) - this.wsClient = ws - return ws - } - - public async requester(options: RainlinkRequesterOptions): Promise { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - if (options.path.includes('/sessions') && this.sessionId == null) - throw new Error('sessionId not initalized! Please wait for lavalink get connected!') - const url = new URL(`${this.httpUrl}${options.path}`) - if (options.params) url.search = new URLSearchParams(options.params).toString() - - if (options.data) { - options.body = JSON.stringify(options.data) - } - - const lavalinkHeaders = { - authorization: this.node!.options.auth, - 'user-agent': this.manager!.rainlinkOptions.options!.userAgent!, - ...options.headers, - } - - options.headers = lavalinkHeaders - - 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'} ${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)}` - ) - return undefined - } - - const finalData = await res.json() - - this.debug( - `${options.method ?? 'GET'} ${url.pathname + url.search} payload=${options.body ? String(options.body) : '{}'}` - ) - - return finalData as D - } - - protected wsMessageEvent(data: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - const wsData = JSON.parse(data.toString()) - this.node!.wsMessageEvent(wsData) - } - - protected debug(logs: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - this.manager!.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Node @ ${this.node?.options.name}] / [Driver] / [Lavalink4] | ${logs}` - ) - } - - public wsClose(): void { - if (this.wsClient) this.wsClient.close(1006, 'Self closed') - } - - public async updateSession(sessionId: string, mode: boolean, timeout: number): Promise { - const options: RainlinkRequesterOptions = { - path: `/sessions/${sessionId}`, - headers: { 'content-type': 'application/json' }, - method: 'PATCH', - data: { - resuming: mode, - timeout: timeout, - }, - } - - await this.requester<{ resuming: boolean; timeout: number }>(options) - this.debug(`Session updated! resume: ${mode}, timeout: ${timeout}`) - return - } -} diff --git a/src/rainlink/Drivers/Nodelink2.ts b/src/rainlink/Drivers/Nodelink2.ts deleted file mode 100644 index 4ad307c5..00000000 --- a/src/rainlink/Drivers/Nodelink2.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Rainlink } from '../Rainlink.js' -import { metadata } from '../metadata.js' -import { LavalinkLoadType, RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkRequesterOptions } from '../Interface/Rest.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { AbstractDriver } from './AbstractDriver.js' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import util from 'node:util' -import { RainlinkWebsocket } from '../Utilities/RainlinkWebsocket.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' - -export enum Nodelink2loadType { - SHORTS = 'shorts', - ALBUM = 'album', - ARTIST = 'artist', - SHOW = 'show', - EPISODE = 'episode', - STATION = 'station', - PODCAST = 'podcast', -} - -export interface NodelinkGetLyricsInterface { - loadType: Nodelink2loadType | LavalinkLoadType - data: - | { - name: string - synced: boolean - data: { - startTime: number - endTime: number - text: string - }[] - rtl: boolean - } - | Record -} - -export class Nodelink2 extends AbstractDriver { - public id: string = 'nodelink/v2/nari' - public wsUrl: string = '' - public httpUrl: string = '' - public sessionId: string | null - public playerFunctions: RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown> - public functions: RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown> - protected wsClient?: RainlinkWebsocket - public manager: Rainlink | null = null - public node: RainlinkNode | null = null - - constructor() { - super() - this.sessionId = null - this.playerFunctions = new RainlinkDatabase<(player: RainlinkPlayer, ...args: any) => unknown>() - this.functions = new RainlinkDatabase<(manager: Rainlink, ...args: any) => unknown>() - this.playerFunctions.set('getLyric', this.getLyric) - } - - public get isRegistered(): boolean { - return ( - this.manager !== null && - this.node !== null && - this.wsUrl.length !== 0 && - this.httpUrl.length !== 0 - ) - } - - public initial(manager: Rainlink, node: RainlinkNode): void { - this.manager = manager - this.node = node - this.wsUrl = `${this.node.options.secure ? 'wss' : 'ws'}://${this.node.options.host}:${this.node.options.port}/v4/websocket` - this.httpUrl = `${this.node.options.secure ? 'https://' : 'http://'}${this.node.options.host}:${this.node.options.port}/v4` - } - - public connect(): RainlinkWebsocket { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - const isResume = this.manager!.rainlinkOptions.options!.resume - const ws = new RainlinkWebsocket(this.wsUrl, { - headers: { - Authorization: this.node!.options.auth, - 'user-id': this.manager!.id, - 'accept-encoding': (process as any).isBun ? 'gzip, deflate' : 'br, gzip, deflate', - '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, - }, - }) - - ws.on('open', () => { - this.node!.wsOpenEvent() - }) - ws.on('message', (data) => this.wsMessageEvent(data)) - ws.on('error', (err) => this.node!.wsErrorEvent(err)) - ws.on('close', (code: number, reason: Buffer) => { - this.node!.wsCloseEvent(code, reason) - ws.removeAllListeners() - }) - this.wsClient = ws - return ws - } - - public async requester(options: RainlinkRequesterOptions): Promise { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - if (options.path.includes('/sessions') && this.sessionId == null) - throw new Error('sessionId not initalized! Please wait for nodelink get connected!') - const url = new URL(`${this.httpUrl}${options.path}`) - if (options.params) url.search = new URLSearchParams(options.params).toString() - - if (options.data) { - options.body = JSON.stringify(options.data) - } - - const lavalinkHeaders = { - authorization: this.node!.options.auth, - 'user-agent': this.manager!.rainlinkOptions.options!.userAgent!, - 'accept-encoding': (process as any).isBun ? 'gzip, deflate' : 'br, gzip, deflate', - ...options.headers, - } - - options.headers = lavalinkHeaders - - const res = await fetch(url, options) - - if (res.status == 204) { - this.debug( - `${options.method ?? 'GET'} ${url.pathname + url.search} payload=${options.body ? String(options.body) : '{}'}` - ) - return undefined - } - if (res.status !== 200) { - 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)}` - ) - return undefined - } - - const preFinalData = (await res.json()) as D - let finalData: any = preFinalData - - if (finalData.loadType) { - finalData = this.convertV4trackResponse(finalData) as D - } - - this.debug( - `${options.method ?? 'GET'} ${url.pathname + url.search} payload=${options.body ? String(options.body) : '{}'}` - ) - - return finalData - } - - protected wsMessageEvent(data: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - const wsData = JSON.parse(data.toString()) - this.node!.wsMessageEvent(wsData) - } - - protected debug(logs: string) { - if (!this.isRegistered) throw new Error(`Driver ${this.id} not registered by using initial()`) - this.manager!.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Node @ ${this.node?.options.name}] / [Driver] / [Nodelink2] | ${logs}` - ) - } - - public wsClose(): void { - if (this.wsClient) this.wsClient.close(1006, 'Self closed') - } - - protected convertV4trackResponse(nl2Data: Record): Record { - if (!nl2Data) return {} - switch (nl2Data.loadType) { - case Nodelink2loadType.SHORTS: { - nl2Data.loadType = LavalinkLoadType.TRACK - return nl2Data - } - case Nodelink2loadType.ALBUM: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - case Nodelink2loadType.ARTIST: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - case Nodelink2loadType.EPISODE: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - case Nodelink2loadType.STATION: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - case Nodelink2loadType.PODCAST: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - case Nodelink2loadType.SHOW: { - nl2Data.loadType = LavalinkLoadType.PLAYLIST - return nl2Data - } - } - return nl2Data - } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async updateSession(sessionId: string, mode: boolean, timeout: number): Promise { - this.debug("WARNING: Nodelink doesn't support resuming, set resume to true is useless") - return - } - - public async getLyric( - player: RainlinkPlayer, - language: string - ): Promise { - const options: RainlinkRequesterOptions = { - path: '/loadlyrics', - params: { - encodedTrack: String(player.queue.current?.encoded), - language: language, - }, - headers: { 'content-type': 'application/json' }, - method: 'GET', - } - const data = await player.node.driver.requester(options) - return data - } - - protected testJSON(text: string) { - if (typeof text !== 'string') { - return false - } - try { - JSON.parse(text) - return true - } catch (error) { - return false - } - } -} diff --git a/src/rainlink/Interface/Connection.ts b/src/rainlink/Interface/Connection.ts deleted file mode 100644 index 8835efaa..00000000 --- a/src/rainlink/Interface/Connection.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Represents the payload from a serverUpdate event - */ -export interface ServerUpdate { - token: string - guild_id: string - endpoint: string -} - -/** - * Represents the partial payload from a stateUpdate event - */ -export interface StateUpdatePartial { - channel_id?: string - session_id?: string - self_deaf: boolean - self_mute: boolean -} diff --git a/src/rainlink/Interface/Constants.ts b/src/rainlink/Interface/Constants.ts deleted file mode 100644 index 5d9936f3..00000000 --- a/src/rainlink/Interface/Constants.ts +++ /dev/null @@ -1,379 +0,0 @@ -/** - * All rainlink manager events - */ -export enum RainlinkEvents { - Debug = 'debug', - // Node - NodeConnect = 'nodeConnect', - NodeDisconnect = 'nodeDisconnect', - NodeClosed = 'nodeClosed', - NodeError = 'nodeError', - // Player - PlayerCreate = 'playerCreate', - PlayerDestroy = 'playerDestroy', - PlayerConnect = 'playerConnect', - PlayerDisconnect = 'playerDisconnect', - PlayerUpdate = 'playerUpdate', - PlayerMoved = 'playerMoved', - PlayerPause = 'playerPause', - PlayerResume = 'playerResume', - PlayerException = 'playerException', - PlayerWebsocketClosed = 'playerWebsocketClosed', - PlayerStop = 'playerStop', - // Track - TrackStuck = 'trackStuck', - TrackStart = 'trackStart', - TrackEnd = 'trackEnd', - TrackResolveError = 'trackResolveError', - // Queue - QueueAdd = 'queueAdd', - QueueRemove = 'queueRemove', - QueueShuffle = 'queueShuffle', - QueueClear = 'queueClear', - QueueEmpty = 'queueEmpty', - // Voice receiver - VoiceConnect = 'voiceConnect', - VoiceDisconnect = 'voiceDisconnect', - VoiceError = 'voiceError', - VoiceStartSpeaking = 'voiceStartSpeaking', - VoiceEndSpeaking = 'voiceEndSpeaking', -} - -/** - * Rainlink node connect state - */ -export enum RainlinkConnectState { - Connected, - Disconnected, - Closed, -} - -/** - * Discord voice state - */ -export enum VoiceState { - SESSION_READY, - SESSION_ID_MISSING, - SESSION_ENDPOINT_MISSING, - SESSION_FAILED_UPDATE, -} - -/** - * Discord voice connect status state - */ -export enum VoiceConnectState { - CONNECTING, - NEARLY, - CONNECTED, - RECONNECTING, - DISCONNECTING, - DISCONNECTED, -} - -/** - * Lavalink load type enum - */ -export enum LavalinkLoadType { - TRACK = 'track', - PLAYLIST = 'playlist', - SEARCH = 'search', - EMPTY = 'empty', - ERROR = 'error', -} - -/** - * Lavalink default source - */ -export const SourceIDs = [ - { name: 'youtube', id: 'yt' }, - { name: 'youtubeMusic', id: 'ytm' }, - { name: 'soundcloud', id: 'sc' }, -] - -/** - * Rainlink plugin type - */ -export enum RainlinkPluginType { - Default = 'default', - SourceResolver = 'sourceResolver', -} - -/** - * Rainlink player connect state - */ -export enum RainlinkPlayerState { - CONNECTED, - DISCONNECTED, - DESTROYED, -} - -/** - * Rainlink loop enum - */ -export enum RainlinkLoopMode { - SONG = 'song', - QUEUE = 'queue', - NONE = 'none', -} - -/** @ignore */ -export const RainlinkFilterData = { - clear: {}, - - eightD: { - rotation: { - rotationHz: 0.2, - }, - }, - - soft: { - lowPass: { - smoothing: 20.0, - }, - }, - - speed: { - timescale: { - speed: 1.501, - pitch: 1.245, - rate: 1.921, - }, - }, - - karaoke: { - karaoke: { - level: 1.0, - monoLevel: 1.0, - filterBand: 220.0, - filterWidth: 100.0, - }, - }, - nightcore: { - timescale: { - speed: 1.05, - pitch: 1.125, - rate: 1.05, - }, - }, - - pop: { - equalizer: [ - { band: 0, gain: -0.25 }, - { band: 1, gain: 0.48 }, - { band: 2, gain: 0.59 }, - { band: 3, gain: 0.72 }, - { band: 4, gain: 0.56 }, - { band: 6, gain: -0.24 }, - { band: 8, gain: -0.16 }, - ], - }, - - vaporwave: { - equalizer: [ - { band: 1, gain: 0.3 }, - { band: 0, gain: 0.3 }, - ], - timescale: { pitch: 0.5 }, - tremolo: { depth: 0.3, frequency: 14 }, - }, - - bass: { - equalizer: [ - { band: 0, gain: 0.1 }, - { band: 1, gain: 0.1 }, - { band: 2, gain: 0.05 }, - { band: 3, gain: 0.05 }, - { band: 4, gain: -0.05 }, - { band: 5, gain: -0.05 }, - { band: 6, gain: 0 }, - { band: 7, gain: -0.05 }, - { band: 8, gain: -0.05 }, - { band: 9, gain: 0 }, - { band: 10, gain: 0.05 }, - { band: 11, gain: 0.05 }, - { band: 12, gain: 0.1 }, - { band: 13, gain: 0.1 }, - ], - }, - - party: { - equalizer: [ - { band: 0, gain: -1.16 }, - { band: 1, gain: 0.28 }, - { band: 2, gain: 0.42 }, - { band: 3, gain: 0.5 }, - { band: 4, gain: 0.36 }, - { band: 5, gain: 0 }, - { band: 6, gain: -0.3 }, - { band: 7, gain: -0.21 }, - { band: 8, gain: -0.21 }, - ], - }, - - earrape: { - equalizer: [ - { band: 0, gain: 0.25 }, - { band: 1, gain: 0.5 }, - { band: 2, gain: -0.5 }, - { band: 3, gain: -0.25 }, - { band: 4, gain: 0 }, - { band: 6, gain: -0.025 }, - { band: 7, gain: -0.0175 }, - { band: 8, gain: 0 }, - { band: 9, gain: 0 }, - { band: 10, gain: 0.0125 }, - { band: 11, gain: 0.025 }, - { band: 12, gain: 0.375 }, - { band: 13, gain: 0.125 }, - { band: 14, gain: 0.125 }, - ], - }, - - equalizer: { - equalizer: [ - { band: 0, gain: 0.375 }, - { band: 1, gain: 0.35 }, - { band: 2, gain: 0.125 }, - { band: 5, gain: -0.125 }, - { band: 6, gain: -0.125 }, - { band: 8, gain: 0.25 }, - { band: 9, gain: 0.125 }, - { band: 10, gain: 0.15 }, - { band: 11, gain: 0.2 }, - { band: 12, gain: 0.25 }, - { band: 13, gain: 0.35 }, - { band: 14, gain: 0.4 }, - ], - }, - - electronic: { - equalizer: [ - { band: 0, gain: 0.375 }, - { band: 1, gain: 0.35 }, - { band: 2, gain: 0.125 }, - { band: 5, gain: -0.125 }, - { band: 6, gain: -0.125 }, - { band: 8, gain: 0.25 }, - { band: 9, gain: 0.125 }, - { band: 10, gain: 0.15 }, - { band: 11, gain: 0.2 }, - { band: 12, gain: 0.25 }, - { band: 13, gain: 0.35 }, - { band: 14, gain: 0.4 }, - ], - }, - radio: { - equalizer: [ - { band: 0, gain: -0.25 }, - { band: 1, gain: 0.48 }, - { band: 2, gain: 0.59 }, - { band: 3, gain: 0.72 }, - { band: 4, gain: 0.56 }, - { band: 6, gain: -0.24 }, - { band: 8, gain: -0.16 }, - ], - }, - - tremolo: { - tremolo: { - depth: 0.3, - frequency: 14, - }, - }, - - treblebass: { - equalizer: [ - { band: 0, gain: 0.6 }, - { band: 1, gain: 0.67 }, - { band: 2, gain: 0.67 }, - { band: 3, gain: 0 }, - { band: 4, gain: -0.5 }, - { band: 5, gain: 0.15 }, - { band: 6, gain: -0.45 }, - { band: 7, gain: 0.23 }, - { band: 8, gain: 0.35 }, - { band: 9, gain: 0.45 }, - { band: 10, gain: 0.55 }, - { band: 11, gain: 0.6 }, - { band: 12, gain: 0.55 }, - ], - }, - - vibrato: { - vibrato: { - depth: 0.3, - frequency: 14, - }, - }, - - china: { - timescale: { - speed: 0.75, - pitch: 1.25, - rate: 1.25, - }, - }, - - chimpunk: { - timescale: { - speed: 1.05, - pitch: 1.35, - rate: 1.25, - }, - }, - - darthvader: { - timescale: { - speed: 0.975, - pitch: 0.5, - rate: 0.8, - }, - }, - - daycore: { - equalizer: [ - { band: 0, gain: 0 }, - { band: 1, gain: 0 }, - { band: 2, gain: 0 }, - { band: 3, gain: 0 }, - { band: 4, gain: 0 }, - { band: 5, gain: 0 }, - { band: 6, gain: 0 }, - { band: 7, gain: 0 }, - { band: 8, gain: -0.25 }, - { band: 9, gain: -0.25 }, - { band: 10, gain: -0.25 }, - { band: 11, gain: -0.25 }, - { band: 12, gain: -0.25 }, - { band: 13, gain: -0.25 }, - ], - timescale: { - pitch: 0.63, - rate: 1.05, - }, - }, - - doubletime: { - timescale: { - speed: 1.165, - }, - }, - - pitch: { - timescale: { pitch: 3 }, - }, - - rate: { - timescale: { rate: 2 }, - }, - - slow: { - timescale: { - speed: 0.5, - pitch: 1.0, - rate: 0.8, - }, - }, -} - -export type RainlinkFilterMode = keyof typeof RainlinkFilterData diff --git a/src/rainlink/Interface/LavalinkEvents.ts b/src/rainlink/Interface/LavalinkEvents.ts deleted file mode 100644 index 4dcef363..00000000 --- a/src/rainlink/Interface/LavalinkEvents.ts +++ /dev/null @@ -1,39 +0,0 @@ -/** - * Lavalink events enum - */ -export enum LavalinkEventsEnum { - Ready = 'ready', - Status = 'stats', - Event = 'event', - PlayerUpdate = 'playerUpdate', -} - -/** - * Lavalink player events enum - */ -export enum LavalinkPlayerEventsEnum { - TrackStartEvent = 'TrackStartEvent', - TrackEndEvent = 'TrackEndEvent', - TrackExceptionEvent = 'TrackExceptionEvent', - TrackStuckEvent = 'TrackStuckEvent', - WebSocketClosedEvent = 'WebSocketClosedEvent', -} - -/** - * Reason why track end - */ -export type TrackEndReason = 'finished' | 'loadFailed' | 'stopped' | 'replaced' | 'cleanup' - -/** - * Exception interface - */ -export interface Exception { - message: string - severity: Severity - cause: string -} - -/** - * Exception severity interface - */ -export type Severity = 'common' | 'suspicious' | 'fault' diff --git a/src/rainlink/Interface/Manager.ts b/src/rainlink/Interface/Manager.ts deleted file mode 100644 index f1fc94d1..00000000 --- a/src/rainlink/Interface/Manager.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { AbstractLibrary } from '../Library/AbstractLibrary.js' -import { RainlinkPlugin } from '../Plugin/RainlinkPlugin.js' -import { RainlinkTrack } from '../Player/RainlinkTrack.js' -import { RainlinkNodeManager } from '../Manager/RainlinkNodeManager.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { RainlinkRest } from '../Node/RainlinkRest.js' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import { AbstractDriver } from '../Drivers/AbstractDriver.js' -import { RainlinkQueue } from '../Player/RainlinkQueue.js' - -/** - * A structure interface - */ -export type Constructor = new (...args: any[]) => T - -export interface Structures { - /** - * A custom structure that extends the RainlinkRest class - */ - rest?: Constructor - /** - * A custom structure that extends the RainlinkPlayer class - */ - player?: Constructor - /** - * A custom structure that extends the RainlinkQueue class - */ - queue?: Constructor -} - -/** - * Rainlink node option interface - */ -export interface RainlinkNodeOptions { - /** Name for get the lavalink server info in rainlink */ - name: string - /** The ip address or domain of lavalink server */ - host: string - /** The port that lavalink server exposed */ - port: number - /** The password of lavalink server */ - auth: string - /** Whenever lavalink user ssl or not */ - secure: boolean - /** The driver class for handling lavalink response */ - driver?: string -} - -/** - * Some rainlink additional config option - */ -export interface RainlinkAdditionalOptions { - /** Additional custom driver for rainlink */ - additionalDriver?: AbstractDriver[] - /** Timeout before trying to reconnect (ms) */ - retryTimeout?: number - /** Number of times to try and reconnect to Lavalink before giving up */ - retryCount?: number - /** The retry timeout for voice manager when dealing connection to discord voice server (ms) */ - voiceConnectionTimeout?: number - /** The default search engine like default search from youtube, spotify,... */ - defaultSearchEngine?: string - /** The default volume when create a player */ - defaultVolume?: number - /** Search track from youtube when track resolve failed */ - searchFallback?: { - /** Whenever enable this search fallback or not */ - enable: boolean - /** Choose a fallback search engine, recommended soundcloud and youtube */ - engine: string - } - /** Whether to resume a connection on disconnect to Lavalink (Server Side) (Note: DOES NOT RESUME WHEN THE LAVALINK SERVER DIES) */ - resume?: boolean - /** When the seasion is deleted from Lavalink. Use second (Server Side) (Note: DOES NOT RESUME WHEN THE LAVALINK SERVER DIES) */ - resumeTimeout?: number - /** User Agent to use when making requests to Lavalink */ - userAgent?: string - /** Node Resolver to use if you want to customize it */ - nodeResolver?: (nodes: RainlinkNodeManager) => Promise - /** Custom structures for rainlink to use */ - structures?: Structures -} - -/** - * Rainlink config interface - */ -export interface RainlinkOptions { - /** The lavalink server credentials array*/ - nodes: RainlinkNodeOptions[] - /** The discord library for using voice manager, example: discordjs, erisjs. Check {@link Library} */ - library: AbstractLibrary - /** The rainlink plugins array. Check {@link Plugin} */ - plugins?: RainlinkPlugin[] - /** Rainlink additional options */ - options?: RainlinkAdditionalOptions -} - -/** - * The type enum of rainlink search function result - */ -export enum RainlinkSearchResultType { - TRACK = 'TRACK', - PLAYLIST = 'PLAYLIST', - SEARCH = 'SEARCH', -} - -/** - * The rainlink search function result interface - */ -export interface RainlinkSearchResult { - type: RainlinkSearchResultType - playlistName?: string - tracks: RainlinkTrack[] -} - -/** - * The rainlink search function options interface - */ -export interface RainlinkSearchOptions { - /** User info of who request the song */ - requester?: unknown - /** Which node do user want to use (get using node name) */ - nodeName?: string - /** Which search engine do user want to use (get using search engine name) */ - engine?: string -} diff --git a/src/rainlink/Interface/Node.ts b/src/rainlink/Interface/Node.ts deleted file mode 100644 index 64059627..00000000 --- a/src/rainlink/Interface/Node.ts +++ /dev/null @@ -1,77 +0,0 @@ -/** The lavalink server status interface */ -export interface NodeStats { - players: number - playingPlayers: number - memory: { - reservable: number - used: number - free: number - allocated: number - } - frameStats: { - sent: number - deficit: number - nulled: number - } - cpu: { - cores: number - systemLoad: number - lavalinkLoad: number - } - uptime: number -} - -/** The lavalink server status response interface */ -export interface LavalinkNodeStatsResponse { - op: string - players: number - playingPlayers: number - memory: { - reservable: number - used: number - free: number - allocated: number - } - frameStats: { - sent: number - deficit: number - nulled: number - } - cpu: { - cores: number - systemLoad: number - lavalinkLoad: number - } - uptime: number -} - -export type NodeInfo = { - version: NodeInfoVersion - buildTime: number - git: NodeInfoGit - jvm: string - lavaplayer: string - sourceManagers: string[] - filters: string[] - plugins: NodeInfoPlugin[] -} - -export type NodeInfoVersion = { - semver: string - major: number - minor: number - patch: number - preRelease?: string - build?: string -} - -export type NodeInfoGit = { - branch: string - commit: string - commitTime: number -} - -export type NodeInfoPlugin = { - name: string - version: string -} diff --git a/src/rainlink/Interface/Player.ts b/src/rainlink/Interface/Player.ts deleted file mode 100644 index 708390bf..00000000 --- a/src/rainlink/Interface/Player.ts +++ /dev/null @@ -1,92 +0,0 @@ -export interface VoiceChannelOptions { - guildId: string - shardId: number - voiceId: string - textId: string - volume?: number - nodeName?: string - deaf?: boolean - mute?: boolean -} - -export interface FilterOptions { - volume?: number - equalizer?: Band[] - karaoke?: Karaoke | null - timescale?: Timescale | null - tremolo?: Freq | null - vibrato?: Freq | null - rotation?: Rotation | null - distortion?: Distortion | null - channelMix?: ChannelMix | null - lowPass?: LowPass | null -} - -export interface Band { - band: number - gain: number -} - -export interface Karaoke { - level?: number - monoLevel?: number - filterBand?: number - filterWidth?: number -} - -export interface Timescale { - speed?: number - pitch?: number - rate?: number -} - -export interface Freq { - frequency?: number - depth?: number -} - -export interface Rotation { - rotationHz?: number -} - -export interface Distortion { - sinOffset?: number - sinScale?: number - cosOffset?: number - cosScale?: number - tanOffset?: number - tanScale?: number - offset?: number - scale?: number -} - -export interface ChannelMix { - leftToLeft?: number - leftToRight?: number - rightToLeft?: number - rightToRight?: number -} - -export interface LowPass { - smoothing?: number -} - -export interface PlayOptions { - noReplace?: boolean - pause?: boolean - startTime?: number - endTime?: number - replaceCurrent?: boolean - position?: number -} - -export interface PlayEncodedOptions { - encoded: string - options?: { - noReplace?: boolean - pause?: boolean - startTime?: number - endTime?: number - volume?: number - } -} diff --git a/src/rainlink/Interface/Rest.ts b/src/rainlink/Interface/Rest.ts deleted file mode 100644 index 9d3451ca..00000000 --- a/src/rainlink/Interface/Rest.ts +++ /dev/null @@ -1,154 +0,0 @@ -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 - useSessionId?: boolean - data?: Record - path: string - rawReqData?: UpdatePlayerInfo -} - -export type LavalinkStats = Omit - -export interface LavalinkPlayer { - guildId: string - track?: RawTrack - volume: number - paused: boolean - voice: LavalinkPlayerVoice - filters: FilterOptions -} - -export interface RawTrack { - encoded: string - info: { - identifier: string - isSeekable: boolean - author: string - length: number - isStream: boolean - position: number - title: string - uri?: string - artworkUrl?: string - isrc?: string - sourceName: string - } - pluginInfo: unknown -} - -export interface LavalinkPlayerVoice { - token: string - endpoint: string - sessionId: string - connected?: boolean - ping?: number -} - -export interface LavalinkPlayerVoiceOptions - extends Omit {} - -export interface LavalinkPlayer { - guildId: string - track?: RawTrack - volume: number - paused: boolean - voice: LavalinkPlayerVoice - filters: FilterOptions -} - -export interface UpdatePlayerTrack { - encoded?: string | null - identifier?: string - userData?: Record - length?: number -} - -export interface UpdatePlayerOptions { - track?: UpdatePlayerTrack - identifier?: string - position?: number - endTime?: number - volume?: number - paused?: boolean - filters?: FilterOptions - voice?: LavalinkPlayerVoiceOptions -} - -export interface UpdatePlayerInfo { - guildId: string - playerOptions: UpdatePlayerOptions - noReplace?: boolean -} - -export interface TrackResult { - loadType: LavalinkLoadType.TRACK - data: RawTrack -} - -export interface PlaylistResult { - loadType: LavalinkLoadType.PLAYLIST - data: Playlist -} - -export interface SearchResult { - loadType: LavalinkLoadType.SEARCH - data: RawTrack[] -} - -export interface EmptyResult { - loadType: LavalinkLoadType.EMPTY - data: Record -} - -export interface ErrorResult { - loadType: LavalinkLoadType.ERROR - data: Exception -} - -export interface Playlist { - encoded: string - info: { - name: string - selectedTrack: number - } - pluginInfo: unknown - tracks: RawTrack[] -} - -export type LavalinkResponse = - | TrackResult - | PlaylistResult - | SearchResult - | EmptyResult - | ErrorResult - -export interface RoutePlanner { - class: - | null - | 'RotatingIpRoutePlanner' - | 'NanoIpRoutePlanner' - | 'RotatingNanoIpRoutePlanner' - | 'BalancingIpRoutePlanner' - details: null | { - ipBlock: { - type: string - size: string - } - failingAddresses: Address[] - rotateIndex: string - ipIndex: string - currentAddress: string - blockIndex: string - currentAddressIndex: string - } -} - -export interface Address { - address: string - failingTimestamp: number - failingTime: string -} diff --git a/src/rainlink/Interface/Track.ts b/src/rainlink/Interface/Track.ts deleted file mode 100644 index 037132dd..00000000 --- a/src/rainlink/Interface/Track.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' - -export interface ResolveOptions { - /** Whenever u want to overwrite the track or not */ - overwrite?: boolean - /** Rainlink player property */ - player?: RainlinkPlayer - /** The name of node */ - nodeName?: string -} diff --git a/src/rainlink/Library/AbstractLibrary.ts b/src/rainlink/Library/AbstractLibrary.ts deleted file mode 100644 index 22fd3e7d..00000000 --- a/src/rainlink/Library/AbstractLibrary.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Get from: https://github.com/shipgirlproject/Shoukaku/blob/396aa531096eda327ade0f473f9807576e9ae9df/src/connectors/Connector.ts -// Special thanks to shipgirlproject team! - -import { RainlinkNodeOptions } from '../Interface/Manager.js' -import { RainlinkEvents } from '../main.js' -import { Rainlink } from '../Rainlink.js' -export const AllowedPackets = ['VOICE_STATE_UPDATE', 'VOICE_SERVER_UPDATE'] - -export abstract class AbstractLibrary { - protected readonly client: any - protected manager: Rainlink | null - constructor(client: any) { - this.client = client - this.manager = null - } - - protected ready(nodes: RainlinkNodeOptions[]): void { - this.manager!.id = this.getId() - this.manager!.shardCount = this.getShardCount() - this.manager!.emit( - RainlinkEvents.Debug, - `[Rainlink] | Finished the initialization process | Registered ${this.manager!.plugins.size} plugins | Now connect all current nodes` - ) - for (const node of nodes) this.manager?.nodes.add(node) - } - - public set(manager: Rainlink): AbstractLibrary { - this.manager = manager - return this - } - - abstract getId(): string - - abstract getShardCount(): number - - abstract sendPacket(shardId: number, payload: any, important: boolean): void - - abstract listen(nodes: RainlinkNodeOptions[]): void - - protected raw(packet: any): void { - if (!AllowedPackets.includes(packet.t)) return - const guildId = packet.d.guild_id - const players = this.manager!.players.get(guildId) - if (!players) return - if (packet.t === 'VOICE_SERVER_UPDATE') return players.setServerUpdate(packet.d) - const userId = packet.d.user_id - if (userId !== this.manager!.id) return - players.setStateUpdate(packet.d) - } -} diff --git a/src/rainlink/Library/DiscordJS.ts b/src/rainlink/Library/DiscordJS.ts deleted file mode 100644 index 900601a2..00000000 --- a/src/rainlink/Library/DiscordJS.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Modded from: https://github.com/shipgirlproject/Shoukaku/blob/396aa531096eda327ade0f473f9807576e9ae9df/src/connectors/libs/DiscordJS.ts -// Special thanks to shipgirlproject team! - -import { AbstractLibrary } from './AbstractLibrary.js' -import { RainlinkNodeOptions } from '../Interface/Manager.js' - -export class DiscordJS extends AbstractLibrary { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.ws.shards.get(shardId)?.send(payload, important) - } - - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id - } - - // getShardCount is for dealing ws with lavalink server - public getShardCount(): number { - return this.client.shard && this.client.shard.count ? this.client.shard.count : 1 - } - - // Listen attaches the event listener to the library you are using - public listen(nodes: RainlinkNodeOptions[]): void { - this.client.once('ready', () => this.ready(nodes)) - this.client.on('raw', (packet: any) => this.raw(packet)) - } -} diff --git a/src/rainlink/Library/ErisJS.ts b/src/rainlink/Library/ErisJS.ts deleted file mode 100644 index f8fd6613..00000000 --- a/src/rainlink/Library/ErisJS.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AbstractLibrary } from './AbstractLibrary.js' -import { RainlinkNodeOptions } from '../Interface/Manager.js' - -export class ErisJS extends AbstractLibrary { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.shards.get(shardId)?.sendWS(payload.op, payload.d, important) - } - - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id - } - - // getShardCount is for dealing ws with lavalink server - public getShardCount(): number { - return this.client.shards && this.client.shards.size ? this.client.shards.size : 1 - } - - // Listen attaches the event listener to the library you are using - public listen(nodes: RainlinkNodeOptions[]): void { - // Only attach to ready event once, refer to your library for its ready event - this.client.once('ready', () => this.ready(nodes)) - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - this.client.on('rawWS', (packet: any) => this.raw(packet)) - } -} diff --git a/src/rainlink/Library/OceanicJS.ts b/src/rainlink/Library/OceanicJS.ts deleted file mode 100644 index 1369d8b8..00000000 --- a/src/rainlink/Library/OceanicJS.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { AbstractLibrary } from './AbstractLibrary.js' -import { RainlinkNodeOptions } from '../Interface/Manager.js' - -export class OceanicJS extends AbstractLibrary { - // sendPacket is where your library send packets to Discord Gateway - public sendPacket(shardId: number, payload: any, important: boolean): void { - return this.client.shards.get(shardId)?.send(payload.op, payload.d, important) - } - - // getId is a getter where the lib stores the client user (the one logged in as a bot) id - public getId(): string { - return this.client.user.id - } - - // getShardCount is for dealing ws with lavalink server - public getShardCount(): number { - return this.client.shards && this.client.shards.size ? this.client.shards.size : 1 - } - - // Listen attaches the event listener to the library you are using - public listen(nodes: RainlinkNodeOptions[]): void { - // Only attach to ready event once, refer to your library for its ready event - this.client.once('ready', () => this.ready(nodes)) - // Attach to the raw websocket event, this event must be 1:1 on spec with dapi (most libs implement this) - this.client.on('packet', (packet: any) => this.raw(packet)) - } -} diff --git a/src/rainlink/Library/index.ts b/src/rainlink/Library/index.ts deleted file mode 100644 index e5432bd7..00000000 --- a/src/rainlink/Library/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { DiscordJS } from './DiscordJS.js' -import { ErisJS } from './ErisJS.js' -import { OceanicJS } from './OceanicJS.js' - -/** - * Import example: - * @example - * ```ts - * new Plugin.DiscordJS(client) - * new Plugin.ErisJS(client) - * new Plugin.OceanicJS(client) - * ``` - */ -export default { - DiscordJS, - ErisJS, - OceanicJS, -} diff --git a/src/rainlink/Manager/RainlinkNodeManager.ts b/src/rainlink/Manager/RainlinkNodeManager.ts deleted file mode 100644 index 3efcba4d..00000000 --- a/src/rainlink/Manager/RainlinkNodeManager.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { RainlinkConnectState, RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkNodeOptions } from '../Interface/Manager.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { Rainlink } from '../Rainlink.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' - -export class RainlinkNodeManager extends RainlinkDatabase { - /** The rainlink manager */ - public manager: Rainlink - - /** - * The main class for handling lavalink servers - * @param manager - */ - constructor(manager: Rainlink) { - super() - this.manager = manager - } - - /** - * Add a new Node. - * @returns RainlinkNode - */ - public add(node: RainlinkNodeOptions) { - const newNode = new RainlinkNode(this.manager, node) - newNode.connect() - this.set(node.name, newNode) - this.debug(`Node ${node.name} added to manager!`) - return newNode - } - - /** - * Get a least used node. - * @returns RainlinkNode - */ - public async getLeastUsed(): Promise { - if (this.manager.rainlinkOptions.options!.nodeResolver) { - const resolverData = await this.manager.rainlinkOptions.options!.nodeResolver(this) - if (resolverData) return resolverData - } - const nodes: RainlinkNode[] = this.values - - const onlineNodes = nodes.filter((node) => node.state === RainlinkConnectState.Connected) - if (!onlineNodes.length) throw new Error('No nodes are online') - - const temp = await Promise.all( - 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[0].node - } - - /** - * Get all current nodes - * @returns RainlinkNode[] - */ - public all(): RainlinkNode[] { - return this.values - } - - /** - * Remove a node. - * @returns void - */ - public remove(name: string): void { - const node = this.get(name) - if (node) { - node.disconnect() - this.delete(name) - this.debug(`Node ${name} removed from manager!`) - } - return - } - - protected debug(logs: string) { - this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [NodeManager] | ${logs}`) - } -} diff --git a/src/rainlink/Manager/RainlinkPlayerManager.ts b/src/rainlink/Manager/RainlinkPlayerManager.ts deleted file mode 100644 index 5bbff397..00000000 --- a/src/rainlink/Manager/RainlinkPlayerManager.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { RainlinkEvents, RainlinkPlayerState, VoiceState } from '../Interface/Constants.js' -import { VoiceChannelOptions } from '../Interface/Player.js' -import { RainlinkPlayer } from '../Player/RainlinkPlayer.js' -import { Rainlink } from '../Rainlink.js' -import { RainlinkPlugin } from '../Plugin/VoiceReceiver/Plugin.js' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' - -export class RainlinkPlayerManager extends RainlinkDatabase { - /** The rainlink manager */ - public manager: Rainlink - - /** - * The main class for handling lavalink players - * @param manager The rainlink manager - */ - constructor(manager: Rainlink) { - super() - this.manager = manager - } - - /** - * Create a player - * @returns RainlinkPlayer - * @internal - */ - async create(options: VoiceChannelOptions): Promise { - const createdPlayer = this.get(options.guildId) - if (createdPlayer) return createdPlayer - const getCustomNode = this.manager.nodes.get(String(options.nodeName ? options.nodeName : '')) - const node = getCustomNode ? getCustomNode : await this.manager.nodes.getLeastUsed() - if (!node) throw new Error("Can't find any nodes to connect on") - const customPlayer = - this.manager.rainlinkOptions.options!.structures && - this.manager.rainlinkOptions.options!.structures.player - let player = customPlayer - ? new customPlayer(this.manager, options, node) - : new RainlinkPlayer(this.manager, options, node) - this.set(player.guildId, player) - try { - player = await player.connect() - } catch (err) { - this.delete(player.guildId) - throw err - } - const onUpdate = (state: VoiceState) => { - if (state !== VoiceState.SESSION_READY) return - player.sendServerUpdate() - } - await player.sendServerUpdate() - player.on('connectionUpdate', onUpdate) - player.state = RainlinkPlayerState.CONNECTED - this.debug('Player created at ' + options.guildId) - this.manager.emit(RainlinkEvents.PlayerCreate, player) - const voiceReceiver = this.manager.plugins.get('rainlink-voiceReceiver') as RainlinkPlugin - if (voiceReceiver && node.driver.id.includes('nodelink')) voiceReceiver.open(node, options) - return player - } - - /** - * Destroy a player - * @returns The destroyed / disconnected player or undefined if none - * @internal - */ - public async destroy(guildId: string = ''): Promise { - const player = this.get(guildId) - if (player) await player.destroy() - } - - protected debug(logs: string) { - this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [PlayerManager] | ${logs}`) - } -} diff --git a/src/rainlink/Node/RainlinkNode.ts b/src/rainlink/Node/RainlinkNode.ts deleted file mode 100644 index 39276728..00000000 --- a/src/rainlink/Node/RainlinkNode.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { RainlinkNodeOptions } from '../Interface/Manager.js' -import { Rainlink } from '../Rainlink.js' -import { RainlinkConnectState, RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkRest } from './RainlinkRest.js' -import { setTimeout } from 'node:timers/promises' -import { RainlinkPlayerEvents } from './RainlinkPlayerEvents.js' -import { LavalinkEventsEnum } from '../Interface/LavalinkEvents.js' -import { LavalinkNodeStatsResponse, NodeStats } from '../Interface/Node.js' -import { AbstractDriver } from '../Drivers/AbstractDriver.js' -// Drivers -import { Lavalink4 } from '../Drivers/Lavalink4.js' -import { RainlinkWebsocket } from '../Utilities/RainlinkWebsocket.js' - -export class RainlinkNode { - /** The rainlink manager */ - public manager: Rainlink - /** The rainlink node options */ - public options: RainlinkNodeOptions - /** The rainlink rest manager */ - public rest: RainlinkRest - /** The lavalink server online status */ - public online: boolean = false - protected retryCounter = 0 - /** The lavalink server connect state */ - public state: RainlinkConnectState = RainlinkConnectState.Closed - /** The lavalink server all status */ - public stats: NodeStats - protected sudoDisconnect = false - protected wsEvent: RainlinkPlayerEvents - /** Driver for connect to current version of Nodelink/Lavalink */ - public driver: AbstractDriver - - /** - * The lavalink server handler class - * @param manager The rainlink manager - * @param options The lavalink server options - */ - constructor(manager: Rainlink, options: RainlinkNodeOptions) { - this.manager = manager - this.options = options - const getDriver = this.manager.drivers.filter((driver) => driver.id === options.driver) - if (!getDriver || getDriver.length == 0) { - this.debug('No driver was found, using lavalink v4 driver instead') - this.driver = new Lavalink4() - } else { - this.debug(`Now using driver: ${getDriver[0].id}`) - this.driver = getDriver[0] - } - this.driver.initial(manager, this) - const customRest = - this.manager.rainlinkOptions.options!.structures && - this.manager.rainlinkOptions.options!.structures.rest - this.rest = customRest - ? new customRest(manager, options, this) - : new RainlinkRest(manager, options, this) - this.wsEvent = new RainlinkPlayerEvents() - this.stats = { - players: 0, - playingPlayers: 0, - uptime: 0, - memory: { - free: 0, - used: 0, - allocated: 0, - reservable: 0, - }, - cpu: { - cores: 0, - systemLoad: 0, - lavalinkLoad: 0, - }, - frameStats: { - sent: 0, - nulled: 0, - deficit: 0, - }, - } - } - - /** Connect this lavalink server */ - public connect(): RainlinkWebsocket { - return this.driver.connect() - } - - /** @ignore */ - public wsOpenEvent() { - this.clean(true) - this.state = RainlinkConnectState.Connected - this.debug(`Node connected! URL: ${this.driver.wsUrl}`) - this.manager.emit(RainlinkEvents.NodeConnect, this) - } - - /** @ignore */ - public wsMessageEvent(data: Record) { - switch (data.op) { - case LavalinkEventsEnum.Ready: { - const isResume = this.manager.rainlinkOptions.options!.resume - const timeout = this.manager.rainlinkOptions.options?.resumeTimeout - this.driver.sessionId = data.sessionId - const customRest = - this.manager.rainlinkOptions.options!.structures && - this.manager.rainlinkOptions.options!.structures.rest - this.rest = customRest - ? new customRest(this.manager, this.options, this) - : new RainlinkRest(this.manager, this.options, this) - if (isResume && timeout) { - this.driver.updateSession(data.sessionId, isResume, timeout) - } - break - } - case LavalinkEventsEnum.Event: { - this.wsEvent.initial(data, this.manager) - break - } - case LavalinkEventsEnum.PlayerUpdate: { - this.wsEvent.initial(data, this.manager) - break - } - case LavalinkEventsEnum.Status: { - this.stats = this.updateStatusData(data as LavalinkNodeStatsResponse) - break - } - } - } - - /** @ignore */ - public wsErrorEvent(logs: Error) { - this.debug(`Node errored! URL: ${this.driver.wsUrl}`) - this.manager.emit(RainlinkEvents.NodeError, this, logs) - } - - /** @ignore */ - public async wsCloseEvent(code: number, reason: Buffer) { - this.online = false - this.state = RainlinkConnectState.Disconnected - this.debug(`Node disconnected! URL: ${this.driver.wsUrl}`) - this.manager.emit(RainlinkEvents.NodeDisconnect, this, code, reason) - if ( - !this.sudoDisconnect && - this.retryCounter !== this.manager.rainlinkOptions.options!.retryCount - ) { - await setTimeout(this.manager.rainlinkOptions.options!.retryTimeout) - this.retryCounter = this.retryCounter + 1 - this.reconnect(true) - return - } - this.nodeClosed() - return - } - - protected nodeClosed() { - this.manager.emit(RainlinkEvents.NodeClosed, this) - this.debug(`Node closed! URL: ${this.driver.wsUrl}`) - this.clean() - } - - protected updateStatusData(data: LavalinkNodeStatsResponse): NodeStats { - return { - players: data.players ?? this.stats.players, - playingPlayers: data.playingPlayers ?? this.stats.playingPlayers, - uptime: data.uptime ?? this.stats.uptime, - memory: data.memory ?? this.stats.memory, - cpu: data.cpu ?? this.stats.cpu, - frameStats: data.frameStats ?? this.stats.frameStats, - } - } - - /** Disconnect this lavalink server */ - public disconnect() { - this.sudoDisconnect = true - this.driver.wsClose() - } - - /** Reconnect back to this lavalink server */ - public reconnect(noClean: boolean) { - if (!noClean) this.clean() - this.driver.connect() - } - - /** Clean all the lavalink server state and set to default value */ - public clean(online: boolean = false) { - this.sudoDisconnect = false - this.retryCounter = 0 - this.online = online - this.state = RainlinkConnectState.Closed - } - - protected debug(logs: string) { - this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [Node @ ${this.options.name}] | ${logs}`) - } -} diff --git a/src/rainlink/Node/RainlinkPlayerEvents.ts b/src/rainlink/Node/RainlinkPlayerEvents.ts deleted file mode 100644 index 1e138249..00000000 --- a/src/rainlink/Node/RainlinkPlayerEvents.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { RainlinkEvents, RainlinkLoopMode, RainlinkPlayerState } from '../Interface/Constants.js' -import { LavalinkEventsEnum } from '../Interface/LavalinkEvents.js' -import { Rainlink } from '../Rainlink.js' - -export class RainlinkPlayerEvents { - protected readonly methods: Record) => void> - - constructor() { - this.methods = { - TrackStartEvent: this.TrackStartEvent, - TrackEndEvent: this.TrackEndEvent, - TrackExceptionEvent: this.TrackExceptionEvent, - TrackStuckEvent: this.TrackStuckEvent, - WebSocketClosedEvent: this.WebSocketClosedEvent, - } - } - - public initial(data: Record, manager: Rainlink) { - if (data.op == LavalinkEventsEnum.PlayerUpdate) return this.PlayerUpdate(manager, data) - const _function = this.methods[data.type] - if (_function !== undefined) _function(manager, data) - } - - protected TrackStartEvent(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - player.playing = true - player.paused = false - manager.emit(RainlinkEvents.TrackStart, player, player.queue.current) - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [Start] | ` + JSON.stringify(data) - ) - } - return - } - - protected TrackEndEvent(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - // This event emits STOPPED reason when destroying, so return to prevent double emit - if (player.state === RainlinkPlayerState.DESTROYED) - return manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [End] | Player ${player.guildId} destroyed from end event` - ) - - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [End] | ` + - `Tracks: ${player.queue.length} ` + - JSON.stringify(data) - ) - - player.playing = false - player.paused = true - - if (data.reason === 'replaced') { - return manager.emit(RainlinkEvents.TrackEnd, player, player.queue.current) - } - if (['loadFailed', 'cleanup'].includes(data.reason)) { - if (player.queue.current) player.queue.previous.push(player.queue.current) - if (!player.queue.length && !player.sudoDestroy) - return manager.emit(RainlinkEvents.QueueEmpty, player) - manager.emit(RainlinkEvents.QueueEmpty, player, player.queue.current) - player.queue.current = null - return player.play() - } - - if (player.loop == RainlinkLoopMode.SONG && player.queue.current) - player.queue.unshift(player.queue.current) - if (player.loop == RainlinkLoopMode.QUEUE && player.queue.current) - player.queue.push(player.queue.current) - - if (player.queue.current) player.queue.previous.push(player.queue.current) - const currentSong = player.queue.current - player.queue.current = null - - if (player.queue.length) { - manager.emit(RainlinkEvents.TrackEnd, player, currentSong) - } else if (!player.queue.length && !player.sudoDestroy) { - return manager.emit(RainlinkEvents.QueueEmpty, player) - } else return - - return player.play() - } - return - } - - protected TrackExceptionEvent(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - manager.emit(RainlinkEvents.PlayerException, player, data) - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [Exception] | ` + JSON.stringify(data) - ) - } - return - } - - protected TrackStuckEvent(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - manager.emit(RainlinkEvents.TrackStuck, player, data) - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [Stuck] | ` + JSON.stringify(data) - ) - } - return - } - - protected WebSocketClosedEvent(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - manager.emit(RainlinkEvents.PlayerWebsocketClosed, player, data) - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [WebsocketClosed] | ` + - JSON.stringify(data) - ) - } - return - } - - protected PlayerUpdate(manager: Rainlink, data: Record) { - const player = manager.players.get(data.guildId) - if (player) { - player.position = Number(data.state.position) - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${data.guildId}] / [Events] / [Updated] | ` + JSON.stringify(data) - ) - manager.emit(RainlinkEvents.PlayerUpdate, player, data) - } - return - } -} diff --git a/src/rainlink/Node/RainlinkRest.ts b/src/rainlink/Node/RainlinkRest.ts deleted file mode 100644 index ee802eb1..00000000 --- a/src/rainlink/Node/RainlinkRest.ts +++ /dev/null @@ -1,175 +0,0 @@ -// Modded from: https://github.com/shipgirlproject/Shoukaku/blob/396aa531096eda327ade0f473f9807576e9ae9df/src/connectors/Connector.ts -// Special thanks to shipgirlproject team! - -import { RainlinkNodeOptions } from '../Interface/Manager.js' -import { Rainlink } from '../Rainlink.js' -import { LavalinkLoadType } from '../Interface/Constants.js' -import { RainlinkNode } from './RainlinkNode.js' -import { - LavalinkPlayer, - LavalinkResponse, - LavalinkStats, - RainlinkRequesterOptions, - RawTrack, - RoutePlanner, - UpdatePlayerInfo, -} from '../Interface/Rest.js' -import { NodeInfo } from '../Interface/Node.js' - -export class RainlinkRest { - /** The rainlink manager */ - public manager: Rainlink - protected options: RainlinkNodeOptions - /** The node manager (RainlinkNode class) */ - public nodeManager: RainlinkNode - protected sessionId: string | null - - /** - * The lavalink rest server handler class - * @param manager The rainlink manager - * @param options The rainlink node options, from RainlinkNodeOptions interface - * @param nodeManager The rainlink's lavalink server handler class - */ - constructor(manager: Rainlink, options: RainlinkNodeOptions, nodeManager: RainlinkNode) { - this.manager = manager - this.options = options - this.nodeManager = nodeManager - this.sessionId = this.nodeManager.driver.sessionId ? this.nodeManager.driver.sessionId : '' - } - - /** - * Gets all the player with the specified sessionId - * @returns Promise that resolves to an array of Lavalink players - */ - public async getPlayers(): Promise { - const options: RainlinkRequesterOptions = { - path: `/sessions/${this.sessionId}/players`, - 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 - */ - public async decodeTrack(base64track: string): Promise { - const options: RainlinkRequesterOptions = { - path: `/decodetrack?encodedTrack=${encodeURIComponent(base64track)}`, - headers: { 'content-type': 'application/json' }, - } - return await this.nodeManager.driver.requester(options) - } - - /** - * Updates a Lavalink player - * @returns Promise that resolves to a Lavalink player - */ - public updatePlayer(data: UpdatePlayerInfo): void { - const options: RainlinkRequesterOptions = { - path: `/sessions/${this.sessionId}/players/${data.guildId}`, - params: { noReplace: data.noReplace?.toString() || 'false' }, - headers: { 'content-type': 'application/json' }, - method: 'PATCH', - data: data.playerOptions as Record, - rawReqData: data, - } - this.nodeManager.driver.requester(options) - } - - /** - * Destroy a Lavalink player - * @returns Promise that resolves to a Lavalink player - */ - public destroyPlayer(guildId: string): void { - const options: RainlinkRequesterOptions = { - path: `/sessions/${this.sessionId}/players/${guildId}`, - headers: { 'content-type': 'application/json' }, - method: 'DELETE', - } - this.nodeManager.driver.requester(options) - } - - /** - * A track resolver function to get track from lavalink - * @returns LavalinkResponse - */ - public async resolver(data: string): Promise { - const options: RainlinkRequesterOptions = { - path: '/loadtracks', - params: { identifier: data }, - headers: { 'content-type': 'application/json' }, - method: 'GET', - } - - const resData = await this.nodeManager.driver.requester(options) - - if (!resData) { - return { - loadType: LavalinkLoadType.EMPTY, - data: {}, - } - } else return resData - } - - /** - * Get routeplanner status from Lavalink - * @returns Promise that resolves to a routeplanner response - */ - public async getRoutePlannerStatus(): Promise { - const options = { - path: '/routeplanner/status', - headers: { 'content-type': 'application/json' }, - } - return await this.nodeManager.driver.requester(options) - } - - /** - * Release blacklisted IP address into pool of IPs - * @param address IP address - */ - public async unmarkFailedAddress(address: string): Promise { - const options = { - path: '/routeplanner/free/address', - method: 'POST', - headers: { 'content-type': 'application/json' }, - data: { address }, - } - await this.nodeManager.driver.requester(options) - } - - /** - * Get Lavalink info - */ - public getInfo(): Promise { - const options = { - path: '/info', - headers: { 'content-type': 'application/json' }, - } - return this.nodeManager.driver.requester(options) - } - - protected testJSON(text: string) { - if (typeof text !== 'string') { - return false - } - try { - JSON.parse(text) - return true - } catch (error) { - return false - } - } -} diff --git a/src/rainlink/Player/RainlinkFilter.ts b/src/rainlink/Player/RainlinkFilter.ts deleted file mode 100644 index 7cf213e6..00000000 --- a/src/rainlink/Player/RainlinkFilter.ts +++ /dev/null @@ -1,184 +0,0 @@ -import util from 'node:util' -import { - RainlinkEvents, - RainlinkFilterData, - RainlinkFilterMode, - RainlinkPlayerState, -} from '../Interface/Constants.js' -import { - Band, - ChannelMix, - Distortion, - FilterOptions, - Freq, - Karaoke, - LowPass, - Rotation, - Timescale, -} from '../Interface/Player.js' -import { RainlinkPlayer } from './RainlinkPlayer.js' - -export class RainlinkFilter { - constructor(protected player: RainlinkPlayer) {} - - /** - * Set a filter that prebuilt in rainlink - * @param filter The filter name - * @returns RainlinkPlayer - */ - public async set(filter: RainlinkFilterMode): Promise { - this.checkDestroyed() - - const filterData = RainlinkFilterData[filter] - - if (!filterData) { - this.debug(`Filter ${filter} not avaliable in Rainlink's filter prebuilt`) - return this.player - } - - await this.player.send({ - guildId: this.player.guildId, - playerOptions: { - filters: filterData, - }, - }) - - this.debug( - filter !== 'clear' - ? `${filter} filter has been successfully set.` - : 'All filters have been successfully reset to their default positions.' - ) - - return this.player - } - - /** - * Clear all the filter - * @returns RainlinkPlayer - */ - public async clear(): Promise { - this.checkDestroyed() - - await this.player.send({ - guildId: this.player.guildId, - playerOptions: { - filters: {}, - }, - }) - - this.debug('All filters have been successfully reset to their default positions.') - - return this.player - } - - /** - * Sets the filter volume of the player - * @param volume Target volume 0.0-5.0 - */ - public async setVolume(volume: number): Promise { - return this.setRaw({ volume }) - } - - /** - * Change the equalizer settings applied to the currently playing track - * @param equalizer An array of objects that conforms to the Bands type that define volumes at different frequencies - */ - public setEqualizer(equalizer: Band[]): Promise { - return this.setRaw({ equalizer }) - } - - /** - * Change the karaoke settings applied to the currently playing track - * @param karaoke An object that conforms to the KaraokeSettings type that defines a range of frequencies to mute - */ - public setKaraoke(karaoke?: Karaoke): Promise { - return this.setRaw({ karaoke: karaoke || null }) - } - - /** - * Change the timescale settings applied to the currently playing track - * @param timescale An object that conforms to the TimescaleSettings type that defines the time signature to play the audio at - */ - public setTimescale(timescale?: Timescale): Promise { - return this.setRaw({ timescale: timescale || null }) - } - - /** - * Change the tremolo settings applied to the currently playing track - * @param tremolo An object that conforms to the FreqSettings type that defines an oscillation in volume - */ - public setTremolo(tremolo?: Freq): Promise { - return this.setRaw({ tremolo: tremolo || null }) - } - - /** - * Change the vibrato settings applied to the currently playing track - * @param vibrato An object that conforms to the FreqSettings type that defines an oscillation in pitch - */ - public setVibrato(vibrato?: Freq): Promise { - return this.setRaw({ vibrato: vibrato || null }) - } - - /** - * Change the rotation settings applied to the currently playing track - * @param rotation An object that conforms to the RotationSettings type that defines the frequency of audio rotating round the listener - */ - public setRotation(rotation?: Rotation): Promise { - return this.setRaw({ rotation: rotation || null }) - } - - /** - * Change the distortion settings applied to the currently playing track - * @param distortion An object that conforms to DistortionSettings that defines distortions in the audio - * @returns The current player instance - */ - public setDistortion(distortion?: Distortion): Promise { - return this.setRaw({ distortion: distortion || null }) - } - - /** - * Change the channel mix settings applied to the currently playing track - * @param channelMix An object that conforms to ChannelMixSettings that defines how much the left and right channels affect each other (setting all factors to 0.5 causes both channels to get the same audio) - */ - public setChannelMix(channelMix?: ChannelMix): Promise { - return this.setRaw({ channelMix: channelMix || null }) - } - - /** - * Change the low pass settings applied to the currently playing track - * @param lowPass An object that conforms to LowPassSettings that defines the amount of suppression on higher frequencies - */ - public setLowPass(lowPass?: LowPass): Promise { - return this.setRaw({ lowPass: lowPass || null }) - } - - /** - * Set a custom filter - * @param filter The filter name - * @returns RainlinkPlayer - */ - public async setRaw(filter: FilterOptions): Promise { - this.checkDestroyed() - await this.player.send({ - guildId: this.player.guildId, - playerOptions: { - filters: filter, - }, - }) - - this.debug('Custom filter has been successfully set. Data: ' + util.inspect(filter)) - - return this.player - } - - protected debug(logs: string) { - this.player.manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${this.player.guildId}] / [Filter] | ${logs}` - ) - } - - protected checkDestroyed(): void { - if (this.player.state === RainlinkPlayerState.DESTROYED) throw new Error('Player is destroyed') - } -} diff --git a/src/rainlink/Player/RainlinkPlayer.ts b/src/rainlink/Player/RainlinkPlayer.ts deleted file mode 100644 index 4090d63b..00000000 --- a/src/rainlink/Player/RainlinkPlayer.ts +++ /dev/null @@ -1,765 +0,0 @@ -import { PlayOptions, VoiceChannelOptions } from '../Interface/Player.js' -import { Rainlink } from '../Rainlink.js' -import { RainlinkNode } from '../Node/RainlinkNode.js' -import { RainlinkQueue } from './RainlinkQueue.js' -import { - RainlinkEvents, - RainlinkFilterData, - RainlinkLoopMode, - RainlinkPlayerState, - VoiceConnectState, - VoiceState, -} from '../Interface/Constants.js' -import { RainlinkTrack } from './RainlinkTrack.js' -import { UpdatePlayerInfo, UpdatePlayerOptions } from '../Interface/Rest.js' -import { RainlinkSearchOptions, RainlinkSearchResult } from '../Interface/Manager.js' -import { RainlinkPlugin } from '../Plugin/VoiceReceiver/Plugin.js' -import { ServerUpdate, StateUpdatePartial } from '../Interface/Connection.js' -import { EventEmitter } from 'node:events' -import { RainlinkDatabase } from '../Utilities/RainlinkDatabase.js' -import { RainlinkFilter } from './RainlinkFilter.js' - -export declare interface RainlinkPlayer { - on(event: 'connectionUpdate', listener: (state: VoiceState) => void): this - emit(event: 'connectionUpdate', args: VoiceState) - removeAllListeners(): void -} - -export class RainlinkPlayer extends EventEmitter { - /** - * Main manager class - */ - public manager: Rainlink - /** - * Player's current using lavalink server - */ - public node: RainlinkNode - /** - * Player's guild id - */ - public guildId: string - /** - * Player's voice id - */ - public voiceId: string | null - /** - * Player's text id - */ - public textId: string - /** - * Player's queue - */ - public readonly queue: RainlinkQueue - /** - * The temporary database of player, u can set any thing here and us like Map class! - */ - public readonly data: RainlinkDatabase - /** - * Whether the player is paused or not - */ - public paused: boolean - /** - * Get the current track's position of the player - */ - public position: number - /** - * Get the current volume of the player - */ - public volume: number - /** - * Whether the player is playing or not - */ - public playing: boolean - /** - * Get the current loop mode of the player - */ - public loop: RainlinkLoopMode - /** - * Get the current state of the player - */ - public state: RainlinkPlayerState - /** - * Whether the player is deafened or not - */ - public deaf: boolean - /** - * Whether the player is muted or not - */ - public mute: boolean - /** - * ID of the current track - */ - public track: string | null - /** - * All function to extend support driver - */ - public functions: RainlinkDatabase<(...args: any) => unknown> - /** - * ID of the Shard that contains the guild that contains the connected voice channel - */ - public shardId: number - /** - * ID of the last voiceId connected to - */ - public lastvoiceId: string | null - /** - * ID of current session - */ - public sessionId: string | null - /** - * Region of connected voice channel - */ - public region: string | null - /** - * Last region of the connected voice channel - */ - public lastRegion: string | null - /** - * Cached serverUpdate event from Lavalink - */ - public serverUpdate: ServerUpdate | null - /** - * Connection state - */ - public voiceState: VoiceConnectState - /** @ignore */ - public sudoDestroy: boolean - public filter: RainlinkFilter - - /** - * The rainlink player handler class - * @param manager The rainlink manager - * @param voiceOptions The rainlink voice option, use VoiceChannelOptions interface - * @param node The rainlink current use node - */ - constructor(manager: Rainlink, voiceOptions: VoiceChannelOptions, node: RainlinkNode) { - super() - this.manager = manager - this.guildId = voiceOptions.guildId - this.voiceId = voiceOptions.voiceId - this.shardId = voiceOptions.shardId - this.mute = voiceOptions.mute ?? false - this.deaf = voiceOptions.deaf ?? false - this.lastvoiceId = null - this.sessionId = null - this.region = null - this.lastRegion = null - this.serverUpdate = null - this.voiceState = VoiceConnectState.DISCONNECTED - this.node = node - this.guildId = voiceOptions.guildId - this.voiceId = voiceOptions.voiceId - this.textId = voiceOptions.textId - const customQueue = - this.manager.rainlinkOptions.options!.structures && - this.manager.rainlinkOptions.options!.structures.queue - this.queue = customQueue - ? new customQueue(this.manager, this) - : new RainlinkQueue(this.manager, this) - this.filter = new RainlinkFilter(this) - this.data = new RainlinkDatabase() - this.paused = true - this.position = 0 - this.volume = this.manager.rainlinkOptions.options!.defaultVolume! - this.playing = false - this.loop = RainlinkLoopMode.NONE - this.state = RainlinkPlayerState.DESTROYED - this.deaf = voiceOptions.deaf ?? false - this.mute = voiceOptions.mute ?? false - this.sudoDestroy = false - this.track = null - this.functions = new RainlinkDatabase<(...args: any) => unknown>() - if (this.node.driver.playerFunctions.size !== 0) { - this.node.driver.playerFunctions.forEach((data, index) => { - this.functions.set(index, data.bind(null, this)) - }) - } - if (voiceOptions.volume && voiceOptions.volume !== this.volume) - this.volume = voiceOptions.volume - } - - /** - * Sends server update to lavalink - * @internal - */ - public async sendServerUpdate(): Promise { - const playerUpdate = { - guildId: this.guildId, - playerOptions: { - voice: { - token: this.serverUpdate!.token, - endpoint: this.serverUpdate!.endpoint, - sessionId: this.sessionId!, - }, - }, - } - this.node.rest.updatePlayer(playerUpdate) - } - - /** - * Destroy the player - * @internal - */ - public async destroy(): Promise { - this.checkDestroyed() - this.sudoDestroy = true - this.clear(false) - this.disconnect() - const voiceReceiver = this.manager.plugins.get('rainlink-voiceReceiver') as RainlinkPlugin - if (voiceReceiver && this.node.driver.id.includes('nodelink')) voiceReceiver.close(this.guildId) - this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - track: { - encoded: null, - length: 0, - }, - }, - }) - this.node.rest.destroyPlayer(this.guildId) - this.manager.players.delete(this.guildId) - this.state = RainlinkPlayerState.DESTROYED - this.debug('Player destroyed at ' + this.guildId) - this.voiceId = '' - this.manager.emit(RainlinkEvents.PlayerDestroy, this) - this.sudoDestroy = false - } - - /** - * Play a track - * @param track Track to play - * @param options Play options - * @returns RainlinkPlayer - */ - public async play(track?: RainlinkTrack, options?: PlayOptions): Promise { - this.checkDestroyed() - - if (track && !(track instanceof RainlinkTrack)) throw new Error('track must be a RainlinkTrack') - - if (!track && !this.queue.totalSize) throw new Error('No track is available to play') - - if (!options || typeof options.replaceCurrent !== 'boolean') - options = { ...options, replaceCurrent: false } - - if (track) { - if (!options.replaceCurrent && this.queue.current) this.queue.unshift(this.queue.current) - this.queue.current = track - } else if (!this.queue.current) this.queue.current = this.queue.shift() - - if (!this.queue.current) throw new Error('No track is available to play') - - const current = this.queue.current - - let errorMessage: string | undefined - - const resolveResult = await current - .resolver(this.manager, { nodeName: this.node.options.name }) - .catch((e: any) => { - errorMessage = e.message - return null - }) - - if (!resolveResult || (resolveResult && !resolveResult.isPlayable)) { - this.manager.emit(RainlinkEvents.TrackResolveError, this, current, errorMessage) - this.debug(`Player ${this.guildId} resolve error: ${errorMessage}`) - this.queue.current = null - - this.queue.size ? await this.play() : this.manager.emit(RainlinkEvents.QueueEmpty, this) - return this - } - - this.playing = true - this.track = current.encoded - - const playerOptions: UpdatePlayerOptions = { - track: { - encoded: current.encoded, - length: current.duration, - }, - ...options, - volume: this.volume, - } - - if (playerOptions.paused) { - this.paused = playerOptions.paused - this.playing = !this.paused - } - if (playerOptions.position) this.position = playerOptions.position - - this.node.rest.updatePlayer({ - guildId: this.guildId, - noReplace: options?.noReplace ?? false, - playerOptions, - }) - - return this - } - - /** - * Set the loop mode of the track - * @param mode Mode to loop - * @returns RainlinkPlayer - */ - public setLoop(mode: RainlinkLoopMode): RainlinkPlayer { - this.checkDestroyed() - this.loop = mode - return this - } - - /** - * Search track directly from player - * @param query The track search query link - * @param options The track search options - * @returns RainlinkSearchResult - */ - public async search( - query: string, - options?: RainlinkSearchOptions - ): Promise { - this.checkDestroyed() - return await this.manager.search(query, options) - } - - /** - * Pause the track - * @returns RainlinkPlayer - */ - public async pause(): Promise { - this.checkDestroyed() - if (this.paused == true) return this - await this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - paused: true, - }, - }) - this.paused = true - this.playing = false - this.manager.emit(RainlinkEvents.PlayerPause, this, this.queue.current) - return this - } - - /** - * Resume the track - * @returns RainlinkPlayer - */ - public async resume(): Promise { - this.checkDestroyed() - if (this.paused == false) return this - this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - paused: false, - }, - }) - this.paused = false - this.playing = true - this.manager.emit(RainlinkEvents.PlayerResume, this, this.queue.current) - return this - } - - /** - * Pause or resume a track but different method - * @param mode Whether to pause or not - * @returns RainlinkPlayer - */ - public async setPause(mode: boolean): Promise { - this.checkDestroyed() - if (this.paused == mode) return this - await this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - paused: mode, - }, - }) - this.paused = mode - this.playing = !mode - this.manager.emit( - mode ? RainlinkEvents.PlayerPause : RainlinkEvents.PlayerResume, - this, - this.queue.current - ) - return this - } - - /** - * Play the previous track - * @returns RainlinkPlayer - */ - public async previous(): Promise { - this.checkDestroyed() - const prevoiusData = this.queue.previous - const current = this.queue.current - const index = prevoiusData.length - 1 - if (index === -1 || !current) return this - await this.play(prevoiusData[index]) - this.queue.previous.splice(index, 1) - return this - } - - /** - * Get all previous track - * @returns RainlinkTrack[] - */ - public getPrevious(): RainlinkTrack[] { - this.checkDestroyed() - return this.queue.previous - } - - /** - * Skip the current track - * @returns RainlinkPlayer - */ - public async skip(): Promise { - this.checkDestroyed() - this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - track: { - encoded: null, - }, - }, - }) - return this - } - - /** - * Seek to another position in track - * @param position Position to seek - * @returns RainlinkPlayer - */ - public async seek(position: number): Promise { - this.checkDestroyed() - if (!this.queue.current) throw new Error("Player has no current track in it's queue") - if (!this.queue.current.isSeekable) throw new Error("The current track isn't seekable") - - position = Number(position) - - if (isNaN(position)) throw new Error('position must be a number') - if (position < 0 || position > (this.queue.current.duration ?? 0)) - position = Math.max(Math.min(position, this.queue.current.duration ?? 0), 0) - - await this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - position: position, - }, - }) - this.queue.current.position = position - return this - } - - /** - * Set another volume in player - * @param volume Volume to cange - * @returns RainlinkPlayer - */ - public async setVolume(volume: number): Promise { - this.checkDestroyed() - if (isNaN(volume)) throw new Error('volume must be a number') - await this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - volume: volume, - }, - }) - this.volume = volume - return this - } - - /** - * Set player to mute or unmute - * @param enable Enable or not - * @returns RainlinkPlayer - */ - public setMute(enable: boolean): RainlinkPlayer { - this.checkDestroyed() - if (enable == this.mute) return this - this.mute = enable - this.sendVoiceUpdate() - return this - } - - /** - * Stop all avtivities and reset to default - * @param destroy Whenever you want to destroy a player or not - * @returns RainlinkPlayer - */ - public async stop(destroy: boolean): Promise { - this.checkDestroyed() - - if (destroy) { - await this.destroy() - return this - } - - this.clear(false) - - this.node.rest.updatePlayer({ - guildId: this.guildId, - playerOptions: { - track: { - encoded: null, - }, - }, - }) - this.manager.emit(RainlinkEvents.TrackEnd, this, this.queue.current) - this.manager.emit(RainlinkEvents.PlayerStop, this) - return this - } - - /** - * Reset all data to default - * @param Whenever emit empty event or not - * @inverval - */ - public clear(emitEmpty: boolean): void { - this.loop = RainlinkLoopMode.NONE - this.queue.clear() - this.queue.current = undefined - this.queue.previous.length = 0 - this.volume = this.manager.rainlinkOptions!.options!.defaultVolume ?? 100 - this.paused = true - this.playing = false - this.track = null - if (!this.data.get('sudo-destroy')) this.data.clear() - this.position = 0 - if (emitEmpty) this.manager.emit(RainlinkEvents.QueueEmpty, this) - return - } - - /** - * Set player to deaf or undeaf - * @param enable Enable or not - * @returns RainlinkPlayer - */ - public setDeaf(enable: boolean): RainlinkPlayer { - this.checkDestroyed() - if (enable == this.deaf) return this - this.deaf = enable - this.sendVoiceUpdate() - return this - } - - /** - * Disconnect from the voice channel - * @returns RainlinkPlayer - */ - public disconnect(): RainlinkPlayer { - this.checkDestroyed() - if (this.voiceState === VoiceConnectState.DISCONNECTED) return this - this.voiceId = null - this.deaf = false - this.mute = false - this.removeAllListeners() - this.sendVoiceUpdate() - this.voiceState = VoiceConnectState.DISCONNECTED - this.pause() - this.state = RainlinkPlayerState.DISCONNECTED - this.debug(`Player disconnected; Guild id: ${this.guildId}`) - return this - } - - /** - * Connect from the voice channel - * @returns RainlinkPlayer - */ - public async connect(): Promise { - if (this.state === RainlinkPlayerState.CONNECTED || !this.voiceId) return this - if ( - this.voiceState === VoiceConnectState.CONNECTING || - this.voiceState === VoiceConnectState.CONNECTED - ) - return this - this.voiceState = VoiceConnectState.CONNECTING - this.sendVoiceUpdate() - this.debugDiscord(`Requesting Connection | Guild: ${this.guildId}`) - const controller = new AbortController() - const timeout = setTimeout( - () => controller.abort(), - this.manager.rainlinkOptions.options!.voiceConnectionTimeout - ) - try { - // @ts-ignore - const [status] = await RainlinkPlayer.once(this, 'connectionUpdate', { - signal: controller.signal, - }) - if (status !== VoiceState.SESSION_READY) { - switch (status) { - case VoiceState.SESSION_ID_MISSING: - throw new Error('The voice connection is not established due to missing session id') - case VoiceState.SESSION_ENDPOINT_MISSING: - throw new Error( - 'The voice connection is not established due to missing connection endpoint' - ) - } - } - this.voiceState = VoiceConnectState.CONNECTED - } catch (error: any) { - this.debugDiscord(`Request Connection Failed | Guild: ${this.guildId}`) - if (error.name === 'AbortError') - throw new Error( - `The voice connection is not established in ${this.manager.rainlinkOptions.options!.voiceConnectionTimeout}ms` - ) - throw error - } finally { - clearTimeout(timeout) - this.state = RainlinkPlayerState.CONNECTED - this.debug(`Player ${this.guildId} connected`) - } - return this - } - - /** - * Set text channel - * @param textId Text channel ID - * @returns RainlinkPlayer - */ - public setTextChannel(textId: string): RainlinkPlayer { - this.checkDestroyed() - this.textId = textId - return this - } - - /** - * Set voice channel and move the player to the voice channel - * @param voiceId Voice channel ID - * @returns RainlinkPlayer - */ - public setVoiceChannel(voiceId: string): RainlinkPlayer { - this.checkDestroyed() - this.disconnect() - this.voiceId = voiceId - this.connect() - this.debugDiscord(`Player ${this.guildId} moved to voice channel ${voiceId}`) - return this - } - - /** - * Set a filter that prebuilt in rainlink - * @param filter The filter name - * @returns RainlinkPlayer - */ - public async setFilter(filter: keyof typeof RainlinkFilterData): Promise { - this.checkDestroyed() - - const filterData = RainlinkFilterData[filter as keyof typeof RainlinkFilterData] - - if (!filterData) throw new Error('Filter not found') - - await this.send({ - guildId: this.guildId, - playerOptions: { - filters: filterData, - }, - }) - - return this - } - - /** - * Send custom player update data to lavalink server - * @param data Data to change - * @returns RainlinkPlayer - */ - public send(data: UpdatePlayerInfo): RainlinkPlayer { - this.checkDestroyed() - this.node.rest.updatePlayer(data) - return this - } - - protected debug(logs: string): void { - this.manager.emit(RainlinkEvents.Debug, `[Rainlink] / [Player @ ${this.guildId}] | ${logs}`) - } - - protected debugDiscord(logs: string): void { - this.manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Player @ ${this.guildId}] / [Voice] | ${logs}` - ) - } - - protected checkDestroyed(): void { - if (this.state === RainlinkPlayerState.DESTROYED) throw new Error('Player is destroyed') - } - - /** - * Send voice data to discord - * @internal - */ - public sendVoiceUpdate() { - this.sendDiscord({ - guild_id: this.guildId, - channel_id: this.voiceId, - self_deaf: this.deaf, - self_mute: this.mute, - }) - } - - /** - * Send data to Discord - * @param data The data to send - * @internal - */ - public sendDiscord(data: any): void { - this.manager.library.sendPacket(this.shardId, { op: 4, d: data }, false) - } - - /** - * Sets the server update data for this connection - * @internal - */ - public setServerUpdate(data: ServerUpdate): void { - if (!data.endpoint) { - this.emit('connectionUpdate', VoiceState.SESSION_ENDPOINT_MISSING) - return - } - if (!this.sessionId) { - this.emit('connectionUpdate', VoiceState.SESSION_ID_MISSING) - return - } - - this.lastRegion = this.region?.repeat(1) || null - this.region = data.endpoint.split('.').shift()?.replace(/[0-9]/g, '') || null - - if (this.region && this.lastRegion !== this.region) { - this.debugDiscord( - `Voice Region Moved | Old Region: ${this.lastRegion} New Region: ${this.region} Guild: ${this.guildId}` - ) - } - - this.serverUpdate = data - this.emit('connectionUpdate', VoiceState.SESSION_READY) - this.debugDiscord(`Server Update Received | Server: ${this.region} Guild: ${this.guildId}`) - } - - /** - * Update Session ID, Channel ID, Deafen status and Mute status of this instance - * @internal - */ - public setStateUpdate({ - session_id, - channel_id, - self_deaf, - self_mute, - }: StateUpdatePartial): void { - this.lastvoiceId = this.voiceId?.repeat(1) || null - this.voiceId = channel_id || null - - if (this.voiceId && this.lastvoiceId !== this.voiceId) { - this.debugDiscord(`Channel Moved | Old Channel: ${this.voiceId} Guild: ${this.guildId}`) - } - - if (!this.voiceId) { - this.voiceState = VoiceConnectState.DISCONNECTED - this.debugDiscord(`Channel Disconnected | Guild: ${this.guildId}`) - } - - this.deaf = self_deaf - this.mute = self_mute - this.sessionId = session_id || null - this.debugDiscord( - `State Update Received | Channel: ${this.voiceId} Session ID: ${session_id} Guild: ${this.guildId}` - ) - } -} diff --git a/src/rainlink/Player/RainlinkQueue.ts b/src/rainlink/Player/RainlinkQueue.ts deleted file mode 100644 index 8869caa5..00000000 --- a/src/rainlink/Player/RainlinkQueue.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { RainlinkEvents } from '../Interface/Constants.js' -import { Rainlink } from '../Rainlink.js' -import { RainlinkPlayer } from './RainlinkPlayer.js' -import { RainlinkTrack } from './RainlinkTrack.js' - -export class RainlinkQueue extends Array { - /** Rainlink manager */ - manager: Rainlink - /** Rainlink player */ - player: RainlinkPlayer - - /** - * The rainlink track queue handler class - * @param manager The rainlink manager - * @param player The current rainlink player - */ - constructor(manager: Rainlink, player: RainlinkPlayer) { - super() - this.manager = manager - this.player = player - } - - /** Get the size of queue */ - public get size() { - return this.length - } - - /** Get the size of queue including current */ - public get totalSize(): number { - return this.length + (this.current ? 1 : 0) - } - - /** Check if the queue is empty or not */ - public get isEmpty() { - return this.length === 0 - } - - /** Get the queue's duration */ - public get duration() { - return this.reduce((acc, cur) => acc + (cur.duration || 0), 0) - } - - /** Current playing track */ - public current: RainlinkTrack | undefined | null = null - /** Previous playing tracks */ - public previous: RainlinkTrack[] = [] - - /** - * Add track(s) to the queue - * @param track RainlinkTrack to add - * @returns RainlinkQueue - */ - public add(track: RainlinkTrack | RainlinkTrack[]): RainlinkQueue { - if (Array.isArray(track) && track.some((t) => !(t instanceof RainlinkTrack))) - throw new Error('Track must be an instance of RainlinkTrack') - if (!Array.isArray(track) && !(track instanceof RainlinkTrack)) track = [track] - - if (!this.current) { - if (Array.isArray(track)) this.current = track.shift() - else { - this.current = track - return this - } - } - - if (Array.isArray(track)) for (const t of track) this.push(t) - else this.push(track) - this.manager.emit(RainlinkEvents.QueueAdd, this.player, this, track) - return this - } - - /** - * Remove track from the queue - * @param position Position of the track - * @returns RainlinkQueue - */ - public remove(position: number): RainlinkQueue { - if (position < 0 || position >= this.length) - throw new Error('Position must be between 0 and ' + (this.length - 1)) - const track = this[position] - this.splice(position, 1) - this.manager.emit(RainlinkEvents.QueueRemove, this.player, this, track) - return this - } - - /** Shuffle the queue */ - public shuffle(): RainlinkQueue { - for (let i = this.length - 1; i > 0; i--) { - const j = Math.floor(Math.random() * (i + 1)) - ;[this[i], this[j]] = [this[j], this[i]] - } - this.manager.emit(RainlinkEvents.QueueShuffle, this.player, this) - return this - } - - /** Clear the queue */ - public clear(): RainlinkQueue { - this.splice(0, this.length) - this.manager.emit(RainlinkEvents.QueueClear, this.player, this) - return this - } -} diff --git a/src/rainlink/Player/RainlinkTrack.ts b/src/rainlink/Player/RainlinkTrack.ts deleted file mode 100644 index 3a35908b..00000000 --- a/src/rainlink/Player/RainlinkTrack.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { RainlinkEvents } from '../Interface/Constants.js' -import { RainlinkSearchResult, RainlinkSearchResultType } from '../Interface/Manager.js' -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 */ - encoded: string - /** Identifier string from lavalink */ - identifier: string - /** Whenever track is seekable or not */ - isSeekable: boolean - /** Track's author */ - author: string - /** Track's duration */ - duration: number - /** Whenever track is stream able or not */ - isStream: boolean - /** Track's position */ - position: number - /** Track's title */ - title: string - /** Track's URL */ - uri?: string - /** Track's artwork URL */ - artworkUrl?: string - /** Track's isrc */ - isrc?: string - /** Track's source name */ - source: string - /** Data from lavalink plugin */ - pluginInfo: unknown - /** Track's requester */ - requester: unknown - /** Track's realUri (youtube fall back) */ - realUri?: string - - /** - * The rainlink track class for playing track from lavalink - * @param options The raw track resolved from rest, use RawTrack interface - * @param requester The requester details of this track - */ - constructor( - protected options: RawTrack, - requester: unknown - ) { - this.encoded = options.encoded - this.identifier = options.info.identifier - this.isSeekable = options.info.isSeekable - this.author = options.info.author - this.duration = options.info.length - this.isStream = options.info.isStream - this.position = options.info.position - this.title = options.info.title - this.uri = options.info.uri - this.artworkUrl = options.info.artworkUrl - this.isrc = options.info.isrc - this.source = options.info.sourceName - this.pluginInfo = options.pluginInfo - this.requester = requester - this.realUri = undefined - } - - /** - * Whenever track is able to play or not - * @returns boolean - */ - get isPlayable(): boolean { - return ( - !!this.encoded && - !!this.source && - !!this.identifier && - !!this.author && - !!this.duration && - !!this.title && - !!this.uri - ) - } - - /** - * Get all raw details of the track - * @returns RawTrack - */ - get raw(): RawTrack { - return { - encoded: this.encoded, - info: { - identifier: this.identifier, - isSeekable: this.isSeekable, - author: this.author, - length: this.duration, - isStream: this.isStream, - position: this.position, - title: this.title, - uri: this.uri, - artworkUrl: this.artworkUrl, - isrc: this.isrc, - sourceName: this.source, - }, - pluginInfo: this.pluginInfo, - } - } - - /** - * Resolve the track - * @param options Resolve options - * @returns Promise - */ - public async resolver(manager: Rainlink, options?: ResolveOptions): Promise { - const { overwrite } = options ? options : { overwrite: false } - - if (this.isPlayable) { - this.realUri = this.raw.info.uri - return this - } - - manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Track] | Resolving ${this.source} track ${this.title}; Source: ${this.source}` - ) - - const result = await this.getTrack(manager, options ? options.nodeName : undefined) - if (!result) throw new Error('No results found') - - this.encoded = result.encoded - this.realUri = result.info.uri - this.duration = result.info.length - - if (overwrite) { - this.title = result.info.title - this.identifier = result.info.identifier - this.isSeekable = result.info.isSeekable - this.author = result.info.author - this.duration = result.info.length - this.isStream = result.info.isStream - this.uri = result.info.uri - } - return this - } - - 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, node) - - if (!result || !result.tracks.length) throw new Error('No results found') - - const rawTracks = result.tracks.map((x) => x.raw) - - if (this.author) { - const author = [this.author, `${this.author} - Topic`] - const officialTrack = rawTracks.find( - (track) => - author.some((name) => - new RegExp(`^${this.escapeRegExp(name)}$`, 'i').test(track.info.author) - ) || new RegExp(`^${this.escapeRegExp(this.title)}$`, 'i').test(track.info.title) - ) - if (officialTrack) return officialTrack - } - if (this.duration) { - const sameDuration = rawTracks.find( - (track) => - track.info.length >= (this.duration ? this.duration : 0) - 2000 && - track.info.length <= (this.duration ? this.duration : 0) + 2000 - ) - if (sameDuration) return sameDuration - } - - return rawTracks[0] - } - - protected escapeRegExp(string: string) { - return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&') - } - - 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(' - ') - const searchFallbackEngineName = manager.rainlinkOptions.options!.searchFallback!.engine - const searchFallbackEngine = manager.searchEngines.get(searchFallbackEngineName) - - 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 - } - - return { - type: RainlinkSearchResultType.SEARCH, - playlistName: undefined, - tracks: [], - } - } -} diff --git a/src/rainlink/Plugin/Apple/Plugin.ts b/src/rainlink/Plugin/Apple/Plugin.ts deleted file mode 100644 index b530be9d..00000000 --- a/src/rainlink/Plugin/Apple/Plugin.ts +++ /dev/null @@ -1,357 +0,0 @@ -import { - RainlinkSearchOptions, - RainlinkSearchResult, - RainlinkSearchResultType, -} from '../../Interface/Manager.js' -import { Rainlink } from '../../Rainlink.js' -import { RainlinkTrack } from '../../Player/RainlinkTrack.js' -import { SourceRainlinkPlugin } from '../SourceRainlinkPlugin.js' -import { RainlinkEvents, RainlinkPluginType } from '../../Interface/Constants.js' -import { fetch } from 'undici' - -const REGEX = - /(?:https:\/\/music\.apple\.com\/)(?:.+)?(artist|album|music-video|playlist)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)/ -const REGEX_SONG_ONLY = - /(?:https:\/\/music\.apple\.com\/)(?:.+)?(artist|album|music-video|playlist)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)\/([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)(\?|\&)([^=]+)\=([\w\-\.]+(\/)+[\w\-\.]+|[^&]+)/ - -type HeaderType = { - Authorization: string - origin: string -} - -/** The rainlink spotify plugin options */ -export type AppleOptions = { - /** The country code that u want to set content, eg: en */ - countryCode?: string - /** Artwork width */ - imageWidth?: number - /** Artwork height */ - imageHeight?: number -} - -const credentials = { - APPLE_TOKEN: - 'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IldlYlBsYXlLaWQifQ.eyJpc3MiOiJBTVBXZWJQbGF5IiwiaWF0IjoxNzA5Njg5NTM4LCJleHAiOjE3MTY5NDcxMzgsInJvb3RfaHR0cHNfb3JpZ2luIjpbImFwcGxlLmNvbSJdfQ.QuL8x1Wb-EjFfRUKmaUlAX4TchI4EmeqYt1tVm2OMvM5Lbmuv45ON5qIDYOPQEPALfaElh1lh3_6g5BNToJh6A', -} - -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 - - /** - * Source identify of the plugin - * @returns string - */ - public sourceIdentify(): string { - return 'am' - } - - /** - * Source name of the plugin - * @returns string - */ - public sourceName(): string { - return 'apple' - } - - /** - * Type of the plugin - * @returns RainlinkPluginType - */ - public type(): RainlinkPluginType { - return RainlinkPluginType.SourceResolver - } - - /** 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', - } - } - - /** - * 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) - } - - /** - * 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 - } - - /** - * 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 - } -} - -// Interfaces -/** @ignore */ -export interface Result { - tracks: RainlinkTrack[] - name?: string -} -/** @ignore */ -export interface Track { - id: string - type: string - href: string - attributes: TrackAttributes -} -/** @ignore */ -export interface TrackAttributes { - albumName: string - hasTimeSyncedLyrics: boolean - genreNames: any[] - trackNumber: number - releaseDate: string - durationInMillis: number - isVocalAttenuationAllowed: boolean - isMasteredForItunes: boolean - isrc: string - artwork: Record - audioLocale: string - composerName: string - url: string - playParams: Record - discNumber: number - hasCredits: boolean - hasLyrics: boolean - isAppleDigitalMaster: boolean - audioTraits: any[] - name: string - previews: any[] - artistName: string -} diff --git a/src/rainlink/Plugin/Deezer/Plugin.ts b/src/rainlink/Plugin/Deezer/Plugin.ts deleted file mode 100644 index 85640a05..00000000 --- a/src/rainlink/Plugin/Deezer/Plugin.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { - RainlinkSearchOptions, - RainlinkSearchResult, - RainlinkSearchResultType, -} from '../../Interface/Manager.js' -import { Rainlink } from '../../Rainlink.js' -import { RainlinkTrack } from '../../Player/RainlinkTrack.js' -import { SourceRainlinkPlugin } from '../SourceRainlinkPlugin.js' -import { RainlinkEvents, RainlinkPluginType } from '../../Interface/Constants.js' -import { fetch, request } from 'undici' - -const API_URL = 'https://api.deezer.com/' -const REGEX = /^https?:\/\/(?:www\.)?deezer\.com\/[a-z]+\/(track|album|playlist)\/(\d+)$/ -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> - /** - * Source identify of the plugin - * @returns string - */ - public sourceIdentify(): string { - return 'dz' - } - - /** - * Source name of the plugin - * @returns string - */ - public sourceName(): string { - return 'deezer' - } - - /** - * Type of the plugin - * @returns RainlinkPluginType - */ - 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 - } - - /** - * 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) - } - - /** - * 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 - } - - /** - * 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 - } -} - -// Interfaces -/** @ignore */ -interface Result { - tracks: RainlinkTrack[] - name?: string -} -/** @ignore */ -interface Album { - title: string - tracks: AlbumTracks -} -/** @ignore */ -interface AlbumTracks { - data: DeezerTrack[] - next: string | null -} -/** @ignore */ -interface Playlist { - tracks: PlaylistTracks - title: string -} -/** @ignore */ -interface PlaylistTracks { - data: DeezerTrack[] - next: string | null -} -/** @ignore */ -interface DeezerTrack { - data: RainlinkTrack[] -} -/** @ignore */ -interface SearchResult { - exception?: { - severity: string - message: string - } - loadType: string - playlist?: { - duration_ms: number - name: string - } - data: RainlinkTrack[] -} diff --git a/src/rainlink/Plugin/Nico/@types/NicoResolver.ts b/src/rainlink/Plugin/Nico/@types/NicoResolver.ts deleted file mode 100644 index ad7722ca..00000000 --- a/src/rainlink/Plugin/Nico/@types/NicoResolver.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Code from: -// https://github.com/y-chan/niconico-dl.js - -export interface NiconicoAPIData { - media: { - delivery: { - movie: { - session: { - videos: string[] - audios: string[] - heartbeatLifetime: number - recipeId: string - priority: number - urls: { - isWellKnownPort: boolean - isSsl: boolean - [key: string]: any - }[] - token: string - signature: string - contentId: string - authTypes: { - http: string - } - contentKeyTimeout: number - serviceUserId: string - playerId: string - [key: string]: any - } - [key: string]: any - } - [key: string]: any - } - [key: string]: any - } - video: OriginalVideoInfo - owner: OwnerInfo - [key: string]: any -} - -export interface OwnerInfo { - id: number - nickname: string - iconUrl: string - channel: string | null - live: { - id: string - title: string - url: string - begunAt: string - isVideoLive: boolean - videoLiveOnAirStartTime: string | null - thumbnailUrl: string | null - } | null - isVideoPublic: boolean - isMylistsPublic: boolean - videoLiveNotice: null - viewer: number | null -} - -export interface OriginalVideoInfo { - id: string - title: string - description: string - count: { - view: number - comment: number - mylist: number - like: number - } - duration: number - thumbnail: { - url: string - middleUrl: string - largeUrl: string - player: string - ogp: string - } - rating: { - isAdult: boolean - } - registerdAt: string - isPrivate: boolean - isDeleted: boolean - isNoBanner: boolean - isAuthenticationRequired: boolean - isEmbedPlayerAllowed: boolean - viewer: null - watchableUserTypeForPayment: string - commentableUserTypeForPayment: string - [key: string]: any -} - -export interface VideoInfo extends OriginalVideoInfo { - owner: OwnerInfo -} diff --git a/src/rainlink/Plugin/Nico/@types/NicoSearch.ts b/src/rainlink/Plugin/Nico/@types/NicoSearch.ts deleted file mode 100644 index 654c234e..00000000 --- a/src/rainlink/Plugin/Nico/@types/NicoSearch.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Code from: -// https://github.com/blackrose514/niconico-search-api - -export interface SearchAPIResponse { - meta: { - id: string - status: number - totalCount: number - } - data: ResponseData -} - -export interface ErrorResponse { - meta: { - id: string - status: number - errorCode: string - errorMessage: string - } -} - -export type ResponseData = F extends '*' - ? Omit[] - : F extends ResponseField[] - ? Pick[] - : never - -export interface SearchParams { - q: string - targets: Target[] - fields?: ResponseField[] | '*' - filters?: JsonFilter - sort: Sort - offset?: number - limit?: number - context?: string -} - -export type Target = 'title' | 'description' | 'tags' | 'tagsExact' -export type FilterField = Exclude< - keyof Fields, - 'contentId' | 'title' | 'description' | 'thumbnailUrl' | 'lastResBody' -> -export type ResponseField = Exclude -export type JsonFilter = EqualFilter | RangeFilter | AndFilter | OrFilter | NotFilter -export type Sort = `${'+' | '-'}${ - | 'viewCounter' - | 'mylistCounter' - | 'lengthSeconds' - | 'startTime' - | 'commentCounter' - | 'lastCommentTime'}` -export interface Fields { - contentId: string - title: string - description: string - viewCounter: number - mylistCounter: number - lengthSeconds: number - thumbnailUrl: string - startTime: string - lastResBody: string - commentCounter: number - lastCommentTime: string - categoryTags: string - tags: string - tagsExact: string - genre: string - // 'genre.keyword': string -} -export interface EqualFilter { - type: 'equal' - field: FilterField - value: string | number -} - -export interface RangeFilter { - type: 'range' - field: FilterField - from?: string | number - to?: string | number - include_lower?: boolean - include_upper?: boolean -} - -export interface AndFilter { - type: 'and' - filters: JsonFilter[] -} - -export interface OrFilter { - type: 'or' - filters: JsonFilter[] -} - -export interface NotFilter { - type: 'not' - filter: JsonFilter -} diff --git a/src/rainlink/Plugin/Nico/NicoResolver.ts b/src/rainlink/Plugin/Nico/NicoResolver.ts deleted file mode 100644 index 53aed326..00000000 --- a/src/rainlink/Plugin/Nico/NicoResolver.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Code from: -// https://github.com/y-chan/niconico-dl.js - -import { parse } from 'node-html-parser' -import { NiconicoAPIData, VideoInfo } from './@types/NicoResolver.js' -import { fetch } from 'undici' - -const niconicoRegexp = 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]+)' -) - -export function isValidURL(url: string): boolean { - return niconicoRegexp.test(url) -} - -class NiconicoDL { - private videoURL: string - private data: NiconicoAPIData | undefined - - constructor(url: string) { - if (!isValidURL(url)) { - throw Error('Invalid url') - } - this.videoURL = url - } - - async getVideoInfo(): Promise { - const fetchSite = await fetch(this.videoURL) - const rawStringText = await fetchSite.text() - const videoSiteDom = parse(rawStringText) - const matchResult = videoSiteDom - .querySelectorAll('div') - .filter((a) => a.rawAttributes.id === 'js-initial-watch-data') - if (matchResult.length === 0) { - throw Error('Failed get video site html...') - } - const patterns = { - '<': '<', - '>': '>', - '&': '&', - '"': '"', - ''': "'", - '`': '`', - } - const fixedString = matchResult[0].rawAttributes['data-api-data'].replace( - /&(lt|gt|amp|quot|#x27|#x60);/g, - function (match: string): string { - return patterns[match] - } - ) - this.data = JSON.parse(fixedString) as NiconicoAPIData - return Object.assign(this.data.video, { - owner: this.data.owner, - }) as VideoInfo - } -} - -export default NiconicoDL diff --git a/src/rainlink/Plugin/Nico/NicoSearch.ts b/src/rainlink/Plugin/Nico/NicoSearch.ts deleted file mode 100644 index fc947a0a..00000000 --- a/src/rainlink/Plugin/Nico/NicoSearch.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Code from: -// https://github.com/blackrose514/niconico-search-api - -import { responseFields } from './NicoSearchConst.js' -import { ErrorResponse, SearchAPIResponse, SearchParams } from './@types/NicoSearch.js' -import { fetch } from 'undici' - -const apiUrl = 'https://api.search.nicovideo.jp/api/v2/snapshot/video/contents/search' - -export default async function search

({ - q, - fields, -}: P): Promise> { - if (fields === '*') { - fields = responseFields - } - - try { - const url = new URL(apiUrl) - url.search = new URLSearchParams({ - q, - targets: 'tagsExact', - fields: 'contentId', - sort: '-viewCounter', - limit: String(10), - }).toString() - - const req = await fetch(url) - const res = (await req.json()) as any - - return res - } catch (err: any) { - if (err?.response) { - const { meta } = err.response as ErrorResponse - throw { - name: 'NiconicoSearchAPIResponseError', - meta, - } - } - throw err - } -} diff --git a/src/rainlink/Plugin/Nico/NicoSearchConst.ts b/src/rainlink/Plugin/Nico/NicoSearchConst.ts deleted file mode 100644 index 0ae46693..00000000 --- a/src/rainlink/Plugin/Nico/NicoSearchConst.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Code from: -// https://github.com/blackrose514/niconico-search-api - -import { ResponseField } from './@types/NicoSearch.js' - -export const responseFields: ResponseField[] = [ - 'contentId', - 'title', - 'description', - 'viewCounter', - 'mylistCounter', - 'lengthSeconds', - 'thumbnailUrl', - 'startTime', - 'lastResBody', - 'commentCounter', - 'lastCommentTime', - 'categoryTags', - 'tags', - 'genre', -] diff --git a/src/rainlink/Plugin/Nico/Plugin.ts b/src/rainlink/Plugin/Nico/Plugin.ts deleted file mode 100644 index f4e8b071..00000000 --- a/src/rainlink/Plugin/Nico/Plugin.ts +++ /dev/null @@ -1,288 +0,0 @@ -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]+)' -) - -/** The rainlink nicovideo plugin options */ -export interface NicoOptions { - /** The number of how many track u want to resolve */ - searchLimit: number -} - -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 - - 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 - } - - /** - * Source identify of the plugin - * @returns string - */ - public sourceIdentify(): string { - return 'nv' - } - - /** - * Source name of the plugin - * @returns string - */ - public sourceName(): string { - return 'nicovideo' - } - - /** - * Type of the plugin - * @returns RainlinkPluginType - */ - 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) - } - - /** - * 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-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 - } - - /** - * 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.') - - if (!query) throw new Error('Query is required') - const [, id] = REGEX.exec(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) - - 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) - - 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) { - try { - const { data } = await search({ - q: query, - targets: ['tagsExact'], - fields: ['contentId'], - sort: '-viewCounter', - limit: 10, - }) - - 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) - } - - 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() - - 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 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 - } -} - -// Interfaces -/** @ignore */ -export interface Result { - tracks: RainlinkTrack[] - name?: string -} -/** @ignore */ -export interface OwnerInfo { - id: number - nickname: string - iconUrl: string - channel: string | null - live: { - id: string - title: string - url: string - begunAt: string - isVideoLive: boolean - videoLiveOnAirStartTime: string | null - thumbnailUrl: string | null - } | null - isVideoPublic: boolean - isMylistsPublic: boolean - videoLiveNotice: null - viewer: number | null -} -/** @ignore */ -interface OriginalVideoInfo { - id: string - title: string - description: string - count: { - view: number - comment: number - mylist: number - like: number - } - duration: number - thumbnail: { - url: string - middleUrl: string - largeUrl: string - player: string - ogp: string - } - rating: { - isAdult: boolean - } - registerdAt: string - isPrivate: boolean - isDeleted: boolean - isNoBanner: boolean - isAuthenticationRequired: boolean - isEmbedPlayerAllowed: boolean - viewer: null - watchableUserTypeForPayment: string - commentableUserTypeForPayment: string - [key: string]: any -} -/** @ignore */ -export interface VideoInfo extends OriginalVideoInfo { - owner: OwnerInfo -} diff --git a/src/rainlink/Plugin/PlayerMoved/Plugin.ts b/src/rainlink/Plugin/PlayerMoved/Plugin.ts deleted file mode 100644 index eb7b3349..00000000 --- a/src/rainlink/Plugin/PlayerMoved/Plugin.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { RainlinkEvents, RainlinkPluginType } from '../../Interface/Constants.js' -import { Rainlink } from '../../Rainlink.js' -import { RainlinkPlugin as Plugin } from './../RainlinkPlugin.js' - -export class RainlinkPlugin extends Plugin { - private rainlink: Rainlink | null = null - - /** - * Initialize the plugin. - * @param client Discord.Client - */ - constructor(public client: any) { - super() - } - - /** - * Type of the plugin - * @returns RainlinkPluginType - */ - public type(): RainlinkPluginType { - return RainlinkPluginType.Default - } - - /** - * Load the plugin. - * @param rainlink rainlink - */ - public load(rainlink: Rainlink): void { - this.rainlink = rainlink - this.client.on('voiceStateUpdate', this.onVoiceStateUpdate.bind(this)) - } - - /** - * The name of the plugin - * @returns string - */ - public name(): string { - return 'rainlink-playerMoved' - } - - /** - * Unload the plugin. - */ - public unload(): void { - this.client.removeListener('voiceStateUpdate', this.onVoiceStateUpdate.bind(this)) - this.rainlink = null - } - - private onVoiceStateUpdate(oldState: any, newState: any): void { - if (!this.rainlink || oldState.id !== this.client.user.id) return - - const newChannelId = newState.channelID || newState.channelId - const oldChannelId = oldState.channelID || oldState.channelId - const guildId = newState.guild.id - - const player = this.rainlink.players.get(guildId) - if (!player) return - - let state = 'UNKNOWN' - if (!oldChannelId && newChannelId) state = 'JOINED' - else if (oldChannelId && !newChannelId) state = 'LEFT' - else if (oldChannelId && newChannelId && oldChannelId !== newChannelId) state = 'MOVED' - - if (state === 'UNKNOWN') return - this.rainlink.emit(RainlinkEvents.PlayerMoved, player, state, { - oldChannelId, - newChannelId, - }) - } -} diff --git a/src/rainlink/Plugin/RainlinkPlugin.ts b/src/rainlink/Plugin/RainlinkPlugin.ts deleted file mode 100644 index 1996fb33..00000000 --- a/src/rainlink/Plugin/RainlinkPlugin.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { RainlinkPluginType } from '../Interface/Constants.js' -import { Rainlink } from '../Rainlink.js' - -/** The interface class for another rainlink plugin, extend it to use */ -export class RainlinkPlugin { - /** Name function for getting plugin name */ - public name(): string { - throw new Error('Plugin must implement name() and return a plguin name string') - } - - /** Type function for diferent type of plugin */ - public type(): RainlinkPluginType { - throw new Error('Plugin must implement type() and return "sourceResolver" or "default"') - } - - /** Load function for make the plugin working */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public load(manager: Rainlink): void { - throw new Error('Plugin must implement load()') - } - - /** unload function for make the plugin stop working */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public unload(manager: Rainlink): void { - throw new Error('Plugin must implement unload()') - } -} diff --git a/src/rainlink/Plugin/SourceRainlinkPlugin.ts b/src/rainlink/Plugin/SourceRainlinkPlugin.ts deleted file mode 100644 index 58c5e773..00000000 --- a/src/rainlink/Plugin/SourceRainlinkPlugin.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { RainlinkSearchOptions, RainlinkSearchResult } from '../Interface/Manager.js' -import { RainlinkPlugin } from './RainlinkPlugin.js' - -/** The interface class for track resolver plugin, extend it to use */ -export class SourceRainlinkPlugin extends RainlinkPlugin { - /** - * sourceName function for source plugin register search engine. - * This will make plugin avalible to search when set the source to default source - * @returns string - */ - public sourceName(): string { - throw new Error('Source plugin must implement sourceName() and return as string') - } - - /** - * sourceIdentify function for source plugin register search engine. - * This will make plugin avalible to search when set the source to default source - * @returns string - */ - public sourceIdentify(): string { - throw new Error('Source plugin must implement sourceIdentify() and return as string') - } - - /** - * directSearchChecker function for checking if query have direct search param. - * @returns boolean - */ - public directSearchChecker(query: string): boolean { - const directSearchRegex = /directSearch=(.*)/ - const isDirectSearch = directSearchRegex.exec(query) - return isDirectSearch == null - } - - /** - * searchDirect function for source plugin search directly without fallback. - * This will avoid overlaps in search function - * @returns RainlinkSearchResult - */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - public async searchDirect( - query: string, - options?: RainlinkSearchOptions - ): Promise { - throw new Error('Source plugin must implement sourceIdentify() and return as string') - } -} diff --git a/src/rainlink/Plugin/Spotify/Plugin.ts b/src/rainlink/Plugin/Spotify/Plugin.ts deleted file mode 100644 index cfc32285..00000000 --- a/src/rainlink/Plugin/Spotify/Plugin.ts +++ /dev/null @@ -1,493 +0,0 @@ -import { request } from 'undici' -import { 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 { RequestManager } from './RequestManager.js' - -const REGEX = - /(?:https:\/\/open\.spotify\.com\/|spotify:)(?:.+)?(track|playlist|album|artist)[\/:]([A-Za-z0-9]+)/ -const SHORT_REGEX = /(?:https:\/\/spotify\.link)\/([A-Za-z0-9]+)/ - -/** The rainlink spotify plugin options */ -export interface SpotifyOptions { - /** The client ID of your Spotify application. */ - clientId: string - /** The client secret of your Spotify application. */ - clientSecret: string - /** The clients for multiple spotify applications. NOT RECOMMENDED */ - clients?: { clientId: string; clientSecret: string }[] - /** 100 tracks per page */ - playlistPageLimit?: number - /** 50 tracks per page */ - albumPageLimit?: number - /** The track limit when searching track */ - searchLimit?: number - /** Enter the country you live in. ( Can only be of 2 letters. For eg: US, IN, EN) */ - searchMarket?: string -} - -export class RainlinkPlugin extends SourceRainlinkPlugin { - /** - * The options of the plugin. - */ - public options: SpotifyOptions - - private _search: - | ((query: string, options?: RainlinkSearchOptions) => Promise) - | null - private rainlink: Rainlink | null - - 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 - } - - /** - * Source identify of the plugin - * @returns string - */ - public sourceIdentify(): string { - return 'sp' - } - - /** - * Source name of the plugin - * @returns string - */ - public sourceName(): string { - return 'spotify' - } - - /** - * Type of the plugin - * @returns RainlinkPluginType - */ - 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) - } - - /** - * 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 - } - - /** - * 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 = - 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 - ) - } -} - -/** @ignore */ -export interface SearchResult { - tracks: Tracks -} -/** @ignore */ -export interface Result { - tracks: RainlinkTrack[] - name?: string -} -/** @ignore */ -export interface TrackResult { - album: Album - artists: Artist[] - available_markets: string[] - disc_number: number - - duration_ms: number - explicit: boolean - external_ids: ExternalIds - external_urls: ExternalUrls - href: string - id: string - is_local: boolean - name: string - popularity: number - preview_url: string - track: any - track_number: number - type: string - uri: string -} -/** @ignore */ -export interface AlbumResult { - album_type: string - artists: Artist[] - available_markets: string[] - copyrights: Copyright[] - external_ids: ExternalIds - external_urls: ExternalUrls - genres: string[] - href: string - id: string - images: Image[] - label: string - name: string - popularity: number - release_date: string - release_date_precision: string - total_tracks: number - tracks: Tracks - type: string - uri: string -} -/** @ignore */ -export interface ArtistResult { - tracks: Track[] -} -/** @ignore */ -export interface PlaylistResult { - collaborative: boolean - description: string - external_urls: ExternalUrls - followers: Followers - href: string - id: string - images: Image[] - name: string - owner: Owner - primary_color: string | null - public: boolean - snapshot_id: string - tracks: PlaylistTracks - type: string - uri: string -} -/** @ignore */ -export interface Owner { - display_name: string - external_urls: ExternalUrls - href: string - id: string - type: string - uri: string -} -/** @ignore */ -export interface Followers { - href: string | null - total: number -} -/** @ignore */ -export interface Tracks { - href: string - items: Track[] - next: string | null -} -/** @ignore */ -export interface PlaylistTracks { - href: string - items: SpecialTracks[] - limit: number - next: string | null - offset: number - previous: string | null - total: number -} -/** @ignore */ -export interface SpecialTracks { - added_at: string - is_local: boolean - primary_color: string | null - track: Track -} -/** @ignore */ -export interface Copyright { - text: string - type: string -} -/** @ignore */ -export interface ExternalUrls { - spotify: string -} -/** @ignore */ -export interface ExternalIds { - isrc: string -} -/** @ignore */ -export interface Album { - album_type: string - artists: Artist[] - available_markets: string[] - external_urls: { [key: string]: string } - href: string - id: string - images: Image[] - name: string - release_date: string - release_date_precision: string - total_tracks: number - type: string - uri: string -} -/** @ignore */ -export interface Image { - height: number - url: string - width: number -} -/** @ignore */ -export interface Artist { - external_urls: { - spotify: string - } - followers: { - href: string - total: number - } - genres: [] - href: string - id: string - images: Image[] - name: string - popularity: number - type: string - uri: string -} -/** @ignore */ -export interface Track { - album?: Album - artists: Artist[] - available_markets: string[] - disc_number: number - duration_ms: number - explicit: boolean - external_urls: ExternalUrls - href: string - id: string - is_local: boolean - name: string - preview_url: string - track_number: number - type: string - uri: string -} diff --git a/src/rainlink/Plugin/Spotify/RequestManager.ts b/src/rainlink/Plugin/Spotify/RequestManager.ts deleted file mode 100644 index b8f7d0a0..00000000 --- a/src/rainlink/Plugin/Spotify/RequestManager.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { SpotifyOptions } from './Plugin.js' -import { SpotifyRequest } from './SpotifyRequest.js' - -export class RequestManager { - private requests: SpotifyRequest[] = [] - private readonly mode: 'single' | 'multi' = 'single' - - constructor(private options: SpotifyOptions) { - if (options.clients?.length) { - for (const client of options.clients) this.requests.push(new SpotifyRequest(client)) - this.mode = 'multi' - // eslint-disable-next-line no-console - console.warn( - '\x1b[31m%s\x1b[0m', - "You are using the multi client mode, sometimes you can STILL GET RATE LIMITED. I'm not responsible for any IP BANS." - ) - } else { - this.requests.push( - new SpotifyRequest({ - clientId: options.clientId, - clientSecret: options.clientSecret, - }) - ) - } - } - - public async makeRequest( - endpoint: string, - disableBaseUri: boolean = false, - tries: number = 3 - ): Promise { - if (this.mode === 'single') return this.requests[0].makeRequest(endpoint, disableBaseUri) - - const targetRequest = this.getLeastUsedRequest() - if (!targetRequest) throw new Error('No available requests [ALL_RATE_LIMITED]') - return targetRequest - .makeRequest(endpoint, disableBaseUri) - .catch((e) => - e.message === 'Rate limited by spotify' && tries - ? this.makeRequest(endpoint, disableBaseUri, tries - 1) - : Promise.reject(e) - ) - } - - protected getLeastUsedRequest(): SpotifyRequest | undefined { - const targetSearch = this.requests.filter((request) => !request.stats.rateLimited) - if (!targetSearch.length) return undefined - - return targetSearch.sort((a, b) => a.stats.requests - b.stats.requests)[0] - } -} diff --git a/src/rainlink/Plugin/Spotify/SpotifyRequest.ts b/src/rainlink/Plugin/Spotify/SpotifyRequest.ts deleted file mode 100644 index 517230ea..00000000 --- a/src/rainlink/Plugin/Spotify/SpotifyRequest.ts +++ /dev/null @@ -1,74 +0,0 @@ -const BASE_URL = 'https://api.spotify.com/v1' - -export class SpotifyRequest { - private token: string = '' - private authorization: string = '' - private nextRenew: number = 0 - public stats: { requests: number; rateLimited: boolean } = { - requests: 0, - rateLimited: false, - } - - constructor(private client: { clientId: string; clientSecret: string }) { - this.authorization = `&client_id=${this.client.clientId}&client_secret=${this.client.clientSecret}` - } - - public async makeRequest(endpoint: string, disableBaseUri: boolean = false): Promise { - await this.renew() - - const request = await fetch( - disableBaseUri - ? endpoint - : `${BASE_URL}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`, - { - headers: { Authorization: this.token }, - } - ) - - const data = (await request.json()) as Promise - - if (request.headers.get('x-ratelimit-remaining') === '0') { - this.handleRateLimited(Number(request.headers.get('x-ratelimit-reset')) * 1000) - throw new Error('Rate limited by spotify') - } - this.stats.requests++ - - return data - } - - private handleRateLimited(time: number): void { - this.stats.rateLimited = true - setTimeout(() => { - this.stats.rateLimited = false - }, time) - } - - private async renewToken(): Promise { - const res = await fetch( - `https://accounts.spotify.com/api/token?grant_type=client_credentials${this.authorization}`, - { - method: 'POST', - headers: { - // Authorization: this.authorization, - 'Content-Type': 'application/x-www-form-urlencoded', - }, - } - ) - - const { access_token, expires_in } = (await res.json()) as { - access_token?: string - expires_in: number - } - - if (!access_token) throw new Error('Failed to get access token due to invalid spotify client') - - this.token = `Bearer ${access_token}` - this.nextRenew = Date.now() + expires_in * 1000 - } - - private async renew(): Promise { - if (Date.now() >= this.nextRenew) { - await this.renewToken() - } - } -} diff --git a/src/rainlink/Plugin/VoiceReceiver/Plugin.ts b/src/rainlink/Plugin/VoiceReceiver/Plugin.ts deleted file mode 100644 index 78016cb7..00000000 --- a/src/rainlink/Plugin/VoiceReceiver/Plugin.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { RainlinkPlugin as Plugin } from '../RainlinkPlugin.js' -import { Rainlink } from '../../Rainlink.js' -import { RainlinkEvents, RainlinkPluginType } from '../../Interface/Constants.js' -import { RainlinkNode } from '../../Node/RainlinkNode.js' -import { metadata } from '../../metadata.js' -import { VoiceChannelOptions } from '../../Interface/Player.js' -import { RainlinkDatabase, RainlinkWebsocket } from '../../main.js' - -export class RainlinkPlugin extends Plugin { - protected manager?: Rainlink - /** Whenever the plugin is enabled or not */ - public enabled: boolean = false - protected runningWs: RainlinkDatabase = - new RainlinkDatabase() - - constructor() { - super() - } - - /** Name function for getting plugin name */ - public name(): string { - return 'rainlink-voiceReceiver' - } - - /** Type function for diferent type of plugin */ - public type(): RainlinkPluginType { - return RainlinkPluginType.Default - } - - /** Open the ws voice reciver client */ - public open(node: RainlinkNode, voiceOptions: VoiceChannelOptions): void { - if (!this.enabled) throw new Error('This plugin is unloaded!') - if (!node.options.driver?.includes('nodelink')) - throw new Error( - 'This node not support voice receiver, please use Nodelink2 to use this feature!' - ) - const wsUrl = `${node.options.secure ? 'wss' : 'ws'}://${node.options.host}:${node.options.port}` - const ws = new RainlinkWebsocket(wsUrl + '/connection/data', { - headers: { - Authorization: node.options.auth, - 'User-Id': this.manager!.id, - 'Client-Name': `${metadata.name}/${metadata.version} (${metadata.github})`, - 'user-agent': this.manager!.rainlinkOptions.options!.userAgent!, - 'Guild-Id': voiceOptions.guildId, - }, - }) - this.runningWs.set(voiceOptions.guildId, ws) - ws.on('open', () => { - this.debug("Connected to nodelink's voice receive server!") - this.manager?.emit(RainlinkEvents.VoiceConnect, node) - }) - ws.on('message', (data) => this.wsMessageEvent(node, data)) - ws.on('error', (err) => { - this.debug("Errored at nodelink's voice receive server!") - this.manager?.emit(RainlinkEvents.VoiceError, node, err) - }) - ws.on('close', (code: number, reason: Buffer) => { - this.debug(`Disconnected to nodelink's voice receive server! Code: ${code} Reason: ${reason}`) - this.manager?.emit(RainlinkEvents.VoiceDisconnect, node, code, reason) - ws.removeAllListeners() - }) - } - - /** Open the ws voice reciver client */ - public close(guildId: string): void { - const targetWs = this.runningWs.get(guildId) - if (!targetWs) return - targetWs.close() - this.runningWs.delete(guildId) - this.debug("Destroy connection to nodelink's voice receive server!") - targetWs.removeAllListeners() - return - } - - protected wsMessageEvent(node: RainlinkNode, data: Record) { - const wsData = JSON.parse(data.toString()) - this.debug(String(data)) - switch (wsData.type) { - case 'startSpeakingEvent': { - this.manager?.emit( - RainlinkEvents.VoiceStartSpeaking, - node, - wsData.data.userId, - wsData.data.guildId - ) - break - } - case 'endSpeakingEvent': { - this.manager?.emit( - RainlinkEvents.VoiceEndSpeaking, - node, - wsData.data.data, - wsData.data.userId, - wsData.data.guildId - ) - break - } - } - // this.node.wsMessageEvent(wsData); - } - - /** Load function for make the plugin working */ - public load(manager: Rainlink): void { - this.manager = manager - this.enabled = true - } - - /** unload function for make the plugin stop working */ - public unload(manager: Rainlink): void { - this.manager = manager - this.enabled = false - } - - private debug(logs: string) { - this.manager - ? this.manager.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Plugin] / [Voice Receiver] | ${logs}` - ) - : true - } -} diff --git a/src/rainlink/Plugin/index.ts b/src/rainlink/Plugin/index.ts deleted file mode 100644 index 035c814d..00000000 --- a/src/rainlink/Plugin/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { RainlinkPlugin as Deezer } from './Deezer/Plugin.js' -import { RainlinkPlugin as Apple } from './Apple/Plugin.js' -import { RainlinkPlugin as Nico } from './Nico/Plugin.js' -import { RainlinkPlugin as Spotify } from './Spotify/Plugin.js' -import { RainlinkPlugin as PlayerMoved } from './PlayerMoved/Plugin.js' -import { RainlinkPlugin as VoiceReceiver } from './VoiceReceiver/Plugin.js' -import { RainlinkPlugin as YoutubeConverter } from './YoutubeConverter/Plugin.js' - -export default { - Deezer, - Apple, - Nico, - Spotify, - PlayerMoved, - VoiceReceiver, - YoutubeConverter, -} diff --git a/src/rainlink/Rainlink.ts b/src/rainlink/Rainlink.ts deleted file mode 100644 index 9198123d..00000000 --- a/src/rainlink/Rainlink.ts +++ /dev/null @@ -1,729 +0,0 @@ -import { - RainlinkAdditionalOptions, - RainlinkOptions, - RainlinkSearchOptions, - RainlinkSearchResult, - RainlinkSearchResultType, -} from './Interface/Manager.js' -import { EventEmitter } from 'node:events' -import { RainlinkNode } from './Node/RainlinkNode.js' -import { AbstractLibrary } from './Library/AbstractLibrary.js' -import { VoiceChannelOptions } from './Interface/Player.js' -import { RainlinkPlayerManager } from './Manager/RainlinkPlayerManager.js' -import { RainlinkNodeManager } from './Manager/RainlinkNodeManager.js' -import { - LavalinkLoadType, - RainlinkEvents, - RainlinkPluginType, - SourceIDs, -} from './Interface/Constants.js' -import { RainlinkTrack } from './Player/RainlinkTrack.js' -import { RawTrack } from './Interface/Rest.js' -import { RainlinkPlayer } from './Player/RainlinkPlayer.js' -import { SourceRainlinkPlugin } from './Plugin/SourceRainlinkPlugin.js' -import { RainlinkQueue } from './Player/RainlinkQueue.js' -import { metadata } from './metadata.js' -import { RainlinkPlugin } from './Plugin/RainlinkPlugin.js' -import { AbstractDriver } from './Drivers/AbstractDriver.js' -import { Lavalink3 } from './Drivers/Lavalink3.js' -import { Nodelink2 } from './Drivers/Nodelink2.js' -import { Lavalink4 } from './Drivers/Lavalink4.js' -import { RainlinkDatabase } from './Utilities/RainlinkDatabase.js' - -export declare interface Rainlink { - /* tslint:disable:unified-signatures */ - // ------------------------- ON EVENT ------------------------- // - /** - * Emitted when rainlink have a debug log. - * @event Rainlink#debug - */ - on(event: 'debug', listener: (logs: string) => void): this - - ////// ------------------------- Node Event ------------------------- ///// - /** - * Emitted when a lavalink server is connected. - * @event Rainlink#nodeConnect - */ - on(event: 'nodeConnect', listener: (node: RainlinkNode) => void): this - /** - * Emitted when a lavalink server is disconnected. - * @event Rainlink#nodeDisconnect - */ - on( - event: 'nodeDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** - * Emitted when a lavalink server is closed. - * @event Rainlink#nodeClosed - */ - on(event: 'nodeClosed', listener: (node: RainlinkNode) => void): this - /** - * Emitted when a lavalink server is errored. - * @event Rainlink#nodeError - */ - on(event: 'nodeError', listener: (node: RainlinkNode, error: Error) => void): this - ////// ------------------------- Node Event ------------------------- ///// - - ////// ------------------------- Player Event ------------------------- ///// - /** - * Emitted when a player is created. - * @event Rainlink#playerCreate - */ - on(event: 'playerCreate', listener: (player: RainlinkPlayer) => void): this - /** - * Emitted when a player is going to destroyed. - * @event Rainlink#playerDestroy - */ - on(event: 'playerDestroy', listener: (player: RainlinkPlayer) => void): this - /** - * Emitted when a player have an exception. - * @event Rainlink#playerException - */ - on( - event: 'playerException', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** - * Emitted when a player updated info. - * @event Rainlink#playerUpdate - */ - on( - event: 'playerUpdate', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** - * Emitted when a playuer is moved into another channel. [Require plugin] - * @event Rainlink#playerMoved - */ - on( - event: 'playerMoved', - listener: (player: RainlinkPlayer, oldChannelId: string, newChannelId: string) => void - ): this - /** - * Emitted when a track paused. - * @event Rainlink#playerPause - */ - on(event: 'playerPause', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** - * Emitted when a track resumed. - * @event Rainlink#playerResume - */ - on(event: 'playerResume', listener: (player: RainlinkPlayer, data: RainlinkTrack) => void): this - /** - * Emitted when a player's websocket closed. - * @event Rainlink#playerWebsocketClosed - */ - on( - event: 'playerWebsocketClosed', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** - * Emitted when a player is stopped (not destroyed). - * @event Rainlink#playerResume - */ - on(event: 'playerStop', listener: (player: RainlinkPlayer) => void): this - ////// ------------------------- Player Event ------------------------- ///// - - ////// ------------------------- Track Event ------------------------- ///// - /** - * Emitted when a track is going to play. - * @event Rainlink#trackStart - */ - on(event: 'trackStart', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** - * Emitted when a track is going to end. - * @event Rainlink#trackEnd - */ - on(event: 'trackEnd', listener: (player: RainlinkPlayer) => void): this - /** - * Emitted when a track stucked. - * @event Rainlink#trackStuck - */ - on( - event: 'trackStuck', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** - * Emitted when a track is failed to resolve using fallback search engine. - * @event Rainlink#trackResolveError - */ - on( - event: 'trackResolveError', - listener: (player: RainlinkPlayer, track: RainlinkTrack, message: string) => void - ): this - ////// ------------------------- Track Event ------------------------- ///// - - ////// ------------------------- Queue Event ------------------------- ///// - /** - * Emitted when a track added into queue. - * @event Rainlink#queueAdd - */ - on( - event: 'queueAdd', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** - * Emitted when a track removed from queue. - * @event Rainlink#queueRemove - */ - on( - event: 'queueRemove', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** - * Emitted when a queue shuffled. - * @event Rainlink#queueShuffle - */ - on(event: 'queueShuffle', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - /** - * Emitted when a queue cleared. - * @event Rainlink#queueClear - */ - on(event: 'queueClear', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - /** - * Emitted when a queue is empty. - * @event Rainlink#queueEmpty - */ - on(event: 'queueEmpty', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - ////// ------------------------- Queue Event ------------------------- ///// - - ////// ------------------------- Voice Event ------------------------- ///// - /** - * Emitted when connected to voice receive server [ONLY Nodelink DRIVER!!!!!!]. - * @event Rainlink#voiceConnect - */ - on(event: 'voiceConnect', listener: (node: RainlinkNode) => void): this - /** - * Emitted when disconnected to voice receive server [ONLY Nodelink DRIVER!!!!!!]. - * @event Rainlink#voiceDisconnect - */ - on( - event: 'voiceDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** - * Emitted when voice receive server errored [ONLY Nodelink DRIVER!!!!!!]. - * @event Rainlink#VoiceError - */ - on(event: 'VoiceError', listener: (node: RainlinkNode, error: Error) => void): this - /** - * Emitted when user started speaking [ONLY Nodelink DRIVER!!!!!!]. - * @event Rainlink#voiceStartSpeaking - */ - on( - event: 'voiceStartSpeaking', - listener: (node: RainlinkNode, userId: string, guildId: string) => void - ): this - /** - * Emitted when user finished speaking [ONLY Nodelink DRIVER!!!!!!]. - * @event Rainlink#voiceEndSpeaking - */ - on( - event: 'voiceEndSpeaking', - listener: (node: RainlinkNode, userTrack: string, userId: string, guildId: string) => void - ): this - ////// ------------------------- Voice Event ------------------------- ///// - // ------------------------- ON EVENT ------------------------- // - - // ------------------------- ONCE EVENT ------------------------- // - /** @ignore */ - once(event: 'debug', listener: (logs: string) => void): this - ////// ------------------------- Node Event ------------------------- ///// - /** @ignore */ - once(event: 'nodeConnect', listener: (node: RainlinkNode) => void): this - /** @ignore */ - once( - event: 'nodeDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** @ignore */ - once(event: 'nodeClosed', listener: (node: RainlinkNode) => void): this - /** @ignore */ - once(event: 'nodeError', listener: (node: RainlinkNode, error: Error) => void): this - ////// ------------------------- Node Event ------------------------- ///// - - ////// ------------------------- Player Event ------------------------- ///// - /** @ignore */ - once(event: 'playerCreate', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - once(event: 'playerDestroy', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - once( - event: 'playerException', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - once( - event: 'playerUpdate', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - once( - event: 'playerMoved', - listener: (player: RainlinkPlayer, oldChannelId: string, newChannelId: string) => void - ): this - /** @ignore */ - once(event: 'playerPause', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** @ignore */ - once(event: 'playerResume', listener: (player: RainlinkPlayer, data: RainlinkTrack) => void): this - /** @ignore */ - once( - event: 'playerWebsocketClosed', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - once(event: 'playerStop', listener: (player: RainlinkPlayer) => void): this - ////// ------------------------- Player Event ------------------------- ///// - - ////// ------------------------- Track Event ------------------------- ///// - /** @ignore */ - once(event: 'trackStart', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** @ignore */ - once(event: 'trackEnd', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - once( - event: 'trackStuck', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - once( - event: 'trackResolveError', - listener: (player: RainlinkPlayer, track: RainlinkTrack, message: string) => void - ): this - ////// ------------------------- Track Event ------------------------- ///// - - ////// ------------------------- Queue Event ------------------------- ///// - /** @ignore */ - once( - event: 'queueAdd', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** @ignore */ - once( - event: 'queueRemove', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** @ignore */ - once( - event: 'queueShuffle', - listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void - ): this - /** @ignore */ - once(event: 'queueClear', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - /** @ignore */ - once(event: 'queueEmpty', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - ////// ------------------------- Queue Event ------------------------- ///// - - ////// ------------------------- Voice Event ------------------------- ///// - /** @ignore */ - once(event: 'voiceConnect', listener: (node: RainlinkNode) => void): this - /** @ignore */ - once( - event: 'voiceDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** @ignore */ - once(event: 'VoiceError', listener: (node: RainlinkNode, error: Error) => void): this - /** @ignore */ - once( - event: 'voiceStartSpeaking', - listener: (node: RainlinkNode, userId: string, guildId: string) => void - ): this - /** @ignore */ - once( - event: 'voiceEndSpeaking', - listener: (node: RainlinkNode, userTrack: string, userId: string, guildId: string) => void - ): this - ////// ------------------------- Voice Event ------------------------- ///// - // ------------------------- ONCE EVENT ------------------------- // - - // ------------------------- OFF EVENT ------------------------- // - /** @ignore */ - off(event: 'debug', listener: (logs: string) => void): this - ////// ------------------------- Node Event ------------------------- ///// - /** @ignore */ - off(event: 'nodeConnect', listener: (node: RainlinkNode) => void): this - /** @ignore */ - off( - event: 'nodeDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** @ignore */ - off(event: 'nodeClosed', listener: (node: RainlinkNode) => void): this - /** @ignore */ - off(event: 'nodeError', listener: (node: RainlinkNode, error: Error) => void): this - ////// ------------------------- Node Event ------------------------- ///// - - ////// ------------------------- Player Event ------------------------- ///// - /** @ignore */ - off(event: 'playerCreate', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - off(event: 'playerDestroy', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - off( - event: 'playerException', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - off( - event: 'playerUpdate', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - off( - event: 'playerMoved', - listener: (player: RainlinkPlayer, oldChannelId: string, newChannelId: string) => void - ): this - /** @ignore */ - off(event: 'playerPause', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** @ignore */ - off(event: 'playerResume', listener: (player: RainlinkPlayer, data: RainlinkTrack) => void): this - /** @ignore */ - off( - event: 'playerWebsocketClosed', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - off(event: 'playerStop', listener: (player: RainlinkPlayer) => void): this - ////// ------------------------- Player Event ------------------------- ///// - - ////// ------------------------- Track Event ------------------------- ///// - /** @ignore */ - off(event: 'trackStart', listener: (player: RainlinkPlayer, track: RainlinkTrack) => void): this - /** @ignore */ - off(event: 'trackEnd', listener: (player: RainlinkPlayer) => void): this - /** @ignore */ - off( - event: 'trackStuck', - listener: (player: RainlinkPlayer, data: Record) => void - ): this - /** @ignore */ - off( - event: 'trackResolveError', - listener: (player: RainlinkPlayer, track: RainlinkTrack, message: string) => void - ): this - ////// ------------------------- Track Event ------------------------- ///// - - ////// ------------------------- Queue Event ------------------------- ///// - /** @ignore */ - off( - event: 'queueAdd', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** @ignore */ - off( - event: 'queueRemove', - listener: (player: RainlinkPlayer, queue: RainlinkQueue, track: RainlinkTrack) => void - ): this - /** @ignore */ - off(event: 'queueShuffle', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - /** @ignore */ - off(event: 'queueClear', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - /** @ignore */ - off(event: 'queueEmpty', listener: (player: RainlinkPlayer, queue: RainlinkQueue) => void): this - ////// ------------------------- Queue Event ------------------------- ///// - - ////// ------------------------- Voice Event ------------------------- ///// - /** @ignore */ - off(event: 'voiceConnect', listener: (node: RainlinkNode) => void): this - /** @ignore */ - off( - event: 'voiceDisconnect', - listener: (node: RainlinkNode, code: number, reason: Buffer) => void - ): this - /** @ignore */ - off(event: 'VoiceError', listener: (node: RainlinkNode, error: Error) => void): this - /** @ignore */ - off( - event: 'voiceStartSpeaking', - listener: (node: RainlinkNode, userId: string, guildId: string) => void - ): this - /** @ignore */ - off( - event: 'voiceEndSpeaking', - listener: (node: RainlinkNode, userTrack: string, userId: string, guildId: string) => void - ): this - ////// ------------------------- Voice Event ------------------------- ///// - // ------------------------- OFF EVENT ------------------------- // - /** @ignore */ - emit(event: (typeof RainlinkEvents)[keyof typeof RainlinkEvents], ...args: unknown[]): this -} - -export class Rainlink extends EventEmitter { - /** - * Discord library connector - */ - public readonly library: AbstractLibrary - /** - * Lavalink server that has been configured - */ - public nodes: RainlinkNodeManager - /** - * Rainlink options - */ - public rainlinkOptions: RainlinkOptions - /** - * Bot id - */ - public id: string | undefined - /** - * Player maps - */ - public players: RainlinkPlayerManager - /** - * All search engine - */ - public searchEngines: RainlinkDatabase - /** - * All search plugins (resolver plugins) - */ - public searchPlugins: RainlinkDatabase - /** - * All plugins (include resolver plugins) - */ - public plugins: RainlinkDatabase - /** - * The rainlink manager - */ - public drivers: AbstractDriver[] - /** - * The current bott's shard count - */ - public shardCount: number = 1 - - /** - * The main class that handle all works in lavalink server. - * Call this class by using new Rainlink(your_params) to use! - * @param options The main ranlink options - */ - constructor(options: RainlinkOptions) { - super() - if (!options.library) - throw new Error( - 'Please set an new lib to connect, example: \nlibrary: new Library.DiscordJS(client) ' - ) - this.library = options.library.set(this) - this.drivers = [new Lavalink3(), new Nodelink2(), new Lavalink4()] - this.rainlinkOptions = options - this.rainlinkOptions.options = this.mergeDefault( - this.defaultOptions, - this.rainlinkOptions.options ?? {} - ) - if ( - this.rainlinkOptions.options.additionalDriver && - this.rainlinkOptions.options.additionalDriver?.length !== 0 - ) - this.drivers.push(...this.rainlinkOptions.options.additionalDriver) - this.nodes = new RainlinkNodeManager(this) - this.players = new RainlinkPlayerManager(this) - this.searchEngines = new RainlinkDatabase() - this.searchPlugins = new RainlinkDatabase() - this.plugins = new RainlinkDatabase() - this.initialSearchEngines() - if ( - !this.rainlinkOptions.options.defaultSearchEngine || - this.rainlinkOptions.options.defaultSearchEngine.length == 0 - ) - this.rainlinkOptions.options.defaultSearchEngine == 'youtube' - - if (this.rainlinkOptions.plugins) { - for (const [, plugin] of this.rainlinkOptions.plugins.entries()) { - if (plugin.constructor.name !== 'RainlinkPlugin') - throw new Error('Plugin must be an instance of RainlinkPlugin or SourceRainlinkPlugin') - plugin.load(this) - - this.plugins.set(plugin.name(), plugin) - - if (plugin.type() == RainlinkPluginType.SourceResolver) { - const newPlugin = plugin as SourceRainlinkPlugin - const sourceName = newPlugin.sourceName() - const sourceIdentify = newPlugin.sourceIdentify() - this.searchEngines.set(sourceName, sourceIdentify) - this.searchPlugins.set(sourceName, newPlugin) - } - } - } - this.library.listen(this.rainlinkOptions.nodes) - } - - protected initialSearchEngines() { - for (const data of SourceIDs) { - this.searchEngines.set(data.name, data.id) - } - } - - /** - * Create a new player. - * @returns RainlinkNode - */ - async create(options: VoiceChannelOptions): Promise { - return await this.players.create(options) - } - - /** - * Destroy a specific player. - * @returns void - */ - async destroy(guildId: string): Promise { - this.players.destroy(guildId) - } - - /** - * Search a specific track. - * @returns RainlinkSearchResult - */ - async search(query: string, options?: RainlinkSearchOptions): Promise { - const node = - options && options?.nodeName - ? this.nodes.get(options.nodeName) ?? (await this.nodes.getLeastUsed()) - : await this.nodes.getLeastUsed() - - if (!node) throw new Error('No node is available') - - let pluginData: RainlinkSearchResult - - const directSearchRegex = /directSearch=(.*)/ - const isDirectSearch = directSearchRegex.exec(query) - const isUrl = /^https?:\/\/.*/.test(query) - - const pluginSearch = this.searchPlugins.get(String(options?.engine)) - - if ( - options && - options!.engine && - options!.engine !== null && - pluginSearch && - isDirectSearch == null - ) { - pluginData = await pluginSearch.searchDirect(query, options) - if (pluginData.tracks.length !== 0) return pluginData - } - - const source = - options && options?.engine - ? this.searchEngines.get(options.engine) - : this.searchEngines.get( - this.rainlinkOptions.options!.defaultSearchEngine - ? this.rainlinkOptions.options!.defaultSearchEngine - : 'youtube' - ) - - const finalQuery = - isDirectSearch !== null ? isDirectSearch[1] : !isUrl ? `${source}search:${query}` : query - - const result = await node.rest.resolver(finalQuery).catch(() => null) - if (!result || result.loadType === LavalinkLoadType.EMPTY) { - return this.buildSearch(undefined, [], RainlinkSearchResultType.SEARCH) - } - - let loadType: RainlinkSearchResultType - let normalizedData: { - playlistName?: string - tracks: RawTrack[] - } = { tracks: [] } - switch (result.loadType) { - case LavalinkLoadType.TRACK: { - loadType = RainlinkSearchResultType.TRACK - normalizedData.tracks = [result.data] - break - } - - case LavalinkLoadType.PLAYLIST: { - loadType = RainlinkSearchResultType.PLAYLIST - normalizedData = { - playlistName: result.data.info.name, - tracks: result.data.tracks, - } - break - } - - case LavalinkLoadType.SEARCH: { - loadType = RainlinkSearchResultType.SEARCH - normalizedData.tracks = result.data - break - } - - default: { - loadType = RainlinkSearchResultType.SEARCH - normalizedData.tracks = [] - break - } - } - - this.emit( - RainlinkEvents.Debug, - `[Rainlink] / [Search] | Searched ${query}; Track results: ${normalizedData.tracks.length}` - ) - - return this.buildSearch( - normalizedData.playlistName ?? undefined, - normalizedData.tracks.map( - (track) => - new RainlinkTrack(track, options && options.requester ? options.requester : undefined) - ), - loadType - ) - } - - protected buildSearch( - playlistName?: string, - tracks: RainlinkTrack[] = [], - type?: RainlinkSearchResultType - ): RainlinkSearchResult { - return { - playlistName, - tracks, - type: type ?? RainlinkSearchResultType.SEARCH, - } - } - - protected get defaultOptions(): RainlinkAdditionalOptions { - return { - additionalDriver: [], - retryTimeout: 3000, - retryCount: 15, - voiceConnectionTimeout: 15000, - defaultSearchEngine: 'soundcloud', - defaultVolume: 100, - searchFallback: { - enable: true, - engine: 'soundcloud', - }, - resume: false, - userAgent: `Discord/Bot/${metadata.name}/${metadata.version} (${metadata.github})`, - nodeResolver: undefined, - structures: undefined, - resumeTimeout: 300, - } - } - - // Modded from: - // https://github.com/shipgirlproject/Shoukaku/blob/2677ecdf123ffef1c254c2113c5342b250ac4396/src/Utils.ts#L9-L23 - protected mergeDefault(def: T, given: T): Required { - if (!given) return def as Required - const defaultKeys: (keyof T)[] = Object.keys(def) - for (const key in given) { - if (defaultKeys.includes(key)) continue - if (this.isNumber(key)) continue - delete given[key] - } - for (const key of defaultKeys) { - if (Array.isArray(given[key]) && given[key] !== null && given[key] !== undefined) { - if (given[key].length == 0) given[key] = def[key] - } - if (def[key] === null || (typeof def[key] === 'string' && def[key].length === 0)) { - if (!given[key]) given[key] = def[key] - } - if (given[key] === null || given[key] === undefined) given[key] = def[key] - if (typeof given[key] === 'object' && given[key] !== null) { - this.mergeDefault(def[key], given[key]) - } - } - return given as Required - } - - protected isNumber(data: string): boolean { - return /^[+-]?\d+(\.\d+)?$/.test(data) - } -} diff --git a/src/rainlink/Utilities/RainlinkDatabase.ts b/src/rainlink/Utilities/RainlinkDatabase.ts deleted file mode 100644 index 63611a57..00000000 --- a/src/rainlink/Utilities/RainlinkDatabase.ts +++ /dev/null @@ -1,76 +0,0 @@ -export class RainlinkDatabase { - protected cache: Record = {} - - /** - * Get data from database - * @param key key of that data - * @returns D - */ - get(key: string): D | undefined { - return (this.cache[key] as unknown as D) ?? undefined - } - - /** - * detete data from database and returns the deleted data - * @param key key of that data - * @returns D - */ - delete(key: string): D | undefined { - const data = (this.cache[key] as unknown as D) ?? undefined - delete this.cache[key] - return data - } - - /** - * detete all data from database - */ - clear(): void { - this.cache = {} - } - - /** - * Set data from database - * @param key the key you want to set - * @param data data of that key - * @returns D - */ - set(key: string, data: D): D | undefined { - this.cache[key] = data as unknown as G - return data - } - - /** - * Get how many elements of current database - * @returns number - */ - get size(): number { - return Object.keys(this.cache).length - } - - /** - * Get all current values of current database - * @returns unknown[] - */ - get values(): G[] { - return Object.values(this.cache) - } - - /** - * Get all current values of current database - * @returns unknown[] - */ - get full(): [string, G][] { - const finalRes: [string, G][] = [] - const keys = Object.keys(this.cache) - const values = Object.values(this.cache) - for (let i = 0; i < keys.length; i++) { - finalRes.push([keys[i], values[i]]) - } - return finalRes - } - forEach(callback: (value: G, key: string) => unknown): void { - for (const data of this.full) { - callback(data[1], data[0]) - } - } -} diff --git a/src/rainlink/Utilities/RainlinkWebsocket.ts b/src/rainlink/Utilities/RainlinkWebsocket.ts deleted file mode 100644 index 04399c70..00000000 --- a/src/rainlink/Utilities/RainlinkWebsocket.ts +++ /dev/null @@ -1,407 +0,0 @@ -// Copyright (c) , The PerformanC Organization -// This is the modded version of PWSL (typescript variant) for running on Rainlink -// Source code get from PerformanC/Internals#PWSL -// Special thanks to all members of PerformanC Organization -// Link: https://github.com/PerformanC/internals/tree/fbc73f6368a6971835683f4b22bb4e3b15fa0b73 -// Github repo link: https://github.com/PerformanC/internals -// PWSL's LICENSE: https://github.com/PerformanC/internals/blob/fbc73f6368a6971835683f4b22bb4e3b15fa0b73/LICENSE - -import https from 'node:https' -import http from 'node:http' -import crypto from 'node:crypto' -import EventEmitter from 'node:events' -import { URL } from 'node:url' -import { Socket } from 'node:net' - -type ContinueInfoType = { - type: number - buffer: Buffer[] -} - -export type RainlinkWebsocketOptions = { - timeout?: number - headers?: Record -} - -export type RainlinkWebsocketFHInfo = { - opcode: number - fin: boolean - payloadLength: number - mask: Buffer | null - startIndex: number -} - -export enum RainlinkWebsocketState { - WAITING = 'WAITING', - PROCESSING = 'PROCESSING', -} - -function parseFrameHeaderInfo(buffer: Buffer) { - let startIndex = 2 - const opcode = buffer[0] & 15 - const fin = (buffer[0] & 128) === 128 - let payloadLength = buffer[1] & 127 - - let mask = null - if ((buffer[1] & 128) === 128) { - mask = buffer.subarray(startIndex, startIndex + 4) - - startIndex += 4 - } - - if (payloadLength === 126) { - startIndex += 2 - payloadLength = buffer.readUInt16BE(2) - } else if (payloadLength === 127) { - startIndex += 8 - payloadLength = buffer.readUIntBE(4, 8) - } - - return { - opcode, - fin, - payloadLength, - mask, - startIndex, - } -} - -function parseFrameHeader(info: RainlinkWebsocketFHInfo, buffer: Buffer) { - const slicedBuffer = buffer.subarray(info.startIndex, info.startIndex + info.payloadLength) - - if (info.mask) { - for (let i = 0; i < info.payloadLength; i++) { - slicedBuffer[i] = slicedBuffer[i] ^ info.mask[i & 3] - } - } - - return { - opcode: info.opcode, - fin: info.fin, - buffer: slicedBuffer, - payloadLength: info.payloadLength, - rest: buffer.subarray(info.startIndex + info.payloadLength), - } -} - -export class RainlinkWebsocket extends EventEmitter { - protected socket: Socket | null - protected continueInfo: ContinueInfoType - protected state: RainlinkWebsocketState - - /** - * Modded version of PWSL class - * @param url The WS url have to connect - * @param options Some additional options of PWSL - * @instance - */ - constructor( - protected url: string, - protected options: RainlinkWebsocketOptions - ) { - super() - this.socket = null - this.continueInfo = { - type: -1, - buffer: [], - } - this.state = RainlinkWebsocketState.WAITING - - this.connect() - - return this - } - - /** - * Connect to current websocket link - * @instance - */ - public connect() { - const parsedUrl = new URL(this.url) - const isSecure = parsedUrl.protocol === 'wss:' - const agent = isSecure ? https : http - const key = crypto.randomBytes(16).toString('base64') - - const request = agent.request( - (isSecure ? 'https://' : 'http://') + - parsedUrl.hostname + - parsedUrl.pathname + - parsedUrl.search, - { - port: parsedUrl.port || (isSecure ? 443 : 80), - timeout: this.options?.timeout ?? 0, - headers: { - 'Sec-WebSocket-Key': key, - 'Sec-WebSocket-Version': 13, - Upgrade: 'websocket', - Connection: 'Upgrade', - ...(this.options?.headers || {}), - }, - method: 'GET', - } - ) - - request.on('error', (err) => { - this.emit('error', err) - this.emit('close') - - this.cleanup() - }) - - request.on('upgrade', (res, socket, head) => { - socket.setNoDelay() - socket.setKeepAlive(true) - - if (head.length !== 0) socket.unshift(head) - - if (res.headers.upgrade?.toLowerCase() !== 'websocket') { - socket.destroy() - - return - } - - const digest = crypto - .createHash('sha1') - .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11') - .digest('base64') - - if (res.headers['sec-websocket-accept'] !== digest) { - socket.destroy() - - return - } - - socket.once('readable', () => this.checkData()) - - socket.on('close', () => { - this.emit('close') - - this.cleanup() - }) - - socket.on('error', (err) => { - this.emit('error', err) - this.emit('close') - - this.cleanup() - }) - - this.socket = socket - - this.emit('open', socket, res.headers) - }) - - request.end() - } - - protected async checkData() { - const data = this.socket?.read() - - if (data && this.state === 'WAITING') { - this.state = RainlinkWebsocketState.PROCESSING - - await this._processData(data) - - this.state = RainlinkWebsocketState.WAITING - } - - this.socket?.once('readable', () => this.checkData()) - } - - protected async _processData(data: Buffer) { - const info = parseFrameHeaderInfo(data) - const bodyLength = Buffer.byteLength(data) - info.startIndex - - if (info.payloadLength > bodyLength) { - const bytesLeft = info.payloadLength - bodyLength - - const nextData = await new Promise((resolve) => { - this.socket?.once('data', (data) => { - if (data.length > bytesLeft) { - this.socket?.unshift(data.subarray(bytesLeft)) - data = data.subarray(0, bytesLeft) - } - resolve(data) - }) - }) - - data = Buffer.concat([data, nextData as Uint8Array]) - } - - const headers = parseFrameHeader(info, data) - - switch (headers.opcode) { - case 0x0: { - this.continueInfo.buffer.push(headers.buffer) - - if (headers.fin) { - this.emit( - 'message', - this.continueInfo.type === 1 - ? this.continueInfo.buffer.join('') - : Buffer.concat(this.continueInfo.buffer) - ) - - this.continueInfo = { - type: -1, - buffer: [], - } - } - - break - } - case 0x1: - case 0x2: { - if (this.continueInfo.type !== -1 && this.continueInfo.type !== headers.opcode) { - this.close(1002, 'Invalid continuation frame') - this.cleanup() - - return - } - - if (!headers.fin) { - this.continueInfo.type = headers.opcode - this.continueInfo.buffer.push(headers.buffer) - } else { - this.emit( - 'message', - headers.opcode === 0x1 ? headers.buffer.toString('utf8') : headers.buffer - ) - } - - break - } - case 0x8: { - if (headers.buffer.length === 0) { - this.emit('close', 1006, '') - } else { - const code = headers.buffer.readUInt16BE(0) - const reason = headers.buffer.subarray(2).toString('utf-8') - - this.emit('close', code, reason) - } - - this.cleanup() - - break - } - case 0x9: { - const pong = Buffer.allocUnsafe(2) - pong[0] = 0x8a - pong[1] = 0x00 - - this.socket?.write(pong) - - break - } - case 0xa: { - this.emit('pong') - } - // eslint-disable-next-line no-fallthrough - default: { - this.close(1002, 'Invalid opcode') - this.cleanup() - - return - } - } - - if (headers.rest.length > 0) this.socket?.unshift(headers.rest) - } - - /** - * Clean up all current websocket state - * @returns boolean - */ - cleanup() { - if (this.socket) { - this.socket.destroy() - this.socket = null - } - - this.continueInfo = { - type: -1, - buffer: [], - } - - return true - } - - /** - * Send raw buffer data to ws server - * @returns boolean - */ - sendData( - data: Buffer, - options: { len: number; fin?: boolean; opcode: number; mask?: Buffer | boolean } - ) { - let payloadStartIndex = 2 - let payloadLength = options.len - let mask = null - - if (options.mask) { - mask = Buffer.allocUnsafe(4) - - while ((mask[0] | mask[1] | mask[2] | mask[3]) === 0) crypto.randomFillSync(mask, 0, 4) - - payloadStartIndex += 4 - } - - if (options.len >= 65536) { - payloadStartIndex += 8 - payloadLength = 127 - } else if (options.len > 125) { - payloadStartIndex += 2 - payloadLength = 126 - } - - const header = Buffer.allocUnsafe(payloadStartIndex) - header[0] = options.fin ? options.opcode | 128 : options.opcode - header[1] = payloadLength - - if (payloadLength === 126) { - header.writeUInt16BE(options.len, 2) - } else if (payloadLength === 127) { - header.writeUIntBE(options.len, 2, 6) - } - - if (options.mask) { - header[1] |= 128 - header[payloadStartIndex - 4] = mask![0] - header[payloadStartIndex - 3] = mask![1] - header[payloadStartIndex - 2] = mask![2] - header[payloadStartIndex - 1] = mask![3] - - for (let i = 0; i < options.len; i++) { - data[i] = data[i] ^ mask![i & 3] - } - } - - this.socket?.write(Buffer.concat([header, data])) - - return true - } - - /** - * Send string data to ws server - * @returns boolean - */ - public send(data: string): boolean { - const payload = Buffer.from(data, 'utf-8') - return this.sendData(payload, { len: payload.length, fin: true, opcode: 0x01, mask: true }) - } - - /** - * Close the connection of tthe current ws server - * @returns boolean - */ - public close(code?: number, reason?: string) { - const data = Buffer.allocUnsafe(2 + Buffer.byteLength(reason ?? 'normal close')) - data.writeUInt16BE(code ?? 1000) - data.write(reason ?? 'normal close', 2) - - this.sendData(data, { len: data.length, fin: true, opcode: 0x8 }) - - return true - } -} diff --git a/src/rainlink/main.ts b/src/rainlink/main.ts deleted file mode 100644 index 2b59acf3..00000000 --- a/src/rainlink/main.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { metadata } from './metadata.js' -import Library from './Library/index.js' -import Plugin from './Plugin/index.js' - -// Export main class -export * from './Rainlink.js' -// Export player class -export * from './Player/RainlinkPlayer.js' -export * from './Player/RainlinkQueue.js' -export * from './Player/RainlinkTrack.js' -// Export node class -export * from './Node/RainlinkNode.js' -export * from './Node/RainlinkRest.js' -export * from './Node/RainlinkPlayerEvents.js' -// Export manager class -export * from './Manager/RainlinkNodeManager.js' -export * from './Manager/RainlinkPlayerManager.js' -//// Export library class -export * from './Library/AbstractLibrary.js' -export { Library } -//Export interface -export * from './Interface/Connection.js' -export * from './Interface/Constants.js' -export * from './Interface/Manager.js' -export * from './Interface/Node.js' -export * from './Interface/Player.js' -export * from './Interface/Rest.js' -export * from './Interface/Track.js' -// Export plugin -export * from './Plugin/RainlinkPlugin.js' -export * from './Plugin/SourceRainlinkPlugin.js' -export { Plugin } -// Export driver -export * from './Drivers/AbstractDriver.js' -export * from './Drivers/Lavalink3.js' -export * from './Drivers/Lavalink4.js' -export * from './Drivers/Nodelink2.js' -// Export utilities -export * from './Utilities/RainlinkDatabase.js' -export * from './Utilities/RainlinkWebsocket.js' -// Export metadata -export * from './metadata.js' -export const version = metadata.version diff --git a/src/rainlink/metadata.ts b/src/rainlink/metadata.ts deleted file mode 100644 index b55900d2..00000000 --- a/src/rainlink/metadata.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** @ignore */ -export const metadata = { - name: 'rainlink', - version: '1.0.0', - github: 'https://github.com/RainyXeon/ByteBlaze', -} diff --git a/src/services/AutoReconnectBuilderService.ts b/src/services/AutoReconnectBuilderService.ts index d6d853dc..40e57878 100644 --- a/src/services/AutoReconnectBuilderService.ts +++ b/src/services/AutoReconnectBuilderService.ts @@ -1,5 +1,5 @@ import { Manager } from '../manager.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class AutoReconnectBuilderService { client: Manager diff --git a/src/services/ClearMessageService.ts b/src/services/ClearMessageService.ts index eb85b296..ef3a035a 100644 --- a/src/services/ClearMessageService.ts +++ b/src/services/ClearMessageService.ts @@ -1,6 +1,6 @@ import { Manager } from '../manager.js' import { TextChannel } from 'discord.js' -import { RainlinkPlayer } from '../rainlink/main.js' +import { RainlinkPlayer } from 'rainlink' export class ClearMessageService { client: Manager diff --git a/src/structures/ExtendedPlayer.ts b/src/structures/ExtendedPlayer.ts new file mode 100644 index 00000000..fab2a654 --- /dev/null +++ b/src/structures/ExtendedPlayer.ts @@ -0,0 +1,41 @@ +import { RainlinkEvents, RainlinkLoopMode, RainlinkPlayer } from 'rainlink' + +export class ExtendedPlayer extends RainlinkPlayer { + public clear(emitEmpty: boolean): void { + this.loop = RainlinkLoopMode.NONE + this.queue.clear() + this.queue.current = undefined + this.queue.previous.length = 0 + this.volume = this.manager.rainlinkOptions!.options!.defaultVolume ?? 100 + this.paused = true + this.playing = false + this.track = null + if (!this.data.get('sudo-destroy')) this.data.clear() + this.position = 0 + if (emitEmpty) this.manager.emit(RainlinkEvents.QueueEmpty, this, this.queue) + return + } + + public async stop(destroy: boolean) { + this.checkDestroyed() + + if (destroy) { + await this.destroy() + return this + } + + this.clear(false) + + this.node.rest.updatePlayer({ + guildId: this.guildId, + playerOptions: { + track: { + encoded: null, + }, + }, + }) + this.manager.emit(RainlinkEvents.TrackEnd, this, this.queue.current) + this.manager.emit('playerStop' as RainlinkEvents.PlayerDestroy, this) + return this + } +} diff --git a/src/structures/Rainlink.ts b/src/structures/Rainlink.ts index f2b77376..b4f098e5 100644 --- a/src/structures/Rainlink.ts +++ b/src/structures/Rainlink.ts @@ -1,11 +1,11 @@ import { Manager } from '../manager.js' -import { - Library, - Plugin, - Rainlink, - RainlinkAdditionalOptions, - RainlinkPlugin, -} from '../rainlink/main.js' +import { Library, Rainlink, RainlinkAdditionalOptions, RainlinkPlugin } from 'rainlink' +import { SpotifyPlugin } from 'rainlink-spotify' +import { NicoPlugin } from 'rainlink-nico' +import { ApplePlugin } from 'rainlink-apple' +import { DeezerPlugin } from 'rainlink-deezer' +import { RainlinkPlugin as YTConverterPlugin } from './YTConverterPlugin.js' +import { ExtendedPlayer } from './ExtendedPlayer.js' export class RainlinkInit { client: Manager @@ -31,6 +31,9 @@ export class RainlinkInit { retryCount: Infinity, retryTimeout: 3000, defaultSearchEngine: 'youtube', + structures: { + player: ExtendedPlayer, + }, searchFallback: { enable: true, engine: 'youtube', @@ -48,21 +51,21 @@ export class RainlinkInit { get plugins(): RainlinkPlugin[] { const defaultPlugins: RainlinkPlugin[] = [ - new Plugin.Deezer(), - new Plugin.Nico({ searchLimit: 10 }), - new Plugin.Apple({ countryCode: 'us' }), + new DeezerPlugin(), + new NicoPlugin({ searchLimit: 10 }), + new ApplePlugin({ countryCode: 'us' }), ] if (this.client.config.player.AVOID_SUSPEND) defaultPlugins.push( - new Plugin.YoutubeConverter({ + new YTConverterPlugin({ sources: ['scsearch'], }) ) if (this.client.config.player.SPOTIFY.enable) defaultPlugins.push( - new Plugin.Spotify({ + new SpotifyPlugin({ clientId: this.client.config.player.SPOTIFY.id, clientSecret: this.client.config.player.SPOTIFY.secret, playlistPageLimit: 1, diff --git a/src/rainlink/Plugin/YoutubeConverter/Plugin.ts b/src/structures/YTConverterPlugin.ts similarity index 91% rename from src/rainlink/Plugin/YoutubeConverter/Plugin.ts rename to src/structures/YTConverterPlugin.ts index db0c46f4..fe85b299 100644 --- a/src/rainlink/Plugin/YoutubeConverter/Plugin.ts +++ b/src/structures/YTConverterPlugin.ts @@ -1,12 +1,8 @@ -import { RainlinkTrack } from '../../main.js' -import { RainlinkPluginType } from '../../main.js' -import { - RainlinkSearchOptions, - RainlinkSearchResult, - RainlinkSearchResultType, -} from '../../main.js' -import { Rainlink } from '../../main.js' -import { RainlinkPlugin as Plugin } from '../../main.js' +import { RainlinkTrack } from 'rainlink' +import { RainlinkPluginType } from 'rainlink' +import { RainlinkSearchOptions, RainlinkSearchResult, RainlinkSearchResultType } from 'rainlink' +import { Rainlink } from 'rainlink' +import { RainlinkPlugin as Plugin } from 'rainlink' const YOUTUBE_REGEX = [ /^https?:\/\//, diff --git a/src/utilities/GetTitle.ts b/src/utilities/GetTitle.ts index 1a02bc12..363ff110 100644 --- a/src/utilities/GetTitle.ts +++ b/src/utilities/GetTitle.ts @@ -1,5 +1,5 @@ import { Manager } from '../manager.js' -import { RainlinkTrack } from '../rainlink/main.js' +import { RainlinkTrack } from 'rainlink' export function getTitle(client: Manager, track: RainlinkTrack) { if (client.config.player.AVOID_SUSPEND) return track.title diff --git a/src/web/route/getSearch.ts b/src/web/route/getSearch.ts index 54c916aa..5545bcc4 100644 --- a/src/web/route/getSearch.ts +++ b/src/web/route/getSearch.ts @@ -2,7 +2,7 @@ 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' +import { RainlinkSearchResultType } from 'rainlink' export async function getSearch( client: Manager, diff --git a/src/web/route/patchControl.ts b/src/web/route/patchControl.ts index 39864f31..78a289e8 100644 --- a/src/web/route/patchControl.ts +++ b/src/web/route/patchControl.ts @@ -1,7 +1,7 @@ import util from 'node:util' import { Manager } from '../../manager.js' import Fastify from 'fastify' -import { RainlinkLoopMode, RainlinkPlayer } from '../../rainlink/main.js' +import { RainlinkLoopMode, RainlinkPlayer } from 'rainlink' export type TrackRes = { title: string