From 3ade320b1e47e1df7646c0823fd5055e99371e18 Mon Sep 17 00:00:00 2001 From: AlexInCube Date: Wed, 13 Nov 2024 14:50:03 +0200 Subject: [PATCH] 4.0-dev-4 Audioplayer now loads from handler. Fixed requester display in audioplayer messages. Fixed command /playing. --- package.json | 4 +- pnpm-lock.yaml | 79 ++++++++++--------- src/audioplayer/AudioPlayersManager.ts | 72 ++++++++++++----- src/audioplayer/PlayerButtons.ts | 2 +- src/audioplayer/PlayerEmbed.ts | 8 +- src/audioplayer/PlayerInstance.ts | 48 ++++++----- .../AudioPlayerEventOnReady.ts | 7 ++ src/audioplayer/tests/AudioServices.test.ts | 8 +- src/audioplayer/util/downloadSong.ts | 20 ++--- .../util/generateAddedPlaylistMessage.ts | 2 +- .../util/generateAddedSongMessage.ts | 2 +- src/commands/audio/download.command.ts | 6 +- src/commands/audio/jump.command.ts | 6 +- src/commands/audio/pl-add.command.ts | 14 ++-- src/commands/audio/pl-play.command.ts | 45 ++++++----- src/commands/audio/playing.command.ts | 24 +++--- src/events/onReady.event.ts | 3 +- src/handlers/AudioPlayer.handler.ts | 12 +++ src/main.ts | 5 +- src/schemas/SchemaPlaylist.ts | 12 +-- src/schemas/SchemaSongsHistory.ts | 21 +++-- src/utilities/isValidURL.ts | 22 +++--- src/utilities/loginBot.ts | 4 +- src/utilities/pagination/pagination.ts | 5 ++ 24 files changed, 252 insertions(+), 179 deletions(-) create mode 100644 src/audioplayer/discordEventsHandlers/AudioPlayerEventOnReady.ts create mode 100644 src/handlers/AudioPlayer.handler.ts diff --git a/package.json b/package.json index ce3483e..4bc85cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicbot", - "version": "4.0.0-dev-3", + "version": "4.0.0-dev-4", "description": "Discord Bot for playing music", "main": "build/main.js", "scripts": { @@ -31,7 +31,7 @@ "node-os-utils": "1.3.7", "opusscript": "0.1.1", "prism-media": "1.3.5", - "riffy": "1.0.7-rc.2", + "riffy": "https://pkg.pr.new/riffy-team/riffy@bcaaf04", "sodium-native": "4.2.1", "uuid": "10.0.0", "zod": "3.23.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b88f01f..86c27c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -42,8 +42,8 @@ importers: specifier: 1.3.5 version: 1.3.5(opusscript@0.1.1) riffy: - specifier: 1.0.7-rc.2 - version: 1.0.7-rc.2 + specifier: https://pkg.pr.new/riffy-team/riffy@bcaaf04 + version: https://pkg.pr.new/riffy-team/riffy@bcaaf04 sodium-native: specifier: 4.2.1 version: 4.2.1 @@ -138,8 +138,8 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.11.1': - resolution: {integrity: sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==} + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.18.0': @@ -166,12 +166,12 @@ packages: resolution: {integrity: sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@humanfs/core@0.19.0': - resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} - '@humanfs/node@0.16.5': - resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': @@ -303,8 +303,8 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.14.0: + resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} engines: {node: '>=0.4.0'} hasBin: true @@ -445,16 +445,16 @@ packages: eslint-config-prettier: optional: true - eslint-scope@8.1.0: - resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} + eslint-scope@8.2.0: + resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.1.0: - resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} + eslint-visitor-keys@4.2.0: + resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint@9.12.0: @@ -467,8 +467,8 @@ packages: jiti: optional: true - espree@10.2.0: - resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} + espree@10.3.0: + resolution: {integrity: sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: @@ -835,8 +835,9 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - riffy@1.0.7-rc.2: - resolution: {integrity: sha512-FpxnmPn4JSoCGF/dizOLRIsAQXhSnqo9hgRMEFEmSZ7j9691oqPUz4e8EPuDNcTqBoITpbca2JESAZAJaTjKTQ==} + riffy@https://pkg.pr.new/riffy-team/riffy@bcaaf04: + resolution: {tarball: https://pkg.pr.new/riffy-team/riffy@bcaaf04} + version: 1.0.7-rc.2 rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -1101,7 +1102,7 @@ snapshots: eslint: 9.12.0 eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.11.1': {} + '@eslint-community/regexpp@4.12.1': {} '@eslint/config-array@0.18.0': dependencies: @@ -1117,7 +1118,7 @@ snapshots: dependencies: ajv: 6.12.6 debug: 4.3.7 - espree: 10.2.0 + espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -1135,11 +1136,11 @@ snapshots: dependencies: levn: 0.4.1 - '@humanfs/core@0.19.0': {} + '@humanfs/core@0.19.1': {} - '@humanfs/node@0.16.5': + '@humanfs/node@0.16.6': dependencies: - '@humanfs/core': 0.19.0 + '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.3.1 '@humanwhocodes/module-importer@1.0.1': {} @@ -1197,7 +1198,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)': dependencies: - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/regexpp': 4.12.1 '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) '@typescript-eslint/scope-manager': 8.8.1 '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) @@ -1278,11 +1279,11 @@ snapshots: '@vladfrangu/async_event_emitter@2.4.6': {} - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.14.0): dependencies: - acorn: 8.12.1 + acorn: 8.14.0 - acorn@8.12.1: {} + acorn@8.14.0: {} agent-base@7.1.1: dependencies: @@ -1413,25 +1414,25 @@ snapshots: optionalDependencies: eslint-config-prettier: 9.1.0(eslint@9.12.0) - eslint-scope@8.1.0: + eslint-scope@8.2.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.1.0: {} + eslint-visitor-keys@4.2.0: {} eslint@9.12.0: dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) - '@eslint-community/regexpp': 4.11.1 + '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.6.0 '@eslint/eslintrc': 3.1.0 '@eslint/js': 9.12.0 '@eslint/plugin-kit': 0.2.0 - '@humanfs/node': 0.16.5 + '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.3.1 '@types/estree': 1.0.6 @@ -1441,9 +1442,9 @@ snapshots: cross-spawn: 7.0.3 debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.1.0 - eslint-visitor-keys: 4.1.0 - espree: 10.2.0 + eslint-scope: 8.2.0 + eslint-visitor-keys: 4.2.0 + espree: 10.3.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -1462,11 +1463,11 @@ snapshots: transitivePeerDependencies: - supports-color - espree@10.2.0: + espree@10.3.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.1.0 + acorn: 8.14.0 + acorn-jsx: 5.3.2(acorn@8.14.0) + eslint-visitor-keys: 4.2.0 esquery@1.6.0: dependencies: @@ -1795,7 +1796,7 @@ snapshots: reusify@1.0.4: {} - riffy@1.0.7-rc.2: + riffy@https://pkg.pr.new/riffy-team/riffy@bcaaf04: dependencies: jsdom: 24.1.3 undici: 6.19.8 diff --git a/src/audioplayer/AudioPlayersManager.ts b/src/audioplayer/AudioPlayersManager.ts index 994e73b..429b47b 100644 --- a/src/audioplayer/AudioPlayersManager.ts +++ b/src/audioplayer/AudioPlayersManager.ts @@ -21,10 +21,13 @@ import { generateLyricsEmbed } from './Lyrics.js'; import { getGuildOptionLeaveOnEmpty, setGuildOptionLeaveOnEmpty } from '../schemas/SchemaGuild.js'; import { addSongToGuildSongsHistory } from '../schemas/SchemaSongsHistory.js'; import { PaginationList } from './PaginationList.js'; -import { nodeResponse, Player, Queue, Riffy, Track } from 'riffy'; +import { Node, nodeResponse, Player, Queue, Riffy, RiffyEventType, Track } from 'riffy'; import { LavaNodes } from '../LavalinkNodes.js'; import { clamp } from '../utilities/clamp.js'; import { formatMilliseconds } from '../utilities/formatMillisecondsToTime.js'; +import { isValidURL } from '../utilities/isValidURL.js'; +import * as process from 'node:process'; +import * as util from 'node:util'; export const loggerPrefixAudioplayer = `Audioplayer`; @@ -42,7 +45,8 @@ export class AudioPlayersManager { if (guild) guild.shard.send(payload); }, defaultSearchPlatform: 'ytmsearch', - restVersion: 'v4' + restVersion: 'v4', + multipleTrackHistory: true }); this.setupEvents(); @@ -51,7 +55,7 @@ export class AudioPlayersManager { async play( voiceChannel: VoiceBasedChannel, textChannel: TextChannel, - query: string, + query: string | nodeResponse, member: GuildMember ): Promise { try { @@ -64,13 +68,14 @@ export class AudioPlayersManager { const player = this.playersManager.get(textChannel.guild.id); - const resolve: nodeResponse = await this.riffy.resolve({ query, requester: member.id }); + const resolve: nodeResponse | undefined = + typeof query === 'string' ? await this.resolve(query, member.id) : query; + if (!resolve) return; if (resolve.loadType === 'playlist') { if (!resolve.playlistInfo) return; for (const track of resolve.tracks) { - track.info.requester = member; riffyPlayer.queue.add(track); } @@ -105,7 +110,6 @@ export class AudioPlayersManager { } else if (resolve.loadType === 'search' || resolve.loadType === 'track') { const track = resolve.tracks.shift(); if (!track) return; - track.info.requester = member; if (ENV.BOT_MAX_SONGS_HISTORY_SIZE > 0) { //await addSongToGuildSongsHistory(textChannel.guild.id, track); @@ -134,6 +138,24 @@ export class AudioPlayersManager { } } + async resolve(query: string, memberId: string): Promise { + const resolve: nodeResponse = await this.riffy.resolve({ query, requester: memberId }); + + if (resolve.loadType === 'playlist') { + if (!resolve.playlistInfo) return undefined; + + for (const track of resolve.tracks) { + track.info.requester = memberId; + } + } else if (resolve.loadType === 'search' || resolve.loadType === 'track') { + const track = resolve.tracks[0]; + if (!track) return undefined; + track.info.requester = memberId; + } + + return resolve; + } + async stop(guildId: string): Promise { const riffyPlayer = this.riffy.players.get(guildId); if (!riffyPlayer) return; @@ -241,14 +263,16 @@ export class AudioPlayersManager { // TODO: Implement Jump in audioplayer async jump(guild: Guild, position: number): Promise { - const riffyPlayer = this.riffy.players.get(guild.id); - if (!riffyPlayer) return; - try { - const queue = riffyPlayer.queue; - if (queue) { - //return riffyPlayer.seek() jump(guild, clamp(position, 1, queue.songs.length)); - } + const riffyPlayer = this.riffy.players.get(guild.id); + if (!riffyPlayer) return; + const jumpTrack = riffyPlayer.queue.at(position); + riffyPlayer.queue.splice( + 0 /* At Position */, + clamp(position, 1, riffyPlayer.queue.length - 1) /* Tracks to jump */ + ); + riffyPlayer.stop(); + return jumpTrack; } catch (e) { if (ENV.BOT_VERBOSE_LOGGING) loggerError(e); } @@ -351,21 +375,27 @@ export class AudioPlayersManager { } private setupEvents() { - this.riffy.on('nodeConnect', async (node) => { + this.riffy.on(RiffyEventType.NodeConnect, async (node) => { loggerSend(`Node ${node.name} has connected.`, loggerPrefixAudioplayer); }); - this.riffy.on('nodeError', async (node, error) => { - loggerSend(`Node ${node.name} encountered an error: ${error.message}`, loggerPrefixAudioplayer); + this.riffy.on(RiffyEventType.NodeError, async (node, error) => { + // @ts-expect-error When Lavalink node found the error, we have field "code" in class "error" + if (error.code === 'ECONNREFUSED') { + loggerSend(`Node ${node.name} failed to connect: ${error.message}`, loggerPrefixAudioplayer); + process.exit(1); + } else { + loggerSend(`Node ${node.name} encountered an error: ${error.message}`, loggerPrefixAudioplayer); + } }); if (ENV.BOT_VERBOSE_LOGGING) { - this.riffy.on('debug', async (message) => { + this.riffy.on(RiffyEventType.Debug, async (message) => { loggerSend(`Riffy Debug: ${message}`, loggerPrefixAudioplayer); }); } - this.riffy.on('playerCreate', async (riffyPlayer) => { + this.riffy.on(RiffyEventType.PlayerCreate, async (riffyPlayer) => { const guildTextChannel = this.client.channels.cache.get(riffyPlayer.textChannel) as GuildTextBasedChannel; await this.playersManager.add(riffyPlayer.guildId, guildTextChannel, this.riffy); @@ -376,18 +406,18 @@ export class AudioPlayersManager { await player.setLeaveOnEmpty(await getGuildOptionLeaveOnEmpty(riffyPlayer.guildId)); }); - this.riffy.on('playerDisconnect', async (riffyPlayer) => { + this.riffy.on(RiffyEventType.PlayerDisconnect, async (riffyPlayer) => { await this.playersManager.remove(riffyPlayer.guildId); }); - this.riffy.on('trackStart', async (riffyPlayer, track, payload) => { + this.riffy.on(RiffyEventType.TrackStart, async (riffyPlayer, track, payload) => { const player = this.playersManager.get(riffyPlayer.guildId); if (player) { await player.setState('playing'); } }); - this.riffy.on('queueEnd', async (riffyPlayer) => { + this.riffy.on(RiffyEventType.QueueEnd, async (riffyPlayer) => { await this.playersManager.get(riffyPlayer.guildId)?.setState('waiting'); }); } diff --git a/src/audioplayer/PlayerButtons.ts b/src/audioplayer/PlayerButtons.ts index fe04343..c13ca14 100644 --- a/src/audioplayer/PlayerButtons.ts +++ b/src/audioplayer/PlayerButtons.ts @@ -223,7 +223,7 @@ export class PlayerButtons { return; } - await UserPlaylistAddFavoriteSong(ButtonInteraction.user.id, riffyPlayer.queue.first); + await UserPlaylistAddFavoriteSong(ButtonInteraction.user.id, riffyPlayer.queue.first!); await ButtonInteraction.reply({ embeds: [ diff --git a/src/audioplayer/PlayerEmbed.ts b/src/audioplayer/PlayerEmbed.ts index 2817577..7210ce3 100644 --- a/src/audioplayer/PlayerEmbed.ts +++ b/src/audioplayer/PlayerEmbed.ts @@ -9,7 +9,7 @@ import { playlistCalculateDuration } from './util/playlistCalculateDuration.js'; export class PlayerEmbed extends EmbedBuilder { private playerState: AudioPlayerState = 'loading'; - private requester: User | undefined = undefined; + private requester: string | undefined = undefined; private uploader = i18next.t('audioplayer:player_embed_unknown'); private songsCount = 0; private queueDuration = '00:00'; @@ -37,7 +37,7 @@ export class PlayerEmbed extends EmbedBuilder { if (this.requester) { this.addFields({ name: i18next.t('audioplayer:player_embed_requester'), - value: this.requester.toString(), + value: `<@${this.requester}>`, inline: true }); } @@ -123,8 +123,8 @@ export class PlayerEmbed extends EmbedBuilder { } } - setRequester(user: User) { - this.requester = user; + setRequester(userId: string) { + this.requester = userId; } setUploader(uploader: string | undefined) { diff --git a/src/audioplayer/PlayerInstance.ts b/src/audioplayer/PlayerInstance.ts index 19f617d..12f0601 100644 --- a/src/audioplayer/PlayerInstance.ts +++ b/src/audioplayer/PlayerInstance.ts @@ -117,7 +117,7 @@ export class PlayerInstance { this.embedBuilder.setUploader(currentSong.info.author); if (currentSong.info.requester) { - this.embedBuilder.setRequester(currentSong.info.requester!); + this.embedBuilder.setRequester(currentSong.info.requester); } } this.embedBuilder.setNextSong(riffyPlayer.queue.at(1)?.info.title); @@ -161,18 +161,22 @@ export class PlayerInstance { if (!this.messageWithPlayer) return; await this.stopRecreationTimer(); // We stop recreation in recreatePlayer to keep "singleton" for this recreatePlayer this.recreationTimer = setTimeout(async () => { - if (!this.messageWithPlayer) return; - const messages = await this.textChannel.messages.fetch({ limit: 1 }); - const lastMessage = messages.first(); + try { + if (!this.messageWithPlayer) return; + const messages = await this.textChannel.messages.fetch({ limit: 1 }); + const lastMessage = messages.first(); - if (lastMessage?.id !== this.messageWithPlayer.id) { - try { - this.lastDeletedMessage = this.messageWithPlayer; - await this.messageWithPlayer.delete(); - } finally { - this.messageWithPlayer = await this.textChannel.send({ embeds: [this.embedBuilder] }); - await this.updateMessageState(); + if (lastMessage?.id !== this.messageWithPlayer.id) { + try { + this.lastDeletedMessage = this.messageWithPlayer; + await this.messageWithPlayer.delete(); + } finally { + this.messageWithPlayer = await this.textChannel.send({ embeds: [this.embedBuilder] }); + await this.updateMessageState(); + } } + } catch { + /* empty */ } }, this.updateTime); } @@ -229,17 +233,21 @@ export class PlayerInstance { // Changed state of the player and update player message async setState(state: AudioPlayerState) { - this.state = state; - const riffyPlayer: Player = this.riffy.get(this.textChannel.guild.id); - if (!riffyPlayer) return; + try { + this.state = state; + const riffyPlayer: Player = this.riffy.get(this.textChannel.guild.id); + if (!riffyPlayer) return; - if (this.state === 'waiting' && this.leaveOnEmpty) { - await this.startFinishTimer(); - } else if (riffyPlayer.queue.length > 0) { - await this.stopFinishTimer(); - } + if (this.state === 'waiting' && this.leaveOnEmpty) { + await this.startFinishTimer(); + } else if (riffyPlayer.queue.length > 0) { + await this.stopFinishTimer(); + } - await this.update(); + await this.update(); + } catch { + /* empty */ + } } async setLeaveOnEmpty(mode: boolean) { diff --git a/src/audioplayer/discordEventsHandlers/AudioPlayerEventOnReady.ts b/src/audioplayer/discordEventsHandlers/AudioPlayerEventOnReady.ts new file mode 100644 index 0000000..b310ade --- /dev/null +++ b/src/audioplayer/discordEventsHandlers/AudioPlayerEventOnReady.ts @@ -0,0 +1,7 @@ +import { Client } from 'discord.js'; + +export async function AudioPlayerEventOnReady(client: Client) { + if (!client.user) return; + + client.audioPlayer.riffy.init(client.user.id); +} diff --git a/src/audioplayer/tests/AudioServices.test.ts b/src/audioplayer/tests/AudioServices.test.ts index 4cc90d3..0cad99b 100644 --- a/src/audioplayer/tests/AudioServices.test.ts +++ b/src/audioplayer/tests/AudioServices.test.ts @@ -3,13 +3,13 @@ import * as assert from 'node:assert'; import { describe, it, before, after } from 'node:test'; import { Client } from 'discord.js'; -import { LoadPlugins } from '../LoadPlugins.js'; import '../../EnvironmentVariables.js'; import { loggerWarn } from '../../utilities/logger.js'; import * as process from 'node:process'; import { clientIntents } from '../../ClientIntents.js'; +import { Riffy } from 'riffy'; -let distube: DisTube; +let riffy: Riffy; const djsClient: Client = new Client({ intents: clientIntents }); before(async () => { @@ -138,6 +138,4 @@ after(() => { process.exit(0); }, 1000); }); - - - */ +*/ diff --git a/src/audioplayer/util/downloadSong.ts b/src/audioplayer/util/downloadSong.ts index 562877a..8bf3806 100644 --- a/src/audioplayer/util/downloadSong.ts +++ b/src/audioplayer/util/downloadSong.ts @@ -1,5 +1,3 @@ -// TODO: Reimplement song downloading -/* import { AttachmentBuilder, Client } from 'discord.js'; import prism from 'prism-media'; import fs, { createReadStream, ReadStream } from 'fs'; @@ -8,6 +6,7 @@ import { v4 as uuidv4 } from 'uuid'; import { unlink } from 'fs/promises'; import i18next from 'i18next'; import path from 'path'; +import { isValidURL } from '../../utilities/isValidURL.js'; const downloadFolderPath = process.cwd() + '/downloads'; @@ -40,19 +39,21 @@ const maxDownloadSizeMB = maxDownloadSize / 1000000; export async function downloadSong(client: Client, request: string): Promise { let streamUrl: string | undefined = ''; - if (!isURL(request)) { + if (!isValidURL(request)) { throw new DownloadSongError('is_not_url'); } - const song: Song | Playlist = await client.audioPlayer.distube.handler.resolve(request); - if (song instanceof Playlist) { - throw new DownloadSongError('this_is_playlist'); + const nodeResponse = await client.audioPlayer.resolve(request, ''); + + if (!nodeResponse) { + throw new DownloadSongError('not_found'); } - await client.audioPlayer.distube.handler.attachStreamInfo(song); + if (nodeResponse?.playlistInfo) { + throw new DownloadSongError('this_is_playlist'); + } - // @ts-expect-error Url property exists, I know it - streamUrl = song.stream.playFromSource ? song.stream.url : song.stream.song?.stream.url; + streamUrl = nodeResponse.tracks[0].info.uri; if (streamUrl === '' || streamUrl === undefined) { throw new DownloadSongError('not_found'); @@ -109,4 +110,3 @@ export function DownloadSongErrorGetLocale(errorMessage: DownloadSongMessage) { return errorMessage; } -*/ diff --git a/src/audioplayer/util/generateAddedPlaylistMessage.ts b/src/audioplayer/util/generateAddedPlaylistMessage.ts index 225dc45..f88375d 100644 --- a/src/audioplayer/util/generateAddedPlaylistMessage.ts +++ b/src/audioplayer/util/generateAddedPlaylistMessage.ts @@ -22,7 +22,7 @@ export function generateAddedPlaylistMessage(playlist: nodeResponse) { .addFields( { name: `${i18next.t('audioplayer:player_embed_requester')}`, - value: `${playlist.tracks[0].info.requester}`, + value: `<@${playlist.tracks[0].info.requester}>`, inline: true }, { diff --git a/src/audioplayer/util/generateAddedSongMessage.ts b/src/audioplayer/util/generateAddedSongMessage.ts index 7ec0c14..adfc804 100644 --- a/src/audioplayer/util/generateAddedSongMessage.ts +++ b/src/audioplayer/util/generateAddedSongMessage.ts @@ -15,7 +15,7 @@ export function generateAddedSongMessage(song: Track) { .addFields( { name: `${i18next.t('audioplayer:player_embed_requester')}`, - value: `${song.info.requester}`, + value: `<@${song.info.requester}>`, inline: true }, { diff --git a/src/commands/audio/download.command.ts b/src/commands/audio/download.command.ts index b3974a9..9ed064b 100644 --- a/src/commands/audio/download.command.ts +++ b/src/commands/audio/download.command.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { CommandArgument, ICommand } from '../../CommandTypes.js'; import { PermissionsBitField, SlashCommandBuilder, TextChannel } from 'discord.js'; import { GroupAudio } from './AudioTypes.js'; @@ -6,6 +5,11 @@ import { services } from './play.command.js'; import i18next from 'i18next'; import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js'; import { ReadStream } from 'fs'; +import { + deleteMP3file, + DownloadSongErrorGetLocale, + getSongFileAttachment +} from '../../audioplayer/util/downloadSong.js'; export default function (): ICommand { return { diff --git a/src/commands/audio/jump.command.ts b/src/commands/audio/jump.command.ts index ad324a6..164ab64 100644 --- a/src/commands/audio/jump.command.ts +++ b/src/commands/audio/jump.command.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { CommandArgument, ICommand } from '../../CommandTypes.js'; import { EmbedBuilder, GuildMember, PermissionsBitField, SlashCommandBuilder } from 'discord.js'; import { GroupAudio } from './AudioTypes.js'; @@ -9,6 +8,7 @@ import { import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js'; import i18next from 'i18next'; import { generateSimpleEmbed } from '../../utilities/generateSimpleEmbed.js'; +import { Track } from 'riffy'; export default function (): ICommand { return { @@ -76,9 +76,9 @@ export default function (): ICommand { }; } -function generateEmbedAudioPlayerJump(member: GuildMember, song: Song): EmbedBuilder { +function generateEmbedAudioPlayerJump(member: GuildMember, song: Track): EmbedBuilder { return generateSimpleEmbed( - `:fast_forward: ${member} ${i18next.t('commands:jump_success')} ${song.name} :fast_forward:` + `:fast_forward: ${member} ${i18next.t('commands:jump_success')} ${song.info.title} :fast_forward:` ); } diff --git a/src/commands/audio/pl-add.command.ts b/src/commands/audio/pl-add.command.ts index 05bad9b..8b87fe5 100644 --- a/src/commands/audio/pl-add.command.ts +++ b/src/commands/audio/pl-add.command.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { CommandArgument, ICommand, ReplyContext } from '../../CommandTypes.js'; import { GroupAudio } from './AudioTypes.js'; import { Message, PermissionsBitField, SlashCommandBuilder, User } from 'discord.js'; @@ -70,10 +69,7 @@ async function plAddAndReply(playlistName: string, url: string, ctx: ReplyContex return; } - const song = await ctx.client.audioPlayer.distube.handler - .resolve(url) - .then((result) => result) - .catch((err) => loggerError(err)); + const song = await ctx.client.audioPlayer.resolve(url, user.id); if (!song) { await ctx.reply({ @@ -83,7 +79,7 @@ async function plAddAndReply(playlistName: string, url: string, ctx: ReplyContex return; } - if (song instanceof Playlist) { + if (song.playlistInfo) { await ctx.reply({ embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_song_must_not_be_playlist'))], ephemeral: true @@ -91,7 +87,7 @@ async function plAddAndReply(playlistName: string, url: string, ctx: ReplyContex return; } - if (song.isLive) { + if (song.tracks[0].info.stream) { await ctx.reply({ embeds: [generateErrorEmbed(i18next.t('commands:pl-add_error_song_must_not_be_live_stream'))], ephemeral: true @@ -99,13 +95,13 @@ async function plAddAndReply(playlistName: string, url: string, ctx: ReplyContex return; } - await UserPlaylistAddSong(user.id, playlistName, song); + await UserPlaylistAddSong(user.id, playlistName, song.tracks[0]); await ctx.reply({ embeds: [ generateSimpleEmbed( i18next.t('commands:pl-add_success', { - song: song.name, + song: song.tracks[0].info.title, playlist: playlistName, interpolation: { escapeValue: false } }) diff --git a/src/commands/audio/pl-play.command.ts b/src/commands/audio/pl-play.command.ts index 0710c17..11e5b81 100644 --- a/src/commands/audio/pl-play.command.ts +++ b/src/commands/audio/pl-play.command.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { CommandArgument, ICommand, ReplyContext } from '../../CommandTypes.js'; import { GroupAudio } from './AudioTypes.js'; import { @@ -18,6 +17,8 @@ import { ENV } from '../../EnvironmentVariables.js'; import { generateErrorEmbed } from '../../utilities/generateErrorEmbed.js'; import { loggerError } from '../../utilities/logger.js'; import { commandEmptyReply } from '../../utilities/commandEmptyReply.js'; +import { nodeResponse, Track } from 'riffy'; +import { undefined } from 'zod'; export default function (): ICommand { return { @@ -79,16 +80,9 @@ async function plPlayAndReply(ctx: ReplyContext, playlistName: string, userID: s }); return; } - const userPlaylist = await UserPlaylistGet(userID, playlistName, true); - const songs: Array = await Promise.all( - userPlaylist.songs.map(async (userSong) => { - return (await ctx.client.audioPlayer.distube.handler.resolve(userSong.url)) as Song; - }) - ); - - if (songs.length === 0) { + if (userPlaylist.songs.length === 0) { await ctx.reply({ embeds: [ generateErrorEmbed( @@ -103,23 +97,36 @@ async function plPlayAndReply(ctx: ReplyContext, playlistName: string, userID: s return; } + // @ts-expect-error flatMap can return empty array, but TS thinks it never returns + const tracks: Array = await Promise.all( + userPlaylist.songs.flatMap(async (userSong) => { + const nodeResponse = await ctx.client.audioPlayer.resolve(userSong.url, userID); + if (!nodeResponse) return []; + if (nodeResponse.loadType === 'playlist') return []; + return nodeResponse.tracks[0]; + }) + ); + + const finalResponse: nodeResponse = { + exception: null, + loadType: 'playlist', + playlistInfo: { + name: userPlaylist.name, + selectedTrack: 0 + }, + pluginInfo: undefined, + tracks: tracks + }; + await commandEmptyReply(ctx); const member = ctx.member as GuildMember; - const DistubePlaylist = await ctx.client.audioPlayer.distube.createCustomPlaylist(songs, { - member, - name: playlistName - }); - await ctx.client.audioPlayer.play( member.voice.channel as VoiceChannel, ctx.channel as TextChannel, - DistubePlaylist, - { - member, - textChannel: ctx.channel as TextChannel - } + finalResponse, + member ); } catch (e) { if (e instanceof PlaylistIsNotExists) { diff --git a/src/commands/audio/playing.command.ts b/src/commands/audio/playing.command.ts index 925c117..ab13d77 100644 --- a/src/commands/audio/playing.command.ts +++ b/src/commands/audio/playing.command.ts @@ -1,4 +1,3 @@ -// @ts-nocheck import { ICommand } from '../../CommandTypes.js'; import { EmbedBuilder, Guild, Message, PermissionsBitField, SlashCommandBuilder } from 'discord.js'; import { GroupAudio } from './AudioTypes.js'; @@ -8,6 +7,8 @@ import { } from '../../audioplayer/util/AudioCommandWrappers.js'; import { splitBar } from '../../utilities/splitBar.js'; import i18next from 'i18next'; +import { Track } from 'riffy'; +import { formatMilliseconds } from '../../utilities/formatMillisecondsToTime.js'; export default function (): ICommand { return { @@ -42,17 +43,17 @@ export default function (): ICommand { } export function generatePlayingMessage(guild: Guild): EmbedBuilder { - const queue = guild.client.audioPlayer.distube.getQueue(guild); + const riffyPlayer = guild.client.audioPlayer.riffy.get(guild.id); const embed = new EmbedBuilder().setColor('#4F51FF'); - if (queue) { - const song = queue.songs[0]; - embed.setTitle(song.name!); - embed.setURL(song.url!); + if (riffyPlayer) { + const track = riffyPlayer.current; + embed.setTitle(track.info.title); + embed.setURL(track.info.uri); embed.setAuthor({ name: `${i18next.t('commands:playing_now_playing')}:` }); embed.addFields({ name: i18next.t('commands:playing_song_length'), - value: generateTimeline(queue), + value: generateTimeline(track, riffyPlayer.position, track.info.length), inline: true }); } else { @@ -63,14 +64,13 @@ export function generatePlayingMessage(guild: Guild): EmbedBuilder { return embed; } -export function generateTimeline(queue: Queue): string { - const song = queue.songs[0]; +export function generateTimeline(track: Track, currentMs: number, maxMs: number): string { let durationValue: string; - if (song.isLive) { - durationValue = `\`${i18next.t('commands:playing_timeline_stream')} [${queue.formattedCurrentTime}]\``; + if (track.info.stream) { + durationValue = `\`${i18next.t('commands:playing_timeline_stream')}\``; } else { - durationValue = `|${splitBar(song.duration, Math.max(queue.currentTime, 1), 25, undefined, '🔷')[0]}|\n\`[${queue.formattedCurrentTime}/${song.formattedDuration}]\``; + durationValue = `|${splitBar(maxMs, Math.max(currentMs, 1), 25, undefined, '🔷')[0]}|\n\`[${formatMilliseconds(currentMs)}/${formatMilliseconds(maxMs)}]\``; } return durationValue; diff --git a/src/events/onReady.event.ts b/src/events/onReady.event.ts index 27c0ff9..396af40 100644 --- a/src/events/onReady.event.ts +++ b/src/events/onReady.event.ts @@ -1,6 +1,7 @@ import { BotEvent } from '../DiscordTypes.js'; import { loggerSend } from '../utilities/logger.js'; import { Events } from 'discord.js'; +import { AudioPlayerEventOnReady } from '../audioplayer/discordEventsHandlers/AudioPlayerEventOnReady.js'; const event: BotEvent = { name: Events.ClientReady, @@ -8,7 +9,7 @@ const event: BotEvent = { execute: (client) => { if (!client.user) return; - client.audioPlayer.riffy.init(client.user.id); + AudioPlayerEventOnReady(client); loggerSend(`Bot ${client.user.username} is successfully started!`); client.user.setActivity('/help'); diff --git a/src/handlers/AudioPlayer.handler.ts b/src/handlers/AudioPlayer.handler.ts new file mode 100644 index 0000000..96b21e0 --- /dev/null +++ b/src/handlers/AudioPlayer.handler.ts @@ -0,0 +1,12 @@ +import { Client } from 'discord.js'; +import { AudioPlayersManager } from '../audioplayer/AudioPlayersManager.js'; +import { loggerSend } from '../utilities/logger.js'; + +export const loggerPrefixAudioplayerHandler = 'Audioplayer Loader'; + +const handler = async (client: Client) => { + loggerSend('Loading audioplayer', loggerPrefixAudioplayerHandler); + new AudioPlayersManager(client); +}; + +export default handler; diff --git a/src/main.ts b/src/main.ts index e771bcf..d6314bd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,7 +2,6 @@ import { clientIntents } from './ClientIntents.js'; import { Client, Partials } from 'discord.js'; import { loggerError, loggerSend } from './utilities/logger.js'; import { loginBot } from './utilities/loginBot.js'; -import { AudioPlayersManager } from './audioplayer/AudioPlayersManager.js'; import loadLocale from './locales/Locale.js'; import { handlersLoad } from './handlersLoad.js'; @@ -19,11 +18,9 @@ client.rest.on('rateLimited', (args) => { loggerError(`Client encountered a rate limit: ${JSON.stringify(args)}`); }); -new AudioPlayersManager(client); - await handlersLoad(client); -loginBot(client); +await loginBot(client); process.on('uncaughtException', (err) => { loggerError(err); diff --git a/src/schemas/SchemaPlaylist.ts b/src/schemas/SchemaPlaylist.ts index 074c059..f1118dd 100644 --- a/src/schemas/SchemaPlaylist.ts +++ b/src/schemas/SchemaPlaylist.ts @@ -1,9 +1,9 @@ -// @ts-nocheck import { Document, model, Schema } from 'mongoose'; import { ENV } from '../EnvironmentVariables.js'; import { getOrCreateUser } from './SchemaUser.js'; import { ApplicationCommandOptionChoiceData, AutocompleteInteraction } from 'discord.js'; import { getSongsNoun } from '../audioplayer/util/getSongsNoun.js'; +import { Track } from 'riffy'; interface ISchemaSongPlaylistUnit { name: string; @@ -189,7 +189,7 @@ export async function UserPlaylistDelete(userID: string, name: string): Promise< await user.save(); } -export async function UserPlaylistAddSong(userID: string, name: string, song: Song): Promise { +export async function UserPlaylistAddSong(userID: string, name: string, track: Track): Promise { const playlist = await UserPlaylistGet(userID, name, true); if (!playlist) throw new PlaylistIsNotExists(name); @@ -197,7 +197,7 @@ export async function UserPlaylistAddSong(userID: string, name: string, song: So throw new PlaylistMaxSongsLimit(name); } - playlist.songs.push({ name: song.name!, url: song.url! }); + playlist.songs.push({ name: track.info.title, url: track.info.uri }); await playlist.save(); } @@ -236,14 +236,14 @@ export async function UserPlaylistNamesAutocomplete(interaction: AutocompleteInt await interaction.respond(finalResult); } -export async function UserPlaylistAddFavoriteSong(userID: string, song: Song): Promise { +export async function UserPlaylistAddFavoriteSong(userID: string, track: Track): Promise { try { - await UserPlaylistAddSong(userID, 'favorite-songs', song); + await UserPlaylistAddSong(userID, 'favorite-songs', track); } catch (e) { if (e instanceof PlaylistIsNotExists) { await UserPlaylistCreate(userID, 'favorite-songs', true); - await UserPlaylistAddSong(userID, 'favorite-songs', song); + await UserPlaylistAddSong(userID, 'favorite-songs', track); return; } diff --git a/src/schemas/SchemaSongsHistory.ts b/src/schemas/SchemaSongsHistory.ts index 985ce90..ecad17e 100644 --- a/src/schemas/SchemaSongsHistory.ts +++ b/src/schemas/SchemaSongsHistory.ts @@ -1,7 +1,7 @@ -// @ts-nocheck import { Document, model, Schema } from 'mongoose'; import { getOrCreateGuildSettings, GuildModelClass } from './SchemaGuild.js'; import { ENV } from '../EnvironmentVariables.js'; +import { nodeResponse } from 'riffy'; interface ISchemaSongHistoryUnit { name: string; @@ -59,17 +59,24 @@ export async function deleteGuildSongsHistory(guildID: string) { await SongsHistoryListModelClass.deleteOne({ _id: guild.songsHistory }); } -export async function addSongToGuildSongsHistory(guildID: string, resource: Song | Playlist): Promise { +export async function addSongToGuildSongsHistory(guildID: string, resource: nodeResponse): Promise { const history = await getOrCreateGuildSongsHistory(guildID); if (!history) return; - // Users' playlists cannot be added to history, because they don't have url - if (resource.name && resource.member?.id && resource.url) { + if (resource.loadType === 'track') { + if (!resource.tracks[0]) return; history.songsHistory.push({ - name: resource.name ?? 'unknown', - requester: resource.member?.id ?? 'unknown', - url: resource.url + name: resource.tracks[0].info.title ?? 'unknown', + requester: resource.tracks[0].info.requester ?? 'unknown', + url: resource.tracks[0].info.uri + }); + } else if (resource.loadType === 'playlist') { + if (!resource.tracks[0]) return; + history.songsHistory.push({ + name: resource.playlistInfo?.name ?? 'unknown', + requester: resource.tracks[0].info.requester ?? 'unknown', + url: resource.tracks[0].info.uri }); } diff --git a/src/utilities/isValidURL.ts b/src/utilities/isValidURL.ts index 6627ab9..ccfc2b3 100644 --- a/src/utilities/isValidURL.ts +++ b/src/utilities/isValidURL.ts @@ -1,12 +1,12 @@ -export function isValidURL(str: string) { - const pattern = new RegExp( - '^(https?:\\/\\/)?' + // protocol - '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name - '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address - '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path - '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', - 'i' - ); // fragment locator - return pattern.test(str); +const SUPPORTED_PROTOCOL = ['https:', 'http:', 'file:'] as const; + +export function isValidURL(input: any): input is `${(typeof SUPPORTED_PROTOCOL)[number]}//${string}` { + if (typeof input !== 'string' || input.includes(' ')) return false; + try { + const url = new URL(input); + if (!SUPPORTED_PROTOCOL.some((p: string) => p === url.protocol)) return false; + } catch { + return false; + } + return true; } diff --git a/src/utilities/loginBot.ts b/src/utilities/loginBot.ts index 6da82ca..95ca06e 100644 --- a/src/utilities/loginBot.ts +++ b/src/utilities/loginBot.ts @@ -1,6 +1,6 @@ import { ENV } from '../EnvironmentVariables.js'; import { Client } from 'discord.js'; -export function loginBot(client: Client) { - void client.login(ENV.BOT_DISCORD_TOKEN); +export async function loginBot(client: Client) { + void (await client.login(ENV.BOT_DISCORD_TOKEN)); } diff --git a/src/utilities/pagination/pagination.ts b/src/utilities/pagination/pagination.ts index f0068ef..00911c0 100644 --- a/src/utilities/pagination/pagination.ts +++ b/src/utilities/pagination/pagination.ts @@ -1,3 +1,8 @@ +/* +I am grab this code from package the name I forgot, +this code was written in JS without TS in mind, so TS gives different warnings/errors. +Ignore them, because pagination works fine. + */ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import {